您正在查看: 更多 分类下的文章

编程札记(一)

Overview

之前跟Young一起做上一个项目时,遇到和解决了很多小的但是又很常见的编程问题,做完项目想总结一下的时候又发现这是一系列问题,没有太多明确的界限,又好像存在于编程的每一个细节中,我觉得应该统称为编程的基础。
一直说要写,可总觉得无从下笔。现在下决心尝试一下,想到哪里就写到哪里吧~

1. 计算机的结构

现在的计算机依然采用的是冯·诺依曼提出的计算机体系结构。计算机由控制器、运算器、存储器、输入设备、输出设备五部分组成。其中控制器和运算器一起组成了CPU,存储器可以分为内存和硬盘,输入设备就是鼠标键盘麦克风等,输出设备主要是显示器一类的设备。

1.1 内存和硬盘的区别:

写这个的时候我内心是挣扎的,因为可能一眼看起来显得很low,但是这确实是我在接触计算机的过程中遇到的一个困惑,在这里记下来,说不上能解决谁的困惑呢。

简单来说,内存和硬盘都是用来存储东西的,只是由于他们的材质不同,导致他们存储东西的失效和存取效率不同,因此在计算机中的用途也就不同。

内存的主要材质是RAM(Ramdom Access Memory,即随机存取存储器),特点是存储空间小,高速存取,读写时间相等,且与地址无关(即可以随机存取),易挥发性。但是存储信息具有瞬时性,断电之后信息就会完全丢失,这就是为什么台式机忽然断电,你正在编辑的东西如果没保存就全没了,除非你已经保存到了硬盘上。

与RAM容易混淆的是ROM(Read Only Memory,即只读存储器),从它的名字就可以知道,ROM与RAM最大的区别在于ROM是只读的,但是断电之后信息不丢失,因此通常被用来做计算机启动用的BIOS芯片,存取速度相对于RAM来说比较低。
这里说的ROM的只读,并不是说完全不能写,BIOS里的信息不就是写进去的嘛,而且我们经常也会听到刷BOIS,其实就是把BOIS的信息更改,也就是更新ROM中的信息。只是相对于RAM,ROM不适合频繁写而已。

硬盘的特点是存储空间大(现在家用硬盘都是1T+),按址存取,存取速度慢,永久保存数据。在出现固态硬盘前,机械硬盘就是硬盘的代名词,跟RAM可以说是对立的存在。
机械硬盘的工作原理跟CD,唱片机等一样,就是一个盘在转,有一个接触盘的磁头读取接触到的区域的信息。只是机械硬盘是一个立体的结构,里面有很多层类似于盘的面,信息的读取就是靠磁头的移动和磁盘的转动实现。知道了机械硬盘的结构,也就能理解它的特点了,因为磁头要动,盘面要转动,所以机械硬盘的动静很大,发热多,不耐摔(一不小心就把磁头摔坏了)。 速度也很大程度上取决于盘面的旋转速度,盘面旋转速度越快,读取信息自然也就越快,买机械硬盘时,硬盘转速可以说是最重要的指标,5400转/秒(没有更低的了),7200转/秒(主流),12000转/秒(企业级硬盘)。

固态硬盘的存在可以说是内存和机械硬盘的折衷,存储空间没有机械硬盘大,但是可以实现随机读取,性能上相对于机械硬盘有了质的飞越,由于固态硬盘没有了磁盘和磁头这些东西,所以机械硬盘那一系列缺点固态硬盘都没有,一定要说缺点的话,那就是存储空间还不够大,贵。不过从它的缺点来看,它应该是未来的趋势,毕竟有些缺点代表过去,而价格贵空间不够大这些问题,未来多半都能解决,从CPU,硬盘的发展我们已经无数次的看到了这一点。

一句话解释就是,开着计算机,操作系统和其他程序运行时需要的东西都要load到内存中,想要永久保存的东西,都需要从内存中写到硬盘上。

既然所有的程序都是CPU计算,内存只是帮CPU存着结果,那一个程序运行时,CPU和内存以及硬盘又是怎么协作的呢。

1.2 CPU、内存和硬盘

在任何一本计算机组成原理教材中,输入和输出设备都是很简略的,因为真的没有太多需要说的,如果一定要说,我更想解释机械键盘,高清显示屏这些东西对程序员的重要性~

