Linux 进程创建与终止全解析:fork 原理 + 退出机制实战

Linux 进程创建与终止全解析:fork 原理 + 退出机制实战
在这里插入图片描述

🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在 Linux 多任务编程中,进程的创建与终止是最基础也是最核心的操作。 fork 函数是创建新进程的 “入口”,而进程终止则涉及资源释放、退出码传递等关键逻辑 — 理解这两个过程,能帮你打通进程管理的底层思维,避免僵尸进程、资源泄漏等常见问题。本文从 fork 函数的工作原理、写时拷贝技术,到进程终止的场景、方法与退出码解析,层层递进拆解核心逻辑,搭配实战代码帮你彻底掌握。

一. 进程创建:fork 函数的底层逻辑与实战

fork函数是 Linux 创建新进程的核心系统调用,它从已存在的进程(父进程)中复制出一个新进程(子进程),父子进程各自独立执行后续逻辑。

1.1 fork 函数基础用法

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

返回值规则(关键!)

  • 子进程中返回 0;
  • 父进程中返回 子进程的 PID(进程唯一标识);
  • 调用失败返回 -1(如系统进程数达到上限)。

(2)三个关键问题解答(之前就解答到过了)

  1. 父进程返回子进程 PID,子进程返回 0
    这是为了区分父子进程。父进程通过返回值知道创建的子进程的 PID,方便管理;子进程返回 0 表示它是新创建的进程,便于执行不同逻辑。
  2. fork 有两个返回值
    因为 fork 创建子进程后,父子进程各自从 fork 函数返回,父进程返回子进程 PID,子进程返回 0,所以看起来像是“一次调用,两个返回值”。
  3. 为什么一个 id 既等于 0 又大于 0
    实际上,父子进程的 id 值不同。在父进程中,fork 返回的是子进程 PID(>0);在子进程中,fork 返回 0。因此不会同时为 0 且大于 0,只是代码看起来像是同一变量,虚拟地址一样而已,这时的物理地址是不同的,其实已经是两个进程的不同值了。

1.2 fork 的底层工作流程

进程调用fork,当控制转移到内核中的fork代码后,内核会执行以下步骤:

  • 为子进程分配新的内存块和内核数据结构(如task_struct、页表);
  • 将父进程的部分数据结构(如进程属性、文件描述符)拷贝至子进程;
  • 将子进程添加到系统进程列表,使其成为可调度进程;
  • 从fork返回,调度器开始调度父子进程(谁先执行由调度器决定,无固定顺序)。
在这里插入图片描述

1.3 写时拷贝技术:父子进程的 “分离术”

(1)核心原理
fork创建子进程时,并不会立即复制父进程的所有数据(代码段、数据段),而是让父子进程共享这些资源,仅当任意一方试图写入数据时,才会触发 “写时拷贝”(Copy-On-Write),为写入方分配独立的副本。

在这里插入图片描述


(2)优势

  • 延迟分配内存,提高整机内存利用率(若子进程仅读取数据,无需额外分配内存);
  • 加速fork调用速度(避免大量数据拷贝);

保证进程独立性(写入时自动分离,互不干扰)。

在这里插入图片描述


(3)代码验证写时拷贝

#include<stdio.h>#include<unistd.h>int g_val =10;// 全局变量,初始父子共享intmain(){ pid_t pid =fork();if(pid ==-1){perror("fork failed");return1;}elseif(pid ==0){// 子进程 g_val =20;// 触发写时拷贝printf("子进程:pid=%d,g_val=%d,地址=%p\n",getpid(), g_val,&g_val);}else{// 父进程sleep(1);// 等待子进程修改完成printf("父进程:pid=%d,g_val=%d,地址=%p\n",getpid(), g_val,&g_val);}return0;}

输出结果

子进程:pid=12345,g_val=20,地址=0x80497e8 父进程:pid=12344,g_val=10,地址=0x80497e8 
  • 现象:父子进程中g_val地址相同(虚拟地址,这里之前讲过),但值不同;
  • 结论:写时拷贝触发后,子进程获得独立的数据副本,实现进程分离。

1.4 fork 的常见用法与失败场景

(1)典型用法

  • 父子进程执行不同代码段(如父进程创建数据,子进程进行备份);
  • 子进程通过exec函数替换为新程序(后续文章详解)。

(2)调用失败原因

  • 系统进程数达到上限(可通过ulimit -u查看进程数限制);
  • 实际用户的进程数超过系统配置上限。
在这里插入图片描述

实际场景演示(备份)

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include<unistd.h>#defineNUM10int data[NUM]={0};voidBackup(){ pid_t id =fork();if(id ==0){// childint i =0;printf("Backup: ");for(i =0; i < NUM; i++){printf("%d ", data[i]);}printf("\n");sleep(10);exit(0);// 进程结束}}voidChangeData(){int i =0;for(; i < NUM; i++){ data[i]= i +rand();}printf("origin data: ");for(i =0; i < NUM; i++){printf("%d ", data[i]);}printf("\n");}intmain(){srand(time(NULL));while(1){// 修改ChangeData();// 备份Backup();sleep(5);}}
时间线: 1. 父进程修改 data → [新值1, 新值2, ...]2. fork() 创建子进程,复制当前 data 3. 子进程打印:[新值1, 新值2, ...] ← 备份成功 4. 父进程继续循环,5秒后再次修改 data 5. 创建新的子进程备份新数据... 

核心逻辑

  1. 父子进程数据隔离fork()创建子进程时,子进程会复制父进程的全部内存(包括data数组)
  2. 备份原理:父进程修改数据创建子进程,子进程看到的就是修改前的数据
  3. 独立运行:子进程打印数据后退出,父子进程互不影响

结果:每次父进程修改数据时,都会创建一个子进程来保存修改前的数据副本。
注意:这个程序会不断创建子进程但从不回收,可能导致僵尸进程累积,我们后面会讲进程等待的,这里先不管它。


二. 进程终止:资源释放与退出机制

进程终止的本质是释放进程占用的系统资源(内核数据结构、内存、文件描述符等),并将退出状态反馈给父进程。

2.1 进程退出的三种场景

  • 正常终止:代码运行完毕,结果正确(如main函数返回0);
  • 正常终止但结果错误:代码运行完毕,但逻辑错误(如main函数返回1);
  • 异常终止:代码运行中遇到错误(如除零、段错误、信号终止)。
在这里插入图片描述


在这里插入图片描述

2.2 进程终止的三种方法

在这里插入图片描述

(1)从 main 函数返回(最常用)
return n 等同于调用 exit(n)n 为进程退出码(0表示成功,非0表示失败)。

// 正常退出,退出码 0intmain(){printf("程序正常执行完毕\n");return0;}

正常退出,可以使用 echo $? 查看进程退出码

(2)调用 exit 函数(标准库函数)
exit 会在终止进程前执行清理工作,最终调用_exit系统调用(_exit 不会主动刷新缓冲区)。

#include<stdio.h>#include<stdlib.h>intmain(){printf("执行exit退出\n");exit(1);// 退出码1,标识执行失败,也可以使用其他的退出码}

(3)调用_exit 函数(系统调用)
_exit 直接终止进程,不执行任何清理工作,速度更快。

#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){printf("执行_exit退出");// 故意不带 \n,体会刷没刷新缓冲区_exit(1);// 退出码1,标识执行失败,也可以使用其他的退出码}
#include<stdio.h>#include<stdlib.h>#include<unistd.h>// exit会冲刷缓存,输出"hello"voidTestExit(){printf("hello");exit(0);}// _exit不冲刷缓存,无输出voidTest_Exit(){printf("hello");_exit(0);}intmain(){// 分别调用一下进行测试看看输出结果TestExit();//Test_Exit();return0;}
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

(4)异常终止(信号触发)
通过Ctrl+C(SIGINT 信号)、kill -9 进程PID(SIGKILL 信号)等方式终止进程,属于异常退出。

2.3 退出码:进程的 “执行结果报告”

退出码是进程终止时反馈给父进程的 “状态信息”,核心规则:

  • 退出码 0:执行成功;
  • 退出码 1~255:执行失败(不同数值可标识不同错误类型);
  • 父进程可通过 wait/waitpid 获取子进程退出码(后续文章详解);
  • 终端中通过 echo $? 查看最近一次执行程序的退出码。
退出码解释
0命令成功执行
1通用错误代码
2命令(或参数)使用不当
126权限被拒绝(或)无法执行
127未找到命令,或 PATH 错误
128+n命令被信号从外部终止,或遇到致命错误
130通过 Ctrl+CSIGINT 终止(终止代码 2 或键盘中断)
143通过 SIGTERM 终止(默认终止)
255/*退出码超过了 0-255 的范围,因此重新计算(LCTT 译注:超过 255 后,用退出取模)

通过 strerror 获取退出码描述:

#include<stdio.h>#include<string.h>#include<stdlib.h>intmain(){int exit_code =1;printf("退出码%d:%s\n", exit_code,strerror(exit_code));return0;}
在这里插入图片描述


注意: 退出码仅低 8 位有效:_exit(-1) 等价于 _exit(255)(-1 & 0xFF = 255);

常见误区澄清:

  • fork 后父子进程共享所有数据:错误!代码段共享,但数据段采用写时拷贝,写入时自动分离;
  • exit 和_exit 完全一致:错误!exit 会冲刷缓存、执行清理函数,_exit 直接终止;
  • 退出码可以是任意整数:错误!仅低 8 位有效,超过 255 会自动取模(如exit(300)等价于exit(44));
  • 子进程退出后资源会自动释放:错误!需父进程调用wait/waitpid回收,否则会变成僵尸进程。

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:进程创建(fork)和终止是 Linux 进程管理的基础,核心要点:fork通过写时拷贝实现高效创建,进程终止需关注资源释放和退出码传递。理解这些逻辑,能帮你规避僵尸进程、资源泄漏等问题,为后续学习进程等待、程序替换打下基础。本文覆盖了fork和进程终止的核心知识点,搭配实战代码可直接上手验证。如果需要深入学习进程等待(wait/waitpid)或程序替换(exec函数簇),可以期待一下后续文章。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

《C/C+++ Boost 轻量级搜索引擎实战:架构流程、技术栈与工程落地指南——构造正/倒排索引(中篇)》

《C/C+++ Boost 轻量级搜索引擎实战:架构流程、技术栈与工程落地指南——构造正/倒排索引(中篇)》

前引:这是一个聚焦基础搜索引擎核心工作流的实操项目,基于 C/C++ 技术生态落地:从全网爬虫抓取网页资源,到服务器端完成 “去标签 - 数据清洗 - 索引构建” 的预处理,再通过 HTTP 服务接收客户端请求、检索索引并拼接结果页返回 —— 完整覆盖了轻量级搜索引擎的端到端逻辑。项目采用 C++11、STL、Boost 等核心技术栈,搭配 CentOS 7 云服务器 + GCC 编译环境(或 VS 系列开发工具)部署,既适配后端工程的性能需求,也能通过可选的前端技术(HTML5/JS 等)优化用户交互,是理解搜索引擎底层原理与 C++ 工程实践的典型案例 目录 【一】Jieba分词工具 【二】正/倒排索引结构设计

By Ne0inhk

5分钟部署通义千问2.5-7B-Instruct,vLLM+Open-WebUI让AI助手快速上线

5分钟部署通义千问2.5-7B-Instruct,vLLM+Open-WebUI让AI助手快速上线 1. 引言:为什么选择通义千问2.5-7B-Instruct? 在当前大模型快速发展的背景下,如何快速将一个高性能、可商用的开源语言模型部署为本地AI助手,成为开发者和企业关注的核心问题。通义千问2.5-7B-Instruct 凭借其“中等体量、全能型、可商用”的定位,成为70亿参数级别中的佼佼者。 该模型不仅在多项基准测试中表现优异——如C-Eval、MMLU等综合评测中位列7B量级第一梯队,还具备出色的代码生成(HumanEval 85+)与数学推理能力(MATH 80+),支持工具调用、JSON格式输出,适用于构建智能Agent系统。更重要的是,它采用RLHF + DPO对齐策略,显著提升有害内容拒答率,并且量化后仅需4GB显存即可运行,RTX 3060等消费级GPU即可轻松承载,推理速度超过100 tokens/s。 本文将详细介绍如何通过 vLLM + Open-WebUI 的组合,在5分钟内完成通义千问2.5-7B-Instruct的本地化部署,实现开箱即用的Web

By Ne0inhk
【前端】使用Vue3过程中遇到加载无效设置点击方法提示不存在的情况,原来是少加了一个属性

【前端】使用Vue3过程中遇到加载无效设置点击方法提示不存在的情况,原来是少加了一个属性

🌹欢迎来到《小5讲堂》🌹 🌹这是《前端》系列文章,每篇文章将以博主理解的角度展开讲解。🌹 🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!🌹 目录 * 前言 * 提示报错 * 问题分析 * 1. **Options API vs Composition API 风格差异** * ✅ **Options API 写法(方法直接放在外面)** * ✅ **Composition API 写法(方法必须在 setup 中定义)** * ✅ **`<script setup>` 语法糖(最简洁的 Composition API)** * 2. **为什么你的代码会报错?** * 3. **解决方案** * 方案 1:改用 **Options API**(适合从 Vue

By Ne0inhk

ESP8266 Web配网+MQTT+STM32串口上云+免AT指令

本文详细讲解 ESP8266/ESP12F Web 配网、MQTT 通信、STM32/Arduino 串口透传一体化实现方案WiFi强制入户,连接自动打开网页配置,核心亮点是单片机免 ESP8266 AT 指令,串口直接上云,通过串口向 ESP8266 发送数据即可自动上传至 MQTT 服务器,固件开源可直接用于学习调试。 固件下载: 通过网盘分享的文件:mqtt_usart_wifi.ino.bin 链接: https://pan.baidu.com/s/1mZt5diatyYvnSZ-N1eF75w?pwd=e8we 提取码: e8we 免AT指令全网首发!数据直接上传MQTT、秒下发指令,无需复杂配置!下载固件即可使用 一、项目背景与开发初衷         在物联网设备开发过程中,配网和远程通信是两个核心痛点:传统的

By Ne0inhk