文章目录
- 引言
- UNIX体系结构
- 登录
- 登录名
- shell
- 文件和目录
- 文件系统
- 文件名
- 路径名
- 工作目录
- 起始目录
- 输入和输出
- 文件描述符
- 标准输入、标准输出和标准错误
- 不带缓冲的IO
- 标准IO
- 程序和进程
- 程序
- 进程和进程ID
- 进程控制
- 线程和线程ID
- 出错处理
- 出错恢复
- 用户标识
- 用户ID
- 组ID
- 附属组ID
- 信号
- 时间值
- 系统调用和库函数
- 概述
- 区别一
- 例一
- 例二
- 小结
- 区别二
引言
所有操作系统都为它们所运行的程序提供服务。典型的服务包括:
- 执行新程序、
- 打开文件、
- 读文件、
- 分配存储区
- 获得当前时间,
- …
本书集中阐述不同版本的UNIX操作系统所提供的服务。
(MyNote:将专注于Linux的。)
本章从程序员的角度快速浏览UNIX,对书中引用的一些术语和概念进行简要的说明并给出实例。在以后各章中,将对这些概念做更详细的说明。
对于初涉UNIX环境的程序员,本章还简要介绍了UNIX提供的各种服务。
UNIX体系结构
从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kernel),因为它相对较小,而且位于环境的核心。下图显示了UNIX 系统的体系结构。
- 内核的接口被称为系统调用(system call,图中的阴影部分)。
- 公用函数库构建在系统调用接口之上,应用程序既可使用公用函数库,也可使用系统调用。
- 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:Stephen Rago:/home/sar:/bin/ksh
目前,所有的系统已将加密口令移到另一个文件中。第6章将说明这种文件以及访问它们的函数。
etc:Editable Text Configuration
shell
用户登录后,系统通常先显示一些系统信息,然后用户就可以向shell程序键入命令。(当用户登录时,某些系统启动一个视窗管理程序,但最终总会有一个shell程序运行在一个视窗中)。
shell是一个命令行解释器,它读取用户输入,然后执行命令。shell 的用户输入通常来自于终端(交互式shell),有时则来自于文件(称为shell脚本)。
下表总结了UNIX系统中常见的shell。
Name | Path | Linux 3.2.0 |
---|---|---|
Bourne shell | /bin/sh | • |
Bourne-again shell | /bin/bash | • |
C shell | /bin/csh | optional |
Korn shell | /bin/ksh | optional |
TENEX C shell | /bin/tcsh | optional |
文件和目录
文件系统
UNIX文件系统是目录和文件的一种层次结构,所有东西的起点是称为根(root)的目录,这个目录的名称是一个字符“/”。
目录(directory)是一个包含目录项的文件。在逻辑上,可以认为每个目录项都包含:
-
一个文件名,
-
说明该文件属性的信息
-
文件类型(是普通文件还是目录等)、
-
文件大小、
-
文件所有者、
-
文件权限(其他用户能否访问该文件)
-
文件最后的修改时间
-
…
stat和fstat函数返回包含所有文件属性的一个信息结构。
第4章将详细说明文件的各种属性。
文件名
目录中的各个名字称为文件名(filename)。只有斜线(/)和空字符这两个字符不能出现在文件名中。斜线用来分隔构成路径名的各文件名,空字符则用来终止一个路径名。尽管如此,好的习惯还是只使用常用印刷字符的一个子集作为文件名字符(如果在文件名中使用了某些shell的特殊字符,则必须使用shell的引号机制来引用文件名,这会带来很多麻烦)。
事实上,为了可移植性,POSIX.1推荐将文件名限制在以下字符集之内:字母(a ~ z、A ~ Z)、数字(0 ~ 9)、句点(.)、短横线(-)和下划线(_)。
创建新目录时会自动创建了两个文件名:
.
(称为点)点指向当前目录..
(称为点点)点点指向父目录。
在最高层次的根目录中,点点与点相同。
注意:空字符(‘\0’)和空格符(’ ')是分别不同的字符。
路径名
由斜线分隔的一个或多个文件名组成的序列(也可以斜线开头)构成路径名(pathname),以斜线开头的路径名称为绝对路径名(absolute pathname)否则称为相对路径名(relative pathname)。相对路径名指向相对于当前目录的文件。
文件系统根的名字(/)是一个特殊的绝对路径名,它不包含文件名。
实例:ls命令的简要实现
#include "apue.h"//1.
#include <dirent.h>//2. 为了使用opendir和readdir的函数原型,以及 dirent 结构的定义int
main(int argc, char *argv[])//3.4.
{DIR *dp;//6.struct dirent *dirp;if (argc != 2)err_quit("usage: ls directory_name");if ((dp = opendir(argv[1])) == NULL)//5.6.err_sys("can't open %s", argv[1]);//7.while ((dirp = readdir(dp)) != NULL)//5.6.printf("%s\n", dirp->d_name);closedir(dp);//5.exit(0);//8.
}
这个程序中,有很多细节需要考虑:
-
首先,其中包含了一个头文件apue.h。本书中几乎每一个程序都包含此头文件。它包含了某些标准系统头文件,定义了许多常量及函数原型,这些都将用于本书的各个实例中,附录B列出了这一头文件源码。
-
接下来,我们包含了一个系统头文件dirent.h,以便使用opendir和readdir的函数原型,以及 dirent 结构的定义。在其他一些系统里,这些定义被分成多个头文件。比如,在Ubuntu 12.04中,/usr/include/dirent.h声明了函数原型,并且包含bits/dirent.h,后者定义了dirent结构(真正存放在/usrlinclude/x86_64-linux-gnu/bits 下)。(MyNote:CentOS的存在/usr/include/dirent.h)
-
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章)以获得该文件的所有属性。
-
程序调用了两个自编的函数对错误进行处理:err_sys 和err_quit。如果用户无权限访问该目录或目录不存在,err_sys 函数打印一条消息(“Permission denied”或“Not a directory”),说明遇到了什么类型的错误。这两个出错处理函数在附录B中说明,随后将更多地叙述出错处理。
-
当程序将结束时,它以参数0调用函数exit。函数exit终止程序。按惯例,参数0的意思是正常结束,参数值1~255则表示出错。第8章将说明一个程序(如 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),该起始目录从口令文件(/etc/passwd)中相应用户的登录项中取得。
输入和输出
文件描述符
文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。在读、写文件时,可以使用这个文件描述符。
标准输入、标准输出和标准错误
按惯例,每当运行一个新程序时,所有的shell都为其打开3个文件描述符,即标准输入(standard input)、标准输出(standard output)以及标准错误(standard error)。
如果不做特殊处理,例如就像简单的命令ls,则这3个描述符都链接向终端。大多数shell都提供一种方法,使其中任何一个或所有这3个描述符都能重新定向到某个文件。例如:
ls > file.list
执行ls命令,其标准输出重新定向到名为file.list的文件。
不带缓冲的IO
函数open、read、write、lseek 以及close提供了不带缓冲的IO。这些函数都使用文件描述符。
实例:如果愿意从标准输入读,并向标准输出写,下面程序可用于复制任一UNIX普通文件。
#include "apue.h"//1.#define BUFFSIZE 4096int
main(void)
{int n;char buf[BUFFSIZE];//3.while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)//1.2.3if (write(STDOUT_FILENO, buf, n) != n)//1.2.err_sys("write error");if (n < 0)err_sys("read error");exit(0);
}
-
头文件<unistd.h> (apue.h中包含了此头文件)及两个常量STDIN_FILENO和STDOUT.FILENO是 POSTX标准的一部分(下一章将对此做更多的说明)。头文件<unistd.h>包含了很多UNIX系统服务的函数原型,例如上例程序中调用的read和write。
-
两个常量STDIN_FILENO和STDOUT_FILENO定义在<unistd.h>头文件中,它们指定了标准输入和标准输出的文件描述符。在POSIX标准中,它们的值分别是0和1,但是考虑到可读性,我们将使用这些名字来表示这些常量。
-
第3章将详细讨论BUFFSIZE常量,说明它的各种不同值将如何影响程序的效率。但是不管该常量的值如何,此程序总能复制任一UNIX普通文件。
-
read函数返回读取的字节数,此值用作要写的字节数。当到达输入文件的尾端时,read返回0,程序停止执行。如果发生了一个读错误,read返回-1。出错时大多数系统函数返回-1。
如果将该程序编译成标准名称的a.out文件,并以下列方式执行它:
./a.out > data
那么标准输入是终端,标准输出则重新定向至文件 data,标准错误也是终端。如果此输出文件并不存在,则shell 会创建它。该程序将用户键入的各行复制到标准输出,键入文件结束符(通常是Ctrl+D)时,将终止本次复制。
若以下列方式执行该程序:
./ a.out < infile > outfile
会将名为infile文件的内容复制到名为outfile的文件中。
第3章将更详细地说明不带缓冲的IO函数。
标准IO
标准IO函数为那些不带缓冲的IO函数提供了一个带缓冲的接口。使用标准I/O函数无需担心如何选取最佳的缓冲区大小。使用标准IO函数还简化了对输入行的处理(常常发生在UNIX的应用程序中)。例如,fgets函数读取一个完整的行,而read函数读取指定字节数。
在第5章中我们将了解到,标准IO函数库提供了使我们能够控制该库所使用的缓冲风格的函数。
我们最熟悉的标准IO函数是printf。在调用printf 的程序中,总是包含<stdio.h>(在本书中,该头文件包含在apue.h中),该头文件包括了所有标准IO函数的原型。
实例:下面程序的功能类似于前一个调用了read和 write的程序,第5章将对此程序进行更详细的说明。它将标准输入复制到标准输出,也就能复制任一UNIX普通文件。
#include "apue.h"int
main(void)
{int c;while ((c = getc(stdin)) != EOF)if (putc(c, stdout) == EOF)err_sys("output error");if (ferror(stdin))err_sys("input error");exit(0);
}
函数getc一次读取一个字符,然后函数putc将此字符写到标准输出。读到输入的最后一个字节时,getc返回常量EOF(该常量在<stdio.h>中定义)。标准IO常量stdin和 stdout也在头文件<stdio.h>中定义,它们分别表示标准输入和标准输出。
程序和进程
程序
程序( program)是一个存储在磁盘上某个目录中的可执行文件。内核使用exec函数(7个exec函数之一),将程序读入内存,并执行程序。第8章将说明这些exec函数。
进程和进程ID
程序的执行实例被称为进程( process)。本书的每一页几乎都会使用这一术语。某些操作系统用任务( task)表示正在被执行的程序。
UNIX系统确保每个进程都有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一个非负整数。
#include "apue.h"int
main(void)
{printf("hello world from process ID %ld\n", (long)getpid());exit(0);
}
此程序运行时,它调用函数 getpid 得到其进程ID。我们将会在后面看到,getpid返回一个pid_t数据类型。我们不知道它的大小,仅知道的是标准会保证它能保存在一个长整型中。因为我们必须在printf函数中指定需要打印的每一个变量的大小,所以我们必须把它的值强制转换为它可能会用到的最大的数据类型(这里是长整型)。
虽然大多数进程ID可以用整型表示,但用长整型可以提高可移植性。
进程控制
有3个用于进程控制的主要函数:fork、exec和waitpid。(exec函数有7种变体,但经常把它们统称为exec函数。)
实例:UNIX系统的进程控制功能可以用一个简单的程序说明。该程序从标准输入读取命令,然后执行这些命令。它类似于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 %% to print %) */while (fgets(buf, MAXLINE, stdin) != NULL) {//1.if (buf[strlen(buf) - 1] == '\n')//2.buf[strlen(buf) - 1] = 0; /* replace newline with null */if ((pid = fork()) < 0) {//3.err_sys("fork error");} else if (pid == 0) { /* child */execlp(buf, buf, (char *)0);//4.err_ret("couldn't execute: %s", buf);exit(127);}/* parent */if ((pid = waitpid(pid, &status, 0)) < 0)//5.err_sys("waitpid error");printf("%% ");}exit(0);
}
实验过程:
[jallen@localhost intro]$ ./shell1
% date
2021年 05月 10日 星期一 12:21:54 PDT
% ls
getcputc hello.c Makefile shell1 shell2.c uidgid
getcputc.c ls1 mycat shell1.c testerror uidgid.c
hello ls1.c mycat.c shell2 testerror.c
% pwd
/home/jallen/Desktop/apue.3e/intro
% who
jallen tty1 2021-05-10 07:19 (:0)
jallen pts/0 2021-05-10 07:58 (:0.0)
% [jallen@localhost intro]$ ^C
[jallen@localhost intro]$
-
用标准IO函数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系统的进程控制功能。
线程和线程ID
通常,一个进程只有一个控制线程(thread)——某一时刻执行的一组机器指令。对于某些问题,如果有多个控制线程分别作用于它的不同部分,那么解决起来就容易得多。另外,多个控制线程也可以充分利用多处理器系统的并行能力(MyNote:多线程优势)。
一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一存储区,所以各线程在访问共享数据时需要采取同步措施以避免不一致性(MyNote:利用多线程优势注意事项)。
与进程相同,线程也用ID标识。但是,线程ID只在它所属的进程内起作用。一个进程中的线程ID在另一个进程中没有意义。当在一进程中对某个特定线程进行处理时,我们可以使用该线程的ID引用它。
控制线程的函数与控制进程的函数类似,但另有一套。线程模型是在进程模型建立很久之后才被引入到UNIX系统中的,然而这两种模型之间存在复杂的交互,在第12章中,我们会对此进行说明。
出错处理
当UNIX系统函数出错时,通常会返回一个负值,而且整型变量errno通常被设置为具有特定信息的值。例如,open 函数如果成功执行则返回一个非负文件描述符,如出错则返回-1。
在open出错时,有大约15种不同的errno值(文件不存在、权限问题等)。而有些函数对于出错则使用另一种约定而不是返回负值。例如,大多数返回指向对象指针的函数,在出错时会返回一个null指针。
文件<errno.h>中定义了errno以及可以赋与它的各种常量。这些常量都以字符E开头。
man errno
POSIX和ISO C将errno定义为一个符号,它扩展成为一个可修改的整形左值(lvalue)。它可以是一个包含出错编号的整数,也可以是一个返回出错编号指针的函数。以前使用的定义是:
extern int errno;
但是在支持线程的环境中,多个线程共享进程地址空间,每个线程都有属于它自己的局部errno以避免一个线程干扰另一个线程。例如,Linux支持多线程存取errno,将其定义为:
extern int *__errno_location (void);
#define errno (*_errno_1ocation())
(MyNote:不懂具体怎么存取。)
对于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值的出错消息,最后是一个换行符。
实例:显示了这两个出错函数的使用方法。
#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);
}
输出结果
[jallen@localhost intro]$ ./testerror
EACCES: Permission denied
./testerror: No such file or directory
注意,我们将程序名(argv[0],其值是. /a.out)作为参数传递给perror。这是一个标准的UNIX惯例。使用这种方法,在程序作为管道的一部分执行时,例如:
prog1 < inputfile | prog2 | prog3 > outputfile
我们就能分清3个程序中的哪一个产生了一条特定的出错消息。
本书中的所有实例基本上都不直接调用strerror或perror,而是使用附录B中的出错函数。该附录中的出错函数使我们只用一条C语句就可利用ISOC的可变参数表功能处理出错情况。
出错恢复
可将在<errno.h>中定义的各种出错分成两类:
- 致命性的。对于致命性的错误,无法执行恢复动作。最多能做的是在用户屏幕上打印出一条出错消息或者将一条出错消息写入日志文件中,然后退出。
- 非致命性的。对于非致命性的出错,有时可以较妥善地进行处理。大多数非致命性出错是暂时的(如资源短缺),当系统中的活动较少时,这种出错很可能不会发生。
与资源相关的非致命性出错包括:
- EAGAIN、
- ENFILE、
- ENOBUFS、
- ENOLCK、
- ENOSPC、
- EWOULDBLOCK,
- 有时ENOMEM也是非致命性出错。
- 当EBUSY指明共享资源正在使用时,也可将它作为非致命性出错处理。
- 当EINTR中断一个慢速系统调用时,可将它作为非致命性出错处理(第10章内容)。
对于资源相关的非致命性出错的典型恢复操作是延迟一段时间,然后重试。这种技术可应用于其他情况。例如,假设出错表明一个网络连接不再起作用,那么应用程序可以采用这种方法,在短时间延迟后,尝试重建该连接。一些应用使用指数补偿算法,在每次迭代中等待更长时间。
最终,由应用的开发者决定在哪些情况下应用程序可以从出错中恢复。如果能够采用一种合理的恢复策略,那么可以避免应用程序异常终止,进而就能改善应用程序的健壮性。
用户标识
用户ID
口令文件登录项中的用户ID (user ID)是一个数值,它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同时,确定其用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户ID。
下面将介绍内核如何使用用户ID来检验该用户是否有执行某些操作的权限。
用户ID为0的用户为根用户(root)或超级用户(superuser)。在口令文件中,通常有一个登录项,其登录名为root,我们称这种用户的特权为超级用户特权。我们将在第4章中看到,如果一个进程具有超级用户特权,则大多数文件权限检查都不再进行。
某些操作系统功能只向超级用户提供,超级用户对系统有自由的支配权。
组ID
口令文件登录项也包括用户的组ID(group ID),它是一个数值。组ID也是由系统管理员在指定用户登录名时分配的。一般来说,在口令文件中有多个登录项具有相同的组ID。组被用于将若干用户集合到项目或部门中去。这种机制允许同组的各个成员之间共享资源(如文件)。第4章将介绍可以通过设置文件的权限使组内所有成员都能访问该文件,而组外用户不能访问。
组文件将组名映射为数值的组ID。组文件通常是/etc/group。
使用数值的用户ID和数值的组ID设置权限是历史上形成的。对于磁盘上的每个文件,文件系统都存储该文件所有者的用户ID和组ID。存储这两个值只需4个字节(假定每个都以双字节的整型值存放)。如果使用完整ASCII登录名和组名,则需更多的磁盘空间。另外,在检验权限期间,比较字符串较之比较整型数更消耗时间。
但是对于用户而言,使用名字比使用数值方便,所以口令文件包含了登录名和用户ID之间的映射关系,而组文件则包含了组名和组D之间的映射关系。例如,ls -l命令使用口令文件将数值的用户ID映射为登录名,从而打印出文件所有者的登录名。
实例:打印用户ID和组ID。
#include "apue.h"int
main(void)
{printf("uid = %d, gid = %d\n", getuid(), getgid());exit(0);
}
附属组ID
除了在口令文件中对一个登录名指定一个组ID外,大多数UNIX系统版本还允许一个用户属于另外一些组。
这一功能是从4.2BSD开始的,它允许一个用户属于多至16个其他的组。登录时,读文件/etc/group,寻找列有该用户作为其成员的前16个记录项就可以得到该用户的附属组ID (supplementary group ID)。
在下一章将说明,POSIX要求系统至少应支持8个附属组,实际上大多数系统至少支持16个附属组。
信号
信号(signal)用于通知进程发生了某种情况。例如,若某一进程执行除法操作,其除数为0则将名为SIGFPE(浮点异常)的信号发送给该进程。进程有以下3种处理信号的方式:
- 忽略信号。有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的存储单元等,因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。
- 按系统默认方式处理。对于除数为0,系统默认方式是终止该进程。
- 提供一个函数,信号发生时调用该函数,这被称为捕捉该信号。通过提供自编的函数,我们就能知道什么时候产生了信号,并按期望的方式处理它。(MyNote:自定义处理)
很多情况都会产生信号。终端键盘上有两种产生信号的方法,它们被用于中断当前运行的进程:分别称为
- 中断键(interrupt key,通常是Delete键或Ctrl+C)
- 退出键(quit key,通常是Ctrl+\)
另一种产生信号的方法是调用kill函数。在一个进程中调用此函数就可向另一个进程发送一个信号。
当然这样做也有些限制:当向一个进程发送信号时,我们必须是那个进程的所有者或者是超级用户。
实例:
回忆一下基本的shell实例(在进程控制章节)。如果调用此程序,然后按下中断键,则执行此程序的进程终止。产生这种后果的原因是:对于此信号(SIGINT)的系统默认动作是终止进程。该进程没有告诉系统内核应该如何处理此信号,所以系统按默认方式终止该进程。
为了能捕捉到此信号,程序需要调用signal函数,其中指定了当产生STGINT信号时要调用的函数的名字。函数名为sig_int,当其被调用时,只是打印一条消息,然后打印一个新提示符。
#include "apue.h"
#include <sys/wait.h>static void sig_int(int); /* our signal-catching function */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; /* replace newline with null */if ((pid = fork()) < 0) {err_sys("fork error");} 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);
}void
sig_int(int signo)
{printf("interrupt\n%% ");
}
实验过程:
[jallen@localhost intro]$ ./shell2
% ls
getcputc hello.c Makefile shell1 shell2.c uidgid
getcputc.c ls1 mycat shell1.c testerror uidgid.c
hello ls1.c mycat.c shell2 testerror.c
% date
2021年 05月 10日 星期一 12:25:06 PDT
% ^Cinterrupt
^C% interrupt
^C% interrupt
date
2021年 05月 10日 星期一 12:25:17 PDT
% % ls
getcputc hello.c Makefile shell1 shell2.c uidgid
getcputc.c ls1 mycat shell1.c testerror uidgid.c
hello ls1.c mycat.c shell2 testerror.c
%
运行程序后,键盘输入Ctrl + D,便会打印“interrupt”,但程序并未结束,还能运行。
在函数的返回类型前加上static,就是静态函数。其特性如下:
- 静态函数只能在声明它的文件中可见,其他文件不能引用该函数
- 不同的文件可以使用相同名字的静态函数,互不影响
Link
时间值
历史上,UNIX系统使用过两种不同的时间值。
-
日历时间。该值是自协调世界时(Coordinated Universal Time,UTC)1970年1月1日00:00:00这个特定时间以来所经过的秒数累计值(早期的手册称UTC为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改时间等。系统基本数据类型time_t用于保存这种时间值。
-
进程时间。也被称为CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为50、60或100个时钟滴答。系统基本数据类型clock_t保存这种时间值。第2章将说明如何用sysconf函数得到每秒的时钟滴答数。
当度量一个进程的执行时间时(第3章),UNIX系统为一个进程维护了3个进程时间值:
- 时钟时间,又称为墙上时钟时间(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 0m0.81s
user 0m0.11s
sys 0m0.07s
time命令的输出格式与所使用的shell有关,其原因是某些shell并不运行/usr/bin/time,而是使用一个内置函数测量命令运行所使用的时间。
第8章将说明一个运行进程如何取得这3个时间。关于时间和日期的一般说明见第6章。
系统调用和库函数
概述
所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call)。Linux 3.2.0提供了380个系统调用,FreeBSD 8.0提供的系统调用超过450个。
系统调用接口总是在《UNIX程序员手册》的第2部分中说明,是用C语言定义的,与具体系统如何调用一个系统调用的实现技术无关。这与很多早期的操作系统不同,那些系统按传统方式用机器的汇编语言定义内核入口点。
UNIX所使用的技术是为每个系统调用在标准C库中设置一个具有同样名字的函数。用户进程用标准C调用序列来调用这些函数,然后,函数又用系统所要求的技术调用相应的内核服务。例如,函数可将一个或多个C参数送入通用寄存器,然后执行某个产生软中断进入内核的机器指令。从应用角度考虑,可将系统调用视为C函数。
《UNIX程序员手册》的第3部分定义了程序员可以使用的通用库函数。虽然这些函数可能会调用一个或多个内核的系统调用,但是它们并不是内核的入口点。例如,printf 函数会调用write系统调用以输出一个字符串,但函数strcpy(复制一个字符串)和atoi(将ASCII转换为整数)并不使用任何内核的系统调用。(MyNote:库函数可能不使用任何内核的系统调用。)
区别一
从实现者的角度来看,系统调用和库函数之间有根本的区别,但从用户角度来看,其区别并不重要。在本书中,系统调用和库函数都以C函数的形式出现,两者都为应用程序提供服务。但是,我们应当理解,如果希望的话,我们可以替换库函数,但是系统调用通常是不能被替换的。
例一
以存储空间分配函数malloc为例。有多种方法可以进行存储空间分配及与其相关的无用空间回收操作(最佳适应、首次适应等),并不存在对所有程序都最优的一种技术。
UNIX系统调用中处理存储空间分配的是sbrk,它不是一个通用的存储器管理器。它按指定字节数增加或减少进程地址空间。如何管理该地址空间却取决于进程。
存储空间分配函数malloc实现一种特定类型的分配。如果我们不喜欢其操作方式,则可以定义自己的malloc函数,它很可能将使用sbrk 系统调用。(malloc可以自定义)
事实上,有很多软件包,它们使用sbrk系统调用实现自己的存储空间分配算法。下图显示了应用程序、malloc函数以及sbrk系统调用之间的关系。
(MyNote:个人理解库函数是应用程序与系统调用之间中间层,可有更多操作空间。)
从中可见,两者职责不同,内核中的系统调用分配一块空间给进程,而库函数malloc则在用户层次管理这一空间。
例二
另一个可说明系统调用和库函数之间差别的例子是,UNIX系统提供的判断当前时间和日期的接口。
一些操作系统分别提供了一个返回时间的系统调用和另一个返回日期的系统调用。任何特殊的处理,例如正常时制和夏令时之间的转换,由内核处理或要求人为干预。UNIX系统则不同,它只提供一个系统调用,该系统调用返回自协调世界时1970年1月1日零时这个特定时间以来所经过的秒数。
对该值的任何解释,例如将其变换成人们可读的、适用于本地时区的时间和日期,都留给用户进程进行处理。在标准C库中,提供了若干例程以处理大多数情况。这些库函数处理各种细节,如各种夏令时算法等。
小结
应用程序既可以调用系统调用也可以调用库函数。很多库函数则会调用系统调用。下图显示这种差别。
区别二
系统调用和库函数之间的另一个差别是:系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。我们从sbrk系统调用和malloc库函数之间的差别中可以看到这一点。当我们比较不带缓冲的I/O函数(见第3章)和标准IO函数(见第5章)时,还将看到这种差别。
进程控制系统调用(fork、exec和 wait)通常由用户应用程序直接调用。但是为了简化某些常见的情况,UNIX系统也提供了一些库函数,如 system和popen。第8章将说明system函数的一种实现,它使用基本的进程控制系统调用。第10章还将强化这一实例以正确地处理信号。
(MyNote:系统调用简单少功能,库函数复杂多功能。)
为使读者了解大多数程序员应用的UNIX系统接口,我们不得不既说明系统调用,又介绍某些库函数。例如,若只描述sbrk系统调用,那么就会忽略很多应用程序使用的malloc库函数。本书除了必须要区分两者时,对系统调用和库函数都使用函数(function)这一术语来表示。