一个程序在运行时(不考虑输入和输出),实际上就是CPU、内存和硬盘的协作过程。 计算机要执行一个程序,需要先把这个程序,程序运行需要的数据等从硬盘导入到内存中,运行结束之后,产生的数据还需要重新写入硬盘永久保存。因此,内存可以看做是CPU和硬盘之间的混缓冲池,CPU是不会直接和硬盘交换数据的,因为硬盘读取的速度太慢了。

但是CPU和内存之间的交换依然存在问题,因为内存这些年的发展也就是变大变大变大,在读写速度上没有什么改进,毕竟材质摆在那里。而CPU的发展日新月异,不只主频越来越高,CPU核数也越来越多,计算能力越来越强,单位时间可以计算的数据量也就越来越大。CPU实际在做运算时,是需要把数据从内存load到CPU中的寄存器中的,随着CPU性能的提升,单位时间在寄存器和内存之间传输的数据量也越来越大,一边算一边取肯定不行,因为CPU就要闲着等数据了。所以CPU在寄存器和内存之间也设置了一个缓冲,那就是CPU缓存。这个缓存是在CPU中的,用来预取内存中的数据,供CPU使用,这样CPU在使用时就不用从内存中获取数据了,而是先从CPU缓存中获取数据。

CPU缓存对CPU性能的影响是至关重要的,CPU读取数据的方式变成了:先去缓存中找,没有命中,再去内存中取,如果缓存中有,就可以直接取到放到寄存器中。如果CPU缓存频繁的不命中,就变成了每次CPU读取数据,都多做了一次无用功,因此CPU缓存的命中率十分重要。不过现在这部分已经相当成熟了,CPU的缓存命中率可以达到95%以上,甚至更高(99%)。

CPU缓存的设计很直观,最早的时候只有一级缓存,CPU在缓存中找不到想要的数据就得去内存了。而且为了保证缓存命中时的速度,一级缓存通常都很小(不然弄个足够大的,程序需要的数据都预取过来,肯定命中啊,但是命中之后取数据的速率会降低),因此命中率不是很高,再后来就有了2级缓存,比一级缓存大,但是存取速度比一级缓存慢一点点,但是肯定比内存存取快得多,这样CPU在一级缓存中找不到的时候,会再去二级缓存找,还找不到的话才会去内存中找。二级可能还不够用,后来CPU又有了三级缓存。当然,也不是越多越好,通常三级缓存就足够了,既保证了命中率,又不至于影响存取速度。
下面是一个主流的Intel CPU的部分配置:

Intel 酷睿i7 4790K:
CPU主频    4GHz
核心数量    四核心 
线程数    八线程
制作工艺    22纳米
三级缓存    8MB 

可以看到,三级缓存也不过8M,跟内存的大小相比,简直可以忽略不计。

跟存取有关的另一个问题就是程序的局部性原理

程序的局部性原理是指程序总是趋向于使用最近使用过的数据和指令,也就是说程序执行时所访问的存储器地址分布不是随机的,而是相对地簇集,这种簇集包括指令和数据两部分。
程序局部性包括程序的时间局部性和程序的空间局部性。
    1. 程序的时间局部性: 是指程序即将用到的信息可能就是目前正在使用的信息。
    2. 程序的空间局部性: 是指程序即将用到的信息可能与目前正在使用的信息在空间上相邻或者临近。

这种局部性很大程度上是由循环语句造成的,在编程中,循环的比例极其高,而循环使得程序具有很好的局部性:程序指令反复执行,数据存储很有规律性。良好的编程规范会极大地提升缓存的使用效率,而缓存能够拥有高命中率的基础也正是程序的局部性原理。

这也是GOTO语句有害性的主要依据,GOTO语句使得程序的执行变得很难提前预测,增加了缓存预测的难度,因此GOTO语句在很多语言中都成了过去。

这里不再展开讲编程对CPU缓存以及内存的影响,有兴趣的话可以自己搜索~

1.3 内存的使用

