进程基础
1. 程序与进程
1.1 程序
1.1.1 程序的核心定义
- 程序是存储在硬盘上的静态指令集合和数据集合,是完成特定任务的代码文件;
- 程序仅占用磁盘资源,不占用 CPU、内存、进程 ID(PID)等系统运行资源;
- 不同操作系统的可执行程序格式对比:
| Windows | .exe(可执行文件) | 双击可直接运行,是 Windows 最核心的可执行程序格式 |
| Android | .apk(安装包) | 本质是 zip 压缩包,内含 dex 字节码、资源文件、清单文件等,安装后才能运行 |
| Linux | ELF(Executable and Linkable Format) | Linux/Unix 类系统的标准二进制文件格式,是所有可执行程序 / 库文件的统一格式 |
易错点:
- ✅ 在 Linux 中可执行文件的类型是 ELF 格式;
- ❌ 在 Linux 中 ELF 就是可执行文件(ELF 包含多种文件类型,并非仅可执行文件)。
1.1.2 ELF 格式文件类型
ELF 格式包含 4 类核心文件类型:
实用命令:readelf -h ELF格式文件 → 查看文件的 ELF 头信息。
1.1.3 ELF 格式文件组成
ELF 文件由以下核心部分构成:
- ELF 头:位于文件最开始,记录文件基本信息(文件类型、架构、入口地址、程序头表 / 节区头表的位置和大小);
- 程序头表:描述文件中的段信息,供加载器将文件加载到内存时使用;
- 节区:文件核心内容,包含代码(.text)、数据(.data、.bss)、符号表(.symtab)、重定位信息等;
- 节区头表:记录每个节区的名称、类型、大小、文件偏移量等,用于链接过程;
- 段:由一个或多个节区组成,是加载到内存的基本单元。
1.2 进程
1.2.1 进程的核心定义
- 进程是程序的一次动态执行过程,占用 CPU、内存等系统运行资源,有完整的生命周期;
- 一个程序可以对应多个进程(如多次启动同一个软件);
- 一个进程在运行过程中,通常只对应一个程序。
1.2.2 程序加载为进程的核心步骤
2. 进程相关核心信息与工具
2.1 task_struct 结构体
- 进程控制块(PCB)的核心实现,存储进程的所有核心信息(PID、状态、内存地址、优先级等);
- 存储路径:/usr/src/linux-headers-4.10.0-28-generic/include/linux/sched.h。
2.2 Linux 中查看进程的命令
| ps -ef / ps aux / ps axjf | 静态查看进程列表(不同参数展示维度不同) |
| top | 动态实时监控进程资源占用情况 |
| htop | 增强版 top(需手动安装:sudo apt-get install htop) |
| pstree | 以树形结构展示进程间的父子关系 |
2.3 进程的父子关系
- Linux 中除初始化进程外,其余所有进程都有父进程;
- 若父进程先退出,子进程会被其他进程(通常是 init/systemd 进程)接管,避免成为 “孤儿进程”。
3. 进程状态
3.1 进程的生命周期(状态转换)
3.1.1 诞生 → 就绪态
- 触发条件:父进程调用 fork 函数,内核为新进程分配 PID、PCB 等资源,新进程进入到就绪态;
- 就绪队列:进程已具备执行条件,但未被 CPU 调度,会在就绪队列中等待。
3.1.2 就绪态 → 执行态
- 触发条件:内核调用调度(sched)算法,选中就绪队列中的进程,为其分配 CPU 时间片,进程进入执行态。
3.1.3 执行态 → 就绪态
触发以下任一条件,进程会从执行态回到就绪态:
3.1.4 执行态 → 睡眠态
- 触发条件:进程因等待资源 / 事件主动放弃 CPU(如读文件、网络请求);
- 可中断睡眠(睡眠态):等待键盘输入等场景,可被信号唤醒,回到就绪态;
- 不可中断睡眠(挂起态):等待磁盘 IO 完成(如写入大文件),不响应信号(避免 IO 中断导致数据损坏),IO 完成后自动回到就绪态。
3.1.5 执行态 → 暂停态
- 触发条件:进程收到 SIGSTOP(暂停)、SIGTSTP(终端暂停)信号。
3.1.6 终止 → 僵尸态 → 死亡态
- 在 main 函数内执行 return
- 调用 exit() / Exit() / _exit() 函数
- 进程中最后一个线程执行 pthread_exit()
- 被信号(如 SIGKILL、SIGTERM)杀死
3.2 进程的状态转换图

