一、PDP-7 Unix file system
PDP-7 Unix 的文件系统和今天的已经有所相似,它具有以下三个
1)An i-list:一个 i-nodes 的线性队列来描述一个文件。每一个i-node 包含的内容比现在少一点,但是基本信息是相同的:
a)the protection mode of the file 文件的保护模式,理解为可读,可写等
b)its types and size 文件的类型和大小
c)the list of physical blocks holding the contents 保存文件内容的物理存储块列表
2)Directories:一种特殊的文件,包含一个含有名字和相关 i-number 的序列
3)描述设备的特殊文件。在 i-node 中设备规范没有被明确的包含,但是用数字编码来代替,即特殊的 i-numbers 与指定的文件相关联。
重要的文件系统调用现在分为 Read, write, open, creat, close。他们和今天的很相近。一个细小的差别是I/O 的单位不是byte而是word,因为PDP-7是word寻址机制,事实上这仅仅意味着程序处理字符流的时候忽略了空字符,因为null 是被用来填充文件使之成为偶数个字符。还有一个令人十分恼火的差别是缺少擦除(erase)和杀死进程来终止。事实上,终止总是在原始模式,只有一些程序(shell和编辑器)费力的实现了 erase-kill 进程。
尽管和当前的文件系统相当的相近,但是PDP-7文件系统在一个方面显著的与之不同,即没有路径名称并且每个系统文件名是一个简单的名字,与它当前所在的文件夹有关。Links 在一般的Unix是存在的,和一系列精巧的设计一起,他们使得没有文件名变成了可以接受的重要方法。
link 调用的格式是
link(dir, file, newname)
其中,dir 是当前目录中的一个目录文件,file 是在这个目录中现有的一个条目,newname 是link 的名字将会被添加到当前目录。因为 dir 需要成为当前目录,所以很明显今天禁止 links 到目录在当时是没有强制的。PDP-7 Unix 文件系统有一个常规有向图。
因此每一个 user 不需要和所有的目录都保持 link, 有一个目录叫做 dd ,包含了每个 user 的目录的条目。因此当想要在 ken 目录创建一个 link 连接到 x 文件可以这样做,请看下文,
ln dd ken ken
ln ken x x
rm ken
这个方案太麻烦所以在实践中没有使用。另一个非常重要的障碍是这里没有办法在系统运行时创建一个目录,上述所有的一切都是在文件系统从纸带重新创建的过程中进行,因此目录实际上是一种不可再生资源。
dd 使 chdir 命令相对简单,它可以传递多个参数,并且轮流切换当前目录到每个命名目录。
chdir dd ken
将会移动到 ken 目录(顺便说一下,chdir 被拼写为 ch),文件系统实现最严重的麻烦除了没有路径名称以外,是难以改变它的配置。就像提到的那样,目录和特殊文件都是只有当磁盘重新创建的时候才会生成,而生成一个新的设备是十分令人头疼的事,因为设备的代码广泛的扩展到了系统的每个地方,例如这里有几个循环来轮流访问每个设备。可以想到的是,这里没有装备一个可换磁盘组的概念,因为机器只有一个单固定头文件(a single fixed-head disk)。
操作系统中实现这个文件系统的代码大大简化到现在的版本。一个重要的简化是系统不能够多程序,同一时间在内存中只能有一个程序,并且控制权在进程之间相互传递当有一个明确的交换(swap)发生时。因此例如,这里有一个 iget 来使一个命名过的 i-node 可用,但是它把 i-node 设成了常亮,静态的位置,而不是返回一个指针到一个大的动态 i-nodes 表中。现有的buffering机制前身就是这个,但是本质上I/O 和计算之间没有重叠。避免重叠不仅仅是为了简化,磁盘接触PDP-7的速度也快了。它每2微秒传递一个18比特的字。另一方面,PDP-7自己每1微秒有一个内存 cycle,并且大多数指令消耗2个 cycle,一个是指令自己,另一个是操作。但是间接寻址指令要求3个 cycle,并且 indirection 也是一样的。因为机器中没有索引寄存器。最后,DMA controller 不能在一条指令运行时访问内存。结果是任何间接寻址指令执行时,磁盘会产生溢出错误。因此当磁盘运行时控制权不能返回给用户,事实上通用的系统代码也不能被执行。需要长时间运行的时钟和终端中断必须以一种奇怪的风格来编码从而避免 indirection。
二、Process control
Process control 是进程创建和使用的机制,今天的系统调用 fork, exec, wait, exit 实现了这种机制。不像文件系统从最初就以现在的形式存在,进程控制方案在PDP-7 Unix 使用之后经过了相当大的变化(在PDP-11 系统中关于路径名的介绍已经是相当大的进步,但是没有改变根本的结构)
现在,用 shell 执行命令可以被总结为下面几步,
1)shell 从终端中读取命令行
2)使用 fork 创建一个子进程
3)子进程使用 exec 从一个文件中调用一个命令
4)同时,父 shell 使用 wait 来等待子进程通过调用 exit 来终止
5)父 shell 返回到第一步
Processes(独立的可执行实体)早在PDP-7 Unix中就已经存在了,这里恰恰有其中的两个,第一个是两个终端的任何一个可以连接到机器。没有 fork, wait, exec. 但是有 exit,但是它的意义大不相同。可以看到下面是 shell 的主循环,
1)shell 关闭所有打开的文件,然后打开终端指定的文件用来标准输入输出(文件描述符为 0 和 1)
2)shell 从终端中读取一行命令
3)shell 链接到指定命令的文件,打开文件,移除 link,然后复制一个小的引导程序到内存的顶部,并跳转到这里,这个引导程序通过shell代码读取文件,然后跳转到指令的第一个位置(实际上是一个 exec)
4)命令执行它的工作,然后通过调用exit 来终止。exit 调用导致系统要通过终端命令读取一个新鲜的shell拷贝,然后跳转到它的起始点,实际上回到了第一步
关于原始的这些实现方法,最有趣的是预先的主题后来都被完全的发展。他不能支持后台进程也不能支持shell 命令文件,更别说管道和过滤器(let alone pipes and filters)。但是IO重定向('<' , '>')之后出现了。重定向的实现十分明确,在上面的第三步中shell只是用适当的文件代替了标准的输入输出。对随后发展至关重要的是实现了shell 作为用户级别程序存储在文件中,而不是操作系统的一部分。
这种一个终端对应一个进程的 process control 方案的结构类似于许多互动式系统,比如CTSS,Multics,Honeywell TSS,还有IBM TSS 和TSO。通常情况下这种系统要求特殊的机制来实现一些实用的工具比如将计算和命令文件分离。Unix在当时没有费时的去提供特殊机制,除此之外它还呈现出一些气人的、怪异的问题。比如新建一个shell 必须去关闭所有它打开的文件,包括所有被刚刚执行过的命令遗留下来打开的文件并且撤销之前的IO 重定向。然后它不得不重新打开一个特殊的文件来对应它的终端为了读取新的命令行。这里没有 /dev 目录因为没有目录名,并且shell 通过命令不能保留任何内存,因为他在每条指令之后被执行刷新操作。因此一个文件系统的标准:每个目录都必须包含一个条目 tty 为特殊文件,来指向打开它的进程的终端。如果意外的一些目录缺少这个条目,shell 将会无限循环。唯一的纠正办法是 reboot(sometimes the missing link could be made from the other terminal 不是很能理解这句话)
process control 现代的格式在短短几天时间就被设计和实现出来了。令人惊奇的是它竟然如此简单的和已存在的系统适配。同时它也很容易看到最初设计的一些不起眼的不常用的点现在都被用到了因为他们只做了很少的简单的编程就改进到了现有的模式。一个很好的例子是将 fork 和 exec 函数分离。创建一个新的进程共同之处在于为进程指定一个程序来执行,在Unix中一个被执行 fork 的进程继续运行和它父进程相同的程序,直到它有一个自己特定的 exec 才得以改变。分离函数功能不只是针对于Unix,事实上也应用于伯克利分时系统。“它在Uni系统中存在的原因是 fork 可以被简单的实现而不用改变太大” 的假设看起来是合理的。系统已经处理多个进程,这里有一个进程表,这些进程在内存和磁盘之间交换,初始实现fork 只要求 a)进程表的扩展。b)使用了已有的 swap IO primitives和对进程表做出一些调整,把当前进程拷贝到磁盘swap 区域,即添加一个fork 调用
事实上,PDP-7的 fork 调用要求精确的27行汇编代码,当然了,其他在操作系统和用户程序中的改变也被要求必须有,并且它们其中的一些是令人有兴趣的并且没有预料的。但是一个 fork-exec 的组合复杂得多,如果只是因为exec 不存在的话,它的功能已经用shell 显式IO实现了。
exit系统调用,事先读取一个 shell 的新的拷贝(实际上是一种自动化的 exec 只不过没有参数)相当的简化。在新版本中一个进程只需要清除它自己进程的 table entry,并且放弃控制权。
奇怪的是,演进成 wait 的原始程序比现有的方案更加一般性。下面是一个示例,在两个命名的进程之间发送一个字的信息:
smes(pid, message)
(pid, message) = rmes()
除了fork 返回每个parent 和child 的ID以外,系统没有为进程间通信提供明确的机制,但是smes的目标进程不需要和接受者有任何的归属关系或继承关系。Messages 不是队列形式的,发送者延迟到接受者读到消息。此处作者理解为socket 连接的类似过程。如果不对,还请纠正。
消息实体按照如下步骤使用
a)父进程在创建一个进程去执行命令之后,通过smes给新的进程发送消息
b)当命令终止shell 的阻塞,smes 调用返回一个错误表示目标进程不存在
因此shell 的smes 实际上相当于 wait
另一种协议利用了由messages 提供的通用性优势,被用来在初始化程序和每个终端的 shell 之间。初始化进程的ID通常是1,为每个终端创建了一个shell,并且发布 rmes 即等待接受信息。每个shell 读取到它自己的输入文件末尾时,使用smes 发送一个 ‘I am terminating’ 的信息给初始化进程,初始化进程将会为这个终端创建一个新的shell进程。
以上内容解释了为什么会被wait 所取代,它虽然更加直接适用于预期的目的,但是却不具备一般性。另外呢也存在bug,举例说明,如果一个命令进程尝试使用messages 来和其他进程通信,他将会干扰shell 的同步性。shell 依赖于发送一个永远不会被收到的message。如果一个命令执行 rmes,他将会收到shell 的虚假的message,并且导致shell 去读取另一个输入行,就好像命令被终止了一样。
无论如何,新的进程控制方案提供了有价值的
三、IO重定向
四、PDP-11的出现和第一个PDP-11系统
从内部结构的角度,第一版的PDP-11系统是在PDP-7基础上有一些小的改进,很大程度上是一种翻译。例如他没有多道程序设计,在一瞬间只有一个用户程序可以存在在内核中。另一方面用户界面上有很重要的改变。现有的带有全路径名的目录结构以及现代形式的exec,wait 已经是完善的了,并且十分方便,比如character-erase 和 line-kill processing for terminals。或许这个计划最有趣的是规格很小,内核内存有24K字节,其中16K准备给系统,8K留给用户程序,并且一个硬盘被分为1K个块。
之后呢大概在1971年,经过了不懈的努力,他们终于在一年时间里完成了对Unix系统的改进,可以支持电传打字机的模型37终端,使得可以打印出需要的大多数符号,另外他们也很快地赋予了roff 产生线数页的能力,这是基于专利中心的要求并且这项技术是其他系统都没有的。
pipes
一个很重要的东西
下面挑一个比较有趣的事,当PDP-11到达时,B语言迅速的移植到其上。事实上,多精度‘desk calculator’ 程序 dc 是最早可以运行在PDP-11上的程序,在硬盘到达之前就如此。但是B语言并没有迅速的接管,只是用B 语言重写操作系统,而不再使用汇编语言。
因此在1971年开始了C 语言。最重要的分水岭在1973年,当操作系统内核使用C语言重写。这个点意味着操作系统呈现出了现代的模式。
现在,Unix程序仍使用汇编的只有汇编自己,其他几乎所有的实用程序都是用C语言。