悼念浩哥(左耳朵耗子),一个纯粹的技术人

上周末听闻浩哥的事了,期初还不信。在网上搜索消息,看来是真的。他才四十多岁,觉得非常可惜。很早就关注过浩哥,他是一位正直纯粹和爱分享的技术大牛。无论是技术分享还是人生感悟,或者是成长相关,都让我学到很多东西。曾经受他文章的影响,让我对技术和linux系统产生兴趣。可惜天妒英才,世界上失去了一位优秀的技术人和引路人。在此也分享下浩哥unix方面的文章,希望更多人对linux系统(类unix)感兴趣。

前言

”芝兰生于深谷,不以无人而不芳;君子修身养德,不以穷困而改志;“ 这是耗子叔的座右铭,我认为他做到了。谨以此文悼念陈皓老师,一路走好!感谢左耳朵耗子老师对IT界做出的贡献!

也希望所有朋友们身体健康,平安,顺遂!在百忙之中多关注下自己的身体,加强锻炼。

浩哥博客地址:https://haoel.blog.csdn.net/?type=blog

酷壳 – CoolShell.cn

330a52843233d655d8a287e489f89adc.jpeg在如今浮躁的社会一切向前看齐,有多少人是真正的热爱分享和喜爱研究技术。我所知道的耗子哥是一位热爱技术和乐于分享的人。陈皓有着20年软件开发及相关工作经验,先后在阿里巴巴、亚马逊、汤森路透等知名公司任职。他擅长底层技术架构,对高性能,高可用性,分布式,高并发,以及大规模数据处理系统都有经验和心得。喜欢关注底层技术平台和互联网行业应用。技术擅长C/C++/Java和Unix/Linux/Windows。

他曾在阿里巴巴北京研发中心、商家业务部曾任资深专家一职,负责电商云平台、开放平台,云监控和电商多媒体平台。也曾在阿里巴巴核心系统专家组从事阿里核心系统和阿里云ECS相关的虚拟化平台的开发工作。

虽然出于物质需要,每个人各有所追求没错,但是出于本心和兴趣爱好的做些技术研究本身也是一种乐趣。尤其是IT这行,莫要追求一些看似强大的框架和工具,卷在各种框架和语言上,而应重视底层打好基础功。

而且造不如买的思想很坑人,我们被卡脖子的教训还不深刻吗?如果我们总是使用老外的先进工具和框架,只在业务层面做些创新和实现,怎能产生大的突破和进步?像金字塔模型,其实业务层处于最上层面的性价比并不高,想想国外仅是一个ARM核的IP授权挣了多少钱,就是因为含金量高。

 在这里分享一篇浩哥的unix系统文章《其实Unix很简单》,希望更多的从事软件这行的朋友,多关注底层技术和基础研究。少用windows系统,多些研究和使用下linux。勿在浮沙筑高台,这样咱们的软件实力才能越来越强大。无论是否做开发的,推荐体验下国产系统。现在的国产深度deepin操作系统不赖,推荐试用_deepin20.8和v23区别_特立独行的猫a的博客-CSDN博客

了解下Unix的过去,我们才能知其然,更知所以然。总结过去,我们才会知道我们明天该如何去规划,该如何去走。在时间的滚轮中,许许多的东西就像流星一样一闪而逝,而有些东西却能经受着时间的考验散发着经久的魅力,让人津津乐道,流传至今。要知道明天怎么去选择,怎么去做,不是盲目地跟从今天各种各样琳琅满目前沿技术,而应该是去 —— 认认真真地了解和回顾历史。

其实Unix很简单

《其实Unix很简单》 作者陈皓,链接:

https://haoel.blog.csdn.net/article/details/1533720?spm=1001.2014.3001.5502

很多编程的朋友都在网上问我这样的几个问题,Unix怎么学?Unix怎么这么难?如何才能学好?并且让我给他们一些学好Unix的经验。在绝大多数时候,我发现问这些问题的朋友都有两个特点:

1)对Unix有畏难心理,对其没有信心;

2)喜欢用拿Windows来和Unix做比较。

这两种特点就像两个“心理暗示”,暗示着自己Unix很不好学,暗示着Unix很糟糕,不如Windows好。于是,自己也就被自己的这种长期的“暗示”所催眠了。因为,从一开始就有畏难情绪,所以也就觉得Unix不好,觉得非常很吃力,最后还会导致对Unix的厌恶和反感的情绪。所以,为了纠正上述朋友们的“心理暗示”。我想写下这篇文章,想告诉大家,Unix真的很简单。

在正式叙述“简单的Unix”之前,我想做几点说明:(以免陷入无意义的争论)

1)  本文是站在开发者的角度来说明的,所以,如果有朋友不同意我的观点,请也以开发者的角度来向我提出质问和讨论,本人非常欢迎。

2)  本文难免要用Windows来和Unix做对比。这并不代表我不喜欢Windows,也不代表我要叫你放弃Windows。我们也知道这种对比已经没有什么意思了,但因为众多的朋友被Windows先入为主了,所以,我一定要拿Windows来开刀,才能扭转那个“心理暗示”。仅此而已。

 OK,言归正传。先说Unix的一个最重要的特点——“高内聚,低藕合”!也就是说,Unix下的各种应用程序都和别人不相干。这就是贯穿整个Unix的思维——模块和程序的高度独立性。这样的设计和做法,会让你的系统比较的稳定,也会让你的系统特别地容易管理和维护。Unix下的应用程序们就像一支正规军一样排列地整整齐齐,只要司令(内核)还在,系统是不会因为某个军队的损失而无法自举的。而Windows的应用程序们就像一片树林一样,从地表上看过去,树木们排列地整整齐齐,但是他们的树根在地下却相互缠绕在一起,剪不断,理还乱,异常地复杂。

 “高内聚,低藕合”的给Unix造成的结果是,其系统中基本上都是功能单一的小程序,这些小程序就像积木一样,当我们需要构造建一个自定义的建筑时,大多数情况下,我们只需要做的只是一个“搭积木”的简单游戏。Windows建设得富丽堂皇,可惜,别人的“积木”你几乎不可能拿到自己的建筑中来。总是要你模仿或重写。

(插一句:你是否注意到在网上下载Windows的软件时,会有一种所谓的“绿色软件”?这就是对Windows的最大讽刺,Windows下装一个软件,N个DLL放到Windows系统目录下,注册表里写入N个键值,还有很多你不知道的动作。而在Unix装软件,你不用担心你的系统目录下会莫明其妙地多出些乱七八糟的文件。就是copy那么简单,那怕是rpm自动安装,安装完后,你也能够查询到软件安装后对系统所做的改变。所以,你在Unix下分发你的软件时,你会觉得比Windows下要做得简单了许多许多。)

       再说说Unix的另一个最重要的特点——“所有的设备都可以像文件一样地操作”。简单吧。所有的调备,文件、打印机、显示器、终端、网络、软盘、磁带、USB、CDROM、等等的I/O操作,都以文件描述符的方式进行操作。两个Unix下最重要的系统调用read/write就可以胜任所有设备的I/O了。Unix早就在/dev目录下为你建好了这些文件。使用起来很简单。

       也许很多人都觉得Unix的命令行太过复杂。一个命令有着若干的参数,异常地复杂。但之所以今天Unix下的应用程序还在以字符界面为主,这恰好体现了Unix的简单的特征。这也是Unix的另一个特点——“命令的相互支持性”,命令们通过一个管道或是重定向,可以互相联系在一起,再加以Shell脚本的支持,哪怕要实现一些复杂的功能(比如一个小型的文本数据库),也是简单之极。

       如果上面的论述依然不能让你信服Unix很简单,那么,让我们来用一些具体地实际的例子来看一下,Unix是如何简单的。让我们试着做下面的这样一个假设:“如果我们在学习编程的时候一开始是学Unix,然后转去做Windows”,那会是怎么样的一个情况?

