本文为 Linux 软件编程学习第五天的笔记整理,系统梳理进程的核心概念、常用操作命令、进程调度与状态、核心 API 函数,并结合实战中遇到的循环 fork 进程爆炸、孤儿进程、kill 命令报错等真实踩坑场景,总结易错点与最佳实践,适合入门学习者参考。


一、进程基础概念

1.1 进程与程序的区别

  • 程序:存放在外存(磁盘)中的静态代码集合,是永久存在的文件,只占用磁盘存储空间。
  • 进程:是程序的一次动态执行过程,包含创建、调度、消亡完整生命周期;是操作系统进行资源分配和调度的基本单位,占用内存、CPU、文件描述符等系统资源。
对比维度 程序 进程
本质 静态二进制文件 动态执行实例
存储位置 磁盘外存 内存中运行
生命周期 永久存在 有创建、运行、退出的完整周期
资源占用 仅占用磁盘空间 占用内存、CPU、PID、文件描述符等系统资源
对应关系 一个程序可对应多个进程 一个进程对应一个运行的程序

1.2 32 位系统进程虚拟地址空间

每个进程都拥有独立的 0~4G 虚拟地址空间,由操作系统通过页表映射到物理内存,保证进程空间相互隔离。整体可划分为三大区域:

  1. 文本段(代码段) 存放程序的二进制机器指令,只读属性,多个进程可共享同一份代码物理内存。
  2. 数据段
    • 字符串常量
    • 未初始化的全局变量、静态变量(BSS 段,程序加载时自动初始化为 0,不占用磁盘空间)
    • 已初始化的全局变量、静态变量
  3. 系统数据段
    • 堆区:动态内存分配区,malloc/free在此管理,地址由低向高增长,由程序员手动控制。
    • 栈区:存放函数局部变量、函数参数、返回地址,地址由高向低增长,由系统自动分配回收。

核心特性:每个进程的地址空间完全独立,一个进程修改自己的内存不会影响其他进程,这也是 fork 后父子进程内存隔离的底层基础。

二、进程常用操作命令

2.1 进程查看命令

  • top:动态查看进程信息,默认按 CPU 占用率排序,可实时查看 PID、CPU 使用率、内存占用等核心指标。
  • ps -ef:查看系统全量进程信息,展示 PID、PPID、启动用户、启动时间、执行命令等信息。
  • ps -aux:查看进程运行状态,包含 CPU、内存占用率、进程状态码、虚拟内存大小等详细数据。
  • pstree:以树状结构展示进程间的父子关系,直观查看进程派生层级与归属。

2.2 进程终止命令

  • kill -9 进程PID:强制杀死指定 PID 的进程,9 号信号SIGKILL无法被捕获和忽略,可强制终止顽固进程。

    ❗ 注意:-9为连续整体,中间不能加空格,否则会报invalid signal specification语法错误。

  • killall -9 进程名:按进程名批量杀死所有同名进程,适合批量终止多实例程序。

2.3 前后台任务控制

  • ./a.out &:让程序在后台运行,终端可继续输入其他命令,不阻塞当前终端操作。
  • jobs:查看当前终端下所有后台任务,显示任务编号、运行状态与对应命令。
  • fg 任务编号:将指定后台任务切换到前台运行。

2.4 进程优先级控制

  • nice -n 优先级值 程序命令:启动程序时指定优先级,优先级值范围为 - 20~19,数值越小优先级越高。
  • renice -n 优先级值 进程PID:修改已运行进程的优先级,动态调整进程调度权重。

三、进程调度与进程状态

3.1 进程调度基础

多个进程需要同时在系统中运行,操作系统通过调度算法从就绪队列中选择进程分配 CPU 时间片,实现宏观并行、微观串行的执行效果。

  • 时间片:CPU 分配给单个进程运行的一小段时间,时间片用完后进程让出 CPU,重新进入就绪队列排队。

3.2 常见调度算法

  1. 先来先服务调度(FCFS) 按进程到达就绪队列的先后顺序调度,属于非抢占式算法,实现简单,但短作业等待时间长,对长作业更友好。
  2. 高优先级调度算法 按进程优先级高低分配 CPU,优先级高的进程优先运行,分为抢占式和非抢占式两种模式。
  3. 时间片轮转调度(RR) 每个进程分配固定大小的时间片,时间片用完强制让出 CPU,回到就绪队列尾部排队。公平性好、响应及时,是分时系统的核心调度算法。
  4. 多级队列反馈调度算法 设置多个优先级队列,高优先级队列时间片短,低优先级队列时间片长;新进程默认进入最高优先级队列,时间片用完未执行完则降级,长时间未调度的进程可升级,兼顾响应速度与长作业执行效率。
  5. 负载均衡调度算法 多核 CPU 场景下,将进程均匀分配到不同 CPU 核心,避免单核心负载过高,提升整体 CPU 利用率。

3.3 进程七大状态

  1. 就绪态(R):具备运行条件,等待系统分配 CPU,位于就绪队列中。
  2. 运行态(R):正在占用 CPU 执行指令。
  3. 可中断等待态(S):因等待资源(如 IO 操作、sleep 休眠)挂起,等待过程中可被信号唤醒。
  4. 不可中断等待态(D):通常等待硬件 IO 操作,不可被信号打断,必须等待事件完成才能唤醒。
  5. 停止态(T):进程收到暂停信号后进入停止状态,可通过对应信号恢复运行。
  6. 僵尸态(Z):进程代码执行结束,但 PCB(进程控制块)资源未被父进程回收,残留系统中。
  7. 结束态(X):进程所有资源被系统回收,是进程销毁的最终状态。