4. fork函数
4.1 作用
- 用于产生一个新的子进程
4.2 接口规范
| 功能 | 创建一个新的子进程 |
| 头文件 | #include <unistd.h> |
| 函数原型 | pid_t fork(void); |
| 返回值 | 成功:父进程返回子进程的 PID(大于 0 的正整数),子进程返回 0失败:返回 -1 |
| 备注 | 函数执行成功后,会分别在父子两个进程中返回,返回值不同以区分身份 |
4.3 调用触发流程
- 为子进程分配唯一的PID,并将子进程的PPID(父进程 ID)设置为当前父进程的 PID。
- 复制父进程的进程控制块(PCB),包含进程状态、优先级、文件描述符、信号掩码等核心信息。
- 基于写时复制机制,建立父子进程的内存映射关系(非立即拷贝物理内存,只有当其中一方修改数据时才会实际复制,以提升效率)。
- 将子进程加入就绪队列,等待 CPU 调度执行。
4.4 两次返回特性
- 在父进程中,返回子进程的PID,父进程可以通过这个返回值来管理子进程。
- 在子进程中,返回0,子进程可以通过 getpid() 获取自身 PID,通过 getppid() 获取父进程 PID。
- 如何fork函数调用失败,则返回-1。
4.5 父子进程的异同点
4.5.1 完全相同的属性(子进程是父进程的 “复制品”,在子进程创建之初时)
- 实际 UID 和 GID,以及有效 UID 和 GID
- 所有环境变量
- 进程组 ID 和会话 ID
- 当前工作路径(除非用 chdir() 修改)
- 打开的文件描述符
- 信号响应函数
- 整个内存空间(栈、堆、数据段、代码段、标准 IO 缓冲区等)
4.5.2 不同的属性
- 进程号(PID):父子进程 PID 唯一,是进程的 “身份证号”。
- 记录锁:父进程对文件加的锁,子进程不会继承。
- 挂起的信号:父进程中 “悬而未决” 的信号,子进程不会继承。
4.6 核心注意事项
4.7 代码演示
4.7.1 示例代码
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
// 全局变量
int g_num2 = 123;
// 静态全局变量
static int num4 = 100;
int main()
{
// 局部变量
int num1 = 10;
// 静态局部变量
static int num3 = 111;
// 堆区
int* num5 = (int*)malloc(4);
*num5 = 1000;
pid_t pid = fork();
if (pid < 0)
{
perror("fork fail");
return –1;
}
// 子进程
if (pid == 0)
{
num1++;
printf("num1=%d, &num1 = %p\\n",num1, &num1);
g_num2++;
printf("g_num2=%d, &g_num2 = %p\\n",g_num2, &g_num2);
num3++;
printf("num3=%d, &num3 = %p\\n",num3, &num3);
num4++;
printf("num4=%d, &num4 = %p\\n",num4, &num4);
(*num5)++;
printf("*num5=%d, num5 = %p, &num5 = %p\\n",*num5, num5, &num5);
printf("我是子进程,pid=%d,ppid=%d\\n",getpid(),getppid());
}
// 父进程
if (pid > 0)
{
sleep(1);
printf("num1=%d, &num1 = %p\\n",num1, &num1);
printf("g_num2=%d, &g_num2 = %p\\n",g_num2, &g_num2);
printf("num3=%d, &num3 = %p\\n",num3, &num3);
printf("num4=%d, &num4 = %p\\n",num4, &num4);
printf("*num5=%d, num5 = %p, &num5 = %p\\n",*num5, num5, &num5);
printf("我是父进程,pid=%d,ppid=%d\\n",getpid(),getppid());
}
while(1)
{
}
}
4.7.2 运行结果

