UNIX环境高级编程--第1章 UNIX基础知识
1.1引言
所有操作系统都向它们运行的程序提供服务。典型的服务是执行一道新程序、打开一个文
件 、读一个文件、分配一个存储区、获得当前时间等等,本书的焦点是说明各种Unix操作
系统版本所提供的服务。
以严格的步进方式,不超前引用尚未说明过的术语来说明Unix几乎是不可能的(可能也会是
令人厌烦的)。本章从程序设计人员的角度快速周游Unix,我们将对书中引用的一些术语和
概念进行简要的说明并给示实例。在以后各章中,我们将对这些作更详细的说明。
本章也对不熟悉Unix的程序设计人员介绍、概述Unix提供的各种服务。
1.2登录(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系统V 386 R3 2和系统VR4(SVR4)提供,(在下一章,我们将对这些
不同的Unix版本作更多说明。KornShell是Bourne shell的后继者,它由SVR4提供。Korn
shell在大多数Unix系统上运行,但在SVR4之前,通常它需要另行购买,所以没有其它两种
shell流行。
Bourne shell是由Steve Bourne在Bell实验室中开发的。其控制流结构使人想起Algol68。
C Shell是在贝克莱由Bill Joy完成的,其基础是第6版shell(不是Bourne shell)。其控
结构很象C语言,它支持了一些Bourne shell没有提供的功能-作业控制,历史机制和命
令行编辑。
Kornshell是由David Korn在Bell实验室中开发的,它兼容Bourne shell,并且也包含了
使C shell非常流行的一些功能-作业控制、命令行编辑等。
在全书中,我们都会使用这种形式的注释以说明历史沿革,并对不同的Unix实现进行比较
。当说明了历史缘由后,常常使得采用一种特定实现技术的理由变得清晰起来。
在全书中,我们将使用很多shell实例,以执行我们已开发的程序,其中将应用Bourne
shell和Kornshell都具有的功能。
1.3文件和目录
文件系统(Filesystem)
Unix文件系统是目录和文件的一种分层次的安排,目录的起点称为根(root),其名字是一
个字符/。
一个目录是一个包含目录项的文件,在逻辑上,我们可以认为每个目录项都包含一个文件
名,同时还包含说明该文件属性的信息。文件属性是:文件类型,文件长度,文件属主,
文件的许可权(例如,其他用户能否存取该文件?)文件的最后修改时间等。stat和fstat函
数返回一个包含所有文件属性的信息结构。在第四章中,我们将详细说明文件的各种属性
。
文件名(Filename)
一个目录中的各个名字称为文件名。不能出现在文件名中的字符只有两个,它们是斜线(/)
和空操作(null)字符,斜线分隔构成路径名(在下面说明)的各文件名,空操作符则终止一
个路径名,尽管如此,一个好的习惯是只使用印刷字符的一个子集作为文件名字符(只使
子集的理由是:如果在文件名中使用了某些shell特殊字符,则必须使用shell的引号机制
来引用文件名)。
当创建一个新目录时,自动创建了两个文件名: (称为点)和 (称为点-点)。点引用当前
目录,点-点则引用文目录。在最高层次的根目录中,点-点与点相同。
某些Unix文件系统限制文件名的最大长度为14个字符,BSD版本则将这种限制扩展为255个
字符。
路径名(Pathname)
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标准所支持的新风格。(在下一章中,我们将对ANSI C作更
多说明。)
·我们取命令行的第1个参数argv〔1〕作为要列表的目录名。在第七章中,我们将说明
main函数是如何被调用的,程序如何存取命令行参数和环境变量。
·因为各种不同Unix系统的目录项的实际格式是不一样的,所以我们使用函数opendir,
readdir和closedir处理目录。
·opendir函数返回指向DIR结构的指针,并将该指针传向readdir函数。我们并不关心DIR
结构中包含了什么。然后,我们在循环中调用readdir,以读每个目录项。它返回一个指向
dirent结构的指针,而当目录中已无目录项可读时则返回null指针。我们在dirent结构中
取出的只是每个目录项的名字(d[CD#*2]name)。使用该名字,我们此后就可调用stat函数
(4.2节)以决定该文件的所有属性。
·调用了两个我们自编的函数对错误进行处理:err-sys和err-quit。我们从上面的输出中
可以看到,err-sys函数打印一条消息,说明遇到了什么类型的错误。("Permission 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-FILENO和STDOU
T-FILENO是POSIX标准的一部分(在下一章,我们将对此作更多的说明)。很多Unix系统服
务的函数原型,例如我们调用的read和write都在此头文件中。函数原型也是ANSI C标准的
一部分,我们将在本章的稍后部分对此作更多说明。
两个常数STDIN-FILENO和STDOUT-FILENO定义在<unistd d>头文件中,它们指定了标准输入
和标准输出的文件描述符。它们的典型值是0和1,但是为了可移植性,我们将使用这些新
名字。
在3.9节中,我们将详细地讨论BUFSIZE常数,说明各种不同值将如何影响程序的效率。但
是不管该常数的值如何,此程序总能复制任一Unix文件。
read函数返回读得的字节数,此值用作为要写的字节数。当到了文件的尾端时,read返回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的程序中,我们总是包括<stdio h>(通
常包括在ourhdr.h中),因为此头文件包括了所有标准I/O函数的原型。
实例
程序1.3的功能类似于调用read和write的前一道程序1.2,我们将在5.8中对程序1.3作更详
细的说明。它将标准输入复制到标准输出,于是也就能复制任一Unix文件。
程序1.3〓用标准I/O将标准输入复制到标准输出
函数getc一次读1个字符,然后putc将此字符写到标准输出。读到输入的最后1个字节时,
getc返回常数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字节代换新行符。这一操作的目的是因为execlp函
数要求的是以null结束的参数,而不是以新行符结束的参数。
·调用fork创建一个新进程,新进程是调用进程的复制品,我们称调用进程为父进程,新
创建的进程为子进程。fork对父进程,返回新子进程的非负进程ID,对子进程则返回0。
因为fork创建一新进程,所以我们说它被调用一次(由父进程),但返回两次(在父进程中
和在子进程中)。
·在子进程中,我们调用execlp以执行从标准输入读入的命令。这使子进程更换了新的程
序文件。fork和跟附其后的exec的组合是某些操作系统所称的产生一个新进程。在Unix中
,这两个部分分成两个函数。在第八章中,我们将对这些函数作更多说明。
·子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这一要求由调用
waitpid实现,其参数指定要等待的进程(在这里,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
shell.c
% ^D 键入我们的文件结束符
$ 输出常规的shell提示
1.6ANSI C
本书中的所有实例都按ANSI C编写
函数原型
头文件<unistd h>包含了许多Unix系统服务的函数原型,例如我们已调用过的read,write
和getpid函数。函数原型是ANSI C标准的组成部分。这些函数原型如下列形式:
ssize_t read(int,void *,size_t);
ssize_t write(int,void *,size_t);
pid_t getpid(void);
最后一个的意思是:getpid没有参数(void),返回值的数据类型pid_t。提供了这些函数原
型后,编译程序在编译时就可以检查我们在调用函数时是否使用了正确的参 数。在程序1.
4中,如果我们带一个参数调用getpid(如同在getpid(1)中一样),则我们将从ANSI C 编译
程序得到下列形式的出错信息:
line 8:too many arguments to function "getpid"
另外,因为编译程序知道参数的数据类型,所以如果可能,它就会将参数强制转换成所需
的数据类型。
类属指针
从上面所示的函数原型中我们可以注意到的另一个区别是:read和write的第二个参数现在
是void *类型。所有较早的Unix系统都使用char *这种指针类型。作这种更改的原因是:
ANSI C使用void *作为类属指针,以代替char *。
函数原型和类属指针相组合使我们消去了很多非ANSI C编译程序需要的显式类型强制转换。
例如,给出了write原型后,我们可以写成:
float data〔100〕;
write (fd,data,sizeof(data));
若使用非ANSI编译程序,或没有给出函数原型,则我们需写成:
write(fd,(void *)data,sizeof(data));
我们也将void *指针特征用于malloc函数(见7.8节)。malloc的原型现在是:
void * malloc(size_t);
这使得我们可以写下面的程序段:
int * ptr;
ptr=malloc (1000 * sizeof(int));
它无需将返回的指针强制转换成int *类型。
原始系统数据类型
前面所示的getpid函数的原型定义了其返回值为pid_t类型。这也是POSIX中的新规定。Uni
x的早期版本规定此函数返回一整型。与此类似,read和write返回类型为ssize_t的值,以
及要求第3个参数的类型是size_t。
以_t结尾的这些数据类型被称为原始系统数据类型。它们通常在头文件<sys/types.h>中定
义(头文件<unistd h>应已包括该头文件)。它们通常以C typedef说明加以定义,typedef
说明在C语言中已超过15年了(所以这并不要求ANSI C),它们的目的是阻止程序(在用专门
的数据类型(例如int,short或long)以允许对于一种特定系统的每个实现,选择所要求的数
据类型。在需要存储进程ID处,我们将分配类型为pid_t的一个变量。(注意,我们在程序
1.5中,已对名为pid的变量这样做了。)在各种不同的实现中,这种数据类型的定义可能是
不同的,但是这种差别现在只出现在一个头文件中。我们所需做的只是在另一个系统上重新
编译应用程序。
1.7出错处理
当Unix函数出错时,往常返回一个负值,而且整型变量errno通常设置为具有特定信息的一
个值。例如,open函数如成功执行则返回一个非负文件描述符。如若出错则返回-1。在ope
n出错时,有大约15种不同的errno值(文件下存在,许可权问题等)。某些函数使用不是返
回负值的另一种约定。例如,返回一个指向一个对象的指针的大多数函数,在出错时,返
回一个null指针。
文件<error.h>中定义了变量errno,以及可以赋与它的各种常数。这些常数都以E开头,另
外,Unix手册第二部分的第一页是intro(2),它通常列出了所有这些出错常数。例如,若e
rrno等于常数EACCES,这表示产生了许可权问题(例如,我们没有打开所要求文件的许可权
)。POSIX定义errno为:
extern int errno;
POSIX.1中errno的定义较C标准中的定义更为苛刻。C标准允许errno可以是一个宏,它扩认
成可修改的整型左值(lvalue)(例如一个函数,它返回一个指向出错数的指针)。
对于errno应当知道两个规则。第一个规则是:如果没有出错,则其值不会被一个例程消除
。因此,仅当函数的返回值指明出错时,才检验其值。第二个规则是:任一函数都不会将e
rrno值设置为0,在<errno.h>中定义的所有常数都不具值0。
C标准定义了两个函数,它们帮助打印出错信息。
#include <string.h>
char *strerror(int errnum);
返回:指向消息字符串的指针
此函数将errnum(它通常就是errno值)映射为一个出错信息字符串,并且返回此字符串的指
针。
perror函数在标准出错上产生一条出错消息(基于errno的当前值),然后返回。
#include <stdio.h>
void perror(const char *msg);
它首先输出由msg指向的字符串,然后是一个冒号,一个空格,然后是对应于errno值的出
错信息,然后是一个新行符。
实例
程序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)和退出
键(通常是控制-反斜线),它们被用于中断当前运行进程。另一种产生信号的方法是调用名
为kill的函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当然这样做也
有些限制:为了向一个进程发送信号,我们必需是该进程的属主。
实例
回忆一下基本shell程序(程序1 5)。如果我们调用此程序,然后键入中断键,则执行此程
序的进程终止。产生这种后果的原因是:对于此信号(SIGINT)的系统默认动作是终止此进
程。该进程没有告诉系统核对此信号作何处理,所以系统按默认方式终止该进程。
为了更改此程序使其能捕捉到该信号,它需要调用signal函数,指定当产生SIGINT信号时
要调用的函数名。我们为此编写了名为sig_int的函数,当其被调用时,它只是打印一条消
息,然后打印一个新提示符。在程序1.5中加了12行构成了程序1.8(添加的12行以行首的+
号指示)。
程序1.8〓从标准输入读命令并执行它们
因为大多数重要的应用程序都将使用信号,所以在第十章,我们将详细说明信号机构。
1.10Unix时间值
长期以来,Unix系统一直使用两种不同的时间值。
1 日历时间。是自1970 1 1,00:00:00以来,国际标准时(UTC)所经过的秒数累计值(
较早的手册称UTC为格林威冶平时)。这些时间值可用于记录文件最近一次的修改时间等。
2 进程时间。这也被称为CPU时间,用以度量进程使用的中央处理机资源。进程时间以时钟
滴答计算,多年来,每秒钟取为50,60或100个滴答。系统基本数据类型clock_t保持这种
时间值。另外,POSIX定义常数CLK[CD#*2]TCK,用其说明每秒滴答数。(常数CLK_TCK现在
已不再使用。我们将在2.5.4节说明如何用sysconf函数得到每秒时钟滴答数。)
当度量一个进程的执行时间时(见3.9节),Unix系统使用三个进程时间值。
·时钟时间
·用户CPU时间
·系统CPU时间
时钟时间又称为墙上时钟时间。它是进程运行的时间总量,其值在与系统中同时运行的其
它进程数有关。无论何时,在我们报告时钟时间时,都是在系统中没有其它活动时进行度
量的。
用户CPU时间是执行用户指令所用的时间量。系统CPU时间是为该进程执行系统核所经历的时
间。例如,只要一个进程执行一个系统服务,例如read或write,则在系统核内执行该服务
所花费的时间就计入该进程的系统CPU时间。用户CPU时间和系统CPU时间的和常被称为CPU时
间。
要取得任一进程的时钟时间、用户时间和系统时间是容易的-----只要执行命令time(1),
其参数是我们要度量其执行时间的命令,例如:
$ cd /usr/include
$ time grep_POSIX_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函数会调
用write系统调用以进行输出操作,但函数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系统调用,那么就会忽略很多应用程序使用的malloc库函数
。
在本书中,除了一定要将两者相区分时,我们都将使用术语"函数"来涉及系统调用和库函
数两者。