今天要早睡_头像
Linux:冯诺依曼体系结构到进程_第一篇封面图

Linux:冯诺依曼体系结构到进程_第一篇

从冯·诺依曼到进程:程序为什么必须先住进内存

这是复习进程笔记的上篇。

上篇只回答一个问题:"进程"这个听起来很抽象的概念,到底是被什么样的现实限制,一步步逼出来的。

下篇再拆进程本身——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 进程的数据)

网卡
(此时是输出设备)

网卡
(此时是输入设备)

内存
(QQ 进程的数据)

显示器
(输出设备)

一步一步对:

  1. 你敲键盘 → 字符被拷贝进内存里 QQ 进程的缓冲区(对它来说是 input);
  2. QQ 打包这条消息 → 数据从内存拷贝到网卡(对它来说是 output);
  3. 网卡把电信号送上网络 → 穿过路由器、光纤,到朋友那台机器;
  4. 朋友的网卡收到 → 拷贝进他内存里的 QQ 进程(input);
  5. QQ 渲染 → 数据从内存拷贝到显示器(output),他在屏幕上看到"开不开?"。

整条链上,没有一步是"设备直接给设备",全是"设备 ↔ 内存"。

而"网络"那一段,本质上是另一台冯·诺依曼机器的输入输出过程。所以把网络画成一朵云、两端各画一整套框图,是有道理的:互联网,就是无数台冯·诺依曼机器互相做 I/O。

发文件呢?只是把第 2 步的"一小段字符"换成"一大块二进制",拆成很多个包,多搬几趟而已。模型一个都不用改。


4. 内存里躺着这么多程序,谁管?

到这里,新问题自己冒出来了:

问:每个程序都必须进内存才能跑。可我同时开着浏览器、音乐、终端、QQ……那内存里岂不是躺着一堆"正在跑的程序"?

答: 是。而且推论很硬:它们一定同时在内存里,也一定同时在抢 CPU 和内存。

那接下来只有一条路:必须有人管。这个"人",就是操作系统。

4.1 "操作系统"这四个字,其实有两种含义

这里有个陷阱:平时说的"操作系统",和内核里说的"操作系统",不是一回事。

说法都包含什么打个比方
广义的操作系统内核 + shell + glibc + 各种库 + 预装软件 + 图形界面(比如 Ubuntu 桌面)整栋楼 + 物业 + 大堂 + 所有服务
狭义的操作系统只有 Kernel 内核只有配电房和安保室

画成图是这样,一个大圈套着两个平级的部分:

广义操作系统:普通人装了系统后看到的那一整套

Kernel 内核 —— 狭义的操作系统

进程管理

内存管理

文件管理

驱动管理

外壳与库层(内核之外的都算)

shell(bash / zsh)

glibc 与各种库

预装软件、图形界面

内核只是大圈里的一小块,但它是最底下、最硬的那一块。 你装了 Ubuntu 之后看到的桌面、终端、文件管理器,全都长在外壳层上;真正跟硬件打交道的那个核,小得多、也稳定得多。

这个区分有个很实用的地方:"重装系统"重装的是广义的;"内核 panic"死掉的是狭义的。 以后你说"我 Linux 崩了",先想想崩的是哪一层。

4.2 内核的四大功能

内核管四件事,而且这四件事,后面会变成四大章的骨架:

  1. 内存管理 —— 谁能用哪块内存,用完了怎么收回来;
  2. 文件管理 —— 磁盘上那一堆字节,怎么变成"文件"和"目录";
  3. 驱动管理 —— 怎么让五花八门的硬件,都用同一套接口被访问;
  4. 进程管理 —— 本篇的终点:一堆"正在跑的程序",怎么共存。

这四块不是四个毫不相干的话题,而是同一个目标的四个侧面:把一堆互相打架的需求,收拾成有秩序的状态。

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 一张图看清整条调用链

把上面这些层摞起来:

应用程序
(用户态)

库函数 / glibc
printf fopen malloc

shell / 系统调用接口
(那扇门)

内核
(内核态)

驱动程序

硬件
磁盘 网卡 显示器

这条链能解释一堆现象:

  • 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 执行的"实体"

这个等式能解开三个特别常见的混乱:

  1. 程序 ≠ 进程。 程序是磁盘上的静态文件,进程是它被加载进内存、内核又给它建了档案之后的动态存在。同一份程序,可以同时跑出好几个进程。
  2. 进程 ≠ 只有代码。 如果只有代码和数据,内核根本"看不见"它、也管不了它。没有那份内核数据结构,就没有"进程"这个身份。
  3. "进程切换"到底在切什么。 不是把代码搬来搬去,而是换掉 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 只能碰内存 → 程序必须先进内存 → 多个程序同时在内存 → 因此必须被管 → 先描述再组织 → 内核里冒出一个描述"正在跑的程序"的结构体 → 这个东西,叫进程。

但悬念还在:那个结构体里,到底装了什么?

下篇拆三件事:

  1. PCB / task_struct 的字段 —— 它凭什么能代表一个进程;
  2. 进程状态机 —— 就绪、运行、阻塞怎么转,为什么非要有"阻塞"这个状态;
  3. 僵尸进程和孤儿进程 —— 它们不是 bug,是这套模型跑到边界时必然出现的东西。

(两篇的切点就是上面那句等式,下篇不会再重复冯·诺依曼。)


本文是复习笔记的上篇。配套还有下篇《进程机制:PCB、状态机、僵尸与孤儿》。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/lemo_n_/article/details/168532036

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--