crypt_blowfish 的一行代码引发的 bug

提起安全,不同的人也许会有不同的理解,从日常可见的各种病毒,到互联网背后的各种安全协议,都属于安全的范畴。但对普通的计算机用户来说,最切身的感受还是登录自己系统的密码的安全。要确保密码的安全,除了要求用户使用强壮的不易猜到的密码外,更重要的是系统使用一个安全的方法来存放用户的密码。使用密码学里的某个哈希算法对密码进行哈希,并存放该哈希值就是一种标准的方法。

使用该方法的一个显而易见的优点是避免了存储密码明文所带来的风险,对用户的认证只需用同样的算法对用户输入的密码生成哈希值,并于存储的哈希值进行比较即可。该方法在 Unix/Linux 下由来已久,接口就是我们所熟悉的 crypt(3) 函数。

可以看出,在密码的哈希值已知的情况下,采用的哈希算法越强壮,密码就越难破解。从密码学的角度来说,一个给定的哈希算法返回的哈希值长度通常是固定的,理论上总会有多个密码的哈希值是相同的。因此这里的强壮意味着,即使已知该哈希值和哈希算法,也很难在可行的时间内推算出该密码,或某个其它的哈希值相同的字串。

传统的 crypt(3) 实现使用的是 DES 加密算法的一个变种,该算法在很长的时间内被认为是安全的,但是随着计算机的迅猛发展,该算法在使用了近 30 年后,已不再认为安全。因此,一些其它的算法被选择来代替它,这其中使用最广泛的无疑是 MD5,本文要讲的基于 blowfish 算法的 crypt_blowfish 则是另外的一个。为了区分开这些由不同的算法生成的哈希值,调用 crypt 函数的 salt 参数被赋予新的约定,即如果 salt 字符串以 $id$ 开头,并且以 $ 结尾,那么 id 的值将表示所采用的哈希算法。MD5 的 id 值为 1,crypt_blowfish 的 id 值则为 2a。

(再废话一点,当然,现在来说,MD5 也不再被认为是安全的了,包括 SHA-1,这要归功于王小云教授在 04 年的密码编码年会上对 MD5 的碰撞攻击的著名演讲,因此当前主流发行版使用的哈希算法是 SHA-512。其实从使用范围上来说,基于 blowfish 的哈希算法从未像 MD5 那样广泛,当然,并不是因为 blowfish 加密算法本身不够安全,而是由于 blowfish 并不在 NIST 认可的算法列表里。因此像 Red Hat 这样的发行版制造商在选择替代 MD5 的算法时,并没有考虑它,而是重新设计了基于 SHA-2 的 SHA-256 以及 SHA-512。当前使用 crypt_blowfish 的发行版包括 OpenWall,SUSE 以及 ATL。除此之外,PHP 5.3+也集成了该算法。)

crypt_blowfish 的这个 bug 非常简单,下面是有问题的代码片段:这里的 tmp 类型是 unsigned int,ptr 与 key 的类型都是 char *,key 指向的是待转换的密码字符串。这段代码可以简单理解为,将密码以 4 个字节为单位放到 tmp 里。

tmp = 0;
for (j = 0; j < 4; j++) {
    tmp <<= 8;
    tmp |= *ptr;

    if (!*ptr) ptr = key; else ptr++;
}

这是 crypt_blowfish 的作者所找到的 bug 以及修复:

Oops. In BF_std.c: BF_std_set_key(), change:
    tmp |= *ptr;
to:
    tmp |= (unsigned char)*ptr;

对于熟悉 C 语言的同学来说,这个 bug 很容易理解,也是 C 语言里最容易出错的地方。即 C 语言的标准并没有指定 char 类型是 signed char 还是 unsigned char,而是留给实现自己去定义。当 char 类型被定义为 signed char 类型时(大部分情况是,比如 x86 下的 Linux),这里的 *ptr,将先被转换为 signed int,然后再与 tmp 进行 OR 操作后赋值给 tmp。也就是说,如果密码里的某个字节最高位为 1(ASCII range 128 ~ 255,e.g. 0x80),那么该字节在符号扩展为 signed int 后,扩展的高 3 个字节全为 1(i.e. 0xffffff80),再于 tmp 进行 OR 操作将冲掉之前存储的字节。

简单来说,在某种情况下,如果用户的密码里的某个字节高位为1,那么该 bug 将使得寻找与该密码有相同哈希值的字串变得容易,从而使得密码的破解也相对容易。举个例子,如果某个密码为 “ab£”,那么在该 bug 的影响下,使用 “£” 或者 “xy£” 都将生成相同的哈希值。显然,这是一个令人恼火的 bug,即使在密码里使用这种字节的情况并不常见。

虽然对该 bug 的修复也很简单,只有一行代码,但一个完整的更新方案还要考虑已存在的大量可能正确也可能错误的哈希值,如果只是单纯的升级该修复,那么有可能在升级后,用户使用旧的密码将无法登录进系统。因此,如何保证在打上该修复后,即保证用户登录系统不受影响,又能将期间的安全隐患降到最低,是一个非常棘手的难题。这也是大部分安全问题所共有的特质。

对于该 bug 来说,更糟糕的地方在于,并不是所有的含有 8bit 字节的密码的哈希值(使用旧算法生成的)都处于相同的情况,在修复后的算法下,有的哈希值仍然可以用旧密码生成,有的则必须用另外一个字串生成,有的则无法通过任何字串生成,从而成为一个无法使用的哈希值。不过一个共同点是,如果某个哈希值是由包含 8bit 字节的密码使用旧的算法生成的,那么在新的算法下,如果该哈希值对应一个密码的话,该密码必然包含字节 0xff。

因此 crypt_blowfish 的作者 Solar Designer 提供的解决方案是扩充上文提到的哈希算法 id,简单的说,两个新的 id,$2x$ 与 $2y$ 被定义,其中 $2x$ 被定义为有问题的 crypt_blowfish 算法,$2y$ 被定义为正确的 crypt_blowfish 算法,原有的 $2a$ 则被认为是不确定的算法。新的修复后的算法生成的哈希值的 id 将为 $2y$。

Solar Designer 的方案是一个通用的方案,具体的发行版或集成了 crypt_blowfish 的应用还需要去处理其余的情况,最主要的一点是如何解析 id 为 $2a$ 的哈希值。为了确保任何用户的登录都不受影响,可以将 $2a$ 按 $2x$ 处理,此时 crypt_blowfish 将使用旧的有问题的算法来比对哈希值,当然相应的风险也将存在。如果将 $2a$ 按 $2y$ 处理,此时 crypt_blowfish 将使用新的修复后的算法来比对哈希值,这意味着黑客仍然有可能会利用旧的漏洞登录进系统,为了防止该情况,可以禁止任何包含 0xff 字节的密码,此外,这也意味着某些用户可能再也无法登录,这种情况需要系统或应用使用其它方法来通知用户重置密码。具体的升级方式依赖于具体的管理员策略,但这里没有一个完美的解决方案。系统或应用所能提供的解决办法是提供两个选项,一是是否将 $2a$ 按 $2x$ 处理,二是在上面选项为否的情况下,是否禁止包含 0xff 字节的密码。

研究 crypt_blowfish 的这个 bug 能够得到许多经验与教训。首先,在使用 C 语言编程时,那些需要大量使用位操作,以及经常在 signed 与 unsigned 之间转换的地方要慎之又慎,这些地方通常都在底层,一旦有 bug,影响的将不只是底层的一小块功能,而且难于找到根源;其次,也是有意思的一点是这个 bug 至少已经存在了 13 年之久,而没有被发现。相信这样的 bug 应该这不是最后一个,这提醒我们即使在使用非常成熟的代码库时(不管是开源还是闭源),仍然需要大量枯燥无味的测试,谁又能保证自己恰好不会碰上这样一个 bug 呢。

引用与致谢

Prefetch,性能,与毒药

性能也许是大多数软件开发人员所不愿意面对的特性之一。编写代码是容易的,但优化代码,提高代码的执行效率并不容易。比起写代码时的酣畅淋漓,提高性能除了一开始的架构设计无误外,到最后,往往变成在一些个别代码上的纠结,很多时候你不得不用一些看起来丑陋的代码去替代那些散发着你的灵感的想法。即使如此,这种性能上的努力有时候也并不如期望的那样,甚至更糟。

在不同的领域,提高性能有着不同的实现方法。有的领域也许需要一个更优的算法,有的领域也许需要一些编程上的技巧。在 Kernel 领域,由于 Kernel 直接面对硬件,性能上的优化往往与硬件提供的功能息息相关。当代计算机硬件在性能上最突出的矛盾是 CPU 的运行速度远远超过内存的速度,如果所有内存引用的数据都需要直接从内存读/写的话,CPU 将不得不把时间花在对内存操作的等待上。为此,当代 CPU 都有一些快速的 CPU Cache 用来临时存放所需要的内存数据,以加快运行速度。Kernel 对性能的优化很大一部分就是利用 Cache 的优化,包括经常见到的仔细定义结构体的成员顺序,以使得那些通常需要一块访问的成员在一个 Cache line 内就可以访问到。

