第1章 温故而知新:计算机、操作系统、内存与线程

这章不是在讲链接器本身,而是在帮你补底层背景。后面讲“链接、装载、运行库”的时候,会反复用到这里的概念:CPU、内存、I/O、操作系统、进程、虚拟地址空间、线程。

1 本章一句话

程序不是孤零零地运行的。它要靠 CPU 执行指令,靠内存存放代码和数据,靠 I/O 设备和外部世界交流,还要靠操作系统把这些有限硬件资源管理起来,并伪装成更好用、更安全的抽象。

2 先建立全局图

应用程序
例如 Hello World

运行库 / 系统 API

操作系统

CPU

内存

I/O 设备
磁盘、键盘、网卡、显示器

进程 / 线程调度

虚拟内存管理

你可以先把计算机看成三类资源:

资源 它负责什么 程序员为什么要关心
CPU 执行指令 你的代码最终都要变成 CPU 能执行的机器指令。
内存 存放代码和数据 变量、函数、栈、堆、动态库都要落到内存空间里。
I/O 与外部世界交互 读文件、写屏幕、收网络包,都不是程序自己凭空完成的。

操作系统夹在程序和硬件中间,主要做两件事:

  1. 提供抽象:让程序不用直接面对复杂硬件。
  2. 管理资源:让多个程序安全、公平、有效率地共享 CPU、内存和 I/O。

2.1 图解 1:程序运行时谁在配合谁

硬件视角

操作系统视角

应用程序视角

业务代码

printf / malloc / fread

进程管理

内存管理

文件与设备管理

调度器

CPU

内存

磁盘 / 网卡 / 显示器

这张图要记住的不是节点名字,而是方向:应用程序通过库和系统接口提出请求,操作系统再去调度硬件资源完成事情。

3 从 Hello World 说起

Hello World 看起来很简单,但它背后藏着一串问题:

#include <stdio.h>

int main(void) {
    printf("Hello World\n");
    return 0;
}

一个小白容易以为:写完代码,点运行,然后屏幕输出文字。

但这本书关心的是中间发生了什么:

  • 为什么 C 代码必须先编译?
  • 编译后得到的文件里面只有机器指令吗?
  • #include <stdio.h> 到底带来了什么?
  • printf 是谁实现的?
  • 程序为什么能被操作系统启动?
  • main 之前发生了什么?
  • main 返回之后又是谁收尾?

所以,第 1 章的作用是告诉你:一个简单程序背后,其实牵动了编译器、链接器、操作系统、运行库、硬件资源管理等一整套系统。

4 万变不离其宗

不同计算机长得不一样:PC、手机、服务器、嵌入式设备,CPU 架构也可能不同,比如 x86、ARM、MIPS。

但从程序运行角度看,它们大体都离不开几个核心部件:

CPU 执行指令
内存存放代码和数据
I/O 设备负责输入输出
总线或控制芯片负责连接它们

你现在不需要记每种硬件的细节,只要先抓住这个本质:

程序运行,就是 CPU 不断从内存取指令、执行指令,并在需要时读写内存或请求 I/O。

后面书里讲 ELF、PE、装载、动态库,其实都没有离开这个基本模型。

5 站得高,望得远

计算机系统是分层的。

应用程序

运行库 / 标准库
printf、malloc、fread 等

系统调用 / 操作系统 API

操作系统内核

驱动程序

硬件

分层的好处是:上层不用知道下层全部细节,只要遵守接口。

举个例子:

  • 你调用 printf,不需要自己控制显卡或终端。
  • 你调用 fread,不需要自己读磁盘扇区。
  • 你申请 malloc,不需要自己直接管理物理内存。

但是,接口不是魔法。printffreadmalloc 背后最终还是要走到操作系统和硬件。

这本书后面讲“链接、装载、库”,其实就是在拆开这些中间层,让你知道平时调用的东西是怎样接起来的。

5.1 图解 2:一次 printf 大致经过哪些层

终端/显示设备 设备驱动 操作系统内核 C 运行库 应用程序 终端/显示设备 设备驱动 操作系统内核 C 运行库 应用程序 printf("Hello") 格式化字符串和缓冲 系统调用 write 交给对应设备驱动 输出字节 屏幕上看到文字

printf 看起来是一个普通函数调用,但它最终会跨过用户态和内核态的边界,请操作系统把数据送到设备。

6 操作系统做什么

操作系统最核心的职责可以理解成:

把难用的硬件包装成好用的接口,同时把有限资源分给多个程序。

6.1 CPU 管理:不要让 CPU 闲着