1)  我们在Unix下创建进程,使用fork调用。到了Windows下,我们查了MSDN,发现了一个叫CreateProcess的系统调用可以创建进程,但我们却发现这个系统调用有10个参数。而Unix下的fork却一个参数都没有。这种情况下,你是否会有一种头大的感觉?因为,在Unix下,你根本看不到会有10参数这样复杂的系统调用API。

2)  我们在Unix下操作文件权限很简单,文件权限分三组(本人,同组,别人),每组都是可读,可写,可执行。两个简单的系统调用chmod/chown就搞定。到了Windows下,如果是NTFS,如果要以程序的方式设置文件权限,呵呵,你需要先了解什么是:SID,什么是DACL,什么是SACL,什么是ACE,还有十几相关的系统API函数等着你。(参看我的《以程序操作NTFS文件权限》)你也许会觉得这么复杂的安全策略是让系统更安全的基础,可以自从Windows出现的那一天以后,在安全方面的表现就没有胜过Unix。这无疑让人感到Windows做了一件吃力不讨好的事。

(插一句:Unix下的用户切换是相当简易方便的。而Windows下的用户切换会导致你需要退出当前用户的前台程序。这导致了Windows下的用户几乎无一例外地都会选择在超级用户的权限下工作/上网,这完全是让自己的机器在裸奔,所以,Windows下的病毒一旦在系统中运行就为所欲为了。而Unix下,很少用户会以root身份操作本机,因为切换用户非常方便。)

3)  在Unix下,用户有ID,用户组有ID,进程/线程都有ID。ID很简单易懂,就像我们的身份证一样。到了Windows下,用户标识叫Token,进程标识叫Handle(其实也就是一个DWORD的类型),我看到网上很多问题都在问Windows下的Handle的概念问题。我一直在想,为什么微软不取一个简单易懂的术语?要取得那么抽象,那么让人很迷惑。虽然这让Windows看起很NB,但也会因此加大了学习复杂度。(Windows的开发学习复杂度要比Unix复杂多了,而且有太多的看似很高深的术语让人一头雾水)

4)  让我们再来看一下用户管理和程序所有者方面的东西。在Unix下,需要你做的是配置NIS服务器和NFS服务器(以Autofs自动mount),简洁,清楚。到了Windows下,与其相似的是一个叫Domain的东西(主域控制器),首先,为了加入域,你需要重启电脑(Unix下只需要配置/etc/nsswitch.conf文件来告诉本机的用户登录源,无需重启),而对开发者来说,Unix的这个配置对程序是完全透明的。而Windows的域用户和本地用户需要一个域名来区分。在程序中切换用户时,Unix只需要setuid/seteuid就行了,Windows对此有三个比较复杂的API:CreateProcessAsUser, ImpersonateLoggedOnUser, LogonUser,其复杂度就不用比较了。另外,在Domain方式下,你的Document and Setting目录下的文件,会全部放到Domain服务器上去,你在别的机器上登录时,需要下载这些文件。最后,我倒不担心你和网友的MSN聊天记录会因为你的登录而到处都是,我担心的是,你在这么复杂的管理环境中写出来的代码是否能让别人放心?:-(

5)  在Unix下,要把自己的程序加入系统的启动服务只需要在/etc/init.d中配置就行了。写下一个有启动停止功能脚本,以特殊命名的方式链接到不同启动模式目录下就行了。Windows下加一个启动服务,如果你不编写程序,估计比较困难。

6)  在Unix下,如果要取得系统的信息。只需要到/proc目录下去cat那堆文件。所有进/线程的状态、命令行、内存/交换区使用情况、打开的文件描述符,等等,系统的CPU,内存,交换区,内存文件IO,分区,信息,网络,系统运行状态,系统设备,等等,要有多详细就有多详细,而且完全是纯文本的,直接就可以看了。到了Windows下,要穷举系统当前进程的信息,就不是一样很容易的事,更别说要取得别的信息了。

我很佩服微软把操作系统搞得那么复杂,又是注册表,又是安全策略,又是OLE,又是COM……。每次打开regedit.exe时,我根本不敢碰HKEY_CLASSES_ROOT,因为我看着里面那些成千上万个CLSID,我有点晕菜。

自1995年以来,10来年过去了。微软推出了多种各式各样的技术。我还记得Visual Studio 6.0中还有两个叫做FoxPro和J++的东西,FoxPro来自Foxbase,还有个几年,J++好像就没有几年。ActiveX Control完全是一个失败的技术,而那个叫做VB的编程语言,今天看来,它的确毁了很多很有潜力的程序员。在COM出现的时候,不知道今天还有多少人还记得一个叫MTS的玩意?今天,不知道还有多少人记得有一个叫ODBC的东西?在这种复杂混乱的Windows世界中,是否让你疲于追赶?今天的.NET不知道又有多少技术会随着时间所沉淀?在Windows上面,我们学习了许多的失败技术或是说是过渡技术。而我们的Unix自从上纪70年代以来,就没有多大的变化,而因为Unix应运而生的C语言直到今天依然光彩夺目。我相信这个30多年来久经考验还那么简单的Unix。

Unix就是这么简单,各位想在Unix下学开发的朋友,Windows那么复杂的操作系统都过来了,你还会怕这么简单的Unix么?

UNIX分时系统介绍

《 The UNIX Time-Sharing System 》这篇论文可谓是操作系统的重大突破,产生了深远的影响。

个人认为这篇文章还是值得一看的,对理解类unix系统很有帮助,知名的论文毕竟算得上是“一手”知识,严谨性毋庸置疑。而且文章不长,整篇都是介绍性的,并不涉及理论研究,所以也不像其他大多数论文那样难懂。往细了讲,现代操作系统已经发展成庞然大物,日常使用往往只见树木、不见森林,而理论知识又与实际情况存在一定的隔阂,所以学习理论知识的同时,了解一下这些庞然大物最初的形态也是有帮助的。

以下内容转自网友chocovon大神对该英文论文的翻译,感谢分享。中文翻译地址:

GitHub - chocovon/the-unix-time-sharing-system-chinese

英文原文地址(可下载):http://www.course.sdu.edu.cn/Download/d8b9e205-789f-40e7-8343-3f933425a5ca.pdf 

摘要

