Linux进程控制(2)

1 进程等待

1-1 进程等待必要性

父进程必须对子进程进行等待,核心原因有三点:

  1. 避免僵尸进程与内存泄漏 子进程退出后,如果父进程完全不管,子进程会进入僵尸进程 状态,PCB 会一直留在系统中,造成内存泄漏,而且即使 kill -9 也无法杀死僵尸进程。
  2. 回收子进程资源 只有父进程通过 wait/waitpid 等待,内核才会释放子进程退出后占用的系统资源。
  3. **获取子进程的退出信息(可选但常用)**父进程需要知道子进程的执行结果:是正常完成、出错退出,还是被信号杀死,这在多进程协作中非常关键。

结论:进程等待的本质就是父进程通过系统调用,回收子进程资源 + 获取退出信息。

1-2 进程等待的方法

  1. 基础版:wait

    pid_t wait(int *status);

  2. 升级版:waitpid(更灵活)

    pid_t waitpid(pid_t pid, int *status, int options);

  • 返回值:
    • 成功:返回退出子进程的 PID;
    • 设置了 WNOHANG 且没有已退出子进程:返回 0;
    • 出错:返回 -1,并设置 errno
参数详解
  1. pid:指定要等待的子进程
    • pid == -1:等待任意子进程(和 wait 等价);
    • pid > 0:等待 PID 等于 pid 的特定子进程。
  2. status:输出型参数,保存子进程的退出状态(和 wait 共用一套解析宏)。
  3. options:选项参数
    • 默认 0:阻塞等待(子进程没退出,父进程就一直卡在 waitpid 这里);
    • WNOHANG:非阻塞等待。如果指定的子进程还没退出,waitpid 直接返回 0,父进程可以继续做别的事,之后再轮询。

总结:

  • 如果子进程已经退出,调用 wait/waitpid 时,wait/waitpid 会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用 wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

1-3 获取子进程status

  • wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递 NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低 16 比特位):

1. status 的本质:位图结构

status 是一个 int 类型,里面的二进制位被划分为两部分,用来保存两个关键信息:

  • 低 7 位 :终止信号编号(signumber),表示进程是不是被信号杀死的;
  • 第 8-15 位 :退出码(exit_code),表示进程正常退出时的返回值(exit(code)main 函数 return code 里的 code)。

2. 常用解析宏

  • WIFEXITED(status):判断进程是否正常终止
    • 非 0:正常退出(没有被信号杀死);
    • 0:被信号杀死。
  • WEXITSTATUS(status):提取进程的退出码
    • 只有 WIFEXITED(status) 为真时,这个值才有意义;
    • 比如 exit(1)return 1,这里就能拿到 1(注意图里的 status:256 就是 1 << 8,对应退出码 1)。

3. 三种退出场景总结

  1. 正常终止(代码跑完)signumber = 0exit_code 是进程的返回值(如 return 0 对应 exit_code=0);
  2. 正常终止但出错退出signumber = 0exit_code != 0(如 exit(1) 对应 exit_code=1);
  3. 被信号杀死signumber != 0,此时 exit_code 无意义。

1-4 关键细节补充

  • 阻塞 vs 非阻塞等待
    • 阻塞等待(waitwaitpidoptions=0):父进程会一直卡着,直到有子进程退出,适合 "必须等子进程完成才能继续" 的场景;
    • 非阻塞等待(waitpidWNOHANG):父进程不会卡住,可以边做自己的事边轮询子进程状态,需要写循环反复调用 waitpid
  • 僵尸进程的解决 只要父进程调用 wait/waitpid,不管是否关心退出码,都会回收子进程的 PCB,从而解决僵尸进程问题。
  • exit()return 的区别
    • exit(code):直接终止整个进程,code 就是进程的退出码;
    • main 函数里的 return code:等价于 exit(code),其他函数里的 return 只是返回函数值,不会终止进程。

1-5 阻塞等待和非阻塞等待

一、基础背景:fork 父子进程的运行与退出

  1. 谁先运行? fork() 创建子进程后,父子进程谁先被调度执行,完全由操作系统的调度器决定,代码中无法控制顺序。
  2. 谁先退出? 通常是子进程先退出 ,然后由父进程通过 wait/waitpid 负责回收子进程的资源,避免产生僵尸进程。

二、进程等待的两种模式:阻塞 vs 非阻塞

1. 核心系统调用:waitpidoptions 参数
复制代码
pid_t waitpid(pid_t pid, int *status, int options);
  • options = 0阻塞等待
  • options = WNOHANG非阻塞等待

2. 阻塞等待(对应图里的 "李四进程 → 阻塞等待 → 操作系统")
  • 工作方式 :父进程调用 waitpid 后,如果子进程还没退出,父进程会直接被挂起(阻塞),什么都不做,一直等到子进程退出才继续运行。
  • 缺点:等待期间父进程完全 "卡住",无法处理其他任务,时间被白白浪费。
  • 图中比喻:就像李四一直等张三,啥也不干,只能干等着。