让事情更美好(或者更糟糕)的是 CPU 制造商也在想尽一切办法尽量减少内存访问所引起的延迟。常见的这样的高级 CPU 功能包括:Prefetch, Out-Of-Order Execution, Branch Prediction 等等,这些功能有的是纯硬件的,对操作系统透明;有的功能 CPU 会提供相应的指令可用。使用这些先进的硬件功能帮助 Kernel 提高性能,是非常诱人的。然而正如前面提到的那样,采用这些功能有时候并不见得一定能提高性能,由于这些硬件功能在实现细节上的不公开等种种原因,结果甚至更差。Prefetch 就是最近 Kernel 开发人员刚刚学到的一例。

Prefetch 简单的说就是 CPU 通过某种算法猜测接下来可能要执行的指令/数据,然后提前从内存里取出来并存放到 Cache 中,如果猜测的结果与实际执行的一样,则执行速度加快。Prefetch 分为硬件 Prefetch 与 软件 Prefetch。硬件 Prefetch 包括 Prefetch 指令与数据,指令的 Prefetch 相对容易理解,并且可以看出顺序执行的指令 Prefetch 的准确率高,而带有条件判断的语句则需要与 Branch Prediction 功能相结合。数据的 Prefetch 则牵涉到 CPU 对内存数据访问模式的分析,比指令 Prefetch 更加复杂,通常是 CPU 在探测到某些特定的事件后(一般是发生了连续的 Cache misses),触发 Prefetch 自动执行。硬件 Prefetch 对操作系统来说是透明的。软件 Prefetch 则是操作系统或应用程序明确要求 CPU 将某个地址的数据 Prefetch 到 Cache 中,以备以后使用。

Prefetch 由于简单的接口,并且以性能为目的,被 Kernel 开发人员大量使用。最常见的地方是 list.h 里的 prefetch() 宏:

#define list_for_each(pos, head) \
        for (pos = (head)->next; prefetch(pos->next), pos != (head); \
                pos = pos->next)

这里的 prefetch() 宏是对底层硬件平台 prefetch 指令的一个封装,不同的平台有自己的 prefetch 实现方式,一般是在平台的 processor.h 里定义的。上面这段代码使用 prefetch 的想法是,在处理链表中当前节点的同时,让 CPU 将下一个节点的数据提前取出来,并放到 Cache 中,这样当处理到下一个节点时,数据已经在 Cache 里了。通常来说,链表在内存中的存放并不像数组那样连续,因此很难通过 Cache 优化。软件 Prefetch 在这儿提供了将下一个节点数据提前拷贝到 Cache 中的可能。

如果一切都如这里分析的一样,内核大量的链表操作将因此变快。可惜,事实并非如此。

最先对这里的 prefetch 提出质疑的是 Andi Kleen,不过 Andi 的理由并不那么充分,他提交的将 prefetch 从 list.h 中去除的 patch 也没有获得通过。最近 Linus 也对 list.h 里的 prefetch 发表了看法,这一次是 Linus 在自己的机器上 build kernel 时发现了 prefetch 的问题,大量使用 prefetch 并没有带来性能上的改进,相反,去掉 prefetch 反而运行的更快。Ingo Molnar 随后跟进对这里的 prefetch 进行了研究,他重现了 Linus 的问题,并且确认在这里使用 prefetch 将降低约 0.5% 的性能。

虽然 0.5 并不是一个很大的数字,但这里的问题在于本来应该提高性能的地方,却降低了性能。Linus 的分析是,他的测试有相当一部分操作是遍历单向链表 hlist,这些单向链表都很短,并没有多少让 prefetch 提高的空间。甚至,大部分链表只有一个节点,因此这里的 prefetch 的地址是 NULL。很明显,按照一般的设想,CPU 在遇到 prefetch(NULL) 的情况时,应该什么也不做,但在 x86 机器上并非如此。根据 Ingo 的分析,prefetch(NULL) 大约需要 20 个 CPU cycles。Ingo 通过软件的方法绕过了 prefetch(NULL) 的问题,即只有在地址非 NULL 时才执行 prefetch 指令,并做了测试,虽然性能有所进步,但比起完全去掉 prefetch 来仍然要差。

根据 Ingo 的观测,无论在这里添加还是去除 prefetch 操作,CPU 总体上执行 prefetch 指令的次数相差并不大。也就是说,此时 CPU 里的硬件 prefetch 已经处于饱和的状态,如果再通过软件显式去 prefetch 某些数据,似乎干扰了 CPU 正常的硬件 prefetch 策略。所以最好的选择就是让硬件自己去 prefetch,硬件在决定哪些数据该 prefetch 时,表现更好。Ingo 据此得出的结论是:

So the conclusion is: prefetches are absolutely toxic, even if the NULL ones are excluded.

这个结论也与最初 Andi 邮件里的说法相似,Andi 曾引述来自硬件设计人员的反馈,表示硬件开发者并不赞成软件开发人员显式的去使用 prefetch,除非有特别好的理由,而这里的 prefetch 并不是。

最终的结果是在即将发布的 Linux 3.0 里,list 操作里的 prefetch 被拿掉。这个结果与 Andi 提交的 patch 并不完全一致,Andi 的 patch 是提供一个 CONFIG 开关来控制,并且对 K7 架构,prefetch 默认是打开的。

仍然有人认为 Linus 与 Ingo 的分析并不能完全说明所有情况,尤其是 Linus 明确表示在他的测试情况下,list 的长度都很短,如果某个另外的情况使得 list 很长,性能是否会完全不一样呢?是否所有硬件平台的 prefetch 都与 x86 类似呢?没有人能明确回答这样的问题。大多数性能问题,答案都来自于大量的测试以及可信的数据,很多时候理论上的分析,并不完全正确,甚至恰恰相反。在这里,我们最好相信硬件 prefetch 能做的更好。

Kernel 开发人员的这个教训似乎告诉我们 prefetch 真的是一剂毒药。但这并不表示在所有情况下,prefetch 都是有害的,有时候,以毒攻毒也是有用的,正确使用 prefetch 仍然能带来性能上的提升,前提是你深入理解了 prefetch 的毒性,当然更重要的还是枯燥乏味的测试与数据。

引用与致谢

一个测试打字速度的小 Shell 脚本 [Linux Journal]

不知您是否跟我一样,曾经有过练打字的一段美好时光。那是毕业之后刚拿到我的第一份工作,一个程序员的职位。对于即将踏入社会的我来说,这个职位让我有些惴惴不安。且不说大学几年把光阴都花在了那些无聊的事儿上,就没认真写过程序,单是连打字都不熟练就能让我瞬间现了原型。我觉得在进入工作单位之前,我至少得把盲打学会了,至于 C/C++ 这些玩意儿先一边儿去吧。

很快我就学会了盲打,当然花的时间也并不短。我还想我是否应该也学学五笔,算了,我应聘的是程序员,又不是打字员。回想起这些,是因为从 Linux Journal 上看到了一个非常简单的测试打字速度的 Shell 脚本。脚本如下所示只有寥寥几行:

#!/bin/sh
# speed.sh: a very tiny utility to measure typing speed.
prompt="Start typing a piece of text. Press Ctrl-d twice to finish."
echo "\n$prompt \n"
start_time=`date +%s`
words=`cat|wc -w`
end_time=`date +%s`
speed=`echo "scale=2; $words / (($end_time - $start_time) / 60)" | bc`
echo "\n\nYou have a typing speed of $speed words per minute."

原理很简单,写成公式是:

typing_speed_in_wpm = num_words / ((end_time - start_time) / 60)

即打字速度等于,在测量的时间范围内所打的单词数除以所用的分钟数(Word Per Miniute)。实现也很简单,echo, date, cat, wc, bc 命令都是常用的命令。在我的 Fedora 环境下,/bin/sh 是到 /bin/bash 的符号链接,echo 命令后面需要加 -e 选项才能正确打印换行符 \n。此外,cat 命令以及后面所附的管道也可以直接去掉,因为 wc 命令可以直接接收来自 stdin 的输入。现在是测试一下速度的时候了,所选的英文文章是 Linus 的 what would you like to see most in minix?。注意,为了保证公平、公正,所打的单词不能有错误。我的速度是 41。

Typing Screenshot

当然,对于中文用户来说,打汉字才适合我们。对此我们需要修改 wc 命令的选项,不能统计单词数,而是统计字数(也就是 character),wc 的 -m 选项支持 UTF8 编码,可以直接统计汉字字数,而不用担心每个汉字所占的字节数。现在是测试一下打汉字的速度的时候了,所选的文章是朱自清的荷塘月色。我的速度是 46。实际的速度跟输入法有很大关系,因为只会用拼音,而且 iBus 的 pinyin 输入法又不太好用,会五笔的话也许好点。

Typing Screenshot

据说打字最快的英文记录是每分钟 212 个单词,中文记录是每分钟 502 个汉字。

Xen 的漫漫人生路

在 Linus 明确表示 Linux Kernel 3.0 只是一个版本号的改变,而非里程碑式的飞跃后,许多人对此表达了失望,一个没有重量级功能的新版本似乎配不上这个新的版本号。不过对有些人来说,其中的一个新功能或许可以担的上这个重任,那就是 Xen 的 block backend driver。这个功能加上之前在 2.6.37,2.6.38,2.6.39 添加的几个 Xen 相关的功能,使得即将发布的 Kernel 3.0 包含了所有成为 Xen 的 Domain0 所必须的功能,从此为 Xen 漫长的 Kernel 之路划上了一个句号,也标志着 Xen 的发展掀开了崭新的一页。