4.7.3 结论
- 子进程创建时,会完整拷贝父进程的栈数据、堆数据、全局数据、静态数据及代码段;
- 父子进程的内存空间相互独立,修改各自数据不会影响对方;
- 父子进程中变量的虚拟内存地址可以相同,但物理内存地址不同;
- 虚拟内存与物理内存的映射规则:
- 每个进程拥有独立的虚拟内存空间,代码中访问的变量地址均为虚拟地址,而非物理内存;
- 写时复制机制:子进程创建后,变量在未被修改时(未进行写操作),通常父子进程共享一份物理内存的映射表,这个时候,父子进程中变量对应的实际物理内存是相同的;如果其中一方需要改变数据(进行写操作),这个时候操作系统才会真正的改变物理内存,但是其虚拟内存地址依旧是相同的。
- 为保证父子进程代码执行逻辑一致,二者的虚拟内存地址设计为相同。
4.8 fork和vfork的区别
| 内存分配机制 | 写时复制:父子进程共享代码段,数据段 / 堆栈等仅在修改时拷贝,初始共享物理内存 | 完全共享地址空间:父子进程共用一套数据段和堆栈,无写时复制,子进程修改数据直接影响父进程 |
| 执行顺序 | 父子进程执行次序随机,由操作系统内核调度算法决定 | 强制子进程优先执行,父进程被阻塞,直到子进程调用 exec 函数簇或 _exit 退出 |
| 设计目的 | 适用于子进程独立执行父进程代码,或后续执行任意操作 | 专为子进程创建后立即调用 exec 设计,减少内存复制开销,提升执行效率 |
| 安全性 | 高:父子进程地址空间相互隔离,数据修改互不影响 | 低:子进程修改数据会覆盖父进程数据;若子进程未及时调用 exec/_exit,易引发程序异常 |
| 返回值规则 | 成功:父进程返回子进程 PID,子进程返回 0;失败:返回 -1 | 与 fork 完全一致 |
| 子进程退出要求 | 可调用 exit() 或 _exit() 退出 | 必须调用 _exit() 退出,禁止调用 exit()(否则会刷新父进程 IO 缓冲区,导致数据混乱) |
5. exec函数簇
5.1 接口规范
功能:在进程中加载新的程序文件或者脚本,覆盖原有代码,重新运行
| 头文件 | #include <unistd.h> |
| 原型 | int execl(const char *path, const char *arg, …); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg, …, char *const envp[]); int execlp(const char *file, const char *arg, …); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]); |
| 参数 | path:即将被加载执行的 ELF 文件或脚本的路径file:即将被加载执行的 ELF 文件或脚本的名字arg:以列表方式罗列的 ELF 文件或脚本的参数argv:以数组方式组织的 ELF 文件或脚本的参数envp:用户自定义的环境变量数组 |
| 返回值 | 成功:不返回失败:-1 |
| 备注 | 1,函数名字带字母 l 意味着其参数以列表(list)的方式提供。2,函数名字带字母 v 意味着其参数以矢量(vector)数组的方式提供。3,函数名字带字母 p 意味着会利用环境变量 PATH 来找寻指定的执行文件。4,函数名字带字母 e 意味着用户提供自定义的环境变量。 |
5.2 参数格式要求
被加载的文件的参数列表必须以自身名字为开始,以 NULL 为结尾。
例如,加载当前目录下 a.out 并传参 "abcd":
execl("./a.out", "a.out", "abcd", NULL);
或用数组方式:
const char *argv[3] = {"a.out", "abcd", NULL};
execv("./a.out", argv);
5.3 执行后行为
exec 函数族执行成功后,原有程序代码会被新文件 / 脚本完全覆盖,后续代码不会执行,且函数不会返回。
只有在执行失败时,才会返回 -1,继续执行后续代码。
6. exit和_exit区别
6.1 exit执行流程
- 1、调用atexit注册退出处理函数。
- 2、刷新并关闭所有打开的标准IO流。
- 3、删除创建的临时文件(tmpfile函数)。
- 4、调用exit进入内核态,进程终止,同时将status(子进程的退出值)传递给父进程。
6.2 _exit执行流程:
- 1、直接触发内核态的系统调用,跳过所有用户态下的清理步骤。
- 2、内核负责关闭进程所有打开的文件描述符、释放内存资源等。
- 3、向父进程发送信号,传递status值。
6.3 函数exit和_exit的接口规范
| 核心功能 | 退出本进程 | 退出本进程 |
| 头文件 | #include <unistd.h> | #include <stdlib.h> |
| 函数原型 | void _exit(int status); | void exit(int status); |
| 参数 | status:子进程的退出值 | status:子进程的退出值 |
| 返回值 | 不返回 | 不返回 |
| 备注1 | 子进程正常退出,status 一般为 0; | 子进程正常退出,status 一般为 0; |
| 备注2 | 子进程异常退出,status 一般为非 0; | 子进程异常退出,status 一般为非 0; |
| 备注3 | 直接退出,不冲洗标准 IO 残留数据到内核,也不执行 “退出处理函数” | 退出时自动冲洗(flush)标准 IO 中残留的数据到内核;若进程注册了 “退出处理函数”,会自动执行这些函数后再退出 |
7. wait和waitpid区别
7.1 区别
| 等待任意一个子进程的退出,不可指定。 | 可以获取指定子进程的退出状态。 |
| 强制阻塞,直到有子进程退出才返回。 | 可以通过第三个形参来设置非阻塞。 |
7.2 函数wait和waitpid的接口规范
功能:等待子进程退出,获取其退出状态;可将子进程状态切换为 EXIT_DEAD,以便系统释放资源
| 头文件 | #include <sys/wait.h> |
| 原型 | pid_t wait(int *stat_loc);``pid_t waitpid(pid_t pid, int *stat_loc, int options); |
| 参数pid: | ①小于 – 1:等待组 ID 的绝对值为 pid 的进程组中的任一子进程 ②-1:等待任一子进程 ③0:等待调用者所在进程组中的任一子进程 ④大于 0:等待进程组 ID 为 pid 的子进程 |
| 参数stat_loc: | 子进程退出状态 |
| 参数option: | WCONTINUED:报告任一从暂停态出来且从未报告过的子进程的状态 WNOHANG:非阻塞等待 WUNTRACED:报告任一当前处于暂停态且从未报告过的子进程的状态 |
| 返回值wait(): | 成功:退出的子进程PID 失败:-1 |
| 返回值waitpid(): | 成功:状态发生改变的子进程 PID(如果 WNOHANG 设置,且由 pid 指定的进程存在但状态未发生改变,则返回 0) 失败:-1 |
| 备注 | 如果不需要获取子进程的退出状态,stat_loc 可以设置为 NULL |
7.3 子进程退出状态解析宏
下表中的宏用于解析通过 wait()/waitpid() 获取到的子进程退出状态:
| WIFEXITED(status) | 如果子进程正常退出,则该宏为真。 |
| WEXITSTATUS(status) | 如果子进程正常退出,则该宏将获取子进程的退出值。 |
| WIFSIGNALED(status) | 如果子进程被信号杀死,则该宏为真。 |
| WTERMSIG(status) | 如果子进程被信号杀死,则该宏将获取导致他死亡的信号值。 |
| WCOREDUMP(status) | 如果子进程被信号杀死且生成核心转储文件(core dump),则该宏为真。(注:该选项在某些 Unix 系统中无效,比如 AIX、sunOS) |
| WIFSTOPPED(status) | 如果子进程的被信号暂停,且 option 中 WUNTRACED 已经被设置时,则该宏为真。 |
| WSTOPSIG(status) | 如果 WIFSTOPPED(status) 为真,则该宏将获取导致子进程暂停的信号值。 |
| WIFCONTINUED(status) | 如果子进程被信号 SIGCONT 重新置为就绪态,该宏为真。 |
7.4 重点
退出状态 vs 退出值
- 退出状态是一个包含更多信息的整数值,而退出值只是子进程通过 exit()/return 返回的具体数值。
- 要想拿到退出值,必须先用 WIFEXITED() 判断子进程是否正常退出,再用 WEXITSTATUS() 提取。
核心转储文件
WCOREDUMP() 宏用于判断子进程是否在被信号杀死时生成了 core dump 文件,但它不是 POSIX 标准,在 AIX、sunOS 等系统中无效。
非阻塞等待
waitpid() 的 WNOHANG 选项可以让函数立即返回,避免父进程被阻塞,适合需要同时处理其他任务的场景。
8. 进程组、对话期、终端
8.1 进程组
- 1、由一个或多个共享进程组ID(PGID)的进程组成的集合
- 2、创建进程组的进程其PID = PGID,即使组长进程终止,进程组依然存在,直到组内所有进程全部结束。
- 3、一个进程只能属于一个进程组。
8.2 对话期(会话期)
- 1、一个或多个进程组的集合,会话由首进程创建,会话内所有进程共享一个控制终端。
- 2、创建会话的进程,其PID = 会话ID(SID),该进程不能是其他进程组的组长。
- 3、一个会话包含多个进程组,进程组又分为前台进程组和后台进程组。
- ①:前台进程组与控制终端关连,能够接收终端输入和信号。
- ②:后台进程组不与控制终端直接交互,通常不受终端信号影响。
- 4、只有非进程组组长的进程(组员进程)才能创建新会话,如果创建会话的进程是组长则创建失败。
- 5、一个会话最多只能绑定一个控制终端,它俩是一对一的关系。
8.3 终端
- 1、在Linux中的终端其实就是一个字符设备文件,作为进程与用户交互的接口,分为控制终端和非控制终端。
- 2、一个会话最多关联一个控制终端,由会话首进程打开。
- 3、控制终端的进程组ID指向当前前台进程组,终端输入的信号会发送给前台进程组。
- 4、当关闭控制终端时,内核会向前台进程组发送信号(SIGHUP)。
网硕互联帮助中心





评论前必须登录!
注册