进程前传:冯·诺依曼、存储分级与操作系统

写在前面:这是我学 Linux 进程管理之前的一篇"前传"。不先把冯·诺依曼、存储分级、操作系统这三件事捋清楚,后面看 task_struct、fork()、进程状态机的时候,会觉得全是凭空出现的名词。这篇博客解决的就是"为什么会有这些东西"。


零、先给一条主线

整篇博客其实只在回答一个问题:一个写在磁盘上的 code.exe,是怎么一步步跑到 CPU 上执行的?

沿着这条线,五个问题依次展开:

#问题关键词
1计算机由什么组成冯·诺依曼、五大部件
2CPU 那么快,磁盘那么慢,怎么配合存储分级、局部性原理
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 很快,外设很慢,速度严重不同步,怎么解?

解决思路不是"让外设变快"(做不到),而是加一层中间人:

  1. 把所有需要跟外设打交道的东西,都做成"内存"
  2. 让 CPU 只跟内存打交道,不直接碰外设
  3. 用存储器预先加载数据(有局部性原理才值得提前加载)
  4. 于是外设和 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)。

笼统地理解,操作系统包括两部分:

  1. 内核:进程管理、内存管理、文件管理、驱动管理
  2. 其他程序:函数库、shell 程序等等

所以:

  • 广义的 OS = 内核 + 一堆系统软件(shell、libc、各种工具),比如我们说"Ubuntu"、“Windows 11”
  • 狭义的 OS = 内核

后面我们所说的"操作系统",都指狭义上的操作系统,也就是操作系统的内核。

到这里就能解释一个日常困惑了:为什么"装系统"和"换桌面"是两回事?

  • Ubuntu 的图形界面、各种发行版的外壳,都是跑在内核之上的程序
  • Android 是基于 Linux 的,所以 Android 本质上是 Linux 的一个"用于显示的外壳",可以挂上各种接口,可裁剪
  • 早期的鸿蒙,也是类似的思路:替换掉上层那一层外壳

内核的四大管理职能

职能管什么典型数据结构(Linux)
进程管理谁在跑、谁该跑、跑在哪个 CPU 上task_struct
内存管理谁用哪块内存、怎么映射、怎么换出mm_struct、页表
文件管理文件、目录、inode、打开的 fdstruct 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 驱动:内核操作硬件的手

驱动在哪

驱动位于内核与硬件之间。它属于系统软件,但它是内核用来操作硬件的具体执行层。

有个说法很形象:每个设备配一个驱动。

驱动为什么要存在

因为内核不可能为每一种硬件都写一套专门逻辑——硬件之间的差异太大了。驱动的职责是:

  1. 封装差异:把不同厂商、不同型号的硬件,包装成内核能统一管理的结构
  2. 隐藏细节:把"操作硬件"的具体过程(读哪个寄存器、等哪个中断)藏起来
  3. 统一接口:对上提供一套标准接口(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 / 部分指令)
   ↓
系统调用接口   ← 这里是边界
   ↓
内核

它不是内核本身,也不是用户程序,而是——用户请求内核服务的入口。

为什么需要系统调用

两个原因:

  1. 安全:用户程序不能直接访问硬件、不能直接改内核数据,否则系统会被搞崩
  2. 统一:所有对硬件的访问都必须经过操作系统,由内核统一管理和保护

笔记里这句话写得很形象,我一直记得:

不相信任何人,但是他必须还得给正常人提供服务。
保证自身安全的情况下,给别人提供服务。

这就是内核的设计原则:

我不信任你,但我仍然要为你服务,所以你必须从窗口递申请。

银行类比:系统调用就是那个窗口

银行里的角色操作系统里的对应物
银行内核
员工 / 大堂经理内核提供的服务
金库、电脑底层硬件
窗口系统调用接口
储户用户程序
递密码、说需求调用系统调用并传参
员工告诉你"存钱成功"系统调用的返回值

在窗口处,用户与员工沟通需求……也就是调用系统调用,并传参;
员工告诉你存钱成功,也就是给返回值。

所以系统调用的两个关键要素就是:

  • 参数:用户把需求和数据交给内核
  • 返回值:内核把执行结果告诉用户

系统调用是用户态和内核态的边界

笔记里写的是"不安全的"和"保证自身安全",换成操作系统的术语就是:

概念含义x86 特权级ARM
用户态用户程序运行的地方,权限受限,不能碰硬件ring 3EL0
内核态内核运行的地方,拥有全部权限ring 0EL1

两者之间怎么切换?系统调用是用户态主动进入内核态的唯一合法入口。

底层大致是这样发生的(以 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. 自测:能答上来就算过关

  1. 冯·诺依曼结构的五大部件是什么?其中"运算器 + 控制器"合起来叫什么?
  2. 为什么说"指令也是数据"?这个结论带来了什么设计上的后果?
  3. 我们说的"输入/输出",参考系是什么?
  4. 存储分级为什么能做到"又快又大又便宜"?它依赖什么前提?
  5. 时间局部性和空间局部性各举一个例子。
  6. 为什么 CPU 不能直接读磁盘?那磁盘的数据怎么才能被用上?
  7. "先描述,再组织"具体到设备管理和进程管理,分别描述什么、组织什么?
  8. 内核、驱动、系统调用三者,谁做决策、谁碰硬件、谁是入口?
  9. 为什么平时写代码"看不到"系统调用?用一条命令验证它。
  10. “把程序加载到内存"和"创建了一个进程”,差别在哪?

参考答案基本都在前文的加粗句里,答不上来的直接回去看对应小节。


10. 一句话总结

冯·诺依曼  →  指令是数据,所以必须进内存
存储分级    →  速度差太多,所以加中间层 + 靠局部性预读
操作系统    →  为了管理这些资源,方法是"先描述,再组织"
内核/驱动/系统调用
            →  内核决策,驱动碰硬件,系统调用是用户递申请的窗口
进程        →  程序 + PCB,依然是"先描述,再组织"

如果只能记一句话:

内核决定"做什么",驱动负责"怎么碰硬件",系统调用负责"用户怎么把请求递进去"。

下一篇正式进入进程:task_struct 里到底有什么、进程有哪几个状态、fork() 为什么返回两次。这篇前传里的每一个概念,都会在那边兑现。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