VMWare,Binary Translation 以及 Full Virtualization

提起虚拟化,一个不得不提的公司或者产品是 VMWare,如果说虚拟化最早的原型可以追溯到上世纪 70 年代的 IBM 的 VM 的话,那么当前的虚拟化热潮却是由 VMWare 引领的。相信有很多人跟我一样,对虚拟化的认知是通过 VMWare 得到的。当我初次接触 VMWare 时,我非常惊讶,居然有这样的产品实现了这么 nice 的功能。VMWare 的成功除了契合了时代的变迁所催生出的虚拟化需求外,也得益于自身产品的优秀。VMWare 产品的简单,便捷,易于理解当然是其中非常重要的一个优势,但更重要的原因来自于 VMWare 创造性的解决了 x86 平台内在不支持虚拟化的难题。

x86 平台难以虚拟化的本质主要来自于 CPU 的虚拟化。众所周知,x86 处理器的指令有 4 个特权等级,分别是 Ring 0 ~ 3。正常情况下,application 工作在最低特权级,即 Ring 3,而 kernel 工作在最高特权级,也就是 Ring 0,因为有许多硬件操作必须要在该级别下才能完成。在虚拟化的情况下,也就是有多个 OS 同时运行的情况下,显然这多个 OS 不能同时运行于 Ring 0,因为 OS 需要运行的某些 Ring 0 特权指令将互相干扰。因此一般的解决方案是将虚拟化软件(通常称作 Virtual Machine Monitor,或 hypervisor)放在 Ring 0,而将运行在虚拟机里的 guest OS 放到 Ring 1 或 Ring 3 中。一个正常的硬件设计是,当这些原本应该运行在 Ring 0 级别的指令在非 Ring 0 的级别里被执行时,处理器报错,这样运行在 Ring 0 的 hypervisor 就能捕获该错误,从而做相应的处理(比如模拟或替换该指令)实现虚拟化,然而 x86 处理器有一些特权指令在 Ring 0 及非 Ring 0 下都能执行, 并且有不同的含义,这就使得运行在 Ring 0 级别里的 hypervisor 无法捕获该指令,而该指令的运行也于原本的意义不同。

对此 VMWare 的解决方案是 Binary Translation,也就是 hypervisor 提前扫描 guest OS 待运行的指令,发现有这种无法捕获或无法虚拟化的特权指令时,将其替换成相应的一系列可捕获的指令,从而实现 guest OS 的虚拟化。当然除了 CPU 的虚拟化外,内存,I/O 设备的虚拟化也不那么容易,不过是 CPU 虚拟化方式的不同决定了各解决方案的不同。VMWare 的这种解决方式最大的优点是 guest OS 无需修改就可以运行在 VMWare 的虚拟机上,当然这个优点是相对于后来出现的 Xen 而言的,这种虚拟化方式也称作 Full Virtualization。

VMWare 的 Full Virtualization 解决方案一个致命的缺点是性能上的瓶颈,因为 hypervisor 要在运行时扫描指令,分析并替换,这个消耗是客观无法去掉的。当然 VMWare 采取了很多方法来弥补这些缺陷,使得虚拟机的运行不至于慢的难以忍受,造成的结果就是实现相当复杂。因此在 VMWare 掀起了虚拟化的浪潮之后,许多想投入到这个领域中去的人开始想办法从其它角度来解决 x86 难以虚拟化的难题。

Xen 的出现以及 Paravirtualization

Xen 的开发人员就想出了一个巧妙的办法。Xen 最初始于剑桥大学的一个研究项目,他们的出发点是既然 x86 系统难以虚拟化,那么我就假定 guest OS 运行在一个类似 x86 的平台上,该平台由 Xen 来提供。具体点说就是 Xen 实现了一个类似微内核这样的系统,该系统运行在物理硬件平台上(Ring 0 级别),而 guest OS 运行在 Xen 上(Ring 1 级别),于 VMWare 不同的是,运行在 Xen 上的 guest OS 需要修改,因为 Xen 提供或虚拟的硬件环境已不再是 x86 平台,而是一个类 x86 平台,当然再上面的 application 不需要修改。这种虚拟化的方式称作 Paravirtualization。

这就意味着,guest 原本需要运行某些 Ring 0 级别的特权指令才能完成的任务,现在要修改为调用 Xen 提供的相应的指令(称作 hypercall,从实现方式来说类似于 system call)。显然开源的易于修改的 Linux 是最好的目标,这也是早期的 Xen 无法支持 Windows 系统的原因。然而 Xen 的架构还远不止与此,由于 Xen 运行在物理硬件与客户机操作系统之间,因此 Xen 的重要性不言而喻,稍有差错就可能导致所有的客户机崩溃,因此要求 Xen 的代码尽量简练,实际上是越少越好,而一个完整的 guest 运行所需要虚拟的硬件除了 CPU 外还有很多,例如内存,I/O 设备等等。显然如果 Xen 即支持底下的硬件设备,又要虚拟上面需要的硬件设备,那么 Xen 的实现无异于重写一个 Linux。Xen 的想法是 Xen 只支持最基本的硬件设备,也就是 CPU,内存,中断等,其它 I/O 设备由一个专门的 guest OS 来支持,而其余的 guest OS 要想访问该设备,需要通过 Xen,然后再传递到该特殊的 guest。Xen 为此将 guest 分为两个级别,一个是有访问 I/O 设备特权的特殊的 guest,称作 Domain0,其它的称作 DomainU。

因此对 Linux 来说,要想运行在 Xen 上,就必须要修改 Kernel 代码,并且根据 Linux 是运行在 Domain0 级别还是 DomainU 级别而修改的代码也不同。早期的 Xen 采用 Linux Kernel 2.6.18 作为基础,进行修改并实现了 Domain0 以及 DomainU 的功能。

由于 Xen 提供的虚拟化解决方式的性能优势,以及 Xen 的开源的特性,Xen 很快就被炒上了天。当初成立该研究项目的大学教授成立了公司,并获得了风投的青睐,继而被 Citrix 收购;各大发行版纷纷抢着支持 Xen 作为自己的虚拟化方案;很多创业公司也一夜之间冒了出来,研究 Xen 或者利用 Xen,以期望在未来的虚拟化与云计算大潮中能博得一位;Xen 俨然成了虚拟化的未来。

硬件虚拟化技术的进步以及 KVM

然而技术的发展往往非人所料,由于虚拟化浪潮的热火朝天,以及 x86 平台难以虚拟化的本质,使得 Intel 与 AMD 这两大芯片商开始思考如何在处理器里添加功能克服原有的缺陷。于是在 2006 年,Intel VT-x 与 AMD-V 出现了。简单的说这两个 CPU 扩展就是在保持四个 Ring 特权级别的条件下,分出两个 forms,分别给客户机与 hypervisor 使用,由于每个 form 都有这四个 Ring 级别,因此客户机的特权指令的捕获就可以轻松实现了。硬件技术的出现很快就带动了软件技术的发展,很快有利用这种硬件虚拟化技术的软件出现了,没错,就是 KVM。

比起 Xen 来,KVM 的实现更加简洁而优雅,除了利用硬件的支持,KVM 还利用了现有的 Linux Kernel 的 CPU 调度等机制,因此 KVM 不需要像 Xen 那样重新实现一个复杂的 guest OS 的调度机制,这个任务交给 Linux Kernel 来完成,此时 guest OS 对 Linux Kernel 所在的 host 来说就是一个进程。此外 KVM 的实现方式也不需要修改 guest OS,因此 KVM 的出现很快引起了 Kernel 社区开发人员的兴趣,几乎是在最短的时间内,KVM 就进入了 Kernel,此时 Xen 仍然按照自己的开发模式在按部就班的进行着。

Xen 的问题

相对 KVM 来说,Xen 是一个庞大的项目,一个完整的 Xen 的搭建,需要四个组成部分,首先是最底层的 hypervisor,其次是需要修改的 Domain0 与 DomainU Kernel,最后是上层的应用管理进程。可以看出,hypervisor 与 管理进程是独立的,可以轻易安装,然而 Domain0 与 DomainU 却需要修改后的 Kernel 支持。早期的 Xen 的源代码库里有一个修改后的 2.6.18,可以下载,编译然后安装。

这个办法并不是长久之计,Linux Kernel 的发展是很快的,一劳永逸的办法是尽快将 Xen 对 Kernel 的修改 merge 到上游 Kernel 中去。然而 Xen 的开发人员似乎并不热衷于 merge 对 Kernel 的修改。带来的问题是,对发行版来说,他们不得不花大量的精力来维护 Xen 相关的 Kernel patch,这个问题在 RHEL 5 上达到了登峰造极式的表现,RHEL 5 的 Kernel SRPM 里与 Xen 相关的 patch 有上百个。此外一些早期投入到 Xen 中去的小公司在快速开始后,发现他们不得不面对一些 tricky 的问题,而且很难判断是 Xen hypervisor 的问题还是 Dom0 或 DomU kernel 的问题,或者有时候在 backport 一些最新的 Kernel bug 修复到 Dom0/DomU 时困难重重。Xen 成了这些人的梦魇。