不同操作系统中对于内存的使用是完全不同的:

  • Windows是按需使用,你打开QQ,操作系统就会把QQ程序和需要的数据等载入内存,你关了QQ,Windows就会把QQ使用的内存释放掉,所以Windows的内存使用量就是实际的使用量,如果你的内存使用比很高,说明你的内存应该升级了。
  • Mac采用不同的内存使用策略,那就是你打开一个程序之后,就算你已经关了这个程序,这个程序实际上依然在内存中,这样当你下次再打开的时候,这个程序响应时间会非常短,就好像你本来就开着一样。实际上它确实一直就开着,常驻内存,只是你不知道而已。
    当然,当开的程序足够多时,内存总是会被占满,这时候Mac系统会用一个内存压缩技术把已经不用的程序压缩,给新程序腾出空间。
    所以,买Mac的话内存配置高一点总是好的,不会浪费。

实际上,无论什么系统,计算机配置多大内存,内存总有占满的时候,那么当内存不够用的时候,操作系统就需要将一部分程序的内存腾出来给新的程序用,这部分内存储存的信息会被写到硬盘上暂存,这块硬盘上的区域有一个专有的名字,叫做交换区(Swap Area)

在linux下,使用free -h命令就可以看到下面的信息:

             total       used       free     shared    buffers     cached
Mem:          992M       909M        82M        14M       133M       216M
-/+ buffers/cache:       560M       432M
Swap:           0B         0B         0B

其中的Swap就是交换区的大小,一般来说Swap used是0,说明内存充足,如果used很大,说明内存不够用,频繁有程序被换出内存。

如果内存不够用了,需要将别的程序占用的内存放到交换区,那么操作系统应该怎么操作呢?
我们举一个极端点的例子,假设程序可用内存为500M,程序A需要使用300M,程序B需要使用250M,那么如果A在运行时,B也运行,系统应该怎么办。

直观的想法就是把A换出去,把B放进来。这样做会有两个问题:一是不管是A在运行,还是B在内存中运行,实际上都是有很多空闲内存未使用的;二是现在的系统都是分式运行的,每个程序都分配时间片,如果A和B同时运行,那么程序会A和B交替运行,这样A和B在内存中换来换去,效率极低,显然是不合理的。

解决的方法就是把内存划分成小份,每个程序拥有很多份内存,就算需要换出去,也只需要换出一部分就可以了,这个思路被称为分页,操作系统会将内存划分成一样大小的小份,每一份被称为一个页面,分页对用户程序是透明的,是一种物理结构的内存划分。这种内存管理方式也叫分页存储管理。但是分页系统也会有别的问题,假设页面大小是4KB,一个程序占用内存10K,那么就需要分配给这个程序3个页面,第3个页面就有2K的空间是没有使用的,也就是有2K的空间是被浪费的,这些被浪费的空间被称为内存碎片,因为碎片是在页面里面的,所以分页产生的内存碎片也叫内碎片

可以看到,页面既不能太大,也不能太小。太小的话一个程序分到的页面会特别多,不方便管理,页表也会很大;太大的话,最后一个页面利用率低,可能产生很多内存碎片,浪费内存空间。

有了页表管理之后,就实现了对内存的细粒度管理,给程序分配内存空间时也不需要连续的内存空间了(因为页表会索引分配的页)。另外,当内存满了之后,将内存交换出去的单位也是页面,使得交换内存的粒度也很小。

当然,还有段式内存管理,现代操作系统大多采用的也都是段页结合,这一就不再详细说明,可以参考 http://blog.csdn.net/wangrunmin/article/details/7967293

1.4 32位系统 or 64位系统

32位和64位的系统都需要配合CPU使用,如果你的CPU是32位的,那么装64位作用也不大。但是现在的CPU一般都支持64位系统。
两者的区别主要有两点,一是寻址能力,一是计算能力。

寻址能力:

  • 32位系统可以寻址的空间是$2^{32}$,即4G的空间,如果你使用了32位的系统,那么最多只可以用4G的内存,更多的内存装上也是无法识别的。
  • 64位系统可以寻址的空间是$2^{64}$,即16TB,当然这只是理论上而已。以Windows 7为例,家庭普通版能支持8GB内存,家庭高级版能支持16GB内存,而64位的Windows7专业版、企业版和旗舰版最高可支持192GB内存。

计算能力:
64位平台上的运行性能要远超过32位平台。原因在与CPU通用寄存器的数据位宽,64位平台是64位,而32位平台是32位,运算时理论上性能会相应提升1倍。实际上,在64位Windows7下运行32位的应用软件并不会让你感觉到性能的飞跃,只有64位的应用软件才能最大化发挥64位平台的优势。但显而易见,目前64位的应用程序在种类的数量上都要远低于32位平台。

