UNIX 环境高级编程 第1章U N I X基础知识


第1章U N I X基础知识
1.1 引言
所有操作系统都向它们运行的程序提供服务。典型的服务有执行新程序、打开文件、读文
件、分配存储区、获得当前时间等等,本书集中阐述了U N I X操作系统各种版本所提供的服务。
以严格的步进方式、不超前引用尚未说明过的术语的方式来说明U N I X几乎是不可能的(可
能也会是令人厌烦的)。本章从程序设计人员的角度快速浏览U N I X,并对书中引用的一些术语
和概念进行简要的说明并给出实例。在以后各章中,将对这些概念作更详细的说明。本章也对
不熟悉U N I X的程序设计人员简要介绍了U N I X提供的各种服务。
1.2 登录
1.2.1 登录名
登录U N I X 系统时,先键入登录名,然后键入口令。系统在其口令文件,通常是
/ e t c / p a s s w d文件中查看登录名。口令文件中的登录项由7个以冒号分隔的字段组成:登录名,
加密口令,数字用户I D ( 2 2 4 ),数字组I D ( 2 0 ),注释字段,起始目录( / h o m e / s t e v e n s ),以及s h e l l
程序( / b i n / k s h )。
很多比较新的系统已将加密口令移到另一个文件中。第6章将说明这种文件以及存取它们
的函数。
1.2.2 shell
登录后,系统先显示一些典型的系统信息,然后就可以向s h e l l程序键入命令。s h e l l是一个
命令行解释器,它读取用户输入,然后执行命令,用户通常用终端,有时则通过文件(称为
s h e l l脚本)向s h e l l进行输入。常用的s h e l l有:
• Bourne shell, /bin/sh
• C shell, /bin/csh
• KornShell, /bin/ksh
系统从口令文件中登录项的最后一个字段中了解到应该执行哪一个s h e l l。
自V 7以来,Bourne shell得到了广泛应用,几乎每一个现有的U N I X系统都提供Bourne shell。
C shell是在伯克利开发的,所有B S D版本都提供这种s h e l l。另外,AT & T的系统V/386 R3.2和
S V R 4也提供C shell(下一章将对这些不同的U N I X版本作更多说明)。K o r n S h e l l是Bourne shell
的后继者,它由S V R 4提供。K o r n S h e l l在大多数U N I X系统上运行,但在S V R 4之前,通常它需
要另行购买,所以没有其他两种s h e l l流行。
本书将使用很多s h e l l实例,以执行已开发的程序,其中将应用Bourne shell和K o r n S h e l l都
具有的功能。
Bourne shell是Steve Bourne在贝尔实验室中开发的,其控制流结构使人想起
Algol 68。C shell是在伯克利由Bill Joy完成的,其基础是第6版s h e l l(不是B o u r n e
s h e l l)。其控制结构很像C语言,它支持一些Bourne shell没有提供的功能,如作业
控制,历史机制和命令行编辑。K o r n S h e l l是David Korn在贝尔实验室中开发的,
它兼容Bourne shell,并且也包含了使C shell非常流行的一些功能,如作业控制、
命令行编译等。
本书将使用这种形式的注释来描述历史,并对不同的U N I X实现进行比较。当
我们了解了历史缘由后,采用某种特定实现技术的原因将变得清晰起来。
1.3 文件和目录
1.3.1 文件系统
U N I X文件系统是目录和文件的一种层次安排,目录的起点称为根( r o o t ),其名字是一个
字符/。
目录(d i r e c t o r y)是一个包含目录项的文件,在逻辑上,可以认为每个目录项都包含一个
文件名,同时还包含说明该文件属性的信息。文件属性是:文件类型,文件长度,文件所有者,
文件的许可权(例如,其他用户能否能访问该文件),文件最后的修改时间等。s t a t和f s t a t函数
返回一个包含所有文件属性的信息结构。第4章将详细说明文件的各种属性。
1.3.2 文件名
目录中的各个名字称为文件名( f i l e n a m e)。不能出现在文件名中的字符只有两个,斜线( / )
和空操作符(n u l l)。斜线分隔构成路径名(在下面说明)的各文件名,空操作符则终止一个路径
名。尽管如此,好的习惯是只使用印刷字符的一个子集作为文件名字符(只使用子集的理由是:
如果在文件名中使用了某些s h e l l特殊字符,则必须使用s h e l l的引号机制来引用文件名)。
当创建一个新目录时,自动创建了两个文件名: . (称为点)和. . (称为点-点)。点引用当前目
录,点-点则引用父目录。在最高层次的根目录中,点-点与点相同。
某些U N I X文件系统限制文件名的最大长度为1 4个字符,B S D版本则将这种限制扩展为2 5 5
个字符。
1.3.3 路径名
0个或多个以斜线分隔的文件名序列(可以任选地以斜线开头)构成路径名(p a t h n a m e),以
斜线开头的路径名称为绝对路径名( absolute pathname),否则称为相对路径名( r e l a t i v e
p a t h n a m e)。
实例
不难列出一个目录中所有文件的名字,程序1 - 1是l s ( 1 )命令的主要实现部分
程序1-1 列出一个目录中的所有文件
2 U N I X环境高级编程
下载
l s ( 1 )这种表示方法是U N I X的惯用方法,用以引用U N I X手册集中的一个特定项。它引用第
一部分中的l s项,各部分通常用数字1至8表示,在每个部分中的各项则按字母顺序排列。假定
你有一份所使用的U N I X系统的手册。
早期的U N I X系统把8个部分都集中在一本手册中,现在的趋势是把这些部分
分别安排在不同的手册中:有用户专用手册,程序员专用手册,系统管理员专用
的手册等等。
某些U N I X系统把一个给定部分中的手册页又用一个大写字母进一步分成若干
小部分,例如,AT & T〔1 9 9 0 e〕中的所有标准I / O函数都被指明在3 S部分中,例如
f o p e n ( 3 S )。
某些U N I X系统,例如以X e n i x为基础的系统,不是采用数字将手册分成若干
部分,而是用C表示命令(第1部分),S表示服务(通常是第2、3部分)等等。
如果你有联机手册,则可用下面的命令查看l s命令手册页:
man 1 ls
程序1 - 1只打印一个目录中各个文件的名字,不显示其他信息,如若该源文件名为m y l s . c ,
则可以用下面的命令对其进行编译,编译的结果送入系统默认名为a . o u t的可执行文件名:
cc myls.c
某种样本输出是:
$ a.out /dev
.
. .
M A K E D E V
c o n s o l e
t t y
m e m
k m e m
n u l l
此处略去多行
第1章U N I X基础知识3
下载
p r i n t e r
$ 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
本书将以这种方式表示输入的命令以及其输出:输入的字符以粗体表示,程序输出则以另
一种字体表示。如果欲对输出添加注释,则以中文宋体表示,输入之前的美元符号( $ )是s h e l l打
印的提示符,本书将s h e l l提示符显示为$。
注意,列出的目录项不是以字母顺序排列的, l s命令则一般按字母顺序列出目录项。
在这2 0行的程序中,有很多细节需要考虑:
• 首先,其中包含了一个头文件o u r h d r. h。本书中几乎每一个程序都包含此头文件。它包含
了某些标准系统头文件,定义了许多常数及函数原型,这些都将用于本书的各个实例中,附录
B列出了常用头文件。
• main函数的说明使用了ANSI C标准所支持的新风格(下一章将对ANSI C作更多说明)。
• 取命令行的第1个参数a rg v〔1〕作为列出的目录名。第7章将说明m a i n函数如何被调用,
程序如何存取命令行参数和环境变量。
• 因为各种不同U N I X系统的目录项的实际格式是不一样的,所以使用函数o p e n d i r, readdir
和c l o s e d i r处理目录。
• opendir函数返回指向D I R结构的指针,并将该指针传向r e a d d i r函数。我们并不关心D I R
结构中包含了什么。然后,在循环中调用r e a d d i r来读每个目录项。它返回一个指向d i r e n t结
构的指针,而当目录中已无目录项可读时则返回n u l l指针。在d i r e n t结构中取出的只是每个
目录项的名字( d _ n a m e )。使用该名字,此后就可调用s t a t函数(见4 . 2节)以决定该文件的所有
属性。
• 调用了两个自编的函数来对错误进行处理:e r r _ s y s和e r r _ q u i t。从上面的输出中可以看到,
e r r _ s y s函数打印一条消息(“Permission denied(许可权拒绝)”或“Not a directory(不是一个目
录)”),说明遇到了什么类型的错误。这两个出错处理函数在附录B中说明, 1 . 7节将更多地叙
述出错处理。这两个出错处理函数在附录B中说明1 . 7节将更详细地叙述出错处理。
• 当程序将结束时,它以参数0调用函数e x i t。函数e x i t终止程序。按惯例,参数0的意思是
正常结束,参数值1~2 5 5则表示出错。8 . 5节将说明一个程序(例如s h e l l或我们所编写的程序)如
何获得它所执行的另一个程序的e x i t状态。
1.3.4 工作目录
每个进程都有一个工作目录(working directory,有时称为当前工作目录(current working
d i r e c t o r y ) )。所有相对路径名都从工作目录开始解释。进程可以用c h d i r函数更改其工作目录。
例如,相对路径名d o c / m e m o / j o e指的是文件j o e,它在目录m e m o中,而m e m o又在目录d o c
中,d o c则应是工作目录中的一个目录项。从该路径名可以看出, d o c和m e m o都应当是目录,
但是却不清楚j o e是文件还是目录。路径名/ u r s / l i b / l i n t是一个绝对路径名,它指的是文件(或目
录) lint,而l i n t在目录l i b中,l i b则在目录u s r中,u s r则在根目录中。
1.3.5 起始目录
登录时,工作目录设置为起始目录(home directory),该起始目录从口令文件(见1 . 2节)中
4 U N I X环境高级编程
下载
的登录项中取得。
1.4 输入和输出
1.4.1 文件描述符
文字描述符是一个小的非负整数,内核用以标识一个特定进程正在存访的文件。当内核打
开一个现存文件或创建一个新文件时,它就返回一个文件描述符。当读、写文件时,就可使
用它。
1.4.2 标准输入、标准输出和标准出错
按惯例,每当运行一个新程序时,所有的s h e l l都为其打开三个文件描述符:标准输入、标
准输出以及标准出错。如果像简单命令l s那样没有做什么特殊处理,则这三个描述符都连向终
端。大多数s h e l l都提供一种方法,使任何一个或所有这三个描述符都能重新定向到某一个文件,
例如:
ls > file.list
执行l s命令,其标准输出重新定向到名为f i l e . l i s t的文件上。
1.4.3 不用缓存的I / O
函数o p e n、r e a d、w r i t e、l s e e k以及c l o s e提供了不用缓存的I / O。这些函数都用文件描述符
进行工作。
实例
如果愿意从标准输入读,并写向标准输出,则程序1 - 2可用于复制任一U N I X文件。
程序1-2 将标准输入复制到标准输出
头文件< u n i s t d . h > ( o u r h d r. h中包含了此头文件)及两个常数S T D I N _ F I L E N O和S T D O U T _
F I L E N O是P O S I X标准的一部分(下一章将对此作更多的说明)。很多U N I X系统服务的函数原
型,例如我们调用的r e a d和w r i t e都在此头文件中。函数原型也是ANSI C标准的一部分,本章的
第1章U N I X基础知识5
下载
稍后部分将对此作更多说明。
两个常数S T D I N _ F I L E N O和S T D O U T _ F I L E N O定义在< u n i s t d . h >头文件中,它们指定了标
准输入和标准输出的文件描述符。它们的典型值是0和1,但是为了可移植性,我们将使用这些
新名字。
3 . 9节将详细讨论B U F F S I Z E常数,说明各种不同的值将如何影响程序的效率。但是不管该
常数的值如何,此程序总能复制任一U N I X文件。
r e a d函数返回读得的字节数,此值用作要写的字节数。当到达文件的尾端时, r e a d返回0,
程序停止执行。如果发生了一个读错误, r e a d返回-1。出错时大多数系统函数返回-1。
如果编译该程序,其结果送入标准的a . o u t文件,并以下列方式执行它:
a.out > data
那么,标准输入是终端,标准输出则重新定向至文件d a t a,标准出错也是终端。如果此输出文
件并不存在,则s h e l l创建它。
第3章将更详细地说明不用缓存的I / O函数。
1.4.4 标准I / O
标准I / O函数提供一种对不用缓存的I / O函数的带缓存的界面。使用标准I / O可无需担心如何
选取最佳的缓存长度,例如程序1 - 2中的B U F F S I Z E常数。另一个使用标准I / O函数的优点与处
理输入行有关(常常发生在U N I X的应用中)。例如,f g e t s函数读一完整的行,而另一方面, r e a d
函数读指定字节数。
我们最熟悉的标准I / O函数是p r i n t f。在调用p r i n t f的程序中,总是包括< s t d i o . h > (通常包括在
o u r h d r. h中),因为此头文件包括了所有标准I / O函数的原型。
实例
程序1 - 3的功能类似于调用r e a d和w r i t e的前一个程序1 - 2,5 . 8节将对程序1 - 3作更详细的说
明。它将标准输入复制到标准输出,于是也就能复制任一U N I X文件。
程序1-3 用标准I / O将标准输入复制到标准输出
函数g e t c一次读1个字符,然后p u t c将此字符写到标准输出。读到输入的最后1个字节时,
g e t c返回常数E O F。标准输入、输出常数s t d i n和s t d o u t定义在头文件< s t d i o . h >中,它们分别表示
标准输入和标准输出文件。
6 U N I X环境高级编程
下载
1.5 程序和进程
1.5.1 程序
程序(p r o g r a m)是存放在磁盘文件中的可执行文件。使用6个e x e c函数中的一个由内核将
程序读入存储器,并使其执行。8 . 9节将说明这些e x e c函数。
1.5.2 进程和进程I D
程序的执行实例被称为进程( p r o c e s s)。本书的每一页几乎都会使用这一术语。某些操作
系统用任务表示正被执行的程序。
每个U N I X进程都一定有一个唯一的数字标识符,称为进程I D(process ID)。进程I D总是
一非负整数。
实例
程序1 - 4用于打印进程I D。
程序1-4 打印进程I D
如果要编译该程序,其结果送入a . o u t文件,然后执行它,则有:
$ a . o u t
hello world from process ID 851
$ a . o u t
hello world from precess ID 854
此程序运行时,它调用函数g e t p i d得到其进程I D。
1.5.3 进程控制
有三个用于进程控制的主要函数: f o r k、e x e c和w a i t p i d(e x e c函数有六种变体,但经常把
它们统称为e x e c函数)。
实例
程序1-5 从标准输入读命令并执行
第1章U N I X基础知识7
下载
U N I X的进程控制功能可以用一个较简单的程序(见程序1 - 5)说明,该程序从标准输入读
命令,然后执行这些命令。这是一个类似于s h e l l程序的基本实施部分。在这个3 0行的程序中,
有很多功能需要思考:
• 用标准I / O函数f g e t s从标准输入一次读一行,当键入文件结束字符(通常是C t r l - D)作为
行的第1个字符时,f g e t s返回一个n u l l指针,于是循环终止,进程也就终止。第11章将说明所有
特殊的终端字符(文件结束、退格字符、整行擦除等等),以及如何改变它们。
• 因为f g e t s返回的每一行都以新行符终止,后随一个n u l l字节,故用标准C函数s t r l e n计算此
字符串的长度,然后用一个n u l l字节替换新行符。这一操作的目的是因为e x e c l p函数要求的是
以n u l l结束的参数,而不是以新行符结束的参数。
• 调用f o r k创建一个新进程。新进程是调用进程的复制品,故称调用进程为父进程,新创
建的进程为子进程。f o r k对父进程返回新子进程的非负进程I D,对子进程则返回0。因为f o r k创
建一新进程,所以说它被调用一次(由父进程),但返回两次(在父进程中和在子进程中)。
• 在子进程中,调用e x e c l p以执行从标准输入读入的命令。这就用新的程序文件替换了子
进程。f o r k和跟随其后的e x e c的组合是某些操作系统所称的产生一个新进程。在U N I X中,这两
个部分分成两个函数。第8章将对这些函数作更多说明。
• 子进程调用e x e c l p执行新程序文件,而父进程希望等待子进程终止,这一要求由调用
w a i t p i d实现,其参数指定要等待的进程(在这里, p i d参数是子进程I D )。w a i t p i d函数也返回子
进程的终止状态( s t a t u s变量)。在此简单程序中,没有使用该值。如果需要,可以用此值精确地
确定子进程是如何终止的。
• 该程序的最主要限制是不能向执行的命令传递参数。例如不能指定列出的目录名,只能
对工作目录执行l s命令。为了传递参数,先要分析输入行,然后用某种约定把参数分开(很可能
使用空格或制表符),然后将分隔后的各个参数传递给e x e c l p函数。尽管如此,此程序仍可用来
说明U N I X的进程控制功能。
如果运行此程序,则得到下列结果。注意,该程序使用了一个不同的提示符( % )。
8 U N I X环境高级编程
下载
$ a . o u t
% d a t e
Fri Jun 7 15:50:36 MST 1991
% w h o
stevens console Jun 5 06:01
stevens ttyp0 Jun 5 06:02
% p w d
/ h o m e / s t e v e n s / d o c / a p u e / p r o c
% l s
M a k e f i l e
a . o u t
s h e l l l . c
% ˆD 键入文件结束符
$ 输出常规的s h e l l提示符
1.6 ANSI C
本书中的所有实例都用ANSI C编写。
1.6.1 函数原型
头文件< u n i s t d . h >包含了许多U N I X系统服务的函数原型,例如已调用过的r e a d,w r i t e和
g e t p i d函数。函数原型是ANSI C标准的组成部分。这些函数原型如下列形式:
ssize_t read(int, void *, size_t);
ssize_t write(int, const void *, size_t);
pid_t getpid(void);
最后一个的意思是:g e t p i d没有参数( v o i d ),返回值的数据类型为p i d _ t。提供了这些函数原
型后,编译程序在编译时就可以检查在调用函数时是否使用了正确的参数。在程序1 - 4中,如
果调用带参数的g e t p i d (如g e t p i d ( 1 ) ),则ANSI C编辑程序将给出下列形式的出错信息:
line 8: too many arguments to function "getpid"
另外,因为编译程序知道参数的数据类型,所以如果可能,它就会将参数强制转换成所需
的数据类型。
1.6.2 类属指针
从上面所示的函数原型中可以注意到另一个区别: r e a d和w r i t e的第二个参数现在是void *
类型。所有早期的U N I X系统都使用char *这种指针类型。作这种更改的原因是: ANSI C使用
void *作为类属指针来代替char *。
函数原型和类属指针的组合消去了很多非ANSI C编辑程序需要的显式类型强制转换。
例如,给出了w r i t e原型后,可以写成:
float data〔1 0 0〕;
write (fd, data, sizeof(data)) ;
若使用非A N S I编译程序,或没有给出函数原型,则需写成:
write(fd, (void *)data, sizeof(data));
也可将void *指针特征用于m a l l o c函数(见7 . 8节)。m a l l o c的原型为:
第1章U N I X基础知识9
下载
void * malloc(size_t) ;
这使得可以有如下程序段:
int * ptr;
ptr = malloc (1000 * sizeof(int));
它无需将返回的指针强制转换成int *类型。
1.6.3 原始系统数据类型
前面所示的g e t p i d函数的原型定义了其返回值为p i d _ t类型,这也是P O S I X中的新规定。
U N I X的早期版本规定此函数返回一整型。与此类似, r e a d和w r i t e返回类型为s s i z e _ t的值,并
要求第三个参数的类型是s i z e _ t。
以_ t结尾的这些数据类型被称为原始系统数据类型。它们通常在头文件< s y s / t y p e s . h >中定
义(头文件< u n i s t d . h >应已包括该头文件)。它们通常以C typedef说明加以定义。t y p e d e f说明在C
语言中已超过1 5年了(所以这并不要求ANSI C),它们的目的是阻止程序使用专门的数据类型
(例如i n t , s h o r t或long) 来允许对于一种特定系统的每个实现选择所要求的数据类型。在需要存储
进程I D的地方,分配类型为p i d _ t的一个变量(注意,程序1 - 5已对名为p i d的变量这样做了)。在
各种不同的实现中,这种数据类型的定义可能是不同的,但是这种差别现在只出现在一个头文
件中。我们只需在另一个系统上重新编辑应用程序。
1.7 出错处理
当U N I X函数出错时,往常返回一个负值,而且整型变量e r r n o通常设置为具有特定信息的一
个值。例如,o p e n函数如成功执行则返回一个非负文件描述符,如出错则返回-1。在o p e n出错
时,有大约1 5种不同的e r r n o值(文件不存在,许可权问题等)。某些函数并不返回负值而是使用
另一种约定。例如,返回一个指向对象的指针的大多数函数,在出错时,将返回一个null指针。
文件< e r r n o . h >中定义了变量e r r n o以及可以赋与它的各种常数。这些常数都以E开头,另外,
U N I X手册第2部分的第1页, intro(2) 列出了所有这些出错常数。例如,若e r r n o等于常数
E A C C E S,这表示产生了权限问题(例如,没有打开所要求文件的权限)。P O S I X定义e r r n o为:
extern int errno;
P O S I X . 1中e r r n o的定义较C标准中的定义更为苛刻。C标准允许e r r n o是一个宏,
它扩充成可修改的整型左值(lvalue) (例如返回一个指向出错数的指针的函数)。
对于e r r n o应当知道两条规则。第一条规则是:如果没有出错,则其值不会被一个例程清除。
因此,仅当函数的返回值指明出错时,才检验其值。第二条是:任一函数都不会将e r r n o值设
置为0,在< e r r n o . h >中定义的所有常数都不为0。
C标准定义了两个函数,它们帮助打印出错信息。
#include <string.h>
char *strerror(int e rr n u m) ;
返回:指向消息字符串的指针
此函数将e rr n u m(它通常就是e r r n o值) 映射为一个出错信息字符串,并且返回此字符串的指针。
1 0 U N I X环境高级编程
下载
p e r r o r函数在标准出错上产生一条出错消息(基于e r r n o的当前值),然后返回。
#include <stdio.h>
void perror(const char * m s g) ;
它首先输出由m s g指向的字符串,然后是一个冒号,一个空格,然后是对应于e r r n o值的出
错信息,然后是一个新行符。
实例
程序1 - 6显示了这两个出错函数的使用方法。
程序1-6 例示s t r e r r o r和p e r r o r
如果此程序经编译,结果送入文件a . o u t,则有:
$ a . o u t
EACCES: Permission denied
a.out: No such file or directory
注意,我们将程序名( a rg v〔0〕,其值是a.out) 作为参数传递给p e r r o r。这是一个标准的
U N I X惯例。使用这种方法,如程序作为管道线的一部分执行,如:
prog1 < inputfile | prog2 | prog3 > outputfile
则我们就能分清三个程序中的哪一个产生了一条特定的出错消息。
本书中的所有实例基本上都不直接调用s t r e r r o r或p e r r o r,而是使用附录B中的出错函数。该
附录中的出错函数使用了ANSI C的可变参数表设施,用一条C语句就可处理出错条件。
1.8 用户标识
1.8.1 用户I D
口令文件登录项中的用户I D(user ID)是个数值,它向系统标识各个不同的用户。系统管理员
在确定一个用户的登录名的同时,确定其用户I D。用户不能更改其用户I D。通常每个用户有一个
唯一的用户I D。下面将介绍内核如何使用用户I D以检验该用户是否有执行某些操作的适当许可权。
用户I D为0的用户为根( r o o t )或超级用户( s u p e r u s e r )。在口令文件中,通常有一个登录项,
其登录名为r o o t,我们称这种用户的特权为超级用户特权。我们将在第4章中看到,如果一个
进程具有超级用户特权,则大多数文件许可权检查都不再进行。某些操作系统功能只限于向超
第1章U N I X基础知识1 1
下载
级用户提供,超级用户对系统有自由的支配权。
实例
程序1 - 7用于打印用户I D和组I D(在下面说明)。
程序1-7 打印用户I D和组I D
调用g e t u i d和g e t g i d以返回用户I D和组I D。运行该程序,产生:
$ a . o u t
uid = 224, gid = 20
1.8.2 组I D
口令文件登录项也包括用户的组I D(group ID),它也是一个数值。组I D也是由系统管理
员在确定用户登录名时分配的。一般来说,在口令文件中有多个记录项具有相同的组I D。在
U N I X下,组被用于将若干用户集合到课题或部门中去。这种机制允许同组的各个成员之间共
享资源(例如文件)。4 . 5节将说明可以设置文件的许可权使组内所有成员都能存取该文件,而组
外用户则不能。
组文件将组名映射为数字组I D,它通常是/ e t c / g r o u p。
对于许可权使用数值用户I D和数值组I D是历史上形成的。系统中每个文件的目录项包含该
文件所有者的用户I D和组I D。在目录项中存放这两个值只需4个字节(假定每个都以双字节的整
型值存放)。如果使用8字节的登录名和8字节的组名,则需较多的磁盘空间。但是对于用户而
言,使用名字比使用数值方便,所以口令文件包含了登录名和用户I D之间的映射关系,而组文
件则包含了组名和组I D之间的映射关系。例如UNIX ls-l命令使用口令文件将数值用户I D映射
为登录名,从而打印文件所有者的登录名。
1.8.3 添加组I D
除了在口令文件中对一个登录名指定一个组I D外,某些U N I X版本还允许一个用户属于另
外一些组。这是从4.2 BSD开始的,它允许一个用户属于多至1 6个另外的组。登录时,读文件
/ e t c / g r o u p,寻找列有该用户作为其成员的前1 6个登记项就可得到该用户的添加组I D
(supplementary group ID)。
1.9 信号
信息是通知进程已发生某种条件的一种技术。例如,若某一进程执行除法操作,其除数为
0,则将名为S I G F P E的信号发送给该进程。进程如何处理信号有三种选择:
(1) 忽略该信号。有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的单元等,
1 2 U N I X环境高级编程
下载
因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。
(2) 按系统默认方式处理。对于0除,系统默认方式是终止该进程。
(3) 提供一个函数,信号发生时则调用该函数。使用这种方式,我们将能知道什么时候产
生了信号,并按所希望的方式处理它。
很多条件会产生信号。有两种键盘方式,分别称为中断键(interrupt key,通常是D e l e t e键
或C t r l - C )和退出键(quit key,通常是C t r l - / ),它们被用于中断当前运行进程。另一种产生信号
的方法是调用名为k i l l的函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当
然这样做也有些限制:当向一个进程发送信号时,我们必需是该进程的所有者。
实例
回忆一下基本s h e l l程序(见程序1 - 5 )。如果调用此程序,然后键入中断键,则执行此程序的
进程终止。产生这种后果的原因是:对于此信号( S I G I N T )的系统默认动作是终止此进程。该进
程没有告诉系统核对此信号作何处理,所以系统按默认方式终止该进程。
为了更改此程序使其能捕捉到该信号,它需要调用s i g n a l函数,指定当产生S I G I N T信号时
要调用的函数名。因此编写了名为s i g _ i n t的函数,当其被调用时,它只是打印一条消息,然后
打印一个新提示符。在程序1 - 5中加了1 2行构成了程序1 - 8 (添加的1 2行以行首的+号指示)。
程序1-8 从标准输入读命令并执行
第1章U N I X基础知识1 3
下载
因为大多数重要的应用程序都将使用信号,所以第1 0章将详细介绍信号。
1.10 UNIX时间值
长期以来,U N I X系统一直使用两种不同的时间值:
(1) 日历时间。该值是自1 9 7 0年1月1日0 0 : 0 0 : 0 0以来国际标准时间(U T C)所经过的秒数累
计值(早期的手册称U T C为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改
时间等。
(2) 进程时间。这也被称为C P U时间,用以度量进程使用的中央处理机资源。进程时间以
时钟滴答计算,多年来,每秒钟取为5 0、6 0或1 0 0个滴答。系统基本数据类型c l o c k _ t保存这种
时间值。另外, P O S I X定义常数C L K _ T C K,用其说明每秒滴答数。(常数C L K _ T C K现在已不
再使用。2 . 5 . 4节将说明如何用s y s c o n f函数得到每秒时钟滴答数。)
当度量一个进程的执行时间时(见3 . 9节),U N I X系统使用三个进程时间值:
• 时钟时间。
• 用户C P U时间。
• 系统C P U时间。
时钟时间又称为墙上时钟时间( wall clock time)。它是进程运行的时间总量,其值与系统
中同时运行的进程数有关。在我们报告时钟时间时,都是在系统中没有其他活动时进行度量的。
用户C P U时间是执行用户指令所用的时间量。系统C P U时间是为该进程执行内核所经历
的时间。例如,只要一个进程执行一个系统服务,例如r e a d或w r i t e,则在内核内执行该服务
所花费的时间就计入该进程的系统C P U时间。用户C P U时间和系统C P U时间的和常被称为
C P U时间。
要取得任一进程的时钟时间、用户时间和系统时间很容易——只要执行命令t i m e ( 1 ),其参
数是要度量其执行时间的命令,例如:
$ cd /usr/include
$ time grep _POSIX_SOURCE */*.h > /dev/null
real 0m19.81s
user 0m0.43s
sys 0m4.53s
t i m e命令的输出格式与所使用的s h e l l有关。
8 . 1 5节将说明一个运行进程如何取得这三个时间。关于时间和日期的一般说明见6 . 9节。
1 . 11 系统调用和库函数
所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的U N I X
都提供经良好定义的有限数目的入口点,经过这些入口点进入内核,这些入口点被称为系统调
用(system call)。系统调用是不能更改的一种U N I X特征。U N I X第7版提供了约5 0个系统调用,
4 . 3 + B S D提供了约11 0个,而S V R 4则提供了约1 2 0个。
1 4 U N I X环境高级编程
下载
系统调用界面总是在《U N I X程序员手册》的第2部分中说明。其定义也包括在C语言中。
这与很多早期的操作系统不同,这些系统按传统方式在机器的汇编语言中定义内核入口点。
U N I X所使用的技术是为每个系统调用在标准C库中设置一个具有同样名字的函数。用户进
程用标准C调用序列来调用这些函数,然后,函数又用系统所要求的技术调用相应的内核服务。
例如函数可将一个或多个C参数送入通用寄存器,然后执行某个产生软中断进入内核的机器指
令。从应用角度考虑,可将系统调用视作为C函数。
《U N I X程序员手册》的第3部分定义了程序员可以使用的通用函数。虽然这些函数可能会
调用一个或多个内核的系统调用,但是它们并不是内核的入口点。例如, p r i n t f函数会调用
w r i t e系统调用以进行输出操作,但函数s t r c p y (复制一字符串)和a t o i (变换A S C I I为整数)并不使用
任何系统调用。
从执行者的角度来看,系统调用和库函数之间
有重大区别,但从用户角度来看,其区别并不非常
重要。在本书中系统调用和库函数都以C函数的形
式出现,两者都对应用程序提供服务,但是,我们
应当理解,如果希望的话,我们可以替换库函数,
但是通常却不能替换系统调用。
以存储器分配函数m a l l o c为例。有多种方法可
以进行存储器分配及与其相关的无用区收集操作(最
佳适应,首次适应等),并不存在对所有程序都最佳
的一种技术。U N I X系统调用中处理存储器分配的是
s b r k ( 2 ),它不是一个通用的存储器管理器。它增加
或减少指定字节数的进程地址空间。如何管理该地
址空间却取决于进程。存储器分配函数m a l l o c ( 3 )实
现一种特定类型的分配。如果我们不喜欢其操作方
式,则可以定义自己的m a l l o c函数,它可能将使用
s b r k系统调用。事实上,有很多软件包,它们实现
自己的存储器分配算法,但仍使用s b r k系统调用。
图1 - 1显示了应用程序、m a l l o c函数以及s b r k系统调
用之间的关系。
从中可见,两者职责不同,相互分开,内核中的系统调用分配另外一块空间给进程,而库
函数m a l l o c则管理这一空间。
另一个可说明系统调用和库函数之间的差别的例子是, U N I X提供决定当前时间和日期的
界面。某些操作系统提供一个系统调用以返回时间,而另一个则返回日期。任何特殊的处理,
例如正常时制和夏时制之间的转换,由内核处理或要求人为干预。U N I X则不同,它只提供一
条系统调用,该系统调用返回国际标准时间1 9 7 0年1月1日零点以来所经过的秒数。对该值的
任何解释,例如将其变换成人们可读的,使用本地时区的时间和日期,都留给用户进程运行。
在标准C库中,提供了若干例程以处理大多数情况。这些库函数处理各种细节,例如各种夏时
制算法。
应用程序可以调用系统调用或者库函数,而很多库函数则会调用系统调用。这在图1 - 2中
显示。
系统调用和库函数之间的另一个差别是:系统调用通常提供一种最小界面,而库函数通
第1章U N I X基础知识1 5
下载
图1-1 malloc函数和s b r k系统调用
应用代码
用户进程
内核
存储分配函数
m a l l o c
sbrk 系统调用
常提供比较复杂的功能。我们从s b r k系统
调用和m a l l o c库函数之间的差别中可以看
到这一点,在以后当比较不带缓存的I / O函
数(见第3章)以及标准I / O函数(见第5章)
时,还将看到这种差别。
进程控制系统调用( fork, exec和w a i t)
通常由用户的应用程序直接调用(请回忆
程序1 - 5中的基本s h e l l)。但是为了简化某
些常见的情况,U N I X系统也提供了一些库
函数;例如s y s t e m和p o p e n。8 . 1 2节将说明
s y s t e m函数的一种实现,它使用基本的进
程控制系统调用。1 0 . 1 8节还将强化这一实
例以正确地处理信号。
为使读者了解大多数程序员应用的
U N I X系统界面,我们不得不既说明系统调
用,只介绍某些库函数。例如若只说明
s b r k系统调用,那么就会忽略很多应用程
序使用的m a l l o c库函数。
本书除了必须要区分两者时,都将使用术语函数( f u n c t i o n)来指代系统调用和库函数
两者。
1.12 小结
本章快速浏览了U N I X。说明了某些以后会多次用到的基本术语,介绍了一些小的U N I X程
序的实例,从中可感知到本书的其余部分将会进一步介绍的内容。
下一章是关于U N I X的标准化,以及这方面的工作对当前系统的影响。标准,特别是A N S I
C标准和P O S I X . 1标准将影响本书的余下部分。
习题
1 . 1在在系统上查证,除根目录外,目录. 和.. 是不同的。
1 . 2在分析程序1 - 4的输出,说明进程I D为8 5 2和8 5 3的进程可能会发生什么情况?
1 . 3在在1 . 7节中,p e r r o r的参数是用ANSI C的属性c o n s t定义的,而r e r r o r的整型参数则没有
用此属性定义,为什么?
1 . 4在附录B包含了出错处理函数e r r _ s y s,当调用该函数时,保存了e r r n o的值,为什么?
1 . 5在若日历时间存放在带符号的3 2位整型数中,那么到哪一年它将溢出?
1 . 6在若进程时间存放在带符号的3 2位整型数中,而且每秒为1 0 0滴答,那么经过多少天后
该时间值将会溢出?
1 6 U N I X环境高级编程
下载
图1-2 C库函数和系统调用之间的差别
应用代码
用户进程
内核
C库函数
系统调用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值