其实 Xen 的开发人员也不是没想过将 Kernel 的修改 merge 到上游去,然而如上面所说,Xen 对 Kernel 的修改是通过类似将 Linux 移植到一个新平台(x86 的一个子平台)的方式进行的,其中有大量的对 x86 平台代码的修改与复制,对此 Kernel 开发人员非常抵触。再加上 Xen 的开发人员并不热衷于此,Xen 进入 Kernel 之路面临着一个个障碍,而且一拖就是几年。

在 KVM 逐渐开始成熟起来,而 Xen 又迟迟无法进入 Kernel 之际,Red Hat 做出了一个艰难的决定,抛弃 Xen,转投 KVM,并收购了最初开发 KVM 的公司。Red Hat 的决定带来了巨大的影响,不少发行版都抛弃了 Xen,很多人开始预言 Xen 即将灭亡,关于 Xen 与 KVM 之争也随处可见,Red Hat 与 始终支持 Xen 的 Novell 就两者的优劣还吵了一架。同时随着 KVM 的成熟,Xen 进入 Kernel 的阻力更大,许多 Kernel 开发人员认为没有必要在 Kernel 里同时支持两个虚拟化框架,有 KVM 就足够了。而有关推动 Xen 修改进入 Kernel 的努力再次失败,Linus 明确表示,Xen 的代码从开发角度来说就是一个混乱,这样的代码进入 Kernel 只会给 Kernel 开发人员带来维护上的灾难,除非 Xen 的开发人员按照 Kernel 的开发规范重写 Kernel 相关的功能,否则 Xen 的修改不可能进入 Kernel。

漫漫人生路

在 Linus 明确对 Xen 的问题表态后,Xen 的开发人员开始了漫漫的 Kernel 人生路。此前在 pv_ops 出现后,Xen 的 DomU 部分已经进入了 Kernel,唯有剩下的 Dom0 部分。Xen 的开发人员开始一点一点的重写,提交,被打回,再重新修改提交,经过了两年时间的积累,Xen 对 Kernel 的修改终于在去年 2.6.37 时有了一个质的变化,2.6.37 包含了核心的 Dom0 支持,当然这还不够,Dom0 还需要一些 backend driver 来支持从 DomU 过来的设备访问请求,不过这些相对来说已不那么困难,轻舟已过万重山。

Xen vs KVM

与此同时,在 Red Hat 加入 KVM 后,KVM 的发展也在日新月异。KVM 虽然利用了 CPU 的虚拟化功能,但对外围设备尤其是硬盘与网卡的虚拟还是通过 QEMU,这样的 Full Virtualization,效率并不高。为了提高效率,就要像 Xen 那样采用 Paravirtualization 的方式,即 guest 的 Kernel 知道自己访问的硬盘/网卡并不是真正的硬件设备,很快 Kernel 内部有了 VirtIO 的框架。

硬件的发展同样迅速,在第一代虚拟化技术催生了 KVM 这样的软件后,第二代的虚拟化技术致力于在性能上的提高,比如 Intel 的 EPT 以及 VT-d,AMD 的 RVI 以及 IO-MMU。在这些技术被 KVM 采用后(当然也被 Xen 采用),Xen 是否还具有天然的性能上的优势就真不好说了。相反,由于 Xen 的 Dom0 支持迟迟无法进入 Kernel,使得很多人在选择虚拟化技术时不得不三思。天平已然倾向了 KVM。

最后

到底 Xen 与 KVM 孰优孰劣,尤其是性能,不是一个轻易就可以下结论的问题。性能上的比试除了产品的架构外,更多依赖于任务本身是 CPU bound 还是 I/O bound,以及在测试过程中对搭建平台的一步步调整。不管怎么说,Xen 依然有存在的价值,Xen 也有大量的用户群。Xen 的 Kernel 部分正式进入 Linux Kernel 是一件值得高兴的事情,相信很多发行版将重新开始支持 Xen,至少在虚拟化技术前,我们又有了一个方便的选择。对于 Xen 来说,这也意味着它与 KVM 的竞争又站到了同一起跑线上。

纵观 Xen 这短短几年的发展,既有巅峰时的人人追捧,也有没落时的失意。除了虚拟化大环境技术上的变迁外,更主要的原因在于 Xen 的开发策略没有坚持通常所说的上游优先(upstream first),也就是在代码还没有进入上游的 Kernel 之前,就发布出去,从而为日后的弯路打下了基础。这样的教训令人足戒。

Xen 的 Dom0 虽然进入了 Kernel,但 Xen 的故事并未结束,Xen 仍然有一些代码需要进入 Kernel,Xen 本身对硬件虚拟化技术的利用也有待提高,不管怎么说,Xen 又重新回到了人们的视野,至于 Xen 是否还能回到巅峰,只能拭目以待了。

资源

严格来说,本文的很多术语并不完全准确,所以,如果您有兴趣,您可以延伸阅读:

  1. 我爱 Wikipedia: Virtualization, Hardware Virtualization, x86 Virtualization, Full Virtualization, Paravirtualization, VMWare, Xen, KVM
  2. Intel 的网站上有许多非常棒的关于 Virtualization 的文章,比如这一篇
  3. VMWare 的网站上有许多非常棒的关于 Virtualization 的文章,比如这一篇
  4. Xen 的著名的论文以及架构介绍
  5. KernelTrapKVM 的主要开发者的专访
  6. LWN 上 Virtualization 相关的文章 Xen: finishing the job, Xen again

Gnome 3 Shell 的使用感受

当 Fedora 15 发布之后,Unity 与 Gnome 3 Shell 的竞争就彻底摆上了台面。这两个从头设计的桌面系统在很多方面有着一定的相似性,毕竟她们在设计的过程中就互相借鉴,当然这种类型的界面也许就代表了未来桌面系统的发展趋势也未可知。不过我并不想在这里对两者进行比较,萝卜青菜,各有所爱。现在我只想吐吐几天来对 Gnome 3 Shell 的使用感受。

我对桌面系统的思考

在最近一段时间内先后使用了 Unity 与 Gnome 3 Shell 后,以及被各种相关评论轰炸过一段时间后,我对用户体验以及桌面系统设计也产生了兴趣,在乘地铁上下班的无聊的路上,我也会装 B 般的考虑一下,如果我是一个新的桌面系统的设计人员,我会怎么来设计一个用户体验超爽的界面呢。OK,我希望设计出一个像苹果的 iPhone 那样颠覆人们既有想法的界面,但同时我也应该考虑用户已有的使用习惯,我还需要考虑各种各样的设备。这样的冥想(或者说意淫)确实让我在拥挤的地铁里心情平静不再烦躁,但同时也令我在上班的时候恍若神游,更甚者晚上躺下以后也会激动莫名。因此,我必须把这几天的思考写下来,就此打住。

其实说到底,一个桌面系统的终极目的是什么,我认为,无非是下面这两点:

  • 更快捷的启动应用程序
  • 更方便的在应用间切换

其它的功能比如:workspace, indicator, notification 等等,固然重要,但并不会在本质上影响一个桌面系统的使用体验。

Gnome 3 Shell 的倒退

基于我对桌面系统的全新认识,不得不承认 Gnome 3 Shell 在许多方面存在着用户使用体验的倒退。当然,这里对 Gnome 3 Shell 的批评指的都是 Gnome 3 Shell 默认的配置,由于 Gnome 3 Shell 的高度可定制性,以下指出的这些地方,我都会介绍如何进行相应的定制或修改。

对 Gnome 3 Shell 进行修改或定制的方法主要是两种,通过 gnome-tweak-tool(或 gsettings 命令行,不推荐)修改 gnome-shell 的配置;通过安装扩展定制 gnome-shell 的行为。大部分修改都需要通过重启 gnome-shell 来生效(Alt+F2 后输入 r),也曾遇见过需要重新登录甚至重启才生效的情况。

[1] 顶部面板默认无法添加应用快捷方式

无论是 Unity 还是 Gnome 3 Shell,在顶部面板的空白处,单击右键都没有任何反应。因此,对 Gnome 3 Shell 来说,要想启动一个应用,你需要先进入 Oerview 界面,或者通过 Dash 上的快捷方式,或者通过搜索找到应用,才能启动。显然在这里,启动应用多了一个步骤(进入 Overview 界面),相对来说,至少 Unity 还可以通过一个不直观的方式将应用快捷方式添加到 Launcher 上。不过,Gnome 3 Shell 的 Activities 按钮的响应速度比 Unity 的 Launcher 可是快多了,两下也算打个平手。

对于 Gnome 3 Shell 的该行为,我们可以通过安装一个扩展,把 Dash 上的应用快捷方式也复制到顶部面板上。Ron Yorston 制作了几个模仿 Gnome 2 行为的扩展,我们这里要安装的是其中的 Favourites in panel 扩展。安装完该扩展后,仍然无法对顶部面板进行快捷方式定制,要想更换面板上的快捷方式,需要先更换 Dash 上的快捷方式,其自然会同步到面板上。

[2] 奇异的 Alt + Tab 与 Alt + ~

我始终不能理解 Gnome 3 Shell 这么做有什么意义,OK,把应用与应用窗口区分开从逻辑上看也许很清晰,但在实际使用中,除了让人手指崩溃以外,这种为了区分而区分的用户体验设计真是让人受不了。直到现在我仍然无法适应在 Alt+Tab 的切换过程中,停下来,按住 Alt 键不放,再换成 ~ 键继续切换,每次使用该组合,我都似乎感觉到手指痉挛。

