news 2026/6/23 22:36:42

进程相关的函数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
进程相关的函数

进程创建

1.fork的本质:一次调用,两次返回

这是fork最让初学者困惑的地方。

函数原型

#include <unistd.h> pid_t fork(void);
  • 现象: 你在代码里只写了一行fork(),但程序运行后,这一行代码似乎“执行”了两次,并且返回了两个不同的值 。
为什么会有两次返回?

当你的程序执行到fork()函数内部时,控制权转移到了操作系统内核。内核做了一件惊天动地的事:

  1. 复制:内核以父进程为模板,克隆了一个一模一样的子进程。
    • 子进程也有自己的 PCB (task_struct)。
    • 子进程也有和父进程一样的代码、数据、文件描述符等。
    • 关键点:子进程的程序计数器 (PC)(记录代码执行到哪一行了)也和父进程一样,都指向fork()函数刚刚执行完的位置 。
  1. 分裂:当内核处理完复制工作,准备从fork()函数返回时,系统中已经有了两个正在运行的进程(父进程和子进程)。
  2. 返回
    • 内核让父进程fork返回,带回子进程的 PID
    • 内核让子进程fork返回,带回0

2. 深入理解返回值:为什么是 0 和 PID?

设计这两个不同的返回值是有深刻用意的 :

  • 父进程返回子进程 PID
    • 父进程可能生了很多孩子(调用多次fork)。父进程必须拿到这个 ID,才能在将来通过waitpid(pid)准确地找到并回收这个特定的子进程,或者给它发信号。
  • 子进程返回 0
    • 子进程不需要fork告诉它父进程是谁,因为子进程可以随时调用getppid()获取父进程 ID。
    • 更重要的是,返回 0 是为了让子进程知道:“我就是那个被创造出来的新生命”。
  • 出错返回 -1
    • 如果系统进程太多(内存不足或 PID 耗尽),创建失败,只会在父进程返回 -1 。
灵魂拷问:同一个变量id怎么可能既等于 0 又大于 0?

看这段经典代码:

pid_t id = fork(); if (id == 0) { // 子进程逻辑 } else if (id > 0) { // 父进程逻辑 }

解释: 并非同一个变量同时有两个值。而是在两个独立的进程空间里,各有一个叫id的变量

  • 父进程里的id变量被赋值为子进程 PID(比如 1234)。
  • 子进程里的id变量被赋值为 0。 它们只是名字相同,但在物理内存中是完全隔离的两个变量 。

3. 核心机制:写时拷贝 (Copy-On-Write, COW)

这是 Linuxfork高效的秘诀。如果不理解这个,你就无法理解为什么fork即使拷贝几 GB 内存的进程也极其迅速。

误区

早期 Unix 的fork是“傻拷贝”:父进程有 1GB 内存,fork就立马申请 1GB 物理内存,把数据全部拷贝一份给子进程。这既浪费时间,又浪费内存。

真相:Linux 的惰性策略

Linux 采用了写时拷贝技术 :

  1. Fork 刚完成时(只读共享)
    • 父子进程的页表(虚拟地址到物理地址的映射表)是完全一样的。
    • 它们指向同一块物理内存
    • 关键动作:内核把这些共享的物理内存页标记为“只读” (Read-Only)
  1. 当任意一方试图写入时(触发拷贝)
    • 比如子进程执行g_val = 100;
    • CPU 发现正在往“只读”页面写入数据,触发缺页中断 (Page Fault)
    • 内核捕获这个中断,发现这是因为 COW 导致的。
    • 执行拷贝:内核立刻申请一个新的物理内存页,把原来的数据拷贝过来,把新页面的权限改为“可读写”,然后让子进程的页表指向这个新页面。
    • 父进程的页表依然指向旧页面(权限也恢复为可读写)。

结论

  • 如果父子进程都只读数据,不修改,物理内存永远共享,零拷贝
  • 只有被修改的那一页数据才会被复制。这就是为什么fork极快,且节省内存 。

4. 虚拟地址的“欺骗”

// 伪代码 int g_val = 0; if (fork() == 0) { g_val = 100; printf("%d, %p\n", g_val, &g_val); // 输出: 100, 地址: 0x601040 } else { sleep(1); printf("%d, %p\n", g_val, &g_val); // 输出: 0, 地址: 0x601040 }
  • 现象:父子进程打印的g_val地址竟然一模一样(0x601040),但值却不同。
  • 解释
    • 0x601040虚拟地址。父子进程拥有完全一样的虚拟地址空间布局。
    • 由于发生了写操作(g_val = 100),触发了写时拷贝
    • 在物理内存层面,父进程的0x601040映射到物理页 A
    • 子进程的0x601040映射到物理页 B
    • 用户看到的只是虚拟地址这个“门牌号”,却不知道背后指向了不同的“房间” 。

进程的退出

1. 进程退出的三种场景

在 Linux 看来,进程退出只有三种情况 1:

  1. 代码跑完,结果正确return 0
  2. 代码跑完,结果不正确return非 0(比如文件不存在、权限不足)。
  3. 代码没跑完,异常终止:程序崩溃了(野指针、除零错误),或者被信号(kill -9)杀死了。

关键点:只有前两种情况(正常退出),进程的退出码 (Exit Code)才有意义。如果进程是异常终止的(被杀死的),它的退出码是无意义的,我们需要关心的是它是被哪个信号杀死的。

2. 退出码 (Exit Code)
  • 概念main函数的返回值,或者exit(n)中的n
  • 规范0表示成功,非0表示失败(不同的数字代表不同的错误原因)。
  • 查看方式:在 Shell 中运行完程序后,立刻输入echo $?可以查看上一个进程的退出码 2。
3. 核心考点:exitvs_exit

这是面试常客。Linux 提供了两个退出函数,虽然结果都是进程结束,但过程不同。

  • _exit(int status)
    • 身份:系统调用 (System Call),直接由内核提供 3。
    • 行为冷酷无情。立刻关闭进程,回收内存,不刷新缓冲区。如果你的printf内容还在缓冲区里没打印出来,调用_exit后这些数据就丢了。
  • exit(int status)
    • 身份:库函数 (Library Function),由 C 标准库提供 4。
    • 行为温柔体贴。它在调用_exit之前,会做很多收尾工作:
      1. 执行用户注册的清理函数(atexit)。
      2. 刷新缓冲区(这是最大的区别):把没打印出来的printf数据强制刷到屏幕或文件中 5。
      3. 最后才调用_exit

代码验证

printf("hello"); // 注意没有 \n,数据会暂存在缓冲区 _exit(0); // 屏幕上什么都不会打印,因为缓冲区直接被丢弃了 // exit(0); // 如果换成这个,屏幕会打印 hello

进程的等待

子进程死了,变成了僵尸 (Zombie),父进程必须负责回收它的资源(PCB)。

1. 为什么要等待?
  1. 防僵尸:解决内存泄漏问题 6。
  2. 获知结果:父进程需要知道子进程的任务完成得怎么样(是成功了,还是被杀死了?)7。
2. 等待的方法:waitwaitpid

A.wait(int* status)—— 简单粗暴

  • 功能:等待任意一个子进程退出。
  • 行为:如果子进程没退,父进程就阻塞(死等),直到有子进程退出为止 8。

B. waitpid(pid_t pid, int* status, int options) —— 精准控制 9

这是更常用的函数,因为它更灵活。

  • pid参数
    • pid > 0:等待指定的那个子进程(比如 PID=1234)。
    • pid = -1:等待任意子进程(等同于wait)。
  • options参数
    • 0阻塞等待。和wait一样,子进程不完我不走。
    • WNOHANG非阻塞等待
      • 这是高并发程序的关键。父进程会问一下内核:“子进程结束了吗?”
      • 如果没有结束,waitpid立刻返回0,父进程可以先去干别的事,过会儿再来问(轮询)。
      • 如果结束了,返回子进程 PID。
      • 如果出错了,返回 -1。
3. 深度解剖:status位图

wait/waitpid的参数status是一个输出型参数。它不仅仅是一个整数,而是一个位图。我们需要像看“验尸报告”一样解读它。

我们只关注低 16 位 10:

位区域

含义

提取宏

高 8 位 (8-15)

退出码(正常退出才有意义)

WEXITSTATUS(status)

低 7 位 (0-6)

终止信号(异常终止才有意义)

status & 0x7F

第 7 位

Core Dump 标志

-

判断流程

  1. 先看低 7 位(是否收到信号):
    • 如果低 7 位是 0,说明是正常退出。此时再看高 8 位拿退出码。
    • 如果低 7 位不是 0,说明是异常退出(被杀)。此时高 8 位的退出码是无效的,不用看。

代码示例

int status; pid_t ret = waitpid(id, &status, 0); if (ret > 0) { if (WIFEXITED(status)) { // 宏:判断是否正常退出 (低7位是否为0) printf("正常退出,退出码: %d\n", WEXITSTATUS(status)); } else { printf("异常退出,被信号杀死: %d\n", status & 0x7F); } }

进程的替换

我们在fork之后,子进程默认执行的是和父进程一样的代码(或者父进程代码的副本)。但通常我们创建子进程,是为了让它去执行一个全新的程序(比如你在 Shell 里输入ls,是希望运行/bin/ls这个程序,而不是再跑一遍 Shell 的代码)。

这时候,就需要exec函数族出场了。

1. 替换原理:

当进程调用exec系列函数时,内核会进行一场彻底的“大换血” :

  1. 清空:内核会把当前进程的用户空间完全清空(代码段、数据段、堆、栈统统不要了)。
  2. 加载:内核找到你指定的那个新程序(比如磁盘上的ls可执行文件),把它的代码和数据加载到内存中。
  3. 重置:重置程序计数器 (PC),指向新程序的入口(通常是_start->main)。
  4. 执行:进程开始执行新程序的代码。

核心特征 (面试考点)

  • PID 不变:这就好比一个人“夺舍”了。躯壳(PCB、PID、PPID)还是原来那个,但灵魂(内存里的代码和数据)已经完全变成了另一个人 。
  • 不创建新进程exec只是用新程序覆盖了旧程序,没有产生新的进程 ID。
  • 一次调用,绝不返回exec函数一旦调用成功,当前进程原本后续的代码就直接灰飞烟灭了,根本没有机会执行“return”。只有在调用失败时(比如找不到文件),它才会返回 -1 。
2.exec函数族:

Linux 提供了 6 个以exec开头的库函数,它们功能一样,只是传参方式不同。记住后缀的含义就能分清了 :

  • l (list):参数用列表一个个列出来,最后必须以NULL结尾。
  • v (vector):参数放进一个数组 (vector)里传进去。
  • p (path):自动在环境变量PATH里找程序,不用写全路径(比如写"ls"就会自动找/bin/ls)。
  • e (env):不使用当前环境变量,而是自己组装一套环境变量传给新程序。

最常用的两个:

execl/execlp(列表传参): 适合参数已知且少的情况。

// 执行 ls -l -a // 这里的第一个 "ls" 是程序名,第二个 "ls" 是 argv[0](占位,但也得写),后面是参数 execlp("ls", "ls", "-l", "-a", NULL);

execv/execvp(数组传参): 适合参数动态生成的情况(比如你自己写的 Shell,用户输入的参数个数不确定,解析后放在数组里)。

char *const argv[] = {"ls", "-l", "-a", NULL}; execvp("ls", argv);

注意:只有execve是真正的系统调用 (System Call),其他 5 个都是 C 标准库封装的函数,它们底层最终都会调用execve

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 3:08:08

12 款 .NET PDF库,到底该选哪个库?

在 .NET 项目里打交道最多的文件格式之一&#xff0c;恐怕就是 PDF 了。发票、合同、报表、证书……几乎每个系统迟早都要生成或处理它。可问题来了&#xff1a;市面上 PDF 库五花八门——有的免费开源&#xff0c;有的收费不菲&#xff1b;有的写几行代码就搞定&#xff0c;有…

作者头像 李华
网站建设 2026/6/23 8:17:05

从入门到精通,R Shiny多用户权限管理系统搭建全记录

第一章&#xff1a;R Shiny多模态用户权限系统概述在构建企业级数据可视化应用时&#xff0c;R Shiny 提供了强大的交互能力&#xff0c;但默认情况下缺乏对用户身份认证与权限控制的内置支持。为满足不同角色对数据访问、操作和界面展示的差异化需求&#xff0c;需设计一套多模…

作者头像 李华
网站建设 2026/6/23 18:09:31

Dify版本回滚从入门到精通:一套被验证的标准化操作流程

第一章&#xff1a;Dify工作流版本回滚的核心概念在Dify平台中&#xff0c;工作流版本回滚是一项关键的运维能力&#xff0c;允许开发者在部署新版本后遇到异常时&#xff0c;快速恢复至先前稳定的状态。该机制依赖于版本控制系统与部署流水线的深度集成&#xff0c;确保每一次…

作者头像 李华
网站建设 2026/6/23 18:09:24

Frdbio®小鼠抗体纯化试剂盒

产品介绍&#xff1a;Frdbio 小鼠抗体纯化试剂盒用于纯化小鼠血清,腹水和含有鼠源抗体的制品;本试剂盒配备了纯化小鼠抗体所必需预装柱及核心试剂。本试剂盒中预装柱的填料为Protein G Beads 4FF。主要优势如下&#xff1a;本蛋白纯化试剂特点&#xff1a; Protein G Beads 4F…

作者头像 李华
网站建设 2026/6/23 5:21:55

告别冗余加载:构建高效量子计算运行时环境的6个不可忽视步骤

第一章&#xff1a;量子计算镜像的依赖精简在构建面向量子计算模拟器的容器化运行环境时&#xff0c;镜像体积与依赖复杂度直接影响部署效率和安全性。通过精简不必要的系统库和开发工具链&#xff0c;可以显著提升镜像启动速度并降低攻击面。依赖分析与最小化策略 采用静态分析…

作者头像 李华
网站建设 2026/6/23 6:29:26

Agent服务扩展难题,如何在Docker Compose中实现无缝横向扩容?

第一章&#xff1a;Agent服务扩展难题&#xff0c;如何在Docker Compose中实现无缝横向扩容&#xff1f;在微服务架构中&#xff0c;Agent类服务常用于采集日志、监控指标或执行远程指令。随着业务规模增长&#xff0c;单实例Agent难以应对高并发任务&#xff0c;亟需通过横向扩…

作者头像 李华