UNIX 是一套通用的、多用户、交互式操作系统,被用于数字设备公司 (DEC) 的 PDP-11/40 和 11/45 计算机。它拥有着即使是较大型的操作系统也很少见的功能,包括:(1)集成了可装卸分卷的层次文件系统;(2)互相兼容的文件、设备和进程间通信;(3)能够初始化异步进程;(4)系统命令语言可根据用户选择;(5)超过 100 个子系统,包括十几种语言。本文讨论了文件系统和用户命令界面的特征和实现。

1. 引言

UNIX 有三个版本。最早的版本(约 1969-70 年)在数字设备公司的 PDP-7 和 -9 计算机上运行。第二个版本在没有保护的 PDP-11/20 计算机上运行。本文只介绍 PDP-11/40 和 /45 [1] 系统,因为它更现代化,它与旧的 UNIX 系统的诸多差别,只在于重新设计了一些有缺陷或缺失的功能。

自 PDP-11 UNIX 于 1971 年 2 月投入实践以来,用在了大约 40 个设备上;这些设备所搭载的系统一般都比本文所述的要小。它们当中大多数都用于诸如编写和格式化专利申请和其他文字材料、收集和处理贝尔系统内各种交换机的故障数据、以及记录和检查电话服务单之类的应用。我们自己的设备主要用于操作系统、语言、计算机网络和其他计算机科学课题的研究,也用于文档编写。

也许 UNIX 最重要的成就是表明,一个强大的交互式操作系统并不需要昂贵的设备或人力。UNIX 可以在成本仅为 40,000 美元的硬件上运行,而在主要系统软件上只花了不到 2 人年的工作量。虽然 UNIX 所包含的一些功能,甚至在一些大得多的系统中也很少提供,但我们希望 UNIX 的用户仍能意识到,该系统最重要的特点是简单、优雅和易于使用。

除了系统本身外,UNIX 下的主要程序有:汇编器、基于 QED 的文本编辑器 [2]、链接加载器、符号调试器、带有类型和结构的类似 BCPL [3] 的语言编译器(C)、BASIC 方言的解释器、文本格式化程序、Fortran 编译器、Snobol 解释器、自上而下的编译器 - 编译器(TMG)[4]、自下而上的编译器 - 编译器(YACC)、表格字母生成器、宏处理器(M6)[5] 和换行索引程序。

此外,还有一系列的维修、实用、娱乐、新奇的程序。这些程序都是本地编写的。值得注意的是,该系统是完全自给自足的。所有的 UNIX 软件都是在 UNIX 下维护的;同样,UNIX 文档也是由 UNIX 编辑器和文本格式化程序生成和格式化的。

2. 硬件和软件环境

安装我们 UNIX 的 PDP-11/45 是一台 16 位字(8 位字节)的计算机,核心内存为 144KB;UNIX 占用 42K 字节。然而,这个系统包括了非常多的设备驱动程序,并为 I/O 缓冲区和系统表分配了大量的空间;一个能够运行上述软件的最小系统,总共只需要 50K 字节的核心。

PDP-11 有一个 1M 字节的固定头磁盘,用于文件系统的存储和交换;四个移动头磁盘驱动器,每个驱动器在可装卸磁盘盒上提供 2.5M 字节;以及一个使用 40M 字节可装卸磁盘组的移动头磁盘驱动器。此外,还有一个高速纸带读卡器-打孔机、九轨磁带和 D-磁带(一种可对单个记录进行寻址和重写的磁带设施)。除控制台打字机外,还有 14 个变速通信接口,连接到 100 系列数据集上,还有一个 201 数据集接口,主要用于向公用行式打印机拼接打印输出。还有一些独一无二的设备,包括 Picturephone® 接口、语音应答装置、语音合成器、照相排字机、数字交换网络,以及在泰克 611 存储管显示器上生成矢量、曲线和字符的卫星 PDP-11/20。

UNIX 软件大部分是用前面提到的 C 语言编写的 [6]。该操作系统的早期版本是用汇编语言编写的,但在 1973 年夏天,我们用 C 语言重写了一版。新系统的规模比旧系统大三分之一。由于新系统不仅更容易理解和修改,而且还包括许多功能上的改进,包括多程序编程和在几个用户程序之间共享重入代码的能力,我们认为这种体积的增加是相当可以接受的。

3. 文件系统

UNIX 最重要的工作是提供一个文件系统。从用户的角度来看,文件有三种:普通磁盘文件、目录和特殊文件。

3.1 普通文件

普通文件包含了用户放在上面的任何信息,例如符号或二进制(对象)程序。系统不会对普通文件的结构有任何预设。文本文件只是由一串字符组成,行与行之间用换行符划分。二进制程序是程序开始执行时将出现在核心内存中的字序列。一些用户程序所操作的文件更结构化:汇编器生成和加载器处理的对象文件都具有特定的格式。但是,文件的结构是由使用它们的程序控制的,而不是由系统控制的。

3.2 目录

目录提供了文件名和文件本身之间的映射,从而在整个文件系统中形成了一种结构。每个用户都有一个自己的文件目录;他也可以创建子目录,将各组文件放进不同的目录,以便处理。目录在形式上与普通的文件完全一样,只是无权限的程序不能对其进行写入,也就是说,目录的内容是由系统控制的。但是,任何有适当权限的用户都可以像读其他文件一样读目录。

系统会维护几个目录供自己使用。其中一个是根(root)目录。系统中的所有文件都可以根据路径中一连串的目录找到。这种搜索的起点往往是根目录。另一个系统目录包含所有常规使用的程序;也就是所有的命令(commands)。然而,正如我们将看到的那样,一个程序要想被执行,并不一定要放在这个目录中。

文件以不超过 14 个字符的序列命名。当向系统指定文件名时,可以采用路径名(path name)的形式,路径名是由斜线 “/” 分隔的目录名序列,以文件名结束。如果该序列以斜线开头,则从根目录开始搜索。文件名 /alpha/beta/gamma 会让系统在根目录下搜索 alpha,然后在 alpha 目录下搜索 beta,最后在 beta 目录下找到 gammagamma 可能是一个普通的文件,一个目录,或者一个特殊的文件。作为一种极端情况,名称 “/” 指的是根目录本身。

不以 “/” 开头的路径名则让系统从用户的当前目录开始搜索。因此,alpha/beta 这个名字指定了当前目录的子目录 alpha 中名为 beta 的文件。最简单的一类文件名,例如 alpha,指的是一个本身就在当前目录中的文件。作为另一种极端情况,空文件名指的就是当前目录。

同一个非目录文件可以出现在若干个目录中,名称也可以不一样。这种特性称为链接技术(linking);指向这个文件的目录项有时也被称为链接(link)。UNIX 与其他允许链接的系统不同的是,所有指向同一文件的链接都具有同等的地位。也就是说,一个文件并不存在于某一特定的目录中;文件的目录项仅仅由它的名称和一个指向真实文件描述信息的指针组成。因此,一个文件独立于任何目录项而存在,尽管在实践中,一个文件会与最后一个链接一起消失。

每个目录至少有两项数据。目录中的名称指的是目录本身。因此,一个程序可以在不知道其完整路径名的情况下,以 “.” 为名读取当前目录。按照惯例,“..” 指的是展现该目录的父目录,也就是指创建它的目录。