可以通过安装扩展来禁掉该行为,gnome-shell 的标准扩展里有一个扩展是用来把 Alt+Tab 回复到旧的行为的,该 RPM 包的名字是 gnome-shell-extensions-alternate-tab。不过,在升级到最新的 gnome shell 3.0.2 版本后,该扩展有严重 bug,按下 Alt+Tab 键将导致 gnome-shell 崩溃。

[3] 消失的 Shutdown/Restart

最让人崩溃的是 Gnome 3 Shell 默认将 Shutdown/Restart 按钮隐藏了。唯一的解释也许是这个修改是专为手机,平板等设备设计的。第一次使用 Gnome 3 Shell 时,因为没有找到 Shutdown/Restart 按钮,我的内心甚至产生了一丝恐慌,莫非系统安装过程中有什么东西漏掉了。

gnome-shell 的标准扩展里有一个扩展可以让隐藏的 Poweroff 重见天日。RPM 包的名字是 gnome-shell-extensions-alternative-status-menu。

除了以上列出的这三个地方外,Gnome 3 Shell 还有许多地方行为怪异,不过我认为都可以通过强迫自己适应来接受。

丑陋的整体风格

[1] 粗糙的主题

我不是一个喜欢折腾主题的用户,但 Gnome 3 Shell 的默认主题 Adwiata 给人的感觉很粗糙,实在是有碍观瞻,好在已经有人为 Gnome 3 Shell 设计了不少主题,比如 deviantART 上就有许多不错的选择,你可以从这里挑一个。

安装主题最好的方法是通过 Gnome Tweak Tool,为此,你除了要安装该工具以外(RPM 包名称是 gnome-tweak-tool),还需要安装一个 RPM 包名称为 gnome-shell-extensions-user-theme 的扩展。

[2] 大图标与小字体

Gnome 3 Shell 与 Unity 有着一样的毛病,硕大无比的图标。好在使用 Unity 一段时间后,我已经对大图标不那么抵触,不过 Gnome 3 Shell 的字体都偏小,尤其是 Application picker 界面的字体。当使用搜索功能寻找某个应用时,那么小的字体,我得使劲咪起眼来看,还看不清。该字体无法通过 Universal Access 或者 Gnome-tweak-tool 来修改,而是在主题里定义的。

打开主题的 css 格式定义文件,如果是安装了自定义主题,该文件在 ~/.themes/XXX/gnome-shell/gnome-shell.css (XXX 为主题名),如果是系统默认的主题,该文件在 /usr/share/gnome-shell/theme/gnome-shell.css。将 .search-result-content > .overview-icon 下的 font-size 从 7.5pt 改为 0.9em。这样修改后,字体看起来就大多了。注意:如果使用的是系统默认的主题,并进行了修改,则当系统主题包升级时,将覆盖该修改。

与无聊的人共勉

进入 Gnome 3 Shell 后,一个最直观的与以往不一致的地方是,在桌面上点击右键无任何反应,并且顶部面板上没有一个 ‘显示桌面’ 的按钮;另一个则是应用窗口的最大化与最小化按钮默认被隐藏了。按照 Gnome 3 Shell 的设计,这两个变化都可以理解。对于最大化按钮来说,由于 Gnome 3 Shell 提供了通过拖动窗口到顶部面板的方式让窗口最大化,再加上已有的双击标题栏来最大化,所以最大化按钮默认就砍掉了。对于最小化按钮来说,由于 Gnome 3 Shell 取消了底部的任务栏,窗口最小化后,无法再通过任务栏上的最小化窗口还原,因此最小化按钮也就砍掉了。至于桌面,由于 Gnome 3 Shell 的这些诸多设计,桌面不再是一个保罗万象的东西,变成了纯粹的桌面,因此右键单击或显示桌面也就没有意义了。

客观来说,Gnome 3 Shell 的这些设计确实调理清晰,并且精简了界面,按理说应该非常优雅。可惜用户的使用体验从来不是以逻辑上的客观清晰来判断的。长久以来,对于那些具有深厚的 Windows 使用背景的用户来说,他们都有一个难以磨灭的使用习惯,那就是右键刷新(或曰寂寞大杀器,Windows 强迫症等等)。当然,Linux 下并没有刷新键,但这不妨碍无聊的人选择其它方式来释放内心的寂寞,比如右键更换桌面壁纸(现在你了解 Linux 下那些经常更换壁纸的人的内心的孤寂了吧)。然而 Gnome 3 Shell 的这些做法,又令那些无聊的人情何以堪啊!

可以通过 gnome-tweak-tool:1. 选择让 Nautilus 管理桌面来回复右键点击,2. 回复最小化与最大化按钮。但我没找到能够在顶部面板上安装 ‘显示桌面’ 按钮的扩展,只好通过在键盘设置里配置 ‘hide all windows’ 快捷键来满足我的要求了。

My Gnome 3 Shell Desktop Screenshot

结论

在一篇文章里把整个 Gnome 3 Shell 的功能评价一遍是不可能的,Gnome 3 Shell 包括了许多我非常喜爱的功能,但实在是懒得说了。提到 Gnome 3 Shell,一个不可避免的事情就是与 Unity 做比较。到底孰优孰劣呢,每个人有每个人的看法,从我的使用体验来说,Unity 更像是为终端用户(或者说非 Linux 用户)设计的,而 Gnome 3 Shell 则散发着浓厚的开发者气息(javascript, css),对开发人员来说,也许会更喜欢 Gnome 3 Shell,而对普通用户,也许 Unity 才是真正的选择。

最后,千万不要把一个无聊的人的使用经验太当回事,允许无聊的人以及这些无聊的吐槽的存在,这个世界才会多姿多彩,你懂得!

资源

  1. Gnome 3 Shell 站点里的介绍术语设计以及 cheat sheet,对于深入理解与掌握 Gnome 3 Shell 是必须的。
  2. fpmurphy 的博客里有许多关于定制 Gnome 3 Shell 以及编写扩展的好文章。
  3. fpmurphy 制作的扩展

Linux 2.6 走向尽头,这次看来是真的了

昨天晚上 Linus Torvalds 先生在一封发给 LKML 的关于缩短 2.6.40 merge window 的中,捎带提到了 Linux Kernel 接下来的版本号问题,这封原本是提醒 Kernel 开发人员不要在最后一刻才提交 pull 请求的邮件,因为 Linus 在信后附上的几句话(如下),很快成了大家讨论 Kernel 版本号的欢乐场所。

PS. The voices in my head also tell me that the numbers are getting too big. I may just call the thing 2.8.0. And I almost guarantee that this PS is going to result in more discussion than the rest, but when the voices tell me to do things, I listen.

目前负责着好几个 2.6 longterm 版本维护的 Greg K.H 首先对此表示了欢迎,显然,每天在几个冗长的版本号(4 个数字)之间来回捣腾肯定令人厌烦。Greg K.H 曾经在 08 年的 LKML 上提出过修改 Kernel 版本号的建议,当时他希望 Kernel 能够采用以”年.月”的格式作为版本号,不过该建议并没有被采纳。

更多的声音表示希望 Linus 能够让 2.6 走到 2.6.42 再考虑更换版本号,以此来表达对《银河系漫游指南》作者 Douglas Adams 的致敬,同时隐喻 Linux 将是解答人生,宇宙以及所有一切问题的终极答案(在该小说里,超级电脑 Deep Thought 经过 7½ 百万年的计算后,得出关于人生,宇宙以及所有一切这一终极问题的终极答案是 42)。

Linus 随后拒绝了该建议,他并没有给出拒绝的理由。Linus 只是表示 40 是一个不错的巡回数字(round number)。有人开玩笑 Linus 这么做不是为了避免该终极答案,而是避免终极问题(超级电脑 Deep Thought 虽然算出了终极答案,但是并不知道该终极问题)。

当然,Linus 并不完全是在开玩笑,Linus 在随后的两封回复(12)中详细谈到了他对切换版本号的思考。Linus 的建议是将版本号跳到 3.0,而不是 2.8.x 或其它,并且主版本号(mainline)将只包含 major 与 minor 号,也就是说下一个 Kernel 版本将有可能是 3.0,而不是 3.0.0。Linus 表示这样做有两个优点,一是去掉一个版本级数可以使得 stable 版本的维护人员直接使用第三个数字来标识相应的发布;其次由于传统上 Kernel 使用偶数版本号表示发布的版本,切换到 3.x 的版本号后,使用奇数(比如 3.0 之后是 3.1)作为发布的版本号也不会显得太让人难以接受。

大部分人对 Linus 的版本号建议表示同意,有人希望最好能在 ARM 相关的代码整理完毕之后再跳到 3.0,毕竟 3.0 总是让人感觉这是一个飞跃。此外也有人表示在 BKL 完全去除之后(刚刚发布的 2.6.39 彻底完成了这一目标),Linux Kernel 就可以称得上 3.0 了。Linus 表示当前的 Kernel 版本号基于的已不再是功能,而是时间,在 Linux 庆贺 20 周年,并即将走入第 3 个 10 年之际,改成 3.0 也是一个不错的借口。

