进程前传:冯·诺依曼、存储分级与操作系统
进程前传:冯·诺依曼、存储分级与操作系统
写在前面:这是我学 Linux 进程管理之前的一篇"前传"。不先把冯·诺依曼、存储分级、操作系统这三件事捋清楚,后面看
task_struct、fork()、进程状态机的时候,会觉得全是凭空出现的名词。这篇博客解决的就是"为什么会有这些东西"。
零、先给一条主线
整篇博客其实只在回答一个问题:一个写在磁盘上的 code.exe,是怎么一步步跑到 CPU 上执行的?
沿着这条线,五个问题依次展开:
| # | 问题 | 关键词 |
|---|---|---|
| 1 | 计算机由什么组成 | 冯·诺依曼、五大部件 |
| 2 | CPU 那么快,磁盘那么慢,怎么配合 | 存储分级、局部性原理 |
| 3 | 程序为什么必须先加载到内存 | 指令即数据、CPU 只认地址 |
| 4 | 谁来管这一切 | 操作系统、先描述再组织 |
| 5 | 用户怎么让内核干活 | 内核 / 驱动 / 系统调用 |
一句话提前剧透:
内核负责"做什么",驱动负责"怎么碰硬件",系统调用负责"用户怎么把请求递进去"。
1. 冯·诺依曼结构及其部件

1.1 五大部件
冯·诺依曼结构把一台计算机拆成五个部分:
- 运算器:负责算术运算和逻辑运算
- 控制器:负责取指令、译码、发控制信号
- 存储器:这里特指内存(重点,后面会反复强调)
- 输入设备:键盘、鼠标、扫描仪、摄像头、话筒、网卡、磁盘……
- 输出设备:显示器、打印机、网卡、磁盘……
其中 运算器 + 控制器 = 中央处理器(CPU)。
注意一个细节:网卡和磁盘同时出现在输入和输出里。因为它们既能往里写,也能往外读——一块磁盘既能把文件读进内存(输入),也能把内存里的数据落盘(输出)。
输入单元和输出单元统称外围设备(外设),相比于 CPU,它们的速度非常慢。
1.2 两条流:数据流与控制流
看那张图上的两个箭头标注就能明白,冯·诺依曼结构里跑的是两种东西:
┌────────┐ 数据信号 ┌────────┐ 数据信号 ┌────────┐
│ 输入设备│ ──────────→ │ 存储器 │ ──────────→ │ 输出设备│
└────────┘ └────────┘ └────────┘
↑ ↓
┌────────┐
│ CPU │ ← 控制信号从这里发出
└────────┘
- 数据流:输入设备 → 存储器 → CPU → 存储器 → 输出设备
- 控制流:控制器发出控制信号,指挥各个部件什么时候取、什么时候算、什么时候写
整个结构的运行方式可以概括成一条流水线:
输入 → 计算 → 输出
1.3 真正的革命:存储程序
冯·诺依曼结构最核心的思想不是"有五个部件",而是这句话:
程序(指令)和数据,以二进制形式,存放在同一个存储器里。
在它之前(比如 ENIAC),"编程"是靠插拔线缆、拨开关来改变电路连接的,换一个题目就要重新接线,人和机器绑死在一起。
冯·诺依曼把这件彻底改了:指令被降格成了数据。既然指令是数据,那它就能被存起来、被搬运、被修改。于是:
- 换程序不用换硬件,只要换内存里的一段内容
- 程序可以被复制、被加载、被多个进程共享
- 计算机变成了"通用"机器
这就是为什么后面第 3 节那句"程序必须先加载到内存"能成立——因为指令本身就是数据,而 CPU 只和存储器打交道。
1.4 一个关键视角:输入输出是站在谁的角度说的
这是笔记里我卡了很久的一个问题:
我们说的"输入"和"输出",是站在谁的角度来考虑问题的?
答案是:站在内存(硬件)的角度。
- 数据流进内存 → 输入(Input)
- 数据流出内存 → 输出(Output)
站在 CPU 角度就没法这么分,因为 CPU 既从内存读,也往内存写。站在人的角度更乱,因为同一个键盘对人是输出、对计算机是输入。
所以以后看到 printf 说"输出"、看到 scanf 说"输入",默认的参考系都是内存。
1.5 顺手解释一个日常现象
我在云南,朋友在北京,我们开 QQ 聊天,数据是怎么过去的?
把两台电脑都画成冯·诺依曼结构,链路就出来了:
云南(我) 北京(朋友)
┌──────────────┐ ┌──────────────┐
│ 键盘→内存→CPU │ │ CPU→内存→显示器│
│ ↓ │ │ ↑ │
│ 内存→网卡(输出)│ ──── 网络(云) ────→ │ 网卡(输入)→内存│
└──────────────┘ └──────────────┘
数据(字符、图片、语音、视频)从我这边输入设备进内存,CPU 处理后从网卡发出去,穿过网络,进到对方的网卡(对他是输入设备),再从显示器出来。
所谓"网络通信",本质上是两台冯·诺依曼机器之间的数据搬运。而这段链路里每一次搬运,都是拷贝。
计算机数据流动的过程,本质就是拷贝。所以计算机的效率,很大程度上由设备的拷贝效率决定。
记住这句话,后面讲缓冲区、零拷贝、DMA 的时候你会回来感谢它。
1.6 冯·诺依曼瓶颈
这套结构有个天生的缺陷:CPU 和内存之间只有一条通路。取指令要走这条路,取数据也要走这条路,两者抢带宽,CPU 就只能干等内存。
这个现象叫冯·诺依曼瓶颈,它直接催生了第 2 节的存储分级和今天 CPU 里的一级/二级/三级缓存。
2. 存储分级:CPU 很快,磁盘很慢

