本文是笔者拜读《UNIX环境高级编程》第8章(进程控制)的学习笔记。本文的主要内容包括进程标识、fork、exit、wait、竞争条件、exec、解释器文件、system、本章习题。
目录
进程标识
每个进程都有一个非负整型表示的唯一进程ID
。
系统中有一些专用进程。ID为0
的进程通常是调度进程(交换进程,系统进程),是内核的一部分。
ID为1
的通常是init
进程,此进程负责在自举内核后启动一个UNIX
系统。init
进程不会终止,它是一个普通的用户进程,但它以超级用户特权运行。
某些系统的进程ID
2
是页守护进程,此进程负责支持虚拟存储器系统的分页操作。
除了进程ID
,每个进程还有一些其它的标识符,下面的函数返回这些标识符:
这些函数都没有出错返回。
函数fork
一个进程可以调用fork
创建一个新进程。
子进程返回0
,父进程返回子进程的ID
,出错返回-1
。
父进程和子进程共享正文段,但各自拥有独立的数据副本。
写时复制:父子进程共享数据,当其中一个试图修改这些区域,内核只为修改区域的那块内存制作一个副本。
如果未刷新缓冲区,子进程也会继承父进程的缓冲区。sizeof
运算是在编译时计算的。
调用了fork
的进程,一种是希望复制自己,一种是希望执行别的进程。
函数vfork
vfork
也用于创建一个新进程,而该新进程的目的是exec
一个新程序,如shell
。
vfork
不将父进程的地址空间完全复制到子进程,在子进程调用exec
或exit
之前,它在父进程的空间中运行(共享代码和数据)。如果子进程修改了数据,则会带来未知的结果。
vfork
保证子进程先运行,在子进程调用exec
或exit
之后,父进程才能被调度运行。
_exit
不执行终止处理程序和标准I/O缓冲区的冲洗操作。
在大多数
exit
的实现中不会关闭流,因为进程终止时,内核会自动关闭进程中的所有文件描述符。在库中关闭这些,增加开销而且可能带来麻烦。
函数exit
exit
调用_exit
,_exit
和_Exit
是同义的。不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
子进程将自己的退出状态作为参数传递给exit
系列函数,父进程通过wait
或waitpid
函数获取其终止状态。在最后调用_exit
时,内核将退出状态转换成终止状态。
对于父进程已经终止的进程,它们的父进程都改变为init
进程,即init
进程收养。
一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程。
函数wait和waitpid
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD
异步信号。默认情况下,父进程忽略该信号。
调用wait
或waitpid
时:
(1)如果其子进程还在运行,则阻塞。
(2)如果一个子进程终止,正等待父进程获取其终止状态,则取得该子进程的终止状态并返回。
(3)如果它没有任何子进程,则立即出错返回。
如果进程在接收到SIGCHLD
信号后调用wait
,则立即返回。
在一个子进程终止前,wait
使其调用者阻塞,而waitpid
可使调用者不阻塞。
wait
等待的是第一个终止子进程,waitpid
可以等待指定的子进程。
参数status
是子进程终止状态的地址,如果该值为空,则父进程获取不到终止状态。
进程异常退出时,可能产生终止进程的core
文件。
对于waitpid
中的pid
参数:
(1)pid == -1
等待任一子进程,和wait
等效。
(2)pid > 0
等待进程ID
与pid
相等的子进程。
(3)pid == 0
等待组ID
等于调用进程组ID
的任一子进程。
(4)pid < -1
等待组ID
等于pid
绝对值的任一子进程。
options
参数使我们能进一步控制waitpid
的操作,可以提供非阻塞版本。
函数waitid
waitid
类似于waitpid
但提供了更多的灵活性,
waitid
允许一个进程指定要等待的子进程,但它使用两个单独的参数表示要等待的子进程所属的类型。
options
参数是下列各标志的位或运算。
infop
参数是指向siginfo_t
结构的指针,该结构包含了造成子进程状态改变有关信号的详细信息。
函数wait3和wait4
wait3
和wait4
允许内核返回由终止进程及其所有子进程使用的资源概况。
资源统计信息包括用户CPU
时间总量、系统CPU
时间总量、缺页次数、接收到信号的次数等。
竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果取决于进程运行的顺序时,我们认为发生了竞争条件。
如果一个进程希望等待一个子进程终止,则它必须调用wait
函数中的一个。如果一个进程要等待其父进程终止,则可以使用循环(等待自己被init
进程收养):
while (getppid() != -1) {
sleep(1);
}
为避免竞争条件和轮询(白白浪费CPU资源),使用睡眠锁实现互斥与同步地访问临界资源。
函数exec
当进程调用一种exec
函数时,该进程执行的程序完全被替换为新程序,而新程序则从其main
函数开始执行。exec
是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆和栈段。
如果file
中包含/
,则将其视为路径名。否则就按PATH
环境变量,在它所指定的目录中搜寻可执行文件。
PATH
变量包含一张目录表(路径前缀),目录之间用:
分隔。如:
最后的.
表示当前目录,空路径前缀也表示当前目录。
如果exec
使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则认为该文件是一个shell
脚本,于是试着调用/bin/sh
,并将file
作为shell
的输入。
调用exec
函数时,通过路径名、文件名、文件描述符指定可执行文件,指针数组、多个字符串地址(包含空指针)指定参数,还可以指定环境表。
在调用了exec
函数的新程序中,对打开文件的处理与每个描述符的执行时关闭(close_on_exec
)标志FD_CLOEXEC
有关。进程中每个打开描述符都有一个执行时关闭标志,若设置了该标志,则在执行exec
时关闭该描述符(使用fcntl
设置该标志),否则该描述符仍然打开(默认)。
在exec
前后实际用户ID
和实际组ID
保持不变,而有效ID
是否改变取决于所执行程序文件的设置用户ID
位和设置组ID
位是否设置。
只有execve
是系统调用,其它都是库函数。
更改用户ID和更改组ID
在UNIX
系统中,特权和访问控制是基于用户ID
和组ID
的,要改变特权就需要更改ID
。可以使用setuid
设置实际用户ID
和有效用户ID
,用setgid
设置实际组ID
和有效组ID
。
更改用户ID
的规则:
(1)若进程具有超级用户权限,则setuid
将实际用户ID
、有效用户ID
以及保存的设置用户ID
设置位uid
。
(2)若进程没有超级用户权限,但是uid
等于实际用户ID
或保存的设置用户ID
,则setuid
只将有效用户ID
设置为uid
,不更改实际用户ID和保存的设置用户ID。
(3)以上条件都不满足的话,将errno
设置为EPERM
,返回-1.
关于内核所维护的3
个用户ID
,要注意以下几点:
(1)只有超级用户进程可以更改实际用户ID
。
(2)仅当对程序文件设置了设置用户ID
位时,exec
函数才设置有效用户ID
。
(3)保存的设置用户ID
是由exec
复制有效用户ID
得到的。
函数setreuid和setregid
交换实际用户和有效用户的ID
值。
如若其中任一参数的值为-1
,则相应的ID
保持不变。
一个非特权用户总能交换实际用户ID
和有效用户ID
。
函数seteuid和setegid
只更改有效用户ID
和有效组ID
。
一个非特权用户可将其有效用户ID
设置为实际用户ID
或保存的设置用户ID
。特权用户可将有效用户ID
设置为euid
。
这些修改ID
的类似方式适用于各个组ID
,附属组不受setgid
、setregid
和setegid
的影响。
解释器文件
解释器文件是一种文本文件,起始行的形式是:#! pathname[optional-argument]
,如#! /bin/sh
,pathname
是解释器,进程实际执行的是解释器。
函数system
system
在其实现中调用了fork
、exec
和waitpid
,有三种返回值:
(1)fork
失败或者waitpid
返回除EINTR
之外的错误,则返回-1
.
(2)如果exec
失败,则返回值如同shell
执行了exit(127)
。
(3)返回shell
的终止状态。
shell
的-c
选项告诉shell
程序取下一个命令行参数(cmdstring
)作为命令输入。
进程会计
大多数UNIX
系统提供了一个选项以进行进程会计处理。启用该选项后,每当进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、CPU
时间总量、用户ID
、组ID
、启动时间等。
用户标识
我们可以调用getpwuid(getuid())
找到运行该进程的用户登录名,如果一个用户有多个登录名(一个人在口令文件中可以有多个登录项,它们的用户ID
相同,但登录shell
不同),用getlogin
获取此登录名。
如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败,通常称这些进程为守护进程。
进程调度
调度策略和调度优先级是由内核确定的,只有特权进程允许提高调度权限。
nice
值越小,优先级越高。
nice
意为“友好的”,越不友好的进程,优先级越高(参考排队和插队)
进程通过nice
函数获取或更改它的nice
值,使用这个函数,进程只能影响自己的nice
值。
getpriority
函数可以像nice
函数那样获取nice
值,但它还可以获取一组相关进程的nice
值。
setpriority
函数可用于为进程、进程组和属于特定用户ID
的所有进程设置优先级。
在Linux
中,子进程从父进程中继承nice
值。
进程时间
进程的时间包括:墙上时钟时间、用户CPU
时间和系统CPU
时间。任一进程都可调用times
函数获得它自己以及已终止子进程的上述值。
若成功,函数返回墙上时钟时间(单位是时钟滴答数),出错返回-1
。
习题
8.1
题目: 在下面的程序中,如果用exit
替换_exit
调用,那么可能会使标准输出关闭,使printf
返回-1
。修改程序以验证在你所使用的系统上是否会产生此种结果。如果并非如此,你怎样处理才能得到类似的结果?
答: 手动关闭标准输出缓冲区。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int global = 6;
int main() {
int var = 88;
printf("before vfork\n");
pid_t pid = vfork();
if (pid < 0) {
perror("vfork error");
}
else if (pid == 0) {
++global;
++var;
exit(0);
}
close(STDOUT_FILENO);
if (printf("%d %d\n", global, var) < 0) {
perror("printf error");
}
return 0;
}
运算结果:
vfork
:父子进程共享数据(包括缓冲区)。
fork
:子进程获得父进程的副本(写时复制)。
exit
和_exit
最大的区别在于:exit
会刷新缓存区、执行终止处理程序,而_exit
直接关闭文件描述符、清理资源。
8.2
题目: 由于对应于每个函数调用的栈帧通常存储在栈中,并且由于调用vfork
后,子进程运行在父进程的地址空间中,如果不是在main
函数中而是在另一个函数中调用vfork
,此后子进程又从该函数返回,将发生什么?
答: 使用vfork
创建的子进程在调用exec
或exit
之前,和父进程共享数据(包括栈帧),且阻塞着父进程。如果子进程从函数返回,则该函数对应的栈帧被释放掉,父进程无法访问该栈帧里的数据。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
pid_t f(int n) {
pid_t t = vfork();
if (t < 0) {
perror("vfork error");
}
else if (t == 0) {
printf("child...\n");
printf("%d\n", n);
}
else {
printf("parent in f...\n");
printf("%d\n", n);
}
return t;
}
int main() {
printf("before f\n");
pid_t t = f(55);
printf("after f\n");
if (t == 0) {
exit(0);
}
return 0;
}
运行结果:
子进程先执行直到退出,父进程在访问函数f
里的自动变量时出现段错误。
8.3
题目: 重写下列程序,把wait
换成waitid
,不调用pr_exit
,而是从siginfo
结构中确定等价的信息。
答:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void prtExit(siginfo_t *info) {
printf("status: %d\n", info->si_status);
switch (info->si_code) {
case (CLD_EXITED):
printf("正常退出\n");
break;
case (CLD_KILLED):
printf("被信号杀死\n");
break;
case (CLD_DUMPED):
printf("异常退出,核心转储\n");
break;
case (CLD_STOPPED):
printf("由信号停止\n");
break;
default:
printf("其他情况");
break;
}
}
int main() {
pid_t t;
siginfo_t info;
if ((t = fork()) < 0) {
perror("fork error");
}
else if (t == 0) {
exit(7);
}
waitid(P_PID, t, &info, WEXITED);
prtExit(&info);
if ((t = fork()) < 0) {
perror("fork error");
}
else if (t == 0) {
abort();
}
waitid(P_PID, t, &info, WEXITED);
prtExit(&info);
if ((t = fork()) < 0) {
perror("fork error");
}
else if (t == 0) {
int n = 1 / 0;
}
waitid(P_PID, t, &info, WEXITED);
prtExit(&info);
return 0;
}
运行结果:
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是
core
,即core dump
(核心转储),该文件可用于后续调试和检查错误。
8.4
**题目:**执行下面的程序一次,其输出是正确的,但是若将该程序执行多次,则输出不正确。
原因是什么?怎样才能更正此类错误?如果使子进程首先输出,还会发生此问题吗?
答: 即使子进程首先输出,也不会有改善。上图的指令表示,shell
创建了3
个子进程,且并发地执行它们,这3
个子进程之间并没有设置互斥或同步关系,所以输出的数据比较混乱。
每个进程和它的子进程之间是互斥同步的。如果这三个进程是依次执行而不是并发执行,那笔者也不知道答案。。。
8.5
题目: 在下面的程序中,调用execl
,指定pathname
为解释器文件。如果将其更改为调用execlp
,指定testinterp
的filename
,并且如果目录/home/sar/bin
是路径前缀,则运行该程序时,argv[2]
的打印输出是什么?
答:
// t5.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
putenv("PATH=.");
pid_t t;
if ((t = fork()) < 0) {
perror("fork error");
}
else if (t == 0) {
if (execlp("test", "test2", "arg1", "arg2", (char*)0) < 0) {
perror("exec error");
}
}
printf("finish\n");
waitpid(t, NULL, 0);
return 0;
}
// test
#! /home/liheng/UNIX_learning/8/test/echoArg testArg1 testArg2
// echoArg.c
#include <stdio.h>
int main(int argc, char **argv) {
printf("%d arguments\n", argc);
for (int i = 0; i < argc; ++i) {
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
运行结果:
argv[2]
打印输出的是./testinterp
。
8.6
题目: 编写一段程序创建一个僵死进程,然后调用system
执行ps
命令以验证该进程是僵死进程。
答:
僵死进程:子进程已退出,而父进程既没退出(没法被init
收养)也没调用wait
清理子进程资源,此时子进程是僵死进程。
// t6.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main() {
pid_t t;
if ((t = fork()) < 0) {
perror("fork error");
}
else if (t == 0) {
printf("child ID: %d\n", getpid());
exit(0);
}
else {
printf("parent ID: %d\n", getpid());
sleep(2);
char str[100];
// 输入search查看僵死进程
while (fgets(str, 100, stdin)) {
if (strcmp(str, "search\n") == 0) {
break;
}
}
sprintf(str, "ps -el | grep %d", t);
if (system(str) < 0) {
perror("system error");
}
// 输入kill杀死僵死进程及其父进程
while (fgets(str, 100, stdin)) {
if (strcmp(str, "kill\n") == 0) {
break;
}
}
exit(0);
}
}
父进程退出时,如果子进程已经退出了,就会自动回收子进程。
运行结果:
Z
表示僵死(zombie)进程。
8.7
题目: POSIX
要求在exec
时关闭打开目录流。按下列方法对此进行验证:对根目录调用opendir
,查看在你的系统上实现的DIR
结构,然后打印执行时关闭标志。接着打开同一目录读并打印执行时关闭标志。
答:
// t7.c
#include <stdio.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
char path[] = "/";
DIR *dir = opendir(path);
int fd = dirfd(dir);
int flag = fcntl(fd, F_GETFD);
printf("%s close-on-exec: %d\n", path, flag);
struct dirent *d;
chdir(path);
while (d = readdir(dir)) {
char str[100];
sprintf(str, "%s%s", path, d->d_name);
fd = open(d->d_name, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "%s ", str);
perror("open error");
continue;
}
flag = fcntl(fd, F_GETFD);
printf("%s close-on-exec: %d\n", str, flag);
}
return 0;
}
在root
用户下启动进程,否则打不开某些文件: