💭 写在前面:CSAPP 是计算机科学经典教材《Computer Systems: A Programmer's Perspective》的缩写,该教材由Randal E. Bryant和David R. O'Hallaron 合著。本文以程序员的视角来看,我们不会深入研究(或编写)实际管理进程的内核代码。 我们将学习当我们的程序想要创建、终止或等待进程时,如何向内核发出请求(即系统调用)。在我们开始之前,让我们简要讨论系统调用包装器。
0x00 进程控制(Process Control)
每当一个程序想要在其自身进程之外产生影响时,它必须请求内核的帮助。比如文件读写:
- fopen 内部调用 open,这时会调用系统调用
- 我们的程序也可以直接调用 open,包含 <fcntl.h>
- 我们也将称此封装函数 (open) 为系统调用
0x01 系统调用错误处理(System Call Error Handling)
几乎所有系统级操作都有可能失败(除了一些返回void的函数之外),因此我们必须明确地检查失败情况。
在错误情况下,Linux 系统级函数通常返回 -1,并设置全局变量 errno 以指示原因。
if (some_syscall() < 0) { fprintf(stderr, "Syscall error: %s\n", strerror(errno)); exit(1); } if (some_syscall() < 0) { perror("Syscall error"); // 更简洁的版本 exit(1); }
0x02 错误报告函数(Error-reporting functions)
可以使用错误报告函数来简化。但请注意,退出可能并不总是正确的操作。
#include <stdlib.h> // exit() #include <unistd.h> // fork() #include <errno.h> // errno() #include <string.h> // strerror() void unix_error(char *msg) /* Unix-style error */ { fprintf(stderr, "%s: %s\n", msg, strerror(errno)); exit(1); } if ((pid = fork()) < 0) unix_error("fork error");
0x03 错误处理封装接口(Error-handling Wrappers)
我们使用以下封装函数进一步简化了先前的代码,这并不是在实际应用程序中通常想要做的事情。
pid_t Fork(void) { pid_t pid; if ((pid = fork()) < 0) unix_error("Fork error"); return pid; } pid = Fork(); // 只在成功时返回
0x04 获取 PID
每一个进程在系统中,都会存在一个惟一的标识符!
这就如同每个人都有身份证号一样,进程也需要标号的,所以每个进程都存在有一个 。
Process ID: a unique integer ID assigned to each process
pid_t getpid(void) // 返回当前进程pid pid_t getppid(void) // 放回父进程pid
* 每个进程都有一个父进程。
下面我们隆重介绍下获取 的函数 —— getpid()
想要查看进程 ,一定是这个进程得运行起来。
我们不妨先问问 Linux 手册中的那个男人,getpid 的下落:
$ man 2 getpid
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(void) { while (1) { printf("I am m a process! , pid: %d\n",getpid()); sleep(1); } }
🚩 运行结果如下:
启动后,我们发现我们的 mytest 可执行程序的 为 。
是否果真如此?我们还是用 ps aux 验证一下看看:
ps aux | head -1 && ps aux | grep 'mytest' | grep -v grep
0x05 进程状态(Process State)
从程序员的角度来看,我们可以将一个进程看作处于三种状态之一。
但是从操作系统的角度来看,可能会有更多的状态,详情可以看下面这篇文章:
【看表情包学Linux】进程状态解析 | 运行态 | 终止态 | 进程挂起与阻塞 | 运行态R | 阻塞态S/D | 死亡态X | 僵尸态Z | 暂停态T/t | 僵尸进程 | 孤儿进程
我们现在以程序员的角度来看,三种状态分别是:
Running: 进程正在执行,或者它正在等待被内核选择并运行。
Stoped (阻塞): 进程的执行已被暂停,直到收到进一步通知才会被调度 (信号)
Terminated and not reaped (僵尸): 进程已完成,但父进程尚未被通知。
0x06 终止进程(Terminating Processes)
进程会因为以下 ”三种之一的情况” 而变成终止状态:
① 接收到一个默认操作为终止进程的信号(下一讲)
② 调用 exit 函数 ▪ 即使主函数返回,也会隐式调用 exit 函数
int fork(void)
③ 以状态值 status 终止进程。约定:正常返回 0,错误返回非零值。另一种显式设置退出状态的方法是从主函数中返回一个整数值(例如在主函数的末尾写上 return 0)
注意:exit 函数只会被调用一次,且不会返回!
0x07 进程创建(Creating Processes)
父进程通过调用 fork 函数创建一个新的正在运行的子进程。
int fork(void);
对于子进程返回 0,对于父进程返回子进程的 pid。
子进程几乎与父进程相同:
- 子进程获得父进程虚拟地址空间的一个相同但独立的副本。
- 子进程获得父进程打开文件描述符的相同副本
- 子进程的PID与父进程不同
fork 是个有趣的函数(也经常令人困惑),因为它只被调用一次,但返回两次。
创建执行状态的完整副本:
- 将一个标识为父进程,将另一个标识为子进程。
- 恢复父进程或子进程的执行。
fork 例子:
int main(void) { pid_t pid; int x = 1; pid = Fork(); if (pid == 0) { /* Child */ printf("child : x=%d\n", ++x); return 0; } /* Parent */ printf("parent: x=%d\n", --x); return 0; }
调用一次,返回两次。如何区分我们程序的进程和子进程? 通过检查返回值!父进程返回子进程 pid,子进程返回 0,所以我们可以通过 if 语句来抓子进程!
Duplicate but separate address space(复制了一份独立地地址空间):这是因为进程具有独立性!在 fork 返回后:x 的值为1 ,对 x 的更改是独立的!
并发执行:无法预测父进程和子进程的执行顺序
共享打开的文件:stdout 在父进程和子进程中是相同的。
0x08 使用进程图建模 fork
进程图是捕捉并发程序中语句的部分排序的有用工具:
- 每个顶点是语句的执行 a -> b 表示 a 发生在 b 之前
- 边缘可以标记为变量的当前值
- printf 顶点可以标记为输出
- 每个图形以没有入边的顶点开始。
图的任何拓扑排序对应于可行的总排序。
- 顶点的总排序,其中所有边缘都指向从左到右
int main(int argc, char** argv) { pid_t pid; int x = 1; pid = Fork(); if (pid == 0) { /* Child */ printf("child: x=%d\n", ++x); return 0; } /* Parent */ printf("parent: x=%d\n", --x); return 0; }
进程图画法:(分叉)
解释过程图:
例子:连续的两个 fork 函数
void fork2() { printf("L0\n"); fork(); printf("L1\n"); fork(); printf("Bye\n"); }
例子:父进程中嵌套 fork
void fork4() { printf("L0\n"); if (fork() != 0) { printf("L1\n"); if (fork() != 0) { printf("L2\n"); } } printf("Bye\n"); }
例子:子进程中嵌套 fork
void fork5() { printf("L0\n"); if (fork() == 0) { printf("L1\n"); if (fork() == 0) { printf("L2\n"); } } printf("Bye\n"); }
0x09 回收子进程(Reaping Child Processes)
进程终止后,仍会占用系统资源!例如:退出状态、操作系统中的各种结构体。
我们称之为 "僵尸进程" ,是一种半死不活的玩意。
回收:父进程在终止子进程时执行(使用 wait 或 waitpid)
父进程会获得子进程退出状态信息,内核然后会删除僵尸子进程。
如果父进程不进行回收,如果任何一个父进程在不回收子进程的情况下终止,那么这个进程就是孤儿进程了,这个孤儿进程将被 init 进程领养,随后回收。因此,只需要在长时间运行的进程中显式地进行回收(例如:shell 和服务器)
0x0A 与子进程同步(Synchronizing with Children)
父进程通过以下系统调用之一来回收子进程:
pid_t wait(int *status)
暂停当前进程,直到其任一子进程终止。
返回子进程的 PID,并在 status 中记录退出状态。
pid_t waitpid(pid_t pid, int *status, int options)
wait 的更灵活版本:
可以等待特定的子进程或一组子进程。
如果没有子进程可回收,可以被告知立即返回。
等待子进程的例子
wait 状态码:(Status codes)
wait 的返回值是终止的子进程的 pid。
如果 status != NULL,则指向的整数将被设置为指示退出状态的值
比传递给 exit 的值提供更多的信息。
必须使用在 sys/wait.h 中定义的宏进行解码:
WIFEXITED,WEXITSTATUS,WIFSIGNALED, WTERMSIG,WIFSTOPPED,WSTOPSIG,WIFCONTINUED
另一个wait示例:如果有多个子进程完成,将以任意顺序进行处理。可以使用宏WIFEXITED和WEXITSTATUS来获取有关退出状态的信息
void fork10() { int i, child_status; for (i = 0; i < N; i++) { if ((fork()) == 0) exit(100 + i); /* Child */ } for (i = 0; i < N; i++) { /* Parent */ pid_t wpid = wait(&child_status); if (WIFEXITED(child_status)) printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status)); else printf("Child %d terminate abnormally\n", wpid); } }
waitpid:等待特定进程
pid_t waitpid(pid_t pid, int &status, int options)
暂停当前进程,直到特定进程终止,支持各种选项:
void fork11() { pid_t pid[N]; int i, child_status; for (i = 0; i < N; i++) { if ((pid[i] = fork()) == 0) exit(100 + i); /* Child */ } for (i = N - 1; i >= 0; i--) { pid_t wpid = waitpid(pid[i], &child_status, 0); if (WIFEXITED(child_status)) printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status)); else printf("Child %d terminate abnormally\n", wpid); } }
0x0B execve: 加载一个运行的程序
int execve(char *filename, char *argv[], char *envp[])
在当前进程中加载并运行:
- 可执行文件文件名
- 可以是目标文件或以 #!interpreter 开头的脚本文件(例如,#!/bin/bash,#!/usr/bin/python)
- … 带有参数列表argv,按照惯例,argv[0] 设置为文件名
- … 和环境变量列表 envp ,“name=value” 字符串(例如,USER=droh) ,参见 getenv、putenv、printenv
覆盖代码、数据和栈
- 保留 PID、打开的文件和信号上下文
仅被调用一次,不会返回
Called once and never returns
- … 除非发生错误
栈布局示例:在子进程中使用当前环境执行“/bin/ls -lt /usr/include”
if ((pid = Fork()) == 0) { /* Child runs program */ if (execve(myargv[0], myargv, environ) < 0) { printf("%s: %s\n", myargv[0], strerror(errno)); exit(1); } }
📌 [ 笔者 ] 王亦优 📃 [ 更新 ] 2023.3.21 ❌ [ 勘误 ] /* 暂无 */ 📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免, 本人也很想知道这些错误,恳望读者批评指正!
📜 参考资料 Bryant and O’Hallaron, Computer Systems: A Programmer’s Perspective, Third Edition Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/. |