进程的创建
fork 创建新进程
系统调用 fork 创建一新进程,新进程为调用进程的翻版:
#include <unistd.h>
// 创建新进程,子进程中返回 0,父进程返回子进程的 ID,失败返回 -1
pid_t fork(void);
两个进程执行相同的代码段,各自拥有不同的堆栈段,数据段,子进程的这几个段初始为父进程的拷贝。此调用失败一般是因为进程数量达到系统上限。 fork 执行后,谁先执行是不确定的。 执行 fork()时,子进程会获得父进程所有文件描述符的副本,类似与 dup 调用。 直接将父进程的堆栈段复制会造成很大的浪费,现代 Unix 一般采取以下措施避免:
为了规避父子进程执行顺序不确定导致的竞争条件,可以采用同步信号来规避(当然可以使用信号量等 IPC 通信)。例如,利用 sigsuspend
调用阻塞父进程,直到子进程向其发送信号。
进程的终止
exit 与 _exit
进程有两种终止方式,异常终止,与正常终止。异常导致的终止一般由信号引发,可能会产生核心转储文件。_exit 调用可以正常终止进程,exit 调用也可以,在底层它也通过调用 _exit 终止进程,不过还会对缓冲区等做额外的操作。
#include <unistd.h>
// status 定义了进程的终止状态
// 父进程可以通过 wait 获取该状态
void _exit(int status);
// 调用退出处理程序 —— atexit() on_exit() 注册的函数
// 刷新 stdio 缓冲区
// 调用 _exit(status)
void exit(int status);
程序末尾的 return n;
等同于执行 exit(n)
进程终止的细节
异常终止与正常终止都会发生以下动作:
如果该进程是终端的管理进程,则向前台进程组发送 SIGHUP 信号
自定义退出处理函数
可以自定义进程终止时采取的一些操作,即退出处理函数。通过注册退出处理函数可以在进程终止前自动执行一些清理动作。
#include <stdlib.h>
// 成功返回 0,否则返回非零值
// 可以注册多个处理函数,进程退出时函数的执行顺序与注册顺序相反
// fork 创建的子进程会继承父进程的退出处理函数,直到执行 exec()
int atexit(void (*func)(void));
// 非标准
#define _BSD_SOURCE
#include <stdlib.h>
// 会传递两个参数给 func,exit 的 status 参数,以及 arg
int on_exit(void (*func)(int, void*), void *arg);
监控子进程
监控子进程即父进程需要知道子进程与何时改变了状态。可以通过系统调用 wait 与 SIGCHLD 信号监控子进程。
等待子进程
可以通过 wait 系列函数等待子进程结束,并捕获其状态
#include <sys/wait.h>
// 该系统调用会阻塞当前进程直到有任一子进程终止
// 如果status 非空,子进程如何终止将会通过这个参数返回
// 成功则返回终止的进程 Id 号,出错时返回 -1,可能是因为所有子进程均退出且被 wait 捕获
// 此时会将 errno 置为 ECHILD
pid_t wait(int *status);
/*
@brief 等待特定子进程 pid 结束
@param pid 需要等待的具体子进程
- pid = 0: 等待与父进程同一个进程组的所有子进程
- pid > 0: 进程 Id 等于 pid 的进程
- pid = -1:等待任意子进程
- pid < -1:等待进程组标识符与其绝对值相等的所有子进程
status 子进程终止的信息
option 位掩码,指定一些操作
- WUNTRACED 除了返回终止子进程的信息,还返回因信号而停止的子进程信息
- WCONTINUED 返回因 SIGCONT 信号恢复执行的进程的状态信息
- WNOHANG 无阻塞返回,没有子进程状态改变则返回 0
@return 返回等待成功的进程 Id,失败返回 -1
*/
pid_t waitpid(pid_t pid, int *status, int option);
其中的 status 值可用于区分以下事件:
指定 WCONTINUED 时,子进程收到 SIGCONT 恢复执行
可以采用以下宏函数判断:
WIFEXITED(status)
:若子进程正常结束,返回 true
WIFSIGNALED(status)
:若通过信号杀死子进程则返回 true
WIFSTOPPED(status)
:若子进程因信号而停止,返回 true,可使用 WSTOPSIG(status)
返回信号编号
WIFCONTINUED(status)
:若子进程收到 SIGCONT 恢复执行,返回 true
#include <sys/wait.h>
/*
@brief Linux 2.6.9 之后支持
@param idtype 指定 id 的类型
- P_ALL 忽略 id 等待任何子进程
- P_PID 等待进程 ID 为 id 的子进程
- P_PGID 等待进程组 ID 为 id 的所有子进程
@param options 控制等待的子进程事件,下面可以用位运算或,来选择
- WEXITED 等待已终止的子进程
- WSTOPPED 等待通过信号停止的子进程
- WCONTINUED 等待由 SIGCONT 恢复的子进程
- WNOHANG 无阻塞返回,没有子进程状态改变则返回 0
- WNOWAIT 直接返回子进程状态 infop(子进程还处于可 wait 状态)
@param infop 状态指针,包含以下字段
- si_code 取 CLD_EXITED, CLD_KILLED, CLD_STOPPED, CLD_CONTINUED
- si_pid 状态发生变化的子进程 ID
- si_signo 总为 SIGCHLD
- si_status _exit 调用的 status 值,或者导致子进程状态改变的信号值
- si_uid 子进程真正用户 ID
*/
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int option);
孤儿进程与僵尸进程
因为父子进程的生命周期不一致,所有会产生孤儿进程与僵尸进程。 孤儿进程为父进程已经终止,子进程仍在运行的进程,此时 init 进程将会接管此子进程,成为其新的父进程。所以可以通过 getppid
调用,查看其父进程 ID 是否为 1 判断其是否为孤儿进程。 僵尸进程指那些在子进程已经退出,但父进程一直未通过 wait
调用回收其状态的进程。僵尸进程的大部分资源会被释放,其唯一保留的是在内核中的一条记录,包含了子进程 ID,终止状态等为 wait
调用使用的信息。父进程直到退出仍未执行 wait
,则该进程将称为孤儿进程,进一步由 init 进程调用 wait
捕获。 僵尸进程不能通过信号杀死(即使是 SIGKILL),从系统中移除僵尸进程的唯一方法就是杀死它的父进程。
SIGCHLD 信号
可以通过 SIGCHLD 信号异步捕获子进程终止。SIGCHLD 信号默认处理是将其忽略。标准信号不会进行排队,即使有多个 SIGCHLD 信号,父进程也只能捕获到一个。可在 SIGCHLD 信号处理函数中执行:
while(waitpid(-1, NULL, WNOHANG) > 0) continue;
不断轮询查询,直到没有其它终止子进程需要处理。 为了防止在创建信号处理程序时已有子进程退出,所以需要在创建任何子进程之前设置 SIGCHLD 处理程序。
程序的执行
执行新程序 execve
通过 execve 族函数可以在进程中执行一个全新的程序。执行此函数可以将新程序加载到某一进程的内存空间,并丢弃旧有程序,包括其堆栈栈、数据等。
#include <unistd.h>
/*
@brief 执行一个新程序
@param pathname 新程序的路径名
@param argv 命令行参数,以 NULL 结束,argv[0] 为命令名
@param envp 新程序的环境列表即新程序的 environ 数组
@return 成功没有返回,失败返回 -1
*/
int execve(const char *pathname, char *const argv[], char *const envp[]);
在 execve 之上还有以下 exec 函数:
#include <unistd.h>
// pathname 为完整文件路径名
// filename 为在系统 PATH 中寻找文件,这种调用均以 p 结尾
// 处于安全考虑,当前工作目录一般会排除在当前 PATH 之外
// v 表示 argv 数组
// l 表示将 argv 用数组形式给出
int execle(const char *pathname, const char *arg, ...
/* , (char*) NULL, char *const envp[] */);
int execlp(const char *filename, const char *arg, ...
/* , (char*) NULL */);
int execvp(const char *filename, char *const argv[]);
int execv(const char *pathname, char *const argv[]);
int execl(const char *pathname, const char *arg, ...
/* , (char*) NULL */);
// 从文件描述符执行程序
int fexecve(int fd, char *const argv[], char * const envp[]);
默认情况下,exec 会继承在原程序中打开的文件描述符,除非文件设置了 close-on-exec 标志,可利用 fcntl 调用设置。当使用 dup 系列函数创建副本时,总是会清楚副本描述符的 close-on-exec 标志。 exec 执行的新程序后会将原有已设信号处置的信号重置为 SIG_DFL。但对 SIGCHLD 的处置是不确定的,所以需要手动将其处置置为 SIG_DFL。在调用 exec 期间,原有进程信号掩码一级挂起信号的设置均得以保存。
执行 shell 命令: system
#include <stdlib.h>
//创建一个子进程运行 shell 并执行 command
// 无法创建子进程或无法获取其终止状态 返回 -1
// 调用成功则返回子 shell 终止状态(128 + n),n 为信号编号
// 如果不能执行 shell 就相当于子 shell 调用 _exit(127) 一样
int system(const char *command);
system 调用会创建两个进程,一个用于 shell 一个用于执行命令。 在 set-user-id 和 set-group-id 程序中应避免使用 system,已避免引起安全隐患。