2.1 为什么要分级
先说结论:没有任何一种存储器能同时做到"又快、又大、又便宜"。
- 想做快 → 只能用 SRAM,成本高、容量小、发热大
- 想做大 → 只能用磁盘/闪存,慢得离谱
- 想便宜 → 只能上机械盘,慢上加慢
于是计算机的做法是把这些存储器叠成一个金字塔:
| 层级 | 名称 | 典型介质 | 一句话 | 典型延迟(数量级) |
|---|---|---|---|---|
| L0 | 寄存器 | CPU 内部 | CPU 直接操作 | < 1 ns |
| L1 | 一级缓存 | SRAM,CPU 内 | 每个核心独占 | ~1 ns |
| L2 | 二级缓存 | SRAM,CPU 内 | 每核独占/共享 | ~5 ns |
| L3 | 三级缓存 | SRAM,CPU 内 | 多核共享 | ~20 ns |
| L4 | 主存 | DRAM,内存条 | 就是我们说的"内存" | ~100 ns |
| L5 | 本地二级存储 | 本地磁盘 / SSD | 掉电不丢 | ~10 μs ~ 10 ms |
| L6 | 远程二级存储 | 分布式文件系统、Web 服务器 | 要走网络 | 毫秒级以上 |
规律:越往上越小、越快、越贵(每字节);越往下越大、越慢、越便宜。
从寄存器到磁盘,延迟差了大约 6~7 个数量级(百万倍级别)。这是什么概念?如果访问寄存器是 1 秒,访问一次机械磁盘差不多要 几个月。
2.2 “一个系统的速度由最慢的那部分决定”
一个系统的速度由最慢的那部分决定,但分级存储让最慢的那部分极少被真的等到。
因为程序有局部性。真实程序 90% 以上的访存都落在 cache 和内存里,只有极少数会真的去碰磁盘。于是整体表现得"接近快的那层",而容量和价格又是"慢的那层"的水平——用便宜的大仓库,装出了昂贵的快体验。
还有个细节值得记住:
- 主存(内存)的速度比磁盘快,而且也没有那么夸张(相对 cache 而言,它慢;相对磁盘而言,它快得多)。
- 所以只要能把数据先搬到内存,CPU 就不至于卡死。
2.3 局部性原理:值得预加载的唯一理由
为什么要"预先加载数据"?因为——
只有存在局部性原理,数据才值得提前加载。
局部性有两种:
时间局部性:刚被访问过的数据,很可能马上还会被访问。
int sum = 0;
for (int i = 0; i < 100000; i++)
sum += i; // 变量 i 和 sum 被反复访问,留在寄存器/缓存里
空间局部性:一个地址被访问了,它旁边的地址很可能马上被访问。
int a[100000];
for (int i = 0; i < 100000; i++)
a[i] = i; // 顺序访问,一整条 cache line(64 字节)一次搬进来
正因为有空间局部性,硬件才敢做按块搬运:CPU 不是读 4 个字节,而是一次把一条 cache line(通常 64 字节) 搬进来;操作系统也不是按字节换页,而是一次换 4 KB 的一页。
如果程序完全没有局部性(随机跳着读一个几十 GB 的文件),级存储就失效了,性能会断崖下跌——这也是 i++ 和 i += 1 遍历二维数组时,"按行遍历"比"按列遍历"快好几倍的真正原因。
2.4 存储器的真正作用:把外设挡在 CPU 外面
问题的起点是:CPU 很快,外设很慢,速度严重不同步,怎么解?
解决思路不是"让外设变快"(做不到),而是加一层中间人:
- 把所有需要跟外设打交道的东西,都做成"内存"
- 让 CPU 只跟内存打交道,不直接碰外设
- 用存储器预先加载数据(有局部性原理才值得提前加载)
- 于是外设和 CPU 被隔开了,而内存的速度也不算慢,交互可以接受
改造前 改造后
CPU ←→ 各种外设(速度天差地别) CPU ←→ 内存 ←→ 各种外设
(速度可控) (慢,但被隔离)
一句话总结:
在数据层面上,CPU 不会与外设(输入输出)直接打交道,CPU 读写数据只会和内存打交道。
这条原则后面会以各种形式反复出现:
- 第 3 节:程序必须先加载到内存,因为 CPU 读不到磁盘
- 第 4 节:内核 IO 要做缓冲区(buffer / page cache)
- 第 5 节:驱动做的事,就是把"内存 ↔ 设备"这段搬运封装好
2.5 冯·诺依曼结构的历史意义
让普通人可以以较低的价格,获得效率不错的计算机,使得计算机被广泛接受,这样促成了全球性网民的产生;也只有这样,才可能孵化出互联网。
逻辑链是这样的:
架构统一 → 成本下降 → 计算机普及 → 人手一台 → 需要互联 → 互联网
如果每台计算机都是定制电路、换个程序就要重新接线,那它永远只是实验室仪器,不可能有今天的规模。
3. 为什么程序必须先加载到内存
C 语言写的程序,编译链接后生成一个二进制可执行文件(比如 code.exe / a.out)。这个文件躺在磁盘上。
注:编译好的程序是存在磁盘上的,这时候它还只是一个"文件"。
要运行它,必须先加载到内存。为什么?
3.1 原因一:指令也是数据
这是最根本的回答:
因为指令程序都是数据,都要先加载到内存,才可以与 CPU 交互。
CPU 执行一条指令的流程是:取指 → 译码 → 执行。而"取指"取的是内存地址上的内容。CPU 根本不知道"磁盘"是什么东西,它的世界里只有寄存器和内存地址。
冯·诺依曼结构的"存储程序"思想在这里完成了闭环:正是因为指令被当成了数据,它才必须像数据一样被放进存储器。
3.2 原因二:CPU 只认地址,不认文件
CPU 访问数据靠的是地址总线,它只能寻址内存(以及寄存器和少量 IO 端口)。
- 磁盘上的
a.out是一个文件,由文件系统管理,有 inode、有目录项,但它没有"可以被 CPU 直接寻址的地址" - 内存是一大片连续的、按字节编址的空间,每个字节都有地址
所以"运行一个程序"= 把这一段字节,从"文件里的偏移量"变成"内存里的地址"。
3.3 原因三:要被操作系统"描述"成进程
前面两个是硬件层面的原因,第三个是软件层面的:
磁盘上的是程序(死的),内存里跑的才是进程(活的)。而进程 = 内核的数据结构 + 这个程序自己的代码和数据。
也就是说,"加载"这一步,除了搬字节,还要让内核建一个结构体来登记它。这部分在第 7 节接到进程时再展开。
3.4 一个常见误解:加载 ≠ 整个文件搬进内存
不是。现代操作系统是按页(4 KB)惰性加载的:
- 可执行文件被划分成若干段(
.text代码段、.data数据段、.bss未初始化段……) - 真正被映射进虚拟地址空间的是"内存映射",物理内存页是用到了才分配
- 访问到还没加载的页,触发缺页中断(page fault),内核再去磁盘取
所以一个 500 MB 的程序,刚启动时占用的物理内存可能只有几 MB。
这也是为什么"程序必须先加载到内存"这句话,准确版本是:
程序必须被加载到内存(哪怕只是按需的一小部分),CPU 才能执行它。
4. 操作系统是什么
4.1 概念:广义与狭义