目前虽然还没有更广泛的讨论,但已经有人在提交 pull 请求时使用 3.0,以希望该版本号的变更能够在下一个发布版本中完成,似乎 Linus 所说的很有可能成为现实。

在此之前,2.4 发布了 24 个版本后,跳到了 2.6。

[更新 20110525] 综合 LKML 以及 LWN 上的反馈,在版本号的建议上,仍然有许多人认为基于时间的版本号格式更好,比如下一个版本也许是 2011.07,不过 Linus 始终未对此表态。至于 3.0 的建议,许多人认为最好能够在完成某个大的改动(比如清除掉旧的硬件支持)后跳到 3.0 才符合人们的期待,不过 Linus 再次重申 Kernel 以后任何新版本的发布都不会再与某个改动相挂钩。

[更新 20110530] 就在半个小时之前,Linus 在动身前往日本参加 LinuxCon 会议之前,将他的 Kernel git tree 打上了 3.0-rc1 的 tag,至此,即将发布的下一个 Kernel 版本将成为 Linux 3.0,如下是 Linus 的 comment。

.. except there are various scripts that really know that there are three numbers, so it calls itself “3.0.0-rc1”.

Hopefully by the time the final 3.0 is out, we’ll have that extra zero all figured out.

Fedora 15 的新的网卡命名方式

在前天通过了最后的 Go/No-Go 会议后,Fedora 15 已定于下周正式发布,这个永远站在开源技术最前沿的发行版,即将迎来又一个新的大的变动。作为 Fedora 用户,相信你已经习惯了每个新版本的新功能所带来的种种不适,Fedora 15 也不会令你意外,它所采用的新的网卡命名方式 Consistent Network Device Naming (CNDN) 就是其中的一个。

比起 Fedora 15 的其它新功能,比如众所周知的 Gnome 3,CNDN 并不是一个多大的改动,但它所引起的争论以及带来的影响却不小,看看 NetworkWorldLWN 以及 Slashdot 上铺天盖地的争论,质疑甚至嘲讽你就知道了。当然这并不表示 CNDN 这个新功能就完全一无是处,或者是画蛇添足。CNDN 还是有一定的益处,前提是你深入理解了作者的意图。现在让我们来仔细看看这个即将影响你日常 Linux 生活的新的网卡命名方式。

具体的命名方式之前已经介绍过,这里不再赘述,需要说明一点的是这个命名方式依赖于机器的 BIOS,如果你的机器比较旧,那么安装后的 Fedora 15 仍然会采用 ethX 的命名规则,并且新的命名规则只适用于有线网卡。

我在 4 个不同的环境中安装了 Fedora 15,得到了不同的结果。在一台 Dell 台式机上,网卡被命名为 em1;在一台稍旧的 hp 笔记本上,网卡仍然命名为 eth0;在 VirtualBox 4.0 虚拟机里,两块虚拟网卡分别被命名为 p2p1 与 p7p1;在 KVM 虚拟机里,网卡仍然被命名为 eth0。现在你是不是觉得这个命名方式应该叫 Inconsistent,而不是 Consistent。

当然任何处于过渡期的新功能都不可避免的会有这样不一致的现象,CNDN 这里的 Consistent 指的也并不是不同的硬件类型都 Consistent,而是在 BIOS 支持的机器上,网卡的命名将一致,最主要的命名规则是两条,主板内置的网卡将变成 emX,外插到 PCI 卡槽上的网卡将变成 pXpX,所有的数字都从 1 开始。

除了名字的字符串前缀与以前不一样以外(这一点你将在接下来的 Linux 生活中不断去适应,如果你习惯于使用 ifconfig, tcpdump 等命令行工具的话),更重要的是网卡的命名依据也将改变,网卡的名字不再与网卡的 MAC 地址绑定,而是依据网卡在主板上的位置。

在 CNDN 出现之前,可以说网卡的命名经历了几个阶段,不过在讲述这些命名方式之前,需要先指出的一点是 Kernel 内部对网卡的命名方式一直未变,那就是先探测到的网卡被命名为 eth0,其次是 eth1 等等,当然 Kernel 探测网卡的顺序结果也不是一成不变的,如果相关的硬件发生了变化的话,Kernel 探测的顺序结果也可能会改变,不过硬件不变(或者硬件型号不变)的情况下,探测顺序通常是不变的。

在 udev 出现之前(RHEL 4 或更早?),RedHat 系列对网卡的命名是简单的,在 /etc/modprobe.conf 文件里会有类似下面这样的配置:

alias eth0 e100
alias eth1 3c59x
...

modprobe.conf 配置文件并不是对网卡的名字进行修改,而是控制 Kernel 加载网卡驱动的顺序,当然也就控制了 Kernel 探测网卡的顺序,从而变相固定了网卡的名字。你可以看出这种方法只对那些不同网卡属于不同驱动的硬件配置有效,如果一个驱动对应多块网卡,则顺序就无法这样确定了。如果 Kernel 某次启动改变了某块网卡的探测顺序,那么 RHEL 系统是否会对该网卡进行重命名,从而可以继续使用之前的网卡配置文件,我已经记不清了。

到了 RHEL 5 系列之后,udev 已经引入,此时主流的发行版都会根据网卡的 MAC 地址使用 udev 对网卡设备进行命名以确保网卡的名字不会变动。举例来说,假设一个系统有两块网卡 eth0 与 eth1,如果将两块网卡调各个,系统启动后,虽然 Kernel 内部对网卡的命名发生了改变(这里假设两块网卡由相同驱动加载),即原来的 eth0 变成了 eth1,eth1 变成了 eth0,但 /etc/udev/rules.d/60-net.rules 文件会调用 /lib/udev/rename_device 对网卡进行命名,而 rename_device 会搜索系统里的网卡配置文件 /etc/sysconfig/network-scripts/ifcfg-eth*,如果该网卡的 MAC 地址匹配到某个配置文件中的 HWADDR 项,就将网卡命名为该文件中的 DEVICE 项。此时用 ip addr 命令查看,你会发现每块网卡仍然使用与之前相同的名字,并且 IP 地址也与之前相同,只不过 ip addr 命令的输出会将 eth1列在前面,因为 eth1 在 Kernel 内部对应的名字是 eth0。

当然这样的情况并不常见,没有人会闲得蛋疼去交换两块网卡玩。常见的与网卡名字相关的情况是替换其中的某块网卡,比如将某个虚拟机映像拷到另一个虚拟机 Host 里,此时就相当于将网卡替换。在 RHEL 5 里,系统启动后,你会发现替换后的网卡名字未变,但网卡配置变成了 DHCP,原来的配置文件自动保存为 /etc/sysconfig/network-scripts/ifcfg-ethX.bak。名字未变是因为 rename_device 未找到 MAC 地址对应的配置文件,所以保留该名字与 Kernel 里的名字一致(当然这里还有一个前提是网卡的型号未变,并且 Kernel 的探测顺序也未变,大部分情况都是这样的),至于配置文件改变则是 Kudzu 造成的,Kudzu 会检测硬件,并对新添加的网卡默认生成 DHCP 配置,当然这里与网卡命名无关了。

到了 RHEL 6 后,仍然使用 udev 对网卡进行命名,所以原理上对网卡进行命名的方式未变,不过 udev 的规则有了一些改变,变成了 /etc/udev/rules.d/70-persistent-net.rules。该文件由 /lib/udev/write_net_rules 在每次启动时生成,里面的内容与如下类似:

SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="52:54:00:be:19:20", ATTR{type}=="1", KERNEL=="eth*", NAME="eth0"

也就是说不管 Kernel 启动时检测到的网卡顺序如何,udev 都会按照该文件里 MAC 地址对应的名字对网卡进行命名。如果某块网卡被替换了,那么在启动时 /lib/udev/write_net_rules 会更新该 70-persistent-net.rules 文件,更新的方式并不是将该行里的 MAC 地址修改为新网卡的 MAC 地址,而是在该文件后面附上新的一行,并生成一个新的网卡名字,此时用 ip 命令查看,你会发现网卡名字变成了 eth1。

现在我们来看看 CNDN 是怎么命名的,CNDN 的原理其实很简单,它仍然采用了 udev 的机制,只不过将 70-persistent-net.rules 文件去掉,换成了 /lib/udev/rules.d/71-biosdevname.rules 文件,71-biosdevname.rules 比起 70-persistent-net.rules 文件要稍微复杂点,但也无非就是调用 biosdevname,并根据该命令的输出对网卡进行相应的命名。biosdevname 内部则调用了 dmidecode 等 BIOS 工具来获取 PCI 号等 BIOS 信息,从而为网卡取一个有意义(但愿如此)的名字,比如 em1 表示网卡内置在主板上,p2p1 表示 PCI 第二个插槽的网卡上的第一个口,还有其它更特殊的命名方式等等。

CNDN 对于那些每天与包含 N 个网卡设备打交道的人来说是一个福音,这会减轻他们在辨别网卡名字所对应的物理网卡时的痛苦。但对于普通用户来说,即使你某天心血来潮买了个包含四块网卡的 Dell 服务器,即使你知道 em1 对应的是第一块内置网卡,你也很难从服务器的后面板上区分出哪块网卡是第一块内置网卡。

