8.1 引言
本章介绍UNIX系统的进程控制,包括:
- 创建新进程、执行程序、进程终止
- 进程属性ID——实际、有效、保存的
用户ID
和组ID
- 解释器文件
system
函数- 进程会计机制
8.2 进程标识
进程ID
:一个非负整数,进程的唯一标识。
- 进程ID可复用:当某个进程终止后,它的进程ID可被之后创建的进程复用;
- 几个
专用进程
:
(1)进程ID为0
的是调度进程,也称交换进程,它是内核中的系统进程;
(2)进程ID为1
的是init
进程,此进程负责在自举内核后启动一个UNIX系统,init进程绝不会终止,它是一个以超级用户特权运行的普通用户进程;
(3)进程ID为2
的是页守护进程,负责支持虚拟存储器系统的分页操作。
UNIX系统提供了返回进程某些标识符的函数:
#include <unistd.h>
pid_t getpid(void);
// 返回值:调用进程的进程ID
pid_t getppid(void);
// 返回值:调用进程的父进程ID
uid_t getuid(void);
// 返回值:调用进程的实际用户ID
uid_t geteuid(void);
// 返回值:调用进程的有效用户ID
gid_t getgid(void);
// 返回值:调用进程的实际组ID
gid_t getegid(void);
// 返回值:调用进程的有效组ID
8.3 函数fork
fork
函数用于创建一个新进程:
#include <unistd.h>
pid_t fork(void);
// 子进程返回0,父进程返回子进程ID;若出错,返回-1
fork
调用一次,返回两次,子进程返回0,父进程返回子进程ID;- 一个进程的子进程可以有多个,但父进程只有一个;
- 子进程是父进程的副本,子进程得到父进程的数据空间、堆和栈的副本,子进程和父进程共享正文段;
- 由于在
fork
之后经常跟随着exec
,所以很多实现并不执行一个父进程数据段、栈和堆的完全副本,而是采用写时复制(Copy-On-Write,COW)技术,这些区域由父进程和子进程共享,且内核将它们的访问权限改为只读,如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟内存系统中的“一页”; fork
之后是父进程还是子进程先执行是不确定的;- 父进程的所有打开文件描述符都被复制到子进程,每个相同的打开描述符共享一个文件表项;
8.4 函数vfork
vfork
函数的调用序列和返回值与fork
相同,但二者语义不同:
vfork
用于创建一个新进程,该新进程的目的是执行一个新程序,vfork
不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec
(或exit
),所以也就不会引用该地址空间,不过子进程在调用exec
或exit
前,它在父进程的空间中执行;vfork
保证子进程先运行,在它exec
或exit
之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。
8.5 函数exit
进程有5种正常终止及3种异常终止方式。
5种正常终止方式:
- 在
main
函数内执行return
语句,等效于调用exit
; - 调用
exit
函数; - 调用
_exit
或_Exit
函数; - 进程的最后一个线程在其启动例程中执行
return
语句,但该线程的返回值不用作进程的返回值,进程以终止状态0返回; - 进程的最后一个线程调用
pthread_exit
函数,进程以终止状态0返回。
3种异常终止方式:
- 调用
abort
,产生SIGABRT
信号; - 当进程接收到某些信号,信号可由进程自身(如调用
abort
函数)、其他进程或内核产生; - 最后一个线程对“取消”请求做出响应,默认情况下,“取消”以延迟方式产生:一个线程要求取消另一个线程,若干时间后,目标线程终止。
关于父进程、子进程先后终止的两种情况:
- 对于父进程已经终止的所有进程,它们的父进程都改变为
init
进程,称这些进程由init
进程收养; - 内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用
wait
或waitpid
时,可以得到这些信息; - 一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程;
- 只要有一个由
init
收养的子进程终止,init
就会调用一个wait
函数取得其终止状态,所以init
的子进程永远不会成为僵死进程。
8.6 函数wait和waitpid
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD
信号,因为子进程终止是个异步事件(这可能在父进程运行的任何时候发生),所以这种信号也是内核向父进程发送的异步通知。
调用wait
或waitpid
的进程可能会:
- 如果其所有子进程都还在运行,则阻塞;
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回;
- 如果它没有任何子进程,则立即出错返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// 两个函数返回值:若成功,返回进程ID;若出错,返回0或-1
wait
函数等待任一终止子进程:
- 如果子进程已经终止,并且是一个僵死进程,则
wait
立即返回并取得该子进程的状态;否则wait
使其调用者阻塞,直到一个子进程终止。如果调用者阻塞而且它有多个子进程,则在其某一个子进程终止时,wait
就立即返回其终止子进程的进程ID; - 参数statloc是一个整型指针,若不为空,则终止进程的终止状态存放其中,若不关心终止状态,可将该指针指定为空指针;终止状态用定义在<sys/wait.h>中的各个宏来查看,有4个互斥的宏可用来取得进程终止的原因,基于这4个宏中哪一个值为真,就可选用其他宏来取得退出状态、信号编号等:
waitpid
函数等待一个特定的进程,其pid参数的作用解释如下:
pid == -1
:等待任一子进程,等效于wait
;pid > 0
:等待进程ID与pid相等的子进程;pid == 0
:等待组ID等于调用进程组ID的任一子进程;pid < -1
:等待组ID等于pid绝对值的任一子进程。
options参数可进一步控制waitpid
的操作,此参数或者是0,或者是下面常量按位或运算的结果:
waitpid
函数提供了wait
函数没有提供的3个功能:
waitpid
函数可等待一个特定进程,而wait
则返回任一终止子进程的状态;waitpid
提供了一个wait
的非阻塞版本;waitpid
通过WUNTRACED和WCONTINUED选项支持作业控制。
8.7 函数waitid
waitid
函数也用于获取进程终止状态,它比waitpid
更灵活:
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
// 返回值:若成功,返回0;若出错,返回-1
- id参数指定要等待的子进程,它的作用与idtype的值有关:
- options参数是下图中个标志的按位或运算,这些标志指示调用者关注哪些状态变化,且WCONTINUED、WEXITED或WSTOPPED这3个常量之一必须在options参数中指定;
- infop参数是指向siginfo结构的指针,该结构包含了造成子进程状态改变有关信号的详细信息。
8.8 函数wait3和wait4
wait3
和wait4
函数提供了获取终止进程及其所有子进程使用资源概况的功能,资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
// 两个函数返回值:若成功,返回进程ID;若出错,返回-1
8.9 竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,就发生了竞争条件。
8.10 函数exec
fork
函数创建子进程后,子进程往往要调用一种exec
函数以执行另一个程序,有7种exec
函数可供使用:
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
// 7个函数返回值:若出错,返回-1;若成功,不返回
这些函数之间的第一个区别是前4个函数取路径名作为参数,后两个函数则取文件名作为参数,最后一个取文件描述符作为参数。当指定filename作为参数时:
- 如果filename种包含/,则就将其视为路径名;
- 否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。
第二个区别与参数表的传递有关(l表示列表list,v表示矢量vector),函数execl
、execlp
和execle
要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾;对于另外4个函数(execv
、execvp
、execve
和fexecve
),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这4个函数的参数;
最后一个区别与向新程序传递环境表相关,以e结尾的3个函数(execle
、execve
和fexecve
)可以传递一个指向环境字符串指针数组的指针,其他4个函数则使用调用进程中的environ变量为新程序复制现有的环境。
7个exec
函数的参数记忆方法:
- 字母p表示该函数取filename作为参数,并且用PATH环境变量寻找可执行文件;
- 字母l表示该函数取一个参数表,它与字母v互斥;
- 字母v表示该函数取一个argv[]矢量;
- 字母e表示该函数取envp[]数组,而不使用当前环境。
exec
新程序对打开文件的处理与每个描述符的执行时关闭(close-on-exec)标志值有关,进程中每个打开描述符都有一个执行时关闭标志(FD_CLOEXEC)标志,若设置了此标志,则在执行exec
时关闭该描述符。
在很多UNIX实现中,这7个函数中只有execve
是内核的系统调用,另外6个只是库函数,它们最终都要调用execve
,这7个函数之间的关系如下:
8.11 更改用户ID和更改组ID
setuid
函数设置实际用户ID和有效用户ID,setgid
函数设置实际组ID和有效组ID:
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
// 两个函数返回值:若成功,返回0;若出错,返回-1
更改用户ID的规则(关于用户ID所说明的一切适用于组ID):
- 若进程具有超级用户特权,则
setuid
函数将实际用户ID、有效用户ID以及保存的设置用户ID设置为uid; - 若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则
setuid
只将有效用户ID设置为uid,不更改实际用户ID和保存的设置用户ID; - 如果上述两个条件都不满足,则errno设置为EPERM,并返回-1。
关于内核所维护的3个用户ID的注意事项:
- 只有超级用户进程可以更改实际用户ID;
- 仅当程序文件设置了设置用户ID位时,
exec
函数才设置有效用户ID; - 保存的设置用户ID是由
exec
复制有效用户ID而得到的。
下图总结了更改这3个用户ID的不同方法:
setreuid
函数功能是交换实际用户ID和有效用户ID的值:
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
// 两个函数返回值:若成功,返回0;若出错,返回-1
- 如若两个参数中任一个的值为-1,则表示相应的ID保持不变。
seteuid
和setegid
函数用于更改有效用户ID和有效组ID:
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
// 两个函数返回值:若成功,返回0;若出错,返回-1
- 一个非特权用户可将其有效用户ID设置为实际用户ID或保存的设置用户ID;
- 一个特权用户可将有效用户ID设置为uid。
更改3个不同用户ID函数之间的关系:
8.12 解释器文件
解释器文件是一个文本文件,其起始行的形式是:
#! pathname [optional-argument]
在感叹号和pathname之间的空格和optional-argument都是可选的,最常见的解释器文件以下列行开始:
! /bin/sh
- pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索);
- 内核使调用
exec
函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。
8.13 函数system
system
函数用于在程序中执行一个命令字符串:
#include <stdlib.h>
int system(const char *cmdstring);
- 如果cmdstring是一个空指针,则仅当命令处理程序可用时,
system
返回非0值,这一特征可以确定在一个给定的操作系统上是否支持system
函数,在UNIX种,system
总是可用的; system
在其实现中调用了fork
、exec
和waitpid
,因此有3种返回值:
(1)fork
失败或者waitpid
返回除EINTR之外的出错,则system
返回-1,并且设置errno以指示错误类型;
(2)如果exec
失败(表示不能执行shell),则其返回值如同shell执行了exit(127)
;
(3)否则所有3个函数(fork
、exec
和waitpid
)都成功,那么system
的返回值是shell的终止状态。
8.14 进程会计
大多数UNIX系统提供了一个选项以进行进程会计处理,启用该选项后,每当进程结束时内核就写一个会计记录,典型的会计记录包含总量较小的二进制数据,一般包括命令名、所使用的CPU时间总量、用户ID和组ID、启动时间等。
8.15 用户标识
系统通常记录用户登陆时使用的名字,用getlogin
函数可以获取此登录名:
#include <unistd.h>
char *getlogin(void);
// 返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL
8.16 进程调度
- 进程可以通过调整友好值选择以更低优先级运行,只有特权进程允许提高调度权限;
- Single UNIX Specification 中友好值的范围:0~(2*NZERO-1),NZERO是系统默认的友好值,默认20;
- 友好值越小,优先级越高。
进行可以通过nice
函数获取或更改它的友好值:
#include <unistd.h>
int nice(int incr);
// 返回值:若成功,返回新的友好值;若出错,返回-1
- incr参数被增加到调用进程的友好值上;
- 如果incr太大,系统直接把它降到最大合法值,不给出提示;如果太小,也会把它调整到最小合法值;
- 由于-1是合法的成功返回值,在调用
nice
函数之前需要清除errno,在nice
函数返回-1时,需要检查它的值; - 进程只能影响自己的友好值,不能影响任何其他进程的友好值。
getpriority
函数可以用于获取进程、一组相关进程的友好值:
#include <sys/resource.h>
int getpriority(int which, id_t who);
// 返回值:若成功,返回-NZERO~NZERO-1之间的友好值;若出错,返回-1
- which参数可取值:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID;
- who参数含义由which参数决定:
(1)如果who参数为0,表示调用进程、进程组或者用户(取决于which参数的值);
(2)当which设为PRIO_USER并且who为0时,使用调用进程的实际用户ID;
(3)如果which参数作用于多个进程,则返回所有作用进程中优先级最高的(最小的友好值)。
setpriority
函数用于为进程、进程组和属于特定用户ID的所有进程设置优先级:
#include <sys/resource.h>
int setpriority(int which, id_t who, int value);
// 返回值:若成功,返回0;若出错,返回-1
- 参数which和who与
getpriority
函数中相同; - value增加到NZERO上,然后变为新的友好值。
8.17 进程时间
任一进程都可调用times
函数获得它自己以及已终止子进程的墙上时钟时间、用户CPU时间和系统CPU时间:
#include <sys/times.h>
clock_t times(struct tms *buf);
// 返回值:若成功,返回流逝的墙上时钟时间(以时钟滴答数为单位);若出错,返回-1
-
此函数填写由buf指向的tm结构:
struct tms { clock_t tms_utime; // 用户CPU时间 clock_t tms_stime; // 系统CPU时间 clock_t tms_cutime; // 终止子进程的用户CPU时间 clock_t tms_cstime; // 终止子进程的系统CPU时间 };
-
函数返回的墙上时钟是相对于过去的某一时刻度量的,不能使用其绝对值而必须使用其相对值。
8.18 实例代码
chapter8