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创建的内存映射

自定义退出处理函数

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

监控子进程

监控子进程即父进程需要知道子进程与何时改变了状态。可以通过系统调用 wait 与 SIGCHLD 信号监控子进程。

等待子进程

可以通过 wait 系列函数等待子进程结束,并捕获其状态

其中的 status 值可用于区分以下事件:

  • 子进程调用 _exit 终止

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

  • 子进程因为信号而停止

  • 指定 WCONTINUED 时,子进程收到 SIGCONT 恢复执行

可以采用以下宏函数判断:

  • WIFEXITED(status):若子进程正常结束,返回 true

  • WIFSIGNALED(status):若通过信号杀死子进程则返回 true

  • WIFSTOPPED(status):若子进程因信号而停止,返回 true,可使用 WSTOPSIG(status)返回信号编号

  • WIFCONTINUED(status):若子进程收到 SIGCONT 恢复执行,返回 true

孤儿进程与僵尸进程

因为父子进程的生命周期不一致,所有会产生孤儿进程与僵尸进程。 孤儿进程为父进程已经终止,子进程仍在运行的进程,此时 init 进程将会接管此子进程,成为其新的父进程。所以可以通过 getppid调用,查看其父进程 ID 是否为 1 判断其是否为孤儿进程。 僵尸进程指那些在子进程已经退出,但父进程一直未通过 wait 调用回收其状态的进程。僵尸进程的大部分资源会被释放,其唯一保留的是在内核中的一条记录,包含了子进程 ID,终止状态等为 wait调用使用的信息。父进程直到退出仍未执行 wait,则该进程将称为孤儿进程,进一步由 init 进程调用 wait 捕获。 僵尸进程不能通过信号杀死(即使是 SIGKILL),从系统中移除僵尸进程的唯一方法就是杀死它的父进程。

SIGCHLD 信号

可以通过 SIGCHLD 信号异步捕获子进程终止。SIGCHLD 信号默认处理是将其忽略。标准信号不会进行排队,即使有多个 SIGCHLD 信号,父进程也只能捕获到一个。可在 SIGCHLD 信号处理函数中执行:

不断轮询查询,直到没有其它终止子进程需要处理。 为了防止在创建信号处理程序时已有子进程退出,所以需要在创建任何子进程之前设置 SIGCHLD 处理程序。

程序的执行

执行新程序 execve

通过 execve 族函数可以在进程中执行一个全新的程序。执行此函数可以将新程序加载到某一进程的内存空间,并丢弃旧有程序,包括其堆栈栈、数据等。

在 execve 之上还有以下 exec 函数:

默认情况下,exec 会继承在原程序中打开的文件描述符,除非文件设置了 close-on-exec 标志,可利用 fcntl 调用设置。当使用 dup 系列函数创建副本时,总是会清楚副本描述符的 close-on-exec 标志。 exec 执行的新程序后会将原有已设信号处置的信号重置为 SIG_DFL。但对 SIGCHLD 的处置是不确定的,所以需要手动将其处置置为 SIG_DFL。在调用 exec 期间,原有进程信号掩码一级挂起信号的设置均得以保存。

执行 shell 命令: system

system 调用会创建两个进程,一个用于 shell 一个用于执行命令。 在 set-user-id 和 set-group-id 程序中应避免使用 system,已避免引起安全隐患。

最后更新于

这有帮助吗?