BBS水木清华站∶精华区

发信人: taosm (128+64-->cool), 信区: unix 
标  题: unix环境高级编程--第1章 UNIX基础知识 
发信站: 西十八BBS (Fri Mar 10 09:22:45 2000), 转信 
 
11〓引言 
所有操作系统都向它们运行的程序提供服务。典型的服务是执行一道新程序、打开 
一个文件 
、读一个文件、分配一个存储区、获得当前时间等等,本书的焦点是说明各种Uni 
x操作系统 
版本所提供的服务。 
以严格的步进方式,不超前引用尚未说明过的术语来说明Unix几乎是不可能的(可 
能也会是 
令人厌烦的)。本章从程序设计人员的角度快速周游Unix,我们将对书中引用的一 
些术语和 
概念进行简要的说明并给示实例。在以后各章中,我们将对这些作更详细的说明。 
本章也对 
不熟悉Unix的程序设计人员介绍、概述Unix提供的各种服务。 
12〓登录(Logging ln) 
登录名 
当我们向Unix系统登录时,先键入登录名,然后键入口令字。系统在其口令文件, 
通常是/e 
tc/passwd文件中查看我们的登录名。在口令文件中的登录项,由7个以冒号分隔的 
字段组成 
:登录名,加密口令字,数字用户ID(224),数字组ID(20),注释字段,起始目录 
(/home/st 
evens),以及shell程序(/bin/ksh)。 
很多比较新的系统已将加密口令字移到另一个文件中。在第六章,我们将说明这种 
文件,以 
及存取它们的函数。 
shell 
我们登录后,系统先典型地显示一些消息,然后我们就可以向shell程序键入命令 
。shell是 
一个命令行解释器,它读用户输入,然后执行命令,用户通常用终端,有时则通过 
文件(称 
为shell脚本)向shell进行输入。常用的shell是: 
·Bourne shell,/bin/sh 
·Cshell,/bin/csh 
·Kornshell,/bin/ksh 
系统从口令字文件中与我们相关的登录项的最后一个字段了解到应为我们执行那一 
个shell 
。自Version 7(第七版)以来,一直在使用Bourne shell,几乎每一个现存的Unix 
系统都提 
供Bourne shell。CShell是在Berkeley(贝克莱)开发的,所有BSD版本都提供这种 
shell。另 
外,Cshell也由AT&T系统V386 R32和系统VR4(SVR4)提供,(在下一章,我们将 
对这些不 
同 
的Unix版本作更多说明。KornShell是Bourne shell的后继者,它由SVR4提供。Ko 
rnshell在 
大多数Unix系统上运行,但在SVR4之前,通常它需要另行购买,所以没有其它两种 
shell流 
行。 
Bourne shell是由Steve Bourne在Bell实验室中开发的其控制流结构使人想起Alg 
ol68C S 
hell是在贝克莱由Bill Joy完成的,其基础是第6版shell(不是Bourne shell)。其 
控制结构 
很象C语言,它支持了一些Bourne shell没有提供的功能-作业控制,历史机制和命 
令行编 
辑。Kornshell是由David Korn在Bell实验室中开发的,它兼容Bourne shell,并 
且也包含 
了使C shell非常流行的一些功能-作业控制、命令行编辑等。 
在全书中,我们都会使用这种形式的注释以说明历史沿革,并对不同的Unix实现进 
行比较。 
当说明了历史缘由后,常常使得采用一种特定实现技术的理由变得清晰起来。 
在全书中,我们将使用很多shell实例,以执行我们已开发的程序,其中将应用Bo 
urne shel 
l和Kornshell都具有的功能。 
13〓文件和目录 
文件系统(Filesystem) 
Unix文件系统是目录和文件的一种分层次的安排,目录的起点称为根(root),其名 
字是一个 
字符/。 
一个目录是一个包含目录项的文件,在逻辑上,我们可以认为每个目录项都包含一 
个文件名 
,同时还包含说明该文件属性的信息。文件属性是:文件类型,文件长度,文件属 
主,文件 
的许可权(例如,其他用户能否存取该文件?)文件的最后修改时间等。stat和fsta 
t函数返回 
一个包含所有文件属性的信息结构。在第四章中,我们将详细说明文件的各种属性 
。 
文件名(Filename) 
一个目录中的各个名字称为文件名。不能出现在文件名中的字符只有两个,它们是 
斜线(/) 
和空操作(null)字符,斜线分隔构成路径名(在下面说明)的各文件名,空操作符则 
终止一个 
路径名,尽管如此,一个好的习惯是只使用印刷字符的一个子集作为文件名字符( 
只使用子 
集的理由是:如果在文件名中使用了某些shell特殊字符,则必须使用shell的引号 
机制来引 
用文件名)。 
当创建一个新目录时,自动创建了两个文件名:(称为点)和(称为点-点)。 
点引用当 
前目录,点-点则引用文目录。在最高层次的根目录中,点-点与点相同。 
某些Unix文件系统限制文件名的最大长度为14个字符,BSD版本则将这种限制扩展 
为255个字 
符。- 
路径名(Palhname) 
0个或多个以斜线分隔的文件名序列(可以任选地以斜线开头)构成路径名,以斜线 
开头的路 
径名称为绝对路径名,否则称为相对路径名。 
实例 
不难列出一个目录中所有文件的名字,程序11是ls(1)命令的主要实现部分 
程序11〓列出一个目录中的所有文件 
ls(1)这种表示方法是Unix的惯用方法,用以引用Unix手册集中的一个特定项。它 
引用第一 
部分中的ls项,各部分通常用数字1至8表示,在每个部分中的各项则按字母顺序排 
列。在全 
书中,我们都假定你有一份你所使用的Unix系统的手册。 
较早的Unix系统把8个部分都集中在一本Unix程序手册中,现在的趋势是把这些部 
分分别按 
排在不同的手册中:一本是由用户使用的,一本是由程序员使用的,一本是由系统 
管理员使 
用的等等。 
某些Unix系统把一个给定部分中的手册页又用一个大写字母进一步分成若干小部分 
,例如, 
AT&T〔1990e〕中的所有标准I/O函数都被指明在3s部分中,例如fopen(3s)。 
某些Unix系统,例如以Xenix为基础的系统,不同数字将手册分成若干部分,代之 
,它们用C 
表示命令(第1部分),S表示服务(通常是第2、3部分)等等。 
如果你有联机手册,则阅看ls命令手册页的方法一般是: 
man 1 ls 
程序11只打印一个目录中各个文件的名字,不显示其它信息,如若该源文件名为 
mylsc, 
则我们可以用下面的命令对其进行编辑,编辑的结果送入系统默认名为aout的可 
执行文件 
名: 
cc mylsc 
某种样本输出是: 
$ aout /dev 
 
 
MAKEDEV 
console 
tty 
mem 
kmem 
null 
 
printer 
$ aout /var/spool/mqueue 
can′t open /var/spool/mqueue:Permission denied 
$ aout /dev/tty 
can′t open /dev/tty:Not a directory 
在全书中,我们都将以这种方式表示我们输入的命令以及其输出:我们输入的字符 
以这种字 
体表示程序输出则以另一种字体表示。如果我们欲对输出添加注释,则以表示注释 
,在我们 
输入之前的美元符号($)是shell打印的提示符,我们总是将shell提示符显示为$。 
 
注意,列出的目录项不是以字母序排列的,ls命令本身则一般以字母序列出目录项 
。在这20 
行程序中,有很多细节可以考虑: 
·首先,其中包含了一个我们自己的头文件ourhdrh。在本书中,几乎每一道程 
序都包含 
此 
头文件。它包含了某些标准系统头文件,定义了许多常数及函数原型,这些都将用 
于本书的 
各个例子中,此头文件包含在附录B中。 
·main函数的说明使用了ANSI C标准所支持的新风格。(在下一章中,我们将对AN 
SI C作更 
多说明。) 
·我们取命令行的第1个参数argv〔1〕作为要列表的目录名。在第七章中,我们 
将说明mai 
n函数是如何被调用的,程序如何存取命令行参数和环境变量。 
·因为各种不同Unix系统的目录项的实际格式是不一样的,所以我们使用函数ope 
ndir,read 
dir和closedir处理目录。 
·opendir函数返回指向DIR结构的指针,并将该指针传向readdir函数。我们并不 
关心DIR结 
构中包含了什么。然后,我们在循环中调用readdir,以读每个目录项。它返回一 
个指向dir 
ent结构的指针,而当目录中已无目录项可读时则返回null指针。我们在dirent结 
构中取出 
的只是每个目录项的名字(d[CD#*2]name)。使用该名字,我们此后就可调用stat函 
数(42 
节)以决定该文件的所有属性。 
·调用了两个我们自编的函数对错误进行处理:err-sys和err-quit。我们从上面 
的输出中 
可以看到,err-sys函数打印一条消息,说明遇到了什么类型的错误。("Permissi 
on denie 
d"或"Not a directory"("许可权拒绝"或"不是一个目录"。)) 
这两个出错处理函数也在附录B中说明,我们也将在17节中更多地叙述出错处理 
。 
·当程序将结束时,它以参数O调用函数exit。函数exit终止一道程序。按惯例, 
参数O的意 
思是正常结束,参数值1~255则表示出了一种错。在85节中,我们将说明一 
道程序( 
例如一个shell或我们所编写的程序)如何获得它所执行的另一道程序的exit状态。 
 
工作目录(Working Directory) 
每个进程都有一个工作目录(有时称为当前工作目录)。所有相对路径名都从工作目 
录开始解 
释。进程可以用chdir函数更改其工作目录。 
例如,相对路径名doc/memo/joe指的是文件joe,它在目录memo中,而memo又在目 
录doc中, 
doc则应是工作目录中的一个目录项。从该路径名可以看出,doc和memo都应当是目 
录,但是 
我们却不清楚joe是文件还是目录。路径名/urs/Lib/Lint是一个绝对路径名,它指 
的是文件 
(或目录)Lint,而Lint在目录lib中,lib则在目录usr中,usr则在根目录中。 
起始目录(Home directory) 
当我们登录时,工作目录设置为起始目录,该起始目录从口令字文件(见12节)中 
我们的记 
录项中取得。 
14〓输入和输出 
文件描述符(File Descriptors) 
文字描述符是一个小的非负整数,系统核用以标识一个特定进程正在存访的文件。 
无论何时 
,系统核打开一个现存文件或创建一个文件,它就返回一个文件描述符。当读、写 
文件时, 
我们就使用它。 
标准输入、标准输出和标准出错 
按惯例,每当运行一道新程序,所有的shell,都与其打开三个文件描述符:标准 
输入、标 
准输出以及标准出错。如若象简单命令ls那样,没有做什么特殊处理,则所有这三 
个都连向 
我们的终端。大多数shell都提供一种方法,使任何一个或所有这三个描述符都能 
重新定向 
到某一个文件,例如: 
ls>filelist 
执行ls命令,其标准输出重新定向到名为filelist的文件点。 
不用缓存的I/O 
函数open、read、write、lseek以及close提供了不同缓存的I/O。这些函数都用文 
件描述符 
进行工作。 
实例 
如若我们愿望从标准输入读,并写向标准输出,则程序12可以复制任一Unix文件 
。 
程序12〓将标准输入复制到标准输出 
头文件<unistdh>(ourhdrh中包含了此头文件)及两个常数STDIN-FILEN 
O和STDOU 
T-FILENO是POSIX标准的一部分(在下一章,我们将对此作更多的说明)。很多Uni 
x系统服务 
的函数原型,例如我们调用的read和write都在此头文件中。函数原型也是ANSI C 
标准的一 
部分,我们将在本章的稍后部分对此作更多说明。 
两个常数STDIN-FILENO和STDOUT-FILENO定义在<unistdd>头文件中,它们指定了 
标准输入 
和标准输出的文件描述符。它们的典型值是0和1,但是为了可移植性,我们将使用 
这些新名 
字。 
在39节中,我们将详细地讨论BUFSIZE常数,说明各种不同值将如何影响程序的 
效率。但 
是不管该常数的值如何,此程序总能复制任一Unix文件。 
read函数返回读得的字节数,此值用作为要写的字节数。当到了文件的尾端时,r 
ead返回0 
,程序停止执行。如果发生了一个读错误,read返回-1,出错时,大多数系统函数 
返回-1。 
 
如若编辑读程序,其结果送入标准的aout文件,并以下列方式执行它: 
aout>data 
那么,标准输入是终端,标准输出则重新定向至文件data,标准出错也是终端。如 
果此输出 
文件并不存在,则shell创建它。 
在第三章中,我们将更详细地说明不用缓存的I/O函数。 
标准I/O 
标准I/O函数提供一种对不用缓存的I/O函数的带缓存的界面。使用标准I/O使我们 
无需担心 
如何选取最佳的缓存长度,例如程序12中的BUFSIZE常数。另一个使用标准I/O函 
数的优点 
与处理输入行有关(常常发生在Unix的应用中)。例如,fgets函数读一完整的行, 
而另一方 
面,read函数读指定字节数。 
我们最熟悉的标准I/O函数是printf。在调用printf的程序中,我们总是包括<std 
ioh>(通 
常包括在ourhdrh中),因为此头文件包括了所有标准I/O函数的原型。 
实例 
程序13的功能类似于调用read和write的前一道程序12,我们将在58中对程 
序13作 
更详细的说明。它将标准输入复制到标准输出,于是也就能复制任一Unix文件。 
 
程序13〓用标准I/O将标准输入复制到标准输出 
函数getc一次读1个字符,然后putc将此字符写到标准输出。读到输入的最后1个字 
节时,ge 
tc返回常数EOF。标准输入、输出常数stdin和stdout定义在头文件<stdioh>中, 
它们分别 
表示标准输入和标准输出文件。 
15〓程序和进程 
程序(Program) 
一道程序是存放在一个磁盘文件中的可执行文件。使用6个exec函数中的一个由核 
将程序读 
入存储器,并使其执行。我们将在89节中说明这些exec函数。 
进程和进程ID(Processes and Process ID) 
一道程序的一个执行实例被称为一个进程。在本书的几乎每一页中都会使用这一术 
语。某些 
操作系统用任务表示正被执行的程序。 
每个Unix进程都一定有一个唯一的数字标识符,被称之为进程ID。进程ID总是一非 
负整数。 
 
实例 
程序14〓打印其进程ID 
程序14〓打印进程ID 
如若编辑该程序,其结果送入aout文件,然后执行它,则有: 
$ aout 
hello world from process ID 851 
$ aout 
hello world from precess ID 854 
此程序运行时,它调用函数getpid得到其进程ID。 
进程控制 
有三个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有六种变体, 
但我们经 
常把它们统称为exec函数。) 
实例 
程序15〓从标准输入读命令并执行它们 
Unix的进程控制功能可以用一个较简单的程序(程序15)说明,该程序从标准输入 
读命令, 
然后执行这些命令。这是一个类似于shell程序的基本实施部分。在这30行程序中 
,有很多 
功能可以思考: 
用标准I/O函数fgets从标准输入一次读一行,当作为行的第1个字符键入文件结束 
字符(通常 
是控制-D)时,fgets返回一个null指针,于是循环终止,进程也就终止。在第十一 
章中,我 
们将说明所有特殊的终端字符(文件结束、退格字符、擦除整行等等),以及如何改 
变它们。 
 
·因为fgets返回的每一行都以新行符终止,后附一个null字节,我们用标准C函数 
strlen计 
算此字符串的长度,然后用一个null字节代换新行符。这一操作的目的是因为exe 
clp函数要 
求的是以null结束的参数,而不是以新行符结束的参数。 
·调用fork创建一个新进程,新进程是调用进程的复制品,我们称调用进程为父进 
程,新创 
建的进程为子进程。fork对父进程,返回新子进程的非负进程ID,对子进程则返回 
0。因为f 
ork创建一新进程,所以我们说它被调用一次(由父进程),但返回两次(在父进程中 
和在子进 
程中)。 
·在子进程中,我们调用execlp以执行从标准输入读入的命令。这使子进程更换了 
新的程序 
文件。fork和跟附其后的exec的组合是某些操作系统所称的产生一个新进程。在U 
nix中,这 
两个部分分成两个函数。在第八章中,我们将对这些函数作更多说明。 
·子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这一要求由 
调用wait 
pid实现,其参数指定要等待的进程(在这里,pid参数是子进程ID)。waitpid函数 
也返回子 
进程的终止状态(status变量)。在此简单程序中,我们没有使用该值。如若需要, 
可以用此 
值精确地确定子进程是如何终止的。 
·该程序的最主要限制是可能向执行的命令传递参数。例如我们不能指定要列表的 
目录名。 
我们只能对工作目录执行ls命令。为了传递参数,先要分析输入行,然后用某种约 
定把参数 
分开(很可能使用空格或制表符),然后将分隔后的各个参数传递给execlp函数。尽 
管如此, 
此程序仍可用来说明Unix的进程控制功能。 
如果运行此程序,则得下列结果。注意,该程序使用了一个不同的提示符(%)。 
 
$ aout 
% date 
Fri Jun 7 15:50:36 MST 1991 
% who 
stevens console Jun 5 06:01 
stevens ttyp0 Jun 5 06:02 
% pwd 
/home/stevens/doc/apue/proc 
% ls 
Makefile 
aout 
shelllc 
% ^D〓〓〓〓〓〓〓〖WB〗键入我们的文件结束符 
$〖DW〗输出常规的shell提示 
16〓ANSI C 
本书中的所有实例都按ANSI C编写 
函数原型 
头文件<unistdh>包含了许多Unix系统服务的函数原型,例如我们已调用过的re 
ad,write 
和getpid函数。函数原型是ANSI C标准的组成部分。这些函数原型如下列形式: 
 
ssize[CD#*2]t read(int,void *,size[CD#*2]t); 
ssize[CD#*2]t write(int,void *,size[CD#*2]t); 
pid[CD#*2]t getpid(void); 
最后一个的意思是:getpid没有参数(void),返回值的数据类型pid[CD#*2]t。提 
供了这些 
函数原型后,编辑程序在编译时就可以检查我们在调用函数时是否使用了正确的参 
数。在程 
序14中,如果我们带一个参数调用getpid(如同在getpid(1)中一样),则我们将 
从ANSI C 
编辑程序得到下列形式的出错信息: 
line 8:too many arguments to function "getpid" 
另外,因为编辑程序知道参数的数据类型,所以如果可能,它就会将参数强制转换 
成所需的 
数据类型。 
类属指针 
从上面所示的函数原型中我们可以注意到的另一个区别是:read和write的第二个 
参数现在 
是void *类型。所有较早的Unix系统都使用char *这种指针类型。作这种更改的原 
因是:AN 
SI C使用void *作为类属指针,以代替char *。 
函数原型和类属指针相组合使我们消去了很多非ANSI C编辑程序需要的显式类型强 
制转换。 
例如,给出了write原型后,我们可以写成: 
float data〔100〕; 
write (fd,data,sizeof(data)); 
若使用非ANSI编程程序,或没有给出函数原型,则我们需写成: 
write(fd,(void *)data,sizgof(data)); 
我们也将void *指针特征用于malloc函数(见78节)。malloc的原型现在是: 
void * malloc(size[CD#*2]t); 
这使得我们可以写下面的程序段: 
int * ptr; 
ptr=malloc (1000 * sizeof(int)); 
它无需将返回的指针强制转换成int *类型。 
原始系统数据类型 
前面所示的getpid函数的原型定义了其返回值为pid[CD#*2]t类型。这也是POSIX中 
的新规定 
。Unix的早期版本规定此函数返回一整型。与此类似,read和write返回类型为SS 
IZE[CD#*2 
]t的值,以及要求第3个参数的类型是SIZE[CD#*2]t。 
以-t结尾的这些数据类型被称为原始系统数据类型。它们通常在头文件<sys/type 
sh>中定 
义(头文件<unistdh>应已包括该头文件)。它们通常以C typedef说明加以定义, 
typedef 
说 
明在C语言中已超过15年了(所以这并不要求ANSI C),它们的目的是阻止程序(在用 
专门的数 
据类型(例如int,short或long)以允许对于一种特定系统的每个实现,选择所要求 
的数据类 
型。在需要存储进程ID处,我们将分配类型为pid[CD#*2]t的一个变量。(注意,我 
们在程序 
15中,已对名为pid的变量这样做了。)在各种不同的实现中,这种数据类型的定 
义可能是 
不同的,但是这种差别现在只出现在一个头文件中。我们所需做的只是在另一个系 
统上重新 
编辑应用程序。 
17〓出错处理 
当Unix函数出错时,往常返回一个负值,而且整型变量errno通常设置为具有特定 
信息的一 
个值。例如,open函数如成功执行则返回一个非负文件描述符。如若出错则返回- 
1。在open 
出错时,有大约15种不同的errno值(文件下存在,许可权问题等)。某些函数使用 
不是返回 
负值的另一种约定。例如,返回一个指向一个对象的指针的大多数函数,在出错时 
,返回一 
个null指针。 
文件<errorh>中定义了变量errno,以及可以赋与它的各种常数。这些常数都以 
E开头,另 
外,Unix手册第二部分的第一页是intro(2),它通常列出了所有这些出错常数。例 
如,若er 
rno等于常数EACCES,这表示产生了许可权问题(例如,我们没有打开所要求文件的 
许可权) 
。POSIX定义errno为: 
extern int errno; 
POSIX1中errno的定义较C标准中的定义更为苛刻。C标准允许errno可以是一个宏 
,它扩认 
成可修改的整型左值(lvalue)(例如一个函数,它返回一个指向出错数的指针)。 
 
对于errno应当知道两个规则。第一个规则是:如果没有出错,则其值不会被一个 
例程消除 
。因此,仅当函数的返回值指明出错时,才检验其值。第二个规则是:任一函数都 
不会将er 
rno值设置为0,在<errnoh>中定义的所有常数都不具值0。 
C标准定义了两个函数,它们帮助打印出错信息。 
#include <stringh> 
char *strerror(int [WTBX]errnum[WTBZ]); 
返回:指向消息字符串的指针 
此函数将errnum(它通常就是errno值)映射为一个出错信息字符串,并且返回此字 
符串的指 
针。 
perror函数在标准出错上产生一条出错消息(基于errno的当前值),然后返回。 
 
#include <stdioh> 
void perror(const char *msg); 
它首先输出由msg指向的字符串,然后是一个冒号,一个空格,然后是对应于errn 
o值的出错 
信息,然后是一个新行符。 
实例 
程序16显示了这两个出错函数的使用方法。 
程序16〓例示strerror和perror 
如果此程序经编辑,结果送入文件aout,则有: 
$ aout 
EACCES:Permission denied 
aout:NO such file or directory 
注意,我们将程序名作为参数(argv〔0〕,其值是aout)传递给perror。这是一 
个标准的 
Unix惯例。使用这种方法,如若程序是作为管道线的一部分执行的,如: 
prog 1 <inputfile |prog 2|prog 3>outputfile 
我们就能分清三个程序中的那一个产生了一条特定的出错消息。 
在本书中的所有实例基本上都不直接调用strerror或perror,而是使用在附录B中 
的出错函 
数。在该附录中的出错函数使用了ANSI C的可变参数表设施,用一条C语句就可处 
理出错条 
件。 
18〓用户标识 
用户ID(User ID) 
口令文件中用户记录项中的用户ID是个数字值,它向系统标识各不同的用户。系统 
管理员在 
确定一个用户的登录名的同时,确定其用户ID。用户不能更改其用户ID。通常每个 
用户有一 
个唯一的用户ID。我们将会了解到系统核如何使用用户ID以检验该用户是否有执行 
某些操作 
的适当许可权。 
我们称用户ID为0的用户为根(root)或超级用户(superuser)。在口令文件中,通常 
有一个记 
录项,其登录名为root,我们称这种用户的特权为超级用户特权。我们将在第四章 
中看到, 
如果一个进程具有超级用户特权,则大多数文件许可权检查都不再进行。某些操作 
系统功能 
只限于向超级用户提供,超级用户对系统有自由的支配权。 
实例 
程序17打印用户ID和组ID(在下面说明)。 
程序17〓打印用户ID和组ID 
调用getuid和getgid以返回用户ID和组ID。运行该程序,产生: 
* $ aout 
uid=224,gid=20 
组ID(Group ID) 
口令文件中用户记录项也包括用户的组ID,它也是一个数字值。组ID也是由系统管 
理员在确 
定用户登录名时分配的。典型地,在口令文件中有多个记录项具有相同的组ID。在 
Unix下, 
组被用于将若干用户集合到课题或部门中去。这种机制允许同组的各个成员之间共 
享资源( 
例如文件)。在45节我们将说明可以设置一个文件的许可权使一个组的所有成员 
都能存取 
该文件,而组外用户则不能。 
也有一个组文件,它将组名映照为数字组I组文件通常是/etc/group。 
对于许可权使用数值用户ID和数值组ID是历史上形成的。系统中每个文件的目录项 
包含该文 
件属主的用户ID和组ID。在目录项中存放这两个值只需4个字节(假定每个都以双字 
节的整型 
值存放)。如果使用八字节的登录名和八字节的组名,则需使用较多的盘空间。但 
是对于用 
户而言,使用名字比使用数值方便,所以口令字文件包含了登录名和用户ID之间的 
映照关系 
,而组文件则包含了组名和组ID之间的映照关系。例如Unix ls-l命令使用口令字 
文件将数 
值用户ID映照为登录名,从而打印文件属主的登录名。 
添加组ID(Supplementary Group IDs) 
除了在口令字文件中对一个登录名指定一个组ID外,某些Unix版本还允许一个用户 
属于另外 
一些组。这是从42 BSD开始的,它允许一个用户属于多至16个另外的组。在登录 
时,读文 
件/etc/group,寻找列有该用户作为其成员的前16个登记项就可得到该用户的添加 
组ID。 
19〓信号(signals) 
信息是通知进程已发生某种条件的一种技术。例如,若一进程执行一除法操作,其 
除数为0 
,则将名为SIGFPE的信号发送给该进程。进程如何处理信号有三种选择: 
1忽略该信号。有些信号表示硬件异常,例如,除以0,或访问进程地址空间以外 
的单元等 
,因为这些异常产生的后果是不确定的,所以不推荐使用这种处理方式。 
2按系统默认方式处理。对于0除,系统默认方式是终止该进程。 
3提供一个函数,信号发生时则调用该函数。使用这种方式,我们将能知道什么 
时候产生 
了信号,并按所希望的方式处理它。 
很多条件会产生信号。有两个终端键,分别称为中断键(通常是DELETE键或控制-C 
)和退出键 
(通常是控制-反斜线),它们被用于中断当前运行进程。另一种产生信号的方法是 
调用名为k 
ill的函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当然这样 
做也有些 
限制:为了向一个进程发送信号,我们必需是该进程的属主。 
实例 
回忆一下基本shell程序(程序15)。如果我们调用此程序,然后键入中断键,则 
执行此程 
序的进程终止。产生这种后果的原因是:对于此信号(SIGINT)的系统默认动作是终 
止此进程 
。该进程没有告诉系统核对此信号作何处理,所以系统按默认方式终止该进程。 
 
为了更改此程序使其能捕捉到该信号,它需要调用signal函数,指定当产生SIGIN 
T信号时要 
调用的函数名。我们为此编写了名为sig[CD#*2]int的函数,当其被调用时,它只 
是打印一 
条消息,然后打印一个新提示符。在程序15中加了12行构成了程序18(添加的 
12行以行 
首的+号指示)。 
程序18〓从标准输入读命令并执行它们 
因为大多数重要的应用程序都将使用信号,所以在第十章,我们将详细说明信号机 
构。 
110〓Unix时间值 
长期以来,Unix系统一直使用两种不同的时间值。 
1日历时间。是自197011,00:00:00以来,国际标准时(UTC)所经过的秒数 
累计值( 
较早的手册称UTC为格林威冶平时)。这些时间值可用于记录文件最近一次的修改时 
间等。 
2进程时间。这也被称为CPU时间,用以度量进程使用的中央处理机资源。进程时 
间以时钟 
滴答计算,多年来,每秒钟取为50,60或100个滴答。系统基本数据类型clock[CD 
#*2]t保持 
这种时间值。另外,POSIX定义常数CLK[CD#*2]TCK,用其说明每秒滴答数。(常数 
CLK〖CD#* 
2〗TCK现在已不再使用。我们将在254节说明如何用sysconf函数得到每秒时钟 
滴答数。 
) 
当度量一个进程的执行时间时(见39节),Unix系统使用三个进程时间值。 
·时钟时间 
·用户CPU时间 
·系统CPU时间 
时间又称为墙上时钟时间。它是进程运行的时间总量,其值在与系统中同时运行的 
其它进程 
数有关。无论何时,在我们报告时钟时间时,都是在系统中没有其它活动时进行度 
量的。 
用户CPU时间是执行用户指令所用的时间量。系统CPU时间是为该进程执行系统核所 
经历的时 
间。例如,只要一个进程执行一个系统服务,例如read或write,则在系统核内执 
行该服务 
所花费的时间就计入该进程的系统CPU时间。用户CPU时间和系统CPU时间的和常被 
称为CPU时 
间。 
要取得任一进程的时钟时间、用户时间和系统时间是容易的〖CD2〗只要执行命令 
time(1), 
其参数是我们要度量其执行时间的命令,例如: 
$ cd/usr/include 
$ time grep[CD#*2]POSIX[CD#*2]SOURCE */*h>/dev/null 
real 0m19:81s 
user 0m043s  
sys 0m453s 
time命令的输出格式与所使用的shell有关。 
在815节中,我们将说明一个运行进程如何取得这三个时间。关于时间,日期的 
一般说明 
见69节。 
111〓系统调用和库函数 
所有操作系统都提供多种服务的入口点,由此程序向系统核请求服务。各种版本的 
Unix都提 
供经良好定义的有限数目的入口点,经过这些入口点进入系统核,这些入口点被称 
之为系统 
调用(system call),系统调用是我们不能更改的一种Unix特征。Unix版本7提供了 
约50个系 
统调用,43+BSD提供了约110个,而SVR4则提供了约120个。 
系统调用界面总是在Unix程序员手册的第二部分中说明。其定义也包括在C语言中 
。这与很 
多较早期的操作系统是不同的,这些系统按传统都在机器的汇编语言中定义系统核 
入口点。 
 
Unix所使用的技术是为每条系统调用在标准C库中设置一个具有同样名字的函数。 
用户进程 
用标准C调用序列来调用这些函数,然后,函数用系统所要求的技术调用相应的系 
统核服务 
。例如函数可将一个或几个C参数送入通用寄存器,然后执行某个产生软中断进入 
系统核的 
机器指令。从应用角度考虑,我们可将系统调用视作为C函数。 
Unix程序员手册的第三部分定义了程序员可以使用的通用函数。虽然这些函数可能 
会调用一 
个或几个系统核的系统调用,但是它们并不是系统核的入口点。例如,printf函数 
会调用wr 
ite系统调用以进行输出操作,但函数strcpy(复制一字符串)和atoi(变换ASCII为 
整数)并不 
使用任何系统调用。 
从实施者的角度,系统调用和库函数之间有重大区别,但从用户角度其区别并不非 
常重要。 
从本书的目的出发,系统调用和库函数在本书中都以正常的C函数的形式出现。两 
者都对应 
用程序提供服务,但是,我们应当理解,如果希望的话,我们可以代换库函数,但 
是通常我 
们却不能代换系统服务。 
以存储器分配函数malloc为例。有多种方法可以进行存储器分配及与其相关的无用 
区收集操 
作(最佳适应,首次适应等),并不存在对所有程序都最佳的一种技术。Unix系统调 
用中处理 
存储器分配的是sbrk(2),它不是一个通用的存储器管理器。它增加或减少指定字 
节数的进 
程地址空间。如何管理该地址空间却取决于进程。存储器分配函数malloc(3)实现 
一种特定 
类型的分配。如果我们不喜欢其操作方式,则我们可以定义自己的malloc函数,极 
其可能, 
它还是要调用sbrk系统调用。事实上,有很多软件包,它们实现自己的存储器分配 
算法,但 
仍使用sbrk系统调用。图11显示了应用程序、malloc函数以及sbrk系统调用之间 
的关系。 
 
图11〓malloc函数和sbrk系统调用 
从中可见,两者职责不同,相互分开,要核中的系统调用分配另外一块空间给进程 
,而库函 
数malloc则管理这种空间。 
另一个可说明系统调用和库函数之间的差别的例子是,Unix提供决定当前时间和日 
期的界面 
。某些操作系统提供一个系统调用以返回时间,而另一个则返回日期。任何特殊的 
处理,例 
如正常时制和日光节约时制之间的转换,由系统核处理或要求人的干予。Unix则不 
同,它只 
提供一条系统调用,该系统调用返回国际标准时公元一九七年一月一日午夜以来 
所经过的 
秒数。对该值的任何解释,例如将其变换成人们可读的,使用本地时区的时间和日 
期,都留 
给用户进程运行。在标准C库中,提供了若干例程以处理大多数情况。这些库函数 
处理各种 
细节,例如各种日光节约时算法。 
应用程序可以或者调用系统调用,或者库函数,而很多库函数则会调用系统调用。 
这在图1 
2中显示。 
图12〓C库函数和系统调用之间的差别 
另一个系统调用和库函数之间的差别是:系统调用通常提供一种最小界面,而库函 
数通常提 
供比较复杂的功能。我们从sbrk系统调用和malloc库函数之间的差别中看到了这一 
点,在以 
后当比较不带缓存的I/O库数(第3章)以及标准I/O标准(在第5章)时,我们还将看到 
这种差别 
。 
进程控制系统调用(fork,exec和wait)通常由用户的应用程序直接调用。(请回忆程 
序15中 
的基本shell)但是为了简化某些常见的情况,UNIX系统也提供了一些库函数;例如 
system和 
popen。在812节中,我们将说明system函数的一种实现,它使用基本的进程控制 
系统调用 
。在1018中,我们还将强化这一实例以正确地处理信号。 
为使读者了解大多数程序员应用的Unix系统界面,我们不得不既说明系统调用,只 
介绍某些 
库函数。例如若我们只说明sbrk系统调用,那么就会忽略很多应用程序使用的mal 
loc库函数 
。 
在本书中,除了一定要将两者相区分时,我们都将使用术语"函数"来涉及系统调用 
和库函 
数两者。 
112〓摘要 
本章旋风式地周游了Unix。我们已说明了某些以后会多次用到的基本术语,介绍了 
一些小的Unix程序的实例,从中可想到本书的其余部分将会进一步介绍的内容。 
下一章是关于Unix的标准化,以及这方面的工作对当前系统的影响。标准,特别是 
ANSI C标准和POSIX1标准将影响本书的余下部分。 
习题 
11〓在你的系统上查证,除根目录中外,目录·和··是不同的。 
12〓分析程序14的输出,说明进程ID为852和853的进程可能是什么? 
13〓在17节中,perror的参数是用ANSI C的属性const定义的,而rerror的整 
型参数则没有用此属性定义,为什么? 
14〓附录B包含了出错处理函数err[CD#*2]sys,当调用该函数时,保存了errno 
的值,为什么? 
15〓若日历时间存放在带符号的32位(32-bit)整型数中,那么到哪一年它将溢 
出? 
16〓若进程时间存放在带符号的32位(32-bit)整型数中,而且每秒为100滴答, 
那么经过多少天后该时间值将会溢出?〖LM〗 
 
 
-- 

BBS水木清华站∶精华区