操作系统的概念:任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。
笼统地理解,操作系统包括两部分:
- 内核:进程管理、内存管理、文件管理、驱动管理
- 其他程序:函数库、shell 程序等等
所以:
- 广义的 OS = 内核 + 一堆系统软件(shell、libc、各种工具),比如我们说"Ubuntu"、“Windows 11”
- 狭义的 OS = 内核
后面我们所说的"操作系统",都指狭义上的操作系统,也就是操作系统的内核。
到这里就能解释一个日常困惑了:为什么"装系统"和"换桌面"是两回事?
- Ubuntu 的图形界面、各种发行版的外壳,都是跑在内核之上的程序
- Android 是基于 Linux 的,所以 Android 本质上是 Linux 的一个"用于显示的外壳",可以挂上各种接口,可裁剪
- 早期的鸿蒙,也是类似的思路:替换掉上层那一层外壳
内核的四大管理职能
| 职能 | 管什么 | 典型数据结构(Linux) |
|---|---|---|
| 进程管理 | 谁在跑、谁该跑、跑在哪个 CPU 上 | task_struct |
| 内存管理 | 谁用哪块内存、怎么映射、怎么换出 | mm_struct、页表 |
| 文件管理 | 文件、目录、inode、打开的 fd | struct file、dentry、inode |
| 驱动管理 | 有哪些设备、怎么操作它们 | struct device、file_operations |
补充一句:现代内核其实还有网络管理(网络协议栈),有的教材会说"五大管理"。本质不变——都是在管理一类资源。
4.2 为什么要有操作系统
操作系统存在的目的,分两个方向看:
人
↑
┌─────────────┐
│ 操作系统 │ ← 对上:给人提供一个良好的使用环境
└─────────────┘ (稳定的、高效的、安全的)
↓
软硬件资源(CPU、内存、磁盘、网卡……)
← 对下:做软硬件资源的管理者
一句话:
操作系统是硬件资源的管理者。
操作系统的目的是给人提供一个良好的使用环境。
注意"良好的使用环境"这个说法很朴素但很准:如果没有操作系统,你想读一个文件,得自己算扇区、写磁头寄存器、控制寻道——那不是给人用的。
4.3 本质:先描述,再组织(本文最重要的一段)
这是整章最有价值的方法论,也是理解内核一切设计的钥匙。
操作系统是一款进行软硬件资源管理的软件。 而它管理资源的方式,永远是:先描述,再组织。
第一步:描述
为每一个被管理对象,建立一个数据结构。
- 设备 →
struct device - 进程 →
task_struct - 文件 →
struct file - 内存 →
mm_struct
第二步:组织
用链表等结构,把这些结构体串起来。
struct device
{
类型;
状态;
厂商;
是否可以被访问;
...
struct device *next; // ← 组织
};
所有设备结构体靠 next 连成一条设备链表。
第三步:管理动作
所有管理动作,最终都转化为对链表的增删查改。
- 新插入一块网卡 → 创建
struct device,挂到链表上 - 拔掉一块硬盘 → 从链表摘除对应节点
- 查询设备状态 → 读结构体里的字段
原话:OS 内部,将来核心的东西是什么?——数据结构!
4.4 管理的本质:对数据做管理
用"校长—辅导员—学生"举例,这个类比能把整件事说透:
校长 → 管理者 → 做决策
↑
辅导员 → 执行层 → 获取、维护数据
↑
学生 → 被管理者 → 提供数据
关键结论:
- 管理者不需要和被管理者见面。 校长从没见过每一个学生,但他能管理全校。
- 管理者通过获取被管理者的数据来做决策。 姓名、院系、专业、班级、学号、成绩、在校表现、特长……这些都是数据。
- 数据是提供决策依据的。
于是:
管理我们,本质是对我们身上的相关数据做管理。
对应到操作系统:内核不需要"看见"网卡、硬盘,它只需要维护这些设备的结构体,然后对这些结构体做操作。
从"校长"到"程序员校长"
现实问题 → 数据化:
现实需求 计算机世界
─────────────────────────────────────────────
我想让成绩最好的去参赛 → 遍历链表,找 max
我想开除一名学生 → 删除链表节点
我想让一名学生留级 → 修改节点里的字段
我想让学生参加篮球队 → 把节点挂到另一条链表
这就是代码化:
struct student
{
char name[20];
char sex[4];
char 院系[20];
int 成绩[20];
int age;
double ...;
struct student *next; // 先描述
};
head → [□] → [□] → [□] → [□] → [□] → NULL
先描述,再组织。
这八个字,是把现实世界的问题进行计算机建模的核心规律。
顺便说一句:C++ 里为什么要学 STL?因为它干的也是同一件事——先描述(容器 + 迭代器),再组织(红黑树、哈希表)。
4.5 内核自己也在内存里
有一个特别容易被忽略的点:
在我们还没有启动进程之前,第一款软件是什么?
操作系统也在内存里。
也就是说:
- 内核本身也是被加载到内存中的程序
- 只不过它拥有最高的管理权限(运行在内核态)
- 电脑一断电,内核也就没了,下次上电重新从磁盘引导加载
这一点其实是第 3 节"程序必须加载到内存"的自指:连管理别人的内核,自己也得先躺在内存里。
4.6 数据流动的本质是拷贝
回到第 1 节那句伏笔:
计算机数据流动的过程本质就是拷贝,所以计算机的效率就是由设备的拷贝效率决定的。
你会发现内核设计里到处都在跟"拷贝"较劲:
| 场景 | 拷贝次数 | 优化手段 |
|---|---|---|
普通 read 文件 | 磁盘 → 内核页缓存 → 用户缓冲区(2 次) | 缓冲区、预读 |
write 发网络 | 用户 → 内核 socket 缓冲区 → 网卡(2 次) | 零拷贝(sendfile/mmap) |
| CPU 读数据 | 内存 → cache → 寄存器 | 局部性友好、cache line 对齐 |
看懂这一点,你就理解了为什么"IO 密集"的程序总是慢在拷贝上。
5. 内核、驱动与系统调用
注:下面的图是从笔记里的分层图整理出来的。
┌─────────────────────────────────────────┐
│ 用户:指令操作 / 开发操作 / 管理操作 │
├─────────────────────────────────────────┤
│ 用户操作接口:shell 外壳 / lib 库 / 部分指令│ ← 用户层
├─────────────────────────────────────────┤
│ system call 系统调用接口 │ ← 安全边界
├─────────────────────────────────────────┤
│ 操作系统(内核) │
│ 内存管理 / 进程管理 / 文件管理 / 驱动管理 │ ← 系统软件层
├─────────────────────────────────────────┤
│ 驱动程序(网卡驱动 / 磁盘驱动 / …) │
├─────────────────────────────────────────┤
│ 基础硬件(网卡 / 键盘 / 硬盘 / 相机 …) │ ← 硬件层
└─────────────────────────────────────────┘
这三层各自干什么,一一说清。
5.1 内核:核心管理者
内核管什么
内存管理、进程管理、文件管理、驱动管理——四大职能,前面已经讲过了。
内核怎么管
先描述,再组织。 这条已经在 4.3 讲透了,这里只强调一句:
内核管理硬件,不是直接去操作寄存器,而是先在软件世界里为每个设备建立一个"代表",再把这些代表组织成链表。
5.2 驱动:内核操作硬件的手
驱动在哪
驱动位于内核与硬件之间。它属于系统软件,但它是内核用来操作硬件的具体执行层。
有个说法很形象:每个设备配一个驱动。
驱动为什么要存在
因为内核不可能为每一种硬件都写一套专门逻辑——硬件之间的差异太大了。驱动的职责是:
- 封装差异:把不同厂商、不同型号的硬件,包装成内核能统一管理的结构
- 隐藏细节:把"操作硬件"的具体过程(读哪个寄存器、等哪个中断)藏起来
- 统一接口:对上提供一套标准接口(Linux 里叫
file_operations),对下完成真正的硬件访问
struct file_operations {
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
...
};
不管你插的是哪家的网卡,内核看到的都是同一套 read/write。差异被驱动吃掉了。
驱动管理 = 对设备链表做增删查改
新插入一块网卡 → 创建 struct device,挂到设备链表
拔掉一块硬盘 → 从链表摘除对应节点
查询设备状态 → 读取结构体里的字段
和"校长管学生"是同一套思路,一点没变。
5.3 系统调用:用户进入内核的窗口
它到底在哪
用户程序
↓
用户操作接口(shell / lib / 部分指令)
↓
系统调用接口 ← 这里是边界
↓
内核
它不是内核本身,也不是用户程序,而是——用户请求内核服务的入口。
为什么需要系统调用
两个原因:
- 安全:用户程序不能直接访问硬件、不能直接改内核数据,否则系统会被搞崩
- 统一:所有对硬件的访问都必须经过操作系统,由内核统一管理和保护
笔记里这句话写得很形象,我一直记得:
不相信任何人,但是他必须还得给正常人提供服务。
保证自身安全的情况下,给别人提供服务。
这就是内核的设计原则:
我不信任你,但我仍然要为你服务,所以你必须从窗口递申请。
银行类比:系统调用就是那个窗口
| 银行里的角色 | 操作系统里的对应物 |
|---|---|
| 银行 | 内核 |
| 员工 / 大堂经理 | 内核提供的服务 |
| 金库、电脑 | 底层硬件 |
| 窗口 | 系统调用接口 |
| 储户 | 用户程序 |
| 递密码、说需求 | 调用系统调用并传参 |
| 员工告诉你"存钱成功" | 系统调用的返回值 |
在窗口处,用户与员工沟通需求……也就是调用系统调用,并传参;
员工告诉你存钱成功,也就是给返回值。
所以系统调用的两个关键要素就是:
- 参数:用户把需求和数据交给内核
- 返回值:内核把执行结果告诉用户
系统调用是用户态和内核态的边界
笔记里写的是"不安全的"和"保证自身安全",换成操作系统的术语就是:
| 概念 | 含义 | x86 特权级 | ARM |
|---|---|---|---|
| 用户态 | 用户程序运行的地方,权限受限,不能碰硬件 | ring 3 | EL0 |
| 内核态 | 内核运行的地方,拥有全部权限 | ring 0 | EL1 |
两者之间怎么切换?系统调用是用户态主动进入内核态的唯一合法入口。
底层大致是这样发生的(以 x86-64 为例):
1. 用户程序把系统调用号放进 rax (write 的号是 1)
2. 把参数放进 rdi / rsi / rdx / r10 / r8 / r9
3. 执行 syscall 指令
4. CPU 切到 ring 0,查系统调用表 sys_call_table
5. 跳转到对应的内核函数执行
6. 结果放回 rax,执行 sysret 切回 ring 3
注意:这个切换是有代价的(保存/恢复上下文、刷 cache/TLB)。这就是为什么"系统调用太多"会拖慢程序,也是缓冲、批量、
mmap这些优化的动机。
5.4 为什么我们平时"没见过"系统调用
笔记里连续问了两次:
系统调用,之前我们怎么没见过??
答案只有一句话:
很多 C/C++ 函数,都是对系统调用的封装!!!
C、C++ 语言提供的库!!!
printf、fopen、malloc 这类库函数,很多最终都会走到系统调用。库函数是"包装好的窗口服务",系统调用才是真正进入内核的那一步。
printf() → C 标准库 → write() 系统调用 → 内核 → 驱动 → 显示器
用 strace 亲眼看看
这个是验证"看不见"的最好办法:
// hello.c
#include <stdio.h>
int main(void) {
printf("hello\n");
return 0;
}
gcc hello.c -o hello
strace ./hello
你会看到类似这样的一行:
write(1, "hello\n", 6) = 6
含义是:往 fd 1(标准输出) 写了 6 个字节,返回 6。
这就把一整条链路坐实了:printf → 库 → write 系统调用 → 内核 → 驱动 → 屏幕。
一个坑:库函数和系统调用不是一一对应
printf 内部是有缓冲区的:
- 输出到终端(行缓冲):遇到
\n就刷一次,可能一条printf对应一次write - 输出到管道/文件(全缓冲):攒够 4 KB 才刷,可能很多条
printf只对应一次write - 程序
exit时才把剩余缓冲刷出去
所以如果你在 printf 后面写 _exit(0) 而不是 return 0,就会丢输出——因为 _exit 是系统调用,直接退出,不刷库的缓冲区。这就是"库函数 ≠ 系统调用"最直观的后果。
补充:不是所有库函数都要进内核
比如 strlen、memcpy、abs,完全在用户态算完就完事,跟内核没关系。只有需要访问"资源"(文件、内存、设备、进程)的时候,才必须过窗口。
6. 一条完整的调用链:printf 的一生
把前五节串起来,看一个字符是怎么从代码走到屏幕的:
① 用户程序 printf("hello\n");
↓ 调用库函数
② 用户操作接口 glibc 的 printf,写进 FILE 缓冲区
↓ 缓冲区满 / 遇 \n / 程序退出
③ 系统调用接口 write(1, "hello\n", 6)
↓ rax=1,执行 syscall 指令,切到 ring 0
④ 内核 sys_write() → 找到 fd 1 对应的 struct file
↓ 文件管理模块认出这是终端设备
⑤ 驱动程序 终端驱动 / 显卡驱动,把字节翻译成设备能懂的操作
↓ 写显存 / 通过总线发出去
⑥ 底层硬件 显示器点亮像素
对应到分层图上,就是从上往下走了一遍。反过来,一次键盘输入就是从下往上走一遍。
核心结论:涉及访问硬件的操作,都要经过操作系统,都要调系统调用。 这就是用户态与内核态之间的安全边界。
7. 从"前传"到"进程":这三节到底铺垫了什么
既然这是"进程前传",最后必须说清楚它跟进程的接口在哪。
7.1 程序 ≠ 进程
| 程序 | 进程 | |
|---|---|---|
| 存在形式 | 磁盘上的文件(code.exe / a.out) | 内存中的运行实例 |
| 状态 | 静态的、死的 | 动态的、活的 |
| 数量 | 一份 | 可以同时跑多份 |
| 由谁管理 | 文件系统 | 内核(进程管理) |
进程 = 内核数据结构对象 + 自己的代码和数据
7.2 只把程序加载到内存,还不是进程
这是笔记里特别强调的一点(我当时也写错了):
但把程序加载到内存,还不是进程。
为什么呢?因为进程还差一个东西:内核里那个描述它的结构体。
磁盘 内存 CPU
┌──────┐ ┌──────────────┐ ┌─────┐
│code.c│ │ ┌──┐ │ │ │
│ ↓ │ ───→ │ │OS│ 代码+数据 │ ←──→ │ CPU │
│code.exe│ │ └──┘ │ │ │
└──────┘ └──────────────┘ └─────┘
文件 进程 执行
加上这个结构体,才算完整:
程序加载到内存 + 进程描述的结构体,加起来才是进程。
而这个结构体,就是大名鼎鼎的 PCB(Process Control Block,进程控制块)。它是所有操作系统学科里描述"进程控制块"的统一称呼。
7.3 Linux 里的 PCB:task_struct
// Linux 内核里,PCB 就叫 task_struct
struct task_struct {
// 进程的所有属性,直接或间接都通过这个结构体找到
...
};
- 定义位置:
include/linux/sched.h - 规模:几千行,包含进程状态、调度信息、内存描述符、打开的文件、信号、父子关系……
于是第 4 节那八个字又一次出现:
描述:为每个进程建一个 task_struct
组织:用链表把 task_struct 串起来
管理:调度、创建、销毁 = 对链表做增删查改
调度器拿到的就是一堆 task_struct 指针,它要做的事情就是"挑一个出来跑"。
7.4 一个特别贴切的类比
笔记里这句话我特别喜欢:
进程要调度,就是用的
task_struct来调度;本质上,来切换、维护进程的。
简化的本质:人对应的结构体,所以人找工作,简历在找工作。
你投简历 → 简历进人才库 → HR 在库里筛 → 挑中你去面试 → 不合适就退回库里。
进程创建 → 建 task_struct → 挂进调度队列 → 调度器挑中 → 上 CPU → 时间片用完放回队列。
简历就是 PCB。 这句话一旦记住,进程管理就不用背了。
7.5 顺带解释:为什么操作系统里一定同时有很多进程
操作系统内,可能同时运行很多程序吗?
↓
每一个都要加载到内存
↓
一定会同时存在很多进程
↓
这些进程,要不要被操作系统管理起来?
↓
要管理 → 先描述,再组织
所以第 4 节讲"先描述再组织",不是泛泛而谈,而是为了给进程管理做铺垫。
8. 易错点清单
| # | 易错点 | 正确理解 |
|---|---|---|
| 1 | 冯·诺依曼结构里的"存储器"指硬盘 | 指内存。硬盘属于二级存储/外设 |
| 2 | "输入输出"是站在人的角度 | 站在内存的角度 |
| 3 | 系统慢就是因为最慢的部件慢 | 分级存储 + 局部性,让慢部件极少被真正等到 |
| 4 | 加载 = 把整个可执行文件搬进内存 | 现代 OS 按页惰性加载,用到才分配 |
| 5 | 程序加载到内存就是进程了 | 还差内核的描述结构体(PCB) |
| 6 | 系统调用是内核的一部分 | 系统调用是接口/边界,不是内核本身 |
| 7 | 库函数就是系统调用 | 库函数封装系统调用,还可能带缓冲区,不是一一对应 |
| 8 | 操作系统 = 内核 | 广义 OS = 内核 + 其他程序;狭义 OS = 内核 |
| 9 | 内核是"看不见"的 | 内核也在内存里,也是被加载进来的程序 |
| 10 | 驱动是硬件 | 驱动是软件,是内核操作硬件的执行层 |
9. 自测:能答上来就算过关
- 冯·诺依曼结构的五大部件是什么?其中"运算器 + 控制器"合起来叫什么?
- 为什么说"指令也是数据"?这个结论带来了什么设计上的后果?
- 我们说的"输入/输出",参考系是什么?
- 存储分级为什么能做到"又快又大又便宜"?它依赖什么前提?
- 时间局部性和空间局部性各举一个例子。
- 为什么 CPU 不能直接读磁盘?那磁盘的数据怎么才能被用上?
- "先描述,再组织"具体到设备管理和进程管理,分别描述什么、组织什么?
- 内核、驱动、系统调用三者,谁做决策、谁碰硬件、谁是入口?
- 为什么平时写代码"看不到"系统调用?用一条命令验证它。
- “把程序加载到内存"和"创建了一个进程”,差别在哪?
参考答案基本都在前文的加粗句里,答不上来的直接回去看对应小节。
10. 一句话总结
冯·诺依曼 → 指令是数据,所以必须进内存
存储分级 → 速度差太多,所以加中间层 + 靠局部性预读
操作系统 → 为了管理这些资源,方法是"先描述,再组织"
内核/驱动/系统调用
→ 内核决策,驱动碰硬件,系统调用是用户递申请的窗口
进程 → 程序 + PCB,依然是"先描述,再组织"
如果只能记一句话:
内核决定"做什么",驱动负责"怎么碰硬件",系统调用负责"用户怎么把请求递进去"。
下一篇正式进入进程:task_struct 里到底有什么、进程有哪几个状态、fork() 为什么返回两次。这篇前传里的每一个概念,都会在那边兑现。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)