早期如果一个程序在等磁盘,CPU 就可能空着。后来操作系统开始让多个程序轮流使用 CPU。

你可以这样理解:

程序 A 运行一会儿
程序 B 运行一会儿
程序 C 运行一会儿
再切回程序 A

切换速度很快,所以用户感觉多个程序好像在同时运行。

这引出几个概念:

概念 白话理解
多道程序 内存中放多个程序,谁能运行就让谁运行,提高 CPU 利用率。
分时系统 把 CPU 时间切成小片,多个程序轮流拿一小片。
抢占式调度 操作系统可以强制暂停某个程序,把 CPU 分给别的程序。
进程 一个正在运行的程序实例,也是操作系统分配资源的重要单位。

对小白来说,先记住:

进程让每个程序“感觉自己独占 CPU”,但实际上 CPU 是被操作系统快速切换共享的。

6.2 I/O 管理:把复杂设备包装成统一接口

磁盘、键盘、鼠标、网卡、显示器,每种设备都很复杂。如果应用程序直接控制硬件,开发会非常痛苦,也很危险。

所以操作系统通过驱动程序管理设备,再向上提供较统一的接口。

比如读文件时,你通常只关心:

打开文件 -> 读取内容 -> 关闭文件

你不需要知道磁盘控制器怎么工作,也不用关心硬盘、U 盘、网络文件系统底层差异。

这就是操作系统的抽象能力。

7 内存不够怎么办

内存管理是本章最重要的部分之一,因为后面的“装载”和“链接”都离不开地址。

7.1 如果程序直接使用物理内存,会有什么问题?

假设所有程序都直接访问真实物理地址,会出现三个大问题:

问题 后果
没有隔离 一个程序可能误写或恶意修改另一个程序的内存。
内存利用率低 程序必须整体装入内存,空间容易浪费。
地址不稳定 程序每次被放到哪里都可能不同,编写和装载都麻烦。

所以现代操作系统引入了虚拟地址空间。

7.2 虚拟地址空间是什么?

虚拟地址空间可以理解成:

操作系统给每个进程画了一张“假地图”,让它以为自己拥有一整片连续、独立的内存。

程序看到的是虚拟地址,不是真实物理地址。

进程看到的虚拟地址
例如 0x8048000

MMU / 页表转换

真实物理内存地址

7.3 图解 3:每个进程都有自己的“假地图”

真实物理内存

进程 B 的虚拟地址空间

进程 A 的虚拟地址空间

代码区

全局数据

共享库映射

代码区

全局数据

共享库映射

物理页 1

物理页 2

物理页 3

共享库物理页

物理页 5

重点是:两个进程看到的地址空间可以长得很像,但背后会通过页表映射到不同物理页;只读的共享库页面还可以被多个进程共同使用。

这样做有几个好处:

  • 每个进程相互隔离,更安全。
  • 程序不用关心自己实际被放在物理内存哪里。
  • 物理内存不够时,可以把暂时不用的内容换到磁盘。
  • 多个进程可以共享同一份只读代码或动态库页面。

7.4 分段和分页

历史上有两类常见思路:分段和分页。

机制 白话理解 问题或特点
分段 按逻辑区域切,例如代码段、数据段、栈段。 更符合程序员理解,但容易产生碎片。
分页 把内存切成固定大小的小块。 管理更规整,是现代虚拟内存的重要基础。

分页的核心思想:

虚拟页 -> 通过页表 -> 物理页

程序以为自己在访问连续内存,但物理上这些页面可以分散在不同地方。

你暂时只要记住一句:

虚拟内存让程序看到的是“虚拟地址”,硬件和操作系统负责把它翻译成“物理地址”。

8 众人拾柴火焰高:线程

进程解决的是“程序之间如何共享计算机资源”。线程解决的是“一个进程内部如何同时做多件事”。

8.1 线程是什么?

线程可以理解成:

进程里面的一条执行路线。

一个进程可以有多个线程。它们共享同一个进程的大部分资源,但每个线程有自己的执行现场。

内容 进程内线程是否共享
代码段 共享
全局变量 / 静态变量 共享
共享
打开的文件 通常共享
每个线程独有
寄存器状态 每个线程独有
当前执行位置 每个线程独有

8.2 图解 4:进程像容器,线程像执行路线

一个进程

线程 3

栈 3

寄存器 / PC

线程 1

栈 1

寄存器 / PC

代码段
共享

线程 2

栈 2

寄存器 / PC

全局变量 / 静态变量
共享


共享

打开的文件
通常共享

这张图对应上面的表:线程共享进程的大部分资源,但每条线程都有自己的栈、寄存器状态和当前执行位置。

