> For the complete documentation index, see [llms.txt](https://linuxprogramming.weijun-lin.top/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://linuxprogramming.weijun-lin.top/3.-jin-cheng.md).

# 3. 进程

### 进程的创建

#### fork 创建新进程

系统调用 fork 创建一新进程，新进程为调用进程的翻版：

```c
#include <unistd.h>
// 创建新进程，子进程中返回 0，父进程返回子进程的 ID，失败返回 -1
pid_t fork(void);
```

两个进程执行相同的代码段，各自拥有不同的堆栈段，数据段，子进程的这几个段初始为父进程的拷贝。此调用失败一般是因为进程数量达到系统上限。 fork 执行后，谁先执行是不确定的。 执行 fork()时，子进程会获得父进程所有文件描述符的副本，类似与 dup 调用。 直接将父进程的堆栈段复制会造成很大的浪费，现代 Unix 一般采取以下措施避免：

* 内核将代码段标记为只读
* 对于堆栈段，数据段采用写时复制技术

为了规避父子进程执行顺序不确定导致的竞争条件，可以采用同步信号来规避（当然可以使用信号量等 IPC 通信）。例如，利用 `sigsuspend`调用阻塞父进程，直到子进程向其发送信号。

### 进程的终止

#### exit 与 \_exit

进程有两种终止方式，异常终止，与正常终止。异常导致的终止一般由信号引发，可能会产生核心转储文件。\_exit 调用可以正常终止进程，exit 调用也可以，在底层它也通过调用 \_exit 终止进程，不过还会对缓冲区等做额外的操作。

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

#### 自定义退出处理函数

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

```c
#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 系列函数等待子进程结束，并捕获其状态

```c
#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

```c
#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 信号处理函数中执行：

```c
while(waitpid(-1, NULL, WNOHANG) > 0) continue;
```

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

### 程序的执行

#### 执行新程序 execve

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

```c
#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 函数：

```c
#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

```c
#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，已避免引起安全隐患。