目录结构被限制为有根树的形式。除了特殊的条目 “.” 和“..”之外,每个目录都必须作为一个条目出现在另一个目录中,也就是它的父目录中。这样做是为了在编写程序时能更简单地访问该目录结构的子树,但更重要的是,避免了层次结构的局部分离。如果允许任意链接到目录,那么就很难检测到从根目录到目录的最后一个连接何时被切断。

3.3 特殊文件

特殊文件构成了 UNIX 文件系统最不寻常的特征。UNIX 支持的每个 I/O 设备都至少与一个这样的文件相关联。特殊文件的读写就像普通的磁盘文件一样,但读写的请求会导致相关设备的激活。每个特殊文件的目录项都在目录 /dev 中,同时也可以像普通文件一样链接到这些文件中的任意一个。因此,例如,要打纸带(punch paper tape),可以写在文件 /dev/ppt 上。每个通信线路、每个磁盘、每个磁带机以及物理核心内存都存在特殊的文件。当然,活动的磁盘和核心特殊文件是受到保护的,不能随意访问。

这样处理 I/O 设备有三方面的好处:文件和设备 I/O 尽可能地相似;文件名和设备名的语法和含义相同,因此,以文件名作为参数的程序也可以传入设备名;最后,特殊文件与普通文件有着相同的保护机制。

3.4 装卸式文件系统

虽然文件系统的根总是存储在同一个设备上,但整个文件系统的层次结构并不一定要保留在这个设备上。有一个 mount 系统请求,它有两个参数:一个是现有普通文件的名称,一个是直接访问的特殊文件的名称,该特殊文件所关联的存储卷(如磁盘组)应该具有独立文件系统结构,且有着它自己的目录层次。mount 的效果是使原先对普通文件的引用改为对可装卸卷上文件系统的根目录的引用。实际上,mount 用一个全新的子树(存储在可装卸卷上的层次结构)取代了当前层次结构树的一个叶子(普通文件)。挂载后,可装卸卷上的文件和永久文件系统中的文件几乎没有区别。以我们自己的设备为例,根目录安装在固定头磁盘上,而包含用户文件的大盘驱动器由系统初始化程序挂载,四个小型磁盘驱动器可供用户挂载自己的磁盘组。通过在其相应的特殊文件上写入,生成可挂载的文件系统。有个实用程序可以创建一个空的文件系统,也可以直接复制一个现有的文件系统。

处理不同设备上文件的规则是相同的,但有一个例外:一个文件系统层次结构与另一个文件系统层次结构之间不得有任何链接。强制对此进行限制,是为了避免繁琐的簿记工作,否则,当可装卸卷最终被卸载时,就需要确保删除这些链接。需要特别指出,在所有文件系统的根目录中,不管是否可装卸,“..” 的名称都是指目录本身,而不是指其父目录。

3.5 保护

虽然 UNIX 的访问控制方案相当简单,但它有一些不同寻常的特点。系统中的每个用户都被分配了一个独特的用户识别号。当一个文件被创建时,它被标记为其所有者的用户 ID。对于新文件,还给出了一组 7 位的保护位。其中 6 位分别指定了文件所有者和所有其他用户的读、写和执行权限。

如果第七位为真,则每当文件作为程序执行时,系统会将当前用户的用户 ID 临时改为该文件创建者的用户 ID。这种用户 ID 的改变只在执行调用它的程序时有效。set-user-ID 功能提供了一种拥有特权的程序,这些程序能使用其他用户无法访问的文件。举例来说,一个程序可能会保有一个会计文件,该文件除了程序本身之外,既不应被读取,也不应被更改。如果该程序的 set-user-ID 位被打开,它就可以访问该文件,而该程序的使用者所执行的其他程序,则不具有该文件的访问权限。由于任何程序都可以拿到其调用者的实际用户 ID,具有 set-user-ID 特权的程序也可以根据需要取得其调用者的权限。这种机制可以让用户执行一些经过谨慎设计的、调用了特权系统入口(system entry)的命令。例如,有一个只有 “超级用户”(如下)才能调用的系统入口,它可以创建一个空目录。如上所述,目录应该有 “.” 和 “..” 两个条目。创建目录的命令由超级用户拥有,并设置了 set-user-ID 位。在检查了其调用者对创建指定目录的授权后,创建目录,并为 “.” 和 “..” 设置条目。

由于任何人都可以在自己的文件上设置 set-user-ID 位,所以这种机制一般不需要管理员干预。例如,这种保护方案很容易解决 [7] 中提出的 MOO 会计问题(译注:实际上是指一个游戏的积分系统不被一般用户任意篡改的问题)。

系统中有一个特殊的用户 ID(即 "超级用户" 的 ID)不受通常的文件访问限制;因此(例如)可以绕开保护系统不必要的干扰,直接编写用于转储和重新加载文件系统的程序。

3.6 I/O 调用

我们通过对 I/O 的系统调用的设计,消除了各种设备和访问方式的差异。没有 “随机” 和顺序 I/O 的区别,系统也不强迫规定逻辑记录的大小。普通文件的大小是由写进文件的最高字节数决定的,没有必要也不可能预先确定文件的大小。

为了说明 UNIX 中 I/O 的精髓,下面总结了一些基本的调用。我们用一种匿名语言指明所需的参数,而不涉及机器语言编程的复杂性。对系统的每一次调用都有可能导致错误返回,为了简单起见,在调用序列中不做表示。

要读取或写入一个假定已经存在的文件,必须通过以下调用打开它:

filep = open(name, flag)

name 表示文件的名称,可以是任意的一个路径名。flag 参数表示文件是读、写还是 “更新”,即同时读和写。

返回值 filep 被称作文件描述符。它是一个小整数,作为该文件的标识,用于后续调用读、写或其他操作。

要创建一个新的文件或完全重写一个旧的文件,可以使用 create 系统调用。当指定的文件不存在时,它会创建一个新文件,若存在,则将旧文件截至零长度。create 也会打开新建的文件以供写入,就像 open 那样,会返回一个文件描述符。

文件系统中没有用户可见的锁,也没有限制打开一个文件进行读写的用户数量;虽然当两个用户同时对一个文件进行写入时,文件的内容有可能会错乱,但在实际操作中,并没有出现困难。我们认为,在我们的环境中,锁既不必要,也不足以防止同一文件中各用户之间的干扰。说它不必要,是因为我们面对的并不是若干个独立的进程维护的大型单文件数据库。说它不足以,则是因为一般意义上的锁,即阻止一个用户在另一个用户正在读取的文件上写入,仍不能防止混乱,例如,当两个用户都在编辑同一个文件,但他们的编辑器会先将文件复制一份出来。

应该说,当两个用户同时进行在同一文件上写东西、在同一目录下创建文件或删除对方打开的文件等相互干扰的活动时,系统有充分的内部互锁来维持文件系统的逻辑一致性。

除了下面的说明,读和写是都是连续的。这意味着,如果文件中的某个字节是最近被写入(或读取)的,那么下一个 I/O 调用就会隐式地指向该字节的下一个字节。对于每一个打开的文件,都有一个指针,由系统维护,它表示下一个要读或写的字节。如果读或写了 n 个字节,指针就会前进 n 个字节。