可以这样想:

进程 = 一个工作间
线程 = 工作间里的工人

工人共享工具和材料,但每个人手上正在做的步骤不同。

8.3 为什么需要线程?

常见原因有四个:

场景 为什么线程有用
等待 I/O 一个线程等网络,另一个线程可以继续做事。
保持界面响应 一个线程处理界面,另一个线程做耗时计算。
逻辑上就是并发 例如下载多个文件、同时处理多个请求。
多核 CPU 多个线程可以真正并行跑在不同核心上。

8.4 线程安全是什么?

多个线程共享内存,优点是通信方便,缺点是容易互相踩到。

比如两个线程同时修改同一个变量:

count = count + 1;

这行代码看起来是一句,但底层可能分成:

读取 count
加 1
写回 count

如果两个线程交错执行,就可能出现结果错误。

这就是线程安全问题。

解决思路通常是:

  • 对共享数据加锁。
  • 减少共享状态。
  • 使用线程安全的数据结构。
  • 明确谁负责读,谁负责写。

8.5 用户线程和内核线程

线程也可以分层理解。

类型 谁管理 白话理解
用户线程 用户态线程库 切换快,但操作系统可能不知道每个用户线程。
内核线程 操作系统内核 操作系统直接调度,能利用多核,但切换成本更高。

常见映射模型:

模型 理解
多对一 多个用户线程映射到一个内核线程。
一对一 一个用户线程对应一个内核线程。
多对多 多个用户线程映射到多个内核线程。

小白阶段不用死记模型细节,只要知道:

线程能不能真正并行、阻塞时会不会拖住整个进程,很大程度取决于它和操作系统内核线程的映射关系。

9 本章和后面章节的关系

后续章节 为什么需要第 1 章背景
第2章 编译和链接 你要知道源代码最终会变成 CPU 执行的机器指令。
第6章 装载与进程 你要理解进程地址空间和虚拟内存。
第7章 动态链接 共享库依赖虚拟内存映射、地址空间、进程隔离。
第10章 内存 栈、堆、代码段、数据段都建立在进程内存布局上。
第11章 运行库 运行库要处理进程启动、线程、本地存储、I/O 等问题。
第12章 系统调用 程序请求操作系统服务,需要理解用户态和内核态的边界。

10 小白复述版

读完第 1 章,你应该能用自己的话讲出这段:

程序运行时,CPU 负责执行指令,内存负责存放代码和数据,I/O 设备负责输入输出。因为硬件资源有限,而且多个程序要同时运行,所以需要操作系统来管理资源和提供抽象。操作系统用进程来隔离程序,用调度让多个程序共享 CPU,用虚拟内存让每个进程以为自己有独立地址空间。一个进程里面还可以有多个线程,它们共享进程资源,但各自有独立的执行状态。理解这些以后,后面讲可执行文件如何装载到内存、动态库如何共享、运行库如何启动程序,就不会那么突兀了。

11 自测问题

  1. 为什么 Hello World 背后并不简单?
  2. CPU、内存、I/O 分别负责什么?
  3. 操作系统主要做哪两类事情?
  4. 为什么多个程序可以“看起来同时运行”?
  5. 直接使用物理内存会带来哪些问题?
  6. 虚拟地址和物理地址有什么区别?
  7. 进程和线程有什么区别?
  8. 为什么多线程会有线程安全问题?

12 答案提示

  1. 因为它涉及编译、链接、装载、运行库、系统调用和硬件输出。
  2. CPU 执行指令,内存存放代码和数据,I/O 负责和外部设备交互。
  3. 提供抽象接口,管理硬件资源。
  4. 操作系统把 CPU 时间切成小片,在多个进程或线程之间快速切换。
  5. 没有隔离、效率低、地址管理困难。
  6. 虚拟地址是进程看到的地址,物理地址是真实内存位置,中间由硬件和操作系统转换。
  7. 进程是资源分配和隔离单位,线程是进程内部的执行流。
  8. 因为多个线程共享同一份内存,可能同时读写同一个数据。

13 本章最该记住的 8 个词

  • CPU
  • I/O
  • 操作系统
  • 进程
  • 线程
  • 虚拟地址空间
  • 分页
  • MMU

14 我的学习建议

这一章不要陷入硬件细节。你只需要抓住四句话:

  1. 程序最终是 CPU 在执行指令。
  2. 操作系统让多个程序安全共享硬件。
  3. 虚拟内存让每个进程拥有独立地址空间的错觉。
  4. 线程让一个进程内部可以有多条执行路线。

这四句话吃透了,第 1 章就过关了。

Logo

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

更多推荐