不过由于 CNDN 的命名方式不再将网卡名字与 MAC 地址绑定,那么上面所说的那种替换网卡的问题也许会得到解决,即如果将某块网卡替换,那么系统在包含新的网卡启动之后,应该不需要做任何网卡配置上的改动,就能保持与之前网卡相同的网络设置。不过目前这一点还做不到,因为 /etc/sysconfig/network-scripts/ifcfg-ethX 里的 HWADDR 选项还在。不清楚 Fedora 15 或者后继版本是否会将 HWADDR 从生成的网卡配置中去掉,或者说这种需求根本就不是一个正常的需求?

最后,如果你无法忍受这个新的憋八的命名方式,你仍然可以回退到之前的命名方式。在 Kernel 的启动参数里加上 biosdevname=0 ,系统在下次启动时会为你生成熟悉的 70-persistent-net.rules 文件,由于该文件在 /etc/udev/rules.d 下,所以优先级要比在 /lib/udev/rules.d/ 下的 71-biosdevname.rules 文件高。如果哪天你又想再回来试试该命名方式,只需将 Kernel 的 biosdevname 参数置成 1,同时不要忘了将 70-persistent-net.rules 文件删掉。

似是而非的 Linux 世界: SSH

日前游历江南途中,路过苏州一酒店,其外观从远处看与周围建筑并无二致,唯侧面黄色招牌上 SSH 三个英文字母赫赫发光,令出租车中的我猛然一怔。车行至酒店正面,我仔细观瞧了一下酒店的字号,一笔草书书就的”香雪海”三个大字遒劲有力。不过 SSH 这个简写是如何从”香雪海”简化得来的却令我费解,按照一般的情况来说,这个饭店的字母简写应该是 XXH (XiangXueHai),莫非吴侬软语将 Xiang 发音成 Siang,正在百思不得其解的时候,出租车到了下榻的旅馆,旅途的劳顿使我很快沉沉睡去。

突然一阵悦耳的声音将我惊醒,睁眼看来,一位江南少女身着绫罗绸缎正凝眸向我,容华绝代,笑容可掬。我正思量身处何方时,少女轻启朱唇:客官醒来的正是时候,且随我来用膳。嗯,这么一说我还真有点饿,在少女的带领下,我来到一处亭楼,四周被梅花包围,一眼望去,如海荡漾,若雪满地。少女告诉我这正是我将要用膳的包间,我还没来得及问少女要菜单,已经有婀娜窈窕的服务员将菜端了上来,定睛看来,苏帮菜跟苏州的美景一样别致。我正欲举箸,少女止住我,客官莫急。不知何时少女手中多了一个古色古香的键盘,桌面上也多了一个显示屏,少女告诉我,她正在为我这次用膳生成一个临时用户与密码,并且每上一个新菜,我都得输入该密码才能享用。

我问少女如此繁琐为何,少女向我娓娓道来。原来该少女正是香雪海饭店的服侍人员,她道如今的朝廷昏庸无道,连民以食为天的食品安全问题也置若罔闻,以致世风日下,人心不古,各酒馆饭肆为了蝇头小利而纷纷在酒菜中偷梁换柱,弄得食客怨声载道,致使酒店生意每况愈下。为了让食客放心,该酒店不得不将所有的餐饮服务通过 SSH 加密隧道来提供,这样做相信可以令食客对食品的安全放心,当然也由此带来了不方便。我问为何不用公私钥的认证方式,即安全,更方便。少女告诉我,酒店也提供了该认证方式,不过只是针对高级会员,因为客官只是观光路过此地,所以并未向您提起。我当即表示我希望就此办理一个高级会员。少女莞尔一笑,告诉我办理会员需要一台便携电脑,需要安装 Linux 系统,需要用 ssh-keygen 生成公私钥,需要用 ssh-copy-id 办理登记,等等。似乎少女认为微醺醺的我根本无法记住这么多繁琐的步骤。

我告诉少女我都有,并赶紧去我的包里找我的笔记本电脑,却无论如何也找不到,回头一想,是啊,这次出门就没带啊,正在懊悔之际,一觉醒来,我仍然在宾馆暗淡的房间里,天色已然黑去。我向窗口望去,外面灯火阑珊,佳人却已杳,徒留衣香鬓影,令人怅然若失。

后记: 因为当时在出租车中行色匆忙,并未拍下该饭店及华丽丽的 SSH 三个大字。回来后从网上搜得该饭店的英文名称为 Snowy Sea Hotel。

Snowy Sea Hotel

拆除 fork 炸弹

在计算机世界里,提到安全,通常人们会首先想到病毒,蠕虫,以及各种网络攻击,在这个领域里,Unix/Linux 系统被证明更能抵御这种攻击。但另一方面,Unix/Linux 系统却也很容易遭受来自内部用户的攻击,fork 炸弹 (fork bomb) 就是其中的一种 (比较自虐的一种)。

fork 炸弹的原理很简单,某个进程通过恶意的 (或者因为编程失误) 疯狂调用 fork() 在短时间内生成了大量的进程,从而将操作系统的进程表填满,在这种情况下,系统将无法生成任何新的进程,除非某个进程退出。当然即使有进程退出,空出来的名额也很可能被 fork 炸弹的子进程所占据。此外,系统的 CPU 与内存等资源也被 fork 炸弹的各个进程所耗尽,事实上整个系统将因此变得巨慢无比而无法响应。

一个经典的 fork 炸弹示例是如下这样一行简洁但晦涩的 Shell 代码:

: () { : | : & }; :

理解这行代码的关键是其中的冒号,它在这里并没有什么特殊含义,只是一个函数名称标识符,将冒号换个名字会更容易理解,比如:

forkbomb () { forkbomb | forkbomb & }; forkbomb

可以看出这行代码一旦运行起来,将按指数级速度不停生成新进程,在我的 Debian 测试机器上,运行这行代码瞬间就能让系统毫无响应。

fork 炸弹的问题在于,当它运行起来以后,你几乎无法铲除它。杀死 fork 炸弹进程似乎是唯一的选择,但通过手工 kill 来杀死一个个 fork 炸弹进程是不现实的,即使为此专门写一个程序,这个程序也很难抓住最初的父进程,或者跟上后续生成的进程,因为该程序运行的时候,这些父进程已经退出,而子进程将被 init 接管。此外,运行这个专门的程序意味着启动一个新进程,在 fork 炸弹已启动的情况下,这几乎无法实现。因此不难想像,拆除 fork 炸弹的最有效办法就是拔下电源线。

也因此通常针对 fork 炸弹的办法都是提前阻止它,简单说就是通过调用 ulimit -u 或者配置 /etc/security/limits.conf 来限制用户所能生成的最大进程数,当进程数达到这个最大值后,任何新的生成进程的尝试都将失败。就上面所列举的 Shell 脚本例子来说,因为该脚本只是简单的 fork 进程,并且 fork 完后就退出,在遇到最大进程数后,这些 fork 炸弹进程将无法完成 fork 而退出,最终系统会回复正常。比如我的 Debian 测试机器的非 root 用户的 ulimit -u 值默认是 2047,运行 fork 炸弹后,系统有很长一段时间无法响应,但随着 fork 炸弹进程填满最大值后,系统就逐渐回复了。这个方法面临的一个问题是如何设置一个恰到好处的最大值,使得即不影响用户的正常使用,同时又能在几个恶意用户同时引爆 fork 炸弹时,还能给管理员留下一定的响应时间来完成一些处理工作。

LWN 的编辑 Jonathan Corbet 先生最近写了一篇文章,介绍了一种新的可能的拆除 fork 炸弹的方法。这个方法是由 Kernel 开发人员 Hiroyuki Kamezawa 提出来的。Kamezawa 的 patch 在 Kernel 里添加了一个新的进程跟踪结构,该结构是一个简单的树结构,记录了系统中的进程谱系。该结构与已存在的进程数据结构主要的区别在于即使某个进程退出了,该进程的相关信息仍然存在于该树中,因此 Kernel 可以据此判断系统是不是正在遭受 fork 炸弹的袭击。

一个直观的感受是该方法将使 Kernel 增加额外的存储开销,因此何时将旧的无用的进程信息清掉是关键的一点,该 patch 默认每30秒 Kernel 将进行一次检测,以判断当前是否正在遭受 fork 炸弹攻击。如果没有检测到攻击,那么已经存在了30秒以上的历史记录将被清除。

那么 Kernel 将如何判断系统正在遭受 fork 炸弹攻击呢,通常 fork 炸弹会耗尽系统的内存,因此该代码检测内存的使用情况,如果自上一次检测后,有内存分配失败发生或 kswapd 有运行,那么这将是一个标志。此外该代码还检测系统中的进程数是否自上次检测后增加了。如果所有这些检测都显示不出有什么异常,Kernel 将清除旧的历史数据。如果显示系统有内存分配困难,或者进程数持续增加,历史数据将保存下来。

当系统遭受 fork 炸弹攻击而导致内存不足时,Kernel 首先会调用 OOM killer 来尝试找出有问题的进程,但在这种情况下,OOM killer 很难意识到这不是一个进程,而是一整棵 fork 炸弹进程树。于是 OOM killer 将调用一个新的 fork bomb killer,fork bomb killer 将以深度优先的方式遍历该进程历史树,并尝试算出树中每个节点的后代子进程数,以及所有后代子进程们所使用的内存。然后计算出得分最高的进程,如果该进程叉下有10个以上的子进程,那么该进程将被认为是 fork 炸弹进程,该进程以及所有的子进程及其后代都将被杀死,于是 fork 炸弹拆除了!