一个文件打开后,可以作如下的调用:

n = read(filep, buffer, count)
n = write(filep, buffer, count)

在 filep 代表的文件和 buffer 代表的字节数组之间,会传输至多 count 个字节的数据。返回值 n 是实际传输的字节数。在写的情况下,n 与 count 会一致,除非一些特殊的情况,如 I/O 错误或特殊文件的物理媒介终止;而在读的情况下,即使不出现异常,n 也可以小于 count。如果读指针很接近文件的末端,以至于读取 count 个字符会导致读到末端之外,那么传输的字节数会让指针刚好抵达文件末端;另外,对于打字机类的设备肯定不会读取超过一行的输入。当 read 调用返回的 n 等于零时,表示文件的结束。对于磁盘文件,当读指针前进到文件当前的大小时,就会出现这种情况。而对于打字机,可以通过由该打字机规定的转义序列来生成文件结束标识(EOF)。

写入到文件的字节只会改变由写指针的位置和 count 计数所指定的那些字节,文件的其他部分保持不变。如果最后一个写入的字节位于文件的末端之外,文件将按需进行扩充。

要随机(直接访问)I/O,只需将读或写的指针移动到文件中的适当位置即可:

location = seek(filep, base, offset)

与文件 filep 相关联的指针,将根据起点 base,从文件的开头、中间或结尾,移动 offset 个字节。offset 可以是负数。对于某些设备 (如纸带和打字机),seek 调用会被忽略。指针距离文件开头的实际偏移量将返回在 location 中。

3.6.1 其他 I/O 调用

还有几个与 I/O 和文件系统有关的系统入口,这里暂不讨论。例如:关闭文件、获取文件的状态、改变文件的保护模式或所有者、创建目录、生成现有文件的链接、删除文件。

4. 文件系统的实现

如上文 §3.2 所述,一个目录项只包含一个关联文件的名称和一个指向文件本身的指针。这个指针是一个整数,称为文件的 i-number(index number)。当访问某个文件时,它的 i-number 将被用作索引,对一个由该目录所在设备的已知区域内所保存的系统表(即 i-list)进行查找。由此找到的条目(文件的 i-node)包含的文件描述如下:

  1. 所有者
  2. 保护位
  3. 物理磁盘或磁带存放文件内容的地址
  4. 文件大小
  5. 最后修改时间
  6. 文件的链接数,即该文件在目录中出现的次数
  7. 一个二进制位,表明该文件是否为目录
  8. 一个二进制位,表明该文件是否为特殊文件
  9. 一个二进制位,表明该文件是 “大文件” 还是 “小文件”

open 或 create 系统调用的目的是通过搜索显式或隐式命名的目录,将用户给出的路径名转换成 i-number。一旦文件被打开,它的设备、i-number 和读写指针就会被存储在一张系统表中,该表由 open 或 create 返回的文件描述符进行索引。因此,后续对文件调用读或写时提供的文件描述符,可以很容易地关联到访问文件所需的信息。

创建一个新的文件时,会给它分配一个 i-node,并建立一个包含文件名和 i-node编号(译注:即 i-number)的目录项。要链接到一个现有的文件,需要创建一个带有新名称的目录项,从原来的文件条目中复制 i-number,并递增 i-node 的链接数(link-count)字段。移除(删除)一个文件,则是通过递减其目录项所指向的 i-node 的链接数,并删除该目录项。如果链接数减到 0,文件占用的任何磁盘块都会被释放,i-node 也会被解除分配。

任何具有文件系统的固定或移动磁盘上的空间都被划分为若干 512 字节的块,逻辑地址从 0 分配到上限,具体上限取决于设备。在每个文件的 i-node 上有一块区域可存放 8 个设备地址。一个(非特殊)小文件 可直接填入不超过 8 个的块;在这种情况下,i-node 上存的是块本身的地址。对于(非特殊)大文件,8 个设备地址中的每一个都可以指向间接的块,一个间接块可存放 256 个块地址用来组成文件。这种文件能够大到 8⋅256⋅512 即 1,048,576(2^20)个字节。

上述讨论适用于普通文件。当对一个在 i-node 中提示为特殊文件的文件发出 I/O 请求时,后 7 个设备地址字是不重要的,该列表被解释为一对构成内部设备(device)名称的字节。这些字节分别指定了设备类型和子设备号。设备类型表示该设备上的 I/O 将由哪种系统程序处理;子设备号则用于选择,例如,选择连接到某一控制器上的磁盘驱动器、选择数个打字机接口中的一个。

在这种环境下,mount 系统调用 (§3.4) 的实现非常简单。mount 维护了一个系统表(译注:一个映射关系),它的自变量是 mount 过程中指定的普通文件的 i-number 和设备名,对应的值是指定的特殊文件的设备名。在 open 或 create 过程中扫描路径名时,会对每一组 (i-number,device) 进行搜索,如果发现匹配,i-number 就会被替换成 1(也就是所有文件系统中根目录的 i-number),设备名则替换成表中对应的值。

在用户看来,文件的读和写都是同步的,没有缓冲。也就是说,在 read 调用返回后,数据立即可以使用,反之,在 write 之后,用户的工作空间可以重新使用。实际上系统维护了一个相当复杂的缓冲机制,大大减少了访问文件所需的 I/O 操作次数。下面假设进行了一次 write 调用,指定传输一个字节。

UNIX 将搜索各个缓冲区,查看受影响的磁盘块当前是否驻留在核心内存中;如果没有,则从设备中读入。然后,受影响的字节会在缓冲区中被替换,并在待写块的列表中新增一项。此后 write 调用便直接返回,尽管实际的 I/O 可能要到以后才会完成。相反,如果是读取一个字节,系统会判断该字节所在的二级存储块是否已经在系统的某个缓冲区中;如果是,则可以立即返回该字节。如果没有,则将该块读入缓冲区,并挑出该字节。

一个以 512 字节为单位读写文件的程序比每次只读或写一个字节的程序有优势,但收益并不是很大,它主要来自于避免系统开销。一个不进行大容量 I/O 或不常用的程序,用尽可能小的单位进行读写仍然是合理的。

i-list 的概念是 UNIX 的一个不同寻常的特征。在实践中,这种组织文件系统的方法被证明是相当可靠和容易处理的。对系统本身来说,它的一个优点是每个文件都有一个简短的、无歧义的名字,这个名字以一种简单的方式与访问文件所需的保护、地址和其他信息相关联。它还允许一种相当简单和快速的算法来检查文件系统的一致性,例如验证每个设备中包含有用信息的部分和可自由分配的部分是否不相交,加起来是否能填满设备上的空间。这种算法是独立于目录层次的,因为它只需要扫描线性排列的 i-list。同时,i-list 的概念还产生了某些在其他文件系统组织中没有的奇特性。例如,有一个问题是,由于一个文件的所有目录项都具有平等的地位,那么一个文件所占的空间应该对谁进行收费?一般来说,向文件的所有者收费是不公平的,因为有可能一个用户创建一个文件,另一个用户链接到该文件,而第一个用户可以删除该文件。第一个用户仍然是文件的所有者,但应该向第二个用户收费。最简单合理公平的算法似乎是将费用均摊到拥有文件链接的用户。当前版本的 UNIX 由于不收取任何费用,避开了这一问题。

