解密:LL与LR解析 1

解密:LL与LR解析

作者:Josh Haberman翻译:杨贵福

由于GFW,我无法联系到作者,所以没有授权,瞎翻译的。原文在这里[http://blog.reverberate.org/2013/07/ll-and-lr-parsing-demystified.html]。

2013年7月22日

我最初解析理论的经历来自大学时自学程序设计语言的时候。当我学到像LL,LR还有它们的变型 (比如Strong-LL, SLR, LALR等等)的时候,我迷惑了。我觉得正注视着的是艰深而强大的咒语,它的重要意义我尚不能领会,但是我确信,总有一天,像"从左至右导出""最右导出"这些术语会融汇贯通,于是我继续努力期待明白的一天。

现在我可以说,经过10年的时间再加上看了一整架解析类的书以后,我把这些算法理解得不错了。但是我看待它们的角度和我看过的文献都非常不同。我更多地从实现的角度,而不是数学的角度,数学的角度也起了一些作用 (杨注:瞎翻译的)。无论如何,我想解释一下我是如何看待这些算法的,希望有人也像我一样觉得这个角度更直观。

这篇文章只涉及到把解析器视为黑盒子这一角度:即解析器的输入/输出,及解析器的限制。后续的文章将打开黑盒子,把这些算法内部工作的更多的细节展示出来。

1. 解析 与 波兰表式法

如果你在大学学习计算机科学,或者甚至你要是有个惠普的计算器 (杨注:我从来没见过逆波兰的HP计算器,而且,空格在那上面如何表示啊?) ,你就见过波兰和逆波兰表示法。它们能不用符号,也不用四则运算顺序规则,就能写出数学运算表达式。我们习惯于把表达式写作中缀形式,在这种形式下,操作符置于操作数二者之间:

1 + 2 * 3

在这种形式下,你如何知道计算的优先级呢?你不得不按约定的规则 (四则混合运算的法则)。你如何想按不同的次邓,就必须用括号了,像这样:

1 (1 + 2) * 3

在波兰和逆波兰表示法中,你不必关心四则运算的优先级,也不必加括号,同样可以避免二义性。这是通过把操作符放在操作数之前(波兰表示法)或之后 (逆波兰表示法)实现的。它们也分别被称为前缀和后缀表示法。

// 第一个例子: 1 + 2 * 3 // 中缀+ 1 * 2 3 // 波兰表示法 (前缀) 1 2 3 * + // 逆波兰表示法 (后缀)
 
// 第二个例子: (1 + 2) * 3 // 中缀* + 1 2 3 // 波兰表示法 (前缀) 1 2 + 3 * // 逆波兰表示法 (后缀)

除了不需要括号,也不需要运算次序的约定以外,波兰和逆波兰表示法在写运算器 (求值)的时候也容易很多 (也许HP计算器的设计师用逆波兰表示法,就是为了能去巴哈马群岛度一周假) 。下面是一个Python实现的逆波兰的简单求值器。

1 # 函数定义了操作符,及如何依据操作符求值
2 # 本例假设操作符都是二值的,不过容易扩展为多值。
3 ops = {
4   "+": (lambda a, b: a + b),
5   "-": (lambda a, b: a - b)
6 }
7  
8 def eval(tokens):
9   stack = []
10  
11   for token in tokens:
12     if token in ops:
13       arg2 = stack.pop()
14       arg1 = stack.pop()
15       result = ops[token](arg1, arg2)
16       stack.append(result)
17     else:
18       stack.append(int(token))
19  
20   return stack.pop()
21  
22 print "Result:",  eval("7 2 3 + -".split())

波兰和逆波兰表示法,确实如通常所说的,需要事先知道所有操作符的参数数量。这里的参数数量,指的是操作符所作用的操作数的数量。这意味着,单值操作符负号和二值操作符减法,是两个不同的操作符。否则,我们在遇到操作符的时候,就不知道从栈中弹出多少个操作数。

一种避免了这个问题的类似表达方法,是Lisp语言的s-表达式。s-表达式 (还有类似的编码形式,比如XML)避免了固定操作符参数个数的需要,实现这一效果的方法是明确标记每个表达式的开始和结束之处。

1 ; Lisp风格的前缀表达式;
2 ; 同一个操作符可以有不同的参数数量
3 (+ 1 2)
4 (+ 1 2 3 4 5)
5
6 ; 我们前两个例子在Lisp中的等价表达方式
7 ; 前缀: + 1 * 2 3
8 (+ 1 (* 2 3))
9
10 ; 前缀: * + 1 2 3
11 (* (+ 1 2) 3)

Lisp这一表达法有不同于前述方法的妥协 (前面的方法中要使用固定数量的参数,Lisp需要括号),但是它们底层的解析/处理算法是非常类似的,因此通常我们把它们视为略有不同的前缀表达式。

看起来我好像有点跑题了,不过,其实我一直在偷偷地讨论LL和LR。按我的观点,LL和LR解析正分别与波兰和逆波兰表示法直接相关。不过为了完整地探索这个想法,我们需要先描述一下我们需要解析器输出什么。

作为一个有趣的练习,请尝试实现一个算法,用于把波兰表达式转化为逆波兰表达式。看看你是否可以不需要先把整个表式式转化为为一棵树;你可以只用一个栈实现这个效果。现在,比如你又要实现相反的过程 (从逆波兰到波兰)--你只需在输入上运行同一个算法,这回转换的方向就相反了。当然,你也可以构造一棵中间的树,但是这导致 O(输入长度) 的空间,而单使用一个栈的解决方案只需要 O(树的深度) 的空间。如何从中缀到后缀呢?有一个非常聪明和高效的算法,称为 调度场算法[http://en.wikipedia.org/wiki/Shunting-yard_algorithm]。

2. 解析器及输出

我们一致认可解析器的输入是token的一个流 (这个流极可能来自一个词法分析器,不过我们可以以后再讨论这一部分)。不过解析器的输出是什么?你可能倾向于说"一棵解析树"。当然你可以用解析器构造出一棵解析树,不过也可能不是这样,而是一种完全不构造解析树的输出。比如,这个Bison的例子[http://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html#Infix-Calc] ,在解析的同时求值了算术表达式。每次当子表达式被识别出来,它立即被求值,直到最终的结果是一个单独的数。从来没有解析树显式地构造出来。

因此,说解析器的输出是一棵解析树不具有足够的一般性。相反地,我断言:解析器的输出,至少我们今天讨论的LL和LR的输出,是解析树的 *遍历*。

如果触动了哪位真理洁癖的神经,我在此道歉。我可以听到有人抗议道,树的遍历是一种算法,是你施加于一棵树上的操作。我怎么能说解析器输出了一棵树的遍历呢?答案在于,请回想一下刚才的波兰和逆波兰表式法。它们通常只是一种数学算式的表示法,不过我们也可以更一般性地把它们视为 对树的遍历的扁平和线性的 (序列化的)编码方式。

回想 下我们的第一个例子 1 + 2 * 3。下面是这个表达式的树形的写法:

    +
   /
  1   *
     /
    2   3

有三种方法遍历这个二叉树,如在维基百科上所给出的:中序遍历 (in-order) ,先序遍历 (pre-order) ,后序遍历 (post-order)。它们的不同只在于你访问父节点的时机,是在访问子节点之前 (先序),之后 (后序),或者左右子树之间(中序)。这三者正与中缀、波兰、逆波兰表示法对应。

1 + 2 * 3 // 中缀表达式,中序遍历+ 1 * 2 3 // 波兰 (前缀)表达式,先序遍历1 2 3 * + // 逆波兰 (后缀)表达式,后序遍历

所以,波兰和逆波兰表示法 完全地编码了一棵树结构,并且规定了你遍历它的步骤。在这些编码方法与一棵实际的解析树之间的主要区别,在于 波兰和逆波兰表示法 编码的访问并非随机的。对于一棵真实的树 (杨注:计算机里的真实,不是现实的真实,哈哈,所谓真实),你可以跟随一个内部节点到它的右子树,或者它的左子树,或者甚至 (对于许多树而言)它的父节点。在这些线性的编码方案中,就没有这种灵活性:你只能采用它已经这样编码了的那种遍历方法。

但是,好的一方面是,它使用解析树的输出是一个流,这个流是在解析行为发生的时候产生的。这也是Bison的那个例子,它如何在没有实现构造一棵树的情况下,就能够求值算术表达式。如果真的需要一棵不是扁平编码的树的话,从线性的树遍历中很容易就能构造出一棵来。不过,当不需要这棵真的树的话,构造它的代价就完全可以避免。

这就引出了关键点:

LL和LR解析器操作之主要不同在于,LL解析器输出解析树的先序遍历,而LR解析器输出后序遍历。

这等价于那些更传统,但是 (按我的观点)更易令人迷惑和不那么直观的关于区别的解释:

* "LL解析器产生一个最左导出,而LR解析器产生一个逆转最右导出。"

* "LL解析器自顶向下把树构造出来,而LR解析器自底向上构造。"

* LL解析器通常称为"带预测的解析器"(杨注:原文predictive parsers,这是不是有约定的翻译啊),而LR解析器称为归约解析器 (杨注:原文shift-reduce )。

今天先翻译到这里,原文后面还有。

昨天CSAPP上的疑问的解答

昨天CSAPP上的疑问的解答

今天整明白了。

CSAPP英文版第2版,826页,或者中文版第2版546页,有这么一段。关于多级页表的。

"But if we had a 32-bit address space, 4KB pages, and a 4-byte

PTE[page table entry, 杨注], then we would need a 4MB page table

resident in memory at all time..."

其中"32-bit address space"的意思是 2^32 bytes,而不是2^32 bits,因为内存是按字节而不是按比特寻址的。

根据公式:页表尺寸 = (地址空间 / 页尺寸) * PTE入口大小........公式1

32-bit address space: 2^32 bytes (昨天误作bits)

4KB pages: 4K bytes

a 4-byte: 4 bytes

B: bytes

K = 2^10

M = 2^22

代入公式1的右侧,得

(2^32 bytes / 4K bytes) * 4 bytes

= 2^32 * 2^2 / (2^2 * 2^10) bytes

= 2^22 bytes

= 2^2 M bytes

= 4MB

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

你最近在读什么书,及CSAPP上的一个疑问

你最近在读什么书,及CSAPP上的一个疑问

"你最近在读什么书?"这句话我在两处看到过。一处是鲁迅先生关怀青少年成长,问某个孩子的,然后向他推荐了《表》这样的道德教育类少儿读物;另一处是贾宝玉问林黛玉,或者反过来,或者是薛宝钗问林黛玉,又或者是贾政问贾宝玉,也许是关心,也许是调情,也许是暗藏机锋,《红楼梦》里这种比生活还复杂的对话,我总也不敢说看懂了。反正有此一问。

最近在douban上看到某位地下书店店员的回忆,写得颇有大家风范,我深以为这是文艺工作者在体验生活之后交的报告。此地下书店中的地下,不是隐藏非常深而不得见的意思,而是真的就在地底下,可能是地铁,也可能是防空洞,我没注意。作者提到很多有意思的小细节,比如证明读者们都如何无知而装作有见识,讲到读者寻求推荐的时候问"最近得诺贝尔奖那个,他的书有没?",似乎并不知道"莫言"。生活在观察之中,作为店员生活得颇有情趣。他提到两件事很有趣,一个是午休的时候挤出时间看溜冰,溜冰的人像水里游泳的鱼,另一个是提到很多明星去买书,他们都买实用型的

(例子可能是,如何治疗拖延症?),只有周迅是个例外,她只买世界名著。

有人可能撇嘴了,"她们都是装的。"一则,人不能处处假装,比如在素不相识的书店店员面前,周迅如何得知这是位文学大家在卧底;二则,又如果周迅只钟看看微博传八卦,喜欢不超过三百字的贴子,但是她只买世界名著,这又不是她所钟爱的,那么她的生活得多么得痛苦啊。

有同学可能说,看看微博怎么了?看微博当然没什么,不过这语气总归有点像李某某的律师的语气,所有回答都是"这是俺们的合法权利"。看微博,就像有人喜欢吃方便面,喜欢吃方便粉丝,喜欢吃咸菜,总也不吃还非常想得慌。你可能会说,微博就方便面,世界名著就满汉全席。没错,我非常明确地认为,作品确实是分三六九等的,无论是思想境界,抽象的高度,还是什么。微博这样短小的文字,甚至包括博客这种篇幅,要是能让一个人进步,或者如某些人回忆到的看了读者知音故事会心灵鸡汤以后提着喷壶灌顶一般顿悟,那都怪了。

我们读那些短小的东西,只是为我们的观点、既有的知识结构找些佐证。就像小女孩左看看右看看,啊,原来大家都跟我一样胖,恩,放心了。这种感觉。颠覆我们观点的,让我们从另一个角度看自己,甚至否定自己的--这就是所谓成长--只能是足够厚的作品。或者偶尔,会有非常薄的作品起到这样的作用,比如离散数学,比如纯粹理性批判,比如爱因斯坦讲相对论的原始那张贴子,不过,无一例外,这种短贴子,非常之难看懂,你得花比看大部头还长的时间去读。

读短东西,我们不过是在自言自语,连反刍都算不上。

我固执地认为,程序员仍然是先进生产力的代表,一个重要原因是,他们在工作以后仍然保持读书。他们终日阅读,读的东西还都够长。

也许他们读的文字只是库函数手册,又或者需求书,但是他们在与别人进行相当精确和深入的交流。有些人也读了,但是只流于表面,用于证明关系,"啊,我也这么想,你看我们的关系多么铁",或者"我完全同意你说的,但是",或者双方各说各话,互无交集。程序员要精确地找出他反对的部分,然后与作者争辩,或者做实验,或者吵得面红耳赤。因为如果双方的观点完全相同,就连交流的必要都没有。这和我们变态的现实生活如此不同,在现实世界,往往观点相同,我们就要一万次确认,观点不同,我们甚至不再说一句话。在现实世界,我们竟然往往只与自己对话,这多么孤独和变态。还是做个程序员更正常。

他们所阅读的,并非自己或自己的同类,那些完全赞同自己观点的人,所产出的东西。不是有些人说,某某类的作品,口戚,我从来不看。以此给自己贴个值钱的标签,供人给些好的品评。毛泽东同志在瑞金的时候,据说曾有人说,你用孙子兵法指导革命战争,如何如何。毛同志说,你知道孙子兵法里面讲些什么,你知道孙子兵法一共有几章?不是有人只读我们自己的传统,不是有人只读非我们自己的传统的东西么。

同时,他们所阅读的,并非完全为了追求阅读当时的的和短暂的快乐,像有些文艺青年那样。他们的阅读是漫长而痛苦的,他们阅读的原因之一是为了追求阅读之后达成的共识,或者挖掘出的差异。这些共识和差异,可能是人与人的,也可能是我们的假想与真实世界之间的。所以,他们不只挑那些读起来爽的,听起来的好的。

这让我想起以前提到过的,小资们艳羡,李安有段时期由老婆供养,成天就是看片儿。看片儿之于李安,与小资们想像的是不同的,我相信,他一定不会只挑自己喜欢的看,还喝着汽水嗑着瓜子,他一定是把那些烦得不行的片子也要看了,看到吐,他一定是把自己喜欢得不行的片子也看了,品评分析很多遍,直到把骨头和肉和神经完全分开了,再也不是最初感情上肤浅地喜欢的那种。

程序员就是这样阅读的。而且,他们一直保持。

还有一些人,从成年 (成熟?腐烂?)开始,就再也不阅读,再也不阅读自己不"喜欢"的,再也不阅读长的,再也不阅读自己不认同或者不认同自己的,再也不阅读对业务

(比如宫斗或者宫斗)有用的任何书籍。他们少年时,读得最多的是考试辅导材料。他们成年时,读得最多的是他们的孩子的考试辅导材料。有些人,即使在工业社会,即使在后工业时代,仍然保持着光荣的传统的小农意识,除了圣经或者语录基本没有读过别的,除了家乡

(不管出生在多么大的城市)再没见过更广阔的土地。如此纯洁,不沾纤尘。

你最近阅读的,不是为了直接的短期见效的功利的动机,超过500页的作品,是什

么?

-----

附记:

今天读CSAPP,第9章。有一处卡住了一个小时,复核书上计算的结果怎么也不对。后来拿着书睡着了。拿着书睡着这件事,一点也不浪漫,它让我意识到衰老,也让我意识到没有读到什么好玩的东西,时光虚度。

CSAPP英文版第2版,826页,或者中文版第2版546页,有这么一段。关于多级页表的。

"But if we had a 32-bit address space, 4KB pages, and a 4-byte

PTE[page table entry, 杨注], then we would need a 4MB page table

resident in memory at all time..."

就这么一段,成了拦路虎。我跳过它,读了后面大半小节,没什么障碍。我是这

么翻译和理解的:

但是如果我们有一个32位的地址空间,每页长度4K字节,PTE每个为4字节,那么,我们将需要4M字节的页表一直驻留在内存中...

以上翻译,有几处需要解释。1.K是1024,不是1000,M是1024*1024,不是1000*1000。2. 4KB

pages,第一篇时我读错了,以为是4*1024页,又重读和读后面,发现作者习惯于这样措辞,其实是每页4KB这么长,即 "splitted

into pages of 4KB page size"。

然后,我按上述理解算了一个小时,没算明白。对不上。

其实这个公式很简单,我甚至google了一下以确认

页表尺寸 = (地址空间 / 页尺寸) * PTE入口大小........公式1

你按我上面的翻译手算一下左边和右边就知道了,不等。为什么不等呢?因为4KB中的B,还有4MB中的B,根本就不是"字节" byte,而是"比特" bit。

网络 (和中文?)中,习惯用 B 表示 字节,b表示 比特。但是CSAPP这段并没认可这个约定。

32-bit address space: 2^32 bit

4KB pages: 4K bytes

a 4-byte: 4 bytes

K = 2^10

M = 2^22

代入公式1的右侧,得

(2^32 bits / 4K bytes) * 4 bytes

= 2^32 * 2^2 / (2^2 * 2^10) bits

= 2^22 bits

= 2^2 M bits

= 4Mb

CSAPP原文和翻译均写作: 4MB

我是不是哪儿算错了啊,各位大师看出来的烦请告诉我。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

除了管道和重定向,还有命令行参数

除了管道和重定向,还有命令行参数

管道和重定向,在Unix Shell

Script入门教程里都要提到。在稍微深入一点的dos/windows批处理教程里也要提到。管道,一般在用前一个进程产生的输出,作为后一个进程的输入;重定向,一般用在把输出由控制台转到文件,或者用文件替代控制台输入。管道和重定向满足了进程的输入和输出的转向。

输入和输出,似乎本来就是进程跑起来以后唯一与外界的联系。根据与外界的联系判定那是个什么东西,而不是根据它的内心,这据说是萨特的观点,小资们不可不知。

不过,输入和输出,并非进程与外界唯一的联系。除了输入以外,命令行参数,也是设定进程行为的重要依据。在这一点上,如果把进程当成一个对象,命令行参数有点像构造函数的参数,虽然跑起来以后就跟它无关了。而且,unix程序一般地,只有命令行参数才是命令,影响进程的行为,而以后从标准输入读进来的所有东西,都只是数据而已,是被进程处理的东西,对进程本身没有影响。

shell (像python一样),是一种不错的粘合剂,它能组装一系的进程,让它们协同工作。管道和重定向,能让它们完成生产者-消费者这样的组合。

不过,如果我们需要一个进程的输出,不是作为另一个进程的输入,而是作为它的命令行参数呢?

下面的写法,

a | b

完成的,是把a的输出作为b的输入。

而如果我们希望下面这样,如何表达?

b -para <此处是a的输出>

有不止一种方法。

1. backtick

backtick,就是"`"。这不是单引号,而是键盘左上角,波浪线 (~)下面的那个小点,像个反向的单引号。

它的作用是,把括起来的命令执行了,并用执行结果中的每一行去替换命令所在的位置。

ls `echo -l`

相当于

ls -l

我们把其中的一段拿出来单独执行一下。

1 $ echo -l

2 -l

可见,"echo -l"的结果,就是"-l"。其中的""中为了避免echo把"-l"当成自己

的参数。

而"-l"替换了"ls `echo -l`"中反引号括起来的部分,所以"ls `echo -l`"就

变成了"ls -l"。

再举个例子。

1 $ grep -w main `find . *c`

2 ./samples/hidraw/hid-example.c:int main(int argc, char **argv)

3 ./samples/trace_events/Makefile:# have that tracer file in its main

search path. This is because

4 ./Makefile:# $(vmlinux-main). Most are built-in.o files from

top-level directories

5 ./Makefile:# +--< $(vmlinux-main)

6 ./Makefile:vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)

7 ./Makefile:vmlinux-all := $(vmlinux-init) $(vmlinux-main)

8 ...

这条命令的作用是,先执行 "find . *c",找到当前目录下所有子目录中的 c文件,然后对每个符合条件的文件执行 "grep -w main"。

命令"grep -w main `find . *c`"中,`find . *c`部分将被替换为很多个匹配的文件名,然后每个文件名都作为

"grep -w main"的命令行参数,类似于:

grep -w main a.c

grep -w main b.c

grep -w main c.c

这样。

2. $(cmd)

backtick倒是挺方便,不过很多同学反映,"`"这个符号太丑陋了。最丑陋的特性之一就是,它跟单引号"'"长得太过地相似了。像我这样认人脸有困难的,本来就希望大家服饰发型更多样化一些,所以你能够理解我现在看到女演员们长得都越来越像,该是多么大的困惑。我指的不光是韩国的,小锥子脸大眼睛面孔白而无表情。backtick也存在这样的问题,跟别人像,本来就是毛病。

所以有别的写法,比如这样:

grep -w main $(find . *c)

这样的效果跟backtick是一样的。一样一样的。区别呢?多少也有一些。比如 $(cmd) 这样写法是可以嵌套的。

3. xargs

不过上述这些需要"代换"的,似乎有些程序员理解起来有困难?后来出现了一个命令,xargs,专门用来做这件事。

find . *c | xargs grep -w main {}

请注意到find和grep的顺序换了。find的输出结果,由xargs转给grep作为命令行参数。事实上,是xargs调用了grep,管道的末端不是grep,而是xargs。这不同于管道的末端是grep,如果是这样,就成了find的输出作为grep的输入,是管道应用的更一般的情况。args把管道的输出转向了grep的命令行参数,而不是作为grep的输入。

"{}"在这里,将被find的输出替代。如果命令行参数的位置不重要,或者在最末端,那么还可以把"{}"省略掉,效果是一样的。像这样:

find . *c | xargs grep -w main

4. -exec

似乎有了xargs以后,还是有些程序员觉得不够简单?又有些命令自带了"-exec"参数,意谓:如果匹配了,就把那些输出作为"-exec后面的进程的命令行参数"。话说,有些事情由于它的领域模型就那么复杂,是不太可能因为表达方式而变得简单的。

以下命令,效果是一样的:

find . *c -exec grep -w main {} -nH ;

行尾的";",是为了标识"-exec"部分结束,其中的""是因为";"是个特殊字符。grep需要"-nH"参数,是因为不然输出的匹配行里没有文件名和行号。grep在此前的几个命令里用的是别名,这里是真正的grep本身。

5. 总结

以下几个命令等价:

$ cat `which 20.sh`

$ cat ${which 20.sh}

$ which 20.sh | xargs cat

cat不支持"-exec"参数,那是find这样的复杂命令的特色,因此以上总结中未包括。

----------

本文的命令在 emacs eshell 不好使,因为 eshell 对管道支持不充分,shell-mode则没有问题。

--------------------

博客会手工同步到以下地址:

----

杨贵福

东北师范大学 计算机科学与信息技术学院

--

Sincerely,

YANG Guifu

School of Computer Science and Information Technology

Northeast Normal University

Changchun, P.R.China

重剑无锋,大巧不工。

无不大工。

如何永生

如何永生

本文介绍通过 守护进程 或 at提交作业

监控某个进程保持运行,本案例中被监控的进程是amule电驴。守护进程方案是李记者提出的,用一个进程看着amule没死,也是李记者提出的。

1. 问题,amule不知道什么时候退出了

李记者,记者二字并不是他的职业,而是名字。这名字是我起的。因为常需要他在故事里扮演角色,而他的本名"粲"字估计很多读者都不太认识,所以就叫李记者。有出租车司机很谨慎地问,"你是记者啊",我哈哈大笑,"他的名字就叫记者。"

李记者来看我,带个黑色的大塑料袋,装垃圾那种,吭哧吭哧扒开,露出里面两大瓶黑可乐。坐定,照例是"你近来一向可好。"

他说,他刚刚休假了一周,天天躺在床上。他说,"我也腰间盘突出了。"有那么一瞬间,我觉得眼泪在眼圈里,腰间盘突出的种种从眼前一闪而过。

我拍腿大笑,哈哈,你也有今天啊,老了吧。然后开始东扯西扯,我们认识的各位。中间准备酒的时候,我看看了机器,抱怨,"电驴不定什么时候就退出去了,真是烦人呐。"

关键就是,你不知道它什么时候死掉了。所以,得隔一会看一眼,看着它干活。可能你忙了一阵,或者开了一天机,回家一看,它死了--而且不知道什么时候死的。

2. 永生之法

李记者说,开个进程,看 (读作一声) 着amule,死了就再启动一次。你才老了呢。

我叹气,看来我确实是老了啊。

这是个标准方案,标准到连病毒和流氓软件都用它。它们一般都用这样的方案保证永生:开几个进程,一个是驱动,一个是应用程序,一个是服务,它们之间互相监视大家的存在,如果发现哪一个挂了,就再把它启动起来。

要求24X7服务的那种应用,一般也需要这样的机制,比如HA (High Ability)

。同时开两台以上的机器,其中一台机器对外提供服务,并向其他的机器发送心跳信号,意思就是我还活着。一旦备用的机器发现主服务器宕掉了,立马启动自己的服务。从用户的角度上看,根本不知道中间还换了个服务生。

这么长时间以来电驴莫名退出困扰着我,我居然没有想到这个标准方案。

3. 守护进程

它的基本机制是轮询,隔一会看看服务还在不在了。像电驴这样的任务,轮询时间2分钟左右就行,反正丢失2分钟也不是很重要。这一点,我和李记者很快达成一致意见。但是,对于使用什么方式轮询,我们各执一端。

他认为,应该开个脚本,作为守护进程,始终在后台。我不同意,觉得这不够酷。李记者说这是标准方案,我说那也白扯,这不够酷。我的意见一会儿再说,他的方案就像下面这样:

$ cat amule_monitor.sh

1 #!/bin/bash

2 amule_status=''

3 while true; do

4 amule_status=$(ps aux | grep -w amule | wc -l)

5 if [ $amule_status = '2' ]; then

6 echo 'amule running.' | tee /var/log/amule_monitor.log

7 else

8 echo 'amule starting.' | tee /var/log/amule_monitor.log

9 amule 2> /dev/null 1> /dev/null &

10 fi

11 sleep 60;

12 done

第1行是指定用哪个程序解释下面的东西。对于熟悉perl,python的同学,这种写法应不陌生。

第2行的 amule_status 是个变量,用来记录 amule 到底是否活着。

第3行 和 第12 之间,是个循环,死循环,没有跳出条件。说起来这是件好玩的事,要想永"生",就得使用"死"循环。

第4行,取得 amule 的状态。状态是这样取得的 $(ps aux | grep -w amule | wc -l),加上$ ()

以后,里面的命令执行的结果就成了个变量。其中,管道分隔的前面两段,执行结果类似于这样:

$ ps aux | grep -w amule

young 5488 14.3 2.4 204256 47632 ? Sl 20:51 2:25 amule

young 5730 0.0 0.0 3324 824 pts/0 S+ 21:08 0:00 grep --color=auto -w amule

其中后一行,是grep本身,前一行,就表明amule正执行。

"wc -l" 是对"ps aux | grep -w amule

"执行的结果统计一下行数。显然,如果一行,那就是amule挂掉了,两行,那就是amule还健在。

在循环之中判断条件,然后有选择地做一些事情--很多算法的核心思想都是这个路子。

现在,我们得到了用于判断的变量 amule_status,接着,我们就要判断了。上面那段代码的判断部分如下:

5 if [ $amule_status = '2' ]; then

6 echo 'amule running.' | tee /var/log/amule_monitor.log

7 else

8 echo 'amule starting.' | tee /var/log/amule_monitor.log

9 amule 2> /dev/null 1> /dev/null &

10 fi

11 sleep 60;

第5行,如果两行的话,那么,第6行,写个日志,说"健在着呢";第7行,否则的话,第8行,日志,"启动一次",然后在第9行启动一次。

其中有些奇怪的符号,都具有特殊的含义。"[];"是条件;"2>"是错误重写向;"1>"是标准输出重定向。fi就是if结束的地方。

第11行,睡60秒。

睡60秒以后呢,就又回到了上面提到的死循环之中。再检查一次amule是否运行着呢,如果没有,启运一次。

这跟你父母看着你学习看书练琴背英语是一个路子:每隔一分钟,露个头,看到你学习着,就说,"啊,我给你拿点水果进来",如果没学习,就咆哮体。这种行为甚是普遍,所以名之为

monitor。也用来称呼班长--替老师监控同学们的人。

当然,小资同学们不会明白,能拥有你自己的屋子,就表明你是生在红旗下长在蜜罐里身在福中不知福。

运行上述程序的方法是:

$ ./amule_monitor.sh &

用"&"代表要求它后台运行,少吱声。然后amule就启运了。因为循环第一圈的时候,发现amule没有启运。然后2分钟之后,日志变成了"amule

running"。不是增加,tee把日志覆盖了。如果增量的话,按李记者的说法,没有时间戳,这样的日志没啥意义,而且,一个多月以后得多少记录啊。这时,我们可以把amule杀了,退出,2分钟以内,monitor就又循环回来,并觉查到amule死了,所以再启动一次amule。

这样,amule就永生了。所以,永生的方法不是永远不被打倒,而是被打倒了以后,马上就爬起来。

3. at,作业

3.1 另一个脚本

虽然amule永生了,但是如上面提到的,我觉得这个方法不酷。我要用at作业的方法实现。

这个版本是 amule_at.sh,在每一次 shell script 执行结束前,执行at,要求2分钟后再启动一次这个 shell script,这样:

1 at now + 2 minutes 2>/dev/null <<EOF

2 amule_at.sh

3 EOF

然后,我们失败了好几次。李记者说,"你老啦,应该跟踪日志输出。"跟踪日志表明,脚本确实执行了很多次,amule活着与否也判断准确,但是,amule启动失败。

李记者又说,"你老啦,应该跟踪日志输出。"我们发现 at 之下执行 amule,失败。又是跟踪日志表明,amule给出了失败信息:

Error: Unable to initialize gtk, is DISPLAY set properly?

李记者说,"你真是老了,怎么不输出日志呢,非常有把握?"我说,"是啊,我本以为一定如此。"

然后我们开始了相对漫长的讨论,关于为什么 在at里amule启动不了,而在 eshell/shell-mode/xterm 下就能启动呢?

3.2 环境变量?

term下能启动amule,而at下不能启动amule。我们猜测 环境变量 没有传递从term传到at,查了一下。

(1) eshell/shell-mode/xterm 下的环境变量:

env | sort > env.log

(2) at 下的环境变量:

1 $ at now <<EOF

2 env | sort > at.log

3 EOF

4 > > warning: commands will be executed using /bin/sh

5 job 6065 at Sat Jul 13 22:02:00 2013

以上,在at下执行"env | sort > at.log"。

为什么要排序呢,因为term下和at下的env顺序可能不同,如果顺序不同,diff

起来就麻烦了。

(3) at 和 非at下的环境变量

1 diff env.log at.log

2

3 6d5

4 < DISPLAY=:0.0

5 26c25

6 < OLDPWD=/home/young/tools

7 ---

8 > OLDPWD=/

9 29c28

10 < PWD=/home/young

11 ---

12 > PWD=/home/young/tools

13 32d30

14 < SHELL=/bin/bash

15 38d35

16 < TERM=dumb

17 41d37

18 < _=/usr/bin/env

我们猜,极可能是第4行"DISPLAY=:0.0"。第4行中的"<",表明后面的这个环境变量只有左边的文件env.log存在,而在at.log中不存在。

我们在at之前置这个环境变量,在at里跑amule,启动成功。

3.3 at版的脚本

1 #!/bin/bash

2 # check if amule is running

3 export DISPLAY=:0.0

4 amule_status=$(ps aux | grep -w amule | wc -l)

5 if [ $amule_status = '2' ]; then

6 echo 'amule running.'

7 else

8 date >> /var/log/amule_monitor.log

9 echo 'amule starting/restarting.' >> /var/log/amule_monitor.log

10 amule 1> /dev/null 2> /dev/null &

11 fi

12

13 # next cycle

14 at now + 2 minutes 2>/dev/null <<EOF

15 amule_at.sh

16 EOF

(1) 环境变量

第3行,"export DISPLAY=:0.0",就是3.2测试的结果。

(2) amule的运行状态

第4行,

4 amule_status=$(ps aux | grep -w amule | wc -l)

这行的内容,其实与死循环的版本没有任何区别。连行号都一样。

(3) 判断

在死循环版本中,有一段判断amule的状态,如果不是两行,就启动amule。在at这一版本中,也有这样一个判断,就是第5行到第11行。

这几行的代码与死循环版本也非常相似,除了加入了时间戳和日志:

8 date >> /var/log/amule_monitor.log

9 echo 'amule starting/restarting.' >> /var/log/amule_monitor.log

但是,在at这一版本中,判断单独存在,并不像死循环版本中;在列循环版本

中,判断是在循环之中的。

(4) 没有死循环

没有死循环,如何实现"永生"的效果呢?这个shell脚本执行完毕,就要退出了,而不是隐藏在后台继续执行。退出以后,如果amule死了,谁来生列肉骨呢?

所谓永生,不外乎一遍一遍跑,发现死了就救活。除了死循环,我们也可以这么干,at版本的第13行至第15行:

13 # next cycle

14 at now + 2 minutes 2>/dev/null <<EOF

15 amule_at.sh

16 EOF

第14行可以逐词译为中文:在 此刻 以后 2 分钟,执行 <<EOF...EOF 括起的语句。

"2>/dev/null"仍是表示错误重定向,重定向给null设备。这个设备有个八卦。最初的unix系统是BSD (还是AT&T?)

开发的,他们发行的是源代码,并在磁带

(没错,不是光盘)的卷标上写着,如果你有啥意见,就发到/dev/null去吧。/dev/null,那是个黑洞,数据只入不出,你只管吐槽,它不起微澜。

在at中指定的作业恰恰就是 amule_at.sh,当前的这个脚本自己。所以说,递归和迭代是多么地相似。

第14行把 amule_at.sh 排在了 atq队列

中,2分钟后,又检查一遍amule是不是活着--同时也很重要的,把amule_at.sh又一次排在了atq中。

循环仍然在,它只是换了个面目,不在shell script中,而是在 atq队列中。

就像我们刚刚从DOS的程序开始学习windows消息循环一样,循环仍然在,只是不再由你来实施,而是由"系统"提供了。这也软件工程中一件可怕的事,系统越来越多地承担任务,最后我们就无事可做,因此无业可就了。

4. 还有一种死亡

amule除了活着运行中和列了不在运行两种状态外,还有一种状态,另一种死亡的状态。

它可能是僵尸--即没有退出,因此还在ps中,amule_status结果有两行;同时也不是在运行中,因为它不对添加任务或列任务清单做出任何反应。

你是不是想到了一些人?

有个命令行工具 amulecmd可以用来帮助检查amule的状态。

如果amule 运行着:

1 $ amulecmd -c status -Pyour_password

2 This is amulecmd 2.2.6

3

4 Creating client...

5 Succeeded! Connection established to aMule 2.2.6

6 > eD2k: Now connecting

7 > Kad: Connected (ok)

8 > Download: 72.73 kB/s

9 > Upload: 49.96 kB/s

10 > Clients in queue: 358

11 > Total sources: 278

如果amule没运行:

1 young@young-laptop:~/media$ amulecmd -v -c status

2 This is amulecmd 2.2.6

3

4 Creating client...

5 Connection Failed. Unable to connect to localhost:4712

如果 amule 僵死着呢?

1 young@young-laptop:~/media$ amulecmd -v -c status

2 This is amulecmd 2.2.6

3

4 Creating client...

5 C-c C-c

第5行的"C-c C-c"不是输出,而是amulecmd一直不结束,我按键中止它运行。

但是对于shell script就麻烦了,你一直不结束,我怎么知道你到底是什么状态呢?这让我们想起了图灵停机问题。

注意:以下代码未经过测试,不保证正确。因为僵死状态我现在没有能成功重现,所以没测试过。李记者说,为了让amule僵死,我可以拔网络摔硬盘砸机器,甚至诅咒...

注意结束。

我试图用下面的代码侦测amule是否僵死:

amule_status=$( (amulecmd -c status &) | (read -t 1; echo $?; killall

amulecmd 2> /dev/null 1> /dev/null))

"amule_status=$(...)"是取后面命令运行的结果,作为变量 amule_status 的值。这个命令是:

(amulecmd -c status &) | (read -t 1; echo $?; killall amulecmd 2>

/dev/null 1> /dev/null)

管道的前半段,用"()"开了一个子进程,后台执行amulecmd -c

status,所以无论amulecmd执行能否"卡住",后面都会继续进行。amulecmd的输出给了管道的后半段:

(read -t 1; echo $?; killall amulecmd 2> /dev/null 1> /dev/null)

这后半段,其中,"read -t 1"是延时1秒,读标准输入

(已被前面的管道转为amulecmd的输出);如果read读到了东西,"echo

$?"的结果是0,否则就是别的什么。"$?"是个变量,表示前一命令执行的错误号。read读到东西了,错误号就是0,否则就不是0。

然后,"echo $?"就变成为了 amule_status的值。后面的呢?后面的killall杀掉amulecmd,以防它"卡"住了。

接着,我们判断这个新的amule_status,如果是0,那么amule还活着,或者死透了。活着,就什么也不用管,死透了,2分钟后at会通过amule_at.sh启动它。都不用担心。如果amule_status不是0,杀死amule,等它死透,再次启动amule。像这样:

1 if [ $amule_status != '0' ]; then

2 date >> /var/log/amule_monitor.log

3 echo 'amule is dead.' >> /var/log/amule_monitor.log

4 killall amule

5 sleep 15

6 amule 1> /dev/null 2> /dev/null &

7 fi

综上所述,at + 处理僵死的 完整版 如下。

1 #!/bin/bash

2 # check if amule is running

3 export DISPLAY=:0.0

4 # echo 'in script.' >> /var/log/amule_monitor.log

5 amule_status=$(ps aux | grep -w amule | wc -l)

6 if [ $amule_status = '2' ]; then

7 # echo 'amule running.' >> /var/log/amule_monitor.log

8 echo 'amule running.'

9 else

10 date >> /var/log/amule_monitor.log

11 echo 'amule starting/restarting.' >> /var/log/amule_monitor.log

12 amule 1> /dev/null 2> /dev/null &

13 fi

14

15 # check if amule is alive

16 amule_status=$( (amulecmd -c status -Pyour_password &) | (read -t

1; echo $?; killall amulecmd 2> /dev/null 1> /dev/null))

17 if [ $amule_status != '0' ]; then

18 date >> /var/log/amule_monitor.log

19 echo 'amule is dead.' >> /var/log/amule_monitor.log

20 killall amule

21 sleep 15

22 amule 1> /dev/null 2> /dev/null &

23 fi

24

25 # next cycle

26 at now + 2 minutes 2>/dev/null <<EOF

27 amule_at.sh

28 EOF

5. 生存状态

(1) 有一种生存状态,是活着,它接受输入,对外输出,做出响应,我们把这种状态叫做活着;有一种生存状态,在进程列表里,它不存在,因此,系统迅速做出响应"此人已死,有事烧纸",这是死亡;还有一种生存状态,它活着,占着CPU和内存,有时还占着屏幕,但是不对外界做出任何响应,这种状态,叫做僵尸?

不过,我们有时称为禅定。有些人看到的是东方的智慧吧。我看到的,是对恶的姑息。

(2) 有同学可能纠结于永生和再生的区别。其实,这一只草履虫和那一只草履虫又有多大的区别呢?《自私的基因》里提到,DNA从产生起,就一直没有"死亡",而只有在一代代生物间遗传或变异,即使肉体死了,DNA还在继续。它还提到,即使肉体死了,你传递给别人的思想也会一直生存下去。有点像日本动画片里的话,"带着你的份儿活下去,把你的份儿也活出来。"

我们把Unix操作系统kernel以外的那部分叫做shell,那么shell里面的是什么呢?也有部动画片以题目回答了这个问题,"Ghost

in the Shell"。

----------

本文的命令在 emacs eshell 不好使,因为 eshell 对管道支持不充分,shell-mode则没有问题。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

如无必要,勿增实体:在很多移动硬盘中找到某个文件

本文简要介绍通过 管道使用 find,bzip2,grep,案例是把诸多硬盘的目录(和文件)结构压缩存储,用于离线查找硬盘上的文件;还涉及到流在通过管道时压缩和解压缩。

最近拍的照片,哈尔滨探望大毅,跟ZHUMAO出去玩,在这里 [http://www.douban.com/photos/album/106351444/]。

1. 问题

事实上,我没有"很多"移动硬盘,而是只有"一些"。我倒是有很多光盘,装在半米高的柜子里几乎塞满,里面都是文件,有的是软件,有的是我的笔记和资料。光盘全都用记号笔写了名字,每张光盘还都配了一张纸,打印整页的树状目录。当年盗版软件光盘能做的工作我都做了,就是为了什么时候需要某个文件的时候能一下子查出来。

但是,失败了。

文件太多,一页纸经常打不下,如果简略一些,日后就可能找不到;光盘太多,一张张找来也真是费劲。现在,移动硬盘又出现了类似的问题。当我查找某个东西的时候,经常需要把六七个移动硬盘挨个插上计算机,一顿搜索。有时候还可能忘了最佳的关键词,再重插重搜。

当年的光盘是这样解决的。用TotalCommander

(绝对一大利器,向用windows的同学强烈推荐)做每一张光盘的目录树压缩包,把这些压缩包放在机器的硬盘里。需要找某个文件的时候,搜索这个压缩包,而不用插光盘。找到文件在哪张光盘里,再按卷标找光盘。当然,这要求给文件起个有区分度的好名字。目录树压缩包,是TotalCommander的一个插件,就像ZIP或RAR一样压缩目录,只不过它的内容不是文件,只是文件名。

移动硬盘多了,也出现了当年光盘的问题。不过当年的方案不能用了,因为我现在的坐机是Linux,没用TotalCommander可用。不过,好在上述功能用简单的脚本也足够能完成,涉及到的命令也只有三个而已。

2. 索引和压缩

这一步,把移动硬盘插上,把目录树列出来,做成个压缩包。

比如,我的这块移动硬盘 mount 在 /media/Elements/ 之下,形成的目录树压缩

包的名字是Elements.dir.zip,命令如下:

$ find /media/Elements/ | bzip2 -c > Elements.dir.zip &&

timidity~/tools/tomato/unlock.mid

(1) "find /media/Elements/" 的作用是把硬盘的目录树列出来,因为后面是管道,所以没有任何输出。如果我们截获它的输出,应该类似于:

/media/Elements/

/media/Elements/

/media/Elements/files

/media/Elements/files/ted

/media/Elements/files/TOP250部电影

/media/Elements/files/video

/media/Elements/files/[沙丘].Dune_CN_01_11.mp3

/media/Elements/files/[沙丘].Dune_CN_01_12.mp3

/media/Elements/files/[沙丘].Dune_CN_01_13.mp3

/media/Elements/files/[沙丘].Dune_CN_01_14.mp3

/media/Elements/files/[沙丘].Dune_CN_01_15_A.mp3

/media/Elements/files/[沙丘].Dune_CN_01_15_B.mp3

/media/Elements/files/[沙丘].Dune_CN_01_16_A.mp3

/media/Elements/files/[沙丘].Dune_CN_01_16_B.mp3

/media/Elements/files/[沙丘].Dune_CN_01_21_A.mp3

/media/Elements/files/[沙丘].Dune_CN_01_21_B.mp3

这里之所以不用tree的原因是,tree会形成下面的效果,因而无法用grep查找到某个文件在哪个目录下。

| |-- Card

| | `-- Characters and Viewpoint Elements of Fiction Writing (33)

| | |-- Characters and Viewpoint Elements of Fiction Writing - Card.mobi

| | |-- Characters and Viewpoint Elements of Fiction Writing - Card.pdf

| | |-- cover.jpg

| | `-- metadata.opf

如上,用grep的时候会查到:

| | |-- cover.jpg

我们只知道cover.jpg在第三级,却不容易找到它所在的目录是 "(某个目录)/Card/Characters and Viewpoint

Elements of Fiction Writing (33)"。而根据后文我们会知道,用grep查找而不用眼睛一行行去看非常重要。

(2) "bzip2 -c > Elements.dir.zip"

的作用是把标准输入来的东西压缩为zip格式,命名为Elements.dir.zip。"bzip2

-c"的意思是把压缩的结果输出到标准输出,所以我们用">"把它重定向了。

在本命令中,标准输入就是 (1) 里面的 find 的输出,那个目录树结构。

(3) 后面的 "&& timidity~/tools/tomato/unlock.mid" 的用途是压缩完成以后让计算机叫一声,通知我。

压缩目录树的速度非常快,但是从一整块移动硬盘里获取目录树有时花的时间比较长,而一直盯着屏幕等程序退出绝非我辈之所为--尽可能把任务交给计算机,只要你能说明白了。

"&&"符合不同于";"之处是,"&&"要求之前的命令执行结果正确,无错误返回值,然后再叫,";"不管前面结果如何。

输完上述命令以后,我们就可以听歌看碟看书喝咖啡,等计算机通知我们这块硬盘压完了。然后换一块移动硬盘,改mount点,改压缩包的名字,回画,再去享受生活。

生活里令我们非常享受的一点是,我们没有把目录树先写成个文件,然后再压缩这个文件。我们使用了管道,而不是文件作为中介。这让我想起,不止一次,研究生同学在做项目的时候告诉我,不可能把摄像头啊数据啊什么的直接读出来,必须先写到硬盘上,或者这么做比较方便。我们不能那么做的原因,首要的原因是是这样的效率要比直接读低很多,因为硬盘比内存慢好几个数量级,另一个更重要的原因是:我们学计算机的,代码早有一天要被别人看,咱丢不起这个人呐。

如无必要,勿增实体。

3. 解压和查找

当哪天我们要查硬盘里有什么数据的时候,我们就这样:

$ bzip2 -cd *.dir.zip | grep -i justice

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.01.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.02.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.03.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.04.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.05.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.06.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.07.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.08.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.09.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.10.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.11.mp4

/media/Elements/emule/incoming/[公平:怎么做才正确?].Justice.What's.The.Right.Thing.To.Do.Episode.12.mp4

/media/Elements/emule/incoming/公平与正义.第06集.Justice.What's.The.Right.Thing.To.Do.EP06.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第07集.Justice.What's.The.Right.Thing.To.Do.EP07.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第10集.Justice.What's.The.Right.Thing.To.Do.EP10.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第11集.Justice.What's.The.Right.Thing.To.Do.EP11.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第12集.Justice.What's.The.Right.Thing.To.Do.EP12.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第1集.Justice.What.The.Right.Thing.To.Do.EP01.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第2集.Justice.What.The.Right.Thing.To.Do.EP02.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第3集.Justice.What.The.Right.Thing.To.Do.EP03.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第4集.Justice.What.The.Right.Thing.To.Do.EP4.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第5集.Justice.What.The.Right.Thing.To.Do.EP5.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第8集.Justice.What.The.Right.Thing.To.Do.EP08.HDTV.HALFCD-TLF.mkv

/media/Elements/emule/incoming/公平与正义.第9集.Justice.What.The.Right.Thing.To.Do.EP09.HDTV.HALFCD-TLF.mkv

命令中,管道的前半段, "bzip2 -cd

*.dir.zip"是把所有文件都解压了,然后输出到标准输出。所有文件,就是目录树的内容。我们没有真正地把这些内容显示在标准输出上,而是通过管道给了另一个程序。

这接收目录树内容的另一个程序,就是"grep -i justice"。其中的"-i"的含义如下:

$ grep --help | grep -w -i

Example: /bin/grep -i 'hello world' menu.h main.c

-i, --ignore-case ignore case distinctions

有的同学可能觉得这样不能一行行看,只能去查,有点别扭。不过,事情是这样:

(1)如果你知道自己要找的东西的名字,那么grep的检索条件就是它。或者

(2)如果你不知道自己要找的东西的名字,你就应该仔细想,想完了再来查。大多数事情都可以委托给别人做,唯独找到自己想要什么,必须亲力亲为,不可假手他人。对于需要别人提示才知道这正是自己想要的,我总是想起老师诱导小学生,"你看,啥啥是不是挺好的。"然后,我们就只能相信。

对于这一点,不少人最初都不太容易接受。她们倾向于在讨论的过程中互相理解和支持,并达成一致意见,"啊,原来咱们要找的是这个。"又或者,他们习惯于在工程中提需求的时候说,"你先这么做着,然后看看不行再改。"这路子就跟敬酒的时候说,"我干了,你随意,随我的意。"我们当然可以随客户的意,如果他们为他们不知道自己想要什么的时候你所完成的将要必要抛弃的工作付钱的话。不过,大家通常都愿意只为自己最后见到的东西付钱。

软件工程的著名案例,对比土木工程,从来没人说,"你先把这桥建起来,不合适的话,再转90度试试。"人们往往还没认识到,软件工程跟刮大白一样,人工也是要钱的。又像下棋,你下错了子儿,就要付出代价,而不能撒娇耍赖。

所以,一定要知道--自己想要的是什么。记得当年外教mimi问我们大家,什么是幸福。众说纷芸,吃好吃的啦,能毕业啦,亲人健康啦。我当时正天天抑郁

(现在似乎也未稍减),所以答:知道自己想要什么,这就是最大的幸福。她老人家唯独觉得我说得挺在理的。不过最后我期末成绩也不怎么样,大概她觉得那也不是我所追求的幸福吧。

跟索引和压缩那步一样,我们没有先把zip里的东西解出来

(你列一下zip里的目录就知道了,里面根本没有文件),然后再去grep,而是通过管道把解压的结果传给了grep。

如无必要,勿增实体。

4. 管道

完成以上步骤就能够保存移动硬盘的目录树和检索了,这一小节只是做个游戏。

$ find . | bzip2 -c | bzip2 -cd

.

./250porcelain.dir.zip

./my_passport.dir.zip

./backup001.dir.zip

./Elements.dir.zip

./gold.dir.zip

./lniu.dir.zip

上述命令以管道分隔成了三个部分。第一部分find,列出目录树;第二部分把目录树的文本压缩了;第三部分把传来的压缩的东西解开,然后写到标准输出上。在管道里,目录树内容的文本先压缩了,然后又解压了。

熟悉windows下的C语言的同学可能会想到,这管道先是处理了ascii-text,然后又处理了二进制,到底它是二进制的啊,还是纯文本的呢?这个问题请你自己查查吧,挺有意思的。

5. 如无必要,勿增实体 以外

计算机就像所有的艺术一样,充满了相互矛盾的各种信条,比如这条就是。一方面,我们相信,在程序设计、脚本写作中,应该尽量避免中间的分支出去的东西。类似的思想,在程序设计中,我们就要减少变量,把表达式的值直接传递给函数或者下一个表达式。比如,不写成:

a = 10 + b;

printf ("%d", a);

而是写成:

printf ("%d", 10+b);

但是另一方面,我们有时候又相信这个信条的反面:过早优化是万恶之源。在这个时候,我们相信,应该先对问题建模,然后再归约。建模和归约两个步骤合并成一个,往往是造成问题的原因,归约后的模型通常不那么易读了。越是聪明人,越熟悉的技术路线

(或者只知道这一两种技术路线),越倾向于把归约视为理所当然,而我们又经常没有聪明到可以像控制直接映射的模型那样控制归约后的模型。典型的例子是,当你脱口而出,"这不就是ABCD嘛",尤其是语带不屑的时候。

所以,如无必要,勿增实体 之外,我们还得知道事情的反面--过早优化是万恶之源。

PS.

本文的命令在 emacs eshell 不好使,因为 eshell 对管道支持不充分,shell-mode则没有问题。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

补充 保持我们最初的理想,当面对无数歧路

补充 保持我们最初的理想,当面对无数歧路

本文继续精略介绍用sed批量修改,补充grep按词匹配。兼极精略讨论周朴园为什么是虚伪的,结论是没文化真可怕。

1. 修正一个错误,find

上文提到 "find ." 中的"."是当前目录,有误。在这里,"."是从当前目录向下递归遍历,而不仅仅是当前目录自己。

如运行:

$ find . -name "*.[h|c]" -exec grep -nH fexecv {} ;

./include/schily.h:107: /* 6th arg not const, fexecv forces av[ac] = NULL */

./include/schily.h:108:extern int fexecv __PR((const char *, FILE *,

FILE *, FILE *, int,

./include/schily.h:110:extern int fexecve __PR((const char *, FILE *,

FILE *, FILE *,

./calltree/calltree.c:232: fexecve (Argv[0], f, fpp[1], stderr,

./libschily/fexec.c:111: ret = fexecv(name, in, out, err, ac, av);

./libschily/fexec.c:168: ret = fexecve(name, in, out, err, av, env);

./libschily/fexec.c:175:fexecv(name, in, out, err, ac, av)

./libschily/fexec.c:182: return (fexecve(name, in, out, err, av, environ));

./libschily/fexec.c:186:fexecve(name, in, out, err, av, env)

搜索到的匹配的文件,全都不在当前目录下,而是在子目录include,lcalltree,libschily中。

2. 去掉杂音 grep找getline

上文中用grep找包含getline的文件,结果有些杂音。

$ find . -name "*.[h|c]" -exec grep -nH getline {} ;

./include/schily.h:117:extern int fgetline __PR((FILE *, char *, int));

./include/schily.h:186:extern int getline __PR((char *, int));

./calltree/calltree.c:852: while (fgetline(fp, fname, sizeof(fname)) >= 0)

./libschily/stdio/fgetline.c:1:/* @(#)fgetline.c 1.6 03/06/15

Copyright 1986, 1996-2003 J. Schilling */

./libschily/stdio/fgetline.c:29:fgetline(f, buf, len)

./libschily/stdio/fgetline.c:67:getline(buf, len)

./libschily/stdio/fgetline.c:71: return (fgetline(stdin, buf, len));

其中有5行不是我们想要的,是因为 fgetline 误伤到的。fgetline比我们要找的多个f。

这些杂音会干扰我们的视线,让我们迷失方向,忘掉最初的理想。解决的方法就是:忽略它们,想办法不去注意它们,让噪音消失。

如果你的意志不够坚强,我们可以借助工具,像下面这样,用"单词"匹配,就把 fgetline 这样的去掉了,同时保留 "getline(" 这样的:

$ find . -name "*.[h|c]" -exec grep -w -nH getline {} ;

./include/schily.h:186:extern int getline __PR((char *, int));

./libschily/stdio/fgetline.c:67:getline(buf, len)

上面多的"-w"参数,代表:

$ grep --help | grep -w -w -w, --word-regexp force PATTERN to match

only whole words

3. sed的使用,批量修改

上文还提到,找到包含getline字样的文件,然后依次用文本编辑器打开,一个个修改。这样还是容易错,在这些细节中,我们也容易迷失理想。而保持理想的秘诀正是忽略现在。

我们可以用sed批量修改。

(1) 先验证一下修改策略。做批处理的计划最怕的是中间某个步骤做了,但这不是放弃使用批量的理由,我们可以在此处加观察验证。

$ find . -name "*.[h|c]" -exec sed 's/<getline>/getline_calltree/'

{} ; | grep getline

extern int fgetline __PR((FILE *, char *, int));

extern int getline_calltree __PR((char *, int));

while (fgetline(fp, fname, sizeof(fname)) >= 0)

/* @(#)fgetline.c 1.6 03/06/15 Copyright 1986, 1996-2003 J. Schilling */

fgetline(f, buf, len)

getline_calltree(buf, len)

return (fgetline(stdin, buf, len));

还是用find找到*.h和*.c文件,然后调用 sed 批量修改。

sed的意思是 stream editor,可以执行对文本的编辑命令。这些命令在命令行里给出,不仅跟IDE这样的图形界面不一样,甚至跟emacs/vi这样的全屏编辑也不一样。不知道sed和行编辑哪一个更早。

我们来看一下sed的这些参数。"sed 's/<getline>/getline_calltree/' {} "

's/<getline>/getline_calltree/'就是编辑的命令,它的意思是:查找 (search,

s)符合条件的文字"<getline>",改成"getline_calltree"。格式是"s/匹配条件/修改成什么样子"。

匹配条件"<getline>"中的"<"和">"表示按词匹配,类似于grep中的-w。""是转义符,因为"<"">"是特殊字符。

所以sed这部分的作用是找到 (*.h和*.c中的)所有getline这样的整词,修改为getline_calltree。

上述命令行最后的 grep getline,是为了检验修改策略是正确的。我们观察结果,果然,所有的getline都变成了getline_calltree,而fgetline无一误伤。

(2) 实施

执行

$ find . -name "*.[h|c]" -exec sed -i 's/<getline>/getline_calltree/' {} ;

这里的"-i"参数的意思是:

-i[SUFFIX], --in-place[=SUFFIX]

edit files in place (makes backup if extension supplied)

(3) 验证

保险起见,可以再看看"-i"到底改了没有。

$ find . -name "*.[h|c]" -exec grep -w -nH getline {} ;

$

果然干干净净的了。

(4) 剩下的工作

然后呢?

find . -name "*.[h|c]" -exec sed -i 's/<fexecve>/fexecve_calltree/' {} ;

然后 make。

然后看看编译出来的东西 (object) 在哪里。

$ find . -name calltree

./calltree

./calltree/OBJ/i686-linux-cc/calltree

大功告成。

(5) 批量

这样,当你告诉别人如何修改 calltree

才能编译的时候,你不必写上一大篇"先找到什么,在哪几个文件里,再找什么,在哪几个文件里"。你可以提供一个脚本完成这个任务。

如下:

find . -name "*.[h|c]" -exec sed -i 's/<getline>/getline_calltree/' {} ;

find . -name "*.[h|c]" -exec sed -i 's/<fexecve>/fexecve_calltree/' {} ;

这两行里蕴含了上述所有要求的步骤。

科幻小说作家Ted姜曾经写过一个情节,两个智商非常高的牛人终于碰面,他们只说了一个字,里面就包含了千言万语,进行了猩猩相惜还有战争。牛人们之所以能进行这种对话的原因在于,话语中的每个词都包含了非常的丰富的约定了确定定义的信息。因为那些是已经知道的了,正如康德说的,分析得到的都不是知识,而综合的,把这些已知知识放在一起的微量的未知,才是知识。调用DLL的程序之所以那么短,就是因为很多动作流程的约定已经在DLL中了。

周朴园之所以可以定性为虚伪的原因,也在于他实施了具有精确约定含义的动作--没有娶四凤她妈,剩下的说啥也白扯。他的各种小动作只是用来表现自己具有何处情怀和愿望而已,于四凤她妈没有任何影响。正是周的这种又做坏事,又要表现自己是好人的的做法,可以性定他为虚伪。所以,有确切含义的约定的知识在判断中至关重要。所有某个知识的背后庞大的网络支撑和构成了这个知识本身。

所以说,没文化真可怕。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

保持我们最初的理想,当面对无数歧路

保持我们最初的理想,当面对无数歧路

本文以实例非常粗浅地介绍Linux下的工具 find, grep, tar

的使用,还有正则表达式。侯捷先生用这些工具帮助写了<STL源码剖析>和<深入浅出MFC>,我们将用这些工具帮助编译一个叫做calltree的小工具。

本文还讨论了如何保持最初的理想。

0. calltee

calltree是个静态分析C代码中的函数调用关系的工具,用来画 call

graph。为了找个工具帮助画几个模块间的依赖关系,找到了它。刚刚这句你看不懂也没关系,你知道calltree是个程序就行了。事实上,大部分文章中的大部分话你看不懂,都没关系。直到因为你不知道这段话所提供的事实影响了你的阅读,那才有关系了。

1. 多歧路

在你读某段话时,如果没有完全读懂,那么宇宙会因此而分裂成为无数条分支,每条分支对应这段话的一种含义的可能。但是,这没有什么影响,你大可以把这些分支都忘了。不过,当你继续读,后面某一段可能会用到刚刚那句话提供的信息了,此时,因为你的注视,所有那些分支宇宙就坍缩为唯一的一个。如果你不知道那句话的确切含义,随着一步步深入,分支就会呈爆炸状迅速增长。

初学者在涉足某个领域的时候,所面临的正是这个问题--在每一步中,可能性都太多了,稍有不慎就拐进了一个死胡同。而且越走越远,接下来全是无用功。正是因为这个,我们要在每一步中检查自己当前的状态,而不能等到最后。

经常有同学问到,为什么我这段程序编译通不过呢?你要来他的程序一看,好几百行,编译一下,错误在第十几行。如果前面的你都没有通过,为什么要继续向下走呢,后面那好几百行都是怎么写出来的啊。

这是理工类的一个重要特点,如果当前的一步没有处理好,未来很难弥补。事实上,能在未来弥补现在的错误,是个巨牛的活儿。一般地,如果你幼稚到现在会犯这样的错误,基本可以断定,你不具备在未来弥补的能力。

所以写代码的时候,如果你承认自己不够牛

(通常如此),那么你应该写一会代码就编译一下,让编译器帮助检查错误。有同学说,我还得一阵才能写到下一个"括回}"呢。你应该在写"{"的下一个字符就写"}",然后再写中间的部分。步子要保证足够小,能在足够短的时间内编译,从而得到检验。如果航海技术不够发达,就应该珍惜生命,不远离陆地,在能看到海岸的地方航行。铁子同学,你是不是想起了《文明II》?

为了回避失败感,初学者们经常想要计划得非常周全,按他们的话说,"我想等想好了的再动手"。他们太骄傲了,计划,这是世界上最难的事实,远远比动手再失败要难很多个数量级。尤其是,你计划这件事情的时候,你尚不知道未来会遇到哪些问题--如果你知道,那么你就是这个并非陌生的领域的专家了。所谓初学者,就是两眼一抹黑地往前走的人。你不走,就永远也不会熟悉这个领域。

2. 解压

你说什么,等专家引领?专家哪有那个功夫。

比如安装calltree。你随便搜索一下,就有人告诉你从哪个网址下载。你下载完了,文件名是 "calltree-2.3.tar.bz2"。

你可能认识不少扩展名,认识 tar.gz,认识 zip,认识 rar,甚至还认识7z。可是bz2是什么?

大家一般的选择:1.找个支持bz2解压的工具,2.搜索一下,3.QQ上找个好友,"bz2怎么解压?"

最后这种人后来就被QQ好友拉黑了。有位大侠在网上提到: tar jxvf

calltree-2.3.tar.bz2。这也可能是你的QQ好友告诉你的。在命令行下跑一遍,你得到了 calltree-2.3 目录。

看起来解压完成了,很多人以为这一节就结束了。不过,在这个时刻,你可能没有注意到,宇宙分裂了。 tar jxvf

是什么意思,你没有注意,它存在各种可能,等到你再遇到需要它的时候,世界将坍缩为"你还是不会"。

这段话的意思,你当然可以搜索一下,或者QQ (你永远可以QQ别人,因此以后这条选择略掉),不过Linux/Unix的习惯是手册里有主要的信息。

我们执行:

$ tar --help | grep -j

-j, --bzip2 filter the archive through bzip2

然后我们就明白了,j的作用是"filter the archive through

bzip2",原来是用bzip2解压缩的。我们的文档刚好是bz2扩展名,合理合理。

"tar --help"会输出tar的所有帮助。有不少人习惯于把 tar --help 全输出来,然后用眼睛找想要的信息。

如果你知道自己想要什么,那么可以直接告诉计算机,而不用自己再亲力亲为。对大量数据的重复工作,计算机比我们擅长,只要我们指令明确。grep就是干这个用的。grep是一个过滤器,它把符合条件的留下来。"-j"前面的那个"",是因为"-"是个特殊字符,它的后面是grep的参数,""的作用是避免grep把"-j"作为参数,而是作为过滤条件。

如果你不知道自己想要什么,就只能在歧路中摸索;如果你知道自己的目标,那么,就可以直接命中,无需犹豫。大毅同学问我,为什么要看康德呢。我说,因为我想要过毫不犹豫的生活。道理是一样的。

初学者和专家最大的区别在于,专家不必尝试,他知道应该如何做,知道这样做的结果如何,甚至还知道误差和出错的概率有多大,甚至还知道如果出错如何改善。这些"如何"的难度,从前向后,越来越大。

2. make失败

解压以后,按Linux下从源代码编译安装,惯例就是进目录,读INSTALL文件;或者不读,直接跑configure或者make。

我make,然后出了一大堆错误。有的同学可能就要一行行找错误了,用眼睛。这很难,因为make的输出信息长达511行。这个数据也不是我一行行查的,而是:

$ make | wc -l

511

你要到哪里找错误呢?先想想错误信息的特征是什么。没错,"error"。所以,我们再用 grep。

$ make | grep -i "error"

../include/schily.h:186: error: conflicting types for 'getline'

make[2]: *** [OBJ/i686-linux-cc/avoffset.o] Error 1

make[1]: *** [all] Error 2

../include/schily.h:110: error: conflicting types for 'fexecve'

../include/schily.h:186: error: conflicting types for 'getline'

make[2]: *** [OBJ/i686-linux-cc/cvmod.o] Error 1

make[1]: *** [all] Error 2

../include/schily.h:110: error: conflicting types for 'fexecve'

../include/schily.h:186: error: conflicting types for 'getline'

make[1]: *** [OBJ/i686-linux-cc/calltree.o] Error 1

~/Downloads/calltree-2.3 $

这错误有点让人感到莫名其妙,因为getline和fexecve是两个著名的函数,你甚至能man出来它们,是posix的一部分。又或者你根本不知道它们是什么也行,你就会觉得,"这源代码不是应该一编译就通过吗,搞什么..."

3. 修改源代码

搜索一下,我找到了EnuLL大侠的博客[http://www.3null.org/?p=439],他说,"在比较新的内核(glibc库)上编译,会出现编译不过的问题。具体现象大概像下面这个样子..."

这个样子,就是你上面看到的样子。

然后EnuLL大侠说,"具体解决办法有两种,一种就是给glibc打patch,另一种就麻烦点,把冲突的命名手动都改了。"

关于解决方案,这句话到这里就完了,初学者就也完了。打patch,怎么打啊,手动改命名,怎么改啊。此处说来话长,我略过吧。总之,我们就知道,应该这样手动命名:把calltree源代码里所有声明、定义、调用这两个函数的地方,都把这两个函数改个名字。只要新的名字不再与posix的重名,那就没冲突了,编译就能通过了。

好了,目标明确了,你打算怎么动手?

有同学可能又想到了,一个个打开文件,然后有可能一行行看,用眼睛找到它们。或者,如果你有个够好的IDE,能全工程全文搜索--但是有时候你正用的项目恰恰不能在这个IDE上打开。再好的IDE和恒久远的钻石都是浮云,只有纯文本才是永恒的。

我们这样:

$ find . -name "*.[c|h]" -exec grep getline -nH {} ;

./include/schily.h:117:extern int fgetline __PR((FILE *, char *, int));

./include/schily.h:186:extern int getline __PR((char *, int));

./calltree/calltree.c:852: while (fgetline(fp, fname, sizeof(fname)) >= 0)

./libschily/stdio/fgetline.c:1:/* @(#)fgetline.c 1.6 03/06/15

Copyright 1986, 1996-2003 J. Schilling */

./libschily/stdio/fgetline.c:29:fgetline(f, buf, len)

./libschily/stdio/fgetline.c:67:getline(buf, len)

./libschily/stdio/fgetline.c:71: return (fgetline(stdin, buf, len));

从输出结果中能够看到:

(1) 在 include/schily.h的186行,是getline的声明;

(2) libschily/stdio/fgetline.c第67行,也提到了getline,查看代码以后发现,这是getline的定义;

(3) 奇怪,我没有找到调用getline的地方,不调用为什么要声明和定义。

(4) 那些fgetline是误伤的。

再来看产生这个结果的命令,"find . -name "*.[c|h]" -exec grep getline -nH {} ;"。有点乱,我们一段一段看。

find是个命令,找符合条件的文件。

(1) "find . -name"的意思是在当前目录"."下,找文件名符合条件的。

(2) "*.[c|h]"就是文件名要符合的条件。这是个正则表达式,意思是文件名任意("*"),扩展名(从"."开始)是"c"或者是"h"。我对正则表达式的运用还不是那么本能,所以实践操作中,没有先想到用正则表达式,而是先找了文件名为"*.c"的,然后找了文件名为"*.h"的。这也是典型的幼推的初学者的表现--不能一次性全部地了解自己的想法。

(3) "-exec ... {}... ;"。"-exec"的作用是,对find到的符合条件的文件,都施加一个动作 (命令)

,在本例中,这个动作就是grep。"{}"就代表找到的文件在这个命令中的位置。";"的作用是标识"-exec"的命令部分结束了,""是避免shell把";"当作find这个命令和下一个命令分隔符。wikipedia说得更明白一些,"The

semicolon (backslashed to avoid the shell interpreting it as a command

separator) indicates the end of the command."

如果找到的文件是 gold.c,那么"-exec"以后的部分相当于:grep getline -nH gold.c。注意,gold.c刚好取代了"{}"。

(4)"grep getline -nH {}"。grep是按正则表达式

(或退化为字符串)在文本中找到符合条件的行。"getline"是要匹配的正则表达式,在本例中,是我们要找到的那个函数。"{}"的意思上面说过了,代表find找到的文件,"-nH"的意思是打印出文件名和行号,你可以这样知道:

$ grep --help | grep -H

-H, --with-filename print the filename for each match

$ grep --help | grep -n,

-n, --line-number print line number with output lines

(5) 以上连起来,就是 "find . -name "*.[c|h]" -exec grep getline -nH {} ;"。

含义是:在当前目录下,找到所有的*.c和*.h;然后在这些文本文件的正文中,找到存在"getline"字样的地方。

然后呢?然后我们用文本编辑器打开那几个文件,找到对应的地方。我把getline都改为 getline_calltree。

然后呢?然后把 fexecve 也都改了,我改成 fexecve_calltree。

4. 可以更自动化

本例中,要改的地方不过五六处而已。如果要改的地方特别多呢?有个工具,叫做sed。

5. 再make

再make,通过了。为什么知道通过了呢?

我们执行

$ make | grep -i "error"

没有输出。没有消息就是好消息。

在".../OBJ/"下,我们得到了 calltree 文件,是可执行的。

6. 回顾歧路

每一个步骤,如果我们不选用自动化的 (批量的)

工具,我们都可能更倾向于陷入细节当中,迷失方向。尤其当我们是初学者的时候,我们尚不知道眼睛该往哪里看。这些细节具有多种可能性,很多步骤的多种可能性叠加起来,我们的面前摆着的就是一张非常复杂的网,每个节点都通向好几个地方。

我们总觉得眼前的就是最重要的,"活在当下"。就这样,我们被眼前的细节引导向完全不同的没有预期的方向;就这样,我们忘掉了最初的理想。

--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]

<我的外骨骼,诺基>后的访谈

在接下采访任务之前,我对杨贵福的了解仅仅限于:有名的科幻作家,男性,成年。我虽然读过他的许多作品,但在这些作品中并不能找到作者的影子。我试着从作品的行文风格中猜测他的个性——他或许是一个内敛沉默的人,因为他的作品处处透出一种平稳内敛的智慧。

他的文章情节总是在主人公的口中娓娓道来,而不是在激烈的矛盾冲突中展开。读他的写的故事,如同听一位老奶奶在午后的阳光里,眯缝着眼睛,讲述那些已经早已尘埃落定、成为历史的过往。那过去,有关于苏恒的记忆(《牧猫人》),有关于苏格拉底的爱情(《回忆苏格拉底》),也有模糊了真实与虚幻,苦苦寻找真相的人的故事(《周宁》)。但讲故事的人到底是什么样子的呢?我不得而知。

虽然钱大师说了,"假如你吃了个鸡蛋,觉得不错,何必要认识那个下鸡蛋的母鸡呢!"但这个每个人都在通过微博、空间极力展现自己的时代,杨贵福的沉默与低调更让我好奇。于是我顺应自己内心的好奇,透过那些字里行间带给我们的悸动,去认识故事背后那个演绎故事的人。

1.杨贵福,你好!请原谅我直奔主题。《我的外骨骼,诺基》从一个孩子的视角描写了一个生存环境恶劣的星球,类似于人类历史上曾经历过的无数这种惨淡的生存环境。机械生命的人性化与人类无情的自我残杀形成鲜明的对比,故事在淡淡的哀伤中充满了对战争的控诉,而少年的成长与机械生命的衰落更使情节充满张力。你是如何构思这篇文章的?创作意图是什么?

从写完这篇故事到今天,时间过去了这么久,这中间发生了很多事情,说实话,我都忘记"创作意图"这回事了。我傻对着您的问题想了半天,窗外是浮沙扬尘的夜晚,面前是...我想起来了,虽然不是创作意图,不过正是写这篇时的情形。当时我们正在做扫描电化学显微镜,在一个技术难题上纠缠了好几天,屡屡尝试而不得前进半步,战线越拉越长,眼看补给和信心都快耗尽。这时包师弟说,咱们不如先放下来写个好玩的小程序,建立一下信心吧。又是当时,我正在修改一篇长点的故事《9000秒》的初稿,同时开始写另一部自己雄心勃勃的故事,期待超过能力,痛苦不已。于是,我按包师弟的建议,写了个短的,就是《我的外骨骼,诺基》。苦中作乐,只求片刻欢愉。不过,写完了才发现,在小行星这样的故事背景中,由于没有大气层,那里可能有各种东西,唯独一定不会有阳光。

2.在文章中,处处能体现出一种让人觉得心酸而又发人深思的冷幽默。比如说,"没走成的人里,主要成份除了官员、士兵、恐怖分子,就剩下两类人,土里刨食的人和土里刨食的人。""占优势的一方,我们称为士兵,占劣势的一方,被士兵称为恐怖分子。"这种语言风格的形成,是缘于你平时的积累还是出于人物塑造的需要?这个主人公身上有你自己的影子吗?

风格?嘿嘿,您真幽默:)如果这算是一种语言风格的话,它既应该缘于平时积累,也应该出于故事场景的需要。这"风格"可能有点直白?咱们中国人一般如下述对话风格。作者说:编辑大人,咱们中午一块吃一口吧(其真实含意是,我请客)。编辑说:不麻烦啦不麻烦啦

(请再邀请一次) 。作者说:哎呀,不麻烦呐,那是给我面子啊

(我已按要求再次发出邀请,请表示同意)。编辑说:那就添麻烦了(谢谢,我接受邀请)。

"我"的身上,真的没有我自己的影子。如果非要找共同点,我家那儿真的有个金厂镇,产真的黄金的。在地球,不是小行星上。

3.我把这篇文章理解成一个温情的故事,文章中有淡淡的情愫在弥漫。"我"对莉亚的那种朦胧的感情,使得"我"为了救莉亚而不惜失去诺基的记忆(虽说最终并未失去),这一举动最终让"我"成功地从一个孩童蜕变成一个有责任有担当的男人,从而得到了诺基的承认,终于成为了它真正的主人。你是否认为人类的感情爆发能让人在一瞬间变得有责任?男孩子的成长是否需要一些突发事件的促进?

我不认为感情的爆发能让人变得有责任,相反的,承担责任往往需要压制自己的感情和感受,而更多地考虑别人的;相反的,承担责任的重点并不是选择什么,而是放弃什么,所以选择从来就是放弃。又,康德说,没有自由就没有道德。只有这一切选择和放弃都出自于你自身,而且不会把后果推卸给他人和环境和社会和父母和威逼和利诱,不会推说那时我还没长大,那么,你才真正长大了。不过,把这些大道理放进一个小故事,不是我这样的九流作者的功力可以达到的,只好跳出来自己在这里说啊说的。

4.文中是这样描述矿菌的:"伞盖刷地爆裂,里面凝结的金矿孢子像子弹一样贯穿宇航服打进身体,血被真空吸出来,喷溅得面前的地面上满满一片。"为什么设置这样一种需要以生命来采摘的矿菌呢?它看起来更像是一种生物,这样一种生物的设置仅仅是为了突出这个星球生存环境的残酷吗?

这基本上就是蘑茹,只是更威猛一点。蘑茹作为真菌,是一种生命,没有哪种生命是为了人类服务而天然生长出来的。我第一次发现课本里的"谷子笑弯了腰,鱼儿欢快地在网里跳着"是骗人的时候我都快哭了,不过,等以后读到猪们争着抢着喊,"吃我吧吃我吧",我哈哈大笑。我希望那些准备伸出拇指搭车瞬间就到了西藏,还有准备去山区支教为孩子们背上可乐的孩子能明白,这个世界真的就是这样残酷的,那些东西都是用生命为代价采摘的。不过直到亲眼所到,可能说了也是白说。甚至亲眼见到以后,也可能会坚定地以为,那样悲惨的生活一定是别人的。甚至亲身经历以后,也可能会坚定地认为,那样悲惨的生活一定应该是由别人来过吧,自己也过这种生活,一定是搞错了,或者不公平。

5.主人公对外骨骼的尊重,在文中处处能体现出来。他能比别人活得长久,能比别人升得更快,全在于外骨骼,在于他一直把它们当成人来看待。你是否认为机器人也应该与人有相同的权利?如果真有智能机器人,他们与人类能和平共存吗?

一、凡是具有理性的,都与所有其他具有理性的,应该享有相同的权利。教师与学生之间,上级与下级之间,资本家与工人之间,奴隶主与奴隶之间。但世界并不总是这样。二、工具与理性者不同之处在于,它没有自由,因此不为自己的行为负责。换句话说,工具没有资格享有权利。很多人,都只是工具而已,或者在需要负责的时候就声称自己是工具

(亲爱的读者,我知道你愤怒地联想到了一些人,不,我指的不是他们,正是你本人) 。在上述语境下,机器的权利只是清谈。

>如果真有智能机器人,他们与人类能和平共存吗?

如果真有智能的狗,如果真有智能的猫,智能的老鼠,智能的蟑螂,他们与人类能和平共存吗?我们一般期待狗和猫应该能跟我们和平共存吧,"毕竟我们是朋友"。但是我们凭什么这样认为,他们也认为我们是他们的朋友吗?如果你像这样提防着你的朋友,你应该期待你的朋友忠实于你吗?机器从还是石器的时候,就从来没有背叛过人类,我们一直在重复做着担心别人伤害自己的事情罢了。机器在还是石器的时候,它们与人类的不同,就比人类自身的差异要小得多。

6.被消除了记忆的诺基,竟然还对"我"的呼唤有所记忆,这种外骨骼已不再是单纯的机器人,而具有了人的情感?你认为未来的机器人,会发展出情感吗?

您是否觉得以前用过的某款键盘或者鼠标,或者小刀铅笔盒特别顺手,那种特别熟悉的感觉,就像不必语言,一个眼神就能懂你到骨子里的老朋友。你对它,亦或它对你发展出了感情,又有什么区别?它早已不是你遇到它时的样子,它的每个裂缝每个破损都与你一起经历。它之于你,不再是外物,它是记忆也就是你自己的一部分。因此无论有了多好的替代都一直不能离弃,它也会如此待你。

7.据我所知,《真实的虚幻》算得上你的第一篇科幻小说,并且内容与你本专业非常接近,并且到目前为止,这篇文章在科幻迷中仍有较大的影响。虚拟世界的主题已经成为科幻小说的过去时时,我们所期待的人机完全交互的时代却还没有来到,你觉得7.人类能制造出完全可以模拟人类的AI来吗?将来能发展出把整个人类上传的技术吗?

关于虚拟现实。如果一个婴儿从出生起就被残忍地蒙住眼睛,一生不得见光明,也从不能听到有人谈论视野,那么满足他所期待的交互就不必包括光线。这样虚拟现实是不是简单得多?我们如何确知不是从一直以来就被剥夺了某些感觉,因此尚无从得知世界之外那个计算我们的CPU的存在。哲学家和科学家于此尚存争论,不是我这样的小子可以发言的。

关于人工智能。就现状看,有的方面,机器已经超过了人类,有的方面,连蚂蚁的眼睛神经还不如。就未来看,如果我们要造出跟人完全一样的AI,生个孩子最方便了,为什么要用机器来实现,挑战自我吗?

关于人类上传。那啥,我可以先回答下一个问题吗?

8.网上有人把你和刘慈欣的作品放在一起比较,都归结到比较硬的科幻里。我发现你们两个其实还有个共同点,就是都是计算机工程师,这一职业你觉得对你创作科幻有什么帮助或者影响吗?

能与大刘先生的名字出现在一起,真是莫大荣幸,必须大笑三声。我有一件签有大刘先生

(及众多科幻作者)大名的T恤衫,一直珍藏,都舍不得穿。以前,我和大刘先生最接近的时候,我和他喝同一个瓶子里的啤酒,其实,他就坐在我旁边的座位,伸伸手就能拍到我的肩膀。现在,我和大刘先生最近的时候,中间只隔了一个"和"字。哈哈哈。虽然和大刘先生真正挨着印刷出来的不是"杨贵福",而是"你"。

不止大刘先生(和我),写科幻小说的人里,还有Ted姜等诸位。回顾历史,战争的时候,作家多战士;八九十年代,作家多工人;更早的时候,作家多乡绅;封建社会的时候,作家多士大夫。知道为什么现在作者里计算机从业者多了吧。

>这一职业你觉得对你创作科幻有什么帮助或者影响吗

有特别大的影响。因为除了这个,我基本别的啥也不懂了,所以也只写能这个。愿天下的人们都来写自己的工作。

9.这么多年来,你写了不少小说,在这些小说中,你觉得哪篇最能代表你的个人风格?原因何在?

基本上,我还在学习写作之中。虽然婴儿也可以说,我这辈子以来的风格就是会哭,不过如果我哪天挂掉了不能再写故事,也就不过就是婴儿水平而已,遑论风格。

10.我看了你发表的几乎所有小说,觉得你比较擅长抒情,但比较"硬"的科幻内核也写得有理有据,你是如何做到在这两者之间游刃有余的?

编辑大人,拜谢。不过,我真的没有做到游刃有余,而且的确生涩得很。之所以要抒情,之所以会有硬核,是因为这是我与读者的共同爱好吧。好比朋友喝酒,肯定选择有共同语言的,一起大骂些别人。

11."杨贵福是东北师范大学的老师,教数字电路与数理逻辑,还是acm的培训人员,还在一个研究所用计算机做一些东西。"这是从网上搜来的有关你的资料,问一下,这些身份里有多少是真实的?如果都是真的,你又是如何做到在这么多重的牛*身份中游刃有余的?(眼冒星星状)另外顺带给我们科普一下,acm是个啥东东?

引号里的都是真的,虽然并非全部。这真算不了牛,人人都在许多身份中生活,他们同时既是学生,也是儿子

(或女儿),是同学,是某个省份市镇的人,是男性

(或女性),是中国人,爱国者,赖床的人,也是对别人的不义义愤填膺,对自己的不义情有可原的人,也是悔过的,也是下决心的人。大家也都没精神分裂。

ACM必须得介绍一下,得意之事。ACM的正式简称是ACM/ICPC

(国际大学生程序设计竞赛)。东北师范大学计算机学院作为中国队之一将参加2013年在圣彼得堡举行的世界总决赛,全世界最顶尖的学生程序员届时将一决高下。我曾担任东北师大最初几届ACM赛队的教练,和我的学生一起出战哈尔滨东北四省赛。他们是我最初的学生,也成为了《北方之城》和正在修改的《9000秒》中的角色,他们将踏遍地球的许多角落,不过将仍然一直伴随着我。

12.谢谢你接受我的采访,祝你写出更多的好作品!

谢谢。:) 您删除的问题,诺基与诺基亚间的关系,请参见我的博文《那些年,我们一起喜欢的诺基亚》[http://blog.csdn.net/younggift/article/details/8275290]。

我的外骨骼,诺基

我的外骨骼,诺基

发表于《新科幻》 2013年6月

作者:杨贵福

我从小生活在金厂镇,一个半废弃的小行星上。金厂镇并不出产黄金,之所以叫这个名字,是因为我们当地人把所有金属都称做"金"。当初发现矿脉,附近几个星系的人都向金厂镇迁,顶峰的时候达到过30万人口。不过,我出生的时候富矿基本采光了,居民外迁,人口锐减。没走成的人里,主要成份除了官员、士兵、恐怖份子,就剩下两类人,土里刨食的人和土里刨食的人。这是个笑话,你们外地人不能理解。前一种土里刨食的人是农民,虽然暖棚的成本很高,不过本地生产的作物还是比其他行星运来的要便宜;另一种土里刨食的人就是我的父母这样的,挖矿为生。

士兵和恐怖分子也很难区分。一个原因是他们的铠甲看起来都差不多,全都很酷,穿上以后力大无穷,行动如飞。另一个原因是他们还经常互换身份。当时泛银河系刚刚接触河外文明,它们一个比一个强,而且都想要侵略人类。各种外星人团结一致,很快把人类打得分裂成两派,其中一派认为应该学习先进文明,融入河外星系,另一派认为应该保持自己的特色,奋战到底。所以,后来的战争主要是在人类和人类之间展开的,跟外星人关系不大了。这两派争夺所有可能得到的资源,当然也包括金厂镇的矿,所以他们交替在这里进进出出。占优势的一方,我们称为士兵,占劣势的一方,被士兵称为恐怖分子。前面我说了,其实他们穿上铠甲看起来都差不多。

遇到诺基的时候,我12岁。诺基是我的外骨骼的名字,这名字我也不知道是什么意思,不是我起的,是它身上写的。我的父母也不是什么矿主,他们才买不起那种奢侈品。我当时只认识一个矿主,就是莉亚的爸爸,不过连他也买不起外骨骼。

那天我跟着大孩子们去采矿菌。采到以后密封运回,可以跟莉亚爸爸换几根烟卷。在家长看不到的地方,几个人靠在墙上,放松肌肉抽上一根,吞云吐雾,那才叫带劲。回家的时候被问到,"又抽烟啦?",就表情木讷地摇头,这是更刺激的时候。这种刺激跟采矿菌那时候的感觉一样。明知道要摘的伞盖随时可能爆炸,可是还是我们小心翼翼地把手伸过去。下一刻,要么又多得了几根烟卷,要么,伞盖刷地爆裂,里面凝结的金矿孢子像子弹一样贯穿宇航服打进身体,血被真空吸出来,喷溅得面前的地面上满满一片。其实别的事情也充满了风险。莉亚爸爸如果被查到非法收购矿菌,会被抓去打个半死,附带损失一大笔资本。如果莉亚爸爸少了资本,安全设施就会更陈旧和短缺维护,我们这些穷小子的父母可能哪天就埋在矿洞里了。我们都是这样的,家长,青少年们,莉亚的爸爸。我们都希望下一刻运气不赖,这些好运气积累起来,我们就有钱离开金厂镇。这就像把垃圾站里捡到的零件拼吧拼吧,只要你活得够久而且够有耐心,最后总可能拼成一台能对付工作的铠甲。

选矿菌的时候,我专挑那些不那么成熟的,虽然金含量低,莉亚爸爸还会骂我是胆小鬼,压低收购价,但是它们爆炸的机会也小得多。同行的大孩子已经开始发育,他们身强力壮,胆大包天,还经常为谁先发现快熟透的矿菌而大打出手。他们好像既不怕被矿菌炸死,也不怕对方下手狠了把供气管打断。我比他们都矮小瘦弱,宁可离他们远些,他们也不喜欢靠近我这样的懦夫和我挑选的下等货。

因此,我遇到诺基的时候是独自一人。我正全神贯住地伸手探向一株矿菌,身子斜斜着,嘴里嘟嚷口诀,"幺幺洞九,九幺洞幺,哈里路亚,千万别炸",脚下突然一绊。你要是正集中精力拆炸弹的时候,旁边突然有人弄出点动静来,哪怕是放个屁,都能把你吓尿了。我当时就是这感觉。等感觉到汗顺着脖子后面淌下来的时候,我的手和身体还保持原来的姿势没动。低头一看,一只机械手从地底下冒出来正攥着我的脚脖子。

那是一个士兵,或者恐怖分子,受了重伤。他身穿铠甲,把自己埋在土里伪装起来,可能是在等救援。可是小行星周围满天卫星,他一点无线电信号也不敢发送。

他哑着嗓子对我说,"我是一个战士。"

我点点头。这句话没有半点信息含量。从铠甲上我就能看出来他是个打仗的,不过,士兵和恐怖分子都称自己为"战士",所以无法分辨他是哪伙的。

"你别低头看我,假装在采矿菌,听我说。"

其实我看他也是白看,他大半埋在土里,露出来的部分涂满了迷彩,头盔标记属于哪一方的铭牌早就磨得一片模糊了。

"你给我一些氧气,再给我一些水。"

他穿着铠甲,能轻松捏断我的脖子,我根本不敢不给,但是不得不犹豫。这些给养都是要花钱买来的,如果父母知道我被一个半死的士兵或恐怖分子抢了,说不定会把我打个半死。

"我把这身外骨骼送你。"

酷哇。虽然铠甲这么大号,也不能开着下矿,不过,有了这铠甲,我就是朋友圈子里第一个有车辆的人啦,连莉亚她爸爸也没舍得给她买呢。而且,我刚刚知道,原来这东西叫做外骨骼,不叫什么土了吧叽的铠甲。氧气和水换这么一身装备,而且叫做外骨骼,真是不赖。我点点头。

"你现在把氧气和水分我一些,"声音从地下传出来,指挥我把管子顺下去接上,"好,再来一些,再来一些。"

"再给你,我就没命回镇里了。"我切断输送。

"小兄弟,放心吧。"他的手在我的脚腕上又加了一分力量,我疼得直咧嘴。他说,"我给你留了足够的份,因为你还得回镇替我办件事。"

他让我去找镇民兵三队典少尉,单独一个人见他,就说"李记者"在这里等他。

我问,"你是个记者?我怎么看不出来。"

他粗声粗气地说,"你照着说就是,快去吧。"

我怀揣着将拥有这身埋在土里的外骨骼的梦想,向镇里跑。那位士兵或者恐怖分子,他骗了我。镇民兵三队里没有叫做典的少尉军官,更没有认识李记者的人。但他骗我最厉害的事不是这个,而是给我留下的氧气不足以支撑我回到镇里。所以,在能看到镇门的时候,我疲惫不堪,本想坐一会儿,却摔下山路,醒过来的时候面罩裂了两道纹,气流吱吱往外冒。右腿跟健断裂,不疼,但是完全不能用力,站不起来。爬了两步,又担心把宇航服磨破透气。我躺下来等死,星斗满天,银河暗涌。

那些同行去采矿菌的朋友们回来发现我的时候,我已经不能说话。我穿着宇航服,他们抬不动我,一起凑了点氧气让我先活着。我面罩的裂纹谁也不敢碰,只好让它继续往外漏气。有几个家伙跑去镇里找人,来的是莉亚的爸爸,开着他那辆在平地上也咯吱咯吱响的破车。

"小伙子,你真有种,摔成这样还没把矿菌扔了呐。"莉亚他爸一边用胶把我的面罩裂纹堵上,一边哈哈大笑说,"回去你抽烟管够,今天算我请的。"

烟不是白抽的,人不是白救的。今天所有的矿菌,所有人的所有的矿菌,都免费归了莉亚爸爸。他说,就这样,他也是稳赔不赚,才没有占我们这些孩子的便宜。

几天以后,有新闻说又抓到恐怖分子,已经处决了。抓获的时候他伪装成矿工,正准备进入金厂镇。没有提到他哪里来的氧气,没有提到他那点氧气怎么撑过了这么多天,也没有提到他的外骨骼。

我能一瘸一拐走动的时候,又去采矿菌。父母说,"你怎么这么不懂事,要是再伤了,又得花钱治。"

我当然得小心,莉亚爸爸帮助粘上的面罩把我的视野划成了几个象限,无论看星空还是行星表面的时候,无时不刻不在提醒我,"要小心"。

我的外骨骼还半埋在那里,里面已经空无一人。我挖到它的肩膀,擦一擦灰土,想看清它的原主人的阵营和阶级。铭牌已经不知什么时候磕碰掉了,露出下面原来出厂的钢印,"始于1865,NOKI",后面似乎还有什么印记,不过早就磨损得一塌糊涂了。

"NOKI,你的名字叫诺基。"我说,"你听到了,你的主人把你送给我了。"

"确认,待命。"那个躺在半截土里的大家伙一动不动,只发出低沉的语调。

"这就是我的外骨胳了啊,"我绕着它转了半圈,根据它的手的大小猜测整个机器的尺寸,"这得什么时候才能挖出来。"

我感到脚下的大地震动,烟尘四起,大机器像是植物一样从土里面伸展出来。低重力,而且没有空气,尘土飘起很高才在远处坠落。我连连后退,诺基就在我面前直立起来,比金厂镇防护门还要高。烟尘褪尽,我俩就这样站在小山丘上,我仰头望着它,他仰头望着远方。

这么庞大的外骨骼,与其说是被人穿戴起来,不如说是人钻进机械中去。不过我没有找到入口开关,也不敢在镇外贸然脱掉宇航服,只隔着手套抚摸这大机械。是核动力的,电池不知道在哪里,但是诺基的通体都是温暖的。

"哈哈,外骨骼,我的啊。"我大声地喊出来,"诺基,指令,蹲下!"

我做好准备这家伙又要激起多少烟尘和地震,但是什么也没有发生。"蹲、下!"可能无线电系统有问题吧,我一字一顿地重新下指令。它还是像是锈住了一样傻站着。

"开步走,向左转,向右转,原地起跳",都没有反应。"原来是个坏的,比莉亚的机器狗还不如。"我暗想,那个士兵还是恐怖分子又多骗了我一件事。

没用的东西就是没用的,开回去弃置的时候还要交费。我扔下诺基,专心采矿菌,贴补家用或是换烟卷,才是正事。这边采着矿菌,那边发现诺基似乎在偷看我,斜着眼睛,仍然骄傲地昂着头。我跟着矿菌的分布,渐行渐远,听到背后沉重的脚步声,是诺基始终跟着我,刚好能看到我,却保持着距离。这倒是跟莉亚的机器狗有点像了。我跑过去再试,"蹲下,开步走,原地起跳。"没有任何反应,诺基居高临下地斜睨着我。

我继续采矿菌,心想,今天可以多采些,至少这个大个儿可以帮我运到镇外。我只盘算着自己的心思,却忘记了这片山丘上的矿菌在我养伤的这几天里继续生长,更近成熟。当接近其中的一株的时候,我看到桔红色的光星星点点在几处菌褶里闪烁,瞬间扩散覆盖了整个伞盖下方。我下意识抬手挡往刺眼的光芒,心想,这下完了。

矿菌爆裂,在这个距离上,我会被飞溅的孢子射成筛子。我看到纯白的光弥漫充满整个视野,鼻子里似乎闻到了金矿特有的死亡气息。我心里最后的念头是,至少,父母迁离金厂镇,又少了一个负担。

我没有死,不然又怎么有命给你讲这个故事。诺基救了我,他以我不能想像的速度冲过来,精确地打飞了正在喷射的矿菌,而我毫发无损。矿菌拖曳着流光溢彩的长尾巴划出一条完美的曲线,照亮了大半个天空,沉到小行星的地平线下面去了。过了许久,矿菌从地平线的另一端显现出来,升得更高了,光彩也黯淡许多。当它再次沉下和升起,已经融入群星之中。

我张大了嘴盯着矿菌消失在星空里,好半天才想起来说,"谢谢你啊。"虽然我也不清楚对机器是不是也要说谢谢,但是我还是忍不住说了。

诺基依然昂着头,俯视着我,轰隆隆地说,"不客气。"

那个时候,我还不懂很多年以后自己讲给别人的道理:所有的机械都是骄傲的,有尊严的,可以指令,而不可以驱使。当你驱使它如牛马,它就蠢如牛马;当你与他心灵契合如挚友,他就待你如知己。

我打开诺基的背板,把采的矿菌都装进去货舱。他载着这许多财富,毫不费力。我钻进诺基胸板后的驾驶舱,兴致勃勃地操纵,嘴里大声复述着指令,准备隆重地把诺基介绍给大家。开到半路的时候才发现,我并没有真正地操作诺基。或者说,它只是允许我接触控制台而已,根本没理采我的指令,自顾自地行动,只是有些时候行动与我的指令一致而已。即使在驾驶舱里,而不是不在货舱或者它的肩膀上,我也更像是个乘客,在仿真训练系统里学习,而他是任性的司机,想走就走,想停就停。很遗憾,如果我获得操作权,就能卸下它的武器装备,说不能能卖个好价钱。不过即使这样,大家也都对诺基的到来非常高兴。

莉亚爸爸看着诺基这大家伙和今天的收获,笑得合不扰嘴。我今天没有要烟卷,而是换成了钱。不同于以前每次的零碎,因为我有了希望,这么多的钱,也许可以帮助父母,早晚有一天可以离开金厂镇。

我的父母很高兴,他们把诺基敲得叮当响,"真是能节省不少燃料呢,能搬运,还能取暖。"他们唯一担心的是诺基报废以后怎么才能偷摸地非法弃置不被罚款。不过这个担心可能有点早,看诺基健硕的样子,会比我们每个人活得都久。

大孩子们也很高兴,我每次都免费帮他们运矿菌。只是如果希望在采矿菌的时候受到诺基的保护,他们就得离我近一些。因为诺基从来不跟着别人走,他一直置我于他的保护范围,对别人只是顺便救助。而且,诺基不听从别人的指令,无论是命令还是恳求。

莉亚也很高兴,她和她的同学每半个月往返天梯,去附近的大行星上学。现在不再是坐莉亚爸爸那辆叮当作响的破车,每次诺基都把莉亚他们装在背板的货舱里奔跑,这样在山间跋涉的时候就快捷舒适多了。我经常一个人在驾驶室里装模作样地操作诺基,好像真的成了它的主人。也有时,我骑在诺基的肩膀上,山影和群星扑面而来,然后飞一般掠到我的身后。当诺基飞奔的时候,我能听到隐约的风声。莉亚爸爸说的对,小行星的表面的确有微量的气体。虽然小行星的引力不足以形成大气层,但是矿脉里不止歇地有气体逸出,在它们消散在宇宙里以前,在地表形成了稀薄的一层。诺基飞速冲击这称薄的一层,就形成了隐约的风声。还有的时候,我也挤进货舱里,昏黄的灯光下,大家都低着头蜷在一起,脸上蹭着机油或者泥污,带着笑。莉亚有时坐在我的对面,有时就坐在我的旁边。她总是那么干干净净的,不吱声,安静地坐着。很多年以后当我回想起骑在诺基肩上风驰电掣的时候,我有时也会后悔,那个时候应该在货舱里多坐一会儿。外面是寒冷的无尽的宇宙荒漠,身边是坚实的诺基的,或者柔软的莉亚的,温暖。

所有这些,再也感受不到。

莉亚他们有一次准备去天梯的时候,战士和恐怖分子们又激战起来了。他们执着于哪一方先开火,进行过旷日持久的争论。其实那不重要,因为对于我们来说,无非是一方与另一方打起来了。也许战争以后战士更名为恐怖分子,也许不,又有什么区别。

对我们这些孩子有影响的是,没有人敢去采矿菌了。莉亚他们也只好搁置去天梯,虽然他们心急如焚,说是学校里有重要的课程。金厂镇外有几座山被轰平了,与外界的所有通道都成了军事无人区。矿工们也都停工了,虽然矿井入口在镇内,但蔓延到镇外的坑道有几处被钻地的炮弹炸塌了。

当然,生活还要继续,我们开始习惯野炮引起的地震。桌椅会摇晃,房屋会摇晃,但是没有人担心。我们没有什么可以失去的,炮火带不走矿脉,也轰不塌金厂镇的天顶。通往其他小行星的轨道也每天被炸上七八十回,士兵和恐怖分子都担心对方从外界得到给养和更多的弹药。也因此,他们都不攻击金厂镇,士兵和恐怖分子都需要金厂镇供给食物。

我们的农业还在继续。从金矿中提取能源,点亮人工光源照射四季收获的作物。虽然收成一季不如一季,不过总还能凑和。战争双方都派了专家研究为什么收成会越来越差。这些敌对方的专家坐下来心平气和地画很多公式,讲很多术语,有时争得面红耳赤摔门而去,然后又在镇中心的酒馆搂着肩膀喝得酩酊大醉抱头痛哭。酒馆老板悄悄说,这样下去收入暴涨,他也许可以早几天离开金厂镇了。

除此以外,镇内的生活也一如既往,连天空的星座都依然耀眼。小行星表面没有大气层,镇外的灰尘不会悬在天空,而是很快就尘降到地面上。所以,无论地面上打得多么惨烈,星空依然如洗清澈。我们都是尘埃,只是过客,留下很多暂时的印迹,而星辰一直在这里。

莉亚爸爸带我和诺基去给士兵或者恐怖分子送过一次食物,差点出事。

见到他们以前,我一直以为士兵和恐怖分子都一定长相可怖,语气粗野。没有想到,我所见到的都是比采矿菌的大孩子年长不了几岁的青年,有一些还会微笑地摸我的头。只是他们大多数身上带着各种伤疤,还有一些干脆就血迹未干还没有结痂。我跟莉亚爸爸提到这一点,他哈哈大笑,说,"你以为他们是哪来的,不就是你们长大了吗,也都是血肉之躯。"

要平安无事离开时,莉亚爸爸正在数着钱,一个家伙看中了诺基,说是军事管制,理应征用。我哭着去求他,他甩手就把我推倒在地。这位士兵或恐怖分子,也一样是大不了几岁的青年,脸上长着青春痘,只是此刻表情狰狞。

他的军官救了我们。军官说,"这种老旧的型号抢它还有个屁用。"

那位军官前后总共只说了这一句话,哑着嗓子。听声音,我觉得像是诺基以前的主人,那个被处决了的李记者。但是我没敢跑过去确认,万一他后悔了把诺基要回去呢。我又想,也许他就是典少尉?我不认识他们一天三变的军衔肩牌,所以连他的阶级也不能说准,更不敢问名字。

这位无名军官的话还是帮了我们。青春痘恢复成小青年,说,"也是,不够维护成本的,不定哪天就散架了。"他挥挥手让我们离开,还算和气,好像刚刚只是想开个玩笑。

在路上,莉亚爸爸让我也喝一口酒压压惊,他说,"要是征用了诺基,咱俩怎么回镇里还真成了问题。"

我们平安回到金厂镇那天,莉亚病倒了。她已经少量咳血一段时间,镇卫生所的赤脚医生说大家都咳,查不出毛病,估计没啥大不了的,养养就能好。我们回去那天莉亚咳血喷了一地。

莉亚爸爸动员了关系,把血样送到军队医院检查,结论是呼吸系统严重受损,来源是空气中的辐射。矿井坑道被炸断以后,小行星表面的气体开始逐渐扩散到金厂镇的天顶内部。专家也终于能够解释作物的收成为什么每况愈下,因为空气里渗透进了小行星矿脉中的金,作物的种子发生变异。变异细胞的最主要表现,就是死亡。金厂镇的人没有全病成莉亚那样,也只是变异的概率不够高,假以时日,生病的人和作物会越来越多。

"瞎扯淡,"莉亚爸爸说,"空气里怎么可能有放射性重金属。"

莉亚说,"氡气就有放射性啊。"她的声音很小,好像自己生病或者知道这些知识都是犯了什么错误。

莉亚爸爸变卖了不少家产,准备把莉亚送到附近大行星上的好医院去,据说那里有先进的设施能治这病。但是战事不止,双方都担心对方从金厂镇获得食物,开始加紧封锁金厂镇对外的交通,由金厂镇到天梯的山地拓展成了军事无人区,每天轰炸,人车禁行。

有人劝他,"就算你上了天梯,也可能在轨道上被打下来啊。拖到打得不那么厉害的时候吧。"

莉亚爸爸怕莉亚拖不到那天,他拿出一半家产,说谁能把他和他女儿送到天梯,就给谁。没有人敢应征,这是拿命赌。他找到我,说只有我有这么一架外骨骼,说不定能通过炮火封锁。我说"好"。他说,"我知道你从来没有真正驾驶过诺基,我们也就是碰碰运气,不行我就认命。"

我知道他想说的是,"没有人工驾驶的细致操作,想通过无人区万难成功。"他没有说,我也没有提。他说的对,不行我就认命。

大家都说我们疯了。他抱着莉亚进了货舱,我骑上诺基的肩头。我对诺基说,"去天梯",诺基沉默着开动,他没有说我疯了。

在无人区外,诺基卸载掉所有的武装,包括弹药、火炮,还有重装甲。莉亚爸爸说,"这是精明的减负准备。通过无人区需要的是速度,我们又不可能打赢军队。"

无人区地形复杂,山势起伏。满山遍野布满了士兵或恐怖分子掘进的坑道,还有连绵的单兵坑和炮兵阵地。所有这些,都是后来我才知道的,当时只知道我们飞奔着越过一个个山头,身后激起的烟尘还没等落下,我们就已经冲进下一个阵地。

士兵和恐怖分子都以为我们是敌对的一方,在试图切割他们的阵地,纷纷射击。但是,我们快到他们来不及反应。诺基开足马力,像一把烧热的快刀,从黄油之中劈出一条急速划过的线条。炮兵来不及校准射角和装药量,我们就已经脱离了他们的射程。步兵还没能解读完雷达传来的信息,我们就转到了另一个山谷。就这样,我们通过了大半段行程。

但是后来,我们开始与越来越密集的军队遭遇,他们显然为拦截我们而提前集结起来。我们尽可能避开军队,不断变更路线,但还是被几组步兵火线阻击。我钻进货舱,诺基的轻装甲保护了我们。

我们不停地突进,通天塔一样的天梯遥遥在望,诺基却停了下来。我们被火炮阵地包围了,他们算准了我们在围追堵截之下铁定会一头钻进这个口袋。他们不知道我们是谁,为了什么,但是他们要阻止所有他们没有掌控的事物。我们不是他们的人,这就是拦截的充足理由。

望着远处天梯的剪影,莉亚爸爸直拍大腿。莉亚抱着爸爸,不知道该如何安慰。包围我们的,不知是士兵还是恐怖分子,反正他们都拥有相同的火炮。他们的火炮第一轮齐射过后,我从昏厥中醒过来的时候,莉亚在我的怀里,她咳出的鲜血染红了我整个前胸。

诺基说,这样的火炮齐射几分钟之后会再来一轮,然后是下一轮和再下一轮,他的轻装甲不知能再承受几次打击。莉亚爸爸的头抵在货舱内壁,表情松驰,还在晕厥中。我没有唤醒他,反正第二轮炮击还是要震晕过去,何必多一次痛苦。

第二轮炮击之后。我朦朦胧胧恢复意识的时候,发现自己仍然抱着莉亚,她的身体轻得就像没有重量,好像我用力就能把她抛到天梯那去。我紧紧地搂住她,生怕她会在炮击中震得飞起来。

还没等我完全清醒,紧接着就是第三轮炮击,整个小行星和我的脑壳都要炸裂开了。但是我努力保持不晕过去,因为我想起了诺基第一次救我的时候,把矿菌击打出小行星的引力场范围。我必须要告诉诺基,我嘶喊,一遍遍重复着。炮声里,我听不到自己的声音,但是我大喊,"把我们扔过去。"最终,我晕了过去。

我醒过来,莉亚爸爸说,"扔过去,你想得倒是很好。但是那边也没有海绵垫子,我们会在那头摔死。"

谈话就到这里中断,然后是第四轮炮击。从这次炮击中醒过来以后,我问诺基它能跳多高,他的历史记录是跳起几米。我摇醒莉亚,"你帮我算,这些数据。我想知道,这样的力量在金厂镇小行星,能跳多高。"

炮击。我不知道在诺基的轻装甲破裂前是否来得及完成计算,是否来得及脱逃,但是我知道,在地球上能跳起几米的诺基,在金厂镇小行星这样的低重力环境下,可以跳得更高,更远。只是诺基可能从来没有设置成这样的模式,它不知道,对于它,在小行星上的最适合的行动方式不是奔跑,而是飞翔。

莉亚没有醒过来告诉我数据,莉亚爸爸清醒的时候完成了计算。不过他说,"你要求的轨道精度必须人工操作,还得有熟练的技术。"

又一轮炮击即将开始,我们能看到天空划过一排排闪亮的影子。我大喊回答他,但是炮声随即掩盖了一切声音。

我看了一眼昏睡的莉亚,然后在剧烈的震动中爬出货舱,外面是地狱之火的海洋,夹杂着金色的矿菌孢子漫天飞舞。我非常害怕,但是听不到牙齿撞击,我颤抖着祈祷,期待无论哪个神能在这样的炮击轰鸣中听到我的声音。

我在火光里钻进诺基的护胸板,关上舱门,炮声渐弱。我一边复述指令一边操作,我大声喊,虽然知道它根本听不清我的哭腔,我说,"关机,手工操作,手工操作。"

我的指令是要告诉诺基,不惜失去它对我的所有记忆,我要莉亚活下来。

然后,我无法呼吸,又在炮击的巨震中晕了过去。我醒来的时候,听到诺基低沉的声音,我知道它还活着,没有关机,不知道是应该高兴,还是悲伤。

"不必关机就可以切换为手工操作。"诺基平静地说,"手工操作需要成年男人的责任和技能,恭喜你都已经具有。"

在下一轮炮击开始前,我操作诺基从弹坑斑驳的土地上起跳,在星空里划一条漫长而平滑的抛物线,掠过脚下正飞向目标的飞蝗般的炮弹。急速扑向天梯前我最后一眼看到的,是远方金厂镇的天顶在矿脉蜿蜒的群山环抱下闪闪发光。

诺基在空中翻转和调整姿态,弯腿缓冲平稳落地。我跌跌撞撞地从驾驶舱中爬出来,正看到近在咫尺的天梯塔身在慢慢倾斜,它的下方烟尘四起,紧接着大地传来剧烈的抖动和轰鸣。他们炸毁了天梯,为了彻底断绝敌人与外部的交通。也断绝了莉亚活下来的希望。我的全身抖个不停,没有力量从驾驶舱滑下来,就坐在那里放声大哭。诺基处于手动模式,比平时更加沉默,只用微温的机壳拥着我。

那一天剩下的时光,我们一直躲在天梯的废墟里,听四周火炮不时轰鸣。莉亚感到非常冷,我和莉亚爸爸轮流抱着温暖她。莉亚没有活着回到金厂镇,她死在了我的怀里。我永远记得最后温暖和柔软的感觉。

当战事不那么惨烈的时候,不知哪一方修复了天梯,另一方没有制止。金厂镇的人弹冠相庆,说矿能运出去,收入好一些,日子可以不那么紧巴。对我,这没有什么变化。有些需要就像降落伞,当你渴求它的时候,如果它不出现,以后出现与否,那也没有什么意义。

我们从天梯回来以后,莉亚爸爸迅速衰老了。他老得那么快,你正跟他喝着酒,就能看到他的腰慢慢地变弯了,脸上的皱纹刀刻一样生长出来。他总是一声不吭地喝完酒,然后拍拍我的肩膀就走了。

他不再收矿菌,矿厂也关掉了。他花了很多钱喝酒,喝到最后几乎所有的财富都变成了酒精,只剩下只身一人离开金厂镇的钱。他拒绝我和诺基送他,执意要徒步去天梯。我说,"你这得什么时候才能走到啊。"他说,"总能走到吧。"这是我听到他说的最后一句话,以后再也没有见面。

我目送莉亚爸爸的背影,突然发现自己想不起来莉亚年轻的面孔。她如何微笑,她的头发是怎样弯曲披散,我以为会永远铭记清晰得触手可及,如今却只剩模糊的印象。我一直以为自己在日夜思念着她,却再也想不起来她的样子。

我跳进诺基,大声叫喊着指令让它在旷野上奔跑,就像失去莉亚的那天晚上。诺基还是微温的,一如既往。这样能让我稍微想起莉亚在我怀里最后的温度。我和诺基在每一个军事无人区狂奔,炮火就在我们身边接连炸响。在整个小行星上放眼望去,是一直曼延到地平线的燃烧的群山。

雷霆轰鸣中,我们被抛起在半空,然后重重地砸在地面上。这是我最后的印象。我以为我和诺基会死在这次鲁莽的奔跑中,但是我们伤痕累累却活了下来。我们落在了士兵或恐怖份子的手里,我和诺基都被征用了。被哪一方征用了,又有什么区别,反正我和诺基受命杀光另一方。

我们经常就在金厂镇的附近战斗,但是我一点也不想逃回去。在这里,或者在那里,又有什么区别?有时我们被这一方俘虏,有时被那一方抓获,我和诺基一声不吭,支持他们所有的政策和理念,伤痕累累地跟他们去杀另一方。

杀光了其中一方,我和战友们就离开金厂镇所在的小行星,被打包成捆一样发送到别的星系。但是诺基没有走成。他型号老旧,体积庞大,不再适合日新月异的战术需求了。

我提出过异议,这是我的个人物品,应该归还金厂镇转交我的父母。只是诺基拒绝跟随任何人,不听其他人的指挥,这很令我犯难。军需官说,"首长,这有什么难的,看我的。"

他熟练地给诺基输入几条指令,诺基突然僵住不动,慢慢变冷。再启动的时候,它不再认识我,但是变得对所有人都温驯和服从,可以留在金厂镇作为有用的设备了。

"你看,标准的老式工业机器人。"军需官说。

"很好,有你的。赏。"我拍拍他的肩膀。

我和我的战友们征战了整个泛银河系,我的战友和外骨骼不断更新,我的阶级不断上升。能一直活着并且升阶级的原因非常简单,别的那些人的运气都非常地不好,他们或者死于我们,或者死于敌人的炮火之下。

他们说,我操作外骨骼似乎有天生的灵性,无论是攻击,还是从溃败的战场下撤,总能得心应手。其实,他们只是不相信我的可以告诉每一个人的秘密。所有的外骨骼都是诺基的亲戚,他们全都血肉相连。每当我更换新的外骨骼,我总是先给他们讲一遍诺基的故事。然后,它就和我成了朋友。这些朋友,后来大多战死了,只有我活了下来。有战友开玩笑,说我会一直这样活下去,不断升阶级,直到只有皇帝或总统一个人可以杀了我那天。

这么长的生命里,终于有一天我又见到了诺基。或者说,我认为见到了它。

那是一次扫荡的超小型战役,几乎没有纠缠,是上级赏给我们的肥肉。在攻击中,我与一台外骨骼狭路相缝,一看它就是老旧笨重的型号,虽然漆得焕然一新。本来没有悬念,无论是我的技术还是我的外骨骼的性能。我的战刀毫不犹豫地斜劈下去,对方奋力一闪,居然躲得半条命在。刀锋无坚不摧,却只切掉了它肩上的铭牌,露出下面陈旧的底子。

我从后面扼住它的脖颈,让它暴露出胸甲且无法行动,召唤旁边的战友,"来,捅一刀。"就在战友驾着他的外骨骼冲过来的时候,我突然看到对手肩上露出的模糊不清的钢印,隐约可以读出厂商的标识"始于1865,NOKI"。

我大喝一声,"诺基!"它正挣扎着的动作一滞,然后瘫软在我的怀里。

我的战友指给我看,"嗨,你也太大意了。这个家伙的肘刀已经刺中你外骨骼的护胸板,我再晚半秒,你就被它从前往后扎透啦。"

操作诺基的家伙束手就擒的时候满嘴血沫子,他对我喊,"如果不是外骨骼突然卡住了,你早就成了我的刀下鬼。"这个勇敢的青年由于信仰坚定被准许弃暗投明,后来在泛银河系不知道哪个角落的拉锯争夺里英勇战死。

那次战斗结束以后,没有人想要这样老旧的型号作为战利品,只是围过来看热闹,看敌人已经破落到何种程度。大家哈哈笑着从列队摆好的机器前走过,指指点点。

有一个说,"居然连这样的型号也拉出来献宝了,诺基A型,你听说过吗?"

我站在诺基的影子里,费力地仰头才能看到它的面孔,用不屑一顾的语气说给我的战友听,"那些老古董,谁又知道。"

诺基的背后,陌生的星座无声地闪烁着布满太空。我问,"战争已经漫延到什么星系了,这里离金厂镇有多远。"

没有人回答我。诺基静立不语,我的战友也已经走远了。