3. 进程

进程的创建,终止,以及监控

进程的创建

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)

进程终止的细节

异常终止与正常终止都会发生以下动作:

  • 关闭所有打开文件描述符,释放该进程持有的文件锁

  • 分离已连接的 System V 共享内存段

  • 如果该进程是终端的管理进程,则向前台进程组发送 SIGHUP 信号

  • 关闭打开的 POSIX 有名信号量

  • 关闭打开的 POSIX 消息队列

  • 取消通过 mmap创建的内存映射

自定义退出处理函数

可以自定义进程终止时采取的一些操作,即退出处理函数。通过注册退出处理函数可以在进程终止前自动执行一些清理动作。

#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 值可用于区分以下事件:

  • 子进程调用 _exit 终止

  • 子进程收到未处理信号终止

  • 子进程因为信号而停止

  • 指定 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,已避免引起安全隐患。

最后更新于