一、什么是进程控制

进程控制是操作系统内核的核心功能,指操作系统对进程的创建、状态切换、调度、终止、资源回收等全生命周期进行管理的全部操作。

简单来说,进程控制负责进程从 “出生” 到 “死亡” 的所有管理工作,保障多个进程能有序、高效地共享 CPU 与系统资源。

二、进程控制的核心基石:PCB

操作系统管理进程的前提是记录每个进程的全部信息,这个 “进程档案” 就是进程控制块(Process Control Block, PCB),Linux 中对应结构体为task_struct

PCB 是进程存在的唯一标志,操作系统通过链表 / 哈希表管理所有 PCB,核心包含以下信息:

  1. 标识信息:PID(进程号)、PPID(父进程号)、用户 ID、组 ID;
  2. 状态信息:就绪、运行、阻塞、终止等当前状态;
  3. CPU 上下文:程序计数器、寄存器值、栈指针,用于进程切换时恢复执行现场;
  4. 内存信息:页表指针、地址空间范围、内存占用情况;
  5. 资源信息:打开的文件描述符、信号处理配置、IO 设备占用;
  6. 调度信息:进程优先级、时间片长度、调度队列指针。

通俗类比:PCB 就是每个进程的 “身份证 + 档案袋”,进程的所有属性、资源、状态都记录在内,操作系统管理进程本质就是操作对应的 PCB。

三、进程五大状态与转换逻辑

进程在生命周期中会在不同状态间切换,这是进程控制的核心逻辑。经典进程模型包含 5 种核心状态:

  1. 新建状态:进程正在创建,PCB 已生成,但尚未加入内存就绪队列;
  2. 就绪状态:进程已准备完毕,等待 CPU 调度分配时间片;
  3. 运行状态:进程正在 CPU 上执行指令;
  4. 阻塞状态(等待状态):进程等待某事件完成(如 IO、锁、信号),暂时无法运行,即使 CPU 空闲也不执行;
  5. 终止状态:进程执行结束或异常终止,等待系统回收资源。

状态转换触发条件

  • 新建→就绪:进程创建完成,加入就绪队列;
  • 就绪→运行:调度器选中该进程,分配 CPU 时间片;
  • 运行→就绪:时间片耗尽,或被高优先级进程抢占 CPU;
  • 运行→阻塞:进程发起 IO 请求、等待信号、申请锁失败,主动放弃 CPU;
  • 阻塞→就绪:等待的事件完成(如 IO 结束、信号到达),进程被唤醒;
  • 运行→终止:进程正常退出、被信号终止、出现致命错误。

四、核心进程控制操作 + 代码举例

Linux 通过系统调用实现进程控制,最核心的四类操作是创建、等待、终止、阻塞唤醒。

1. 进程创建:fork() 系统调用

fork()是 Linux 创建进程的核心方式,特点是调用一次,返回两次

  • 父进程中返回子进程的 PID;
  • 子进程中返回 0;
  • 创建失败返回 - 1。

子进程会复制父进程的地址空间,采用 ** 写时复制(Copy On Write)** 优化:创建时父子共享物理内存,只有某一方修改内存时,才复制对应页面。

代码示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("fork创建失败");
        return 1;
    } else if (pid == 0) {
        printf("【子进程】PID=%d, 父进程PID=%d\n", getpid(), getppid());
    } else {
        printf("【父进程】PID=%d, 子进程PID=%d\n", getpid(), pid);
    }
    return 0;
}

2. 进程等待:wait() / waitpid()

父进程通过等待操作回收子进程资源,避免产生 “僵尸进程”:

  • wait():阻塞父进程,直到任意一个子进程终止;
  • waitpid():可指定等待具体 PID 的子进程,支持非阻塞模式。

3. 进程终止

进程终止分为两类:

  • 正常终止main函数return、调用exit()/_exit()
  • 异常终止:收到终止信号、段错误等致命运行错误。

