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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://linuxprogramming.weijun-lin.top/3.-jin-cheng.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
