A-程序员的自我修养之第1章 温故而知新:计算机、操作系统、内存与线程
第1章 温故而知新:计算机、操作系统、内存与线程
这章不是在讲链接器本身,而是在帮你补底层背景。后面讲“链接、装载、运行库”的时候,会反复用到这里的概念:CPU、内存、I/O、操作系统、进程、虚拟地址空间、线程。
1 本章一句话
程序不是孤零零地运行的。它要靠 CPU 执行指令,靠内存存放代码和数据,靠 I/O 设备和外部世界交流,还要靠操作系统把这些有限硬件资源管理起来,并伪装成更好用、更安全的抽象。
2 先建立全局图
你可以先把计算机看成三类资源:
| 资源 | 它负责什么 | 程序员为什么要关心 |
|---|---|---|
| CPU | 执行指令 | 你的代码最终都要变成 CPU 能执行的机器指令。 |
| 内存 | 存放代码和数据 | 变量、函数、栈、堆、动态库都要落到内存空间里。 |
| I/O | 与外部世界交互 | 读文件、写屏幕、收网络包,都不是程序自己凭空完成的。 |
操作系统夹在程序和硬件中间,主要做两件事:
- 提供抽象:让程序不用直接面对复杂硬件。
- 管理资源:让多个程序安全、公平、有效率地共享 CPU、内存和 I/O。
2.1 图解 1:程序运行时谁在配合谁
这张图要记住的不是节点名字,而是方向:应用程序通过库和系统接口提出请求,操作系统再去调度硬件资源完成事情。
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,不需要自己控制显卡或终端。 - 你调用
fread,不需要自己读磁盘扇区。 - 你申请
malloc,不需要自己直接管理物理内存。
但是,接口不是魔法。printf、fread、malloc 背后最终还是要走到操作系统和硬件。
这本书后面讲“链接、装载、库”,其实就是在拆开这些中间层,让你知道平时调用的东西是怎样接起来的。
5.1 图解 2:一次 printf 大致经过哪些层
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 虚拟地址空间是什么?
虚拟地址空间可以理解成:
操作系统给每个进程画了一张“假地图”,让它以为自己拥有一整片连续、独立的内存。
程序看到的是虚拟地址,不是真实物理地址。
7.3 图解 3:每个进程都有自己的“假地图”
重点是:两个进程看到的地址空间可以长得很像,但背后会通过页表映射到不同物理页;只读的共享库页面还可以被多个进程共同使用。
这样做有几个好处:
- 每个进程相互隔离,更安全。
- 程序不用关心自己实际被放在物理内存哪里。
- 物理内存不够时,可以把暂时不用的内容换到磁盘。
- 多个进程可以共享同一份只读代码或动态库页面。
7.4 分段和分页
历史上有两类常见思路:分段和分页。
| 机制 | 白话理解 | 问题或特点 |
|---|---|---|
| 分段 | 按逻辑区域切,例如代码段、数据段、栈段。 | 更符合程序员理解,但容易产生碎片。 |
| 分页 | 把内存切成固定大小的小块。 | 管理更规整,是现代虚拟内存的重要基础。 |
分页的核心思想:
虚拟页 -> 通过页表 -> 物理页
程序以为自己在访问连续内存,但物理上这些页面可以分散在不同地方。
你暂时只要记住一句:
虚拟内存让程序看到的是“虚拟地址”,硬件和操作系统负责把它翻译成“物理地址”。
8 众人拾柴火焰高:线程
进程解决的是“程序之间如何共享计算机资源”。线程解决的是“一个进程内部如何同时做多件事”。
8.1 线程是什么?
线程可以理解成:
进程里面的一条执行路线。
一个进程可以有多个线程。它们共享同一个进程的大部分资源,但每个线程有自己的执行现场。
| 内容 | 进程内线程是否共享 |
|---|---|
| 代码段 | 共享 |
| 全局变量 / 静态变量 | 共享 |
| 堆 | 共享 |
| 打开的文件 | 通常共享 |
| 栈 | 每个线程独有 |
| 寄存器状态 | 每个线程独有 |
| 当前执行位置 | 每个线程独有 |
8.2 图解 4:进程像容器,线程像执行路线
这张图对应上面的表:线程共享进程的大部分资源,但每条线程都有自己的栈、寄存器状态和当前执行位置。
可以这样想:
进程 = 一个工作间
线程 = 工作间里的工人
工人共享工具和材料,但每个人手上正在做的步骤不同。
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 自测问题
- 为什么
Hello World背后并不简单? - CPU、内存、I/O 分别负责什么?
- 操作系统主要做哪两类事情?
- 为什么多个程序可以“看起来同时运行”?
- 直接使用物理内存会带来哪些问题?
- 虚拟地址和物理地址有什么区别?
- 进程和线程有什么区别?
- 为什么多线程会有线程安全问题?
12 答案提示
- 因为它涉及编译、链接、装载、运行库、系统调用和硬件输出。
- CPU 执行指令,内存存放代码和数据,I/O 负责和外部设备交互。
- 提供抽象接口,管理硬件资源。
- 操作系统把 CPU 时间切成小片,在多个进程或线程之间快速切换。
- 没有隔离、效率低、地址管理困难。
- 虚拟地址是进程看到的地址,物理地址是真实内存位置,中间由硬件和操作系统转换。
- 进程是资源分配和隔离单位,线程是进程内部的执行流。
- 因为多个线程共享同一份内存,可能同时读写同一个数据。
13 本章最该记住的 8 个词
- CPU
- I/O
- 操作系统
- 进程
- 线程
- 虚拟地址空间
- 分页
- MMU
14 我的学习建议
这一章不要陷入硬件细节。你只需要抓住四句话:
- 程序最终是 CPU 在执行指令。
- 操作系统让多个程序安全共享硬件。
- 虚拟内存让每个进程拥有独立地址空间的错觉。
- 线程让一个进程内部可以有多条执行路线。
这四句话吃透了,第 1 章就过关了。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)