成为本地安全专家实际上并不是什么困难的事情,只要你可以对网络资源进行加密操作,指出安全政策改革的来源,并象TechRepublic社区要求的那样撰写安全方面的文章。换句话说,只要用心去写了,没有语法和拼写错误就可以了。对我来说,这并不是什么困难的事情。你只要五个步骤就可以完成这样的工作。
1.从习惯的领域跳出来。使用你不熟悉的软件。了解新技术。我的意思并不是要你尝试不同的防病毒解决方案,而是你应该使用一些根本的不同软件。
如果你是一名微软认证系统工程师的话,就不应该管理活动目录域,而是利用Linux和FreeBSD操作系统建立家庭网络。如果你是一个操作系统发烧友,在家里有Linux、Windows和MacOS X等多台机器,甚至还有古老的BeOS或者Amiga操作系统的话,就应该建立一个备份服务器以及一个自动记录服务器,并且从零开始建立防火墙与路由器。
我已经做了大量这样的工作。作为学习一个新操作系统的挑战,我关注于Plan 9 分布式操作系统。正如我刚才说,如果你走出熟悉的地带,并且了解了不同的技术,就可以开始学习新的东西,已经使用了的新技术,并且会加深对系统的理解。
2.学习一些编程技术。即使只是一点点的编程技术也可以帮助你了解更多的关于如何在整个软件体系结构的系统安全中扮演重要角色。只要多一点点就会教你更多地了解它。
当你了解了如何为某一特定的操作系统编写驱动程序,就可以发现这个操作系统的安全缺陷。当你了解了如何编写文件系统的代码,就可以知道文件系统的设计和操作系统的特权分离事宜,这样的话,就可以拥有自己的观点。
3.大量地阅读。首先,加入邮件列表。合适的列表包括,开放源代码社区、编程技术社区以及信息安全研究社区提供的内容。
作为安全学习的原则。配合安全领域相关的提问,从世界上最有影响力的黑客电子邮件组BugTraq你可以获得安全方面的最新消息。此外,你也可以阅读象布鲁斯·施奈尔一类专家的安全报告。
经常保持阅读和安全有关的好书。安全类的书籍可以让你了解不少有关安全的原理,帮助你更快得成长。
4.经常检查你的设定。加密不等于安全,你不能一直确认防伪和加密会让系统变得更安全。我不是要你忽略知道的一切,而是应该仔细再三的检查,并对设定进行思考和分析。
5.最后,了解自身的实际情况。不要在别人告诉你应该如何进行安全操作的时间,就无条件相信。你应该仔细考虑,慢慢分析,如果存在多种可能性的话,确认它适合实际的情况。思考的时间,要注意,什么是在他告诉你的时间可能没提到的,它的来源是哪里。每个人都有自己的目标,你要考虑自己的目标和其他的有什么不同,避免可能存在的陷阱。
额外奉送一个建议,第六个步骤就是把我的blog加入你的列表。在不久的将来我将在TechRepublic社区信息安全的blog里提供更多的可以帮助你成为安全大师的资料。
2008年8月28日星期四
2008年8月16日星期六
UTF-8
目录·历史
·描述
·UTF-8的衍生物
·设计UTF-8的理由
·过长的资料排列(overlong forms)、输入无效及保安的考虑
·优点及缺点
·使用UTF-8的原因
·UTF-8的编码方式
·UTF-8的特性
·UTF-8编码的缺点
UTF-8(8 位元 Universal Character Set/Unicode Transformation Format)是针对Unicode 的一种可变长度字符编码。它可以用来表示 Unicode 标准中的任何字符,而且其编码中的第一个字节仍与 ASCII 相容,使得原来处理 ASCII 字符的软件无需或只作少部份修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他储存或传送文字的应用中,优先采用的编码。
UTF-8 使用一至四个字节为每个字符编码:
128 个 US-ASCII 字符只需一个字节编码(Unicode 范围由 U+0000 至 U+007F)。
带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码(Unicode 范围由 U+0080 至 U+07FF)。
其他基本多文种平面(BMP)中的字符(这包含了大部分常用字)使用三个字节编码。
其他极少使用的 Unicode 辅助平面的字符使用四字节编码。
对上述提及的第四种字符而言,UTF-8 使用四个字节来编码似乎太耗费资源了。但 UTF-8 对所有常用的字符都可以用三个字节表示,而且它的另一种选择,UTF-16编码,对前述的第四种字符同样需要四个字节来编码,所以要决定 UTF-8 或 UTF-16 哪种编码比较有效率,还要视所使用的字符的分布范围而定。不过,如果使用一些传统的压缩系统,比如 DEFLATE,则这些不同编码系统间的的差异就变得微不足道了。若顾及传统压缩算法在压缩较短文字上的效果不大,可以考虑使用 Standard Compression Scheme for Unicode(SCSU)。
因特网工程工作小组(IETF)要求所有因特网协议都必须支援 UTF-8 编码。[1] 互联网邮件联盟(IMC)建议所有电子邮件软件都支援 UTF-8编码。所有主要的电子邮件软件中,只有 Eudora 不支援 UTF-8 编码。[1]
历史
1992年初,为建立良好的字节串编码系统(byte-stream encoding)以供多字节字符集(multi-byte character sets)使用,开始了一个正式的研究。ISO/IEC 10646的初稿中有一个非必须的附录,名为UTF。当中包含了一个供32位元的字符使用的字节串编码系统。这个编码方式的性能并不令人满意,但它提出了将0-127的范围保留给ASCII以相容旧系统的概念。
1992年7月,X/Open委员会XoJIG开始寻求一个较佳的编码系统。UNIX 系统实验室(UNIX System Laboratories, USL)的Dave Prosser为此提出了一个编码系统的建议。它具备可更快速实作的特性,并引入一项新的改进。其中,7位元的ASCII符号只代表原来的意思,所有多字节序列则会包含第8位元的符号,也就是所谓的最高有效位元。
1992年8月,这个建议由IBMX/Open的代表流传到一些感兴趣的团体。与此同时,贝尔实验室Plan 9操作系统工作小组的肯·汤普逊对这编码系统作出重大的修改,让编码可以自我同步(self-synchronizing),使得不必从字串的开首读取,也能找出字符间的分界。1992年9月2日,汤普逊和Pike一起在美国新泽西州一架餐车的餐桌垫上描绘出此设计的要点。接下来的日子,Pike及汤普逊将它实现,并将这编码系统完全应用在Plan 9当中,及后他将有关成果回馈X/Open。
1993年1月25-29日的在圣地牙哥举行的USENIX会议首次正式介绍UTF-8。
自1996年起,微软的CAB(MS Cabinet)规格在UTF-8标准正式落实前就明确容许在任何地方使用UTF-8编码系统。但有关的编码器实际上从来没有实作这方面的规格。
描述
目前有好几份关于UTF-8详细规格的文件,但这些文件在定义上有些许的不同:
RFC 3629 / STD 63(2003),这份文件制定了UTF-8是标准的因特网协议元素
第四版,The Unicode Standard,§3.9-§3.10(2003)
ISO/IEC 10646-1:2000附加文件D(2000)
它们取代了以下那些被淘汰的定义:
ISO/IEC 10646-1:1993修正案2/附加文件R(1996)
第二版,The Unicode Standard,附录A(1996)
RFC 2044(1996)
RFC 2279(1998)
第三版,The Unicode Standard,§2.3(2000)及勘误表#1:UTF-8 Shortest Form(2000)
Unicode Standard 附加文件#27: Unicode 3.1(2001)
事实上,所有定义的基本原理都是相同的,它们之间最主要的不同是支援的字符范围及无效输入的处理方法。
Unicode字符的位元被分割为数个部分,并分配到UTF-8的字节串中较低的位元的位置。在U+0080的以下字符都使用内含其字符的单字节编码。这些编码正好对应7位元的ASCII字符。在其他情况,有可能需要多达4个字符组来表示一个字符。这些多字节的最高有效位元会设定成1,以防止与7位元的ASCII字符混淆,并保持标准的字节主导字串(standard byte-oriented string)运作顺利。
Unicode在范围D800-DFFF中不存在任何字符,基本多文种平面中约定了这个范围用于UTF-16扩展标识辅助平面(两个UTF-16表示一个辅助平面字符). 当然,任何编码都是可以被转换到这个范围,但在unicode中他们并不代表任何合法的值
例如,希伯来语字母 aleph (?)的Unicode代码是 U+05D0,按照以下方法改成 UTF-8:
它属于 U+0080到U+07FF区域,这个表说明它使用双字节, 110yyyyy 10zzzzzz.
十六进制 的 0x05D0换算成二进制就是 101-1101-0000.
这11位数按顺序放入"y"部分和"z"部分: 11010111 10010000.
最后结果就是双字节,用十六进制写起来就是 0xD7 0x90,这就是这个字符aleph (?)的UTF-8编码。
所以开始的128个字符(US-ASCII)只需一字节,接下来的1920个字符需要双字节编码,包括带变音符号的拉丁字母, 希腊字母, 西里尔字母, 科普特语字母, 亚美尼亚语字母, 希伯来文字母和阿拉伯字母的字符。基本多文种平面中其余的字符使用三个字节,剩余字符使用四个字节。
根据这种方式可以处理更大数量的字符。原来的规范允许长达6字节的序列,可以覆盖到31位元 (通用字符集原来的极限)。尽管如此,2003年11月UTF-8 被 RFC 3629 重新规范,只能使用原来Unicode定义的区域, U+0000到U+10FFFF。根据这些规范,以下字节值将无法出现在合法 UTF-8序列中:
UTF-8的衍生物
Windows
虽然不是标准,但许多Windows 程序(包括Windows 笔记本) 在UTF-8编码的档案的开首加入一段字节串EF BB BF。这是编码成UTF-8的Byte Order Mark U+FEFF。没有预期要处理UTF-8的文字编辑器和浏览器会会显示为ISO-8859-1字符"???"。
Java
在通常用法下,Java程序语言在通过Template:Javadoc:SE 和Template:Javadoc:SE读取和写入串的时候支持标准UTF-8。但是,Java也支持一种非标准的变体UTF-8,供对象的系列化,Java本地界面和在class文件中的嵌入常数时使用的Template:Javadoc:SE。
标准和改正的UTF-8有两个不同点。第一,空字符 (null character,U+0000)使用双字节,而不是单字节,分别是11000000 10000000。这保证了在已编码字串中没有嵌入空字节。因为C语言等语言程序中,单字节空字符是用来标志串串结尾的。当已编码字串放到这样的语言中处理,一个嵌入的空字符将把字串一刀两断。
第二个不同点是基本多文种平面之外字符的编码的方法。在标准UTF-8中,这些字符使用4字节形式编码,而在改正的UTF-8中,这些字符和UTF-16一样首先表示为代理对(surrogate pairs),然后再像CESU-8那样按照代理对分别编码。这样改正的原因更是微妙。Java中的字符为16位长,因此一些Unicode字符需要两个Java字符来表示。语言的这个性质盖过了Unicode的增补平面的要求。尽管如此,为了要保持良好的向后兼容、要改变也不容易了。这个改正的编码系统保证了一个已编码字串可以一次编为一个UTF-16码,而不是一次一个Unicode码点。 不幸的是,这也意味着UTF-8中需要4字节的字符在改正UTF-8中变成需要6字节。
因为改正的UTF-8并不是 UTF-8,所以用户在交换信息和使用互联网的时候需要特别注意不要误把改正UTF-8当成UTF-8数据。
Mac OS X
Mac OS X操作系统使用正式分解万国码(canonically decomposed Unicode),在文件系统中使用UTF-8编码进行文件命名,这做法通常被称为UTF-8-MAC。正式分解万国码中,预分解字符是被禁止使用的,必须以组合字符取代。
这种方法使分类变得非常简单,但是会搞混那些使用预分解字符为标准、组合字符用来显示特殊字符的软件。Mac系统的这种NFD数据是万国码规范化(Unicode normalization)的一种格式。而其他系统, 包括Windows和 Linux, 使用万国码规范的NFC形式,也是W3C标准使用的形式。所以NFD数据必须典型的转换成NFC才能被其他平台或者网络使用。
在此有关于此问题的讨论 Apple Q&A 1173.
设计UTF-8的理由
UTF-8的设计有以下的多字符组序列的特质:
单字节字符的最高有效位元永远为0。
多字节序列中的首个字符组的几个最高有效位元决定了序列的长度。最高有效位为
多字节序列中其余的字节中的首两个最高有效位元为
UTF-8的这些特质,保证了一个字符的字节序列不会包含在另一个字符的字节序列中。这确保了以字节为基础的部份字串比对(sub-string match)方法可以适用于在文字中搜寻字或词。有些比较旧的可变长度8位元编码(如Shift-JIS)没有这个特质,故字串比对的算法变得相当复杂。虽然这增加了UTF-8编码的字串的冗余,但是利多于弊。另外,资料压缩并非Unicode 的目的,所以不可混为一谈。即使在传送过程中有部份字节因错误或干扰而完全遗失,还是有可能在下一个字符的起点重新同步,令受损范围受到限制。
另一方面,由于其字节序列设计,如果一个疑似为字符串的序列被验证为UTF-8编码,那么我们可以有把握地说它是UTF-8字符串。一段两字节随机序列碰巧为合法的UTF-8而非ASCII 的机率为32分1。对于三字节序列的机率为256分3,对更长的序列的机率就更低了。
过长的资料排列(overlong forms)、输入无效及保安的考虑
优点及缺点
关于字符串长度的一个注解:
总体来说,在Unicode字符串中不可能由码点数量决定显示它所需要的长度,或者显示字符串之后在文本缓冲区中光标应该放置的位置;组合字符、变宽字体、不可打印字符和从右至左的文字都是其归因。
所以尽管在UTF-8字符串中字符数量与码点数量的关系比UTF-32更为复杂,在实际中很少会遇到有不同的情形。
总体
优点
UTF-8是ASCII的一个超集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用。
使用标准的面向字节的排序例程对UTF-8排序将产生与基于Unicode代码点排序相同的结果。(尽管这只有有限的有用性,因为在任何特定语言或文化下都不太可能有仍可接受的文字排列顺序。)
UTF-8和UTF-16都是扩展标记语言文档的标准编码。所有其它编码都必须通过显式或文本声明来指定。[2]
任何面向字节的字符串搜索算法都可以用于UTF-8的数据(只要输入仅由完整的UTF-8字符组成)。但是,对于包含字符记数的正则表达式或其它结构必须小心。
UTF-8字符串可以由一个简单的算法可靠地识别出来。就是,一个字符串在任何其它编码中表现为合法的UTF-8的可能性很低,并随字符串长度增长而减小。举例说,字符值C0,C1,F5至FF从来没有出现。为了更好的可靠性,可以使用正则表达式来统计非法过长和替代值(可以查看W3 FAQ: Multilingual Forms上的验证UTF-8字符串的正则表达式)。
缺点
一份写得很差(并且与当前标准的版本不兼容)的UTF-8解析器可能会接受一些不同的伪UTF-8表示并将它们转换到相同的Unicode输出上。这为设计用于处理八位表示的校验例程提供了一种遗漏信息的方式。
使用UTF-8的原因
ASCII转换成UCS-2,在编码前插入一个0x0。用这些编码,会含括一些控制符,比如 " 或 ''''/'''',这在UNIX和一些C函数中,将会产生严重错误。因此可以肯定,UCS-2不适合作为Unicode的外部编码,也因此诞生了UTF-8。
UTF-8的编码方式
UTF-8是UNICODE的一种变长度的编码表达方式 〈一般UNICODE为双字节(指UCS2)〉,它由Ken Thompson于1992年建立,现在已经标准化为RFC 3629。UTF-8就是以8位为单元对UCS进行编码,而UTF-8不使用大尾序和小尾序的形式,每个使用UTF-8储存的字符,除了第一个字节外,其余字节的头两个位元都是以 "10" 开始,使文字处理器能够较快地找出每个字符的开始位置。
但为了与以前的ASCII码相容 (ASCII为一个字节),因此 UTF-8 选择了使用可变长度字节来储存 Unicode:
在ASCII码的范围,用一个字节表示,超出ASCII码的范围就用字节表示,这就形成了我们上面看到的UTF-8的表示方法,这様的好处是当UNICODE文件中只有ASCII码时,储存的文件都为一个字节,所以就是普通的ASCII文件无异,读取的时候也是如此,所以能与以前的ASCII文件相容。
大于ASCII码的,就会由上面的第一字节的前几位表示该unicode字符的长度,比如110xxxxxx前三位的二进制表示告诉我们这是个 2BYTE的UNICODE字符;1110xxxx是个三位的UNICODE字符,依此类推;xxx 的位置由字符编码数的二进制表示的位填入. 越靠右的 x 具有越少的特殊意义.只用最短的那个足够表达一个字符编码数的多字节串. 注意在多字节串中, 第一个字节的开头"1"的数目就是整个串中字节的数目.。
ASCII字母继续使用1字节储存,重音文字、希腊字母或西里尔字母等使用2字节来储存,而常用的汉字就要使用3字节。辅助平面字符则使用4字节。
在UTF-8文件的开首,很多时都放置一个U+FEFF字符 (UTF-8 以 EF,BB,BF 代表),以显示这个文字档案是以UTF-8编码。
UTF-8的特性
UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容),这也意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的.
所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集。因此,ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分。
表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里,并指出这个字符包含多少个字节。多字节串的其余字节都在 0x80 到 0xBF 范围里,这使得重新同步非常容易,并使编码无国界,且很少受丢失字节的影响。
可以编入所有可能的 231个 UCS 代码
UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长。
Bigendian UCS-4 字节串的排列顺序是预定的。
字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到,同时,UTF-8以字节为编码单元,它的字节顺序在所有系统中都是一様的,没有字节序的问题,也因此它实际上并不需要BOM。
与 UTF-16 或其他 Unicode 编码相比,对于不支援 Unicode 和 XML 的系统,UTF-8 更不容易造成问题。
【注】
UTF为UCS / Unicode Transformation Format“Unicode转换格式”的缩写。
UCS的中文全称为:信息技术--通用多八位编码字符集 (Universal Multi-octet Coded Character Set),由ISO/IEC 10646 标准描述。
UTF-8编码的缺点
不利于正则表达式检索
正则表达式可以进行很多英文高级的模糊检索。例如,[a-h]表示a到h间所有字母。
同样GBK编码的中文也可以这样利用正则表达式,比如在只知道一个字的读音而不知道怎么写的情况下,也可用正则表达式检索,因为GBK编码是按读音排序的。只是UTF-8不是按读音排序的,所以会对正则表达式检索造成不利影响。但是这种使用方式并未考虑中文中的破音字,因此影响不大。Unicode是按部首排序的,因此在只知道一个字的部首而不知道如何发音的情况下,UTF-8 可用正则表达式检索而GBK不行。
其他
与其他 Unicode 编码相比,特别是UTF-16,在 UTF-8 中 ASCII 字符占用的空间只有一半,可是在一些字符的 UTF-8 编码占用的空间就要多出,特别是中文、日文和韩文(CJK)这样的象形文字,所以具体因素因文档而异,但不论哪种情况,差别都不可能很明显。
·描述
·UTF-8的衍生物
·设计UTF-8的理由
·过长的资料排列(overlong forms)、输入无效及保安的考虑
·优点及缺点
·使用UTF-8的原因
·UTF-8的编码方式
·UTF-8的特性
·UTF-8编码的缺点
UTF-8(8 位元 Universal Character Set/Unicode Transformation Format)是针对Unicode 的一种可变长度字符编码。它可以用来表示 Unicode 标准中的任何字符,而且其编码中的第一个字节仍与 ASCII 相容,使得原来处理 ASCII 字符的软件无需或只作少部份修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他储存或传送文字的应用中,优先采用的编码。
UTF-8 使用一至四个字节为每个字符编码:
128 个 US-ASCII 字符只需一个字节编码(Unicode 范围由 U+0000 至 U+007F)。
带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码(Unicode 范围由 U+0080 至 U+07FF)。
其他基本多文种平面(BMP)中的字符(这包含了大部分常用字)使用三个字节编码。
其他极少使用的 Unicode 辅助平面的字符使用四字节编码。
对上述提及的第四种字符而言,UTF-8 使用四个字节来编码似乎太耗费资源了。但 UTF-8 对所有常用的字符都可以用三个字节表示,而且它的另一种选择,UTF-16编码,对前述的第四种字符同样需要四个字节来编码,所以要决定 UTF-8 或 UTF-16 哪种编码比较有效率,还要视所使用的字符的分布范围而定。不过,如果使用一些传统的压缩系统,比如 DEFLATE,则这些不同编码系统间的的差异就变得微不足道了。若顾及传统压缩算法在压缩较短文字上的效果不大,可以考虑使用 Standard Compression Scheme for Unicode(SCSU)。
因特网工程工作小组(IETF)要求所有因特网协议都必须支援 UTF-8 编码。[1] 互联网邮件联盟(IMC)建议所有电子邮件软件都支援 UTF-8编码。所有主要的电子邮件软件中,只有 Eudora 不支援 UTF-8 编码。[1]
历史
1992年初,为建立良好的字节串编码系统(byte-stream encoding)以供多字节字符集(multi-byte character sets)使用,开始了一个正式的研究。ISO/IEC 10646的初稿中有一个非必须的附录,名为UTF。当中包含了一个供32位元的字符使用的字节串编码系统。这个编码方式的性能并不令人满意,但它提出了将0-127的范围保留给ASCII以相容旧系统的概念。
1992年7月,X/Open委员会XoJIG开始寻求一个较佳的编码系统。UNIX 系统实验室(UNIX System Laboratories, USL)的Dave Prosser为此提出了一个编码系统的建议。它具备可更快速实作的特性,并引入一项新的改进。其中,7位元的ASCII符号只代表原来的意思,所有多字节序列则会包含第8位元的符号,也就是所谓的最高有效位元。
1992年8月,这个建议由IBMX/Open的代表流传到一些感兴趣的团体。与此同时,贝尔实验室Plan 9操作系统工作小组的肯·汤普逊对这编码系统作出重大的修改,让编码可以自我同步(self-synchronizing),使得不必从字串的开首读取,也能找出字符间的分界。1992年9月2日,汤普逊和Pike一起在美国新泽西州一架餐车的餐桌垫上描绘出此设计的要点。接下来的日子,Pike及汤普逊将它实现,并将这编码系统完全应用在Plan 9当中,及后他将有关成果回馈X/Open。
1993年1月25-29日的在圣地牙哥举行的USENIX会议首次正式介绍UTF-8。
自1996年起,微软的CAB(MS Cabinet)规格在UTF-8标准正式落实前就明确容许在任何地方使用UTF-8编码系统。但有关的编码器实际上从来没有实作这方面的规格。
描述
目前有好几份关于UTF-8详细规格的文件,但这些文件在定义上有些许的不同:
RFC 3629 / STD 63(2003),这份文件制定了UTF-8是标准的因特网协议元素
第四版,The Unicode Standard,§3.9-§3.10(2003)
ISO/IEC 10646-1:2000附加文件D(2000)
它们取代了以下那些被淘汰的定义:
ISO/IEC 10646-1:1993修正案2/附加文件R(1996)
第二版,The Unicode Standard,附录A(1996)
RFC 2044(1996)
RFC 2279(1998)
第三版,The Unicode Standard,§2.3(2000)及勘误表#1:UTF-8 Shortest Form(2000)
Unicode Standard 附加文件#27: Unicode 3.1(2001)
事实上,所有定义的基本原理都是相同的,它们之间最主要的不同是支援的字符范围及无效输入的处理方法。
Unicode字符的位元被分割为数个部分,并分配到UTF-8的字节串中较低的位元的位置。在U+0080的以下字符都使用内含其字符的单字节编码。这些编码正好对应7位元的ASCII字符。在其他情况,有可能需要多达4个字符组来表示一个字符。这些多字节的最高有效位元会设定成1,以防止与7位元的ASCII字符混淆,并保持标准的字节主导字串(standard byte-oriented string)运作顺利。
Unicode在范围D800-DFFF中不存在任何字符,基本多文种平面中约定了这个范围用于UTF-16扩展标识辅助平面(两个UTF-16表示一个辅助平面字符). 当然,任何编码都是可以被转换到这个范围,但在unicode中他们并不代表任何合法的值
例如,希伯来语字母 aleph (?)的Unicode代码是 U+05D0,按照以下方法改成 UTF-8:
它属于 U+0080到U+07FF区域,这个表说明它使用双字节, 110yyyyy 10zzzzzz.
十六进制 的 0x05D0换算成二进制就是 101-1101-0000.
这11位数按顺序放入"y"部分和"z"部分: 11010111 10010000.
最后结果就是双字节,用十六进制写起来就是 0xD7 0x90,这就是这个字符aleph (?)的UTF-8编码。
所以开始的128个字符(US-ASCII)只需一字节,接下来的1920个字符需要双字节编码,包括带变音符号的拉丁字母, 希腊字母, 西里尔字母, 科普特语字母, 亚美尼亚语字母, 希伯来文字母和阿拉伯字母的字符。基本多文种平面中其余的字符使用三个字节,剩余字符使用四个字节。
根据这种方式可以处理更大数量的字符。原来的规范允许长达6字节的序列,可以覆盖到31位元 (通用字符集原来的极限)。尽管如此,2003年11月UTF-8 被 RFC 3629 重新规范,只能使用原来Unicode定义的区域, U+0000到U+10FFFF。根据这些规范,以下字节值将无法出现在合法 UTF-8序列中:
UTF-8的衍生物
Windows
虽然不是标准,但许多Windows 程序(包括Windows 笔记本) 在UTF-8编码的档案的开首加入一段字节串EF BB BF。这是编码成UTF-8的Byte Order Mark U+FEFF。没有预期要处理UTF-8的文字编辑器和浏览器会会显示为ISO-8859-1字符"???"。
Java
在通常用法下,Java程序语言在通过Template:Javadoc:SE 和Template:Javadoc:SE读取和写入串的时候支持标准UTF-8。但是,Java也支持一种非标准的变体UTF-8,供对象的系列化,Java本地界面和在class文件中的嵌入常数时使用的Template:Javadoc:SE。
标准和改正的UTF-8有两个不同点。第一,空字符 (null character,U+0000)使用双字节,而不是单字节,分别是11000000 10000000。这保证了在已编码字串中没有嵌入空字节。因为C语言等语言程序中,单字节空字符是用来标志串串结尾的。当已编码字串放到这样的语言中处理,一个嵌入的空字符将把字串一刀两断。
第二个不同点是基本多文种平面之外字符的编码的方法。在标准UTF-8中,这些字符使用4字节形式编码,而在改正的UTF-8中,这些字符和UTF-16一样首先表示为代理对(surrogate pairs),然后再像CESU-8那样按照代理对分别编码。这样改正的原因更是微妙。Java中的字符为16位长,因此一些Unicode字符需要两个Java字符来表示。语言的这个性质盖过了Unicode的增补平面的要求。尽管如此,为了要保持良好的向后兼容、要改变也不容易了。这个改正的编码系统保证了一个已编码字串可以一次编为一个UTF-16码,而不是一次一个Unicode码点。 不幸的是,这也意味着UTF-8中需要4字节的字符在改正UTF-8中变成需要6字节。
因为改正的UTF-8并不是 UTF-8,所以用户在交换信息和使用互联网的时候需要特别注意不要误把改正UTF-8当成UTF-8数据。
Mac OS X
Mac OS X操作系统使用正式分解万国码(canonically decomposed Unicode),在文件系统中使用UTF-8编码进行文件命名,这做法通常被称为UTF-8-MAC。正式分解万国码中,预分解字符是被禁止使用的,必须以组合字符取代。
这种方法使分类变得非常简单,但是会搞混那些使用预分解字符为标准、组合字符用来显示特殊字符的软件。Mac系统的这种NFD数据是万国码规范化(Unicode normalization)的一种格式。而其他系统, 包括Windows和 Linux, 使用万国码规范的NFC形式,也是W3C标准使用的形式。所以NFD数据必须典型的转换成NFC才能被其他平台或者网络使用。
在此有关于此问题的讨论 Apple Q&A 1173.
设计UTF-8的理由
UTF-8的设计有以下的多字符组序列的特质:
单字节字符的最高有效位元永远为0。
多字节序列中的首个字符组的几个最高有效位元决定了序列的长度。最高有效位为
110的是2字节序列,而1110的是三字节序列,如此类推。多字节序列中其余的字节中的首两个最高有效位元为
10。UTF-8的这些特质,保证了一个字符的字节序列不会包含在另一个字符的字节序列中。这确保了以字节为基础的部份字串比对(sub-string match)方法可以适用于在文字中搜寻字或词。有些比较旧的可变长度8位元编码(如Shift-JIS)没有这个特质,故字串比对的算法变得相当复杂。虽然这增加了UTF-8编码的字串的冗余,但是利多于弊。另外,资料压缩并非Unicode 的目的,所以不可混为一谈。即使在传送过程中有部份字节因错误或干扰而完全遗失,还是有可能在下一个字符的起点重新同步,令受损范围受到限制。
另一方面,由于其字节序列设计,如果一个疑似为字符串的序列被验证为UTF-8编码,那么我们可以有把握地说它是UTF-8字符串。一段两字节随机序列碰巧为合法的UTF-8而非ASCII 的机率为32分1。对于三字节序列的机率为256分3,对更长的序列的机率就更低了。
过长的资料排列(overlong forms)、输入无效及保安的考虑
优点及缺点
关于字符串长度的一个注解:
总体来说,在Unicode字符串中不可能由码点数量决定显示它所需要的长度,或者显示字符串之后在文本缓冲区中光标应该放置的位置;组合字符、变宽字体、不可打印字符和从右至左的文字都是其归因。
所以尽管在UTF-8字符串中字符数量与码点数量的关系比UTF-32更为复杂,在实际中很少会遇到有不同的情形。
总体
优点
UTF-8是ASCII的一个超集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用。
使用标准的面向字节的排序例程对UTF-8排序将产生与基于Unicode代码点排序相同的结果。(尽管这只有有限的有用性,因为在任何特定语言或文化下都不太可能有仍可接受的文字排列顺序。)
UTF-8和UTF-16都是扩展标记语言文档的标准编码。所有其它编码都必须通过显式或文本声明来指定。[2]
任何面向字节的字符串搜索算法都可以用于UTF-8的数据(只要输入仅由完整的UTF-8字符组成)。但是,对于包含字符记数的正则表达式或其它结构必须小心。
UTF-8字符串可以由一个简单的算法可靠地识别出来。就是,一个字符串在任何其它编码中表现为合法的UTF-8的可能性很低,并随字符串长度增长而减小。举例说,字符值C0,C1,F5至FF从来没有出现。为了更好的可靠性,可以使用正则表达式来统计非法过长和替代值(可以查看W3 FAQ: Multilingual Forms上的验证UTF-8字符串的正则表达式)。
缺点
一份写得很差(并且与当前标准的版本不兼容)的UTF-8解析器可能会接受一些不同的伪UTF-8表示并将它们转换到相同的Unicode输出上。这为设计用于处理八位表示的校验例程提供了一种遗漏信息的方式。
使用UTF-8的原因
ASCII转换成UCS-2,在编码前插入一个0x0。用这些编码,会含括一些控制符,比如 " 或 ''''/'''',这在UNIX和一些C函数中,将会产生严重错误。因此可以肯定,UCS-2不适合作为Unicode的外部编码,也因此诞生了UTF-8。
UTF-8的编码方式
UTF-8是UNICODE的一种变长度的编码表达方式 〈一般UNICODE为双字节(指UCS2)〉,它由Ken Thompson于1992年建立,现在已经标准化为RFC 3629。UTF-8就是以8位为单元对UCS进行编码,而UTF-8不使用大尾序和小尾序的形式,每个使用UTF-8储存的字符,除了第一个字节外,其余字节的头两个位元都是以 "10" 开始,使文字处理器能够较快地找出每个字符的开始位置。
但为了与以前的ASCII码相容 (ASCII为一个字节),因此 UTF-8 选择了使用可变长度字节来储存 Unicode:
在ASCII码的范围,用一个字节表示,超出ASCII码的范围就用字节表示,这就形成了我们上面看到的UTF-8的表示方法,这様的好处是当UNICODE文件中只有ASCII码时,储存的文件都为一个字节,所以就是普通的ASCII文件无异,读取的时候也是如此,所以能与以前的ASCII文件相容。
大于ASCII码的,就会由上面的第一字节的前几位表示该unicode字符的长度,比如110xxxxxx前三位的二进制表示告诉我们这是个 2BYTE的UNICODE字符;1110xxxx是个三位的UNICODE字符,依此类推;xxx 的位置由字符编码数的二进制表示的位填入. 越靠右的 x 具有越少的特殊意义.只用最短的那个足够表达一个字符编码数的多字节串. 注意在多字节串中, 第一个字节的开头"1"的数目就是整个串中字节的数目.。
ASCII字母继续使用1字节储存,重音文字、希腊字母或西里尔字母等使用2字节来储存,而常用的汉字就要使用3字节。辅助平面字符则使用4字节。
在UTF-8文件的开首,很多时都放置一个U+FEFF字符 (UTF-8 以 EF,BB,BF 代表),以显示这个文字档案是以UTF-8编码。
UTF-8的特性
UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容),这也意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的.
所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集。因此,ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分。
表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里,并指出这个字符包含多少个字节。多字节串的其余字节都在 0x80 到 0xBF 范围里,这使得重新同步非常容易,并使编码无国界,且很少受丢失字节的影响。
可以编入所有可能的 231个 UCS 代码
UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长。
Bigendian UCS-4 字节串的排列顺序是预定的。
字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到,同时,UTF-8以字节为编码单元,它的字节顺序在所有系统中都是一様的,没有字节序的问题,也因此它实际上并不需要BOM。
与 UTF-16 或其他 Unicode 编码相比,对于不支援 Unicode 和 XML 的系统,UTF-8 更不容易造成问题。
【注】
UTF为UCS / Unicode Transformation Format“Unicode转换格式”的缩写。
UCS的中文全称为:信息技术--通用多八位编码字符集 (Universal Multi-octet Coded Character Set),由ISO/IEC 10646 标准描述。
UTF-8编码的缺点
不利于正则表达式检索
正则表达式可以进行很多英文高级的模糊检索。例如,[a-h]表示a到h间所有字母。
同样GBK编码的中文也可以这样利用正则表达式,比如在只知道一个字的读音而不知道怎么写的情况下,也可用正则表达式检索,因为GBK编码是按读音排序的。只是UTF-8不是按读音排序的,所以会对正则表达式检索造成不利影响。但是这种使用方式并未考虑中文中的破音字,因此影响不大。Unicode是按部首排序的,因此在只知道一个字的部首而不知道如何发音的情况下,UTF-8 可用正则表达式检索而GBK不行。
其他
与其他 Unicode 编码相比,特别是UTF-16,在 UTF-8 中 ASCII 字符占用的空间只有一半,可是在一些字符的 UTF-8 编码占用的空间就要多出,特别是中文、日文和韩文(CJK)这样的象形文字,所以具体因素因文档而异,但不论哪种情况,差别都不可能很明显。
2008年8月12日星期二
未来系统:Plan9
动机
到80年代中期,计算的趋势从大的集中式的分时计算向更小的个人机器(典型的如UNIX工作站) 组成的网络方向转移。人们早已对过载的和受严格管束的分时机器厌倦了,渴望转而使用小的, 自维护的系统,即使意味着计算能力上有不少损失。微型计算机变得越来越快,像那样的损失是可以弥补的,时至今日这种计算方式依然流行。
尽管个人工作站在急促发展,但是它们的某些缺点还是被忽视了。首先,它们运行的操作系统 UNIX本身是一个古老的分时系统而且很难和在它诞生之后的思想相适应。图形和网络功能被很好地加入到UNIX生存期之中,同时是很差劲地被集成进去的以及难于管理。更为重要的是早期集中在拥有私人机器从而让计算机网络像老的单一分时系统一样无缝地提供服务变得困难了。分时把管理和分期偿还投资和资源集中化了;个人计算分散了,民主化了以及从根本上扩大了管理问题。选择一个老的分时操作系统来运行这些个人机器使得平滑地一起绑定一些东西变得困难了。
开始于20世纪80年代后期 Plan 9 作为一个尝试通过两种方法实现:构建一个被集中管理的和好的价格性能比--通过使用便宜的现代微型计算机作为它的计算元素--的系统。意图是要构建一个脱离工作站的分时系统,但是以新颖的方式实现。不同的计算机处理不同的任务(tasks):在人们的办公室里的小的便宜的机器作为终端,提供对大的,集中的共享资源的如计算服务器和文件服务器的访问能力。对于集中的机器,正在来临的共享存储多处理器(译者注:一种多处理器的方案,可参考<;高等计算机体系结构>;;一书,徐志伟&;黄铠,机械工业出版社)高潮好像是一种明显的候选方案。这里的想法很像剑桥分布式系统(CambridgeDistributed System)所采用的[NeHe82]。早期的口号是要构建一个脱离许多小系统的UNIX,而不是一个脱离许多小UNIX的系统。
UNIX的问题是太深难于修改,但是它的某些思想还是可以继续发展的。最好的一个就是它的文件系统的使用,用一致的方法命名和访问资源,甚至那些如设备,不再传统地看作文件。对于Plan 9,我们采用了这个思想,设计了一个称作9p的网络级协议能让机器访问远端系统上的文件。在此之上,我们构建了一个命名系统,它让人们和它们的计算代理建立网络中资源的个性化视图。这就是Plan 9第一个开始看起来不同的地方:一个Plan 9用户构建一个私人的计算环境还可以在任何想要的地方重新创建,而不是在一个私人机器上做所有计算。很快就变得清楚了,这个模型比我们预见的还要更丰富,像每个进程的名字空间和类文件系统资源这样的想法被扩展到整个系统--到进程,图形,甚至网络本身。
到1989年,系统已经足够稳定,以至于我们当中的一些人把它用作我们的高级(exclusive)计算环境。这就意味着带来我们已在UNIX上使用的许多服务和应用。我们借此机会重新思考了许多问题, 而不仅仅是驻留在内核中的,我们认为UNIX被很差地访问。Plan 9有新的编译程序,程序设计语言, 程序库,窗口系统和许多新的应用程序。许多旧的工具已不再使用,而这些一起带来的应用程序则被改进或重写。
为什么会如此的全部包含呢?因为操作系统,库和应用程序之间的差别对操作系统研究者是重要的,但是用户却对它没有兴趣。重要的是清晰的功能性。通过建立一个全新的系统,我们能去解决我们认为应当要解决的问题。例如,内核中没有实际的'tty 驱动程序';因为那是窗口系统的职责。在当今世界,多家销售商和多种体系结构的计算机这一事实是关键的,而通常的编译程序和工具假定程序是被建立在本地环境运行的;我们有必要重新思考这个问题。然而最重要的是系统的其余部分是它所提供的计算环境。探索一个运行老的UNIX系统更有效率的方式是吃力不讨好的;我们对由底层系统体系启示得来的新想法是否能够促进工作效率的提高更感兴趣。因此,Plan 9为运行POSIX 命令提供一仿真环境,它是系统的死水(backwater)。系统软件的绝大部分都是在本地的Plan 9 环境中开发出来的。
拥有一个全新的系统有几个好处。首先,我们的实验室由一个制造试验用外设板卡的历史。为了使写驱动程序更简单些,我们想要一个原始形式可得的系统(不再向UNIX保证,即使在它所诞生的实验室)。其次,我们想重新发布我们的作品,这就意味着软件必须本地化生成。例如我们已经在我们的系统上使用了某些销售商的C编译程序,但是即使我们克服了交叉编译的问题,我们还会有重新发布最终软件的困难。
本篇论文是对Plan 9系统的概述。它讨论了系统结构--从最底层的构件模块到用户可见的计算环境。也作为对Plan 9程序员手册的其它部分的导论,本论文和它们放在一起。关于论文中相关主题的更多细节可以在手册中的其它地方找到。
设计
系统的视图构建在三个原理的基础上。第一,资源像层次式文件系统中的文件一样被命名和访问。其次,有一个名叫9P的协议用于访问这些资源。第三,不同服务提供的不相交的层次被连接进单一的私有的层次式文件名字空间。Plan 9的不同一般的特性来源于对这三个原则的始终如一的和主动的应用。
大型的Plan 9的安装有许多通过网络连接在一起的计算机,每一台提供一个具体的服务级别。共享型多处理器服务器提供计算周期;其它大机器提供文件存储。这些机器放置在带空调机的房间,且用高性能的网络连接起来。低带宽网络如以太网(Ethernet)或ISDN把这些服务器连接到办公室和家里的工作站或PC,在Plan 9术语中叫终端。图1显示了这样的布局安排。
计算的现代方式是为每一个用户提供一台专用的工作站或者PC。Plan 9的方法是不同的。各种不同的机器都有提供访问网络资源的屏幕,键盘和鼠标,因此它们功能上同连接到老式的分时系统的终端在习惯上是相同的。然而,当某人使用系统时,终端从术语上讲被用户独自占有。作为自定义硬件的替代,Plan 9给予用户自定义自己系统视图的能力,这些能力由软件提供。定制是通过给网络中公共可见的资源以局部的私有的名字来完成的。Plan 9 提供这样一个机制:用全局可访问资源的局部名字集成一个公共空间的私有视图。因为网络的最重要资源是文件,这个视图的模型是面向文件的。
客户的本地名字空间提供一个定制网络的用户视图的方法。网络中可以获得的服务都导出文件层次结构。这些对用户重要的东西被一起集成到用户的名字空间;那些没有直接影响的就被忽略了。这是一个来自 "单一全局名字空间"思想的不同的使用风格。在Plan 9中,有众所周知的名字用于服务和用于由这些服务导出的文件的单独名字,但是视图却是完全本地的。作为一个类比,考虑短语'我的房子'和说话人的家的精确地址之间的区别。后者可以被任何人使用,但前者更容易说和讲得通。还可以依赖于谁说它而改变含义,而且不会导致混淆。类似的,在Plan 9中 名字/dev/cons总是指用户的终端,而/bin/date指要运行的date命令的恰当版本,但是这些名字所代表的文件取决于执行date命令时的环境例如机器的体系结构。 Plan 9由遵循全局可理解的惯例的本地名字空间;惯例是:在局部名字面前保证健全的行为。
9P协议是用一组事务(transactions)的集合建构成的:从一个客户发送一个请求到(本地或远程) 服务器,然后返回结果。9P控制文件系统,不仅仅是文件:它包含解析(resolve)文件名字的过程和转换由服务器提供的文件系统上的名字层次结构。另一方面,客户名字空间仅由客户系统处理, 不在也不和服务器一起处理,和Sprite[OCDNW88]这样的系统不一样。而且文件的访问是在字节的水平上,而不是块,这就把9P和NFS及RFS这样的协议区分开来。Welch 写的一篇文章比较了Sprite, NFS和Plan 9的网络文件系统结构[Welc94]。
此方法用传统文件设计的,但是可以被扩展到许多其它资源。Plan 9的导出文件层次的服务包括 I/O设备,备份服务,窗口系统,网络接口和许多其它的东西。一个例子就是进程文件系统,/proc, 它提供一个干净的方法来监测和控制运行的进程。Precursor系统有一个相似的思想[Kill84],但是 Plan 9把文件隐喻推进的更深一点[PPTTW93]。文件系统模型是容易理解的,不管是系统构件者还是一般用户,因此表示类文件(file-like)接口的服务是容易构造的,易于理解的和易于使用的。 为了保护命名和访问本地和远程文件,采用了向上兼容(agreed-upon)规则,因此这种方式构建服务是为分布系统的准备性工作(ready-made)。(这与'面向对象模型'不同,在那里这些问题必须重新思考(faced anew) 对象(object)的每一个类(class)) 以下分节示例了正在实践中的这些思想。
命令级视图
Plan 9被打算从带有一个运行窗口系统的屏幕的机器上使用。它没有在UNIX意义上的'teletype'的概念。裸机系统的键盘处理是基本的,但是一旦运行了窗口系统,8½; [Pike91] ,文本就可以从弹出式菜单中的剪和贴操作来编辑,在窗口之间拷贝,等等。8½; 允许从过去的输入行编辑文本,不仅是当前输入行。8½; 的文本编辑能力强大到可以替换特殊特征如shell的历史,分页和滚动以及邮件编辑器。8½; 窗口系统不支持光标访问,除非一个为了简化连接到传统系统的终端模拟器,在Plan 9中没有光标访问式的软件。
每一个窗口在一个单独的名字空间中创建。对一个窗口中的名字空间的调整不会影响其它窗口或程序,使得对名字空间的局部修改的试验变得安全,例如当调试时从dump文件系统中代替文件。一旦调试完毕,窗口就可以被删除,所有试验机构的遗迹全部消失。应用到每一窗口拥有的私有空间的相似参数用于环境变量, 说明(和UNIX的信号相似),等等。
每一个窗口被创建来运行一个应用程序,例如shell,带有连接到窗口的可编辑文本的标准I/O。每一个窗口还有一个私有位图以及通过向这样的文件/dev/mouse,/devbitblt和/dev/cons(类似于UNIX的/dev/tty) 来多工访问键盘,鼠标和其它图形资源。这些文件由8½; 提供,它被实现成一个文件服务器。不像X窗口系统,那里一个新的应用程序要创建一个新的窗口在其中运行,一个8½; 图形应用程序通常运行在它所启动的窗口里。为一个应用程序创建一个新的窗口时可能的和有效的,但那不是系统的风格。再次和X(窗口系统)对比,在那里一个远程应用程序创建一个对X服务器的网络调用来启动运行,一个远程的8½; 应用程序为窗口察看通常在/dev目录下的mouse,bitblt和 cons文件;它不知道文件是否在本地。它为了控制窗口仅仅读和写它们;网络连接已经存在且是多工的。
预期的使用方式是诸如在终端上的窗口系统和文本编辑的交互式应用程序和运行计算-或远程服务器上文件密集(file-intensive)的应用程序。不同的窗口可能通过不同的网络运行不同的程序,但是通过让名字空间在所有窗口中等价,这是透明的:无论计算在何时执行,用相同的名字都可以获得相同的命令和资源。
Plan 9的命令集合和UNIX的命令集合是相似的。命令分成概括性的几个类别。有些是用于旧的作业的新程序:像ls,cat和who这样的程序保持相同的名字和功能,但是新的,简化了的实现。例如who命令是一个 shell脚本(script),而ps命令只有95行C语言代码。某些命令和它的UNIX祖先那里基本上是相同的:awk, troff和其它已经被转换成ANSI C的和扩充成处理Unicode,但仍然是相似的工具。某些就的事务是用完全新的程序:shell的rc文件,文本编辑程序sam,调试程序acid,以及其它用相似作用的程序替换了更为知名的 UNIX工具。最后,大约有一半的命令是新的。
兼容性不是系统的一个要求。只要老的命令或说明看上去足够好,我们就保留它,否则我们就替换它。
文件服务器
中央文件服务器存储永久文件,使用9P协议把它们导出成文件层次结构并呈现给网络。服务器是一个独立(stand-alone)的系统,只可以通过网络访问,被设计成只把它的一件事情做好即可。它不运行用户进程,只运行固定的一套被编译进引导图像的例程(routines)。服务器导出的主要层次结构(多于一组磁盘或单独的文件系统)是一棵单一的树,把文件表示在许多磁盘上。这个层次结构被广泛范围内的许多用户通过不同的网络共享。服务器导出的其它文件树包括专用的系统如临时存储和下面就要解释的一个备份服务。
文件服务器有三个存储级别。我们安装的系统中的中央服务器有大约100MB的内存缓冲,27GB的磁盘空间,还有在一个WORM点唱柜中350GB的存储容量。磁盘是WORM的高速缓冲,而内存是磁盘的一个高速缓冲;每一个都比它所缓冲的级别更快,要处理的数据流量按顺序增加。文件系统中可寻址的数据可以比磁盘的大小还要大,因为它们仅是一个高速缓冲;我们的主文件服务器大约有40GB的活动存储量。
文件服务器的最不一般的特征是使用一个WORM设备来用作稳定存储。每天早上5点钟自动发生一次文件系统的转储dump。文件系统被冻结,所有自最近一次转储(dump)以来被修改的块被排列成队列写入 WORM中。一旦块被排队,服务就被恢复,转储的文件系统的只读的根(root)出现在所有已发生的转储层次中,且以它的日期命名。例如,目录/n/dump/1995/0315是文件系统图像的一个根目录,和出现在1995年3月15日早上的一样。要花几分钟时间来排队这些块,但是拷贝块到WORM中的进程运行后台,可能要花好几个小时。
转储的文件系统有两种用途。第一种是用户自己使用,它们可以直接浏览转储文件系统,把当中的几个关连(attach)到它们自己的名字空间。例如,为了追踪一个错误,直接从三个月以前尝试编译程序或者把一个程序和昨天的库连接。用所有文件的每日快照,那么查找什么时候一个特殊的改变发生或一个特别的日子发生了什么改变这样的事是容易的。在了解文件可以用一个拷贝命令收回的条件下,人们可以自由地对文件做大的冒险性的改变。没有备份这样的系统;相反,因为转储是在文件名字空间中,备份问题可以诸如cp,ls,grep和diff这样的标准工具来解决。
其它的(很少)使用是完全系统备份。如果发生灾难,通过清除磁盘高速缓冲和把活动文件系统的根设置为转储的文件系统的根,这样就可以从任何转储来初始化活动文件系统。尽管容易做,还是不能轻率的完成:除了丢失了转储后所做的任何改变,还会导致系统速度很低。高速缓冲必须从WORM重新装入,它比磁盘慢的多。文件系统要花好几天重新装入工作集(working set),重新获得它的全部性能。
转储中的文件的访问权限和转储生成时是一样的。普通的实用程序有正常权限,在转储中没有特殊的处理。然而,转储文件系统是只读的,这就意味着转储中的文件的无论权限位如何都不能被写;实际上,既然目录是只读结构的一部分,甚至权限都不能被改变。
一旦一个文件被写入WORM中,它就不能被删除,因此我们的用户从来不会看到“请清除你的文件”这样的消息,也没有df命令。我们把WORM柜看作一个无限的资源。只有一个问题就是花多长时间就会写满数据。我们的WORM已经为一个大约50个用户的社团服务了五年,吸收每日的转储,耗费了总量的65%的存储空间。那时,制造商已经改进了技术,把每一个磁盘的容量翻倍(doubling)。如果我们打算升级成新的介质,我们就会有比当初的空磁盘更多的空闲空间。技术已经创造了比我们所使用的更快的存储设备。
不寻常的文件服务器
Plan 9的特征表现在有许多这样的服务器:它们向不一般的服务提供一个类文件(file-like)的界面。许多都是由用户级进程实现的,尽管对于它们的客户而言差别是不重要的;一个服务是由内核,一个用户进程还是一个远程服务器来提供和服务的使用方式是不相关的。有许多这样的服务器;本节我们介绍三个代表性的服务器。
Plan 9种可能最引人注目的文件服务器是8½; ,即窗口系统。它在别 的地方[Pike91]有详细的讨论,但是此处值得给一个简要的解释。8½; 提 供两个接口:给终端旁的用户,它提供和多窗口交互的传统风格,每一个都运行一个应用程序,都由 鼠标和键盘控制。给客户程序,视图也是相当传统的:运行在一个窗口中的程序看到/dev目录下的一 些文件,如 mouse,screen 和cons。想要把文本显示到它们对应窗口的程序向/dev/cons中写;读鼠 标的话,则读 /dev/mouse。以Plan 9的方式,位映射图形是这样实现的:提供一个文件 /dev/bitblt, 客户程序把经过编码的消息写到执行图形运算的程序如bitblt(RasterOp)。所不平常的是这是如何 完成的:8½; 是一个文件服务器,用/dev目录下的文件向运行在每一个窗口 中的客户提供服务。虽然每一个窗口看上去和它的客户一样,但是每一个窗口拥有/dev目录下的不同的 一组文件。8½; 通过服务多组文件来多工它的客户对终端资源的访问。通过 一个不同的文件组为每一个客户给定一个私有名字空间,这些文件组的行为和在所有其它窗口 中一样。这样的结构由许多优点。其中之一就是为相同的文件,用于它自己的实现所需要的-它多工自 己的界面-因此它可以运行,急救,把自己作为一个客户。另外,考虑 UNIX 中/dev/tty的实现,它要 求在内核中有特殊的代码把open 调用重新定向到适当的设备。相反,在 8½; 中,等价的服务自动地实现:它把/dev/cons作为基本的功能;决没有什么其它要做的。当程序象想从 键盘上读时,它打开/dev/cons,但它是一个私有文件,不是一个有特殊属性的共享文件。还有,局部 名字空间让这成为可能;它们内部的文件一致性的惯例让这变得自然。
由于8½; 的设计使得它由一个统一的特征成为可能。因为它被实现成为 一个文件服务器,它有推迟对一个具体窗口的读请求做出回答的权力。这个行为由键盘上的一个保留键 触发。触发一次对来自窗口的客户读请求的挂起;再一次触发就让正常的读继续进行,它接受任何已经 准备好的文本,一次一行。这就允许用户在引用程序得到文本之前就可以在屏幕上编辑多行输入文本, 避免了需要调用一个特别的编辑程序来准备象邮件消息这样的文本。一个相关的性质是,读直接 地从显示器上的文本定义的数据结构得到回应:文本可以被一直编辑直到它的最后新行符让文本的 预备行由客户可读为止。甚至即使到行已读入,客户要读的文本还可以被改变。例如,输入以下 命令给shell后
% make
rm *
,用户可以在make完成之前的任何时候会退过最后的新行符,延迟rm命令的执行,或者甚至在 rm之前用鼠标点击,输入另一个命令先执行。
Plan 9中没有ftp命令。相反,一个叫ftpfs的用户级文件服务器向FTP站点拨号,代表用户登录,用FTP 协议检查远程目录中的文件。对于本地用户而言,它提供一个文件层次结构,关连到本地名字空间中的/n/ftp,镜像了FTP站点的内容。也就是说,它把FTP协议转换成9P协议来提供Plan 9访问FTP站点的能力。实现是复杂而微妙的;出于效率ftpfs必须做一些 复杂的缓冲和用启发式方法来解码(decode)目录信息。但是结果是值得的:所有本地文件 管理工具诸如cp,grep,diff当然还有ls对于FTP服务提供的的文件就像它们是本地文件一样。其它系统如Jade和Prospero已经开拓出一些机会[Rao81,Neu92],但是由于本地名字空间和9P 协议实现的简洁性,比起其它环境,这个方法更自然地适合Plan 9。
一个名叫exportfs的用户进程把它自己所有的名字空间的一部分变为其它进程可获得的 ,方法是把9P请求转换成对Plan 9内核的系统调用。它所导出的文件层次结构可能包含来自 多个服务器的文件。Exportfs 通常由本地程序启动作为远程服务器运行,要么是import,要么是cpu。Import创建一个对远程机器的网络调用,在远程机器上启动exportfs,然后把它的9P连接(connection)关联(attache)到本地名字空间。例如,
import helix /net
让Helix的网络接口在本地/net目录中可见。Helix是一个有许多网络接口的中央服务器,因此这就允许有一个网络的某台机器可以访问到任何Helix的网络。这样一个导出之后,本地机器可以创建连接到Helix的任一网络的调用。另一个例子是
import helix /proc
,它让Helix的进程在本地/proc目录中可见,允许本地的调试程序检查远程进程。
cpu命令把本地终端连接到一个远程CPU服务器。它以和import相反的方向工作:调用服务器之后,它启动一个本地的exportfs进程,把它挂接到一个进城的本地名字空间中,典型的是服务器上的一个新创建的shell。然后它重新安排名字空间让本地设备文件(如那些由终端的窗口系统所服务的设备) 在服务器的/dev目录中可见。运行一个CPU命令的效果就是在一台快速的机器上启动shell,一个模拟本地名字空间的更紧密地耦合到文件服务器的shell。所有本地设备文件都是远程地可见的,因此远程应用程序具有完全访问本地服务的能力,例如位映射图形,/dev/cons等等。这和rlogin不一样,它(rlogin) 不会在远程系统上重新生成本地名字空间,而且和NFS上的文件共享不一样,NFS可以获得一部分对等的名字空间但不是对本地硬件设备,远程文件和远程CPU资源访问的复合。CPU命令是一个单一地透明机制。比如,在一个正在运行CPU命令的窗口中启动一个窗口系统是适当的;所有已经创建的窗口自动地启动CPU 服务器上的进程。
配置能力和管理
Plan 9中组件(components)的统一互连使得以多种方式配置Plan 9的安装成为可能。一个单独的膝上型PC可以作为一个单独的(stand-alone)Plan 9系统;另一个极端是我们的安装有中央的多处理器CPU服务器和文件服务器以及许多终端(从小的PC到高端图形工作站)。这样大的安装最好地展示了Plan 9是如何工作的。
系统软件是可移植的而且同样的操作系统运行在所有硬件上。除了系统的性能,系统的外观对 SGI工作站和一台膝上型电脑是一样的。既然计算和文件服务被集中了,而且终端没有永久文件存储, 所有终端功能上是相同的。 像这种方式,Plan 9 有一个老的分时系统的好的特点,即用户可以坐在任何机器面前而且看到的是相同的系统。像现代工作站的一致性,机器倾向于被个人占有,它们在机器的本地硬盘上存储私人信息来定制它们的机器。尽管系统本身可以这样使用,但是我们拒绝这种使用方式。在我们的研究组里,我们的实验室有许多可以公共使用的机器---一个终端室,用户可以坐在任何一个终端面前工作。
中央文件服务器不仅集中了文件,而且包括它们的管理和维护。实际上,一个服务器是主服务器 ,处理所有系统文件;其它服务器提供额外的存储或者用于调试和其它特殊用法,但是系统文件驻留在一台机器上。这就意味每一个程序对于每一种体系结构有唯一的二进制副本,因此安装更新(updates) 和排除错误(bug fixes)的价值不大。还有一个单一的用户数据库;没有必要去同步不同的/etc/passwd 文件(即让/etc/passwd文件尽快保持相同,译者注)。另一方面,对于一个中央服务器的依赖并不一个安装的大小。
集中式文件服务的另一个强大能力的另一个例子Plan 9管理网络信息的方式。在中央服务器上有一个目录/lib/ndb,它包含所有管理本地以太网和其它网络的必要的信息。所有的机器使用相同的数据库和网络对话;没有必要去管理一个分布式命名系统或者保持并行的文件都是新的。为了在本地以太网上安装一个新的机器,选择一个名字和IP地址,然后把这些加入到/lib/ndb目录下的单个文件中;安装中的所有机器可以立即和它对话。为了启动运行,把机器加入到网络中,打开机器然后使用BOOTP和TFTP协议装入核心。其它的一切都是自动完成的。
最后,自动的转储文件系统根据维护它们的系统的需要释放所有用户的空间,然而提供一个方便的访问备份文件系统的方法--不需要磁带,特殊命令或者重要支持的复杂情况。夸大这种服务提供的工作方式的改进是困难的。
Plan 9可以运行在许多硬件上,不限制如何设置一个安装。在我们的实验室,我们选择使用中央服务器因为它们让投资和管理可以分期偿还。这是一个好的决定的标志就是我们的便宜的终端已经适宜地工作了大约5年时间,比那些必须提供完整的计算环境的工作站的时间长的多。然而,我们的确升级过中央的机器,因此甚至旧的Plan 9终端可以获得的计算能力也能及时的改进。通过避免对终端的正常升级,节省的钱被花在最新的,最快的多处理器服务器上。我们估计这个成本只花了联网的工作站的一半,却提供了对更强大的机器的通常使用(general access)。
C 语言程序设计
Plan 9的实用程序使用了好几种语言写成。一些是shell(rc)[Duff90]的脚本(script),有一小部分是用新型的称为Alef[Wint95]的类似C的并行编程语言写成,在下面有介绍。不过,绝大多数还是用 ANSI C[ANSIC]的方言(dialect)版本写成。这些程序中绝大多数是全新的,但是有一些是从我们的研究用的UNIX系统[UNIX85]的前标准C代码。这些已经被升级到ANSI C,出于移植和干静又重写了一遍。
Plan 9的C语言有一些小的扩充和几个主要的限制,在[Pike95]有所介绍。最重要的限制是编译器要求所有函数定义有ANSI的原型(prototype),而且所有的函数调用出现在函数原型声明的作用域。作为一个文体规则,原型的声明放在一个被所有调用那个函数的头文件中。每一个系统库有一个关联的头文件,它定义了那个库中的所有函数。例如,标准的Plan 9库被称为libc,所以所有C源代码文件包含。这些规则保证所有函数被调用时带有期望类型的参数--对于ANSI C之前的C代码则不是这样的。
另一个限制是C编译器只接受ANSI要求的预处理程序制导的子集。主要的省略是#if,因为我们认为这是从不必要的而且经常混淆。同样,它的作用可以通过其它的方法更好的得到。例如,使用一个#if在编译时触发一个特征,它可以被写成正规的if语句,取决于编译时常量卷和为了释放目标码的固定代码省略量。(此句较难译,望专家指教)
条件编译甚至是#ifdef在Plan 9中也是很少使用的。系统中唯一的依赖于体系结构的 #ifdefs是图形库中的低层例程。相反,我们避免这样的依赖性,或者当必要时把它们放入单独的源文件或库中。除了使代码难于阅读以外,#ifdefs让知道什么源码被编译进库或者由它们保护的源代码是否会正常的编译和工作。它使得维护软件更难。
标准的Plan 9库有许多和ANSI C及POSIX[POSIX]重叠,但是适合Plan 9的目标和实现时是分开的。当一个函数的语义改变时,我们也改变名称。例如,相对于UNIX的creat, Plan 9有一个create函数它带有三个参数,原来的两个加上第三个---和open的第二个参数一样,定义了返回的文件描述符被打开来读,写或者读写。9P实现的创新方式促成了这个设计,但是它也简化了create的通常使用来初始化临时文件。
另一个不同于ANSI C的是Plan 9使用称为Unicode [ISO 10646,Unicode] 16位字符集。尽管我们避免了完全国际化的缺点,Plan 9 通过它的所有软件统一了对所有主要语言的表示。为了简化在程序之间文本的交换,字符使用了我们设计的编码方法打包成字节流,这个方法称为UTF-8,现在已经变成一个被接受的标准[FSSUTF]。它有几个具有诱惑力的特点,字节顺序无关性,和ASCII码向后兼容以及易于实现。
把已经存在的软件转换为大字符集的会有许多问题,这种大字符集的编码用可变数目的字节来表示字符。ANSIC提到过这些问题但是并没有完全解决。它不选择一个字符集的编码,也不定义所有必要的I/O库例程。更何况,它的函数定义有工程上的问题。既然表准遗留了太多未解决的问题,我们就决定构建我们的界面。另有一篇专题论文讨论细节[Pike93]。
小规模(class)的Plan 9程序不遵循本届所讨论的惯例。由一些来自UNIX社团维护的程序被收入系统; tex就是一个代表性的例子。为了避免每次重新移植这些程序就要发行一个新版本,我们构建了一个移植(porting)环境,称为ANSI C /POSIX环境,或APE[Tric95]。APE有独立的包含文件;库和命令组成,尽可能地和严格的ANSI C和基本的POSIX 规范一致。为了移植基于网络的软件如X-Windows,对这些规范增加一些扩充是必要的,就像BSD的联网功能。
可移植性和编译
Plan 9可以移植到许多处理器结构上。在单一的计算会晤期(Session)内,使用好几种体系结构是普遍的:窗口系统可能运行在一个连接到基于MIPS的CPU服务器上的Intel处理器,此服务器和文件主留在 SPARC系统上。为了达到异构透明性,必须有程序间交换数据所需要的协议;为了直接的维护软件,必须有跨平台编译的协议。
为了避免字节顺序问题,数据以文本形式在程序间传送任何时候都是可行的。尽管有时数据总量高到需要二进制形式,这样的数据用多字节值的预定义编码来作为字节流传送。在很少情况下,格式复杂到要用数据结构定义,此结构从不作为一个单元传送;相反,它先被拆分成单独的域,再编码成顺序字节流,最后由接受器重新汇编。这些协议影响很多数据,从内核到应用程序状态信息和由编译器生成的中间目标文件。
包括核心在内的程序常用文件系统接口表示它们的数据,这个借口是继承性的可移植访问机制。例如系统时钟由在文件/dev/time中的十进制数表示;time库函数(没有time系统调用)读文件然后转换成二进制形式。类似的,和把一个应用进程的状态编码到私有存储中的一些标志和为中不一样,核心把文本字符串表示在一个叫status的文件中,status文件在和每个进程关联的/proc目录。Plan 9的ps命令是简单的:它经过某些简单的重新格式化后打印想要的文件的状态信息;在输入下面的命令以后 import helix /proc 一个本地的ps命令给出Helix进程的状态报表。
每一个支持的体系结构都有相应的编译程序和装入程序。C和]Alef编译程序生成的中间文件是经过可移植编码的;内容相对目标结构是统一的,但是文件格式是独立于编译处理器类型的。当给定体系结构的编译程序在另一种处理器类型上编译时,在新的处理器上编译一个程序,生成的中间文件和原处理器上生成的文件是相同的。从编译程序的角度看,每一次编译都是交叉编译(cross-compilation)。(交叉编译:在一种处理器上为编译另一种体系结构的可执行文件而进行的编译-译者注。)
尽管每一种体系结构的装入程序只接受由对应的编译程序生成的中间文件,这种中间文件可以在另一种类型的处理器上编译得到。例如,在486上运行MIPS编译器,然后用SPARC上的MIPS装入程序生成MIPS可执行文件是可能的,
因为Plan 9可以运行在许多体系结构上,即使在单机安装中,在单一的源代码树种区别编译程序和中间名字简化了多体系结构的开发。每一种体系结构的编译程序和装入程序被统一命名;系统里没有CC命令(CC:c语言编译程序-译者注)。
名称通过和目标体系结构相关联的编码字母的连接而得到,还和编译程序及装入程序的名字有关。例如,字母‘s’是Intel X86处理器的编码字母;相应的编译程序叫8c;Alef编译程序称为为8al,而装入程序称为8l。类似的,编译程序的中间文件名称为。8而不是.O后缀。
Plan 9的构造程序叫mk,一个和make相对的程序,它从环境变量$cputype和$objtype读出当前的和目标的体系结构名称。默认的,当前处理器即为目标处理器,但是可以设置$objtype的值为另一个体系结构,让mk进行交叉构造:
%objtype = sparc mk
为sparc体系结构构造一个程序(不管执行的处理器为什么类型)。$objtype变量的值选择一个体系结构相关的变量定义文件,此文件把构造配置成使用适当的编译程序和装入程序。虽然此方法简单,但这个技术在实践中工作的很好:Plan 9中的所有应用程序都从单一源代码树构造,以并行而无冲突地构造不同体系结构的应用程序是可能的。
并行程序设计
Plan 9对并行程序设计的支持有两个方面。首先,核心提供一个简单的进程模型以及一些用于同步和共享而仔细设计的系统调用。第二,一个新的称为Alef的并行程序设计语言支持协同程序设计。尽管用C写并行程序是可能的,但是Alef是并行语言的当然选择
在新式操作系统领域中有一个趋势,就是实现两极进程:普通的Unix方式的进程和轻量级内核线程。相反,Plan 9一个进程级别,但是能够很好地控制一个进程的资源的共享(这些资源如内存储器和文件描述符)。单一级别的进程在Plan 9中之所以是可行的,因为核心有一个有效的系统调用接口和低开销的进程创建和调度方法。
并行程序有三个基本的要求:进程间共享资源的管理,一个调度接口和一个使用自旋锁(spin locks)的细粒度(fine-grain)的进程同步机制。在Plan 9上,新的进程用rfork系统调用创建。Rfork使用一个参数和一个向量(它指定父进程的哪一个资源应被共享,复制,或在子进程中创建新的)。Rfork控制的资源包含名字空间,环境,文件描述符表,内存段和说明(对Unix信号的Plan 9模拟)。其中一位控制rfork调用是否创建新进程;如果此位关闭,最终对资源的修改发生在进行调用时。例如,一个进程调用rfork(RFNAMEG)把它的名字空间和父进程的名字空间分开。Alef使用一个细粒度的fork,其中所有的资源在(包括内存储器)在父子进程间共享,和许多系统中创建一个内核线程相似。
Rfork的使用方式是多种多样的说明了rfork是合适的模型。和fork过程的通常使用不同很难发现两个使用rfork的调用有相同的位集合;程序用它创建许多不同形式的共享和资源分配。仅有两类进程的系统-正规进程和线程-不能处理这样的变化。
共享内存有两种方式。第一,rfork的一个标志使得父进程的所有内存段和子进程共享(除了栈,它是写时复制的)。可选地,内存的一个新段可用segattach系统调用接上;这样的段将总被父子进程共享。
Rendezvous系统调用提供进程同步的方法。Alef用它实现通信通道,排队锁,多个读写锁和睡眠唤醒机制。Rendezvous使用两个参数:一个标记和一个值。当一个进程调用带一个标记的rendezvous时,它一直睡眠到另一个进程产生一个匹配的标记。如果一对标记匹配,那么值就在两个进程间交换且两个rendzevous调用都返回。此原语已经足够实现全部的同步过程。
最后,在用户级的体系结构相关库提供自旋锁。大多数处理器提供可以用来实现锁的原子的测试设置指令。一个著名的例外是MIPS R3000,因此SGI Power系列多处理器在总线上有特殊的锁硬件。用户进程通过使用segattach系统调用访问锁硬件,这个调用把硬件锁的页照应到它们的地址空间。
在系统调用中的Plan 9进程无论它的优先级怎样都会阻塞。这就是说当一个程序希望从没有阻塞整个计算的慢速设备上读时,它必须为读创建一个进程。方法是启动一个辅助进程通过共享内存或一个管道来进行I/O和传递对主程序的反应。这听起来很麻烦而实际上很容易且效率很好;事实上,大多数Plan 9交互程序都是作为多进程程序运行的,即使是用C写的相对简单的文本编辑程序sam[Pike87]也是如此。
Plan 9中内核对并行程序设计的支持有几百行可移植代码;几个简单的原语使问题可以干静的在用户级处理。尽管原语用C工作的很好,但是在Alef中是特别有意义的。从属的I/O进程的创建和管理可以用几行的Alef写成,为任意进程间的多工数据流的一致方法提供基础。而且在语言中实现而不是在内核中为所有设备间保证一致的语义以及提供一个更通用的多工原语。把它和Unix中的select系统调用相比较:select仅用在设备的优先集合中,确立了内核多进程程序设计的风格,但是未扩展到网络,它还难于实现和使用。
Plan 9中并行程序设计重要的另一个原因是多线程的用户级文件服务器是实现服务的更好的方法。这样的服务器的例子包括程序设计环境Acme[Pike94],名字空间导出工具exportfs[PPTTW93],HTTP守护程序和网络名字服务器,和DNS[Prwi93]。像Acme这样复杂的应用程序证明精心的操作系统支持可以降低写多线程应用程序的难度,不用把线程和同步原语移动到内核中。
名字空间的实现
用户进程用三个系统调用构建名字空间:mount,bind和unmount。mount系统调用把一个文件服务器生成的树添加到当前名字空间。在调用mount之前,客户(通过外部方法)请求一个到服务器的连接,用一个可以被读写用来传递9p消息的文件描述符的形式实现请求。那个文件描述符表示一个管道或网络连接。
Mount系统调用把一个新的层次添加到现存的名字空间。另一方面,bind系统调用在名字空间的另一点上复制现存名字空间的一部分。Unmount系统调用可以让组成(名字空间的)部分被删除。
使用bind或者mount,多个目录可以在名字空间的一个点上建栈。用Plan 9的术语说这是一个联合(union)目录,行为像成组目录的连接一样。Bind和mount的一个标志参数指定在联合目录中新目录的位置,允许在联合目录之前或之后增加新元素或用新元素整个地替换。当在联合目录中执行查找文件时,联合目录的每一部分被依次查找,首次匹配的作为结果;同样的,当读一个联合目录时,组成目录的每一部分的内容被依次读取。联合目录是Plan 9名字空间的组成性特征中最广泛使用的一个。例如,目录/bin被构造成/$cputype/bin(程序二进制文件),/rc/bin(shell脚本)以及可能由用户提供的更多的目录组成的一个联合目录。这种构造使得shell的$path变量成为多余的。
Union目录带来了一个问题:Union的哪一个元素接受新创建的文件。经过几次设计之后,我们决定采用下述方法。默认的,union中的目录不接受新创建的文件,尽管应用到一个已存在文件的creat系统调用正常地完成了。当一个目录被加到一个联合目录中时,bind和mount的一个标记允许在一个目录中有创建权限(名字空间的一个属性。当在一个联合目录中创建一个新文件时,它以创建权限被创建在union中的第一个目录中;如果创建失败,整个creat调用也失败。这个方案使得通常把一个私人目录放在公共的联合目录中的任何一个中,而仅允许在私人目录中创建文件。
根据约定,核心设备文件系统被绑定到/dev目录中,但是为了自举名字空间构造进程,必要说明一下:允许对没有现存名字空间的设备的直接访问。有设备驱动保存的树的根目录可以用语法#c访问,c是唯一的确定设备类型的字符(典型地是一个字母)。简单的设备驱动器提供包含一些文件的单级目录。作为一个例子,每一个串行端口被表示成一个数据和一个控制文件:
% bind -a '#t' /dev
% cd /dev
% ls -l eia*
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia1
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia1ctl
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia2
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia2ctl
bind程序是bind系统调用的一个封装;-a标记定位新目录在union的末尾。数据文件eia1和eia2可以通过串行线通信读写。与这些设备的控制对文件进行特殊操作不同,为eia1ctl和eia2ctl写的命令控制相应的设备;例如把文本字符串b1200写到/dev/eia1ctl把线速设置为1200波特率。和Unix的ioctl系统调用相比:在Plan 9中设备由文本的消息控制,不受字节顺序问题约束,且有清晰的读写语义。用shell脚本设置或调试设备是平常的。
普遍用9P协议把Plan 9的组成部分联在一起形成一个分布的系统。没有为每一个服务如rlogin,FTP,TFTP和X windows去发明新的协议,Plan 9用对文件对象的操作的方法实现服务,然后使用唯一的,经过良好说明的协议在计算机间交换信息。和NFS不一样,9P把文件看作是字节的序列而不是块的序列;还有不同,那就是9P是有状态的:客户执行远程调用过程建立指向远程文件服务器上的对象的指针。这些指针称为文件ID或fids。对文件的所有操作提供一个fid来识别远程文件系统中的一个对象。
9P协议定义17个消息,通过这些消息鉴别用户,在一个文件系统层次中遍历fid,拷贝fid,执行I/O,改变文件属性,以及创建和删除文件。它的完整规范在程序员手册(9man)的第五部分。下面是访问一个文件服务器上名字层次的过程。通过管道或网络连接建立和文件服务器的连接。一个最初的session消息执行客户进程和服务进程间的双方鉴别。attache消息把由客户进程建议的fid连接到服务器文件树的根。attache消息包括执行attache的用户ID号;自此以后,所有从根fid获得的fid拥有与那个用户相关的权限。多个用户可以共享连接,但每一个用户必须执行一个attache建立它/它的ID。
Walk消息在文件系统层次的单级间移动fid。clone消息得到一个已经建立的fid然后生成一个指向和原来文件相同的指针的拷贝。目的是在目录中移动一个文件时不丢失目录上的fid。Open消息把一个fid锁到层次中的一个具体的文件上,检查访问权限,以及为I/O准备fid。Read和write消息允许在文件中的任何位置进行I/O;传送的最大值已由协议定义。clunk消息说明客户进程没有为一个fid的进一步使用。Remove消息的行为和clunk相似,但是使和fid相关联的文件被删除以及服务器上任何相关的资源都被释放。
9p有两种形式:通过管道或网络连接发送的RPC消息和内核中的过程接口。因为核心设备驱动是直接可寻址的,就没有必要传送消息和它们通信;相反,每一个9p事务由一个直接的过程调用实现。对每一个fid,核心用一个称为channel的数据结构维护一个本地表示;因此由核心执行的所有对文件的操作都涉及到和fid关联的一个通道。最简单的例子是一个用户进程的文件描述符。它由通道构成的数组来索引。核心中的一张表提供和每一设备相应的9P消息一一对应的入口点。如来自用户的读调用通过那张表被转换成一个或多个过程调用,用存储在通道中的类型字符索引:procread,eiaread等等。每一个调用至少使用一个通道作为参数。一个特殊的核心驱动称为mount驱动把过程调用转换成消息,即它把本地过程调用转换成远程的。从效果上看,这个特殊驱动变成远程文件服务器上文件的本地代理。本地调用中的通道指针被转换成传送消息中相关联的fid。
Mount驱动是系统使用的唯一RPC机制。提供的文件的语义,而不是在它们上执行的操作创建了一个像cpu命令这样的具体服务。Mount 驱动解多路协议消息,这些消息在和文件服务器共享通信通道的客户进程间。对每一个发出的RPC消息,mount驱动为之分配一个用小整数(称为tag)标记的缓冲区。对RPC的回应也用它标记上,这被mount驱动用来匹配请求和回应。
名字空间的核心表示称为mount表。它保存一张通道间绑定列表。Mount表中的每一个入口包含一对通道:一个from通道和一个to通道。每次一个遍历(walk)成功地把一个通道移动到一个名字空间中新的位置时,就检查mount表是否一个“from”通道与新名字匹配;若是则克隆(clone)“to”通道代替原来的。联合目录通过把“to”通道转换到一张通道列表中实现:到联合目录的成功遍历返回一个“to”通道,它构成通道列表头,每一个都表示联合目录的组成目录。如果遍历没有在联合目录中的首目录中发现文件,则列表被接上,克隆下一个组成目录,且尝试遍历那个目录。
Plan 9中的每一个文件由一组整数唯一的识别:通道的类型(用作函数调用表的索引),服务器或其它相同类型(由驱动程序本地决定)的服务器相区别的设备号和一个来自名叫path和version的32位数字形成的一个qid。路径是创建文件时一个由设备驱动或文件服务器指定的文件号。只要文件被修改版本号就更新;像下一节描述的一样,它用作维护客户和服务器之间的高速缓存一致性。
类型和设备号是对UNIX主次设备号的模拟;qid是对i号的模拟。设备和类型把通道连接到一个设备驱动而qid则识别设备中的文件。如果从遍历中恢复的文件有相同的类型设备和作为安装表中入口点的qid路径,则它们是相同的文件,进行安装表中相应的替换。名字空间就是这样实现的。
文件高速缓冲
在客户端9P协议没有清楚的对高速缓冲文件的支持。文件服务器的大内存扮演它的所有客户的一个共享高速缓冲,它降低了网络上所有机器需要的内存总量。不过在客户端高速缓冲文件有合理的原因,诸如一个到文件服务器的低速连接。
Qid的版本域在每次文件被修改时都变化,它使得弱的高速缓冲一致性形式成为可能。最重要的是客户缓冲文本和可执行文件的数据段。当一个进程执行(execs)一个程序时,文件被重新打开,把qid的版本和高速缓冲中的相比较;若匹配,则使用本地备份。可以使用相同的方法构建一个本地缓冲文件系统。这个用户级服务器把9P连接插入到远程服务器,监视本地磁盘的流量和拷贝的数量。当它看到一个队已知数据的读时,它直接回答,而写被立即传送,使高速缓冲被写穿(write-through)保持了中央拷贝为最新。这对终端上的进程是透明的且不要求改变9P;它在串行线连接的家用机上工作的很好。相似的方法可用来在未用的本地存储器上构件一个通用的客户高速缓冲,但是在Plan 9中没这样做。
网络及通信设备
网络接口是驻留在核心的文件系统,是对较早描述的EIA设备的模拟。通过向和设备相关的控制文件写入文本字符串实现调用设置和关闭;通过读写数据文件来收发信息。设备的结构和语义对所有的网络是公共的,而不是一个文件名替换,同一过程创建一个跨以太网的使用TCP的调用,像URP对Datakit一样[Fra80]。
这样的例子演示了TCP设备的结构:
% ls -lp /net/tcp
d-r-xr-xr-x I 0 bootes bootes 0 Feb 23 20:20 0
d-r-xr-xr-x I 0 bootes bootes 0 Feb 23 20:20 1
--rw-rw-rw- I 0 bootes bootes 0 Feb 23 20:20 clone
% ls -lp /net/tcp/0
--rw-rw---- I 0 rob bootes 0 Feb 23 20:20 ctl
--rw-rw---- I 0 rob bootes 0 Feb 23 20:20 data
--rw-rw---- I 0 rob bootes 0 Feb 23 20:20 listen
--r--r--r-- I 0 bootes bootes 0 Feb 23 20:20 local
--r--r--r-- I 0 bootes bootes 0 Feb 23 20:20 remote
--r--r--r-- I 0 bootes bootes 0 Feb 23 20:20 status
%
顶级目录/net/tcp包含一个clone文件和一个用于每一个连接的目录,号数从0到n。每一个连接目录对应于一个TCP/IP连接。打开clone保留一个未用的连接,且返回它的控制文件。读这个控制文件就返回文本形式的连接号,因此用户进程可以创建新分配的连接目录的完整名字。Local,remote和status文件是诊断;例如remote包含远端的地址(对于TCP,是IP地址和端口号)。
通过写一个connect消息初始化一个调用,此消息带有一个作为参数的具体的网络地址;例如,为打开一个和远程机器的Telnet会话(端口号23)用IP地址135.104.9.52,字符串是:
connect 135.104.9.52!23
向控制文件写块(block)直到建立了连接;如果目标不可到达,则写返回一个错误。一旦建立连接,telnet应用程序读写数据文件以和远端的Telnet守护程序对话。在另一端,Telnet守护程序启动,通过写:
announce 23
到它的控制文件说明它希望接受对此端口的调用。在Plan 9中这样的守护程序被称为listener。
网络设备的一个统一结构不能隐藏不同网络的寻址和通信细节。例如,Datakit使用文本的层次式的地址(和IP的32位地址不同),因此给出一个控制文件的应用程序必须还要知道它表示的网络。不是让每个应用程序都知道每个网络的寻址,Plan 9把这些细节称为CS的连接服务器中。CS是一个安装在已知位置的文件系统,它提供一个控制文件,应用程序用它发现如何连接到主机。应用程序为想要创建的连接写符号的地址和服务名,读回要打开的克隆文件的名字和表示它的地址。如果在机器间有多个网络,CS就表示出一张可能网络和地址的列表,以逐个尝试;它使用启发式方法确定顺序。例如,它表示成最高带宽优先的。
一个称为dial的库函数和CS对话以建立连接。使用dial的应用程序不需要改变,甚至不需要重新编译就能适应新的网络;CS的接口隐藏了细节。
在Plan 9中网络的统一结构使得import是所有建构网关所需要的。
网络的核心结构
用来构建Plan 9通信通道的内核探查被称为streams[Rit84][presotto]。流是一个双向通道。它把一个物理的设备或伪设备连接到一个用户进程。用户进程在流的一段插入和删除数据,内核进程根据设备在另一端操作。一个流由一个进程模块的线性列表组成。每一模块有一个向上流(朝向进程)和向下流(朝向设备)的put过程。在流的任一端调用put过程就把数据插入到流中。每一个模块调用后继的一个以向上或向下发送数据。像Unix的流一样[Rit84],Plan 9的流可以被动态的配置。
IL协议
9P协议必须运行在带定界消息的可靠传输协议上。9P没有从传输错误中恢复的机制,系统假定每一次对通信通道的读会返回一个9P消息;它不会为了发现消息边界而分析数据流。管道和一些网络协议已经有这些属性,但标准的IP协议没有。TCP不划定消息边界,而UDP[RFC768]不提供可靠的顺序吞吐。
我们设计了一个新协议称为IL(Internet Link),通过IP传输9P消息。它是一个基于连接的协议,提供机器之间可靠的顺序消息传送。因为每一进程只有一个未决的9P请求,因此IL不需要流控制。像TCP一样,IL有自适应超时设定:它测定(scale)确认与重传时间以匹配网络速度。这让协议能很好地在Internet和本地以太网上运行。同样IL不会盲目重传,是为了避免给繁忙的网络增加拥挤。完整的细节在另一篇文章中[Prwi95]。在Plan 9中IL的实现比TCP更小更快,IL是我们主要的Internet传输协议。
鉴别概述
鉴别为访问资源的用户建立一个ID。请求资源的用户称为客户(client),授予对资源的访问的用户称为服务器(server)。这通常在一个9P消息attach的帮助下完成。服务器总是依据某些用户,一个普通客户或某些管理实体而活动,因此,鉴别被定义为用户间的,而不是机器间的。
每一个Plan 9用户由一个相关的DES[NBS77]鉴别密钥(key);对用户ID的审查是通过加密和解密特殊消息的能力实现的,此消息称为询问(challenge),因为对一个用户的密钥的知晓才让用户访问它的资源,Plan 9的鉴别协议从来不传送一个有清楚可读(cleartext)的密钥的消息。
鉴别是双向的:在鉴别交换(exchange)结束时,每一方都确信了另一方的身份。每一机器用内存中的一个DES密钥开始交换。在CPU和文件服务器的一方,密钥、用户名、和服务器的域名是从永久存储中得到的,通常是非易失性的RAM。在终端一方,密钥是在引导时从用户输入的口令获得。一个特殊的机器,即鉴别服务器,为它的管理域中所有的用户和鉴别协议中的参与者维护一个密钥数据库。
鉴别协议如下:交换询问(challenge)之后,一方和鉴别服务器联系以创建权限保证(permission-granting)的票据(ticket);这个票据用每一方的保密密钥加密且包含一个新的会话密钥。每一方解密自己的票据,且用会话密钥加密另一方的询问。
这个结构有点像Kerberos[MBSS87],但是避免了它对同步时钟的信任。和Kerberos不一样的还有,Plan 9鉴别支持一个“speak for”关系[LABW91];它能使一个用户拥有另一个用户的授权;CPU服务器就是这样根据它的客户的行为而运行进程的。
Plan 9的鉴别结构构建安全的服务而不是依赖于防火墙。鉴于防火墙对每一种服务渗透的“墙”都要求特殊的代码,Plan 9方法允许鉴别在一个地方-9P(对所有服务)完成。例如,命令安全地运行在Internet上。
鉴别外部连接
正规的Plan 9鉴别协议不适合于像Telnet或FTP这样的基于文本的服务。这种情况下,Plan 9用户用手头上的称为鉴别码(authenticator)的DES算子鉴别。鉴别码为用户处理一个密钥,它不同于用户的普通鉴别密钥。用户使用一个4位数字的PIN登录到鉴别码。正确的PIN使得一个询问/回应的鉴别码和服务器交换。因为一个询问/回应交换只有一次是合法的,且密钥从不通过网络传送,这个过程不受中继攻击(relay attacks)的影响,还和Telnet及FTP这样的协议兼容。
特殊用户
Plan 9没有超级用户。每一服务器负责维护自己的安全,通常只允许从控制台访问,它由一个口令保护。例如,文件服务器有唯一的一个管理的用户称为adm,它的特殊特权是仅能应用到由服务器的物理控制台输入的命令。这些特权考虑到服务器每天的维护,如添加新用户和配置磁盘及网络。特权不包含修改,检查或改变文件权限的能力。如果一个文件被某用户只读保护,只有那个用户可以个其它人授予访问。
CPU服务器有一个相似的用户名,它允许对服务器上资源的管理性访问,这些资源如用户进程的控制文件。这样的权限是必要的,例如杀死破坏进程,但是不超出服务器的范围。另一方面,通过存储在受保护的非易失RAM中的密钥,管理的用户身份被鉴别服务器证实。这就允许CPU服务器鉴别远程用户,处于访问服务器本身以及服务器根据它们的行为而作为一个代理。
最后,一个特殊用户名为none,它没有口令且总允许连接;任何用户都可以申称自己为none。None 有受限的权限,例如,不允许检查转储文件且只能读大家都可读的文件。
隐藏在none后面的思想是对FTP服务中匿名用户的模拟。在Plan 9上,访问FTP服务器更被限制在一个特殊约束的名字空间。它把访问服务器的用户从系统程序上断开,例如/bin的内容,但使得可以创建本地文件,用户可以通过把它们显式地绑定到none而获得本地文件。受限的名字空间比通常的导出一个ad hoc目录树更安全;其结果就是围绕不被信任(untrusted)用户周围的环(cage)。
CPU命令和代理的鉴别
当为一个用户如Peter创建一个对CPU服务器的调用时,其意思是Peter用它自己的权限运行进程。为实现这个特征,CPU服务器收到调用时按以下过程处理。首先,听者(listener)进程创建一个进程处理调用。进程改变为用户none,以避免权限泄漏。它然后执行鉴别,如果是折衷的,协议核实调用的用户确实是Peter,还要向Peter证明机器是可信任的。最后,它使用鉴别协议重新添加(reattach)到所有相关的文件服务器核实它本身是Peter。这种情况下,cpu服务器是文件服务器的一个客户,依据Peter的行为执行鉴别交换的客户部分。只有CPU服务器的管理用户名字允许要求得到“speak for”Peter时,鉴别服务器才会给进程票据以完成鉴别。
“Speak for”关系被保存在鉴别服务器上的一张表中。为了简化对在不同鉴别域中计算的用户的管理,它还包含不同域中用户名间的照映,例如,就像一个域中的用户rtm和另一个域中的rtmorris是同一个人。
文件权限
构造如文件系统这样的服务的一个优势是自然地得出了解决所有权和权限问题的方法。和在Unix中一样,每一个文件或目录对文件的所有者,文件的组及其它任何人都有独立的读写,执行/搜索权限。组的想法是不一般的:任何用户名都潜在地是一个组名称。组就是带有组中其它用户列表的一个用户。习惯在成了不同,大多数用户由用户名却没有组成员,而组有附属的名字的长列表。例如,传统上sys组有所有的系统程序员,系统文件可以用组sys访问。考虑以下的存储在服务器上的一个用户的两行信息:
pjw:pjw:
sys::pjw,ken,philw,presotto
第一行把用户pjw建立为一个正规用户。第二行建立一个名为sys的组,列出了组中的四个成员用户。空的帽号分隔的域是命名为组领袖的用户的空间。如果组有一个领袖,则那个用户有组的特殊权限,如可以自由改变组中文件的组权限。若没有指定领袖,则组中每一个成员被平等对待,就像每一个都是领袖一样。在我们的例子中,只有用户pjw可以向它的组中添加成员,但是sys中的所有成员都是这个组中平等的伙伴。
正规文件由创建它们的用户所有。组名称从持有新文件的目录继承。设备文件被特别处理:内核可能把一个文件的所有权和权限安排给访问此文件的适当用户。
提供的一般性的好例子是进程文件,它被进程的所有者所有及读保护。若所有者想让另外的某人访问进程的存储区,例如让程序的作者调试一个破坏的图像,则对进程文件应用标准命令chmod完成此时。
文件权限的另一个不寻常应用是转储文件系统,它不仅由和原始数据一样的文件服务器提供,而且用相同的用户数据表示。因此转储的文件就给予和正规文件系统中的文件一样的保护;如果一个文件由pjw所有和读保护,只要它在转储文件系统它就仍然由pjw所有和读保护。而且,因为转储文件系统是不变的,其文件不能被改变;它被永远的读保护。不利的一点是如果文件是可读的,但应当已经是读保护的,则它就是永远可读的,且那个用户名难于重新使用。
性能
作为对Plan 9核心的一个简单测量,我们比较了在两种系统上完成相同的简单操作需要的时间:Plan 9系统和SGI的IRIX Release 5.3,它运行在SGI Challenge M机器上,它有一个100MHz MIPS R4400处理器和一个1MB二级高速缓冲。测试程序用Alef写成,用相同的编译程序编译,且运行在相同的硬件上,所以只有操作系统和库是不同的。
程序测试了以下操作的时间:一个上下文切换(Plan 9上的rendezvous,IRIX上的blockproc();一个平凡的系统调用(rfork(0)和nap(0));以及轻量级fork(rfork(RFPROC()和sproc(PR_SFDS|PR_SADDR))。它还测试了从一个进程通过管道向另一个进程发送一个字节的时间和两进程间一个管道的吞吐量。结果如下表:
尽管所花的时间并不特别有优势,但是还是说明核心和商业系统是有竞争力的。
讨论
Plan 9由一个常规的核心;系统的新奇之处是核心之外的部分和它们之间相互作用的方式。在构建Plan 9时,我们综合考虑了系统的所有方面,用最合适的方法解决问题。有时方法产生了许多组成部分。一个例子就是异构指令体系结构的问题,通过编译器(不同的编码字符,可移植的目标码),环境($cputype和$objtype),名字空间(绑定在/bin中)以及其它部分来寻址。有时许多问题可以在一个地方解决。最好的例子是9P,它集中了命名,访问和鉴别。9P实际上是系统的核心部分;可以清楚地说Plan 9核心主要地就是一个9P多路转换器。
Plan 9集中于文件和命令是它的表现性的中心。尤其在分布式计算中,命名事物的方式对系统[Nee89]有深远的影响。局部名字空间的结合以及对互连网络的资源的全局约定避免了维护一个全局同一名字空间的困难,而把所有的东西都像文件命名使得系统易于理解,即使对于新手也是这样。考虑转储文件系统,任何人都可以像层次文件系统一样简单的使用它。在更深的层次上,把所有资源构造在一个统一的接口上使得易于互操作。一旦一个资源导出一个9P接口,它可以透明地和系统的任何其它部分结合以构建不一般的应用;其细节被隐藏了。这听起来好像是面向对象的,但是和它有所区别。第一,9P定义一个固定的“方法”集合;它不是一个可扩展协议。更为重要的是,文件被良好的定义以及易于理解,且用和访问,保护,命名及网络相类似的方法预打包。尽管对象有共性,但是对象没有这些已经定义的属性。通过把“对象”还原为“文件”,Plan 9获得了一些出于自由的技术(technology for free)。
不仅如此,还可能把基于文件计算的思想推向更远。把系统中的每一个资源转换为文件系统是一种比喻,且比喻会被滥用。关于限制的一个好例子是/proc,它仅仅是进程的一个视图,而不是进程的表示。要运行进程,通常的fork和exec调用仍然是必须的,而不是像这样做:
cp /bin/date /proc/clone/mem
这个例子的问题是要求服务器做不在其控制之下的事情。像这样为一个命令指定方法的能力并不意味着这个方法并不自然地得出对9P请求生成的回答的结构。作为一个相关的例子,Plan 9不把机器的网络名字放到文件名空间。网络接口提供一个非常不同的命令模型,因为对这样的文件使用open,create,read和write不提供一个合适的地方来放置所有的为任意网络建立的调用细节,这不意味着网络接口不能提供类文件的,仅仅是因为必须要有一个更紧凑定义的结构。
下一步我们会做哪些不同的呢?实现的某些部分不令人满意。在核心中使用流来实现网络接口允许协议可以被一起被动态的连接,诸如添加同一个TTY驱动到TCP,URP和IL连接,但是Plan 9没有使用这个配置能力。(已经做了努力,然而,在research Unix 系统中为实现此目的发明了流技术。)使用静态的I/O队列替换流不仅使代码简化且加快代码的运行。
尽管主要的Plan 9核心可以移植到许多机器上,但文件服务器被独立地实现。这已经导致了几个问题:驱动程序必须写两次,同样必须两次排除错误,且降低了文件系统代码的可移植性。方法时容易的:文件服务器核心应作为一个正规操作系统的变形来维护,它没有用户进程,且没有特殊的为了实现文件服务的内编译(compiled-in)核心进程。对文件系统的另一个改进会是内部结构的变化。WORM光盘柜是硬件中最不可靠的部分,但是因为它保存文件系统的元数据(metadata),为了提供文件它必须是在场的(present)。系统可以用适当驻留在磁盘上的文件系统重构,因此WORM只是一个备份设备了。这不会要求改变内部接口。
尽管Plan 9有每个进程的名字空间,它还没有这样的机制:把一个进程名字空间的描述给除了直接继承的进程以外的进程。例如CPU命令一般不能再现终端的名字空间;它只能重新解释用户的登录概貌(profile)文件,为诸如要装入的二进制目录名创建一个替代者。这就丢失了在运行CPU命令之前所做的任何本地修改。相反,捕获终端名字空间且把它的描述传送给一个远程进程应当是可能的。
虽然有些问题,Plan 9还是工作的很好。它已经成为一个支持我们的研究的成熟系统,而不是对研究本身的约束。新的试验性工作包括开发更快的网络接口,客户核心中的文件高速缓冲,封装和导出名字空间,以及在一个服务器崩溃后重新建立客户状态的能力。现在的注意力集中于用这个系统构造分布是应用。
Plan 9成功的一个原因是我们用它完成我们日常的工作,而不仅仅作为一个研究工具。当出现活跃的用户时,它们促使我们查找缺陷,以使系统适应于解决我们的问题。经过这一过程,Plan 9已经变成一个舒服而有生产效率的程序设计环境,而且是进一步系统研究的手段。
到80年代中期,计算的趋势从大的集中式的分时计算向更小的个人机器(典型的如UNIX工作站) 组成的网络方向转移。人们早已对过载的和受严格管束的分时机器厌倦了,渴望转而使用小的, 自维护的系统,即使意味着计算能力上有不少损失。微型计算机变得越来越快,像那样的损失是可以弥补的,时至今日这种计算方式依然流行。
尽管个人工作站在急促发展,但是它们的某些缺点还是被忽视了。首先,它们运行的操作系统 UNIX本身是一个古老的分时系统而且很难和在它诞生之后的思想相适应。图形和网络功能被很好地加入到UNIX生存期之中,同时是很差劲地被集成进去的以及难于管理。更为重要的是早期集中在拥有私人机器从而让计算机网络像老的单一分时系统一样无缝地提供服务变得困难了。分时把管理和分期偿还投资和资源集中化了;个人计算分散了,民主化了以及从根本上扩大了管理问题。选择一个老的分时操作系统来运行这些个人机器使得平滑地一起绑定一些东西变得困难了。
开始于20世纪80年代后期 Plan 9 作为一个尝试通过两种方法实现:构建一个被集中管理的和好的价格性能比--通过使用便宜的现代微型计算机作为它的计算元素--的系统。意图是要构建一个脱离工作站的分时系统,但是以新颖的方式实现。不同的计算机处理不同的任务(tasks):在人们的办公室里的小的便宜的机器作为终端,提供对大的,集中的共享资源的如计算服务器和文件服务器的访问能力。对于集中的机器,正在来临的共享存储多处理器(译者注:一种多处理器的方案,可参考<;高等计算机体系结构>;;一书,徐志伟&;黄铠,机械工业出版社)高潮好像是一种明显的候选方案。这里的想法很像剑桥分布式系统(CambridgeDistributed System)所采用的[NeHe82]。早期的口号是要构建一个脱离许多小系统的UNIX,而不是一个脱离许多小UNIX的系统。
UNIX的问题是太深难于修改,但是它的某些思想还是可以继续发展的。最好的一个就是它的文件系统的使用,用一致的方法命名和访问资源,甚至那些如设备,不再传统地看作文件。对于Plan 9,我们采用了这个思想,设计了一个称作9p的网络级协议能让机器访问远端系统上的文件。在此之上,我们构建了一个命名系统,它让人们和它们的计算代理建立网络中资源的个性化视图。这就是Plan 9第一个开始看起来不同的地方:一个Plan 9用户构建一个私人的计算环境还可以在任何想要的地方重新创建,而不是在一个私人机器上做所有计算。很快就变得清楚了,这个模型比我们预见的还要更丰富,像每个进程的名字空间和类文件系统资源这样的想法被扩展到整个系统--到进程,图形,甚至网络本身。
到1989年,系统已经足够稳定,以至于我们当中的一些人把它用作我们的高级(exclusive)计算环境。这就意味着带来我们已在UNIX上使用的许多服务和应用。我们借此机会重新思考了许多问题, 而不仅仅是驻留在内核中的,我们认为UNIX被很差地访问。Plan 9有新的编译程序,程序设计语言, 程序库,窗口系统和许多新的应用程序。许多旧的工具已不再使用,而这些一起带来的应用程序则被改进或重写。
为什么会如此的全部包含呢?因为操作系统,库和应用程序之间的差别对操作系统研究者是重要的,但是用户却对它没有兴趣。重要的是清晰的功能性。通过建立一个全新的系统,我们能去解决我们认为应当要解决的问题。例如,内核中没有实际的'tty 驱动程序';因为那是窗口系统的职责。在当今世界,多家销售商和多种体系结构的计算机这一事实是关键的,而通常的编译程序和工具假定程序是被建立在本地环境运行的;我们有必要重新思考这个问题。然而最重要的是系统的其余部分是它所提供的计算环境。探索一个运行老的UNIX系统更有效率的方式是吃力不讨好的;我们对由底层系统体系启示得来的新想法是否能够促进工作效率的提高更感兴趣。因此,Plan 9为运行POSIX 命令提供一仿真环境,它是系统的死水(backwater)。系统软件的绝大部分都是在本地的Plan 9 环境中开发出来的。
拥有一个全新的系统有几个好处。首先,我们的实验室由一个制造试验用外设板卡的历史。为了使写驱动程序更简单些,我们想要一个原始形式可得的系统(不再向UNIX保证,即使在它所诞生的实验室)。其次,我们想重新发布我们的作品,这就意味着软件必须本地化生成。例如我们已经在我们的系统上使用了某些销售商的C编译程序,但是即使我们克服了交叉编译的问题,我们还会有重新发布最终软件的困难。
本篇论文是对Plan 9系统的概述。它讨论了系统结构--从最底层的构件模块到用户可见的计算环境。也作为对Plan 9程序员手册的其它部分的导论,本论文和它们放在一起。关于论文中相关主题的更多细节可以在手册中的其它地方找到。
设计
系统的视图构建在三个原理的基础上。第一,资源像层次式文件系统中的文件一样被命名和访问。其次,有一个名叫9P的协议用于访问这些资源。第三,不同服务提供的不相交的层次被连接进单一的私有的层次式文件名字空间。Plan 9的不同一般的特性来源于对这三个原则的始终如一的和主动的应用。
大型的Plan 9的安装有许多通过网络连接在一起的计算机,每一台提供一个具体的服务级别。共享型多处理器服务器提供计算周期;其它大机器提供文件存储。这些机器放置在带空调机的房间,且用高性能的网络连接起来。低带宽网络如以太网(Ethernet)或ISDN把这些服务器连接到办公室和家里的工作站或PC,在Plan 9术语中叫终端。图1显示了这样的布局安排。
计算的现代方式是为每一个用户提供一台专用的工作站或者PC。Plan 9的方法是不同的。各种不同的机器都有提供访问网络资源的屏幕,键盘和鼠标,因此它们功能上同连接到老式的分时系统的终端在习惯上是相同的。然而,当某人使用系统时,终端从术语上讲被用户独自占有。作为自定义硬件的替代,Plan 9给予用户自定义自己系统视图的能力,这些能力由软件提供。定制是通过给网络中公共可见的资源以局部的私有的名字来完成的。Plan 9 提供这样一个机制:用全局可访问资源的局部名字集成一个公共空间的私有视图。因为网络的最重要资源是文件,这个视图的模型是面向文件的。
客户的本地名字空间提供一个定制网络的用户视图的方法。网络中可以获得的服务都导出文件层次结构。这些对用户重要的东西被一起集成到用户的名字空间;那些没有直接影响的就被忽略了。这是一个来自 "单一全局名字空间"思想的不同的使用风格。在Plan 9中,有众所周知的名字用于服务和用于由这些服务导出的文件的单独名字,但是视图却是完全本地的。作为一个类比,考虑短语'我的房子'和说话人的家的精确地址之间的区别。后者可以被任何人使用,但前者更容易说和讲得通。还可以依赖于谁说它而改变含义,而且不会导致混淆。类似的,在Plan 9中 名字/dev/cons总是指用户的终端,而/bin/date指要运行的date命令的恰当版本,但是这些名字所代表的文件取决于执行date命令时的环境例如机器的体系结构。 Plan 9由遵循全局可理解的惯例的本地名字空间;惯例是:在局部名字面前保证健全的行为。
9P协议是用一组事务(transactions)的集合建构成的:从一个客户发送一个请求到(本地或远程) 服务器,然后返回结果。9P控制文件系统,不仅仅是文件:它包含解析(resolve)文件名字的过程和转换由服务器提供的文件系统上的名字层次结构。另一方面,客户名字空间仅由客户系统处理, 不在也不和服务器一起处理,和Sprite[OCDNW88]这样的系统不一样。而且文件的访问是在字节的水平上,而不是块,这就把9P和NFS及RFS这样的协议区分开来。Welch 写的一篇文章比较了Sprite, NFS和Plan 9的网络文件系统结构[Welc94]。
此方法用传统文件设计的,但是可以被扩展到许多其它资源。Plan 9的导出文件层次的服务包括 I/O设备,备份服务,窗口系统,网络接口和许多其它的东西。一个例子就是进程文件系统,/proc, 它提供一个干净的方法来监测和控制运行的进程。Precursor系统有一个相似的思想[Kill84],但是 Plan 9把文件隐喻推进的更深一点[PPTTW93]。文件系统模型是容易理解的,不管是系统构件者还是一般用户,因此表示类文件(file-like)接口的服务是容易构造的,易于理解的和易于使用的。 为了保护命名和访问本地和远程文件,采用了向上兼容(agreed-upon)规则,因此这种方式构建服务是为分布系统的准备性工作(ready-made)。(这与'面向对象模型'不同,在那里这些问题必须重新思考(faced anew) 对象(object)的每一个类(class)) 以下分节示例了正在实践中的这些思想。
命令级视图
Plan 9被打算从带有一个运行窗口系统的屏幕的机器上使用。它没有在UNIX意义上的'teletype'的概念。裸机系统的键盘处理是基本的,但是一旦运行了窗口系统,8½; [Pike91] ,文本就可以从弹出式菜单中的剪和贴操作来编辑,在窗口之间拷贝,等等。8½; 允许从过去的输入行编辑文本,不仅是当前输入行。8½; 的文本编辑能力强大到可以替换特殊特征如shell的历史,分页和滚动以及邮件编辑器。8½; 窗口系统不支持光标访问,除非一个为了简化连接到传统系统的终端模拟器,在Plan 9中没有光标访问式的软件。
每一个窗口在一个单独的名字空间中创建。对一个窗口中的名字空间的调整不会影响其它窗口或程序,使得对名字空间的局部修改的试验变得安全,例如当调试时从dump文件系统中代替文件。一旦调试完毕,窗口就可以被删除,所有试验机构的遗迹全部消失。应用到每一窗口拥有的私有空间的相似参数用于环境变量, 说明(和UNIX的信号相似),等等。
每一个窗口被创建来运行一个应用程序,例如shell,带有连接到窗口的可编辑文本的标准I/O。每一个窗口还有一个私有位图以及通过向这样的文件/dev/mouse,/devbitblt和/dev/cons(类似于UNIX的/dev/tty) 来多工访问键盘,鼠标和其它图形资源。这些文件由8½; 提供,它被实现成一个文件服务器。不像X窗口系统,那里一个新的应用程序要创建一个新的窗口在其中运行,一个8½; 图形应用程序通常运行在它所启动的窗口里。为一个应用程序创建一个新的窗口时可能的和有效的,但那不是系统的风格。再次和X(窗口系统)对比,在那里一个远程应用程序创建一个对X服务器的网络调用来启动运行,一个远程的8½; 应用程序为窗口察看通常在/dev目录下的mouse,bitblt和 cons文件;它不知道文件是否在本地。它为了控制窗口仅仅读和写它们;网络连接已经存在且是多工的。
预期的使用方式是诸如在终端上的窗口系统和文本编辑的交互式应用程序和运行计算-或远程服务器上文件密集(file-intensive)的应用程序。不同的窗口可能通过不同的网络运行不同的程序,但是通过让名字空间在所有窗口中等价,这是透明的:无论计算在何时执行,用相同的名字都可以获得相同的命令和资源。
Plan 9的命令集合和UNIX的命令集合是相似的。命令分成概括性的几个类别。有些是用于旧的作业的新程序:像ls,cat和who这样的程序保持相同的名字和功能,但是新的,简化了的实现。例如who命令是一个 shell脚本(script),而ps命令只有95行C语言代码。某些命令和它的UNIX祖先那里基本上是相同的:awk, troff和其它已经被转换成ANSI C的和扩充成处理Unicode,但仍然是相似的工具。某些就的事务是用完全新的程序:shell的rc文件,文本编辑程序sam,调试程序acid,以及其它用相似作用的程序替换了更为知名的 UNIX工具。最后,大约有一半的命令是新的。
兼容性不是系统的一个要求。只要老的命令或说明看上去足够好,我们就保留它,否则我们就替换它。
文件服务器
中央文件服务器存储永久文件,使用9P协议把它们导出成文件层次结构并呈现给网络。服务器是一个独立(stand-alone)的系统,只可以通过网络访问,被设计成只把它的一件事情做好即可。它不运行用户进程,只运行固定的一套被编译进引导图像的例程(routines)。服务器导出的主要层次结构(多于一组磁盘或单独的文件系统)是一棵单一的树,把文件表示在许多磁盘上。这个层次结构被广泛范围内的许多用户通过不同的网络共享。服务器导出的其它文件树包括专用的系统如临时存储和下面就要解释的一个备份服务。
文件服务器有三个存储级别。我们安装的系统中的中央服务器有大约100MB的内存缓冲,27GB的磁盘空间,还有在一个WORM点唱柜中350GB的存储容量。磁盘是WORM的高速缓冲,而内存是磁盘的一个高速缓冲;每一个都比它所缓冲的级别更快,要处理的数据流量按顺序增加。文件系统中可寻址的数据可以比磁盘的大小还要大,因为它们仅是一个高速缓冲;我们的主文件服务器大约有40GB的活动存储量。
文件服务器的最不一般的特征是使用一个WORM设备来用作稳定存储。每天早上5点钟自动发生一次文件系统的转储dump。文件系统被冻结,所有自最近一次转储(dump)以来被修改的块被排列成队列写入 WORM中。一旦块被排队,服务就被恢复,转储的文件系统的只读的根(root)出现在所有已发生的转储层次中,且以它的日期命名。例如,目录/n/dump/1995/0315是文件系统图像的一个根目录,和出现在1995年3月15日早上的一样。要花几分钟时间来排队这些块,但是拷贝块到WORM中的进程运行后台,可能要花好几个小时。
转储的文件系统有两种用途。第一种是用户自己使用,它们可以直接浏览转储文件系统,把当中的几个关连(attach)到它们自己的名字空间。例如,为了追踪一个错误,直接从三个月以前尝试编译程序或者把一个程序和昨天的库连接。用所有文件的每日快照,那么查找什么时候一个特殊的改变发生或一个特别的日子发生了什么改变这样的事是容易的。在了解文件可以用一个拷贝命令收回的条件下,人们可以自由地对文件做大的冒险性的改变。没有备份这样的系统;相反,因为转储是在文件名字空间中,备份问题可以诸如cp,ls,grep和diff这样的标准工具来解决。
其它的(很少)使用是完全系统备份。如果发生灾难,通过清除磁盘高速缓冲和把活动文件系统的根设置为转储的文件系统的根,这样就可以从任何转储来初始化活动文件系统。尽管容易做,还是不能轻率的完成:除了丢失了转储后所做的任何改变,还会导致系统速度很低。高速缓冲必须从WORM重新装入,它比磁盘慢的多。文件系统要花好几天重新装入工作集(working set),重新获得它的全部性能。
转储中的文件的访问权限和转储生成时是一样的。普通的实用程序有正常权限,在转储中没有特殊的处理。然而,转储文件系统是只读的,这就意味着转储中的文件的无论权限位如何都不能被写;实际上,既然目录是只读结构的一部分,甚至权限都不能被改变。
一旦一个文件被写入WORM中,它就不能被删除,因此我们的用户从来不会看到“请清除你的文件”这样的消息,也没有df命令。我们把WORM柜看作一个无限的资源。只有一个问题就是花多长时间就会写满数据。我们的WORM已经为一个大约50个用户的社团服务了五年,吸收每日的转储,耗费了总量的65%的存储空间。那时,制造商已经改进了技术,把每一个磁盘的容量翻倍(doubling)。如果我们打算升级成新的介质,我们就会有比当初的空磁盘更多的空闲空间。技术已经创造了比我们所使用的更快的存储设备。
不寻常的文件服务器
Plan 9的特征表现在有许多这样的服务器:它们向不一般的服务提供一个类文件(file-like)的界面。许多都是由用户级进程实现的,尽管对于它们的客户而言差别是不重要的;一个服务是由内核,一个用户进程还是一个远程服务器来提供和服务的使用方式是不相关的。有许多这样的服务器;本节我们介绍三个代表性的服务器。
Plan 9种可能最引人注目的文件服务器是8½; ,即窗口系统。它在别 的地方[Pike91]有详细的讨论,但是此处值得给一个简要的解释。8½; 提 供两个接口:给终端旁的用户,它提供和多窗口交互的传统风格,每一个都运行一个应用程序,都由 鼠标和键盘控制。给客户程序,视图也是相当传统的:运行在一个窗口中的程序看到/dev目录下的一 些文件,如 mouse,screen 和cons。想要把文本显示到它们对应窗口的程序向/dev/cons中写;读鼠 标的话,则读 /dev/mouse。以Plan 9的方式,位映射图形是这样实现的:提供一个文件 /dev/bitblt, 客户程序把经过编码的消息写到执行图形运算的程序如bitblt(RasterOp)。所不平常的是这是如何 完成的:8½; 是一个文件服务器,用/dev目录下的文件向运行在每一个窗口 中的客户提供服务。虽然每一个窗口看上去和它的客户一样,但是每一个窗口拥有/dev目录下的不同的 一组文件。8½; 通过服务多组文件来多工它的客户对终端资源的访问。通过 一个不同的文件组为每一个客户给定一个私有名字空间,这些文件组的行为和在所有其它窗口 中一样。这样的结构由许多优点。其中之一就是为相同的文件,用于它自己的实现所需要的-它多工自 己的界面-因此它可以运行,急救,把自己作为一个客户。另外,考虑 UNIX 中/dev/tty的实现,它要 求在内核中有特殊的代码把open 调用重新定向到适当的设备。相反,在 8½; 中,等价的服务自动地实现:它把/dev/cons作为基本的功能;决没有什么其它要做的。当程序象想从 键盘上读时,它打开/dev/cons,但它是一个私有文件,不是一个有特殊属性的共享文件。还有,局部 名字空间让这成为可能;它们内部的文件一致性的惯例让这变得自然。
由于8½; 的设计使得它由一个统一的特征成为可能。因为它被实现成为 一个文件服务器,它有推迟对一个具体窗口的读请求做出回答的权力。这个行为由键盘上的一个保留键 触发。触发一次对来自窗口的客户读请求的挂起;再一次触发就让正常的读继续进行,它接受任何已经 准备好的文本,一次一行。这就允许用户在引用程序得到文本之前就可以在屏幕上编辑多行输入文本, 避免了需要调用一个特别的编辑程序来准备象邮件消息这样的文本。一个相关的性质是,读直接 地从显示器上的文本定义的数据结构得到回应:文本可以被一直编辑直到它的最后新行符让文本的 预备行由客户可读为止。甚至即使到行已读入,客户要读的文本还可以被改变。例如,输入以下 命令给shell后
% make
rm *
,用户可以在make完成之前的任何时候会退过最后的新行符,延迟rm命令的执行,或者甚至在 rm之前用鼠标点击,输入另一个命令先执行。
Plan 9中没有ftp命令。相反,一个叫ftpfs的用户级文件服务器向FTP站点拨号,代表用户登录,用FTP 协议检查远程目录中的文件。对于本地用户而言,它提供一个文件层次结构,关连到本地名字空间中的/n/ftp,镜像了FTP站点的内容。也就是说,它把FTP协议转换成9P协议来提供Plan 9访问FTP站点的能力。实现是复杂而微妙的;出于效率ftpfs必须做一些 复杂的缓冲和用启发式方法来解码(decode)目录信息。但是结果是值得的:所有本地文件 管理工具诸如cp,grep,diff当然还有ls对于FTP服务提供的的文件就像它们是本地文件一样。其它系统如Jade和Prospero已经开拓出一些机会[Rao81,Neu92],但是由于本地名字空间和9P 协议实现的简洁性,比起其它环境,这个方法更自然地适合Plan 9。
一个名叫exportfs的用户进程把它自己所有的名字空间的一部分变为其它进程可获得的 ,方法是把9P请求转换成对Plan 9内核的系统调用。它所导出的文件层次结构可能包含来自 多个服务器的文件。Exportfs 通常由本地程序启动作为远程服务器运行,要么是import,要么是cpu。Import创建一个对远程机器的网络调用,在远程机器上启动exportfs,然后把它的9P连接(connection)关联(attache)到本地名字空间。例如,
import helix /net
让Helix的网络接口在本地/net目录中可见。Helix是一个有许多网络接口的中央服务器,因此这就允许有一个网络的某台机器可以访问到任何Helix的网络。这样一个导出之后,本地机器可以创建连接到Helix的任一网络的调用。另一个例子是
import helix /proc
,它让Helix的进程在本地/proc目录中可见,允许本地的调试程序检查远程进程。
cpu命令把本地终端连接到一个远程CPU服务器。它以和import相反的方向工作:调用服务器之后,它启动一个本地的exportfs进程,把它挂接到一个进城的本地名字空间中,典型的是服务器上的一个新创建的shell。然后它重新安排名字空间让本地设备文件(如那些由终端的窗口系统所服务的设备) 在服务器的/dev目录中可见。运行一个CPU命令的效果就是在一台快速的机器上启动shell,一个模拟本地名字空间的更紧密地耦合到文件服务器的shell。所有本地设备文件都是远程地可见的,因此远程应用程序具有完全访问本地服务的能力,例如位映射图形,/dev/cons等等。这和rlogin不一样,它(rlogin) 不会在远程系统上重新生成本地名字空间,而且和NFS上的文件共享不一样,NFS可以获得一部分对等的名字空间但不是对本地硬件设备,远程文件和远程CPU资源访问的复合。CPU命令是一个单一地透明机制。比如,在一个正在运行CPU命令的窗口中启动一个窗口系统是适当的;所有已经创建的窗口自动地启动CPU 服务器上的进程。
配置能力和管理
Plan 9中组件(components)的统一互连使得以多种方式配置Plan 9的安装成为可能。一个单独的膝上型PC可以作为一个单独的(stand-alone)Plan 9系统;另一个极端是我们的安装有中央的多处理器CPU服务器和文件服务器以及许多终端(从小的PC到高端图形工作站)。这样大的安装最好地展示了Plan 9是如何工作的。
系统软件是可移植的而且同样的操作系统运行在所有硬件上。除了系统的性能,系统的外观对 SGI工作站和一台膝上型电脑是一样的。既然计算和文件服务被集中了,而且终端没有永久文件存储, 所有终端功能上是相同的。 像这种方式,Plan 9 有一个老的分时系统的好的特点,即用户可以坐在任何机器面前而且看到的是相同的系统。像现代工作站的一致性,机器倾向于被个人占有,它们在机器的本地硬盘上存储私人信息来定制它们的机器。尽管系统本身可以这样使用,但是我们拒绝这种使用方式。在我们的研究组里,我们的实验室有许多可以公共使用的机器---一个终端室,用户可以坐在任何一个终端面前工作。
中央文件服务器不仅集中了文件,而且包括它们的管理和维护。实际上,一个服务器是主服务器 ,处理所有系统文件;其它服务器提供额外的存储或者用于调试和其它特殊用法,但是系统文件驻留在一台机器上。这就意味每一个程序对于每一种体系结构有唯一的二进制副本,因此安装更新(updates) 和排除错误(bug fixes)的价值不大。还有一个单一的用户数据库;没有必要去同步不同的/etc/passwd 文件(即让/etc/passwd文件尽快保持相同,译者注)。另一方面,对于一个中央服务器的依赖并不一个安装的大小。
集中式文件服务的另一个强大能力的另一个例子Plan 9管理网络信息的方式。在中央服务器上有一个目录/lib/ndb,它包含所有管理本地以太网和其它网络的必要的信息。所有的机器使用相同的数据库和网络对话;没有必要去管理一个分布式命名系统或者保持并行的文件都是新的。为了在本地以太网上安装一个新的机器,选择一个名字和IP地址,然后把这些加入到/lib/ndb目录下的单个文件中;安装中的所有机器可以立即和它对话。为了启动运行,把机器加入到网络中,打开机器然后使用BOOTP和TFTP协议装入核心。其它的一切都是自动完成的。
最后,自动的转储文件系统根据维护它们的系统的需要释放所有用户的空间,然而提供一个方便的访问备份文件系统的方法--不需要磁带,特殊命令或者重要支持的复杂情况。夸大这种服务提供的工作方式的改进是困难的。
Plan 9可以运行在许多硬件上,不限制如何设置一个安装。在我们的实验室,我们选择使用中央服务器因为它们让投资和管理可以分期偿还。这是一个好的决定的标志就是我们的便宜的终端已经适宜地工作了大约5年时间,比那些必须提供完整的计算环境的工作站的时间长的多。然而,我们的确升级过中央的机器,因此甚至旧的Plan 9终端可以获得的计算能力也能及时的改进。通过避免对终端的正常升级,节省的钱被花在最新的,最快的多处理器服务器上。我们估计这个成本只花了联网的工作站的一半,却提供了对更强大的机器的通常使用(general access)。
C 语言程序设计
Plan 9的实用程序使用了好几种语言写成。一些是shell(rc)[Duff90]的脚本(script),有一小部分是用新型的称为Alef[Wint95]的类似C的并行编程语言写成,在下面有介绍。不过,绝大多数还是用 ANSI C[ANSIC]的方言(dialect)版本写成。这些程序中绝大多数是全新的,但是有一些是从我们的研究用的UNIX系统[UNIX85]的前标准C代码。这些已经被升级到ANSI C,出于移植和干静又重写了一遍。
Plan 9的C语言有一些小的扩充和几个主要的限制,在[Pike95]有所介绍。最重要的限制是编译器要求所有函数定义有ANSI的原型(prototype),而且所有的函数调用出现在函数原型声明的作用域。作为一个文体规则,原型的声明放在一个被所有调用那个函数的头文件中。每一个系统库有一个关联的头文件,它定义了那个库中的所有函数。例如,标准的Plan 9库被称为libc,所以所有C源代码文件包含。这些规则保证所有函数被调用时带有期望类型的参数--对于ANSI C之前的C代码则不是这样的。
另一个限制是C编译器只接受ANSI要求的预处理程序制导的子集。主要的省略是#if,因为我们认为这是从不必要的而且经常混淆。同样,它的作用可以通过其它的方法更好的得到。例如,使用一个#if在编译时触发一个特征,它可以被写成正规的if语句,取决于编译时常量卷和为了释放目标码的固定代码省略量。(此句较难译,望专家指教)
条件编译甚至是#ifdef在Plan 9中也是很少使用的。系统中唯一的依赖于体系结构的 #ifdefs是图形库中的低层例程。相反,我们避免这样的依赖性,或者当必要时把它们放入单独的源文件或库中。除了使代码难于阅读以外,#ifdefs让知道什么源码被编译进库或者由它们保护的源代码是否会正常的编译和工作。它使得维护软件更难。
标准的Plan 9库有许多和ANSI C及POSIX[POSIX]重叠,但是适合Plan 9的目标和实现时是分开的。当一个函数的语义改变时,我们也改变名称。例如,相对于UNIX的creat, Plan 9有一个create函数它带有三个参数,原来的两个加上第三个---和open的第二个参数一样,定义了返回的文件描述符被打开来读,写或者读写。9P实现的创新方式促成了这个设计,但是它也简化了create的通常使用来初始化临时文件。
另一个不同于ANSI C的是Plan 9使用称为Unicode [ISO 10646,Unicode] 16位字符集。尽管我们避免了完全国际化的缺点,Plan 9 通过它的所有软件统一了对所有主要语言的表示。为了简化在程序之间文本的交换,字符使用了我们设计的编码方法打包成字节流,这个方法称为UTF-8,现在已经变成一个被接受的标准[FSSUTF]。它有几个具有诱惑力的特点,字节顺序无关性,和ASCII码向后兼容以及易于实现。
把已经存在的软件转换为大字符集的会有许多问题,这种大字符集的编码用可变数目的字节来表示字符。ANSIC提到过这些问题但是并没有完全解决。它不选择一个字符集的编码,也不定义所有必要的I/O库例程。更何况,它的函数定义有工程上的问题。既然表准遗留了太多未解决的问题,我们就决定构建我们的界面。另有一篇专题论文讨论细节[Pike93]。
小规模(class)的Plan 9程序不遵循本届所讨论的惯例。由一些来自UNIX社团维护的程序被收入系统; tex就是一个代表性的例子。为了避免每次重新移植这些程序就要发行一个新版本,我们构建了一个移植(porting)环境,称为ANSI C /POSIX环境,或APE[Tric95]。APE有独立的包含文件;库和命令组成,尽可能地和严格的ANSI C和基本的POSIX 规范一致。为了移植基于网络的软件如X-Windows,对这些规范增加一些扩充是必要的,就像BSD的联网功能。
可移植性和编译
Plan 9可以移植到许多处理器结构上。在单一的计算会晤期(Session)内,使用好几种体系结构是普遍的:窗口系统可能运行在一个连接到基于MIPS的CPU服务器上的Intel处理器,此服务器和文件主留在 SPARC系统上。为了达到异构透明性,必须有程序间交换数据所需要的协议;为了直接的维护软件,必须有跨平台编译的协议。
为了避免字节顺序问题,数据以文本形式在程序间传送任何时候都是可行的。尽管有时数据总量高到需要二进制形式,这样的数据用多字节值的预定义编码来作为字节流传送。在很少情况下,格式复杂到要用数据结构定义,此结构从不作为一个单元传送;相反,它先被拆分成单独的域,再编码成顺序字节流,最后由接受器重新汇编。这些协议影响很多数据,从内核到应用程序状态信息和由编译器生成的中间目标文件。
包括核心在内的程序常用文件系统接口表示它们的数据,这个借口是继承性的可移植访问机制。例如系统时钟由在文件/dev/time中的十进制数表示;time库函数(没有time系统调用)读文件然后转换成二进制形式。类似的,和把一个应用进程的状态编码到私有存储中的一些标志和为中不一样,核心把文本字符串表示在一个叫status的文件中,status文件在和每个进程关联的/proc目录。Plan 9的ps命令是简单的:它经过某些简单的重新格式化后打印想要的文件的状态信息;在输入下面的命令以后 import helix /proc 一个本地的ps命令给出Helix进程的状态报表。
每一个支持的体系结构都有相应的编译程序和装入程序。C和]Alef编译程序生成的中间文件是经过可移植编码的;内容相对目标结构是统一的,但是文件格式是独立于编译处理器类型的。当给定体系结构的编译程序在另一种处理器类型上编译时,在新的处理器上编译一个程序,生成的中间文件和原处理器上生成的文件是相同的。从编译程序的角度看,每一次编译都是交叉编译(cross-compilation)。(交叉编译:在一种处理器上为编译另一种体系结构的可执行文件而进行的编译-译者注。)
尽管每一种体系结构的装入程序只接受由对应的编译程序生成的中间文件,这种中间文件可以在另一种类型的处理器上编译得到。例如,在486上运行MIPS编译器,然后用SPARC上的MIPS装入程序生成MIPS可执行文件是可能的,
因为Plan 9可以运行在许多体系结构上,即使在单机安装中,在单一的源代码树种区别编译程序和中间名字简化了多体系结构的开发。每一种体系结构的编译程序和装入程序被统一命名;系统里没有CC命令(CC:c语言编译程序-译者注)。
名称通过和目标体系结构相关联的编码字母的连接而得到,还和编译程序及装入程序的名字有关。例如,字母‘s’是Intel X86处理器的编码字母;相应的编译程序叫8c;Alef编译程序称为为8al,而装入程序称为8l。类似的,编译程序的中间文件名称为。8而不是.O后缀。
Plan 9的构造程序叫mk,一个和make相对的程序,它从环境变量$cputype和$objtype读出当前的和目标的体系结构名称。默认的,当前处理器即为目标处理器,但是可以设置$objtype的值为另一个体系结构,让mk进行交叉构造:
%objtype = sparc mk
为sparc体系结构构造一个程序(不管执行的处理器为什么类型)。$objtype变量的值选择一个体系结构相关的变量定义文件,此文件把构造配置成使用适当的编译程序和装入程序。虽然此方法简单,但这个技术在实践中工作的很好:Plan 9中的所有应用程序都从单一源代码树构造,以并行而无冲突地构造不同体系结构的应用程序是可能的。
并行程序设计
Plan 9对并行程序设计的支持有两个方面。首先,核心提供一个简单的进程模型以及一些用于同步和共享而仔细设计的系统调用。第二,一个新的称为Alef的并行程序设计语言支持协同程序设计。尽管用C写并行程序是可能的,但是Alef是并行语言的当然选择
在新式操作系统领域中有一个趋势,就是实现两极进程:普通的Unix方式的进程和轻量级内核线程。相反,Plan 9一个进程级别,但是能够很好地控制一个进程的资源的共享(这些资源如内存储器和文件描述符)。单一级别的进程在Plan 9中之所以是可行的,因为核心有一个有效的系统调用接口和低开销的进程创建和调度方法。
并行程序有三个基本的要求:进程间共享资源的管理,一个调度接口和一个使用自旋锁(spin locks)的细粒度(fine-grain)的进程同步机制。在Plan 9上,新的进程用rfork系统调用创建。Rfork使用一个参数和一个向量(它指定父进程的哪一个资源应被共享,复制,或在子进程中创建新的)。Rfork控制的资源包含名字空间,环境,文件描述符表,内存段和说明(对Unix信号的Plan 9模拟)。其中一位控制rfork调用是否创建新进程;如果此位关闭,最终对资源的修改发生在进行调用时。例如,一个进程调用rfork(RFNAMEG)把它的名字空间和父进程的名字空间分开。Alef使用一个细粒度的fork,其中所有的资源在(包括内存储器)在父子进程间共享,和许多系统中创建一个内核线程相似。
Rfork的使用方式是多种多样的说明了rfork是合适的模型。和fork过程的通常使用不同很难发现两个使用rfork的调用有相同的位集合;程序用它创建许多不同形式的共享和资源分配。仅有两类进程的系统-正规进程和线程-不能处理这样的变化。
共享内存有两种方式。第一,rfork的一个标志使得父进程的所有内存段和子进程共享(除了栈,它是写时复制的)。可选地,内存的一个新段可用segattach系统调用接上;这样的段将总被父子进程共享。
Rendezvous系统调用提供进程同步的方法。Alef用它实现通信通道,排队锁,多个读写锁和睡眠唤醒机制。Rendezvous使用两个参数:一个标记和一个值。当一个进程调用带一个标记的rendezvous时,它一直睡眠到另一个进程产生一个匹配的标记。如果一对标记匹配,那么值就在两个进程间交换且两个rendzevous调用都返回。此原语已经足够实现全部的同步过程。
最后,在用户级的体系结构相关库提供自旋锁。大多数处理器提供可以用来实现锁的原子的测试设置指令。一个著名的例外是MIPS R3000,因此SGI Power系列多处理器在总线上有特殊的锁硬件。用户进程通过使用segattach系统调用访问锁硬件,这个调用把硬件锁的页照应到它们的地址空间。
在系统调用中的Plan 9进程无论它的优先级怎样都会阻塞。这就是说当一个程序希望从没有阻塞整个计算的慢速设备上读时,它必须为读创建一个进程。方法是启动一个辅助进程通过共享内存或一个管道来进行I/O和传递对主程序的反应。这听起来很麻烦而实际上很容易且效率很好;事实上,大多数Plan 9交互程序都是作为多进程程序运行的,即使是用C写的相对简单的文本编辑程序sam[Pike87]也是如此。
Plan 9中内核对并行程序设计的支持有几百行可移植代码;几个简单的原语使问题可以干静的在用户级处理。尽管原语用C工作的很好,但是在Alef中是特别有意义的。从属的I/O进程的创建和管理可以用几行的Alef写成,为任意进程间的多工数据流的一致方法提供基础。而且在语言中实现而不是在内核中为所有设备间保证一致的语义以及提供一个更通用的多工原语。把它和Unix中的select系统调用相比较:select仅用在设备的优先集合中,确立了内核多进程程序设计的风格,但是未扩展到网络,它还难于实现和使用。
Plan 9中并行程序设计重要的另一个原因是多线程的用户级文件服务器是实现服务的更好的方法。这样的服务器的例子包括程序设计环境Acme[Pike94],名字空间导出工具exportfs[PPTTW93],HTTP守护程序和网络名字服务器,和DNS[Prwi93]。像Acme这样复杂的应用程序证明精心的操作系统支持可以降低写多线程应用程序的难度,不用把线程和同步原语移动到内核中。
名字空间的实现
用户进程用三个系统调用构建名字空间:mount,bind和unmount。mount系统调用把一个文件服务器生成的树添加到当前名字空间。在调用mount之前,客户(通过外部方法)请求一个到服务器的连接,用一个可以被读写用来传递9p消息的文件描述符的形式实现请求。那个文件描述符表示一个管道或网络连接。
Mount系统调用把一个新的层次添加到现存的名字空间。另一方面,bind系统调用在名字空间的另一点上复制现存名字空间的一部分。Unmount系统调用可以让组成(名字空间的)部分被删除。
使用bind或者mount,多个目录可以在名字空间的一个点上建栈。用Plan 9的术语说这是一个联合(union)目录,行为像成组目录的连接一样。Bind和mount的一个标志参数指定在联合目录中新目录的位置,允许在联合目录之前或之后增加新元素或用新元素整个地替换。当在联合目录中执行查找文件时,联合目录的每一部分被依次查找,首次匹配的作为结果;同样的,当读一个联合目录时,组成目录的每一部分的内容被依次读取。联合目录是Plan 9名字空间的组成性特征中最广泛使用的一个。例如,目录/bin被构造成/$cputype/bin(程序二进制文件),/rc/bin(shell脚本)以及可能由用户提供的更多的目录组成的一个联合目录。这种构造使得shell的$path变量成为多余的。
Union目录带来了一个问题:Union的哪一个元素接受新创建的文件。经过几次设计之后,我们决定采用下述方法。默认的,union中的目录不接受新创建的文件,尽管应用到一个已存在文件的creat系统调用正常地完成了。当一个目录被加到一个联合目录中时,bind和mount的一个标记允许在一个目录中有创建权限(名字空间的一个属性。当在一个联合目录中创建一个新文件时,它以创建权限被创建在union中的第一个目录中;如果创建失败,整个creat调用也失败。这个方案使得通常把一个私人目录放在公共的联合目录中的任何一个中,而仅允许在私人目录中创建文件。
根据约定,核心设备文件系统被绑定到/dev目录中,但是为了自举名字空间构造进程,必要说明一下:允许对没有现存名字空间的设备的直接访问。有设备驱动保存的树的根目录可以用语法#c访问,c是唯一的确定设备类型的字符(典型地是一个字母)。简单的设备驱动器提供包含一些文件的单级目录。作为一个例子,每一个串行端口被表示成一个数据和一个控制文件:
% bind -a '#t' /dev
% cd /dev
% ls -l eia*
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia1
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia1ctl
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia2
--rw-rw-rw- t 0 bootes bootes 0 Feb 24 21:14 eia2ctl
bind程序是bind系统调用的一个封装;-a标记定位新目录在union的末尾。数据文件eia1和eia2可以通过串行线通信读写。与这些设备的控制对文件进行特殊操作不同,为eia1ctl和eia2ctl写的命令控制相应的设备;例如把文本字符串b1200写到/dev/eia1ctl把线速设置为1200波特率。和Unix的ioctl系统调用相比:在Plan 9中设备由文本的消息控制,不受字节顺序问题约束,且有清晰的读写语义。用shell脚本设置或调试设备是平常的。
普遍用9P协议把Plan 9的组成部分联在一起形成一个分布的系统。没有为每一个服务如rlogin,FTP,TFTP和X windows去发明新的协议,Plan 9用对文件对象的操作的方法实现服务,然后使用唯一的,经过良好说明的协议在计算机间交换信息。和NFS不一样,9P把文件看作是字节的序列而不是块的序列;还有不同,那就是9P是有状态的:客户执行远程调用过程建立指向远程文件服务器上的对象的指针。这些指针称为文件ID或fids。对文件的所有操作提供一个fid来识别远程文件系统中的一个对象。
9P协议定义17个消息,通过这些消息鉴别用户,在一个文件系统层次中遍历fid,拷贝fid,执行I/O,改变文件属性,以及创建和删除文件。它的完整规范在程序员手册(9man)的第五部分。下面是访问一个文件服务器上名字层次的过程。通过管道或网络连接建立和文件服务器的连接。一个最初的session消息执行客户进程和服务进程间的双方鉴别。attache消息把由客户进程建议的fid连接到服务器文件树的根。attache消息包括执行attache的用户ID号;自此以后,所有从根fid获得的fid拥有与那个用户相关的权限。多个用户可以共享连接,但每一个用户必须执行一个attache建立它/它的ID。
Walk消息在文件系统层次的单级间移动fid。clone消息得到一个已经建立的fid然后生成一个指向和原来文件相同的指针的拷贝。目的是在目录中移动一个文件时不丢失目录上的fid。Open消息把一个fid锁到层次中的一个具体的文件上,检查访问权限,以及为I/O准备fid。Read和write消息允许在文件中的任何位置进行I/O;传送的最大值已由协议定义。clunk消息说明客户进程没有为一个fid的进一步使用。Remove消息的行为和clunk相似,但是使和fid相关联的文件被删除以及服务器上任何相关的资源都被释放。
9p有两种形式:通过管道或网络连接发送的RPC消息和内核中的过程接口。因为核心设备驱动是直接可寻址的,就没有必要传送消息和它们通信;相反,每一个9p事务由一个直接的过程调用实现。对每一个fid,核心用一个称为channel的数据结构维护一个本地表示;因此由核心执行的所有对文件的操作都涉及到和fid关联的一个通道。最简单的例子是一个用户进程的文件描述符。它由通道构成的数组来索引。核心中的一张表提供和每一设备相应的9P消息一一对应的入口点。如来自用户的读调用通过那张表被转换成一个或多个过程调用,用存储在通道中的类型字符索引:procread,eiaread等等。每一个调用至少使用一个通道作为参数。一个特殊的核心驱动称为mount驱动把过程调用转换成消息,即它把本地过程调用转换成远程的。从效果上看,这个特殊驱动变成远程文件服务器上文件的本地代理。本地调用中的通道指针被转换成传送消息中相关联的fid。
Mount驱动是系统使用的唯一RPC机制。提供的文件的语义,而不是在它们上执行的操作创建了一个像cpu命令这样的具体服务。Mount 驱动解多路协议消息,这些消息在和文件服务器共享通信通道的客户进程间。对每一个发出的RPC消息,mount驱动为之分配一个用小整数(称为tag)标记的缓冲区。对RPC的回应也用它标记上,这被mount驱动用来匹配请求和回应。
名字空间的核心表示称为mount表。它保存一张通道间绑定列表。Mount表中的每一个入口包含一对通道:一个from通道和一个to通道。每次一个遍历(walk)成功地把一个通道移动到一个名字空间中新的位置时,就检查mount表是否一个“from”通道与新名字匹配;若是则克隆(clone)“to”通道代替原来的。联合目录通过把“to”通道转换到一张通道列表中实现:到联合目录的成功遍历返回一个“to”通道,它构成通道列表头,每一个都表示联合目录的组成目录。如果遍历没有在联合目录中的首目录中发现文件,则列表被接上,克隆下一个组成目录,且尝试遍历那个目录。
Plan 9中的每一个文件由一组整数唯一的识别:通道的类型(用作函数调用表的索引),服务器或其它相同类型(由驱动程序本地决定)的服务器相区别的设备号和一个来自名叫path和version的32位数字形成的一个qid。路径是创建文件时一个由设备驱动或文件服务器指定的文件号。只要文件被修改版本号就更新;像下一节描述的一样,它用作维护客户和服务器之间的高速缓存一致性。
类型和设备号是对UNIX主次设备号的模拟;qid是对i号的模拟。设备和类型把通道连接到一个设备驱动而qid则识别设备中的文件。如果从遍历中恢复的文件有相同的类型设备和作为安装表中入口点的qid路径,则它们是相同的文件,进行安装表中相应的替换。名字空间就是这样实现的。
文件高速缓冲
在客户端9P协议没有清楚的对高速缓冲文件的支持。文件服务器的大内存扮演它的所有客户的一个共享高速缓冲,它降低了网络上所有机器需要的内存总量。不过在客户端高速缓冲文件有合理的原因,诸如一个到文件服务器的低速连接。
Qid的版本域在每次文件被修改时都变化,它使得弱的高速缓冲一致性形式成为可能。最重要的是客户缓冲文本和可执行文件的数据段。当一个进程执行(execs)一个程序时,文件被重新打开,把qid的版本和高速缓冲中的相比较;若匹配,则使用本地备份。可以使用相同的方法构建一个本地缓冲文件系统。这个用户级服务器把9P连接插入到远程服务器,监视本地磁盘的流量和拷贝的数量。当它看到一个队已知数据的读时,它直接回答,而写被立即传送,使高速缓冲被写穿(write-through)保持了中央拷贝为最新。这对终端上的进程是透明的且不要求改变9P;它在串行线连接的家用机上工作的很好。相似的方法可用来在未用的本地存储器上构件一个通用的客户高速缓冲,但是在Plan 9中没这样做。
网络及通信设备
网络接口是驻留在核心的文件系统,是对较早描述的EIA设备的模拟。通过向和设备相关的控制文件写入文本字符串实现调用设置和关闭;通过读写数据文件来收发信息。设备的结构和语义对所有的网络是公共的,而不是一个文件名替换,同一过程创建一个跨以太网的使用TCP的调用,像URP对Datakit一样[Fra80]。
这样的例子演示了TCP设备的结构:
% ls -lp /net/tcp
d-r-xr-xr-x I 0 bootes bootes 0 Feb 23 20:20 0
d-r-xr-xr-x I 0 bootes bootes 0 Feb 23 20:20 1
--rw-rw-rw- I 0 bootes bootes 0 Feb 23 20:20 clone
% ls -lp /net/tcp/0
--rw-rw---- I 0 rob bootes 0 Feb 23 20:20 ctl
--rw-rw---- I 0 rob bootes 0 Feb 23 20:20 data
--rw-rw---- I 0 rob bootes 0 Feb 23 20:20 listen
--r--r--r-- I 0 bootes bootes 0 Feb 23 20:20 local
--r--r--r-- I 0 bootes bootes 0 Feb 23 20:20 remote
--r--r--r-- I 0 bootes bootes 0 Feb 23 20:20 status
%
顶级目录/net/tcp包含一个clone文件和一个用于每一个连接的目录,号数从0到n。每一个连接目录对应于一个TCP/IP连接。打开clone保留一个未用的连接,且返回它的控制文件。读这个控制文件就返回文本形式的连接号,因此用户进程可以创建新分配的连接目录的完整名字。Local,remote和status文件是诊断;例如remote包含远端的地址(对于TCP,是IP地址和端口号)。
通过写一个connect消息初始化一个调用,此消息带有一个作为参数的具体的网络地址;例如,为打开一个和远程机器的Telnet会话(端口号23)用IP地址135.104.9.52,字符串是:
connect 135.104.9.52!23
向控制文件写块(block)直到建立了连接;如果目标不可到达,则写返回一个错误。一旦建立连接,telnet应用程序读写数据文件以和远端的Telnet守护程序对话。在另一端,Telnet守护程序启动,通过写:
announce 23
到它的控制文件说明它希望接受对此端口的调用。在Plan 9中这样的守护程序被称为listener。
网络设备的一个统一结构不能隐藏不同网络的寻址和通信细节。例如,Datakit使用文本的层次式的地址(和IP的32位地址不同),因此给出一个控制文件的应用程序必须还要知道它表示的网络。不是让每个应用程序都知道每个网络的寻址,Plan 9把这些细节称为CS的连接服务器中。CS是一个安装在已知位置的文件系统,它提供一个控制文件,应用程序用它发现如何连接到主机。应用程序为想要创建的连接写符号的地址和服务名,读回要打开的克隆文件的名字和表示它的地址。如果在机器间有多个网络,CS就表示出一张可能网络和地址的列表,以逐个尝试;它使用启发式方法确定顺序。例如,它表示成最高带宽优先的。
一个称为dial的库函数和CS对话以建立连接。使用dial的应用程序不需要改变,甚至不需要重新编译就能适应新的网络;CS的接口隐藏了细节。
在Plan 9中网络的统一结构使得import是所有建构网关所需要的。
网络的核心结构
用来构建Plan 9通信通道的内核探查被称为streams[Rit84][presotto]。流是一个双向通道。它把一个物理的设备或伪设备连接到一个用户进程。用户进程在流的一段插入和删除数据,内核进程根据设备在另一端操作。一个流由一个进程模块的线性列表组成。每一模块有一个向上流(朝向进程)和向下流(朝向设备)的put过程。在流的任一端调用put过程就把数据插入到流中。每一个模块调用后继的一个以向上或向下发送数据。像Unix的流一样[Rit84],Plan 9的流可以被动态的配置。
IL协议
9P协议必须运行在带定界消息的可靠传输协议上。9P没有从传输错误中恢复的机制,系统假定每一次对通信通道的读会返回一个9P消息;它不会为了发现消息边界而分析数据流。管道和一些网络协议已经有这些属性,但标准的IP协议没有。TCP不划定消息边界,而UDP[RFC768]不提供可靠的顺序吞吐。
我们设计了一个新协议称为IL(Internet Link),通过IP传输9P消息。它是一个基于连接的协议,提供机器之间可靠的顺序消息传送。因为每一进程只有一个未决的9P请求,因此IL不需要流控制。像TCP一样,IL有自适应超时设定:它测定(scale)确认与重传时间以匹配网络速度。这让协议能很好地在Internet和本地以太网上运行。同样IL不会盲目重传,是为了避免给繁忙的网络增加拥挤。完整的细节在另一篇文章中[Prwi95]。在Plan 9中IL的实现比TCP更小更快,IL是我们主要的Internet传输协议。
鉴别概述
鉴别为访问资源的用户建立一个ID。请求资源的用户称为客户(client),授予对资源的访问的用户称为服务器(server)。这通常在一个9P消息attach的帮助下完成。服务器总是依据某些用户,一个普通客户或某些管理实体而活动,因此,鉴别被定义为用户间的,而不是机器间的。
每一个Plan 9用户由一个相关的DES[NBS77]鉴别密钥(key);对用户ID的审查是通过加密和解密特殊消息的能力实现的,此消息称为询问(challenge),因为对一个用户的密钥的知晓才让用户访问它的资源,Plan 9的鉴别协议从来不传送一个有清楚可读(cleartext)的密钥的消息。
鉴别是双向的:在鉴别交换(exchange)结束时,每一方都确信了另一方的身份。每一机器用内存中的一个DES密钥开始交换。在CPU和文件服务器的一方,密钥、用户名、和服务器的域名是从永久存储中得到的,通常是非易失性的RAM。在终端一方,密钥是在引导时从用户输入的口令获得。一个特殊的机器,即鉴别服务器,为它的管理域中所有的用户和鉴别协议中的参与者维护一个密钥数据库。
鉴别协议如下:交换询问(challenge)之后,一方和鉴别服务器联系以创建权限保证(permission-granting)的票据(ticket);这个票据用每一方的保密密钥加密且包含一个新的会话密钥。每一方解密自己的票据,且用会话密钥加密另一方的询问。
这个结构有点像Kerberos[MBSS87],但是避免了它对同步时钟的信任。和Kerberos不一样的还有,Plan 9鉴别支持一个“speak for”关系[LABW91];它能使一个用户拥有另一个用户的授权;CPU服务器就是这样根据它的客户的行为而运行进程的。
Plan 9的鉴别结构构建安全的服务而不是依赖于防火墙。鉴于防火墙对每一种服务渗透的“墙”都要求特殊的代码,Plan 9方法允许鉴别在一个地方-9P(对所有服务)完成。例如,命令安全地运行在Internet上。
鉴别外部连接
正规的Plan 9鉴别协议不适合于像Telnet或FTP这样的基于文本的服务。这种情况下,Plan 9用户用手头上的称为鉴别码(authenticator)的DES算子鉴别。鉴别码为用户处理一个密钥,它不同于用户的普通鉴别密钥。用户使用一个4位数字的PIN登录到鉴别码。正确的PIN使得一个询问/回应的鉴别码和服务器交换。因为一个询问/回应交换只有一次是合法的,且密钥从不通过网络传送,这个过程不受中继攻击(relay attacks)的影响,还和Telnet及FTP这样的协议兼容。
特殊用户
Plan 9没有超级用户。每一服务器负责维护自己的安全,通常只允许从控制台访问,它由一个口令保护。例如,文件服务器有唯一的一个管理的用户称为adm,它的特殊特权是仅能应用到由服务器的物理控制台输入的命令。这些特权考虑到服务器每天的维护,如添加新用户和配置磁盘及网络。特权不包含修改,检查或改变文件权限的能力。如果一个文件被某用户只读保护,只有那个用户可以个其它人授予访问。
CPU服务器有一个相似的用户名,它允许对服务器上资源的管理性访问,这些资源如用户进程的控制文件。这样的权限是必要的,例如杀死破坏进程,但是不超出服务器的范围。另一方面,通过存储在受保护的非易失RAM中的密钥,管理的用户身份被鉴别服务器证实。这就允许CPU服务器鉴别远程用户,处于访问服务器本身以及服务器根据它们的行为而作为一个代理。
最后,一个特殊用户名为none,它没有口令且总允许连接;任何用户都可以申称自己为none。None 有受限的权限,例如,不允许检查转储文件且只能读大家都可读的文件。
隐藏在none后面的思想是对FTP服务中匿名用户的模拟。在Plan 9上,访问FTP服务器更被限制在一个特殊约束的名字空间。它把访问服务器的用户从系统程序上断开,例如/bin的内容,但使得可以创建本地文件,用户可以通过把它们显式地绑定到none而获得本地文件。受限的名字空间比通常的导出一个ad hoc目录树更安全;其结果就是围绕不被信任(untrusted)用户周围的环(cage)。
CPU命令和代理的鉴别
当为一个用户如Peter创建一个对CPU服务器的调用时,其意思是Peter用它自己的权限运行进程。为实现这个特征,CPU服务器收到调用时按以下过程处理。首先,听者(listener)进程创建一个进程处理调用。进程改变为用户none,以避免权限泄漏。它然后执行鉴别,如果是折衷的,协议核实调用的用户确实是Peter,还要向Peter证明机器是可信任的。最后,它使用鉴别协议重新添加(reattach)到所有相关的文件服务器核实它本身是Peter。这种情况下,cpu服务器是文件服务器的一个客户,依据Peter的行为执行鉴别交换的客户部分。只有CPU服务器的管理用户名字允许要求得到“speak for”Peter时,鉴别服务器才会给进程票据以完成鉴别。
“Speak for”关系被保存在鉴别服务器上的一张表中。为了简化对在不同鉴别域中计算的用户的管理,它还包含不同域中用户名间的照映,例如,就像一个域中的用户rtm和另一个域中的rtmorris是同一个人。
文件权限
构造如文件系统这样的服务的一个优势是自然地得出了解决所有权和权限问题的方法。和在Unix中一样,每一个文件或目录对文件的所有者,文件的组及其它任何人都有独立的读写,执行/搜索权限。组的想法是不一般的:任何用户名都潜在地是一个组名称。组就是带有组中其它用户列表的一个用户。习惯在成了不同,大多数用户由用户名却没有组成员,而组有附属的名字的长列表。例如,传统上sys组有所有的系统程序员,系统文件可以用组sys访问。考虑以下的存储在服务器上的一个用户的两行信息:
pjw:pjw:
sys::pjw,ken,philw,presotto
第一行把用户pjw建立为一个正规用户。第二行建立一个名为sys的组,列出了组中的四个成员用户。空的帽号分隔的域是命名为组领袖的用户的空间。如果组有一个领袖,则那个用户有组的特殊权限,如可以自由改变组中文件的组权限。若没有指定领袖,则组中每一个成员被平等对待,就像每一个都是领袖一样。在我们的例子中,只有用户pjw可以向它的组中添加成员,但是sys中的所有成员都是这个组中平等的伙伴。
正规文件由创建它们的用户所有。组名称从持有新文件的目录继承。设备文件被特别处理:内核可能把一个文件的所有权和权限安排给访问此文件的适当用户。
提供的一般性的好例子是进程文件,它被进程的所有者所有及读保护。若所有者想让另外的某人访问进程的存储区,例如让程序的作者调试一个破坏的图像,则对进程文件应用标准命令chmod完成此时。
文件权限的另一个不寻常应用是转储文件系统,它不仅由和原始数据一样的文件服务器提供,而且用相同的用户数据表示。因此转储的文件就给予和正规文件系统中的文件一样的保护;如果一个文件由pjw所有和读保护,只要它在转储文件系统它就仍然由pjw所有和读保护。而且,因为转储文件系统是不变的,其文件不能被改变;它被永远的读保护。不利的一点是如果文件是可读的,但应当已经是读保护的,则它就是永远可读的,且那个用户名难于重新使用。
性能
作为对Plan 9核心的一个简单测量,我们比较了在两种系统上完成相同的简单操作需要的时间:Plan 9系统和SGI的IRIX Release 5.3,它运行在SGI Challenge M机器上,它有一个100MHz MIPS R4400处理器和一个1MB二级高速缓冲。测试程序用Alef写成,用相同的编译程序编译,且运行在相同的硬件上,所以只有操作系统和库是不同的。
程序测试了以下操作的时间:一个上下文切换(Plan 9上的rendezvous,IRIX上的blockproc();一个平凡的系统调用(rfork(0)和nap(0));以及轻量级fork(rfork(RFPROC()和sproc(PR_SFDS|PR_SADDR))。它还测试了从一个进程通过管道向另一个进程发送一个字节的时间和两进程间一个管道的吞吐量。结果如下表:
尽管所花的时间并不特别有优势,但是还是说明核心和商业系统是有竞争力的。
讨论
Plan 9由一个常规的核心;系统的新奇之处是核心之外的部分和它们之间相互作用的方式。在构建Plan 9时,我们综合考虑了系统的所有方面,用最合适的方法解决问题。有时方法产生了许多组成部分。一个例子就是异构指令体系结构的问题,通过编译器(不同的编码字符,可移植的目标码),环境($cputype和$objtype),名字空间(绑定在/bin中)以及其它部分来寻址。有时许多问题可以在一个地方解决。最好的例子是9P,它集中了命名,访问和鉴别。9P实际上是系统的核心部分;可以清楚地说Plan 9核心主要地就是一个9P多路转换器。
Plan 9集中于文件和命令是它的表现性的中心。尤其在分布式计算中,命名事物的方式对系统[Nee89]有深远的影响。局部名字空间的结合以及对互连网络的资源的全局约定避免了维护一个全局同一名字空间的困难,而把所有的东西都像文件命名使得系统易于理解,即使对于新手也是这样。考虑转储文件系统,任何人都可以像层次文件系统一样简单的使用它。在更深的层次上,把所有资源构造在一个统一的接口上使得易于互操作。一旦一个资源导出一个9P接口,它可以透明地和系统的任何其它部分结合以构建不一般的应用;其细节被隐藏了。这听起来好像是面向对象的,但是和它有所区别。第一,9P定义一个固定的“方法”集合;它不是一个可扩展协议。更为重要的是,文件被良好的定义以及易于理解,且用和访问,保护,命名及网络相类似的方法预打包。尽管对象有共性,但是对象没有这些已经定义的属性。通过把“对象”还原为“文件”,Plan 9获得了一些出于自由的技术(technology for free)。
不仅如此,还可能把基于文件计算的思想推向更远。把系统中的每一个资源转换为文件系统是一种比喻,且比喻会被滥用。关于限制的一个好例子是/proc,它仅仅是进程的一个视图,而不是进程的表示。要运行进程,通常的fork和exec调用仍然是必须的,而不是像这样做:
cp /bin/date /proc/clone/mem
这个例子的问题是要求服务器做不在其控制之下的事情。像这样为一个命令指定方法的能力并不意味着这个方法并不自然地得出对9P请求生成的回答的结构。作为一个相关的例子,Plan 9不把机器的网络名字放到文件名空间。网络接口提供一个非常不同的命令模型,因为对这样的文件使用open,create,read和write不提供一个合适的地方来放置所有的为任意网络建立的调用细节,这不意味着网络接口不能提供类文件的,仅仅是因为必须要有一个更紧凑定义的结构。
下一步我们会做哪些不同的呢?实现的某些部分不令人满意。在核心中使用流来实现网络接口允许协议可以被一起被动态的连接,诸如添加同一个TTY驱动到TCP,URP和IL连接,但是Plan 9没有使用这个配置能力。(已经做了努力,然而,在research Unix 系统中为实现此目的发明了流技术。)使用静态的I/O队列替换流不仅使代码简化且加快代码的运行。
尽管主要的Plan 9核心可以移植到许多机器上,但文件服务器被独立地实现。这已经导致了几个问题:驱动程序必须写两次,同样必须两次排除错误,且降低了文件系统代码的可移植性。方法时容易的:文件服务器核心应作为一个正规操作系统的变形来维护,它没有用户进程,且没有特殊的为了实现文件服务的内编译(compiled-in)核心进程。对文件系统的另一个改进会是内部结构的变化。WORM光盘柜是硬件中最不可靠的部分,但是因为它保存文件系统的元数据(metadata),为了提供文件它必须是在场的(present)。系统可以用适当驻留在磁盘上的文件系统重构,因此WORM只是一个备份设备了。这不会要求改变内部接口。
尽管Plan 9有每个进程的名字空间,它还没有这样的机制:把一个进程名字空间的描述给除了直接继承的进程以外的进程。例如CPU命令一般不能再现终端的名字空间;它只能重新解释用户的登录概貌(profile)文件,为诸如要装入的二进制目录名创建一个替代者。这就丢失了在运行CPU命令之前所做的任何本地修改。相反,捕获终端名字空间且把它的描述传送给一个远程进程应当是可能的。
虽然有些问题,Plan 9还是工作的很好。它已经成为一个支持我们的研究的成熟系统,而不是对研究本身的约束。新的试验性工作包括开发更快的网络接口,客户核心中的文件高速缓冲,封装和导出名字空间,以及在一个服务器崩溃后重新建立客户状态的能力。现在的注意力集中于用这个系统构造分布是应用。
Plan 9成功的一个原因是我们用它完成我们日常的工作,而不仅仅作为一个研究工具。当出现活跃的用户时,它们促使我们查找缺陷,以使系统适应于解决我们的问题。经过这一过程,Plan 9已经变成一个舒服而有生产效率的程序设计环境,而且是进一步系统研究的手段。
订阅:
评论 (Atom)