4.1 文件系统的效率

为了说明 UNIX 的总体效率,特别是文件系统的效率,我们记录了对一个 7621 行程序的汇编耗时。汇编在机器上单独运行;总时钟时间为 35.9 秒,每秒钟 212 行。时间分配如下:63.5% 的汇编器执行时间,16.5% 的系统开销,20.0% 的磁盘等待时间。我们不试图对这些数字进行任何解释,也不与其他系统进行比较,只是指出我们对系统的整体性能基本上是满意的。

5. 进程和映像

一个映像(image)是一个计算机执行环境。它包括核心映像、通用寄存器值、打开文件的状态、当前目录等。一个映像就是一台伪计算机的当前状态。

一个进程(process)就是一个映像的执行。当处理器代表进程执行时,映像必须驻留在核心中;在其他进程执行期间,映像仍然驻留在核心中,除非出现一个活动的、优先级较高的进程,迫使它被换出到固定头磁盘中。

映像的用户核心部分分为三个逻辑段。程序文本段开始于虚拟地址空间的 0 位置。在执行过程中,该段受到写保护,并且在执行同一程序的所有进程之间共享一个副本。在虚拟地址空间中程序文本段上方的第一个 8K 字节边界处,开辟了一个非共享的、可写的数据段,该数据段的大小可以通过系统调用进行扩展。从虚拟地址空间的最高地址往下是栈段,随着硬件的栈指针的波动,栈段自动向下扩充。

5.1 进程

除非 UNIX 正在引导自己启动运行,只有使用 fork 系统调用才能产生一个新的进程。

processid = fork(label)

当 fork 被一个进程执行时,它会分裂成两个独立执行的进程。这两个进程拥有原始核心映像的独立副本,并共享任何打开的文件。新进程的不同之处仅在于其中一个被认为是父进程:在父进程中,控制权直接从 fork 返回,而在子进程中,控制权被传递给位置标记 labelfork 调用返回的 processid 是另一个进程的 ID。

因为父进程和子进程的返回点不一样,所以 fork 后存在的每个映像都可以判断它是父进程还是子进程。

5.2 管道

进程可以使用跟文件系统 I/O 相同的 read 和 write 系统调用与相关进程进行通信。调用

filep = pipe()

返回一个文件描述符 filep,并创建一个称为管道(pipe)的进程间通道。这个通道就像其他打开的文件一样,通过 fork 调用在映像中从父进程传递到子进程。使用管道文件描述符进行读取时,会先等待另一个进程使用同一管道的文件描述符写入。此时,数据在两个进程的映像之间传递。两个进程都不需要知道,它们读写的是管道而非普通文件。

虽然通过管道进行的进程间通信是一个相当有价值的工具(见 §6.2),但它不是一个完全通用的机制,因为管道必须由相关进程共同的祖先建立。

5.3 程序的执行

另一个重要的系统原语是

execute(file, arg1, arg2, ..., argn)

它请求系统读入并执行以 file 命名的程序,并传入字符串参数 arg1arg2,...,argn。通常情况下,arg1 应该是与 file 相同的字符串,这样程序就可以确定它被调用的名称。使用 execute 的进程中的所有代码和数据都会从该文件替换,但打开的文件、当前目录和进程间的关系都不会改变。只有当调用失败时,例如因为找不到文件或因为其执行权限位没有设置,才会从 execute 原语中返回;它类似于 “跳转” 机器指令,而非子程序调用。

5.4 进程同步

另一个操控进程的系统调用

processid = wait()

会使其调用者暂停执行,直到它的一个子进程完成执行。然后 wait 返回被终止进程的 processid。如果调用进程没有子进程,则会采取错误返回。wait 也可以呈现来自子进程的某些状态,甚至还可以呈现来自孙子进程或更远的祖先进程的状态;参见 §5.5。

5.5 终止

最后,

exit(status)

会终止一个进程,销毁它的映像,关闭它打开的文件,通常还会消除该进程。当通过 wait 原语通知父进程时,父进程可以拿到指定的状态(status);如果父进程已经终止,则祖父进程可以得到该状态,以此类推。进程也可能由于各种非法行为或用户产生的信号而终止(下文 §7)。

6. 外壳程序(Shell)

对于大多数用户来说,与 UNIX 的通信是借助于一个叫做 Shell 的程序进行的。Shell 是一个命令行解释器:它读取用户输入的行,并将其解释为执行其他程序的请求。一条命令行,在最简的形式下,由命令名称和命令参数组成,所有参数用空格分隔:

command arg1 arg2 ... argn

Shell 将命令名和参数分割成独立的字符串。然后寻找一个名字为 command 的文件,command 可以是一个路径名,包含 “/” 字符来指定系统中的任何文件。若找到了 command,则将其带入核心并执行。Shell 收集到的参数可以被命令访问。当命令执行完毕后,Shell 恢复自己的执行,并通过一个提示字符表示准备接受另一条命令。

如果找不到文件 command,Shell 会在命令前加上字符串 /bin/,并再次尝试寻找文件。目录 /bin 包含了所有需要普遍使用的命令。

6.1 标准 I/O

上文 §3 中对 I/O 的讨论似乎意味着,程序使用的每一个文件都必须由该程序打开或创建,才能得到文件的文件描述符。然而,由 Shell 执行的程序,一开始就有两个打开的文件,其文件描述符分别为 0 和 1。当这样的程序开始执行时,文件 1 是用来写入的,最好将其理解为标准输出文件。除了下面指出的情况外,这个文件就是用户的打字机(译注:本文出现的打字机包含打印输出功能,可以理解为现在的终端)。因此,希望写入提示或诊断信息的程序通常使用文件描述符 1。相反地,文件 0 则默认用于读取,希望读取用户输入的信息的程序通常会读取这个文件。

Shell 能够改变这两个文件描述符默认的打字机显示器和键盘的标准分配。如果一行命令的其中一个参数以 “>” 为前缀,文件描述符 1 将在命令执行期间,引用 “>” 后面所命名的文件。例如,

ls

通常会在打字机上列出当前目录下各文件的名称。命令

ls >there

创建一个名为 there 的文件,并将列出来的结果放在文件中。因此,参数 “>there” 的意思是,“把输出放入 there”。另一方面,

ed

通常会进入编辑器,它通过打字机接受用户的请求。命令

ed <script

将 script 解释为编辑器命令的一个文件,因此 “<script” 的意思是,“从 script 中获取输入”。

虽然 “<” 或 “>” 后面的文件名看起来是命令的一个参数,但实际上它完全由 Shell 解释,根本没有传递给命令。因此,命令的内部不需要特殊的代码来处理 I/O 重定向,只需要在适当的地方使用标准的文件描述符 0 和 1 即可。

6.2 过滤器

这里对标准 I/O 的概念进行了扩展,从而能够将一个命令的输出引导到另一个命令的输入。由竖线隔开的命令序列会让 Shell 同时执行所有命令,并安排将每个命令的标准输出传给序列中下一个命令的标准输入。因此,在命令行

