news 2026/8/12 16:54:41

Linux基础:进程概念

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux基础:进程概念

文章目录

  • 🚀 吐血整理!小白也能秒懂的 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_structfs_struct结构体的pwd字段内核PCBtask_struct中记录的可执行文件路径字段
核心作用进程使用相对路径访问文件时的基准目录,决定相对路径文件的读写位置标识该进程由哪个磁盘上的可执行程序启动,用于定位进程的原始程序文件
fork子进程继承规则子进程直接继承父进程的cwd子进程直接继承父进程的exe
exec加载新程序后的变化保持不变,继续沿用当前进程的工作目录同步更新为新加载的可执行文件路径
用户态修改方式可通过chdir()系统调用修改,仅影响当前进程,不影响父进程/其他进程用户态无直接修改的系统调用,仅exec加载新程序时会自动更新
对应文件/目录被删除后的表现符号链接标记(deleted),进程仍持有目录句柄,已打开的文件可正常读写符号链接标记(deleted),进程仍可正常运行(内核持有文件描述符,磁盘数据不会被回收)
核心易混点不是环境变量PWDPWD是用户空间字符串,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调用成功会产生两次返回

  1. 父进程中返回子进程的PID(正数),赋值给父进程自己局部变量id
  2. 子进程中返回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残留内核中。
  • 危害
    1. 用户层代码、堆栈全部释放;
    2. 内核task_struct不释放 → 内核内存轻微泄漏
    3. PID号不回收复用 → PID资源耗尽(严重)。

❗重点:不是内存满崩,是PID编号被僵尸占死,内存空闲也无法创建新进程

  • 👶 孤儿进程:父进程先退出,子进程被 systemd(1号进程) 领养,自动回收,无危害。

3.6 谁先吃肉?进程调度与 O(1) 算法

CPU资源有限,进程需要竞争,这就有了优先级相关的一整套体系:niceprioritycounter

📌概念区分

  • 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?

  1. 用户态/内核态硬件隔离:task_struct存放在内核地址空间,用户进程不能直接读写内核内存,只能通过系统调用陷入内核代为修改。
  2. 权限管控:如果直接暴露priority,普通用户可以随意把自己改成最高优先级,恶意抢占CPU,造成整机卡死;nice封装权限校验逻辑在系统调用内部。
  3. 内外解耦:priority是调度器内部实现,不同调度器版本数值规则会变;nice作为稳定用户API,内核升级用户脚本不需要改动。
  4. 修改优先级不是单纯赋值:改priority需要把进程从旧链表摘下、重新计算队列下标、挂入新链表、更新bitmap位图,一整套队列维护逻辑,必须由内核完成,用户直接改字段会造成runqueue数据错乱崩溃。

⚠重要考点:修改nice,不会改变当前已经拿到手的counter剩余时间片,只会影响下一轮轮转分配的counter大小。

为了让调度快如闪电,Linux 2.6 内核搞了个牛逼的O(1) 调度算法

💡概念图:O(1) 调度队列机制

  • 内核维护了两个队列:活动队列 (active)过期队列 (expired)
  • 队列里有140个格子(数组queue[140]),对应不同的优先级。
  • 系统还搞了个 5*32 位的bitmap(位图)。CPU找进程时,不遍历链表,直接查位图,瞬间就能找到哪个优先级格子里有进程!时间复杂度永远是常数。
  • 活动队列的进程跑完了,就把指针跟过期队列一换(交换activeexpired指针),继续跑,永不停歇!

3.7 高频误区汇总、bash原理、内核链表深度考点

3.7.1 bash外壳进程原理

bash本身也是一个普通进程。打开终端,系统就会启动一份bash进程。

  • 我们在命令行敲下指令(lspwd./a.out),bash会调用fork创建子进程,再调用exec替换子进程的程序镜像,去执行对应的命令/可执行文件。
  • bash是父进程,我们执行的命令几乎全部是bash的子进程。
  • cdexport这类内置命令,不会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上限 & 进程数量限制
  1. pid_max 只是编号上限
    • 64位系统最大 4194304(2^22)写更大无效
    • PID正常退出可循环复用,不会耗尽
  2. 真正卡死的是僵尸进程
    • 僵尸不释放PID,占住编号不归还
    • PID耗尽:内存再大也无法fork新进程
  3. 真正限制系统最大进程数的是:
    • threads-max:系统全局最大任务数
    • ulimit -u:单用户最大进程数
    • 内核内存大小
3.7.4 内存泄漏终极考点
  • 短生命周期进程:就算代码泄漏,进程退出OS全部回收,无害。
  • 常驻内存进程(nginx、mysql、360、服务进程):
    永久不退出,泄漏内存不断累积,越跑越吃内存、最终OOM被杀,危害极大。

