• 用一个月左右的时间读了《自己动手写操作系统》,这是一本让人读着很过瘾,却也特别累的一本书。

    对操作系统的兴趣由来已久,只是一直未能找到入门之径。操作系统教材是个令人生畏的东西,它可以告诉人有什么,却不能告诉人为什么,从那里了解的操作系统有如盲人摸到的象,得到各个部分,却不能拥有整体,加之缺乏实践的支持,理论显得苍白空洞。如Linux般的开源操作系统,虽然可以让人坐拥全部源码,但一来规模庞大,让人不知从何入手,二来源码背后更多的是业务——操作系统和硬件知识,不了解业务的人很难凭一己之力破解源码的奥秘。客观如此,更重要的还是主观的不努力。

    《自己动手写操作系统》则为如我一般挑剔的人打开了一扇门,从一点一滴的小处着手,一步步构建出一个简陋的操作系统雏形——Tinix,虽然它还不具备任何实际的价值,甚至算不上一只五脏俱全的麻雀,但对于想走近操作系统的人来说,这已经足够了,如果能够随着它一路走来,至少可以具备更进一步的基础,再去遨游广阔天地,便不会迷失于庞杂的理论与源码之中。

    这本书的名字给人的提示是,它是一本以实践为基础的书,因此,阅读之初我便给自己定下了步步紧跟的策略。照着书敲代码也难免出错,再者书中有一些遗漏,只有对比光盘中提供的源码才能发现其中的细微之处,所以,常常是一段代码要花相当长的时间进行调试。实践证明,这种方法需要花费相当的精力,这也是我为什么会认为读这本书很累的原因。但是这种做法对于理解书中内容大有裨益。看明白,做一遍,调试,这是几个截然不同的境界。很多细节的东西,只有经过调试才能发现。即便是自己编写的代码,如果没有经过调试,恐怕也难说真正的理解。

    作者在后记中写到,这本书最大的价值在于,它让操作系统的实现这个问题变得具备“可操作性”。正是因为这样,我才可以追随它一步步走来。在这点上,我读到第三章《保护模式》就已经体会颇深了。我曾经读过很多关于保护模式的内容,不过,由于缺乏实验,我只是了解一些概念,却不曾深刻体会。在第三章中,通过一个个简单的小例子,切换至保护模式,设置GDT和LDT,使用分页,中断处理等等书本上的概念活灵活现的展现在我的面前,那些艰涩的概念一下子便得再简单不过了。

    市面上关于源码剖析的书很多,但是,即便像侯捷先生这样教育大家编写的《深入浅出MFC》、《STL源码剖析》大多数情况下也只是讲了怎么做,而无法说出为什么,原因很简单,这些书是站在旁观者的角度看问题,而很多问题只有开发者才是真正理解的。所以,这些源码剖析的书有其价值,但深度上还存在相当的欠缺。这本书的作者恰恰是站在了开发者的角度来讨论问题,所以,我们有机会看到了一个思考的过程,而不仅仅是一个结果。这一点从第六章《进程》中时钟中断处理程序的一步步进化便得以管中窥豹。

    不得不提一下的是作者的写作功力,读过了许多生涩的技术书籍,这本读起来很舒服的书倒显得有些另类。它属于我心目中期盼的那种“形神兼备”的好书,正是作者相当不错的表达,才是这本书让人享受技术的同时又可以体味阅读的乐趣。当然,其中还是有些技术细节让人昏昏欲睡。

    读书,首先要找到适合自己的书,这样我们才能从中有所收获,毕竟,技术书籍中很少能够找到满足所有层次需要的书。《自己动手写操作系统》的定位是一本入门书,显然,它不适合已经过了这个阶段的人,如果因此埋怨这本书档次太低,那就怪不得旁人,因为自己走错了路。

    如果你和我一样,对操作系统有兴趣却不得门径,不妨《自己动手写操作系统》。

  • 2005-11-21

    买书不读

    王垠:再来一个书评:对TAOCP的真正看法

    Knuth的经典《The Art of Computer Programming》,我也有一套,那是去年过生日时,痛下决心送给自己的生日礼物。正如王垠所说,每一个人声称看了他的书,或者买了他的书,不过是作为一种炫耀的资本或者摆设。买这套书时,我估计自己不会深读它们。至于购买的原因,很简单,这是一套在计算机领域享有盛名的经典,作为一个程序员加书虫,我应该拥有一套,生日只不过是给自己一个比较充足的借口。时间证明,我的估计是正确的。这套书进入我的藏书之列后,我只是偶尔拿起来景仰一下,翻上几页然后合上,大多数时间它们只是躺在那里,接受灰尘的洗礼。

    在我的藏书中,像TAOCP这样,从书店来到我这里,却很少看或是压根没看的书不在少数,而且随着时间推移,这样的书数量还在不断的增加。

    开始学习计算机的时候,这也想学,那也想学,所以,买了不少各种不同种类的书。念大学的时候,生活费很少,除了必须要花的饭钱,就没剩几个钱,不过,在买书的问题上,我还是相当舍得,当然,每次掏钱之前,都会经历痛苦的抉择。买《VC技术内幕》的那次,前前后后不知往书店跑了多少趟,才终于下定决心,把它买了回来。

    后来,自己开始挣钱,对计算机的认知也有了一些提升,买书的盲目性有所下降,开始懂得要买好书。每次买书前,必先各大网上书店游历一番,看看先驱们的高谈阔论或是胡说八道,再做定夺。即便这样,我发现“经典”、“必读”居然有这么多,以至于想只买好书,都有一大堆的书进入了候选队列。于是,我依然不知疲惫的为我国的计算机图书事业默默的做着自己的“贡献”。每次回家,老妈都会语重心长的说:“少买点书吧!”,而我诚恳的点着头,实际上却置若罔闻,我行我素。

    我曾对一个朋友说过,按照现在的读书进度,即便从现在开始不买书,我都不愁几年内没有书读。我们这里有家书店了有这样一条标语:买书没有读书难,读书没有消化难。是啊!买了这么多,也确实读了不少,吸收了不少,但与总数比起来,比例实在是可怜。有一段时间,我也考虑过这样的问题,觉得自己做得似乎有些过,于是,大有收手之势。但是Darwin的一个观点给了我重新起步的动力:自己买的书说不准在什么时候就会有作用。说这话时,他刚好在研究编译的知识,而手边的材料竟是几年前购入却始终未读的藏书。后来的一段时间,我逐渐开始理解了这句话:常常是遇到一个问题,忽然想起自己有什么书能够帮到自己,于是兴高采烈的在自己的藏书中进行搜索,不过,其结果往往是“书到用时方恨少”。正是这种感觉,让我得以继续购入自己喜欢的书,乐此不疲。

    虽然TAOCP已经在睡了很长时间,我期待它们发挥作用的那天。

  • 2005-11-16

    乱弹设计

    Tony:tony说设计-实践后的体会

    Tony的这篇blog让我想起了自己两年前第一次真正扛起一个项目架构设计的时候,意气风发,一门心思琢磨如何做一个漂亮的架构。不过,翻遍了整篇blog,我没有找到我心中的设计第一要素:正确性。是的,正确性!

    曾经的我就是一个为技术而技术的人,当一个项目启动的时候,我首先考虑的就是如何展现自己的功力,如何编写优雅的程序,而忽视了根本性的问题:需求。真正让一个软件产生价值的就是需求。少了需求,软件的意义也就荡然无存了。当一个人做事连自己的目标都搞不清楚的时候,不做错事,已经要感谢上天的眷顾了。没有正确的目标,其它的努力只是让自己在未卜之途上越走越远。有许多部影片为我们展示了邪恶科学家的威力,他们不是不努力,只是弄错了方向。

    在设计中,功能性需求的意义要远远超过非功能性需求。非功能性需求多半是为了让自己未来的日子好过一点,少了非功能性的需求,大不了是自己不爽,周遭的同事们不爽,有些糊涂的家伙说不定还会心甘情愿忍受这种折磨。少个功能性的试试,用户不掏钱,老板会给你好看。当然,让自己和用户都满意是我们的最高目标,我们犯不着只顾用户而忽视了自我,再者,功能性的东西可能不会追随我们一生,而非功能性的则可以在日后的日子里让我们不断受益。

    所谓设计,对个人而言,就是一个想明白怎么做的过程。如果只是在自娱自乐,想明白之后去做就是了。但是现在我们多半是与人合作,在这种情况下,设计是一个合同。既然要做一个设计师,我们必须考虑如何为别人(这个别人有时包括了自己)分配任务,这时候,我们就用合同——设计规定了双方的权力和义务,这样,在出错的时候,我们才能根据合同找到对应负责人。由此说来,契约式设计(Design by Contract)就是一件顺理成章的事了。设计就是分工,分工的目的就是为了各司其责,不明确的设计也就意味着责任不能对应到人身上。只要设计上存在盲点,我们就有机会遭遇互相推诿。Grady Booch曾经举过一个狗窝和大厦的例子,其实这就是没规矩和有章法的差异。我们通常所说的设计,多半是这种有规矩的设计,为了与人协作的设计。

    在我看来,谈到设计,最好的设计标准还是那条经典的“高内聚,低耦合”。了解了这么多的设计手法,体会那么多的设计原则,到最后,基本上都能归结到这条标准上来。设计的过程是一个分分合合的过程,先是把系统分拆,再把功能相近的东西合并,这样就形成了一个模块,有人叫服务、子系统、类、组件、函数、方面……,都是一回事。设计者需要考虑的就是让怎么去做这个游戏,以便让各人能够独立工作而不致于相互影响,这就需要请出“高内聚,低耦合”作为衡量标准。模块之间的合同就是我们常说的“接口”,它可能是函数调用,可能是参数传递,可能是共享数据,也可能是远程调用。总之,有了合同好办事,谁的问题谁负责。
       
    Tony有一点说得很好,设计是一种权衡的艺术,只有在不断学习实践中才能培养这种权衡的感觉。不要怕犯错误,只要实现了功能就是对的,设计是一种“只有更好,没有最好”的东西。大多数做设计的人是在为公司服务,这就决定了你想犯大错误都不现实,既然公司为我们提供了这么好的锻炼机会,我们不妨充分利用这些机会,让自己的设计水平不断提升,毕竟本事才是自己的。

    关于设计,胡扯了很多,到此为止吧!

  • 2005-11-10

    探索Antlr

    时过境迁, Antlr 3.0已经发布了,请阅读更新后的版本——《探索Antlr(Antlr 3.0更新版)》。

    简介
    Antlr(ANother Tool for Language Recognition)是一个工具,它为我们构造自己的识别器(recognizers)、编译器(compiler)和转换器(translators)提供了一个基础。通过定义自己的语言规则,Antlr可以为我们生成相应的语言解析器,这样便可以省却了自己全手工打造的劳苦。

    目标
    如同程序设计语言入门大多采用“Hello World”一样,编译领域的入门往往选择计算器。而这里迈出的第一步更为简单:一个只能计算两个数相加的计算器,也就是说,它可以计算“1+1”。

    基础知识
    先来考虑一下如何下手,如果你曾经接受过编译原理的教育,权当忆苦思甜了。这个计算器工作的前提是有一个需要计算的东西,不管我们是以文件的形式提供,还是手工输入,至少我们可以让我们的计算器知道“1+1”的存在。

    有了输入之后,我们要先检查输入的正确性,只有对正确的输入进行计算才是有意义的。如同写文章有形式和内容之分,这里的检查也要细分一下,率先完成的检查当然是面子功夫——形式上的东西,看看是否有错别字的存在,我们要做的是数值相加,结果人家给出了一个字母,这肯定不是我们希望得到的,所以我们有权力拒绝这个不合法的东西。对于程序员来说,如果在自己的程序里写了一个语言不接受的标识符,比如在Java里用“123r”做标识符,那编译器肯定会罢工,拒绝让程序通过编译的。在编译原理里面,这个过程叫做词法分析。在我们的计算器中,我们只接受整数和加号,其它的一概不理。这里我们说的是“整数”,而非“1”、“2”……,对我们来说,它们代表着同一类的东西,编译原理教导我们把这这种东西叫做token,那些数字对我们来说,都是一样的token,不同的仅仅是它们的值而已。

    形式说得过去并不代表内容就可以接受,南北朝时期许多骈体文让我们看到了隐藏在华丽的外表下的空虚灵魂。你可以说“我吃饭”,如果说“饭吃我”,除非是在练习反正话的场合,否则没有人会认为它是有意义的。只有在闯过了词法分析的关口,才能到达这里,在编译原理里面,我们把这个阶段叫做语法分析。如果说词法分析阶段的输入是字符流的话,那么语法分析阶段的输入就是token流——词法分析的输出。我们这里接受的合法语法是“整数 加号 整数”。

    编写语法文件
    好了,制订好自己的语言规则之后,我们需要以Antlr的语言把它描述出来。
    下面便是以Antlr的语言描述的语法:
    class CaculatorParser extends Parser;
    expr: INT PLUS INT;

    class CaculatorLexer extends Lexer;
    PLUS : '+' ;
    INT : ('0'..'9')+ ;

    Antlr的语法文件通常会保存在一个“.g”的文件中,我们的语法文件叫做“caculator.g”。

    先来看看Lexer部分,它便是我们前面所说的词法分析器。首先声明自己的Lexer:
    class CaculatorLexer extends Lexer;
    这句话有两个作用,其一,为生成代码中的词法分析器定义名字,其二,告诉Antlr,我要定义词法规则了。既然说到词法规则,紧接着我们就定义了两条词法规则:
    PLUS : '+' ;
    INT : ('0'..'9')+ ;
    这里的规则很容易看懂:
    * PLUS定义的token,就是一个单一的“+”
    * INT定义的token,由从'0'到'9'之间任意的数字组成,后面的加号表示它是可以重复一次到多次

    定义好Lexer之后,便轮到Parser了:
    class CaculatorParser extends Parser;
    它的作用同Lexer的定义一样,之后是语法规则:
    expr: INT PLUS INT;
    有了词法分析的经验,这条语法规则也很容易看懂,不同于Lexer中规则使用字符定义,这里我们用Lexer中定义的token定义语法。这也符合前面所说的,词法分析的输入是字符,语法分析的输入是token。我们定义了一个表达式:
    * 一个INT,后面跟着一个PLUS,后面在接着一个INT。

    编译语法文件
    如同不编译的程序是无法发挥其威力一样,单单语法文件对我们来说,并没有很大的价值。我们的工作就是使用Antlr提供工具对我们的语法文件进行编译,不同于日常的编译器输出可执行文件,这里的输出是程序语言的源文件。Antlr缺省目标语言是Java语言,它也可以支持C++和C#语言,2.7.5之后,Python也走入这个家族。

    将antlr.jar加到classpath中,然后把语法文件的名称作为参数传给语法编译器:
    java antlr.Tool caculator.g

    在确保命令正确执行,且语法文件编写正确的情况下,Antlr为我们生成了几个文件:
    CaculatorLexer.java
    CaculatorLexerTokenTypes.java
    CaculatorLexerTokenTypes.txt
    CaculatorParser.java
    CaculatorParserTokenTypes.java
    CaculatorParserTokenTypes.txt

    这里我们主要关心的是CaculatorLexer.java和CaculatorParser.java,它们就是我们在语法文件中定义的Lexer和Parser。其它几个文件只是定义了一些常量,让我们暂时忽略它们的存在。

    运行程序
    生成代码之后,就是如何使用这些生成的代码。下面就是我们的主程序,它负责将Lexer和Parser驱动起来:
    public class Main {
    public static void main(String[] args) throws Exception {
    CaculatorLexer lexer = new CaculatorLexer(System.in);
    CaculatorParser parser = new CaculatorParser(lexer);
    try {
    parser.expr();
    } catch (Exception e) {
    System.err.println(e);
    }
    }
    }
    从这段代码中可以清晰的看出,Lexer的输入是一个字符流,而Parser则需要Lexer的协助来完成工作。一切就绪,我们让它跑起来,尝试输入一些内容,看它是否能够通过验证。事实证明,我们的程序可以轻松识别“1+1”,而对于不合法的东西,它会产生一些抱怨。

    计算结果
    还记得我们的目标吗?我们的目标是计算出“1+1”的结果,而现在这个程序刚刚能够识别出“1+1”,我们还要继续前进。

    熟悉XML解析的朋友对于SAX和DOM一定不陌生,二者之间差别在于SAX属于边解析边处理,而DOM则是把所有的内容解析全部解析完(在内存中形成一棵树)之后,再统一处理。Antlr也有与之类似的两种处理方式,SAX的朋友是在Parser中加入处理动作(Action)处理将随着解析的过程进行,而DOM的伙伴则是解析形成一棵抽象语法树(Abstract Syntax Tree,AST),再对树进行处理。

    加入Action
    先来看看SAX的朋友。因为处理动作是加在Parser中的,所以,我们的Lexer保持不变,下面是修改过的Parser。
    class CaculatorParser extends Parser;
    expr returns [int value=0]
    : a : INT PLUS b : INT {
    int aValue = Integer.parseInt(a.getText());
    int bValue = Integer.parseInt(b.getText());
    value = aValue + bValue;
    };
    看到常用的字符串转整数的方法,熟悉Java的朋友想必已经露出了会心的微笑。没错,这里定义Action的方法采用就是Java语言,因为我们生成的目标是Java,如果你期待另辟蹊径,那这里的代码就要用你的目标语言来编写。

    仔细看一下不难发现,action完全是在原有的规则基础上改造的来。首先用returns定义了这个Action的返回值,它将返回value这个变量的值,其类型是int,我们还顺便定义这个变量的初始值——“0”。接下来,我们用a、b拿住了两个token的值,我们前面说过,在检查的过程中,我们并不关心每个token具体的内容,只要token的类型满足需要即可,但在action中,我们要计算结果,那必须使用token具体的内容,所以,我们用变量拿住了token。在生成的代码中,a的类型antlr.Token,因此,我们通过a.getText()来获得token的具体值。剩下的动作就很简单了,把文本转换为数字,进行加法运算。

    是不是对我们的计算器有些迫不及待了,那就挥动工具生成全新的Parser。不过,在新的体验之前,我们还要稍微修改一下主程序,以体现我们的劳动成果。
    public class Main {
    public static void main(String[] args) throws Exception {
    CaculatorLexer lexer = new CaculatorLexer(System.in);
    CaculatorParser parser = new CaculatorParser(lexer);
    try {
    System.out.println(parser.expr());
    } catch (Exception e) {
    System.err.println(e);
    }
    }
    }
    好了,让这个计算器来为我们求证“1+1”吧!

    AST
    SAX的朋友表演完了,下面就是DOM的伙伴登场了。
    建立AST的方式很简单,只要我们Antlr一个建立AST的选项即可,下面就是新的Parser:
    class CaculatorParser extends Parser;

    options {
    buildAST=true;
    }

    expr: INT PLUS^ INT;

    稍微有些不同的地方在PLUS上面的“^”,这个符号用来告诉Antlr创建一个节点,以此作为当前树的根节点。

    你也许会有些疑问,怎么没看到计算的加法的地方?别急,大戏要压轴。下面登场的是Antlr整个故事最后一个大角,TreeParser:
    class CaculatorTreeParser extends TreeParser;

    expr returns [int value = 0;]
    : #(PLUS a : INT b : INT) {
    int aValue = Integer.parseInt(a.getText());
    int bValue = Integer.parseInt(b.getText());
    value = aValue + bValue;
    };
    Antlr可以接受三种类型语法规范——Lexer、Parser和Tree-Parser。如果说Lexer处理的是字符流、Parser处理的是Token流,那么TreeParser处理的则是AST。前面Action的处理方式中,我们看到,规则同处理放到了一起,显得有些混乱,而采用了AST的处理方式,规则同处理就完全分离了:在Parser中定义规则,在TreeParser中定义处理,如果我们需要对同样的语法进行另外的处理,我们只要重新TreeParser,而不必在规则与Action混合的世界中苦苦挣扎。

    有了前面Action的基础,来看TreeParser如何编写也就简单许多,需要说明的就是:
    #(PLUS a : INT b : INT)
    除去变量的说明,简化一下这段代码
    #(PLUS INT INT)
    第一个符号PLUS对应了表示着根节点,两个INT则分别代表了两棵子树。

    再来看看重新打造的主程序
    public class Main {
    public static void main(String[] args) {
    CaculatorLexer lexer = new CaculatorLexer(System.in);
    CaculatorParser parser = new CaculatorParser(lexer);
    try {
    parser.expr();
    AST t = parser.getAST();
    CaculatorTreeParser treeParser = new CaculatorTreeParser();
    System.out.println(treeParser.expr(t));
    } catch (Exception e) {
    System.err.println(e);
    }
    }
    }

    结语
    体验过最简单的Antlr程序,我们就有了让它更为丰富的基础,接下来便是自己动手的时间了。

    参考资料
    《ANTLR入门》 2004年第三期《程序员》
    《ANTLR Reference Manual》

  • 刚刚拜读了开复先生的《做最好的自己》,趁着脑中尚存余温,我写下了下面的文字。

    我是一个书虫,不愿意让任何好书与自己擦肩而过,加之开复先生在我心目中一贯的良师形象,使得《做最好的自己》成了我当然之选。在我看来,一本好书的价值并不仅仅在于阅读过程的片刻愉悦,更是为人打开一扇通往新世界的大门。在这一点上,《做最好的自己》没有辜负我的厚望。

    书中,开复先生提出了“成功同心圆”,即以正确的价值观为核心,辅以积极、同理心、自信、自省、勇气、胸怀六种重要的人生态度作为同心圆的第二层,再以追寻理想、发现兴趣、有效执行、努力学习、人际交流、合作沟六种行为方式通构成同心圆的最外一环。

    这本书大多的观点在开复老师先前的文章中已有阐述,对于拜读过那些文章的我来说,这本书并没有太多全新的观点。但这本书的价值就在于,将所有散落于各处的观点系统化,汇集成册,让人一次领略,就如同转瞬之间遍览名山大川一般,痛快!

    在阅读过程中,我总会情不自禁用自身与之对比,不经意间,便成就了一次心灵的洗礼。对我而言,这是一次很好的自省,让我对自己的优势与不足有了一个相对清醒的认识,也让我得以重视自己曾经忽略的一些东西。课前的预习可以让人更好的理解老师所讲的内容,同样,仅仅被动接受一本书带给你的内容并意味可以深刻的理解。一本好书,不同的人品味,感受差之甚远。工作的几年,我学会了思考,而这本书上的内容恰好是我近一年多时间里思考的问题,许多道理都是付出相当的代价才得以悟出,无意间的预习让我从阅读中得到了更多的震动。虽然感叹开复先生没有早点完成此书,但转念,即便那时我读到此书,没有一些刻骨铭心的东西,对于开复先生的箴言,我不可能有今天这样的体会。

    良师益友对一个人的成长大有助益,但良师益友却可遇不可求,良师尤为如此。可称之为良师的人,大多是在某一领域经验丰富且善于思考者,如此才能高屋建瓴为人指出明路。虽然不遇良师并不妨碍继续前行,但良师的催化作用,可以让人少走许多弯路,加速成长。开复先生便是一位良师,《做最好的自己》算得上这位良师悉心打造的一本好教材,通过它,虽不曾与开复先生谋面,却不妨碍与先生神交,听着先生将一些道理娓娓道来,辅以一个个真实的故事,乐在其中。

    尽信书不如无书,简单的照搬书中建议恐怕也不是开复先生希望看到的,毕竟,每个人都有自己的精彩。再好的一本书也不会让人读罢便脱胎换骨,成就一番大事业,它给人带来更多的是一些可以指导我们未来学习工作生活的原则,和一些对待问题的思考方法。

    作为一名读者,我感谢开复先生带来了《做最好的自己》。