从冯·诺依曼到进程:程序为什么必须先住进内存
这是复习进程笔记的上篇。
上篇只回答一个问题:"进程"这个听起来很抽象的概念,到底是被什么样的现实限制,一步步逼出来的。
下篇再拆进程本身——PCB 里装了什么、进程有哪几个状态、僵尸和孤儿怎么来的。
0. 先看全景:这一章其实只在回答六个问题
复习最怕的不是记不住,是知识点散成一地,串不起来。先看这条链:
| 顺序 | 问题 | 答案落点 |
|---|---|---|
| ① | 输入→计算→输出,中间"计算"是谁在干? | 冯·诺依曼五大部件 |
| ② | 图里那个"存储器"指的是谁? | 内存,不是硬盘 |
| ③ | CPU 能不能直接读外设? | 不能,根本没有那条线 |
| ④ | 那为什么非得有内存? | 存储分级 + 局部性 + 拷贝效率 |
| ⑤ | 所以程序怎么才能跑? | 必须先加载进内存 |
| ⑥ | 内存里一堆程序,谁管? | 操作系统,先描述再组织 |
一路推到底,最后落在这一句上:
进程 = 内核数据结构 + 自己的代码和数据。
这句话是上篇的终点,也是下篇的起点。
1. 冯·诺依曼给的硬件骨架:CPU 只跟内存打交道
问:平时说"电脑在算",到底是哪个部件在算?
(先别往下看。拿张纸,画一下你脑子里那台机器的样子,再对比。)
答: 这张图值得自己动手画一遍。看着简单,但它决定了后面所有结论。
图上有两样东西要看清:方框和箭头。
先看方框,一共五个:输入设备(键盘、鼠标、摄像头、麦克风)、输出设备(显示器、打印机、喇叭)、存储器(这里特指内存)、运算器、控制器。最后两个框在一起,就是我们天天说的 CPU。
这里要补一下"两个框怎么就等于一个 CPU",因为这是最容易含糊过去的一步:
- 运算器(ALU)负责"算"——加减乘除、与或非、比较大小,都在这里完成;
- 控制器(CU)负责"指挥"——从内存里取指令、翻译指令、控制各个部件下一步干什么、什么时候干;
- 这两块合起来才是 CPU。缺了控制器,运算器不知道算什么;缺了运算器,控制器指挥了也没人干活。
真实芯片里 CPU 内部还有寄存器堆、L1/L2 缓存、总线接口单元等,但在这张概念图里,我们只认这两个逻辑核心部件,其余都算实现细节。
再堵一个特别常见的混淆:CPU ≠ 主机。CPU 只是"算 + 指挥"那块芯片;主机是机箱里那一整套——CPU、内存、主板、总线。主机包含内存,而 CPU 得通过总线才能去内存取数据。面试里混着说,很容易被抓。
再看箭头,一共两种颜色,这才是重点:
- 黑箭头 = 控制信号,全都是从控制器发出去的。控制器就是"下命令的那个"。
- 红箭头 = 数据信号,是数据被搬来搬去的路线。
顺着红箭头看一遍,你会发现一个特别关键的事:数据是从输入设备进内存,从内存出到输出设备,运算器只跟内存之间来回搬。整张图上,没有任何一条红线是从输入设备直接连到运算器的。
1.1 这里的"存储器",指的是硬盘吗?
(停三秒。这问题看着傻,但这是这一节唯一的坑。)
答:内存。只能是内存。
“这里的存储器,指的是内存!!!” 紧接着又丢出一个问题:“为什么要有内存?”
注意,这句不是随口一说,它是第 2 节真正要回答的技术问题。反过来想一下:如果这个框指的是硬盘,那这话根本不用问——硬盘本来就是仓库,"为什么要有仓库"是个废话问题。
1.2 第一个反直觉:CPU 根本不直接碰外设
问:CPU 那么快,外设那么慢。让 CPU 直接去读键盘、直接往屏幕写,不是少搬一趟吗?
(我以前的标准答案是"因为外设慢"。你先写下你的答案——如果你也只想到"慢",那你和我一样,只答对了一半。)
答: 这里有两层,而且第一层比第二层更根本。
第一层:物理上就没有这条通路。
CPU 的地址线和数据线,接的是存储器。外设挂在另一套 I/O 通路上。在数据这一层,CPU 和外设之间根本没有一根线连着。
所以这不是"慢不慢"的问题,是接不上的问题。就像你的 U 盘只能插 USB 口,USB 口的另一头通向内存——你没法把 U 盘直接怼到 CPU 的针脚上。
第二层:就算接得上,也不该接。
外设比 CPU 慢了不止一个数量级。键盘按一下是毫秒级,CPU 执行一条指令是纳秒级,中间差了上百万倍。让 CPU 直接等外设,等于让一个短跑冠军站在传送带旁边等快递——整台机器的速度,被最慢的那一环锁死了。
这就是像那个漏水的木桶:木桶能装多少水,取决于最短的那块板。
1.3 于是有了本章最该背的一句话
把两层合起来:
在数据层面上,CPU 不跟外设打交道,CPU 只跟内存打交道。
所有设备,也只能直接跟内存打交道。
反过来更常用:外设要输入数据,只能写进内存;要输出数据,只能从内存里读。 没有任何一个设备,能把数据直接交给另一个设备。
另外,冯·诺依曼这套结构之所以能被全世界接受,是因为它给了一个性价比——用一块相对便宜的大容量内存,把"贵得离谱的高速 CPU"和"便宜的慢速外设"隔开,于是普通人也能用不太贵的价格,买到一台效率够用的电脑。
这不只是技术结论,这也是为什么电脑能普及、能长出互联网。
2. 为什么非得有内存:分级、局部性与拷贝
问:内存只是个"中转站"?那能不能不要中转,数据直接从硬盘进 CPU?
答: 不能。而且原因比"内存比硬盘快"深得多。
2.1 存储是分级的:越快越小越贵
先记住一个残酷的现实:没有任何一种存储介质,能同时做到又快、又大、又便宜。 所以只能分级,让它们互相打配合。你可以想象成——
- 寄存器:手里正拿着的那支笔,一伸手就够到;
- L1/L2/L3 缓存:桌上的笔筒,一起身就够到;
- 内存(主存):旁边的书架,走两步能拿到;
- 磁盘:楼下图书馆的书库,得下去找;
- 远程存储:别的城市的图书馆,得寄过来。
越近越快越小越贵,越远越大越慢越便宜。
| 层级 | 介质 | 大致容量 | 大致速度 | 谁在用 |
|---|---|---|---|---|
| L0 寄存器 | CPU 内部 | 几十~几百字节 | 1 个时钟周期 | CPU 运算 |
| L1~L3 缓存 | SRAM | 几十 KB ~ 几十 MB | 几~几十周期 | CPU |
| L4 主存 | DRAM | 几 GB ~ 几百 GB | 上百周期 | 程序运行时 |
| L5 本地二级 | 磁盘/SSD | 几百 GB ~ 几 TB | 毫秒级 | 持久保存 |
| L6 远程二级 | 网络/分布式 | 近乎无限 | 受网络限制 | 共享、备份 |
这张表还有一句潜台词:每一层都在给上一层当"大仓库"。寄存器里放的,是从缓存取来的字;缓存里放的,是从内存取来的缓存行;内存里放的,是从磁盘取来的磁盘块。
数据就这样从下往上,被一段一段地"搬近"CPU。
2.2 局部性原理:效率提升的真正机制
问:等等——如果 CPU 每次要用的数据都得现从硬盘搬,那内存不就成了个减速带?凭什么说它提升了效率?
答: 这是这次复习我最大的收获。以前我只记住了"局部性原理"这五个字,但根本说不清它到底省了什么。
先说清楚它没省什么:它没有让任何一次访问变快。 从内存读一个字节还是要上百个时钟周期,从硬盘读还是几毫秒——单次访问的时间,一点没变。
那它省的是什么?省的是"去慢设备的趟数"。
算笔账就懂了。假设遍历一个 100 万个 int 的数组:
- 如果每次访问都真的去读硬盘:100 万次 × 每次几毫秒 ≈ 2.8 小时;
- 如果先把这 4 MB 一次性搬进内存:搬运花几十毫秒,之后 100 万次全在内存里读,总共 0.14 秒左右。
差了大约七万倍。而硬盘的速度一次都没变,内存的速度也没变。变的是去硬盘的次数:从 100 万次,变成了 1 次。
所以这条公式比"局部性"三个字有用得多:
单位使用成本 = 搬运成本 ÷ 复用次数。
内存不是"更快的中转站",内存是个杠杆:用一次昂贵搬运,换后面成千上万次便宜访问。只要复用次数够大,那次搬运就被摊薄到几乎为零。
那"复用次数足够大"这件事,凭什么成立?
凭的是程序访问内存不是随机的。它有两条脾气:
- 时间局部性:刚用过的东西,很可能马上还要用。——最典型的是循环里的累加变量
sum,就那 4 个字节,却被用了 100 万次。搬一次,用一百万次。 - 空间局部性:用了这个地址,它隔壁的地址很可能也要用。——数组就是这样,你访问
a[0],下一步几乎必然访问a[1]。
空间局部性还引出一个容易忽略的设计:搬运从来不是按字节搬,是按"块"搬。 硬盘一次读 4 KB,缓存一次读一行 64 B。你只要 4 个字节,它送来 4096 个——看着浪费,却一次搬运换来 1024 次命中。
那什么时候不成立?
反过来想就清楚了:如果程序真在大范围随机访问——比如在 100 GB 数据里乱跳着查——每块数据都只被访问一次,复用次数 ≈ 1,单位成本就等于搬运成本本身。这时候"先搬进内存"毫无意义。
这正好解释了数据库为什么要建索引:它不是魔法,而是人为把"随机访问"改造成"局部性访问"——先顺着 B+ 树走到目标附近,再在那个小范围里连续读。
顺带一提:同样是遍历,数组比链表快得多,很大程度上也是空间局部性的功劳——数组地址连续,一次搬运能命中一大片;链表节点散在各处,每次跳转都可能是一趟新搬运。
所以那句"计算机的效率,就以内存为主了"不是夸张——内存是整台机器上被访问最频繁的一级存储,它快不快、装得下多少"热点",直接决定体感。
2.3 一句话看穿计算机在干什么:拷贝
问:从刚才那张图上再往下想一层——整台机器,本质上在干嘛?
答: 一句我当时没当回事、后来越用越顺手的话:
计算机数据流动的过程,本质就是拷贝。
所以计算机的效率问题,由设备的拷贝效率决定。
回头再看红箭头,每走一步都是一次拷贝:键盘→内存、内存→运算器、运算器→内存、内存→显示器。所谓"计算",只是两次拷贝之间夹着的一小段变换而已。
这个视角特别实用:以后遇到程序卡,先问一句"这里数据搬了几次?",往往比"我算法是不是不够优"更快抓到问题。一次多余的 memcpy、一次本可以避免的用户态和内核态之间的复制,比一个 O(n²) 的小循环更容易拖垮真实程序。
2.4 那还要硬盘和网络干什么?
问:既然内存又快又好,为什么还留着慢吞吞的磁盘和网络?
答: 因为内存有三个绕不过去的毛病:贵、小、断电就没了。
- 断电就丢 → 必须有磁盘来永久保存;
- 装不下全部资料 → 必须有磁盘来存全量;
- 一台机器装不下全世界的资料 → 还得有远程存储来共享。
所以分级的全部逻辑就是:用便宜的大容量存"全量",用昂贵的小容量装"高频",中间靠"提前搬一部分上来"骗过速度差。
内存不是减速带,内存是让这个"骗术"成立的关键零件。
3. 所以程序必须先被搬进内存
问:硬件层面清楚了。那一个 .c 文件,是怎么变成"正在跑的东西"的?
答: 这条链其实特别短:
hello.c --编译--> hello(二进制可执行文件) --加载--> 内存 --CPU执行--> 结果
关键在中间那一步的性质:hello 这个文件躺在磁盘上的时候,就是个普普通通的二进制文件——和一张 .jpg、一个 .mp4 没有本质区别。它不会自己动,也不会自己算。
程序要运行,必须先加载到内存里。
问:为什么?凭什么非加载不可?
答: 因为体系结构规定。回到 1.3 的结论:CPU 只会从内存取指令、读写数据。磁盘根本不在 CPU 的通路上,所以磁盘上的程序,CPU 够不着。
这就是全部的答案。它不是软件设计上的选择,而是硬件接线方式带来的硬约束。
3.1 程序和指令,本质上都是"数据"
问:那"指令"和"数据"总该是两码事吧?程序是指令,变量才是数据。
答: 在冯·诺依曼体系里,它们是同一回事。这正是这套结构最核心的思想:
- 指令和数据存在同一个存储器里,走同一套总线;
- 它到底是指令还是数据,取决于你怎么解读它,而不是它被存在哪;
- 所以那句"程序就是数据",字面意思就是字面意思。
这个认识有用在哪?它解释了缓冲区溢出、代码注入这类攻击为什么能成立——因为数据区可以被当成代码去执行。如果指令和数据是物理隔开的(那是另一种叫"哈佛结构"的设计),这类问题会少一大半。
3.2 输入输出,是站在谁的角度说的?
问:printf 叫输出,scanf 叫输入。这个"输入输出",是站在人的角度?还是屏幕的角度?
答: 站在内存的角度。更准确说,站在已经加载进内存的那个程序的角度。
- 数据从外面进到内存 → 对这个程序来说就 input(读文件、收网络包、按键);
- 数据从内存出去 → 对它来说就 output(写文件、发网络包、显示)。
代码里也藏着线索:<stdio.h> 里的 std 是 standard,i 是 input,o 是 output;C++ 的 <iostream> 名字里直接写着 i/o stream。
"输入输出"从来是对内存里的那个程序说的,不是对硬件说的。
问:那磁盘是输入设备还是输出设备?网卡呢?
答: 都是,看你站在哪一边:
- 读文件时:磁盘把数据交给内存 → 磁盘是输入设备;
- 写文件时:内存把数据交给磁盘存下去 → 磁盘是输出设备;
- 网卡同理:收包是输入,发包是输出。
教材常把"网卡、磁盘"同时在输入和输出两栏里各列一遍,原因就在这里——设备本身没有固定身份,"输入/输出"只是相对于内存里那个程序的一次数据流向。
3.3 完整走一遍:给朋友发一条 QQ 消息
一个很经典的检验题:把"登录 QQ 到发出消息"的数据流讲清楚。用上面这套模型走一遍,正好检验是不是真懂了:
一步一步对:
- 你敲键盘 → 字符被拷贝进内存里 QQ 进程的缓冲区(对它来说是 input);
- QQ 打包这条消息 → 数据从内存拷贝到网卡(对它来说是 output);
- 网卡把电信号送上网络 → 穿过路由器、光纤,到朋友那台机器;
- 朋友的网卡收到 → 拷贝进他内存里的 QQ 进程(input);
- QQ 渲染 → 数据从内存拷贝到显示器(output),他在屏幕上看到"开不开?"。
整条链上,没有一步是"设备直接给设备",全是"设备 ↔ 内存"。
而"网络"那一段,本质上是另一台冯·诺依曼机器的输入输出过程。所以把网络画成一朵云、两端各画一整套框图,是有道理的:互联网,就是无数台冯·诺依曼机器互相做 I/O。
发文件呢?只是把第 2 步的"一小段字符"换成"一大块二进制",拆成很多个包,多搬几趟而已。模型一个都不用改。
4. 内存里躺着这么多程序,谁管?
到这里,新问题自己冒出来了:
问:每个程序都必须进内存才能跑。可我同时开着浏览器、音乐、终端、QQ……那内存里岂不是躺着一堆"正在跑的程序"?
答: 是。而且推论很硬:它们一定同时在内存里,也一定同时在抢 CPU 和内存。
那接下来只有一条路:必须有人管。这个"人",就是操作系统。
4.1 "操作系统"这四个字,其实有两种含义
这里有个陷阱:平时说的"操作系统",和内核里说的"操作系统",不是一回事。
| 说法 | 都包含什么 | 打个比方 |
|---|---|---|
| 广义的操作系统 | 内核 + shell + glibc + 各种库 + 预装软件 + 图形界面(比如 Ubuntu 桌面) | 整栋楼 + 物业 + 大堂 + 所有服务 |
| 狭义的操作系统 | 只有 Kernel 内核 | 只有配电房和安保室 |
画成图是这样,一个大圈套着两个平级的部分:
内核只是大圈里的一小块,但它是最底下、最硬的那一块。 你装了 Ubuntu 之后看到的桌面、终端、文件管理器,全都长在外壳层上;真正跟硬件打交道的那个核,小得多、也稳定得多。
这个区分有个很实用的地方:"重装系统"重装的是广义的;"内核 panic"死掉的是狭义的。 以后你说"我 Linux 崩了",先想想崩的是哪一层。
4.2 内核的四大功能
内核管四件事,而且这四件事,后面会变成四大章的骨架:
- 内存管理 —— 谁能用哪块内存,用完了怎么收回来;
- 文件管理 —— 磁盘上那一堆字节,怎么变成"文件"和"目录";
- 驱动管理 —— 怎么让五花八门的硬件,都用同一套接口被访问;
- 进程管理 —— 本篇的终点:一堆"正在跑的程序",怎么共存。
这四块不是四个毫不相干的话题,而是同一个目标的四个侧面:把一堆互相打架的需求,收拾成有秩序的状态。
4.3 设计 OS 的目的:对下管资源,对上给环境
归结起来就干干净净两句话:
- 对下:跟硬件打交道,管理所有软硬件资源;
- 对上:给用户程序提供一个良好的执行环境。
"良好"具体指什么?三个词:稳定、高效、安全。这不是喊口号,它们分别对应三件实事——稳定靠隔离和容错,高效靠调度和缓存,安全靠权限和地址空间。
4.4 分层的比方:校长、辅导员、学生
问:操作系统为什么要分层?
答: 有个特别土的比方,但结构一模一样:
| 层 | 现实里的角色 | 系统里的东西 | 干什么 |
|---|---|---|---|
| 最上 | 人 | 用户 | 提需求 |
| 中间 | 校长 | 操作系统 | 做决策 |
| 中间 | 辅导员 | 驱动程序 | 传达、执行、抹平差异 |
| 最下 | 学生 | 网卡、硬盘等硬件 | 干具体活 |
这个比方一眼就照出了"驱动"的位置:它既不是硬件,也不是操作系统本体,而是夹在中间的那个翻译。
为什么需要翻译?因为硬盘有无数型号、网卡有无数芯片,而上层只想说一句"读这个文件"。驱动的活儿,就是把"人话"翻成"这块硬件的方言"。
5. 管理的本质:先描述,再组织
这一节是我最想记下来的,因为它是"从现实世界跨到代码"的那一步。
5.1 校长其实从没见过你
问:校长管着几千个学生,他需要认识每一个人吗?
答: 不需要,而且根本做不到。这里有句说得很重的话:
“管理者和被管理者,管理者管理我们,不需要和我们见面。”
那校长靠什么管?靠数据。 姓名、院系、班级、学号、各科成绩、在校表现、奖惩记录、特长……这些东西汇总到他手上,他就能做决定。
所以:
管理我们,本质上是对我们身上的那些数据做管理。
数据,是拿来做决策的依据。
这就解释了:为什么计算机里的"管理",从来不是"盯着你",而是"存着你的信息"。
5.2 把现实世界翻译成数据结构
问:那这些数据,在 C 语言里长什么样?
答: 一个结构体就装下了:
/* 概念性伪代码,不是可编译代码 */
struct student {
char name[32]; // 姓名
char sex; // 性别
char class[16]; // 班级
int score[8]; // 各科成绩
int age; // 年龄
/* ... 其他属性 */
struct student *next; // 组织方式:链表指针
};
注意最后那一行 struct student *next。这一行就是"描述"和"组织"的分界线:上面全在"描述"(这个学生有哪些属性),这一行在"组织"(怎么把所有学生串起来)。
有了它,现实里的管理动作,就一条条变成链表的操作了:
| 现实里的管理动作 | 变成什么操作 |
|---|---|
| 让成绩最好的去参加比赛 | 遍历链表,找 score 最大的节点 |
| 开除一名学生 | 从链表里删掉一个节点 |
| 让一名学生留级 | 改一下这个节点的状态字段 |
| 让一名学生加入篮球队 | 把这个节点插进另一条链表(球队链表) |
现实世界的管理需求,一条不落地变成了"链表的增删改查"。 这就是那句话的分量:
先描述,再组织。
这是把现实世界的问题,变成计算机模型的核心规律。
5.3 这套办法不止用来管学生
问:那操作系统管硬件,也是这么干?
答: 一模一样。操作系统管硬件,用的也是同一套办法——struct device:
struct device {
int type; // 类型:网卡还是硬盘
int state; // 状态:空闲还是忙
char vendor[32]; // 厂商
int accessible; // 能不能被访问
struct device *next; // 组织:把所有设备串成一条链
};
字段换成了硬件的属性,方法一个字没改。
所以说"这是真实世界的真相"才成立:不是"操作系统碰巧用了链表",而是"管理这件事,在现实世界和计算机里,本来就是同一个形状"。
顺带还能解释一个语言层面的问题:C++ 为什么要有类和 STL? 因为类就是"先描述"(把一类事物的属性捆成一个类型),容器就是"再组织"(把这些对象按某种结构放起来)。语言特性不是凭空冒出来的,它是对这套建模规律的语法化。
5.4 所以 OS 内部最核心的东西是数据结构
这里有个必须用力强调的结论:
OS 内部,将来核心的东西是什么?数据结构!!!
我现在理解是:内核里那些 task_struct、mm_struct、files_struct、inode、dentry、file_operations,无非就是一堆精心设计过的 struct,再加上把它们串起来的数据结构。 所谓"读懂内核",很大程度上就是读懂这些结构体的字段,以及它们的组织关系。
举个例子,file_operations 就是一张函数指针表:把"上层统一的 read/write"和"下层每块硬盘各不相同的读写实现"缝在一起。这也是"换块硬盘,上层代码不用改"能成立的原因。
6. 用户态与内核态:为什么不让我直接碰硬件
问:既然内核是"管家",那我的程序为什么不直接去操作硬件,非要绕一层?
6.1 系统调用,为什么"以前从没见过"
写 C 写了那么久,printf、fopen、malloc 天天用,可"系统调用"这词是最近才听说的。答案很朴素:因为它们被包起来了。
很多 C/C++ 函数,本身就是对系统调用的封装。
- 你写
printf,它内部最终会调write这个系统调用,把数据交给内核,内核再交给驱动,驱动最后写进屏幕; - 你写
fopen,内部最终会调open; - 你写
malloc,要么在用户态的堆里切一块(不够就用brk扩堆),要么对大块内存直接走mmap。
你其实一直在用系统调用,只是从没直接写过 syscall 这几个字母。 库函数是糖衣,系统调用是里面那颗药。
6.2 银行大厅:大堂经理、保安、金库
问:那为什么非要裹这层糖衣?我直接调系统调用不行吗?
答: 技术上能,但权限模型不允许你随便碰硬件。理解这件事最好的比方,是银行:
对着看:
- 金库 = 硬件。你不能自己进去拿钱(不能直接读写硬件)。
- 保安 = 权限检查。每次请求都得过一遍身份核验(从用户态进内核态)。
- 大堂经理 = 系统调用接口。他是你唯一能合法说话的人;你说"我要取 5000",他来决定给不给、怎么给。
有两句话概括得特别精准:
“不相信任何人,但是他必须得给正常的人提供服务!”
“在保证自身安全的情况下,给别人提供服务。”
这就是内核的设计悖论:它不能信任任何一个用户程序(因为群众里有坏人),但它又必须为所有用户程序服务。 解法就是:只开一扇窄门(系统调用),门后的活儿全由内核代劳,每次进门都验一遍身份。
6.3 一张图看清整条调用链
把上面这些层摞起来:
这条链能解释一堆现象:
printf有时候"没输出"——数据还堵在用户态缓冲区,压根没进内核;- 一次
write可能比一万次加法还慢——它要穿两道门、过一次拷贝; - “内核崩了整台机器就完”,而"你的程序崩了只死一个"——权限和地址空间都不同。
6.4 操作系统自己,也是内存里的一个程序
最后补一个容易被忽略的前置问题:“在我们还没启动任何进程之前,第一款软件是什么?”
答案是:操作系统自己。 而操作系统也是软件,它也在内存里。
所以此刻你的内存里同时躺着:操作系统 + 一堆用户程序。
这句话是通往下一节的桥——因为**"内存里同时存在多个程序"这件事本身,就是"进程"这个概念产生的全部理由。**
7. 落点:内存里那个"正在跑的程序",到底叫什么
问:最后一问。内存里有操作系统、有浏览器、有 QQ。内核要怎么描述"浏览器正在跑"这件事?
答: 用第 5 节那套办法:先描述,再组织。
7.1 从 struct device 到 struct XXXX
内核里必然存在这么一个结构体,专门用来描述"一个正在内存里跑的程序"。写法很朴素:
/* 概念性伪代码 */
struct XXXX {
/* 进程的所有属性:它是谁、它在哪、它跑到哪了、它占着哪些资源 */
struct XXXX *next; /* 组织:把所有进程串成一条链 */
};
这个 XXXX,在内核源码里就是 task_struct(进程控制块 PCB 的一种实现)。它的字段会包括:进程 ID、状态、优先级、程序计数器、栈指针、打开的文件表、内存映射、父子关系……这些留到下篇讲。
7.2 进程 = 内核数据结构 + 自己的代码和数据
整个推导到这儿,就收在这一句上:
进程 = 内核数据结构 + 自己程序的代码和数据。
拆开看,等号两边各是什么:
| 部分 | 在哪 | 谁维护 | 作用 |
|---|---|---|---|
| 内核数据结构(PCB / task_struct) | 内核空间 | 操作系统 | 这个进程的"档案" |
| 自己的代码和数据 | 用户空间 | 进程自己 | 真正被 CPU 执行的"实体" |
这个等式能解开三个特别常见的混乱:
- 程序 ≠ 进程。 程序是磁盘上的静态文件,进程是它被加载进内存、内核又给它建了档案之后的动态存在。同一份程序,可以同时跑出好几个进程。
- 进程 ≠ 只有代码。 如果只有代码和数据,内核根本"看不见"它、也管不了它。没有那份内核数据结构,就没有"进程"这个身份。
- "进程切换"到底在切什么。 不是把代码搬来搬去,而是换掉 CPU 当前正在用的那套内核数据结构和地址空间。想通这点,后面所有关于调度、上下文切换的内容都会顺理成章。
7.3 一句话讲给面试官听
如果只能答一句,我会这么说:
“进程是操作系统对正在内存里运行的程序的抽象,由两部分组成:内核为它维护的数据结构(PCB),和它自己的代码和数据。前者让它’可被管理’,后者让它’能被执行’。”
8. 面试速记卡
考前扫这一张就够。右边那列,是你答案里必须出现的那个关键词。
| 问题 | 一句话答案 |
|---|---|
| 冯·诺依曼五大部件? | 输入设备、输出设备、存储器、运算器、控制器;后两个合称 CPU |
| 这里的"存储器"指什么? | 内存,不是硬盘 |
| CPU 能直接读外设吗? | 不能,数据层面根本没有通路;而且外设太慢 |
| 所有设备怎么交换数据? | 一律只跟内存打交道,设备之间不直接通信 |
| 为什么必须有内存? | 存储分级(快大便宜不可兼得)+ 局部性(能提前加载)+ 拷贝效率 |
| 为什么程序要先加载进内存? | 体系结构规定:CPU 只会从内存取指、读写数据 |
| 指令和数据有区别吗? | 冯·诺依曼里没有物理区别,都在同一存储器,看你怎么解读 |
| 输入输出是站在谁的角度? | 站在内存里那个程序的角度 |
| 磁盘是输入还是输出设备? | 都是,看数据流向 |
| 计算机本质在干嘛? | 拷贝,效率由拷贝效率决定 |
| 广义/狭义操作系统? | 广义含 shell、glibc、图形界面;狭义只有内核 |
| 内核四大功能? | 内存、文件、驱动、进程管理 |
| 设计 OS 的两个目的? | 对下管资源,对上给执行环境(稳定、高效、安全) |
| 管理的本质? | 先描述,再组织;管理者靠数据做决策 |
| 系统调用为什么平时没见过? | 都被库函数封装了(printf → write) |
| 为什么要用户态/内核态? | 不信任任何用户程序,但必须为它服务;只开一扇窄门 |
| 进程的定义? | 内核数据结构(PCB)+ 自己的代码和数据 |
| 程序和进程的区别? | 程序是磁盘上的静态文件,进程是加载进内存并被内核登记后的动态实体 |
| 进程切换在切什么? | 切的是内核数据结构和地址空间,不是搬代码 |
9. 追问预演
面试最怕的从来不是第一问,是第二问。这里是我预演过的连环追问。
Q1:为什么 CPU 不直接访问外设?
两层缺一不可。第一,体系结构上没有这条数据通路——CPU 的地址线和数据线接的是存储器,外设挂在 I/O 通路上;第二,就算接得上,外设比 CPU 慢几个数量级,整机会被最慢那环锁死。所以设计上直接规定死:所有设备只跟内存换数据。
Q2:那为什么不让 CPU 直接读硬盘,非要多一层内存?
因为存储介质的速度、容量、成本三者不可兼得,只能分级。而内存真正起的作用,靠的是局部性原理——程序访问有时间和空间局部性,一小块热点数据搬进内存后能被反复命中,于是"CPU 等硬盘"变成了"CPU 跟内存交互"。内存的价值不在于它比硬盘快,而在于它让"提前加载"这件事划得来。
Q3:局部性原理具体指什么?有反例吗?
时间局部性:刚访问的数据很可能马上再被访问(循环变量)。空间局部性:访问了某地址,邻近地址很可能也被访问(数组、连续结构体)。反例是大范围随机访问——几百 GB 数据上随机查,缓存全不命中,内存和磁盘的差距就真实暴露了。这也正是数据库要做索引和预读的原因。
Q4:程序加载进内存,到底加载了什么?
至少三样:代码段(指令)、数据段(全局静态数据)、栈和堆(运行时动态用)。此外内核还会给它建一套地址空间映射(页表),让它以为自己独占整块内存。所以"加载"不只是拷贝,还包括建映射。
Q5:进程的数据结构里,到底装了什么?
给个骨架:标识(PID、PPID)、状态与调度信息(状态、优先级、时间片、程序计数器、栈指针)、资源信息(打开的文件表、内存映射、信号处理表)、组织信息(父子关系、链表指针)。一句话:PCB 就是这个进程的"档案 + 现场快照"。
Q6:操作系统自己也在内存里,它不用被加载吗?
要。开机时引导程序先把内核从磁盘读进内存,再把控制权交出去。之后内核常驻内存,因为它得随时响应系统调用和中断。顺带一个推论:内核是个特殊存在——它没有普通进程那样的身份,更准确地说,它运行在某些进程的上下文里,或者跑在独立的内核线程上。
Q7:“先描述再组织”,在 Linux 里对应什么代码?
描述层是
struct task_struct(进程)、struct device+struct file_operations(设备)、struct inode/struct dentry(文件);组织层是各种链表和树——进程有链表和调度用的红黑树,文件系统是树,设备挂在总线链表上。内核的骨架,就是这些结构体加上它们的组织关系。
10. 易错点对照表
这些是我这次复习真实踩过的坑,面试说错一句就露馅。
| 你可能以为…… | 其实是这样 | 错在哪 |
|---|---|---|
| 存储器就是硬盘 | 是内存 | 那个框跟运算器直接互连,硬盘没这条线 |
| CPU 直接读外设会很慢 | CPU 根本没有直连外设的通路 | 只答"慢",会被追问"那让它快点不就行" |
| 外设之间可以互传数据 | 所有设备只能跟内存换数据 | 图上没有一条设备到设备的箭头 |
| 加载进内存是为了快 | 是因为体系结构规定 CPU 只能从内存取指 | "快"是结果,通路才是原因 |
| 指令和数据是分开放的 | 冯·诺依曼里它们同在一个存储器 | 混淆了哈佛结构 |
| 输入设备就是键盘鼠标 | 输入/输出是相对内存里那个程序的角色,磁盘网卡都双重身份 | 设备没有固定身份 |
| 操作系统就是那个图形界面 | 图形界面属于广义 OS,狭义只有内核 | 把外壳当成了本体 |
| 进程就是运行中的程序 | 不完整。进程 = 内核数据结构 + 代码和数据 | 少了 PCB,就解释不了"怎么管" |
| 同一个程序跑两次还是同一个进程 | 是两个进程,各有独立 PCB 和地址空间 | 混淆程序与进程 |
| 进程切换是把代码搬走 | 切的是内核数据结构和地址空间(页表) | 代码一直在内存里没动 |
| 系统调用是 C 语言才有的 | 系统调用是内核提供的接口,什么语言都能调 | 库函数只是封装 |
| 调库函数就等于进内核 | 只有封装的系统调用才进内核 | printf 进,strlen 不进 |
11. 下一篇讲什么
上篇把"为什么会有进程"推到尽头了:
CPU 只能碰内存 → 程序必须先进内存 → 多个程序同时在内存 → 因此必须被管 → 先描述再组织 → 内核里冒出一个描述"正在跑的程序"的结构体 → 这个东西,叫进程。
但悬念还在:那个结构体里,到底装了什么?
下篇拆三件事:
- PCB /
task_struct的字段 —— 它凭什么能代表一个进程; - 进程状态机 —— 就绪、运行、阻塞怎么转,为什么非要有"阻塞"这个状态;
- 僵尸进程和孤儿进程 —— 它们不是 bug,是这套模型跑到边界时必然出现的东西。
(两篇的切点就是上面那句等式,下篇不会再重复冯·诺依曼。)
本文是复习笔记的上篇。配套还有下篇《进程机制:PCB、状态机、僵尸与孤儿》。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/lemo_n_/article/details/168532036