ls | pr –2 | opr

中,ls 列出了当前目录中的文件名;它的输出被传递给 prpr 用日期标题对输入进行分页。参数 “-2” 表示双列。同样地,pr 的输出也会被输入到 opr 中,这个命令会把它的输入汇集到一个文件中,以便离线打印。

这一过程也可以用比较笨重的方式来实现:

ls >temp1
pr –2 <temp1 >temp2
opr <temp2

然后再删除临时文件。在没有重定向输出和输入的能力的情况下,一个更笨重的方法是要求 ls 命令能接受将其输出分页、以多栏格式打印、并安排其输出脱机传送的用户请求。事实上,期望 ls 等命令的作者提供如此广泛的输出选项是令人吃惊的,实际上出于效率的考虑,这也是不明智的。

像 pr 这样把标准输入(处理后)复制到标准输出的程序称为过滤器(filter)。一些我们认为有用的过滤器可以进行字符翻译、输入的排序以及加密和解密。

6.3 命令分隔符:多任务

Shell 提供的另一个功能比较简单。命令不需要在不同的行中,而可以用分号来分隔。

ls ; ed

将首先列出当前目录的内容,然后进入编辑器。

一个相关的功能比较有趣。如果一个命令后面跟着 “&”,Shell 不会等待命令结束后再提示,而是立即准备接受新的命令。比如说

as source >output &

会对 source 进行汇编, 并将诊断输出到 output;无论汇编时间多长,Shell 都会立即返回。当 Shell 不等待命令的完成时,会打印出运行该命令的进程的 ID。这个 ID 可以用来等待命令完成或终止该命令。在一行中可以多次使用 “&”:

as source >output & ls >files &

在后台同时进行汇编和列表。在上面使用 “&” 的例子中,提供了打字机以外的输出文件;如果不这样,各种命令的输出就会混在一起。

Shell 还允许在上述操作中使用括号。例如:

(date; ls) >x &

将当前的日期和时间,接上当前目录的列表,打印到文件 x 上。Shell 也会立即返回,等待下一个请求。

6.4 Shell作为命令:命令文件

Shell 本身就是一个命令,可以递归调用。假设文件 tryout 包含以下几行

as source
mv a.out testprog
testprog

mv 命令使文件 a.out 重命名为 testproga.out 是汇编程序的 (二进制) 输出,准备用于执行。因此,如果在控制台上输入上述三行代码,source 将被汇编,产生的程序被命名为 testprog,并执行 testprog。当这几行放在 tryout 文件中时,命令

sh < tryout

会让 Shell 程序 sh 来顺序执行那些命令。

Shell 还有更多的功能,包括替换参数和从目录中指定的文件名子集构建参数列表的能力。还可以根据字符串比较或给定文件存在与否,条件地执行命令,以及在命令序列文件中进行控制转移。

6.5 Shell的实现

现在可以了解 Shell 运作的概要了。大多数时候,Shell 都在等待用户输入命令。当敲入换行符来结束当前行时,Shell 的 read 调用就会返回。Shell 对命令行进行分析,将参数变成适合 execute 的形式。然后调用 fork。子进程的代码当然还是 Shell 的代码,它会试图以适当的参数调用 execute。如果可以执行,就会带入并开始执行给定名称的程序。同时,由 fork 产生的另一个进程,也就是父进程,会等待子进程死亡。此后,Shell 知道命令已经结束,于是键入它的提示符并读取打字机以获得另一条命令。

在这个框架下,后台进程的实现就稀松平常了;每当命令行包含 “&” 时,Shell 仅仅是不会再去等待它所创建的执行该命令的线程。

令人高兴的是,所有这些机制都与标准输入输出文件的概念很好地融合在一起。当一个进程被 fork 原语创建时,它不仅继承了父进程的核心映像,而且还继承了父进程中当前打开的所有文件,包括文件描述符为 0 和 1 的文件。 当然,Shell 使用这两个文件来读取命令行和写入提示及诊断,且在普通情况下,它的子进程——命令程序——会自动继承这些文件。但是,当给出一个带有 “<” 或 “>” 的参数时,子程序就会在执行 execute 前,让标准的 I/O 文件描述符 0 或 1,引用相应参数所指定名称的文件。这很容易做到,因为根据约定,当打开(或创建)一个新文件时,会分配一个最小的未使用的文件描述符;所以只需要关闭文件 0(或 1),然后打开指定的文件即可。由于运行命令程序的进程在运行完后会直接终止,所以当进程死亡时,“<” 或 “>” 后指定的文件与文件描述符 0 或 1 之间的关联会自动结束。因此 Shell 不需要知道自身的标准输入和输出的文件的实际名称,因为它根本不需要重新打开这些文件。

过滤器是标准 I/O 重定向的简单扩展,用管道代替了文件。

在一般情况下,Shell 的主循环永远不会终止。(主循环包括 fork 返回的属于父进程的那个分支,也就是会执行 wait,然后读取下一条命令行的分支)。让 Shell 终止的一种方式是在其输入文件上探到一个文件结束(EOF)条件。因此,当 Shell 作为命令与给定的输入文件一起执行时,如

sh < comfile

comfile 中的命令将被执行,直到到达 comfile 的结束点;然后 sh 调用的 Shell 实例将终止。由于该 Shell 进程是另一个 Shell 实例的子进程,在后者中执行的等待将返回,从而可以处理下一条命令。

6.6 初始化

用户键入命令的 Shell 实例本身就是另一个进程的子进程。UNIX 初始化的最后一步是创建一个进程,并(由 execute)调用一个名为 init 的程序。init 的作用是为用户拨号接入的各个打字机信道创建一个进程。init 的各个子实例会打开相应的打字机来输入和输出。由于调用 init 时没有文件打开,所以在每个进程中,文件描述符 0 会指向打字机键盘,文件描述符 1 会指向打印机。每个进程都会打出一条消息,请求用户登录,并等待,读取打字机输入的用户回应。一开始,没有人登录,所以每个进程只是挂起。最后,有人输入了自己的名字或其他 ID。init 的相应实例被唤醒,接收登录行,并读取密码文件。如果找到了用户名,并且他能够提供正确的密码,init 就会切换到用户的默认当前目录,将进程的用户 ID 设置为登录者的 ID,并执行 execute 启动一个 Shell。至此,该 Shell 已经可以接收命令,而登录协议就此完成。

同时,init 的主流路径(即那些之后会成为 Shell 的子实体的父体)会执行一个 wait。如果其中一个子进程终止,无论是因为 Shell 探到了文件结束,还是因为用户输入了错误的名称或密码,init 的这条路径就会简单地重新创建这个已经失效的进程,而这个进程又会重新打开相应的输入和输出文件,并再次打出登录提示。因此,用户只需键入文件结束序列就可以注销,省得使用 Shell 命令。

6.7 其他程序作为外壳程序

上文所述的 Shell 是为了让用户充分使用系统的设施而设计,因为它执行任何程序时都有适当保护模式。然而,有时我们会想要一个不同的系统界面,但这同样很容易实现。