Kamezawa 的 patch 还通过 /sys/kernel/mm/oom 提供了相应的用户接口。比如只有当 mm_tracking_enabled 设置为 “enabled” (默认值) 时,该功能才启用,mm_tracking_reset_interval_msecs 则用来控制多长时间清一次进程历史树。不过一个很重要的值却没有提供接口,即判断 fork 炸弹的进程数目,该值在代码中写死为10,这个数令人担心有点小。

该 patch 看起来似乎不错,并且没有多大副作用,不过在 linux-mm 的邮件列表里并不太受欢迎,评论者除了担心可能增加的 Kernel 运行时开销外,还认为 fork 炸弹问题通过用户空间的工具来解决会更好。Kamezawa 似乎也就此放弃了进一步去推动该 patch 进入 Kernel 的努力,他表示,”To go to other buildings to press reset-button is good for my health”。

引用与致谢

Unladen Swallow 的失败与教训

在淡出人们的视线近一年之后,Unladen Swallow 这个曾经被 Python 用户寄予厚望,号称要将 Python 性能提高5倍的 Python 分支,最近似乎真的走到了尽头。来自 Unladen Swallow 的开发人员 Reid Kleckner 前一段时间在他的博客上对 Unladen Swallow 做了一个回顾,这篇日志虽然没有正式宣布 Unladen Swallow 的死亡 (PyCon 2011 也只是将 Unladen Swallow 定义为处于休息状态),但却像墓志铭一样将 Unladen Swallow 的消逝记录了下来,仔细读来,颇多感慨,也令人收获良多。

曾经的希望

Unladen Swallow 是在2009年初开始进入 Python 用户视线的,它的出现一度引起了很多人的兴奋。

一是因为它诱人的目标,这个由来自 Google 的几个工程师发起的开源项目旨在大幅提高 Python 的性能 (5x),同时能还保持 Python 用户代码的兼容;

二是因为它的背景,Unladen Swallow 的开发人员都来自 Google,虽然他们宣称这个项目并不属于 Google 所有 (Google 只是赞助这些开发人员全职工作在该项目上),但众所周知 Python 是 Google 的官方语言之一,Google 内部使用了大量的 Python 代码,Google 本身也雇佣了大量的 Python 高手,其中就包括 Python 的发明者 Guido van Rossum 先生,这样的背景很难不让人对它的前景产生期望;

三是因为 Unladen Swallow 所选择的实现方法,Unladen Swallow 并不是从头去实现 Python,而是在 Python 2.6.1 上开出了一个新的分支,并将重点放在了那些 CPython 的性能瓶颈上,至于其它功能比如运行时库,内置函数,C 扩展等等则仍然使用 CPython 已有的代码。对于如何提高 Python 代码的执行速度,Unladen Swallow 的开发人员最主要的想法是为 CPython 添加一个基于 LLVM 的 JIT 编译器。由于 Unladen Swallow 的开发人员同时也是 CPython 的核心开发人员,再加上他们所选择的实现方法,使得 Unladen Swallow 的成果很有可能移植到 CPython 上,从而惠及所有 Python 用户。

不错的开始

Unladen Swallow 以一个季度为开发周期,在2009年一共发布了3个版本。

第一个版本 2009Q1 并没有对 CPython 做大的改动,只是一些小的性能相关的修订,根据发布说明给出的性能测试数据,2009Q1 比起 Python 2.6.1 有大约 25% ~ 35% 的性能提升。

第二个版本 2009Q2 引入了 LLVM 支持。开发人员声明该版本的目的主要是让 Python 能够在 LLVM 基础上工作,并为以后的性能优化打下基础。2009Q2 是令人兴奋的一个版本,因为通过引入 LLVM JIT 支持,所有的 Python 代码都可以编译成原生的机器码  (JIT可以动态编译那些调用次数多的函数,或使用-j always 选项强制编译所有代码),此外性能也比 2009Q1 提高了10%左右。美中不足的是内存的使用量增大了10倍。

第三个版本 2009Q3 原本是在 2009Q2 的基础上开始优化性能,但在遇到几个 LLVM 的问题后,开发人员只能暂时将这个计划搁在一边,不过该版本仍然令人兴奋,尤其是内存使用情况比起 2009Q2 来减少了930% (一个很令人困惑的数字,原因是 Google 采用了一个与众不同的百分比计算方法,即 (new – old) / new,而不是通常的 (new – old) / old)。

嘎然而止

在取得了不错的开始以及相当的关注后,备受期待的  2009Q4 版本,却迟迟没有推出。从2010年中开始,就不停的有人问 Unladen Swallow 是否已经死了。而直到现在,才一切真相大白。坦白的说,最根本的原因是因为开发人员无法实现当初为 Unladen Swallow 制定的目标 (将性能提升5倍)。这其中,又有各种不同的因素。

一是缺乏 Google 的支持,Unladen Swallow 虽然是开源社区共同参与的项目,但项目的主要开发人员却是在 Google 的赞助下,全职工作在该项目上,Google 希望通过提高 Python 的性能来优化内部使用的 Python 代码。然而在真正需要性能优先的地方,Google 仍然使用 Java 或 C++。Google 内部的其他 Python 开发人员,即使对 Unladen Swallow 感兴趣,在部署它时也面临着困难。去年11月份,两位核心的全职开发人员更是被 Google 调到了别的项目上,事实上宣布了该项目的结束。

二是缺乏社区的支持,在 Unladen Swallow 的开发上,主要以 Google 的工程师为主,缺乏其它主要社区人员;在 python-dev 上关于将 Unladen Swallow 合并到 Py3k 的反馈也颇多疑虑;此外,另一个与 Unladen Swallow 相像的项目 PyPy 则开始受到更多的关注,并成为 Python 社区的焦点,Unladen Swallow 的那些雄伟目标似乎已不再重要。

三是技术上的缺陷,Reid Kleckner 表示 LLVM 虽然提供了一个很好的编译器框架,但更像是为类似 C/C++ 这样静态编译的语言设计的,比如 LLVM 项目的亮点 optimization 就是专为 C/C++ 这样的语言编译生成的 IR 所设计的。此外 LLVM 本身也有不少限制。

当然关于 LLVM 是否真的不适合类似 Python 这样的动态语言还有争议,不过一个不争的事实是 LLVM 本身的优化大都工作在最底层,LLVM 不会去试图理解 Python 语言里那些动态特性,要想利用 LLVM,必须要先手工完成一些更高层级的 Python 的语言分析与优化。

Unladen Swallow 的遗产

Unladen Swallow 虽然失败了,但仍然留下了许多宝贵的遗产。Reid Kleckner 表示 Unladen Swallow 的性能测试工具包是其中最有价值的一个,现在被 PyPy 用于自己的性能测试。

Unladen Swallow 的开发人员还做了大量的针对 LLVM 的工作,比如为 LLVM 的 JIT 添加了 GDB 接口等等,这些工作大部分也都集成到了 LLVM 中去。

此外,某些 Unladen Swallow 已完成的性能优化工作,比如针对 pickle 模块的优化 (1, 2),也集成到了 Python 3.2 中。

下一站

提起 Unladen Swallow,就不得不提另一个项目 PyPy。PyPy 不是一个单纯的 Python 实现 (更像是一个编译器框架),它的主要目的也不是为了提高 Python 的性能 (性能只是 PyPy  众多优先级里的一个),但 PyPy 在自身发展的过程中,却正在实现 Unladen Swallow 当初所提出的宏伟目标,一个快速的 Python 实现。

理所当然的,在 Unladen Swallow 掩旗息鼓之后,快速发展的 PyPy 成为了 Python 社区的下一个热点。事实上,Unladen Swallow 的开发人员在当时并不是没有注意到 PyPy,只不过那时 PyPy 仍然有许多不足,比如速度慢,只支持 x86,不支持 C 扩展与 SWIG 等等。Unladen Swallow 的核心开发人员 Collin Winter 在谈到当时的选择时表示,如果选择 PyPy,那么将面临着许多非性能优化的工作,比如添加x86-64支持,添加C扩展以及SWIG支持等等,这些工作量将耗费他们很大的精力,而选择 LLVM 这条道则相对要容易的多,此外 LLVM 在当时比 PyPy 也稳定的多,至少苹果曾将 LLVM 的 JIT 用于自己的产品当中。

然而技术上的不确定性,却往往很容易的将当初的选择以及曾经的美好愿望绊倒,Unladen Swallow 的开发人员在利用 LLVM 迅速开始后,很快就遇到了问题。他们不得不花时间来解决 LLVM 自身的一些 bug,以及调整那些没用的功能。于此同时,PyPy 也在迅速的发展,他们添加了x86_64支持,添加了C扩展的支持,在性能上也有了长足的进步。这样的对比使得连 Unladen Swallow 的开发人员也对自己的项目失去了兴趣。

Unladen Swallow 开发人员的努力并不会因为项目的失败而白白流失,除了那些可见的成果外,他们还为我们留下了丰富的教训,有关开源项目的发展,有关技术上的抉择,有关具体的编译器开发的技术等等,至少我们希望后来者不再犯同样的错误,我们希望有一天 Python 真的能在性能上完成蜕变。