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 环境变量查找
相关推荐
AI行业学习15 小时前
CC-Switch 下载、安装windows\macOS \Linux 安装
linux·运维·macos
KaMeidebaby15 小时前
卡梅德生物技术快报|基因测序技术在 46,XY 性发育障碍变异筛查中的流程与数据分析
服务器·前端·数据库·人工智能·算法·数据挖掘·数据分析
mosaic_born15 小时前
systemctl restart reload enable 重启服务时的区别
linux
m0_7381207215 小时前
渗透测试基础——黑盒测试下的Web漏洞挖掘与利用解析(二)
服务器·前端·python·网络协议·安全·网络安全
文青小兵15 小时前
Linux云计算——docker compose haibor elfk (四)
linux·服务器·docker·云计算
思麟呀15 小时前
C++11并发编程:互斥锁
linux·开发语言·c++·windows
顺风尿一寸15 小时前
深度解析 Linux touch 命令:从用户输入到磁盘 Inode 的完整旅程
linux
j_xxx404_16 小时前
Linux 线程日志系统设计:从策略模式、RAII 到 pthread 线程安全与内核写入路径|附源码
linux·运维·服务器·开发语言·c++·人工智能·策略模式
keke.shengfengpolang16 小时前
2026出纳职业能力提升指南:从“收付款”到“洞察资金流”
大数据·服务器·人工智能
明天…ling16 小时前
CentOS 7 安装 Docker 踩坑全记录(含 sudo 权限、yum 源失效、命令报错解决方案)
linux·docker·centos