回顾一下,当用户通过提供名字和密码成功登录后,init 通常会调用 Shell 来解释命令行。用户在密码文件中的条目可以包含一个程序的名称,在登录后将调用这个程序,而不是 Shell。这个程序可以自由地以任何方式解释用户的信息。

例如,对秘书编辑系统的用户而言,其密码文件条目中指定使用的是编辑器 ed 而不是 Shell。这样,当编辑系统的用户登录后,他们就在编辑器中,可以立即开始工作;同时,也可以防止他们调用不想让他们使用的 UNIX 程序。实践证明,允许暂时离开编辑器,以执行格式化程序和其他实用程序是可取的。

UNIX 上的一些游戏(如国际象棋、21 点、3D 井字棋)体现了一种更为严格的限制环境。每一款游戏的密码文件中都有一个条目,指定要调用的相应的游戏程序而不是 Shell。当人们以其中一个游戏的玩家身份登录时,会发现自己被限制在游戏中,无法探寻 UNIX 这个整体中可能更为有趣的地方。

7. 陷阱

PDP-11 硬件可以检测到一些程序故障,例如对不存在的内存的引用,未实现的指令,以及在需要偶数地址的地方使用奇数地址。这类故障会导致处理器陷入系统例程。当捕捉到非法操作时,除非有其他安排,否则系统会终止进程,并将用户的映像写入当前目录下的核心(core)文件。然后可以使用调试器来确定故障发生时程序的状态。

当一个程序正在循环,或产生了不想要的输出,又或者其用户有了别的想法时,可以使用中断(interrupt)信号中止该程序,该信号通过输入 “delete” 字符产生。除非采取了特殊的行动,否则这个信号只是使程序中止执行,而不产生核心映像文件。

还有一个退出(quit)信号,用于强制生成核心映像。因此,即使在不预先安排的情况下中止意外循环的程序,仍然能够检查其核心映像。

硬件产生的故障以及中断和退出信号,可以通过请求,被进程忽略或捕获。例如,Shell 会忽略退出信号,以防止退出导致用户注销。编辑器会捕获中断信号,并退回到命令层。这很有用,因为可以停止较长的打印输出,而不丢失正在进行中的工作(编辑器操作的是所编辑文件的副本)。没有浮点硬件的系统,会捕获未实现指令,并解释浮点指令。

8. 展望

也许矛盾的是,UNIX 的成功在很大程度上是由于它并非为任何预定的目标而设计。当时我们之中有一个人(Thompson)对现有的计算机设施不满意,发现了很少被使用的 PDP-7 系统,并开始着手创造一个更友好的环境,于是编写了第一个版本。这一基本上属于个人的工作取得了足够的成功,引起了其余作者和其他人的兴趣,后来又以此购置了 PDP-11/20,专门用来支持文本编辑和格式化系统。然后又轮到 11/20 被淘汰,UNIX 已经被证明是有用的,足以说服管理层投资于 PDP-11/45。我们整个工作的目标,如果说有的话,总是关注于与机器建立一种舒适的关系,以及探索操作系统中的创意和发明。我们并不迫于去满足他人的要求,对于这种自由,我们心存感激。

回顾起来,可以看到影响 UNIX 设计的三个因素。

首先,因为我们是程序员,自然而然地就将系统设计成能够方便地编写、测试和运行程序。为了编程的方便,我们希望该系统最突出的表现就是被做成交互式使用,尽管最初的版本只支持一个用户。我们相信,一个设计得当的交互式系统比 “批处理” 系统用起来更有效率、更令人满意。而且这样的系统相当容易适配非交互式使用,反之则不然。

其次,对系统及其软件的大小一直存在着相当严格的限制。另一方面,出于对合理的效率和表现力的需求,这种大小的限制所促进的不仅仅是经济上的节省,还有一种设计上的优雅。这可能是 “苦难救赎” 哲学的一个简陋版本,但在我们的案例中,它起了作用。

第三,几乎从一开始,该系统就能够而且确实做到了对自身的维护。这一事实比看起来更重要。如果一个系统的设计者被强制使用该系统,他们很快就能意识到该系统在功能上和表面上的缺陷,并有强烈的动机在为时已晚之前纠正这些缺陷。由于所有的源程序总是可以在网上获得,而且很容易修改,所以每当发明、发现或有人提议新的想法时,我们会愿意修改和重写系统及其软件。

本文所讨论的 UNIX 的各个方面至少清楚地展示了上述设计考虑中的前两个方面。例如,从编程的角度来看,文件系统的接口是非常方便的。设计了尽可能低级的接口,从而消除各种设备和文件之间的区别,以及直接访问和顺序访问之间的区别。不需要大型的 “访问方法” 例程来隔离程序员与系统调用;事实上,所有的用户程序要么直接调用系统,要么用一个只有几十条指令的小库程序,缓冲若干字符,一次性读取或写入。

编程方便的另一个重要方面是没有结构复杂的 “控制块”,其中一部分结构由文件系统或其他系统调用来维护和依赖。一般来说,程序地址空间的内容归属程序自身,我们尽量避免对地址空间内的数据结构施加限制。

考虑到所有程序都应该可以将任何文件或设备作为输入或输出来使用,从空间效率的角度来看,将设备依赖的考量放进操作系统中也是可取的。唯一的取舍似乎在于,是在所有程序中加载处理每个设备的例程,占用比较大的空间,还是在实际需要的时候依靠某种方法动态地链接到适合每个设备的例程,花费更多的硬件资源。

同样,进程控制方案和命令界面也被证明是方便和有效的。由于 Shell 是作为一个普通的、可交换的用户程序来运行的,因此它在系统中不占用任何固有的空间,而且可以用很少的开销达到预期的效果。特别是,在这一框架下,由于 Shell 是作为一个进程来执行的,而这个进程又会派生出其他进程来执行命令,所以 I/O 重定向、后台进程、命令文件和用户可选系统接口等概念在实现上都变得稀松平常。

8.1 影响

UNIX 的成功并不那么在于新的发明,而在于充分地利用了一套经过精心挑选的富饶的思想,尤其在于它体现了这些思想可以成为实现一个小而强大的操作系统的关键。

我们实现的 fork 操作,基本上是在 Berkeley 分时系统中出现的 [8]。在很多方面,我们受到了 Multics 的影响,它展现了 I/O 系统调用的独特形式 [9],也提出了 Shell 的名称和它的一般功能。Multics 的早期设计也给我们提示了 Shell 应该为每条命令创建一个进程的概念,尽管在那个系统中,后来出于效率的考虑,放弃了这个概念。TENEX [10] 也采用了类似的方案。

引用

Unix 40年:Unix年鉴_unix 40年:unix年鉴》_haoel的博客-CSDN博客

Unix传奇 (上篇)

Unix传奇 (下篇)

其实Unix很简单

the-unix-time-sharing-system-chinese/the_unix_time-sharing_system_CN.md at master · chocovon/the-unix-time-sharing-system-chinese · GitHub

https://bbs.csdn.net/topics/39060161

猜你喜欢

转载自blog.csdn.net/qq8864/article/details/130704963