news 2026/3/20 23:06:32

进程相关的函数

作者头像

张小明

前端开发工程师

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/3/20 19:58:32

【搜索排序性能跃迁】:基于Dify的重排序算法对比与落地实践

第一章&#xff1a;搜索排序性能跃迁的背景与挑战在现代搜索引擎和推荐系统中&#xff0c;排序算法的性能直接影响用户体验与系统吞吐能力。随着数据规模呈指数级增长&#xff0c;传统排序策略面临响应延迟高、资源消耗大等瓶颈&#xff0c;亟需实现性能跃迁。业务场景对实时性…

作者头像 李华
网站建设 2026/3/18 20:33:32

Tesseract在Dify中的误差补偿机制详解:提升生产环境OCR稳定性的关键

第一章&#xff1a;Dify Tesseract 的识别误差修正在使用 Dify 集成 Tesseract 进行 OCR 文本识别时&#xff0c;常因图像质量、字体样式或语言模型限制导致识别结果出现偏差。为提升识别准确率&#xff0c;需对原始输出进行系统性误差修正。预处理优化图像输入 Tesseract 对输…

作者头像 李华
网站建设 2026/3/16 19:28:14

负载突增怎么办?,Docker MCP 网关动态均衡策略深度解析

第一章&#xff1a;负载突增怎么办&#xff1f;Docker MCP 网关动态均衡策略概述在微服务架构中&#xff0c;当系统面临突发流量时&#xff0c;传统静态负载均衡机制往往难以快速响应&#xff0c;导致部分容器过载而其他资源闲置。Docker MCP&#xff08;Microservice Control …

作者头像 李华
网站建设 2026/3/15 16:26:21

文献学考试重点梳理与复习指南

科研新人做综述时最痛苦&#xff1a;一搜就是几十页论文&#xff0c;重复、无关、没用。下面三款工具让我效率翻倍。 ① WisPaper&#xff08;智能学术搜索 文献管理&#xff09; 官网&#xff1a;https://www.wispaper.ai WisPaper 能通过关键词和语义搜索快速找到相关文献&…

作者头像 李华
网站建设 2026/3/19 11:39:52

LobeChat能否设置敏感词过滤?内容安全控制机制介绍

LobeChat能否设置敏感词过滤&#xff1f;内容安全控制机制介绍 在企业级AI助手日益普及的今天&#xff0c;一个看似简单的问题却频频被提出&#xff1a;用户输入“如何绕过公司防火墙”时&#xff0c;系统该不该回答&#xff1f;这背后折射出的是AI对话系统面临的核心挑战——如…

作者头像 李华
网站建设 2026/3/15 21:47:50

文献管理工具考核要点与实践应用探讨

科研新人做综述时最痛苦&#xff1a;一搜就是几十页论文&#xff0c;重复、无关、没用。下面三款工具让我效率翻倍。 ① WisPaper&#xff08;智能学术搜索 文献管理&#xff09; 官网&#xff1a;https://www.wispaper.ai WisPaper 能通过关键词和语义搜索快速找到相关文献&…

作者头像 李华