将台式机上的NDNSIM1.0(包含实验脚本和转发策略代码)移植到Mac上的问题

Overview

这篇文章是我很久之前移植NDNSIM1.0时的笔记,最早想从台式机的Ubuntu 12.04上移植到别的机器的Ubuntu上,都会出现各种编译问题,一直没能成功,之后想在Mac上做实验,所以下决心把台式机中的NDNSIM 1.0的源代码移植到Mac上,主要遇到了以下问题,做了一下记录,以备以后查看。这个移植应该不只是移植到Mac上的方法,而是适用所有Ubuntu系统的。

g++编译选项-Werror的问题

将源代码编译之后,运行./waf报以下错:

../src/ndnSIM/model/wire/ccnb/ccnb-parser/syntax-tree/block.cc:43:15: error: unused variable 'CCN_MAX_TINY' [-Werror,-Wunused-const-variable]

如下图:

NDNSIM移植mac问题1.png

这个问题经常出现在NDNSIM移植的过程中。主要是由于NS3中编译时,自动加了-Werror选项,该选项会把编译中遇到的警告全部当错误处理,因此会报错。

解决方法:
使用以下编译命令:

CXXFLAGS="-O0 -g" ./waf configure --with-python=/opt/local/bin/python2.7 --enable-examples

代替原来的:

./waf configure --with-python=/opt/local/bin/python2.7 --enable-examples

显式去除-Werror选项即可。

自己编写的模块编译问题

由于我在NS3中,添加了一个叫做ndnPlaneFow的模块,用于做实验,发现编译之后,使用./waf运行,会报以下错误:

ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

详细错误如下图:

NDNSIM移植mac问题2.png

可见,ndnPlaneFow模块里的源文件在链接上找不到ndnSIM模块的依赖文件。
解决方案:
ndnPlaneFow模块的wscript文件的编译依赖中,添加ndnSIM

module = bld.create_ns3_module('ndnPlaneFow', ['internet', 'propagation', 'antenna', 'applications','ndnSIM'])

现在就可以在Mac成功编译和运行了NDNSIM了。

Typecho文章评论时评论者头像引发的性能问题

Overview

最近在阅读博客中的文章时发现一个奇怪的现象,就是有评论的文章加载速度特别慢,代码块的颜色渲染也要等很久才能显示出来。这让我很困惑,因为我将代码块高亮渲染的代码也放在了本地。最初我以为是代码块中的代码太多,导致渲染时间久。后来发现只有有评论的文章才会出现这种情况,使用了Chrome发现,有评论的页面会出现http://www.gravatar.com/avatar/#####链接请求失败。#####是我省略的一串不可读字符串,后面会说这些字符串是什么。
因为http://www.gravatar.com/avatar这个服务不可用,所以导致页面一直在等待这个网址的回复,直到超出响应时间,才会运行接下来的动作,比如渲染代码块高亮颜色

评论者头像提供服务

其实blog在显示评论者图片时,并不是显示本地的图片或者评论者自己设定的图像,而是用用头像提供网站服务给评论者一个特定的头像,想知道具体细节,就需要查看下面的文件。
打开blog/var/Typecho/文件夹中的Common.php文件,找到下面的代码:

$url = $isSecure ? 'https://secure.gravatar.com' : 'http://www.gravatar.com';
$url .= '/avatar/';

这里定义了取头像的地址,即https://secure.gravatar.com/avatar或者https://gravatar.com/avatar

总不能所有的评论者都用一样的图片吧,继续查看紧接着下面的代码:

if (!empty($mail)) {
        $url .= md5(strtolower(trim($mail)));
}

原来Typecho根据评论者提供的邮箱,进行md5转化,形成一串字符串,就可以形成一个新的请求链接:http://www.gravatar.com/avatar/#####,这里的#####就是这个md5编码之后的字符串。但是这个请求不只有邮箱md5之后这一个字符串,还有下面几个参数:

$url .= '?s=' . $size;
$url .= '&r=' . $rating;
$url .= '&d=' . $default;

这三个参数从在后台设置->评论中的选项中读取数据,比如$rating取的数据就是 启用Gravatar头像服务, 最高显示评级为## 的头像,这里的##有一下选项:

G - 普通
PG - 十三岁以上
R - 十七岁以上成人
X - 限制级    

知道了这些以后,我们要做的就很简单了,只需要修改头像服务提供站点就可以了。将Common.php中的

$url = $isSecure ? 'https://secure.gravatar.com' : 'http://www.gravatar.com';
$url .= '/avatar/';

替换为

$url = 'http://cdn.v2ex.com/gravatar/';

我们这里使用的是国内很好的一个程序员社区V2EX提供的CDN服务,速度很快。
如果你想在评论时显示一个自定义头像,去 www.gravatar.com 注册一个账号,并上传一张自己喜欢的头像,就可以了。如果你没设置过,这个网站也会给你返回一个默认头像。
现在评论之后,就可以出现评论者的头像了,而且速度非常快。

怎么找到Common.php

可能有人会好奇,我怎么恰好知道在Common.php里面修改呢。这其实是个通用的方法:

  1. 在我用Chrome查看完页面之后,就知道http://www.gravatar.com的网站服务提供方出现了问题,既然后台没办法设置这个,那一定是在代码中哪个地方设置的。

  2. blog的根目录,使用下面的linux命令查找所有包含gravatar.com的文件:

    grep -nr gravatar.com .
    

    输出下面的结果:

    ./admin/profile.php:14:                <p><a href="http://gravatar.com/emails/" title="<?php _e('在 Gravatar 上修改头像'); ?>"><?php echo '<img class="profile-avatar" src="' . Typecho_Common::gravatarUrl($user->mail, 220, 'X', 'mm', $request->isSecure()) . '" alt="' . $user->screenName . '" />'; ?></a></p>
    ./var/Typecho/Common.php:939:        $url = $isSecure ? 'https://secure.gravatar.com' : 'http://www.gravatar.com';
    ./var/Widget/Options/Discussion.php:54:            'commentsAvatar'        =>  _t('启用 <a href="http://gravatar.com">Gravatar</a> 头像服务, 最高显示评级为 %s 的头像'
    

    可以看到,有三个文件包含了gravatar.com,分别是./admin/profile.php的第14行,./var/Typecho/Common.php的第939行,./var/Widget/Options/Discussion.php的第54行,./表示当前搜索的位置。
    查看这三个文件,会发现profile.phpDiscussion.php对应后台管理界面的两个页面,Common.php就是定义头像服务网址的地方。

小结

这个问题本身非常小,但是给网站本身带来的性能影响是非常大的,而且当管理员登陆后台,进入管理菜单下的评论页面时,居然会直接导致站点宕机,需要重启Apache服务器才可以。因此小的细节对整体的影响还是很大的,自己写代码的时候也要多多注意。

Typecho博客的速度优化

Overview

由于在写笔记的时候会用到公式,因此使用了一个插件MathJax,可以很漂亮显示公式,但是发现用了这个插件之后,有公式显示的页面加载特别慢,使用Chrome查看了链接请求,发现了问题所在:这个插件会请求 cdn.mathjax.com 上的js代码,而 cdn.mathjax.com 的服务器非常不稳定,因此经常需要等待很久,网页才能正常显示。
好在因为Chrome会缓存jscss,图片等信息,因此通常这种情况会发生在用户第一次访问或者定期清理了缓存之后。
为了有更好的用户体验和加载速度,我把插件MathJax插件用到的库放到了我们自己的服务器上。
修改之后,发现还有外链,干脆直接一起给放到服务器上了。

MathJax公式插件

下载MathJax开源库

这里的MathJax库是指插件用到的开源库,可以在github上下载到:https://github.com/mathjax/MathJax

上传到服务器

下载之后解压缩,重命名为MathJax,我放在了blog的一个目录下:blog/usr/uploads/js

修改代码

blog/usr/plugins中找到MathJax.phpMathJax.php就是插件MathJax的所有文件,编辑MathJax.php,找到下面这行代码:

echo '<script type="text/x-mathjax-config">MathJax.Hub.Config({tex2jax: {inlineMath: [[\'$\',\'$\'], [\'\\\\(\',\'\\\\)\']]}});</script><script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>';

可以看到src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML",修改为src="http://####/blog/usr/uploads/js/MathJax/MathJax.js?config=TeX-AMS-MML_HTMLorMML"就可以了。
这里的http://####/blog就是我们博客的地址,可以看得出来,我们修改插件,让他指向了放在服务器上的MathJax库。

现在重新打开网站涉及公式的文章,瞬间响应。

font-awesome

修改了MathJax插件之后,发现还是有外链存在,借助Chrome看了下博客的外链请求,发现下面的外链指向:http://apps.bdimg.com/libs/fontawesome/4.2.0/css/font-awesome.min.css,想了下还是放在自己的服务器上吧,一来是可以加速访问,二来可以防止哪天别人不提供服务了,我们就没办法加载这个css文件了。

下载font-awesome.min.css

直接从http://apps.bdimg.com/libs/fontawesome/4.2.0/css/font-awesome.min.css就可以打开这个css,右键另存就可以了。

上传到服务器

我们把自己保存的font-awesome.min.css上传到blog所在的文件夹下blog/usr/themes/jianshu/css,因为我们使用了jianshu主题,为了方便,我直接放在了jianshucss文件夹中。

修改代码

找到 ,将下面这句代码:

<link rel="stylesheet" href="http://apps.bdimg.com/libs/fontawesome/4.2.0/css/font-awesome.min.css">

修改为

<link rel="stylesheet" href="<?php $this->options->themeUrl('css/font-awesome.min.css'); ?>">

就可以访问到我们服务器上的font-awesome.min.css了。

下载font-awesome资源文件

重新加载网页,发现网站上的小图标都没了!我开始去了解下font-awesome是什么,找到下面的解释:

Font Awesome 是为 Twitter Bootstrap 设计的图标字体。通过Web Font的方式来显示一些图标,好处是图标可以被任意缩放、改变颜色,你需要做的只是像修改文字样式那样修改图标样式。

清楚它的作用之后,再仔细查看font-awesome.min.css文件,发现里面有几处代码,举一例说明:

src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');
src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') 

原来font-awesome.min.css定义的样式中用到的素材库我们没有放在服务器上。
github上下载下来完整的font-awesome库:https://github.com/FortAwesome/Font-Awesome。下载之后解压缩,把其中的font文件夹上传到blog/usr/themes/jianshu文件夹中,跟这个文件夹中的css文件夹一个层级。

现在访问,就可以正常使用我们的网站了。

《德米安:彷徨少年时》- 赫曼·黑塞

“ 辛克莱,当您忽然想到一些疯狂或邪恶的念头,想要把某人给杀了,或者想要犯下罪大恶极的罪行,这时您要想一想,那是阿布拉克萨斯在您内心制造的幻想!您想要杀害的绝不是一个真实的人,肯定只是一个伪装而已。假如我们怨恨一个人,我们恨的是在他形象中的某些东西,这些东西也是我们本身所拥有的。凡是我们本身没有的东西,并不能激动我们的心。”

  今天是中秋节,跟我爸爸聊天时,他顺便转发给了我这段话,是我弟弟发给他的读书截图。看了之后感触很深,读书越多心胸就越开阔,那些原来想不明白的事情也都豁然开朗了。

  记得有人问杨绛先生,大致是自己非常喜欢思考,但又受困于思考的一些人生困惑,杨绛先生的回答简洁明了:「你的问题主要是读书不多而想得太多」

  关于这点,很多有思想的人都曾表达过相同的观点。

吾尝终日而思矣,不如须臾之所学也;吾尝跂而望矣,不如登高之博见也。 --《荀子·劝学篇》

识不足则多虑,威不足则多怒,信不足则多言。 --弘一法师

  很多时候,当你困扰于生活的时候,过分的思考只会让你更加焦虑,去读书吧,读一本书就是与作者进行心灵上的沟通,与大师对话,消解心中的烦恼,提升自己的修养,生活也就自然会少很多苦恼。

  特意去查了本文开头的这句话的出处,赫曼·黑塞《德米安:彷徨少年时》,快速看了下书评和大致内容,我居然不知道有此书的存在,相见恨晚。抽空慢慢读完这本书,补上一篇关于本书的读书笔记。

  感概于自己见识仍旧十分有限,还有太多的优秀的文化和美好的事物去体验,去发现。如果身边的人也有好的书籍或者其他,还请多多推荐。

  以生活之所感去读书,以读书之所得去生活。,谨以此,与各位共勉。