所有操作系统都为它们所运行的程序提供服务。典型的服务包括:执行新程序、打开文件、读文件、分配存储区以及获得当前时间等,本书集中阐述不同版本的UNIX操作系统所提供的服务。
想要按严格的先后顺序介绍UNIX,而不超前引用尚未介绍过的术语,这几乎是不可能的(可能也会令人厌烦)。本章从程序员的角度快速浏览UNIX,对书中引用的一些术语和概念进行简要的说明并给出实例。在以后各章中,将对这些概念做更详细的说明。对于初涉UNIX环境的程序员,本章还简要介绍了UNIX提供的各种服务。
从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kernel),因为它相对较小,而且位于环境的核心。图1-1显示了UNIX系统的体系结构。
内核的接口被称为系统调用(system call,图1-1
中的阴影部分)。公用函数库构建在系统调用接口之上,应用程序既可使用公用函数库,也可使用系统调用。(我们将在1.11节对系统调用和库函数做更多说明。)shell是一个特殊的应用程序,为运行其他应用程序提供了一个接口。
从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特性。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等。
例如,Linux是GNU操作系统使用的内核。一些人将这种操作系统称为GNU/Linux操作系统,但是,更常见的是简单地称其为Linux。虽然这种表达方法在严格意义上讲并不正确,但鉴于“操作系统”这个词的双重含义,这种叫法还是可以理解的(这样的叫法更简洁)。
用户在登录UNIX系统时,先键入登录名,然后键入口令。系统在其口令文件(通常是/etc/ passwd文件)中查看登录名。口令文件中的登录项由7个以冒号分隔的字段组成,依次是:登录名、加密口令、数字用户ID(205)、数字组ID(105)、注释字段、起始目录(/home/sar)以及shell程序(/bin/ksh)。
sar:x:205:105:StephenRago:/home/sar:/bin/ksh
目前,所有的系统已将加密口令移到另一个文件中。第6章将说明这种文件以及访问它们的函数。
用户登录后,系统通常先显示一些系统信息,然后用户就可以向shell程序键入命令。(当用户登录时,某些系统启动一个视窗管理程序,但最终总会有一个shell程序运行在一个视窗中)。shell是一个命令行解释器,它读取用户输入,然后执行命令。shell的用户输入通常来自于终端(交互式shell),有时则来自于文件(称为shell脚本)。图1-2总结了UNIX系统中常见的shell。
系统从口令文件中相应用户登录项的最后一个字段中了解到应该为该登录用户执行哪一个shell。图1-2 UNIX系统中常见的shell
自V7以来,由Steve Bourne在贝尔实验室开发的Bourne shell得到了广泛应用,几乎每一个现有的UNIX系统都提供Bourne shell,其控制流结构类似于Algol 68。
C shell是由Bill Joy在伯克利开发的,所有BSD版本都提供这种shell。另外,AT&T的System V/386 R3.2和System V R4(SVR4)也提供C shell(下一章将对这些不同版本的UNIX系统做更多说明)。C shell是在第6版shell而非Bourne shell的基础上构造的,其控制流类似于C语言,它支持Bourne shell没有的一些特色功能,例如作业控制、历史机制以及命令行编辑等。
Korn shell是Bourne shell的后继者,它首先在SVR4中提供。Korn shell是由贝尔实验室的David Korn开发的,在大多数UNIX系统上运行,但在SVR4之前,通常它需要另行购买,所以没有其他两种shell流行。它与Bourne shell向上兼容,并具有使C shell广泛得到应用的一些特色功能,包括作业控制以及命令行编辑等。
Bourne-again shell是GNU shell,所有Linux系统都提供这种shell。它的设计遵循POSIX标准,同时也保留了与Bourne shell的兼容性。它支持C shell和Korn shell两者的特色 功能。
TENEX C shell是C shell的加强版本。它从TENEX操作系统(1972年BBN公司开发)借鉴了很多特色,例如命令完备。TENEX C shell在C shell基础上增加了很多特性,常被用来替换C shell。
POSIX 1003.2标准对shell进行了标准化。这项规范基于Korn shell和Bourne shell的特性。
不同的Linux系统使用不同的默认shell。一些Linux默认使用Bourne-again shell。另外一些使用BSD的对Bourne shell的替代品dash(Debian Almquistshell,最早由KennethAlmquist开发,并在后来移植入Linux)。FreeBSD的默认用户shell衍生于Almquist shell。Mac OS X的默认shell是Bourne-again shell。
Solaries继承了BSD和System V两者,它提供了图1-2中所示的所有shell。在因特网上可以找到shell的自由移植版软件。
本书将使用这种形式的注释来描述历史注释,并对不同的UNIX系统的实现进行比较。当我们了解到历史缘由后,会更好地理解采用某种特定实现技术的原因。
本书将使用很多交互式shell实例来执行所开发的程序,这些实例使用了Bourne shell、Korn shell和Bourne-again shell通用的功能。
UNIX文件系统是目录和文件的一种层次结构,所有东西的起点是称为根(root)的目录,这个目录的名称是一个字符“/”。
目录(directory)是一个包含目录项的文件。在逻辑上,可以认为每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。文件属性是指文件类型(是普通文件还是目录等)、文件大小、文件所有者、文件权限(其他用户能否访问该文件)以及文件最后的修改时间等。stat和fstat函数返回包含所有文件属性的一个信息结构。第4章将详细说明文件的各种属性。
目录项的逻辑视图与实际存放在磁盘上的方式是不同的。UNIX文件系统的大多数实现并不在目录项中存放属性,这是因为当一个文件具有多个硬链接时,很难保持多个属性副本之间的同步。这一点将在第4章讨论硬链接时理解得更明晰。
目录中的各个名字称为文件名(filename)。只有斜线(/)和空字符这两个字符不能出现在文件名中。斜线用来分隔构成路径名的各文件名,空字符则用来终止一个路径名。尽管如此,好的习惯还是只使用常用印刷字符的一个子集作为文件名字符(如果在文件名中使用了某些shell的特殊字符,则必须使用shell的引号机制来引用文件名,这会带来很多麻烦)。事实上,为了可移植性,POSIX.1推荐将文件名限制在以下字符集之内:字母(a~z、A~Z)、数字(0~9)、句点(.)、短横线(-)和下划线(_)。
创建新目录时会自动创建了两个文件名:.(称为点)和..(称为点点)。点指向当前目录,点点指向父目录。在最高层次的根目录中,点点与点相同。
Research UNIX System和某些早期UNIX System V的文件系统限制文件名的最大长度为14个字符,BSD版本则将这种限制扩展为255个字符。现今,几乎所有商业化的UNIX文件系统都支持超过255个字符的文件名。
由斜线分隔的一个或多个文件名组成的序列(也可以斜线开头)构成路径名(pathname),以斜线开头的路径名称为绝对路径名(absolute pathname),否则称为相对路径名(relative pathname)。相对路径名指向相对于当前目录的文件。文件系统根的名字(/)是一个特殊的绝对路径名,它不包含文件名。
实例不难列出一个目录中所有文件的名字,
图1-3是ls(1)命令的简要实现。
#include"apue.h"
#include<dirent.h>
int
main(int argc, char*argv[])
{
DIR *dp;
struct dirent *dirp;
if (argc != 2)
err_quit("usage: lsdirectory_name");
if((dp = opendir(argv[1])) == NULL)
err_sys("can't open %s", argv[1]);
while ((dirp = readdir(dp)) != NULL)
printf("%s\n",dirp->d_name);
closedir(dp);
exit(0);
}
ls(1)这种表示方法是UNIX系统的惯用方法,用以引用UNIX系统手册中的一个特定项。ls(1)引用第一部分中的ls项。各部分通常用数字1~8编号,在每个部分中的各项则按字母顺序排列。在本书中始终假定你有自己所使用的UNIX系统的手册。
早期的UNIX系统把8个部分都集中在一本《UNIX程序员手册》(UNIXProgrammer’sManual)中。随着页数的增加,现在的趋势是把这些部分分别安排在不同的手册中,例如用户手册、程序员手册以及系统管理员手册等。
一些UNIX系统用大写字母把某一部分手册进一步分成若干小部分,例如,AT&T[1990e]中的所有标准I/O函数都被指明位于3S部分中,例如fopen(3S)。另一些UNIX系统不用数字而是用字母将手册分成若干部分,如用C表示命令部分等。
现今,大多数手册都以电子文档形式提供。如果用的是联机手册,则可用下面的命令查看ls命令手册页:
man 1 ls
或
man -s1 ls
图1-3只打印一个目录中各个文件的名字,不显示其他信息,如果该源文件名为myls.c,则可以用下面的命令对其进行编译,编译结果是生成默认名为a.out的可执行文件中。
cc myls.c
历史上,cc(1)是C编译器。在配置了GNU C编译系统的系统中,C编译器是gcc(1)。其中,cc通常链接至gcc。
示例输出如下:
$ ./a.out /dev
.
..
cdrom
stderr
stdout
stdin
fd
sda4
sda3
sda2
sda1
sda
tty2
tty1
console
tty
zero
null
很多行未显示
mem
$ ./a.out /etc/ssl/private
can't open /etc/ssl/private:Permission denied
$ ./a.out/dev/tty
can't open/dev/tty: Not a directory
本书将以以下方式表示输入的命令及其输出:输入的字符以等宽粗体表示,程序输出则以上面所示的等宽字体表示。对输出的注释以中文宋体表示。输入之前的美元符号($)是shell的提示符,本书总是将shell提示符表示为$。
注意,myls程序列出的目录中的文件名不是以字母顺序列出的,而ls命令一般是按字母顺序打印目录项。
在这个20行的程序中,有很多细节需要考虑。
首先,其中包含了一个头文件apue.h。本书中几乎每一个程序都包含此头文件。它包含了某些标准系统头文件,定义了许多常量及函数原型,这些都将用于本书的各个实例中,附录B列出了这一头文件。
接下来,我们包含了一个系统头文件dirent.h,以便使用opendir和readdir的函数原型,以及dirent结构的定义。在其他一些系统里,这些定义被分成多个头文件。比如,在Ubuntu12.04中,/usr/include/dirent.h声明了函数原型,并且包含bits/dirent.h,后者定义了dirent结构(真正存放在/usr/include/x86_64- linux-gnu/bits下)。
main函数的声明使用了ISO C标准所使用的风格(下一章将对ISO C标准进行更多说明)。
程序获取命令行的第1个参数argv[1]作为要列出其各个目录项的目录名。第7章将说明main函数如何被调用,程序如何存取命令行参数和环境变量。
因为各种不同UNIX系统目录项的实际格式是不一样的,所以使用函数opendir、readdir和closedir对目录进行处理。
opendir函数返回指向DIR结构的指针,我们将该指针传送给readdir函数。我们并不关心DIR结构中包含了什么。然后,在循环中调用readdir来读每个目录项。它返回一个指向dirent结构的指针,而当目录中已无目录项可读时则返回null指针。在dirent结构中取出的只是每个目录项的名字(d_name)。使用该名字,此后就可调用stat函数(见4.2节)以获得该文件的所有属性。
程序调用了两个自编的函数对错误进行处理:err_sys和err_quit。从上面的输出中可以看到,err_sys函数打印一条消息(“Permission denied”或“Not a directory”),说明遇到了什么类型的错误。这两个出错处理函数在附录B中说明,1.7节将更多地叙述出错处理。
· 当程序将结束时,它以参数0调用函数exit。函数exit终止程序。按惯例,参数0的意思是正常结束,参数值1~255则表示出错。8.5节将说明一个程序(如shell或我们所编写的程序)如何获得它所执行的另一个程序的exit状态。
每个进程都有一个工作目录(working directory),有时称其为当前工作目录(current working directory)。所有相对路径名都从工作目录开始解释。进程可以用chdir函数更改其工作目录。
例如,相对路径名doc/memo/joe指的是当前工作目录中的doc目录中的memo目录中的文件(或目录)joe。从该路径名可以看出,doc和memo都应当是目录,但是却不能分辨joe是文件还是目录。路径名/urs/lib/lint是一个绝对路径名,它指的是根目录中的usr目录中的lib目录中的文件(或目录)lint。
登录时,工作目录设置为起始目录(home directory),该起始目录从口令文件(见1.3节)中相应用户的登录项中取得。
文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。在读、写文件时,可以使用这个文件描述符。
按惯例,每当运行一个新程序时,所有的shell都为其打开3个文件描述符,即标准输入(standard input)、标准输出(standard output)以及标准错误(standard error)。如果不做特殊处理,例如就像简单的命令ls,则这3个描述符都链接向终端。大多数shell都提供一种方法,使其中任何一个或所有这3个描述符都能重新定向到某个文件,例如:
ls > file.list
执行ls命令,其标准输出重新定向到名为file.list的文件。
函数open、read、write、lseek以及close提供了不带缓冲的I/O。这些函数都使用文件描述符。
实例
如果愿意从标准输入读,并向标准输出写,则图1-4中所示的程序可用于复制任一UNIX普通文件。
#include"apue.h"
#define BUFFSIZE 4096
int
main(void)
{
int n;
char buf[BUFFSIZE];
while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
if (write(STDOUT_FILENO, buf, n) !=n)
err_sys("writeerror");
if(n < 0)
err_sys("read error");
exit(0);
}
头文件<unistd.h>(apue.h中包含了此头文件)及两个常量STDIN_FILENO和STDOUT_FILENO是POSIX标准的一部分(下一章将对此做更多的说明)。头文件<unistd.h>包含了很多UNIX系统服务的函数原型,例如图1-4程序中调用的read和write。
两个常量STDIN_FILENO和STDOUT_FILENO定义在<unistd.h>头文件中,它们指定了标准输入和标准输出的文件描述符。在POSIX标准中,它们的值分别是0和1,但是考虑到可读性,我们将使用这些名字来表示这些常量。
3.9节将详细讨论BUFFSIZE常量,说明它的各种不同值将如何影响程序的效率。但是不管该常量的值如何,此程序总能复制任一UNIX普通文件。
read函数返回读取的字节数,此值用作要写的字节数。当到达输入文件的尾端时,read返回0,程序停止执行。如果发生了一个读错误,read返回−1。出错时大多数系统函数返回−1。
如果将该程序编译成标准名称的a.out文件,并以下列方式执行它:
./a.out > data
那么标准输入是终端,标准输出则重新定向至文件data,标准错误也是终端。如果此输出文件并不存在,则shell会创建它。该程序将用户键入的各行复制到标准输出,键入文件结束符(通常是Ctrl+D)时,将终止本次复制。
若以下列方式执行该程序:
./a.out< infile > outfile
会将名为infile文件的内容复制到名为outfile的文件中。
第3章将更详细地说明不带缓冲的I/O函数。
标准I/O函数为那些不带缓冲的I/O函数提供了一个带缓冲的接口。使用标准I/O函数无需担心如何选取最佳的缓冲区大小,如图1-4中的BUFFSIZE常量的大小。使用标准I/O函数还简化了对输入行的处理(常常发生在UNIX的应用程序中)。例如,fgets函数读取一个完整的行,而read函数读取指定字节数。在5.4节中我们将了解到,标准I/O函数库提供了使我们能够控制该库所使用的缓冲风格的函数。
我们最熟悉的标准I/O函数是printf。在调用printf的程序中,总是包含<stdio.h>(在本书中,该头文件包含在apue.h中),该头文件包括了所有标准I/O函数的原型。
实例
图1-5程序的功能类似于前一个调用了read和write的程序,5.8节将对此程序进行更详细的说明。它将标准输入复制到标准输出,也就能复制任一UNIX普通文件。
#include"apue.h"
int
main(void)
{
int c;
while ((c = getc(stdin)) != EOF)
if (putc(c, stdout) == EOF)
err_sys("outputerror");
if (ferror(stdin))
err_sys("input error");
exit(0);
}
函数getc一次读取一个字符,然后函数putc将此字符写到标准输出。读到输入的最后一个字节时,getc返回常量EOF(该常量在<stdio.h>中定义)。标准I/O常量stdin和stdout也在头文件<stdio.h>中定义,它们分别表示标准输入和标准输出。
程序(program)是一个存储在磁盘上某个目录中的可执行文件。内核使用exec函数(7个exec函数之一),将程序读入内存,并执行程序。8.10节将说明这些exec函数。
程序的执行实例被称为进程(process)。本书的每一页几乎都会使用这一术语。某些操作系统用任务(task)表示正在被执行的程序。
UNIX系统确保每个进程都有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一个非负整数。
实例
图1-6程序用于打印进程ID。
#include"apue.h"
int
main(void)
{
printf("hello world from process ID%ld\n", (long)getpid());
exit(0);
}
如果将该程序编译成a.out文件,然后执行它,则有:
$ ./a.out
helloworld from process ID 851
$ ./a.out
helloworld from process ID 854
此程序运行时,它调用函数getpid得到其进程ID。我们将会在后面看到,getpid返回一个pid_t数据类型。我们不知道它的大小,仅知道的是标准会保证它能保存在一个长整型中。因为我们必须在printf函数中指定需要打印的每一个变量的大小,所以我们必须把它的值强制转换为它可能会用到的最大的数据类型(这里是长整型)。虽然大多数进程ID可以用整型表示,但用长整型可以提高可移植性。
有3个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有7种变体,但经常把它们统称为exec函数。)
实例
UNIX系统的进程控制功能可以用一个简单的程序说明(见图1-7)。该程序从标准输入读取命令,然后执行这些命令。它类似于shell程序的基本实施部分。
#include"apue.h"
#include<sys/wait.h>
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
printf("%% "); /* print prompt (printf requires %% toprint %) */
while (fgets(buf, MAXLINE, stdin) != NULL){
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0;/* replace newline with null */
if ((pid = fork()) < 0) {
err_sys("forkerror");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s",buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) <0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
在这个30行的程序中,有很多功能需要考虑。
用标准I/O函数fgets从标准输入一次读取一行。当键入文件结束符(通常是Ctrl+D)作为行的第一个字符时,fgets返回一个null指针,于是循环停止,进程也就终止。第18章将说明所有特殊的终端字符(文件结束、退格字符、整行擦除等),以及如何改变它们。
因为fgets返回的每一行都以换行符终止,后随一个null字节,因此用标准C函数strlen计算此字符串的长度,然后用一个null字节替换换行符。这样做是因为execlp函数要求的参数是以null结束的而不是以换行符结束的。
调用fork创建一个新进程。新进程是调用进程的一个副本,我们称调用进程为父进程,新创建的进程为子进程。fork对父进程返回新的子进程的进程ID(一个非负整数),对子进程则返回0。因为fork创建一个新进程,所以说它被调用一次(由父进程),但返回两次(分别在父进程中和在子进程中)。
在子进程中,调用execlp以执行从标准输入读入的命令。这就用新的程序文件替换了子进程原先执行的程序文件。fork和跟随其后的exec两者的组合就是某些操作系统所称的产生(spawn)一个新进程。在UNIX系统中,这两部分分离成两个独立的函数。第8章将对这些函数进行更多说明。
子进程调用execlp执行新程序文件,而父进程希望等待子进程终止,这是通过调用waitpid实现的,其参数指定要等待的进程(即pid参数是子进程ID)。waitpid函数返回子进程的终止状态(status变量)。在我们这个简单的程序中,没有使用该值。如果需要,可以用此值准确地判定子进程是如何终止的。
该程序的最主要限制是不能向所执行的命令传递参数。例如不能指定要列出目录项的目录名,只能对工作目录执行ls命令。为了传递参数,先要分析输入行,然后用某种约定把参数分开(可能使用空格或制表符),再将分隔后的各个参数传递给execlp函数。尽管如此,此程序仍可用来说明UNIX系统的进程控制功能。
如果运行此程序,将得到下列结果。注意,该程序使用了一个不同的提示符(%),以区别于shell的提示符。
$ ./a.out
% date
SatJan 21 19:42:07 EST 2012
% who
sar console Jan 1 14:59
sar ttys000 Jan 1 14:59
sar ttys001 Jan 15 15:28
% pwd
/home/sar/bk/apue/3e
% ls
Makefile
a.out
shell1.c
% ^D 键入文件结束符
$ 常规的shell提示符
^D表示一个控制字符。控制字符是特殊字符,其构成方法是:在键盘上按下控制键——通常被标记为Control或Ctrl,同时按另一个键。Ctrl+D或^D是默认的文件结束符。在第18章中讨论终端I/O时,会介绍更多的控制字符。
通常,一个进程只有一个控制线程(thread)—某一时刻执行的一组机器指令。对于某些问题,如果有多个控制线程分别作用于它的不同部分,那么解决起来就容易得多。另外,多个控制线程也可以充分利用多处理器系统的并行能力。
一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一存储区,所以各线程在访问共享数据时需要采取同步措施以避免不一致性。
与进程相同,线程也用ID标识。但是,线程ID只在它所属的进程内起作用。一个进程中的线程ID在另一个进程中没有意义。当在一进程中对某个特定线程进行处理时,我们可以使用该线程的ID引用它。
控制线程的函数与控制进程的函数类似,但另有一套。线程模型是在进程模型建立很久之后才被引入到UNIX系统中的,然而这两种模型之间存在复杂的交互,在第12章中,我们会对此进行说明。
当UNIX系统函数出错时,通常会返回一个负值,而且整型变量errno通常被设置为具有特定信息的值。例如,open函数如果成功执行则返回一个非负文件描述符,如出错则返回−1。在open出错时,有大约15种不同的errno值(文件不存在、权限问题等)。而有些函数对于出错则使用另一种约定而不是返回负值。例如,大多数返回指向对象指针的函数,在出错时会返回一个null指针。
文件<errno.h>中定义了errno以及可以赋与它的各种常量。这些常量都以字符E开头。另外,UNIX系统手册第2部分的第1页,intro(2)列出了所有这些出错常量。例如,若errno等于常量EACCES,表示产生了权限问题(例如,没有足够的权限打开请求文件)。
在Linux中,出错常量在errno(3)手册页中列出。
POSIX和ISO C将errno定义为一个符号,它扩展成为一个可修改的整形左值(lvalue)。它可以是一个包含出错编号的整数,也可以是一个返回出错编号指针的函数。以前使用的定义是:
externint errno;
但是在支持线程的环境中,多个线程共享进程地址空间,每个线程都有属于它自己的局部errno以避免一个线程干扰另一个线程。例如,Linux支持多线程存取errno,将其定义为:
extern int *__errno_location(void);
#define errno (*__errno_location())
对于errno应当注意两条规则。第一条规则是:如果没有出错,其值不会被例程清除。因此,仅当函数的返回值指明出错时,才检验其值。第二条规则是:任何函数都不会将errno值设置为0,而且在<errno.h>中定义的所有常量都不为0。
C标准定义了两个函数,它们用于打印出错信息。
#include <string.h> char *strerror(int errnum); 返回值:指向消息字符串的指针 |
strerror函数将errnum(通常就是errno值)映射为一个出错消息字符串,并且返回此字符串的指针。
perror函数基于errno的当前值,在标准错误上产生一条出错消息,然后返回。
#include <stdio.h> void perror(const char *msg); |
它首先输出由msg指向的字符串,然后是一个冒号,一个空格,接着是对应于errno值的出错消息,最后是一个换行符。
实例
图1-8程序显示了这两个出错函数的使用方法。
#include"apue.h"
#include<errno.h>
int
main(int argc,char *argv[])
{
fprintf(stderr,"EACCES: %s\n", strerror(EACCES));
errno = ENOENT;
perror(argv[0]);
exit(0);
}
如果将此程序编译成文件a.out,然后执行它,则有
$ ./a.out
EACCES:Permission denied
./a.out: No such file or directory
注意,我们将程序名(argv[0],其值是./a.out)作为参数传递给perror。这是一个标准的UNIX惯例。使用这种方法,在程序作为管道的一部分执行时,例如:
prog1< inputfile | prog2 | prog3 > outputfile
我们就能分清3个程序中的哪一个产生了一条特定的出错消息。
本书中的所有实例基本上都不直接调用strerror或perror,而是使用附录B中的出错函数。该附录中的出错函数使我们只用一条C语句就可利用ISO C的可变参数表功能处理出错情况。
可将在<errno.h>中定义的各种出错分成两类:致命性的和非致命性的。对于致命性的错误,无法执行恢复动作。最多能做的是在用户屏幕上打印出一条出错消息或者将一条出错消息写入日志文件中,然后退出。对于非致命性的出错,有时可以较妥善地进行处理。大多数非致命性出错是暂时的(如资源短缺),当系统中的活动较少时,这种出错很可能不会发生。
与资源相关的非致命性出错包括:EAGAIN、ENFILE、ENOBUFS、ENOLCK、ENOSPC、EWOULDBLOCK,有时ENOMEM也是非致命性出错。当EBUSY指明共享资源正在使用时,也可将它作为非致命性出错处理。当EINTR中断一个慢速系统调用时,可将它作为非致命性出错处理(在10.5节对此会进行更多说明)。
对于资源相关的非致命性出错的典型恢复操作是延迟一段时间,然后重试。这种技术可应用于其他情况。例如,假设出错表明一个网络连接不再起作用,那么应用程序可以采用这种方法,在短时间延迟后,尝试重建该连接。一些应用使用指数补偿算法,在每次迭代中等待更长时间。
最终,由应用的开发者决定在哪些情况下应用程序可以从出错中恢复。如果能够采用一种合理的恢复策略,那么可以避免应用程序异常终止,进而就能改善应用程序的健壮性。
口令文件登录项中的用户ID(user ID)是一个数值,它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同时,确定其用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户ID。下面将介绍内核如何使用用户ID来检验该用户是否有执行某些操作的权限。
用户ID为0的用户为根用户(root)或超级用户(superuser)。在口令文件中,通常有一个登录项,其登录名为root,我们称这种用户的特权为超级用户特权。我们将在第4章中看到,如果一个进程具有超级用户特权,则大多数文件权限检查都不再进行。某些操作系统功能只向超级用户提供,超级用户对系统有自由的支配权。
Mac OS X客户端版本交由用户使用时,禁用超级用户账户,服务器版本则可使用该账户。在Apple的网站可以找到使用说明,它告知如何才能使用该账户。参见http://support. apple.com/kb/HT1528。
2.组ID
口令文件登录项也包括用户的组ID(group ID),它是一个数值。组ID也是由系统管理员在指定用户登录名时分配的。一般来说,在口令文件中有多个登录项具有相同的组ID。组被用于将若干用户集合到项目或部门中去。这种机制允许同组的各个成员之间共享资源(如文件)。4.5节将介绍可以通过设置文件的权限使组内所有成员都能访问该文件,而组外用户不能访问。
组文件将组名映射为数值的组ID。组文件通常是/etc/group。
使用数值的用户ID和数值的组ID设置权限是历史上形成的。对于磁盘上的每个文件,文件系统都存储该文件所有者的用户ID和组ID。存储这两个值只需4个字节(假定每个都以双字节的整型值存放)。如果使用完整ASCII登录名和组名,则需更多的磁盘空间。另外,在检验权限期间,比较字符串较之比较整型数更消耗时间。
但是对于用户而言,使用名字比使用数值方便,所以口令文件包含了登录名和用户ID之间的映射关系,而组文件则包含了组名和组ID之间的映射关系。例如,ls -l命令使用口令文件将数值的用户ID映射为登录名,从而打印出文件所有者的登录名。
早期的UNIX系统使用16位整型数表示用户ID和组ID。现今的UNIX系统使用32位整型数表示用户ID和组ID。
实例
图1-9程序用于打印用户ID和组ID。
#include"apue.h"
int
main(void)
{
printf("uid = %d, gid = %d\n",getuid(), getgid());
exit(0);
}
程序调用getuid和getgid以返回用户ID和组ID。运行该程序的结果如下:
$ ./a.out
uid = 205, gid = 105
3.附属组ID
除了在口令文件中对一个登录名指定一个组ID外,大多数 UNIX系统版本还允许一个用户属于另外一些组。这一功能是从4.2BSD开始的,它允许一个用户属于多至16个其他的组。登录时,读文件/etc/group,寻找列有该用户作为其成员的前16个记录项就可以得到该用户的附属组ID(supplementary group ID)。在下一章将说明,POSIX要求系统至少应支持8个附属组,实际上大多数系统至少支持16个附属组。
信号(signal)用于通知进程发生了某种情况。例如,若某一进程执行除法操作,其除数为0,则将名为SIGFPE(浮点异常)的信号发送给该进程。进程有以下3种处理信号的方式。
(1)忽略信号。有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的存储单元等,因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。
(2)按系统默认方式处理。对于除数为0,系统默认方式是终止该进程。
(3)提供一个函数,信号发生时调用该函数,这被称为捕捉该信号。通过提供自编的函数,我们就能知道什么时候产生了信号,并按期望的方式处理它。
很多情况都会产生信号。终端键盘上有两种产生信号的方法,分别称为中断键(interruptkey,通常是Delete键或Ctrl+C)和退出键(quit key,通常是Ctrl+\),它们被用于中断当前运行的进程。另一种产生信号的方法是调用kill函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当然这样做也有些限制:当向一个进程发送信号时,我们必须是那个进程的所有者或者是超级用户。
实例
回忆一下基本的shell实例(见图1-7程序)。如果调用此程序,然后按下中断键,则执行此程序的进程终止。产生这种后果的原因是:对于此信号(SIGINT)的系统默认动作是终止进程。该进程没有告诉系统内核应该如何处理此信号,所以系统按默认方式终止该进程。
为了能捕捉到此信号,程序需要调用signal函数,其中指定了当产生SIGINT信号时要调用的函数的名字。函数名为sig_int,当其被调用时,只是打印一条消息,然后打印一个新提示符。在图1-7程序中添加了11行,构成了图1-10程序(添加的11行以行首的+号指示)。
#include "apue.h"
#include <sys/wait.h>
+ static voidsig_int(int); /* our signal-catchingfunction */
+
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
+ if(signal(SIGINT, sig_int) == SIG_ERR)
+ err_sys("signal error");
+
printf("%%"); /* print prompt (printf requires %% to print %) */
while(fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replacenewline with null */
if ((pid = fork()) < 0) {
err_sys("forkerror");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn'texecute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0))< 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
+
+ void
+ sig_int(intsigno)
+ {
+ printf("interrupt\n%%");
+ }
因为大多数重要的应用程序都对信号进行处理,所以第10章将详细介绍信号。
历史上,UNIX系统使用过两种不同的时间值。
(1)日历时间。该值是自协调世界时(Coordinated Universal Time,UTC)1970年1月1日00:00:00这个特定时间以来所经过的秒数累计值(早期的手册称UTC为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改时间等。
系统基本数据类型time_t用于保存这种时间值。
(2)进程时间。也被称为CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为50、60或100个时钟滴答。
系统基本数据类型clock_t保存这种时间值。2.5.4节将说明如何用sysconf函数得到每秒的时钟滴答数。
当度量一个进程的执行时间时(见3.9节),UNIX系统为一个进程维护了3个进程时间值:
· 时钟时间;
· 用户CPU时间;
· 系统CPU时间。
时钟时间又称为墙上时钟时间(wall clock time),它是进程运行的时间总量,其值与系统中同时运行的进程数有关。每当在本书中提到时钟时间时,都是在系统中没有其他活动时进行度量的。
用户CPU时间是执行用户指令所用的时间量。系统CPU时间是为该进程执行内核程序所经历的时间。例如,每当一个进程执行一个系统服务时,如read或write,在内核内执行该服务所花费的时间就计入该进程的系统CPU时间。用户CPU时间和系统CPU时间之和常被称为CPU时间。
要取得任一进程的时钟时间、用户时间和系统时间是很容易的—只要执行命令time(1),其参数是要度量其执行时间的命令,例如:
$ cd /usr/include
$ time -p grep_POSIX_SOURCE */*.h > /dev/null
real om0.81s
user om0.11s
sys om0.07s
time命令的输出格式与所使用的shell有关,其原因是某些shell并不运行/usr/bin/time,而是使用一个内置函数测量命令运行所使用的时间。
8.17节将说明一个运行进程如何取得这3个时间。关于时间和日期的一般说明见6.10节。
所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call,见图1-1)。ResearchUNIX系统第7版提供了约50个系统调用,4.4BSD提供了约110个系统调用,而SVR4则提供了约120个系统调用。具体数字在不同操作系统版本中会不同,新近的大多数系统大大增加了支持的系统调用的个数。Linux 3.2.0提供了380个系统调用,FreeBSD8.0提供的系统调用超过450个。
系统调用接口总是在《UNIX程序员手册》的第2部分中说明,是用C语言定义的,与具体系统如何调用一个系统调用的实现技术无关。这与很多早期的操作系统不同,那些系统按传统方式用机器的汇编语言定义内核入口点。
UNIX所使用的技术是为每个系统调用在标准C库中设置一个具有同样名字的函数。用户进程用标准C调用序列来调用这些函数,然后,函数又用系统所要求的技术调用相应的内核服务。例如,函数可将一个或多个C参数送入通用寄存器,然后执行某个产生软中断进入内核的机器指令。从应用角度考虑,可将系统调用视为C函数。
《UNIX程序员手册》的第3部分定义了程序员可以使用的通用库函数。虽然这些函数可能会调用一个或多个内核的系统调用,但是它们并不是内核的入口点。例如,printf函数会调用write系统调用以输出一个字符串,但函数strcpy(复制一个字符串)和atoi(将ASCII转换为整数)并不使用任何内核的系统调用。
从实现者的角度来看,系统调用和库函数之间有根本的区别,但从用户角度来看,其区别并不重要。在本书中,系统调用和库函数都以C函数的形式出现,两者都为应用程序提供服务。但是,我们应当理解,如果希望的话,我们可以替换库函数,但是系统调用通常是不能被替换的。
以存储空间分配函数malloc为例。有多种方法可以进行存储空间分配及与其相关的无用空间回收操作(最佳适应、首次适应等),并不存在对所有程序都最优的一种技术。UNIX系统调用中处理存储空间分配的是sbrk(2),它不是一个通用的存储器管理器。它按指定字节数增加或减少进程地址空间。如何管理该地址空间却取决于进程。存储空间分配函数malloc(3)实现一种特定类型的分配。如果我们不喜欢其操作方式,则可以定义自己的malloc函数,它很可能将使用sbrk系统调用。事实上,有很多软件包,它们使用sbrk系统调用实现自己的存储空间分配算法。图1-11显示了应用程序、malloc函数以及sbrk系统调用之间的关系。
从中可见,两者职责不同,内核中的系统调用分配一块空间给进程,而库函数malloc则在用户层次管理这一空间。
另一个可说明系统调用和库函数之间差别的例子是,UNIX系统提供的判断当前时间和日期的接口。一些操作系统分别提供了一个返回时间的系统调用和另一个返回日期的系统调用。任何特殊的处理,例如正常时制和夏令时之间的转换,由内核处理或要求人为干预。UNIX系统则不同,它只提供一个系统调用,该系统调用返回自协调世界时1970年1月1日零时这个特定时间以来所经过的秒数。对该值的任何解释,例如将其变换成人们可读的、适用于本地时区的时间和日期,都留给用户进程进行处理。在标准C库中,提供了若干例程以处理大多数情况。这些库函数处理各种细节,如各种夏令时算法等。
应用程序既可以调用系统调用也可以调用库函数。很多库函数则会调用系统调用。图1-12显示了这种差别。
系统调用和库函数之间的另一个差别是:系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。我们从sbrk系统调用和malloc库函数之间的差别中可以看到这一点。当我们比较不带缓冲的I/O函数(见第3章)和标准I/O函数(见第5章)时,还将看到这种差别。
进程控制系统调用(fork、exec和wait)通常由用户应用程序直接调用(请回忆图1-7中的基本shell)。但是为了简化某些常见的情况,UNIX系统也提供了一些库函数,如system和popen。8.13节将说明system函数的一种实现,它使用基本的进程控制系统调用。10.18节还将强化这一实例以正确地处理信号。
为使读者了解大多数程序员应用的UNIX系统接口,我们不得不既说明系统调用,又介绍某些库函数。例如,若只描述sbrk系统调用,那么就会忽略很多应用程序使用的malloc库函数。本书除了必须要区分两者时,对系统调用和库函数都使用函数(function)这一术语来表示。
本章快速浏览了UNIX系统。说明了某些以后会多次用到的基本术语,介绍了一些小的UNIX程序实例。读者可以从中大概了解到本书其余部分将要介绍的内容。