UNIX环境高级编程(引言)
尤晋元 译
第一章 引言
11 引言
所有操作系统都向它们运行的程序提供服务。典型的服务是执行一道新程序、打开一个文件、读一个文件、分配一个存储区、获得当前时间等等,本书的焦点是说明各种Unix操作系统版本所提供的服务。
以严格的步进方式,不超前引用尚未说明过的术语来说明Unix几乎是不可能的(可能也会是令人厌烦的)。本章从程序设计人员的角度快速周游Unix,我们将对书中引用的一些术语和概念进行简要的说明并给示实例。在以后各章中,我们将对这些作更详细的说明。本章也对不熟悉Unix的程序设计人员介绍、概述Unix提供的各种服务。
12 登录(Logging ln)
登录名
当我们向Unix系统登录时,先键入登录名,然后键入口令字。系统在其口令文件,通常是/etc/passwd文件中查看我们的登录名。在口令文件中的登录项,由7个以冒号分隔的字段组成:登录名,加密口令字,数字用户ID(224),数字组ID(20),注释字段,起始目录(/home/stevens),以及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系统V386 R32和系统VR4(SVR4)提供,(在下一章,我们将对这些不同的Unix版本作更多说明。KornShell是Bourne shell的后继者,它由SVR4提供。Kornshell在大多数Unix系统上运行,但在SVR4之前,通常它需要另行购买,所以没有其它两种shell流行。
Bourne shell是由Steve Bourne在Bell实验室中开发的其控制流结构使人想起Algol68C Shell是在贝克莱由Bill Joy完成的,其基础是第6版shell(不是Bourne shell)。其控制结构很象C语言,它支持了一些Bourne shell没有提供的功能—作业控制,历史机制和命令行编辑。Kornshell是由David Korn在Bell实验室中开发的,它兼容Bourne shell,并且也包含
了使C shell非常流行的一些功能—作业控制、命令行编辑等。
在全书中,我们都会使用这种形式的注释以说明历史沿革,并对不同的Unix实现进行比较。
当说明了历史缘由后,常常使得采用一种特定实现技术的理由变得清晰起来。
在全书中,我们将使用很多shell实例,以执行我们已开发的程序,其中将应用Bourne shell和Kornshell都具有的功能。
13 文件和目录
文件系统(Filesystem)
Unix文件系统是目录和文件的一种分层次的安排,目录的起点称为根(root),其名字是一个字符/。
一个目录是一个包含目录项的文件,在逻辑上,我们可以认为每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。文件属性是:文件类型,文件长度,文件属主,文件的许可权(例如,其他用户能否存取该文件?)文件的最后修改时间等。stat和fstat函数返回一个包含所有文件属性的信息结构。在第四章中,我们将详细说明文件的各种属性。
文件名(Filename)
一个目录中的各个名字称为文件名。不能出现在文件名中的字符只有两个,它们是斜线(/)和空操作(null)字符,斜线分隔构成路径名(在下面说明)的各文件名,空操作符则终止一个路径名,尽管如此,一个好的习惯是只使用印刷字符的一个子集作为文件名字符(只使用子集的理由是:如果在文件名中使用了某些shell特殊字符,则必须使用shell的引号机制来引用文件名)。
当创建一个新目录时,自动创建了两个文件名:. (称为点)和 ..(称为点—点)。点引用当前目录,点—点则引用文目录。在最高层次的根目录中,点—点与点相同。
某些Unix文件系统限制文件名的最大长度为14个字符,BSD版本则将这种限制扩展为255个字符。
—路径名(Palhname)
0个或多个以斜线分隔的文件名序列(可以任选地以斜线开头)构成路径名,以斜线开头的路径名称为绝对路径名,否则称为相对路径名。
实例
不难列出一个目录中所有文件的名字,程序11是ls(1)命令的主要实现部分
程序11 列出一个目录中的所有文件
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
程序11只打印一个目录中各个文件的名字,不显示其它信息,如若该源文件名为myls.c,
则我们可以用下面的命令对其进行编辑,编辑的结果送入系统默认名为aout的可执行文件名:
cc mylsc
某种样本输出是:
$ aout /dev
MAKEDEV
console
tty
mem
kmem
null
printer
$ aout /var/spool/mqueue
can′t open /var/spool/mqueue:Permission denied
$ aout /dev/tty
can′t open /dev/tty:Not a directory
在全书中,我们都将以这种方式表示我们输入的命令以及其输出:我们输入的字符以这种字体表示程序输出则以另一种字体表示。如果我们欲对输出添加注释,则以表示注释,在我们输入之前的美元符号($)是shell打印的提示符,我们总是将shell提示符显示为$。
注意,列出的目录项不是以字母序排列的,ls命令本身则一般以字母序列出目录项。在这20行程序中,有很多细节可以考虑:
·首先,其中包含了一个我们自己的头文件ourhdrh。在本书中,几乎每一道程序都包含此头文件。它包含了某些标准系统头文件,定义了许多常数及函数原型,这些都将用于本书的各个例子中,此头文件包含在附录B中。
·main函数的说明使用了ANSI C标准所支持的新风格。(在下一章中,我们将对ANSI C作更多说明。)
·我们取命令行的第1个参数argv〔1〕作为要列表的目录名。在第七章中,我们将说明main函数是如何被调用的,程序如何存取命令行参数和环境变量。
·因为各种不同Unix系统的目录项的实际格式是不一样的,所以我们使用函数opendir,readdir和closedir处理目录。
·opendir函数返回指向DIR结构的指针,并将该指针传向readdir函数。我们并不关心DIR结构中包含了什么。然后,我们在循环中调用readdir,以读每个目录项。它返回一个指向dirent结构的指针,而当目录中已无目录项可读时则返回null指针。我们在dirent结构中取出的只是每个目录项的名字(d[CD#*2]name)。使用该名字,我们此后就可调用stat函数(42节)以决定该文件的所有属性。
·调用了两个我们自编的函数对错误进行处理:err-sys和err-quit。我们从上面的输出中可以看到,err-sys函数打印一条消息,说明遇到了什么类型的错误。(“Permission denied”或“Not a directory”(“许可权拒绝”或“不是一个目录”。))
这两个出错处理函数也在附录B中说明,我们也将在17节中更多地叙述出错处理。
·当程序将结束时,它以参数O调用函数exit。函数exit终止一道程序。按惯例,参数O的意思是正常结束,参数值1~255则表示出了一种错。在85节中,我们将说明一道程序(例如一个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)
当我们登录时,工作目录设置为起始目录,该起始目录从口令字文件(见12节)中我们的记录项中取得。
14输入和输出
文件描述符(File Descriptors)
文字描述符是一个小的非负整数,系统核用以标识一个特定进程正在存访的文件。无论何时,系统核打开一个现存文件或创建一个文件,它就返回一个文件描述符。当读、写文件时我们就使用它。
标准输入、标准输出和标准出错按惯例,每当运行一道新程序,所有的shell,都与其打开三个文件描述符:标准输入、标准输出以及标准出错。如若象简单命令ls那样,没有做什么特殊处理,则所有这三个都连向我们的终端。大多数shell都提供一种方法,使任何一个或所有这三个描述符都能重新定向到某一个文件,例如:
ls>filelist
执行ls命令,其标准输出重新定向到名为filelist的文件点。
不用缓存的I/O函数open、read、write、lseek以及close提供了不同缓存的I/O。这些函数都用文件描述符进行工作。
实例
如若我们愿望从标准输入读,并写向标准输出,则程序12可以复制任一Unix文件。
程序12〓将标准输入复制到标准输出
头文件(ourhdrh中包含了此头文件)及两个常数STDIN-FILENO和STDOUT-FILENO是POSIX标准的一部分(在下一章,我们将对此作更多的说明)。很多Unix系统服务的函数原型,例如我们调用的read和write都在此头文件中。函数原型也是ANSI C标准的一部分,我们将在本章的稍后部分对此作更多说明。
两个常数STDIN-FILENO和STDOUT-FILENO定义在头文件中,它们指定了标准输入和标准输出的文件描述符。它们的典型值是0和1,但是为了可移植性,我们将使用这些新名字。
在39节中,我们将详细地讨论BUFSIZE常数,说明各种不同值将如何影响程序的效率。但是不管该常数的值如何,此程序总能复制任一Unix文件。
read函数返回读得的字节数,此值用作为要写的字节数。当到了文件的尾端时,read返回0,程序停止执行。如果发生了一个读错误,read返回-1,出错时,大多数系统函数返回-1。
如若编辑读程序,其结果送入标准的aout文件,并以下列方式执行它:
aout>data
那么,标准输入是终端,标准输出则重新定向至文件data,标准出错也是终端。如果此输出文件并不存在,则shell创建它。
在第三章中,我们将更详细地说明不用缓存的I/O函数。
标准I/O
标准I/O函数提供一种对不用缓存的I/O函数的带缓存的界面。使用标准I/O使我们无需担心如何选取最佳的缓存长度,例如程序12中的BUFSIZE常数。另一个使用标准I/O函数的优点与处理输入行有关(常常发生在Unix的应用中)。例如,fgets函数读一完整的行,而另一方面,read函数读指定字节数。
我们最熟悉的标准I/O函数是printf。在调用printf的程序中,我们总是包括(通常包括在ourhdr.h中),因为此头文件包括了所有标准I/O函数的原型。
实例
程序13的功能类似于调用read和write的前一道程序12,我们将在58中对程序13作更详细的说明。它将标准输入复制到标准输出,于是也就能复制任一Unix文件。
程序13〓用标准I/O将标准输入复制到标准输出
函数getc一次读1个字符,然后putc将此字符写到标准输出。读到输入的最后1个字节时,getc返回常数EOF。标准输入、输出常数stdin和stdout定义在头文件中,它们分别表示标准输入和标准输出文件。
15〓程序和进程
程序(Program)
一道程序是存放在一个磁盘文件中的可执行文件。使用6个exec函数中的一个由核将程序读入存储器,并使其执行。我们将在89节中说明这些exec函数。
进程和进程ID(Processes and Process ID)
一道程序的一个执行实例被称为一个进程。在本书的几乎每一页中都会使用这一术语。某些操作系统用任务表示正被执行的程序。
每个Unix进程都一定有一个唯一的数字标识符,被称之为进程ID。进程ID总是一非负整数。
实例
程序14〓打印其进程ID
程序14〓打印进程ID
如若编辑该程序,其结果送入aout文件,然后执行它,则有:
$ aout
hello world from process ID 851
$ aout
hello world from precess ID 854
此程序运行时,它调用函数getpid得到其进程ID。
进程控制有三个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有六种变体,但我们经常把它们统称为exec函数。)
实例
程序15〓从标准输入读命令并执行它们
Unix的进程控制功能可以用一个较简单的程序(程序15)说明,该程序从标准输入读命令,然后执行这些命令。这是一个类似于shell程序的基本实施部分。在这30行程序中,有很多功能可以思考:
用标准I/O函数fgets从标准输入一次读一行,当作为行的第1个字符键入文件结束字符(通常是控制-D)时,fgets返回一个null指针,于是循环终止,进程也就终止。在第十一章中,我们将说明所有特殊的终端字符(文件结束、退格字符、擦除整行等等),以及如何改变它们。
·因为fgets返回的每一行都以新行符终止,后附一个null字节,我们用标准C函数strlen计算此字符串的长度,然后用一个null字节代换新行符。这一操作的目的是因为execlp函数要求的是以null结束的参数,而不是以新行符结束的参数。
·调用fork创建一个新进程,新进程是调用进程的复制品,我们称调用进程为父进程,新创建的进程为子进程。fork对父进程,返回新子进程的非负进程ID,对子进程则返回0。因为fork创建一新进程,所以我们说它被调用一次(由父进程),但返回两次(在父进程中和在子进程中)。
·在子进程中,我们调用execlp以执行从标准输入读入的命令。这使子进程更换了新的程序文件。fork和跟附其后的exec的组合是某些操作系统所称的产生一个新进程。在Unix中,这两个部分分成两个函数。在第八章中,我们将对这些函数作更多说明。
·子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这一要求由调用waitpid实现,其参数指定要等待的进程(在这里,pid参数是子进程ID)。waitpid函数也返回子进程的终止状态(status变量)。在此简单程序中,我们没有使用该值。如若需要,可以用此值精确地确定子进程是如何终止的。
·该程序的最主要限制是可能向执行的命令传递参数。例如我们不能指定要列表的目录名。
我们只能对工作目录执行ls命令。为了传递参数,先要分析输入行,然后用某种约定把参数分开(很可能使用空格或制表符),然后将分隔后的各个参数传递给execlp函数。尽管如此,此程序仍可用来说明Unix的进程控制功能。
如果运行此程序,则得下列结果。注意,该程序使用了一个不同的提示符(%)。
$ aout
% 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
aout
shelllc
% ^D〓〓〓〓〓〓〓〖WB〗键入我们的文件结束符
$〖DW〗输出常规的shell提示
16〓ANSI C
本书中的所有实例都按ANSI C编写
函数原型
头文件包含了许多Unix系统服务的函数原型,例如我们已调用过的read,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。提供了这些
函数原型后,编辑程序在编译时就可以检查我们在调用函数时是否使用了正确的参数。在程
序14中,如果我们带一个参数调用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函数(见78节)。malloc的原型现在是:
void * malloc(size[CD#*2]t);
这使得我们可以写下面的程序段:
int * ptr;
ptr=malloc (1000 * sizeof(int));
它无需将返回的指针强制转换成int *类型。
原始系统数据类型
前面所示的getpid函数的原型定义了其返回值为pid[CD#*2]t类型。这也是POSIX中的新规定。Unix的早期版本规定此函数返回一整型。与此类似,read和write返回类型为SSIZE[CD#*2]t的值,以及要求第3个参数的类型是SIZE[CD#*2]t。
以-t结尾的这些数据类型被称为原始系统数据类型。它们通常在头文件中定义(头文件应已包括该头文件)。它们通常以C typedef说明加以定义,typedef说明在C语言中已超过15年了(所以这并不要求ANSI C),它们的目的是阻止程序(在用专门的数据类型(例如int,short或long)以允许对于一种特定系统的每个实现,选择所要求的数据类型。在需要存储进程ID处,我们将分配类型为pid[CD#*2]t的一个变量。(注意,我们在程序15中,已对名为pid的变量这样做了。)在各种不同的实现中,这种数据类型的定义可能是不同的,但是这种差别现在只出现在一个头文件中。我们所需做的只是在另一个系统上重新编辑应用程序。
17〓出错处理
当Unix函数出错时,往常返回一个负值,而且整型变量errno通常设置为具有特定信息的一个值。例如,open函数如成功执行则返回一个非负文件描述符。如若出错则返回-1。在open出错时,有大约15种不同的errno值(文件下存在,许可权问题等)。某些函数使用不是返回负值的另一种约定。例如,返回一个指向一个对象的指针的大多数函数,在出错时,返回一个null指针。
文件中定义了变量errno,以及可以赋与它的各种常数。这些常数都以E开头,另外,Unix手册第二部分的第一页是intro(2),它通常列出了所有这些出错常数。例如,若errno等于常数EACCES,这表示产生了许可权问题(例如,我们没有打开所要求文件的许可权)
。POSIX定义errno为:
extern int errno;
POSIX1中errno的定义较C标准中的定义更为苛刻。C标准允许errno可以是一个宏,它扩认成可修改的整型左值(lvalue)(例如一个函数,它返回一个指向出错数的指针)。
对于errno应当知道两个规则。第一个规则是:如果没有出错,则其值不会被一个例程消除。因此,仅当函数的返回值指明出错时,才检验其值。第二个规则是:任一函数都不会将errno值设置为0,在中定义的所有常数都不具值0。
C标准定义了两个函数,它们帮助打印出错信息。
#include
char *strerror(int [WTBX]errnum[WTBZ]);
返回:指向消息字符串的指针
此函数将errnum(它通常就是errno值)映射为一个出错信息字符串,并且返回此字符串的指针。
perror函数在标准出错上产生一条出错消息(基于errno的当前值),然后返回。
#include
void perror(const char *msg);
它首先输出由msg指向的字符串,然后是一个冒号,一个空格,然后是对应于errno值的出错信息,然后是一个新行符。
实例
程序16显示了这两个出错函数的使用方法。
程序16〓例示strerror和perror
如果此程序经编辑,结果送入文件aout,则有:
$ aout
EACCES:Permission denied
aout:NO such file or directory
注意,我们将程序名作为参数(argv〔0〕,其值是aout)传递给perror。这是一个标准的Unix惯例。使用这种方法,如若程序是作为管道线的一部分执行的,如:
prog 1 outputfile
我们就能分清三个程序中的那一个产生了一条特定的出错消息。
在本书中的所有实例基本上都不直接调用strerror或perror,而是使用在附录B中的出错函数。在该附录中的出错函数使用了ANSI C的可变参数表设施,用一条C语句就可处理出错条件。
18〓用户标识
用户ID(User ID)
口令文件中用户记录项中的用户ID是个数字值,它向系统标识各不同的用户。系统管理员在确定一个用户的登录名的同时,确定其用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户ID。我们将会了解到系统核如何使用用户ID以检验该用户是否有执行某些操作的适当许可权。
我们称用户ID为0的用户为根(root)或超级用户(superuser)。在口令文件中,通常有一个记录项,其登录名为root,我们称这种用户的特权为超级用户特权。我们将在第四章中看到,如果一个进程具有超级用户特权,则大多数文件许可权检查都不再进行。某些操作系统功能只限于向超级用户提供,超级用户对系统有自由的支配权。
实例
程序17打印用户ID和组ID(在下面说明)。
程序17〓打印用户ID和组ID
调用getuid和getgid以返回用户ID和组ID。运行该程序,产生:
* $ aout
uid=224,gid=20
组ID(Group ID)
口令文件中用户记录项也包括用户的组ID,它也是一个数字值。组ID也是由系统管理员在确定用户登录名时分配的。典型地,在口令文件中有多个记录项具有相同的组ID。在Unix下,组被用于将若干用户集合到课题或部门中去。这种机制允许同组的各个成员之间共享资源(例如文件)。在45节我们将说明可以设置一个文件的许可权使一个组的所有成员都能存取该文件,而组外用户则不能。
也有一个组文件,它将组名映照为数字组I组文件通常是/etc/group。
对于许可权使用数值用户ID和数值组ID是历史上形成的。系统中每个文件的目录项包含该文件属主的用户ID和组ID。在目录项中存放这两个值只需4个字节(假定每个都以双字节的整型值存放)。如果使用八字节的登录名和八字节的组名,则需使用较多的盘空间。但是对于用户而言,使用名字比使用数值方便,所以口令字文件包含了登录名和用户ID之间的映照关系,而组文件则包含了组名和组ID之间的映照关系。例如Unix ls-l命令使用口令字文件将数值用户ID映照为登录名,从而打印文件属主的登录名。
添加组ID(Supplementary Group IDs)除了在口令字文件中对一个登录名指定一个组ID外,某些Unix版本还允许一个用户属于另外一些组。这是从42 BSD开始的,它允许一个用户属于多至16个另外的组。在登录时,读文件/etc/group,寻找列有该用户作为其成员的前16个登记项就可得到该用户的添加组ID。
19〓信号(signals)
信息是通知进程已发生某种条件的一种技术。例如,若一进程执行一除法操作,其除数为0,则将名为SIGFPE的信号发送给该进程。进程如何处理信号有三种选择:
1忽略该信号。有些信号表示硬件异常,例如,除以0,或访问进程地址空间以外的单元等,因为这些异常产生的后果是不确定的,所以不推荐使用这种处理方式。
2按系统默认方式处理。对于0除,系统默认方式是终止该进程。
3提供一个函数,信号发生时则调用该函数。使用这种方式,我们将能知道什么时候产生了信号,并按所希望的方式处理它。
很多条件会产生信号。有两个终端键,分别称为中断键(通常是DELETE键或控制-C)和退出键(通常是控制-反斜线),它们被用于中断当前运行进程。另一种产生信号的方法是调用名为kill的函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当然这样做也有些限制:为了向一个进程发送信号,我们必需是该进程的属主。
实例
回忆一下基本shell程序(程序15)。如果我们调用此程序,然后键入中断键,则执行此程序的进程终止。产生这种后果的原因是:对于此信号(SIGINT)的系统默认动作是终止此进程。该进程没有告诉系统核对此信号作何处理,所以系统按默认方式终止该进程。
为了更改此程序使其能捕捉到该信号,它需要调用signal函数,指定当产生SIGINT信号时要调用的函数名。我们为此编写了名为sig[CD#*2]int的函数,当其被调用时,它只是打印一条消息,然后打印一个新提示符。在程序15中加了12行构成了程序18(添加的12行以行首的+号指示)。
程序18〓从标准输入读命令并执行它们
因为大多数重要的应用程序都将使用信号,所以在第十章,我们将详细说明信号机构。
110〓Unix时间值
长期以来,Unix系统一直使用两种不同的时间值。
1日历时间。是自197011,00:00:00以来,国际标准时(UTC)所经过的秒数累计值(较早的手册称UTC为格林威冶平时)。这些时间值可用于记录文件最近一次的修改时间等。
2进程时间。这也被称为CPU时间,用以度量进程使用的中央处理机资源。进程时间以时钟滴答计算,多年来,每秒钟取为50,60或100个滴答。系统基本数据类型clock[CD#*2]t保持这种时间值。另外,POSIX定义常数CLK[CD#*2]TCK,用其说明每秒滴答数。(常数CLK〖CD#*2〗TCK现在已不再使用。我们将在254节说明如何用sysconf函数得到每秒时钟滴答数。)
当度量一个进程的执行时间时(见39节),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 0m043s
sys 0m453s
time命令的输出格式与所使用的shell有关。
在815节中,我们将说明一个运行进程如何取得这三个时间。关于时间,日期的一般说明
见69节。
111〓系统调用和库函数
所有操作系统都提供多种服务的入口点,由此程序向系统核请求服务。各种版本的Unix都提供经良好定义的有限数目的入口点,经过这些入口点进入系统核,这些入口点被称之为系统调用(system call),系统调用是我们不能更改的一种Unix特征。Unix版本7提供了约50个系统调用,43+BSD提供了约110个,而SVR4则提供了约120个。
系统调用界面总是在Unix程序员手册的第二部分中说明。其定义也包括在C语言中。这与很多较早期的操作系统是不同的,这些系统按传统都在机器的汇编语言中定义系统核入口点。
Unix所使用的技术是为每条系统调用在标准C库中设置一个具有同样名字的函数。用户进程用标准C调用序列来调用这些函数,然后,函数用系统所要求的技术调用相应的系统核服务。例如函数可将一个或几个C参数送入通用寄存器,然后执行某个产生软中断进入系统核的机器指令。从应用角度考虑,我们可将系统调用视作为C函数。
Unix程序员手册的第三部分定义了程序员可以使用的通用函数。虽然这些函数可能会调用一个或几个系统核的系统调用,但是它们并不是系统核的入口点。例如,printf函数会调用write系统调用以进行输出操作,但函数strcpy(复制一字符串)和atoi(变换ASCII为整数)并不使用任何系统调用。
从实施者的角度,系统调用和库函数之间有重大区别,但从用户角度其区别并不非常重要。
从本书的目的出发,系统调用和库函数在本书中都以正常的C函数的形式出现。两者都对应用程序提供服务,但是,我们应当理解,如果希望的话,我们可以代换库函数,但是通常我们却不能代换系统服务。
以存储器分配函数malloc为例。有多种方法可以进行存储器分配及与其相关的无用区收集操作(最佳适应,首次适应等),并不存在对所有程序都最佳的一种技术。Unix系统调用中处理存储器分配的是sbrk(2),它不是一个通用的存储器管理器。它增加或减少指定字节数的进程地址空间。如何管理该地址空间却取决于进程。存储器分配函数malloc(3)实现一种特定类型的分配。如果我们不喜欢其操作方式,则我们可以定义自己的malloc函数,极其可能它还是要调用sbrk系统调用。事实上,有很多软件包,它们实现自己的存储器分配算法,但仍使用sbrk系统调用。图11显示了应用程序、malloc函数以及sbrk系统调用之间的关系。
图11〓malloc函数和sbrk系统调用
从中可见,两者职责不同,相互分开,要核中的系统调用分配另外一块空间给进程,而库函数malloc则管理这种空间。
另一个可说明系统调用和库函数之间的差别的例子是,Unix提供决定当前时间和日期的界面。某些操作系统提供一个系统调用以返回时间,而另一个则返回日期。任何特殊的处理,例如正常时制和日光节约时制之间的转换,由系统核处理或要求人的干予。Unix则不同,它只提供一条系统调用,该系统调用返回国际标准时公元一九七年一月一日午夜以来所经过的秒数。对该值的任何解释,例如将其变换成人们可读的,使用本地时区的时间和日期,都留给用户进程运行。在标准C库中,提供了若干例程以处理大多数情况。这些库函数处理各种细节,例如各种日光节约时算法。
应用程序可以或者调用系统调用,或者库函数,而很多库函数则会调用系统调用。这在图12中显示。
图12〓C库函数和系统调用之间的差别
另一个系统调用和库函数之间的差别是:系统调用通常提供一种最小界面,而库函数通常提供比较复杂的功能。我们从sbrk系统调用和malloc库函数之间的差别中看到了这一点,在以后当比较不带缓存的I/O库数(第3章)以及标准I/O标准(在第5章)时,我们还将看到这种差别。
进程控制系统调用(fork,exec和wait)通常由用户的应用程序直接调用。(请回忆程序15中的基本shell)但是为了简化某些常见的情况,UNIX系统也提供了一些库函数;例如system和popen。在812节中,我们将说明system函数的一种实现,它使用基本的进程控制系统调用。在1018中,我们还将强化这一实例以正确地处理信号。
为使读者了解大多数程序员应用的Unix系统界面,我们不得不既说明系统调用,只介绍某些库函数。例如若我们只说明sbrk系统调用,那么就会忽略很多应用程序使用的malloc库函数。
在本书中,除了一定要将两者相区分时,我们都将使用术语“函数”来涉及系统调用和库函数两者。
112〓摘要
本章旋风式地周游了Unix。我们已说明了某些以后会多次用到的基本术语,介绍了一些小的Unix程序的实例,从中可想到本书的其余部分将会进一步介绍的内容。
下一章是关于Unix的标准化,以及这方面的工作对当前系统的影响。标准,特别是ANSI C标准和POSIX1标准将影响本书的余下部分。
习题
11〓在你的系统上查证,除根目录中外,目录·和··是不同的。
12〓分析程序14的输出,说明进程ID为852和853的进程可能是什么?
13〓在17节中,perror的参数是用ANSI C的属性const定义的,而rerror的整型参数则
没有用此属性定义,为什么?
14〓附录B包含了出错处理函数err[CD#*2]sys,当调用该函数时,保存了errno的值,为
什么?
15〓若日历时间存放在带符号的32位(32-bit)整型数中,那么到哪一年它将溢出?
16〓若进程时间存放在带符号的32位(32-bit)整型数中,而且每秒为100滴答,那么经过
多少天后该时间值将会溢出?〖LM〗
发布人:netbull 来自:LinuxAid