360难删原理(操作系统原理):
多进程守护 + 系统服务自启 + 内核驱动拦截删除,常驻系统,普通用户无法终结。

3.7.5 完整误区大全(12条终极版)
  1. ❌fork同一个进程返回两个值
    ✅父子两个进程分别返回,各自局部变量独立

  2. ❌fork后全局变量共享
    ✅只读共享,写时拷贝,修改互相隔离

  3. ❌task_struct链表是父子关系
    ✅全局链表;父子靠parent指针

  4. ❌虚拟地址相同=物理地址相同
    ✅虚拟地址进程私有,可重复映射不同物理内存

💡补充:写时拷贝COW关键易错点
&gval拿到的是虚拟地址,发生写时拷贝之后,虚拟地址数字完全不变,变化的只有页表映射关系,背后映射到了一块全新的物理内存。C语言用户层拿不到物理地址,所以打印地址看不出变化。虚拟地址相当于门牌号,物理内存相当于真实房子;房子换了,门牌号保持原样。

  1. ❌子进程全盘复制父进程
    ✅PID/PPID全新,上下文快照继承

  2. ❌僵尸进程占用代码数据内存
    ✅用户资源全释放,只剩内核PCB

  3. ❌cd命令创建子进程
    ✅内置命令,bash内部执行

  4. ❌D状态进程可以被kill -9杀死
    ✅D状态屏蔽所有软件信号,OOM也杀不动

  5. ❌所有IO都会进入D状态
    ✅普通网络IO是S状态,硬件块设备IO才是D

  6. ❌list_head指针直接指向task_struct
    ✅只指向内部成员,必须container_of回推

  7. ❌一个进程只能挂一条内核链表
    ✅可嵌入多个list_head,同时挂多条链表

  8. ❌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统一管理。

💡虚拟地址空间分区概览(低地址 → 高地址)

  1. 正文代码段:存放只读程序指令
  2. 初始化数据段data:初始化全局、static变量
  3. BSS段:未初始化全局/static变量,内核自动清零
  4. 堆heap:malloc申请,地址向上增长
  5. 大片镂空空洞:无页表映射,访问直接段错误
  6. mmap共享区:动态库、文件映射
  7. 栈stack:局部变量,地址向下增长
  8. 栈顶端:存放argv命令行参数、env环境变量
  9. 最高段:内核空间,用户态无权访问

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)

为什么不直接操作物理内存,非要搞这么复杂?

  1. 安全风险控制:如果进程能直接访问物理内存,流氓软件就能随意修改其他程序的内存甚至系统内核导致死机。有了虚拟地址,所有访问都必须经过OS的页表审查。

  2. 解耦合与效率(延迟分配):进程申请内存时(如malloc),OS只在虚拟地址空间里给你分地盘。等你真正去写数据时,OS才会在物理内存里给你找个位置并建立映射。这就做到了进程管理和内存管理的完美解耦。

  3. 统一视角:让每个进程都觉得自己拥有连续、完整的内存空间,程序在物理内存中其实可以见缝插针地随便放,大大提高了空间利用率。

5.3.1 页表权限拦截:字符串常量区崩溃案例

char*str="helloworld";*str='H';

字符串字面量存放在只读字符常量区,对应页表项标记只读权限。
当代码尝试写入该地址,MMU硬件在地址翻译时检测权限冲突,触发异常,内核发送SIGSEGV段错误,程序直接崩溃。

💡重点:不是C语言语法禁止修改,是页表硬件权限拦截保护内存
对比:char str[] = "helloworld"; str[0]='H';可以正常修改;字符串拷贝到栈空间,栈内存页具备读写权限。

5.4 结合虚拟内存理解进程挂起

很多同学容易混淆进程睡眠和进程挂起,借助虚拟内存、页表的知识就很好理解。

  1. 普通睡眠(S状态)
    进程PCB驻留内存,进程依旧占用自己的物理内存,仅仅不参与CPU调度,等待事件唤醒。物理内存数据不会被搬走。

  2. 进程挂起(换出到Swap)
    当系统物理内存资源紧张,操作系统会把进程挂起

  • task_structmm_struct、虚拟区间描述全部保留在内核,虚拟地址空间完整保留,图纸不丢。
  • 修改该进程的页表项:不再指向物理内存,标记数据存放于磁盘Swap分区。
  • 进程原本占用的物理内存页全部回收,分配给其他急需内存的进程。

进程自身完全感知不到,它眼中的虚拟地址空间没有任何变化。

当进程需要恢复运行,CPU访问虚拟地址,页表提示页面在磁盘,触发缺页异常。内核将Swap磁盘的数据重新加载回物理内存,更新页表映射,进程就可以继续执行。

💡核心:没有虚拟内存与页表机制,就实现不了进程挂起。
如果没有虚拟地址隔离,一旦把进程数据丢到磁盘,程序地址全部错乱,无法复原。

