文章目录
- 🚀 吐血整理!小白也能秒懂的 Linux 进程概念大揭秘(硬核详细版)
- 1. 祖师爷的宝训:冯·诺依曼体系结构
- 1.1 硬件的“权力游戏”
- 2. 计算机里的大管家:操作系统 (OS)
- 2.1 操作系统是个啥?
- 2.2 管理的六字真言:先描述,再组织
- 3. 揭开进程的神秘面纱
- 3.1 到底什么是进程?
- 3.2 进程的“户口本”:PCB
- 3.2.1 cwd 和exe
- cwd 与 exe 核心对比表
- 3.3 影分身之术:fork()
- 3.4 进程的“七情六欲”:状态流转
- 3.5 让人头疼的“僵尸”与“孤儿”
- 3.6 谁先吃肉?进程调度与 O(1) 算法
- 3.7 高频误区汇总、bash原理、内核链表深度考点
- 3.7.1 bash外壳进程原理
- 3.7.2 内核list_head 多链表超级重点
- 3.7.3 PID上限 & 进程数量限制
- 3.7.4 内存泄漏终极考点
- 3.7.5 完整误区大全(12条终极版)
- 4. 进程的“朋友圈”:环境变量
- 4.1 环境变量是什么鬼?
- 4.2 环境变量的绝技
- 5. 操作系统的大忽悠:程序地址空间
- 5.1 你的内存不是你的内存
- 5.2 见证奇迹的时刻:同地址不同值
- 5.3 为什么OS要“画大饼”?
- 5.3.1 页表权限拦截:字符串常量区崩溃案例
- 5.4 结合虚拟内存理解进程挂起
- 5.5 内核核心思想:再谈「先描述,后组织」
- 5.6 三大核心结构体最终从属关系(PCB / mm_struct / list_head)
- 6. 全文完整流程总结
🚀 吐血整理!小白也能秒懂的 Linux 进程概念大揭秘(硬核详细版)
哈喽,各位未来的技术大牛们!今天咱们就来一场深度探索,用最通俗幽默的语言,配合丰富的概念图和代码示例,带你把进程的底裤彻底看穿!
1. 祖师爷的宝训:冯·诺依曼体系结构
在聊进程之前,咱们得先拜一拜祖师爷冯·诺依曼。我们现在用的笔记本、不常见的服务器,绝大部分都死死守着他老人家定的规矩。
1.1 硬件的“权力游戏”
计算机就是个大工厂,里面有几个核心部门:
- 输入设备:键盘、鼠标、麦克风等,负责收集外界情报。
- 输出设备:显示器、打印机等,负责向外界展示成果。
- 中央处理器 (CPU):包含运算器和控制器,是工厂里唯一干活的“超级打工人”。
- 存储器 (内存):核心的中转仓库。
💡概念图:冯·诺依曼数据流向图
[输入设备] —> (写入) —> 【存储器 (内存)】 —> (读取) —> [输出设备]
↑ ↓
(读取) (写入)
【 CPU 】
这里有个铁律:所有设备都只能直接和内存打交道!
CPU这位高冷员工只认内存,绝不和外设直接说话;外设想输入或输出数据,也只能乖乖往内存里写或者从内存里读。
- 示例:你用QQ给朋友发消息,数据是怎么跑的?你敲击键盘(输入设备),数据先加载到内存。CPU从内存读取数据进行处理(比如加密),写回内存。最后内存把数据送给网卡(输出设备)发出去。
2. 计算机里的大管家:操作系统 (OS)
2.1 操作系统是个啥?
在整个计算机软硬件架构中,操作系统的定位非常清晰:它就是一款纯正的“搞管理”的软件。对下要管理好各种软硬件资源(CPU、内存、硬盘),对上要给咱们的应用程序提供一个舒舒服服的执行环境。
2.2 管理的六字真言:先描述,再组织
操作系统是怎么管理的?咱们想象一下大学里的校长(OS),他不需要认识每一个学生,他只需要辅导员提供学生的数据信息即可。
- 描述:在C语言中,就是用
struct结构体把对象的属性(姓名、学号、成绩)记录下来。 - 组织:用链表或其他高效的数据结构,把这些结构体串起来,方便增删查改。
💡概念图:系统调用与银行柜台
操作系统就像一家银行,底层的硬件就是金库。
银行绝对不允许你直接进金库拿钱,而是给你提供了**“综合窗口”(系统调用 System Call)**。
开发者通过调用OS提供的接口(如printf底层调用的系统接口),来安全地使用硬件资源。
扩展:比如面向对象的C++,Java等,都有类和容器,类即是对物品的描述,容器即是组织
3. 揭开进程的神秘面纱
3.1 到底什么是进程?
课本上说进程是“运行中的程序”。但在内核眼里,进程 = 内核数据结构 (task_struct) + 自己的程序代码和数据。
3.2 进程的“户口本”:PCB
为了管理进程,Linux内核给每个进程建了个档案,叫做进程控制块(PCB),具体名叫task_struct。里面记录了:
- 标示符 (PID):进程的唯一身份证号。
- 状态:是跑着呢,还是睡着呢,还是死了?
- 上下文数据:进程被切走时,CPU寄存器里保存的临时数据,方便下次回来接着跑。
- 所有的
task_struct会以双向链表的形式被内核组织起来。
这里很多同学容易产生错觉:觉得 PCB (task_struct) 就是一个 “类”。
但要注意,task_struct 是C 语言的 struct 结构体,并不是面向对象里的类。
C 语言的结构体,只能存放成员变量,不能存放成员函数,它的作用仅仅是把对象的各类描述信息打包收纳在一起。操作 PCB 的相关逻辑函数,全部是独立写在结构体外部,通过传入结构体指针完成操作,函数并不属于结构体本身。
3.2.1 cwd 和exe
cwd 与 exe 核心对比表
| 对比维度 | cwd(Current Working Directory,/proc/[PID]/cwd) | exe(/proc/[PID]/exe) |
|---|---|---|
| 核心含义 | 进程的当前工作目录,可理解为进程「运行时干活的文件夹」 | 进程对应的磁盘可执行二进制文件本体路径,可理解为进程「工具本身存放的位置」 |
| 内核存储位置 | 内核PCBtask_struct→fs_struct结构体的pwd字段 | 内核PCBtask_struct中记录的可执行文件路径字段 |
| 核心作用 | 进程使用相对路径访问文件时的基准目录,决定相对路径文件的读写位置 | 标识该进程由哪个磁盘上的可执行程序启动,用于定位进程的原始程序文件 |
| fork子进程继承规则 | 子进程直接继承父进程的cwd | 子进程直接继承父进程的exe |
| exec加载新程序后的变化 | 保持不变,继续沿用当前进程的工作目录 | 同步更新为新加载的可执行文件路径 |
| 用户态修改方式 | 可通过chdir()系统调用修改,仅影响当前进程,不影响父进程/其他进程 | 用户态无直接修改的系统调用,仅exec加载新程序时会自动更新 |
| 对应文件/目录被删除后的表现 | 符号链接标记(deleted),进程仍持有目录句柄,已打开的文件可正常读写 | 符号链接标记(deleted),进程仍可正常运行(内核持有文件描述符,磁盘数据不会被回收) |
| 核心易混点 | 不是环境变量PWD!PWD是用户空间字符串,cwd是内核维护的真实进程属性,二者可不一致 | 不是进程的运行目录!exe是程序文件的静态路径,和进程当前在哪运行完全无关 |
| 典型场景示例 | 在/root目录执行/opt/bin/app,cwd为/root | 在/root目录执行/opt/bin/app,exe为/opt/bin/app |
3.3 影分身之术:fork()
在Linux下怎么创造新进程?用fork()系统调用!
📌对话知识点补充:fork返回值底层原理
pid_t id = fork();并不是同一个进程拿到两个返回值。
fork调用成功会产生两次返回:
- 在父进程中返回子进程的PID(正数),赋值给父进程自己局部变量
id;- 在子进程中返回
0,赋值给子进程自己局部变量id。
⚠父子是两个独立进程,各自拥有一份独立的id变量,不是共享同一个变量。之后父子两个进程拿着属于自己的
id,各自独立执行if‑else判断,自己对号入座进入对应分支。父id>0进入else;子id==0进入else if(id==0)分支。两个进程并行向后跑代码,所以会看到父子都打印输出。
fork()有个极其反人类的特性:它有两个返回值执行后,父子进程代码共享,但数据会各自开辟空间,私有一份(采用写时拷贝)。
👇 深度代码示例:
#include<stdio.h>#include<unistd.h>intmain(){pid_tid=fork();// 召唤影分身if(id<0){// 创建失败return1;}elseif(id==0){// 这一部分只有【子进程】会执行!printf("我是儿子,我的PID: %d\n",getpid());}else{// 这一部分只有【父进程】会执行!id就是儿子的PIDprintf("我是老爸,我的PID: %d\n",getpid());}return0;}💡拓展实验代码(带全局变量、while循环版本,观察写时拷贝现象)
#include<stdio.h>#include<unistd.h>#include<sys/types.h>intgval=100;intmain(){printf("进程开始运行 ,pid: %d\n",getpid());pid_tid=fork();if(id<0){perror("fork");return1;}elseif(id==0){printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n",getpid(),getppid());sleep(5);while(1){sleep(1);printf("子进程修改变量 : %d->%d",gval,gval+10);gval+=10;printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n",getpid(),getppid());}}else{while(1){sleep(1);printf("我是一个父进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n",getpid(),getppid(),gval);}}return0;}✨现象解读:子进程内部不断修改全局变量
gval,父进程打印出来的gval始终是初始值。
很多同学会误以为全局变量父子共享,实际fork触发写时拷贝,修改时父子拥有各自独立的数据副本,互相隔离。
⚠注意:打印出来的ppid只是拿到父进程的PID数字,并不代表那个进程在执行本程序代码。
3.4 进程的“七情六欲”:状态流转
进程有几种典型状态:
R (Running):运行态。并不意味着正在疯狂占用CPU,只要在运行队列里排队,随时准备跑的,都是R态。
S (Sleeping):浅度睡眠。等事件完成(比如等键盘输入),随时能被叫醒(可中断睡眠)。
D (Disk sleep):深度睡眠。不可中断睡眠。
✅底层核心原理(老师课堂考点):
进程正在做磁盘IO、硬件读写,手里握着硬件锁、缓冲区资源。
如果此时允许被kill、被信号唤醒,进程直接退出,硬件还在写入、锁不释放、数据没收尾,直接导致文件损坏、文件系统崩溃。
所以D状态屏蔽所有软件信号(kill -9无效),只能等待硬件IO完成、硬件中断唤醒进程。
系统内存爆满触发OOM时,OOM会跳过D进程,防止IO半路中断丢数据。
T (Stopped):暂停状态。被信号强制暂停了,比如发了
SIGSTOP信号。Z (Zombie):僵死状态。进程退出了,等老爸来收尸。
3.5 让人头疼的“僵尸”与“孤儿”
- 🧟♂️ 僵尸进程 (Zombie):子进程退出,父进程不调用wait回收,PCB残留内核中。
- 危害:
- 用户层代码、堆栈全部释放;
- 内核task_struct不释放 → 内核内存轻微泄漏;
- PID号不回收复用 → PID资源耗尽(严重)。
❗重点:不是内存满崩,是PID编号被僵尸占死,内存空闲也无法创建新进程。
- 👶 孤儿进程:父进程先退出,子进程被 systemd(1号进程) 领养,自动回收,无危害。
3.6 谁先吃肉?进程调度与 O(1) 算法
CPU资源有限,进程需要竞争,这就有了优先级相关的一整套体系:nice、priority、counter。
📌概念区分
nice:用户层可见的谦让度,范围-20 ~ 19,只是对外接口,不直接参与内核调度运算。nice值越小代表越不谦让,希望拿到更多CPU时间。普通用户只能调大nice(降低优先级),root才允许设置负数nice。priority:内核静态优先级,O(1)调度范围60‑99。换算公式:priority = 80 + nice,真正用来挂调度队列链表。counter:时间片,由priority计算得出,进程拿到的CPU时间配额,时钟中断下不断递减;counter == 0,进程移入过期队列。
❓为什么要有nice值?
nice是给用户提供一套带权限限制的稳定接口。内核内部priority不希望直接暴露给用户。普通用户只能“谦让”,不能恶意抢占CPU;root才可以提升优先级。同时隔离内核内部实现,调度器版本变更,用户层脚本不需要修改。
❓为什么用户不能直接操作task_struct里的priority?
- 用户态/内核态硬件隔离:
task_struct存放在内核地址空间,用户进程不能直接读写内核内存,只能通过系统调用陷入内核代为修改。- 权限管控:如果直接暴露priority,普通用户可以随意把自己改成最高优先级,恶意抢占CPU,造成整机卡死;nice封装权限校验逻辑在系统调用内部。
- 内外解耦:priority是调度器内部实现,不同调度器版本数值规则会变;nice作为稳定用户API,内核升级用户脚本不需要改动。
- 修改优先级不是单纯赋值:改priority需要把进程从旧链表摘下、重新计算队列下标、挂入新链表、更新bitmap位图,一整套队列维护逻辑,必须由内核完成,用户直接改字段会造成runqueue数据错乱崩溃。
⚠重要考点:修改nice,不会改变当前已经拿到手的counter剩余时间片,只会影响下一轮轮转分配的counter大小。
为了让调度快如闪电,Linux 2.6 内核搞了个牛逼的O(1) 调度算法:
💡概念图:O(1) 调度队列机制
- 内核维护了两个队列:活动队列 (active)和过期队列 (expired)。
- 队列里有140个格子(数组
queue[140]),对应不同的优先级。- 系统还搞了个 5*32 位的
bitmap(位图)。CPU找进程时,不遍历链表,直接查位图,瞬间就能找到哪个优先级格子里有进程!时间复杂度永远是常数。- 活动队列的进程跑完了,就把指针跟过期队列一换(交换
active和expired指针),继续跑,永不停歇!
3.7 高频误区汇总、bash原理、内核链表深度考点
3.7.1 bash外壳进程原理
bash本身也是一个普通进程。打开终端,系统就会启动一份bash进程。
- 我们在命令行敲下指令(
ls、pwd、./a.out),bash会调用fork创建子进程,再调用exec替换子进程的程序镜像,去执行对应的命令/可执行文件。 - bash是父进程,我们执行的命令几乎全部是bash的子进程。
cd、export这类内置命令,不会fork子进程,直接在bash进程内部执行。如果cd创建子进程去执行,子进程修改cwd,父bash的工作目录完全不受影响,cd就会失效。
简单流程:
终端启动 → 创建bash进程 用户输入ls → bash fork()出子进程 → 子进程exec("ls") → ls运行结束退出 → bash继续等待输入3.7.2 内核list_head 多链表超级重点
Linux内核采用侵入式双向链表 list_head:
structlist_head{structlist_head*next,*prev;};structtask_struct{structlist_headtasks;// 全局进程链表structlist_headrun_list;// 调度队列链表structlist_headchildren;// 子进程链表structlist_headsibling;// 兄弟进程链表};✅核心作用:
一个进程结构体,可以同时挂在多条不同内核链表上,各司其职、互不干扰。
tasks:让系统能遍历所有进程run_list:让调度器挑选就绪进程children/sibling:维护父子、兄弟进程关系
如果只有一组指针,进程同一时刻只能在一条链表,操作系统完全无法管理。
✅container_of原理:
链表指针只指向list_head成员,不指向整个task_struct。
需要通过结构体偏移量回推整个PCB地址。
写错member参数直接内存越界、内核崩溃。
3.7.3 PID上限 & 进程数量限制
- pid_max 只是编号上限
- 64位系统最大 4194304(2^22)写更大无效
- PID正常退出可循环复用,不会耗尽
- 真正卡死的是僵尸进程
- 僵尸不释放PID,占住编号不归还
- PID耗尽:内存再大也无法fork新进程
- 真正限制系统最大进程数的是:
threads-max:系统全局最大任务数ulimit -u:单用户最大进程数- 内核内存大小
3.7.4 内存泄漏终极考点
- 短生命周期进程:就算代码泄漏,进程退出OS全部回收,无害。
- 常驻内存进程(nginx、mysql、360、服务进程):
永久不退出,泄漏内存不断累积,越跑越吃内存、最终OOM被杀,危害极大。
360难删原理(操作系统原理):
多进程守护 + 系统服务自启 + 内核驱动拦截删除,常驻系统,普通用户无法终结。
3.7.5 完整误区大全(12条终极版)
❌fork同一个进程返回两个值
✅父子两个进程分别返回,各自局部变量独立❌fork后全局变量共享
✅只读共享,写时拷贝,修改互相隔离❌task_struct链表是父子关系
✅全局链表;父子靠parent指针❌虚拟地址相同=物理地址相同
✅虚拟地址进程私有,可重复映射不同物理内存
💡补充:写时拷贝COW关键易错点
&gval拿到的是虚拟地址,发生写时拷贝之后,虚拟地址数字完全不变,变化的只有页表映射关系,背后映射到了一块全新的物理内存。C语言用户层拿不到物理地址,所以打印地址看不出变化。虚拟地址相当于门牌号,物理内存相当于真实房子;房子换了,门牌号保持原样。
❌子进程全盘复制父进程
✅PID/PPID全新,上下文快照继承❌僵尸进程占用代码数据内存
✅用户资源全释放,只剩内核PCB❌cd命令创建子进程
✅内置命令,bash内部执行❌D状态进程可以被kill -9杀死
✅D状态屏蔽所有软件信号,OOM也杀不动❌所有IO都会进入D状态
✅普通网络IO是S状态,硬件块设备IO才是D❌list_head指针直接指向task_struct
✅只指向内部成员,必须container_of回推❌一个进程只能挂一条内核链表
✅可嵌入多个list_head,同时挂多条链表❌pid_max决定系统最大进程数
✅pid_max是编号池,真正限制是threads-max、内存、ulimit
4. 进程的“朋友圈”:环境变量
4.1 环境变量是什么鬼?
平时敲ls指令直接就能出结果,但跑咱们自己的程序得敲./a.out(带上路径)。为啥?
因为系统里有个叫PATH的环境变量,它指定了命令的默认搜索路径。系统在PATH的路径里找到了ls,但找不到你的a.out。
📌补充:shell本地变量 vs 环境变量
- 本地变量:
var=123,仅当前bash可用,不会被子进程继承,子进程getenv()拿到NULL。- 环境变量:
export var=123,存入bash环境表,fork出来的所有子进程都可以通过getenv()读取。- 子进程内部调用
setenv(),仅仅修改自己进程的环境变量副本,完全无法改变父bash的变量。getenv()返回的内存只能读,禁止直接改写。
4.2 环境变量的绝技
每个程序都会收到一张环境表(字符指针数组environ)。
最牛的是,环境变量具有全局属性,可以被子进程继承下去!
- 示例:在代码里怎么拿到环境变量?用系统调用
getenv("PATH")就行!
5. 操作系统的大忽悠:程序地址空间
5.1 你的内存不是你的内存
C语言老师告诉我们,内存分栈区、堆区、未初始化数据、代码段。
但实际上,我们用C/C++打印出来的地址,全都是虚拟地址!真正的物理地址用户一概看不到,由OS统一管理。
💡虚拟地址空间分区概览(低地址 → 高地址)
- 正文代码段:存放只读程序指令
- 初始化数据段data:初始化全局、static变量
- BSS段:未初始化全局/static变量,内核自动清零
- 堆heap:malloc申请,地址向上增长
- 大片镂空空洞:无页表映射,访问直接段错误
- mmap共享区:动态库、文件映射
- 栈stack:局部变量,地址向下增长
- 栈顶端:存放argv命令行参数、env环境变量
- 最高段:内核空间,用户态无权访问
5.2 见证奇迹的时刻:同地址不同值
看下面这段神奇的验证代码:
👇 深度代码示例:
#include<stdio.h>#include<unistd.h>intg_val=0;// 全局变量intmain(){pid_tid=fork();if(id==0){// 子进程先跑,修改变量g_val=100;printf("子进程: 值 = %d, 地址 = %p\n",g_val,&g_val);}else{// 父进程等一会再跑sleep(3);printf("父进程: 值 = %d, 地址 = %p\n",g_val,&g_val);}return0;}输出结果:
子进程: 值 = 100, 地址 = 0x80497e8 父进程: 值 = 0, 地址 = 0x80497e8惊不惊喜?父子进程打印的地址一模一样,但里面的值却不一样!
这说明这个地址绝对不是物理地址。
5.3 为什么OS要“画大饼”?
OS给每个进程都画了一张叫做mm_struct的大饼(虚拟地址空间)。
💡概念图:虚拟内存到物理内存的映射
【进程A的虚拟地址空间】 (地址: 0x80497e8)
|
【页表 (映射表)】 ----------------> 【物理内存】(某真实地址,存了100)
【进程B的虚拟地址空间】 (地址: 0x80497e8)
|
【页表 (映射表)】 ----------------> 【物理内存】(另一真实地址,存了0)
为什么不直接操作物理内存,非要搞这么复杂?
安全风险控制:如果进程能直接访问物理内存,流氓软件就能随意修改其他程序的内存甚至系统内核导致死机。有了虚拟地址,所有访问都必须经过OS的页表审查。
解耦合与效率(延迟分配):进程申请内存时(如
malloc),OS只在虚拟地址空间里给你分地盘。等你真正去写数据时,OS才会在物理内存里给你找个位置并建立映射。这就做到了进程管理和内存管理的完美解耦。统一视角:让每个进程都觉得自己拥有连续、完整的内存空间,程序在物理内存中其实可以见缝插针地随便放,大大提高了空间利用率。
5.3.1 页表权限拦截:字符串常量区崩溃案例
char*str="helloworld";*str='H';字符串字面量存放在只读字符常量区,对应页表项标记只读权限。
当代码尝试写入该地址,MMU硬件在地址翻译时检测权限冲突,触发异常,内核发送SIGSEGV段错误,程序直接崩溃。
💡重点:不是C语言语法禁止修改,是页表硬件权限拦截保护内存。
对比:char str[] = "helloworld"; str[0]='H';可以正常修改;字符串拷贝到栈空间,栈内存页具备读写权限。
5.4 结合虚拟内存理解进程挂起
很多同学容易混淆进程睡眠和进程挂起,借助虚拟内存、页表的知识就很好理解。
普通睡眠(S状态)
进程PCB驻留内存,进程依旧占用自己的物理内存,仅仅不参与CPU调度,等待事件唤醒。物理内存数据不会被搬走。进程挂起(换出到Swap)
当系统物理内存资源紧张,操作系统会把进程挂起:
task_struct、mm_struct、虚拟区间描述全部保留在内核,虚拟地址空间完整保留,图纸不丢。- 修改该进程的页表项:不再指向物理内存,标记数据存放于磁盘Swap分区。
- 进程原本占用的物理内存页全部回收,分配给其他急需内存的进程。
进程自身完全感知不到,它眼中的虚拟地址空间没有任何变化。
当进程需要恢复运行,CPU访问虚拟地址,页表提示页面在磁盘,触发缺页异常。内核将Swap磁盘的数据重新加载回物理内存,更新页表映射,进程就可以继续执行。
💡核心:没有虚拟内存与页表机制,就实现不了进程挂起。
如果没有虚拟地址隔离,一旦把进程数据丢到磁盘,程序地址全部错乱,无法复原。
区分重点:
睡眠:进程数据还在物理内存,只是不跑CPU。
挂起:进程数据挪到磁盘Swap,物理内存释放。
5.5 内核核心思想:再谈「先描述,后组织」
mm_struct这个内核结构体,仅仅是对虚拟地址空间的描述、记账,相当于一张图纸。
它记录各个段的起止地址、虚拟区域链表、页表基地址,但是它本身不做地址翻译,不操作硬件。
真正完成内存组织依靠两部分:
CPU硬件MMU(内存管理单元)+页表
每一次访问内存,由硬件完成虚拟地址到物理地址的翻译,同时做权限校验。操作系统内核软件逻辑
内核负责修改mm_struct登记虚拟地址区间,分配回收物理内存,填充修改页表,处理缺页异常、swap挂起换入换出。
逻辑链条:
mm_struct保存页表基地址 → 内核操作页表 → CPU‑MMU硬件完成地址翻译。
举个例子:调用malloc,仅仅修改mm_struct、vm_area_struct完成虚拟地址预约;
只有访问内存触发缺页异常,内核才分配物理内存,硬件完成映射。
图纸只规划地盘,真正盖房子干活的是系统和硬件。
💡拓展历史小彩蛋:虚拟内存并不是某个人灵光一闪的天才脑洞。
上世纪50‑60年代物理内存硬件价格昂贵,内存资源紧缺倒逼出这套技术。
1956年德国博士生提出虚拟内存理论构想;1962年英国曼彻斯特大学Atlas计算机,世界第一台实现分页虚拟内存的机器;后续Unix、Mach微内核迭代出写时拷贝COW;Linux使用mm_struct、vm_area_struct把这套模型落地实现。
⚠虚拟地址翻译依赖CPU硬件MMU;没有MMU,光靠内核结构体,虚拟内存完全无法工作。
5.6 三大核心结构体最终从属关系(PCB / mm_struct / list_head)
这是整篇博客最核心的底层架构,串联所有知识点,彻底理清三者从属、关联关系:
list_head 与 PCB(task_struct)
list_head是PCB内部嵌入的成员,依靠侵入式链表设计,一个task_struct里面可以塞多个list_head。让同一个进程PCB可以同时挂载到多条不同内核链表:全局进程链表、调度运行队列、父子兄弟链表,实现多维度管理,配合container_of宏从链表成员反向拿到完整PCB结构体。mm_struct 与 PCB(task_struct)
mm_struct是task_struct内部的指针成员,每一个用户进程都会关联一份mm_struct,用来完整描述该进程的整套虚拟地址空间;mm_struct维护各个虚拟段、页表基地址,和硬件MMU配合完成地址映射。整体关系总结
task_struct(PCB)是进程的总档案,里面既包含调度、状态、PID、上下文、文件信息,又通过list_head挂入各类内核链表做组织管理,再通过mm_struct指针管理整个进程虚拟内存。完美践行操作系统“先描述,后组织”的核心思想。
6. 全文完整流程总结
从计算机硬件到进程运行,整套链路可以完整串起来:
冯·诺依曼体系规定硬件的数据交互规则,操作系统作为软硬件中间层,遵循先描述,后组织的思想,使用C语言结构体描述一切软硬件资源,链表完成组织管理。
当我们在bash终端敲下一条命令,bash进程调用fork()创建子进程,依靠写时拷贝复制父进程PCB、mm_struct、环境变量、cwd等信息;子进程再调用exec替换程序镜像,生成我们需要执行的业务进程。
每一个新进程,都会生成自己独立的task_structPCB,内部嵌入多份list_head挂载到内核不同链表;PCB内部指针指向专属的mm_struct虚拟地址空间,由mm_struct配合页表、CPU的MMU硬件完成虚拟地址到物理内存的映射。
内核调度器O(1)算法根据nice、priority、时间片,从调度链表挑选进程上CPU运行;进程运行过程中会发生状态切换:运行R、浅睡眠S、深度不可中断D、暂停T;进程退出后,如果父进程没有wait回收就变成僵尸进程,PCB残留在内核占用PID资源;父进程提前退出则子进程被1号进程领养。
进程访问内存拿到的全部是虚拟地址,malloc仅仅预约虚拟地址,缺页异常才分配真实物理内存;内存紧张时操作系统可以把进程换出到Swap磁盘实现进程挂起,依靠虚拟内存机制保证程序无感知。
一句话概括:进程 = task_struct(PCB描述信息 + list_head链表组织) + mm_struct虚拟地址空间 + 代码数据。硬件MMU完成地址翻译,调度器完成CPU时间分配,系统调用完成用户态与内核态交互。