其中exit()会刷新 IO 缓冲区、执行注册的清理函数后再退出;_exit()直接进入内核终止进程,不处理用户态缓冲区。

4. 阻塞与唤醒

进程等待 IO、锁、信号等事件时,会主动调用阻塞原语进入阻塞状态;当事件完成时,内核调用唤醒原语,将进程从阻塞队列移至就绪队列。 例如调用read()读取磁盘文件时,进程会进入阻塞;数据读取完成后,内核唤醒该进程。

五、进程控制进阶场景与特殊进程

1. 进程替换:exec 系列函数

fork()创建的子进程完全复制父进程的代码和数据,而实际开发中,子进程往往需要执行全新的独立程序,这就需要进程替换(exec)

exec 系列函数会用新程序的代码、数据完整覆盖当前进程的地址空间,重新初始化代码段、数据段、堆、栈,但进程 PID 保持不变。Linux 下fork()+exec是创建全新业务进程的标准范式:父进程创建子进程,子进程立刻替换为目标程序执行。

代码示例:fork + exec 执行系统命令
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:替换为 ls -l 命令
        execl("/bin/ls", "ls", "-l", NULL);
        perror("execl执行失败"); // 只有出错才会走到这里
        return 1;
    } else if (pid > 0) {
        wait(NULL); // 父进程阻塞等待子进程执行完毕
        printf("子进程执行完成\n");
    }
    return 0;
}

2. 三类特殊进程

  • 僵尸进程:子进程终止后,父进程未调用wait()/waitpid()回收资源,子进程的 PCB 仍残留在系统中,成为僵尸进程。它几乎不占用内存资源,但会占用 PID 编号,大量僵尸进程会耗尽系统 PID 池。
  • 孤儿进程:父进程提前终止,子进程仍在运行,此时子进程会被 init/systemd 进程(PID=1)收养,由其负责后续资源回收,孤儿进程无系统危害。
  • 守护进程(Daemon):脱离终端、在后台长期稳定运行的特殊进程,常用于服务类程序(如 Web 服务、定时任务),通常通过两次 fork + 脱离会话组的标准范式创建。

3. 进程上下文切换的完整流程

当 CPU 从一个进程切换到另一个进程时,需要完成上下文的保存与恢复,这是进程控制的核心底层操作,完整流程如下:

  1. 保存当前进程的 CPU 寄存器、程序计数器等硬件上下文到其 PCB 中;
  2. 更新该进程的状态,从运行态转为就绪态或阻塞态,移入对应队列;
  3. 进程调度器从就绪队列中选中下一个要执行的进程,更新其状态为运行态;
  4. 从新进程的 PCB 中加载硬件上下文到 CPU 寄存器,同时切换页表以启用新进程的地址空间;
  5. 跳转到程序计数器对应的指令位置,恢复进程继续执行。

上下文切换存在固定的时间开销,频繁切换会降低 CPU 有效利用率,这也是高并发场景需要控制进程数量的核心原因。

六、进程控制与进程调度的区别

两者常被混淆,核心区别如下:

  • 进程控制:负责进程全生命周期管理(创建、状态切换、销毁),是进程的 “管理员”;
  • 进程调度:负责从就绪队列中挑选进程上 CPU 运行,是 CPU 资源的 “分配员”。

调度是进程控制的组成部分,进程控制为调度提供状态基础,二者配合实现多进程并发。

七、Linux 进程控制实操命令

结合命令行工具可以直观观察进程控制的实际效果:

  • ps aux:查看系统中所有进程的 PID、状态、内存占用、启动时间,对应 PCB 中的标识、状态、资源信息;
  • ps -ef --forest:以树状结构展示进程父子关系,直观呈现进程层次结构;
  • kill -9 PID:向指定进程发送 SIGKILL 信号,强制终止进程,属于异常终止控制;
  • top:实时查看进程的 CPU、内存占用,以及运行状态变化;
  • pstree:直接查看系统完整的进程树结构。

谢谢
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