区分重点:
睡眠:进程数据还在物理内存,只是不跑CPU。
挂起:进程数据挪到磁盘Swap,物理内存释放。

5.5 内核核心思想:再谈「先描述,后组织」

mm_struct这个内核结构体,仅仅是对虚拟地址空间的描述、记账,相当于一张图纸
它记录各个段的起止地址、虚拟区域链表、页表基地址,但是它本身不做地址翻译,不操作硬件。

真正完成内存组织依靠两部分:

  1. CPU硬件MMU(内存管理单元)+页表
    每一次访问内存,由硬件完成虚拟地址到物理地址的翻译,同时做权限校验。

  2. 操作系统内核软件逻辑
    内核负责修改mm_struct登记虚拟地址区间,分配回收物理内存,填充修改页表,处理缺页异常、swap挂起换入换出。

逻辑链条:
mm_struct保存页表基地址 → 内核操作页表 → CPU‑MMU硬件完成地址翻译。

举个例子:调用malloc,仅仅修改mm_struct、vm_area_struct完成虚拟地址预约;
只有访问内存触发缺页异常,内核才分配物理内存,硬件完成映射。

图纸只规划地盘,真正盖房子干活的是系统和硬件。

💡拓展历史小彩蛋:虚拟内存并不是某个人灵光一闪的天才脑洞。
上世纪50‑60年代物理内存硬件价格昂贵,内存资源紧缺倒逼出这套技术。
1956年德国博士生提出虚拟内存理论构想;1962年英国曼彻斯特大学Atlas计算机,世界第一台实现分页虚拟内存的机器;后续Unix、Mach微内核迭代出写时拷贝COW;Linux使用mm_structvm_area_struct把这套模型落地实现。
⚠虚拟地址翻译依赖CPU硬件MMU;没有MMU,光靠内核结构体,虚拟内存完全无法工作。

5.6 三大核心结构体最终从属关系(PCB / mm_struct / list_head)

这是整篇博客最核心的底层架构,串联所有知识点,彻底理清三者从属、关联关系:

  1. list_head 与 PCB(task_struct)
    list_head是PCB内部嵌入的成员,依靠侵入式链表设计,一个task_struct里面可以塞多个list_head。让同一个进程PCB可以同时挂载到多条不同内核链表:全局进程链表、调度运行队列、父子兄弟链表,实现多维度管理,配合container_of宏从链表成员反向拿到完整PCB结构体。

  2. mm_struct 与 PCB(task_struct)
    mm_structtask_struct内部的指针成员,每一个用户进程都会关联一份mm_struct,用来完整描述该进程的整套虚拟地址空间;mm_struct维护各个虚拟段、页表基地址,和硬件MMU配合完成地址映射。

  3. 整体关系总结
    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时间分配,系统调用完成用户态与内核态交互。

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

基于VSCode与Zephyr RTOS的STM32F103C8T6开发环境搭建与实战

在嵌入式开发领域&#xff0c;Zephyr RTOS 以其模块化、高度可配置和跨平台特性&#xff0c;正成为越来越多开发者的选择。然而&#xff0c;对于习惯了传统 IDE&#xff08;如 Keil、IAR&#xff09;的 STM32 开发者&#xff0c;尤其是使用 STM32F103C8T6 这类经典“蓝桥杯”最…

作者头像 李华
网站建设 2026/8/12 16:50:52

二元混合气焊接成本高?节气装置一套搞定

一、前言&#xff1a;制造业二元混合气焊接的成本痛点在自动化焊接批量生产的制造行业里&#xff0c;二元混合气凭借焊缝成型好、飞溅少、抗氧化能力强的工艺优势&#xff0c;成为汽车零部件、工程机械、钢结构等高精度焊接场景的标配保护气体。但长期深耕车间成本管控的工艺工…

作者头像 李华
网站建设 2026/8/12 16:50:22

VSCode终端刷屏问题的诊断与解决方案

1. 问题现象与初步诊断最近在使用VSCode时遇到一个相当恼人的问题&#xff1a;每次打开集成终端&#xff0c;屏幕就会开始疯狂刷屏换行&#xff0c;就像有人不停按回车键一样。这种状况不仅影响工作效率&#xff0c;还会导致终端无法正常输入命令。经过多次测试和排查&#xff…

作者头像 李华
网站建设 2026/8/12 16:50:21

全栈实战:为大型活动构建数字支撑体系的技术方案

这次我们来看一个名为“Main Stem by Lindy Hop SG - World Lindy Hop Day 2026 - Singapore”的项目。从标题来看&#xff0c;这并非一个传统的软件或AI模型项目&#xff0c;而是一个围绕2026年世界林迪舞日在新加坡举办的舞蹈活动。对于技术博客读者而言&#xff0c;其核心价…

作者头像 李华