四、进程核心 API 函数详解

4.1 fork:创建子进程

函数原型

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

返回值

  • 成功:父进程中返回子进程的 PID 号;子进程中返回 0
  • 失败:返回 -1,并设置 errno

执行逻辑: 调用 fork 后,操作系统创建子进程,复制父进程的地址空间、文件描述符表等资源;fork 返回后,父子两个进程从 fork 的下一行代码开始并发执行,执行顺序由操作系统调度决定,无固定先后。

实战重点:循环创建 N 个子进程的标准写法

错误写法(高频踩坑):子进程分支不加 exit,会导致子进程继续执行循环,派生大量孙进程,进程数量呈2^n指数爆炸。

// 错误示例:子进程无exit,进程指数级增长
for(int i = 0; i < 8; i++){
    pid_t pid = fork();
    if(pid == 0){
        printf("子进程%d\n", i);
        // 没有exit!子进程会继续循环,创建孙子进程
    }
}

标准正确写法:子进程执行完业务逻辑后,立即调用exit(0)退出,保证只有父进程持续循环创建子进程。

// 正确模板:循环创建N个子进程
for(int i = 0; i < 8; i++){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork fail");
        return -1;
    }
    if(pid == 0){ // 子进程分支
        printf("子进程%d, PID:%d, PPID:%d\n", i, getpid(), getppid());
        exit(0); // 子进程直接退出,不再参与后续循环
    }
    // 父进程跳过if,继续下一轮循环
}

4.2 getpid / getppid

pid_t getpid(void);  // 获取当前进程的PID号
pid_t getppid(void); // 获取当前进程父进程的PID号

4.3 exit:进程退出

函数原型

#include <stdlib.h>
void exit(int status);
  • 功能:终止当前进程,status 为进程退出状态码,可被父进程通过 wait 函数获取。
  • 与 return 的区别:
    • main 函数中调用exitreturn效果基本一致,都会终止进程。
    • 普通函数内return仅结束当前函数,exit会直接终止整个进程。

4.4 wait:回收子进程

函数原型

#include <sys/wait.h>
pid_t wait(int *wstatus);

核心功能

  1. 阻塞父进程,等待任意一个子进程退出
  2. 回收退出子进程的 PCB 资源,避免产生僵尸进程
  3. 获取子进程的退出状态信息

参数解析wstatus为传出参数,保存子进程的退出状态,需通过专用宏解析:

作用
WIFEXITED(wstatus) 判断子进程是否为正常退出
WEXITSTATUS(wstatus) 获取子进程正常退出的状态码,仅 WIFEXITED 为真时有效
WIFSIGNALED(wstatus) 判断子进程是否被信号杀死
WTERMSIG(wstatus) 获取杀死子进程的信号编号,仅 WIFSIGNALED 为真时有效

返回值:成功返回被回收子进程的 PID,失败返回 - 1。

五、经典问题:僵尸进程与孤儿进程

5.1 僵尸进程

产生原因:子进程先于父进程退出,父进程未调用wait/waitpid回收子进程资源,子进程的 PCB 残留在系统中,成为僵尸进程。 危害:不占用用户内存等运行资源,但会占用 PID 号;系统 PID 数量有上限,大量僵尸进程会导致无法创建新进程。

避免方法

  1. 父进程主动调用wait/waitpid回收子进程,是最规范、最推荐的方式。
  2. 让父进程先退出,子进程变为孤儿进程,由 init 进程收养并自动回收。
  3. 捕捉SIGCHLD信号,在信号处理函数中异步回收子进程,是工业界常用的非阻塞方案。

5.2 孤儿进程

产生原因:父进程先于子进程退出,子进程失去父进程,成为孤儿进程。 系统处理:Linux 系统中,孤儿进程会被 init(或 systemd)进程自动收养,子进程的 PPID 会变为 init 进程的 PID。

实战现象:父进程创建完子进程直接 return 退出,后续调度运行的子进程 PPID 不再是原父进程 PID,就是变成了孤儿进程。 注意:孤儿进程不会造成资源泄漏,退出后由 init 自动回收,不属于严重问题,但属于不规范的编程写法。

六、实战踩坑与易错点总结

  1. 循环 fork 不加 exit,进程指数爆炸 子进程会继续执行循环代码,持续派生孙进程,8 次循环会产生 256 个进程,完全失控。子进程分支必须加exit(0)终止。
  2. 父进程提前退出,子进程变孤儿 父进程创建完子进程后直接 return 退出,部分后调度的子进程会变成孤儿进程,PPID 变为系统 init 进程 PID。需通过 wait 让父进程等待子进程全部结束后再退出。
  3. kill 命令语法错误kill - 9 PID(- 和 9 之间加空格)会报invalid signal specification错误,正确写法为kill -9 PID-9为连续整体,中间不能有空格。
  4. 进程执行顺序不可控 fork 后父子进程、多个子进程的执行顺序由操作系统调度器决定,输出乱序是正常现象。业务逻辑不能依赖执行顺序,如需控制顺序需使用进程间同步机制。
  5. wait 的双重作用 wait 不仅能回收僵尸进程,还具备阻塞父进程的同步功能,可保证子进程执行完后父进程再继续运行,实现进程间时序同步。

以上就是系统编程第五天进程模块的全部核心知识点。进程是 Linux 系统编程的核心基础,其中 fork 机制、地址空间隔离、僵尸 / 孤儿进程是高频考点,实战中尤其要注意循环 fork 的 exit 写法和子进程回收,避免踩坑。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