3. 非阻塞等待(对应图里的 "非阻塞轮询 → 手机、C、新闻")
  • 工作方式 :父进程调用 waitpid 时设置 WNOHANG,如果子进程还没退出,waitpid 会直接返回 0,父进程不会被挂起,可以继续执行自己的代码。之后父进程可以通过循环,反复调用 waitpid 轮询子进程状态。
  • 优点:等待期间父进程可以处理其他任务(比如图里的看手机、写代码、看新闻),CPU 时间被充分利用,效率更高。
  • 图中比喻:就像李四每隔一会儿问一下张三完事没,没问的时候可以做自己的事。

三、两种等待方式的核心对比

对比维度 阻塞等待 非阻塞等待(WNOHANG)
options 参数 0 WNOHANG
子进程未退出时的行为 父进程挂起,停止运行 父进程返回 0,继续运行
CPU 利用率 低(父进程空等) 高(父进程可处理其他任务)
实现复杂度 简单,直接调用一次即可 稍复杂,需要写循环轮询
适用场景 父进程没有其他任务,只需要等子进程 父进程需要同时处理多个任务

四、关键理解点

  1. 等待时间由子进程决定:父进程需要等待多久,完全取决于子进程什么时候退出,父进程无法控制这个时间。
  2. 效率差异的本质:阻塞等待是 "被动等待",非阻塞等待是 "主动轮询 + 并行处理",所以非阻塞等待的资源利用率更高。
  3. WNOHANG 的作用 :它只是让 waitpid 不阻塞,并不是 "不等待",父进程还是需要通过循环轮询,最终完成子进程的回收。

2 进程程序替换

程序替换是通过特定的接口,加载磁盘上的⼀个全新的程序(代码和数据),加载到调用进程的地址空间中!

一、核心现象:exec 程序替换的直观效果

  1. 示例现象

    • 代码中调用 printf("我变成了一个进程:%d\n", getpid()); 后,紧接着调用 exec 系列函数。
    • 运行结果:只会打印这一行,exec 后面的所有代码(后续的printf)都不会执行。
    • 关键结论:exec 成功调用后,原进程的代码和数据会被新程序完全替换,原程序后续代码不再执行
  2. 是否创建了新进程?

    • 没有!
    • exec 只是替换了进程的代码段、数据段、堆和栈,进程的 PID(进程 ID)、PCB(进程控制块)这些内核信息完全不变。
    • 所以 getpid() 拿到的 PID 还是原来的进程号,只是跑的程序变了。

二、程序替换的本质原理

  1. 替换的核心动作

    • 把磁盘上可执行文件(ELF 格式)的代码段和数据段,加载到当前进程的内存空间中。
    • 同时更新进程的页表,让虚拟内存映射到新程序的物理内存。
    • 原进程的代码段、数据段会被直接覆盖,栈和堆也会被重置,所以原程序后续代码无法执行。
  2. 为什么能做到?

    • 程序运行的前提是加载到内存,这个加载过程必须由操作系统完成。
    • exec 系列函数本质上是操作系统提供的系统调用,只有内核有权限修改进程的内存映射,完成程序的加载与替换。

三、exec 函数的关键特性

  1. 返回值的特殊含义

    • 调用成功:没有返回值,因为原程序已经被替换,后续代码不会执行,程序会直接从新程序的入口点开始运行。
    • 调用失败:返回 -1,此时原程序的代码还在,可以继续执行后续逻辑。
  2. fork() 的经典搭配

    • 实际开发中,很少直接在父进程里调用 exec,通常的流程是:
      1. fork() 创建子进程(复制父进程的 PCB 和内存空间);
      2. 在子进程中调用 exec 替换程序;
      3. 父进程通过 wait/waitpid 回收子进程资源。
    • 这样父进程的代码不会被破坏,同时实现了 "子进程执行新程序" 的效果。

四、补充:常见的 exec 系列函数

exec 系列有多个变体,核心功能一致,只是传参方式不同:

函数名 传参方式 路径查找
execl 列表传参,以 NULL 结尾 需指定完整路径
execlp 列表传参,以 NULL 结尾 可通过 PATH 环境变量查找
execv 数组传参 需指定完整路径
execvp 数组传参 可通过 PATH 环境变量查找
相关推荐
热爱Liunx的丘丘人2 小时前
PlayBook常用的模块编写
linux·服务器·ansible
SilentSamsara2 小时前
etcd 运维:数据一致性、备份恢复与性能调优
运维·服务器·数据库·kubernetes·kubectl·k8s·etcd
RH2312112 小时前
2026.4.21Linux 共享内存
linux·服务器·网络
wanhengidc2 小时前
云主机的核心原理与架构
运维·服务器·科技·游戏·智能手机·架构
idolao2 小时前
PE启动盘制作与启动教程 Windows版:NTFS格式化+一键制作+双模式引导指南
linux·运维·服务器
程序员晨曦2 小时前
理解函数调用Function Call
java·运维·服务器
positive_zpc2 小时前
计算机网络——网络层(二)
服务器·网络·计算机网络
花无缺就是我2 小时前
内网穿透哪个好,之神卓互联Linux版Arm安装教程2026最新
linux·运维·arm开发
of Watermelon League2 小时前
SQL server配置ODBC数据源(本地和服务器)
运维·服务器·github