理解操作系统:进程管理、内存管理与 Linux 开发基础
理解操作系统:进程管理、内存管理与 Linux 开发基础
1. 前言:为什么学 Linux 进程之前,要先补操作系统?
前两篇博客我分别整理了 Linux 环境搭建和 C 语言的编译调试流程。掌握了 vim、gcc、gdb 之后,我开始接触一个更核心的模块:Linux 进程。
但很快我就发现一个问题:老师课上讲的 fork、exec、PCB、进程状态,听起来全是抽象的概念。如果不理解操作系统到底是怎么管理硬件、怎么调度程序的,这些知识点就只是零散的“名词解释”,很难串起来。
于是我暂停了进程的系统调用学习,先把操作系统的核心理论补了一遍。这篇文章就是这次学习整理的笔记,聚焦两个核心问题:
- CPU 怎么分配时间?(进程管理)
- 内存怎么分配空间?(内存管理)
理解了这两件事,后面再学 fork、exec、进程间通信,就会有“原来如此”的感觉。
2. 硬件基础:操作系统管的是什么?
在聊操作系统之前,先看一眼它管理的硬件长什么样。
现代计算机都遵循冯·诺依曼体系结构,由五大部件组成:
| 部件 | 作用 | 常见形态 |
|---|---|---|
| 运算器 | 执行算术和逻辑运算 | CPU 内部 |
| 控制器 | 取指令、解析指令、控制执行流程 | CPU 内部 |
| 存储器 | 存放数据和程序 | 内存(RAM)、硬盘 |
| 输入设备 | 接收外部数据 | 键盘、鼠标、网卡 |
| 输出设备 | 输出计算结果 | 显示器、打印机 |
💡 核心公式:运算器 + 控制器 = CPU
常见的 i5、i7、i9、R9、M5 等 CPU 型号,本质上都遵循这个架构。
指令系统:CPU 的语言
CPU 只认识机器指令。不同 CPU 支持的指令集不同:
| 类型 | 全称 | 特点 | 典型代表 |
|---|---|---|---|
| RISC | 精简指令集 (Reduced Instruction Set Computer) | 指令简单,执行效率高 | ARM、MIPS |
| CISC | 复杂指令集 (Complex Instruction Set Computer) | 指令丰富,功能强大 | x86 (Intel/AMD) |
你写的每行 C 代码,最终都会被翻译成一条条 CPU 指令。程序 = 指令的集合。
总线:数据流动的高速公路
CPU、内存、外部设备之间通过总线来传递数据,分为三类:
- 地址总线:指定数据在内存中的位置(决定了寻址能力)。
- 数据总线:传输实际的数据(决定了传输速度)。
- 控制总线:传递控制信号(读/写/中断等)。
3. 操作系统是什么?
有了硬件,还需要一个“管家”来管理这些资源。这就是操作系统。
3.1 操作系统的定义
操作系统(OS)是管理计算机硬件和软件资源的系统软件。Windows 10/11、Linux(Ubuntu/CentOS)、macOS,本质上都是操作系统。
它的核心职责就三件事:
- 提供交互接口:让用户和程序能方便地使用硬件(命令行、图形界面)。
- 管理内存:决定哪个程序使用哪块内存,各进程互不侵犯。
- 管理进程:决定哪个程序什么时候用 CPU,分配时间片。
3.2 系统分类
| 类型 | 特点 | 典型场景 |
|---|---|---|
| 批处理系统 | 用户提交作业,系统成批处理(A→B→C,一个接一个) | 早期大型机 |
| 分时系统 | 多个用户轮流使用 CPU,切换速度极快,每个用户感觉像独占 | Linux 服务器 |
| 实时系统 | 对响应时间有严格硬性要求 | 车载控制、工业控制 |
💡 重点: Linux 属于分时系统——多个用户可以同时登录,操作系统快速轮流为每个人服务,让你感觉自己是独占整台机器的。
4. 进程管理:CPU 怎么分配时间?
4.1 进程是什么?
进程 = 一个正在运行的程序。
你写的 hello.c 编译后是一个可执行文件(一堆指令躺在硬盘里)。在终端输入 ./hello 之后,这个文件被加载到内存,CPU 开始逐条执行里面的指令——它就变成了一个进程。
每个进程有一个唯一的 PID(Process ID,进程 ID)来标识它。在 Linux 终端输入 ps -ef,看到的第一列就是每个进程的 PID。
4.2 PCB:进程的身份证
操作系统如何管理成千上万个进程?用一个叫 PCB(Process Control Block,进程控制块) 的结构体。
在 Linux 内核中,PCB 对应的就是 struct task_struct。这个结构体非常庞大,里面记录了:
- 进程的 PID
- 进程当前的状态(运行?等待?)
- 进程占用的内存信息
- CPU 寄存器的值(进程被切换时要保存现场)
- 打开的文件列表
- 父进程是谁
PCB 是进程存在的唯一标志。 操作系统通过链表把所有进程的 PCB 串在一起,遍历这个链表就能管理所有进程。
4.3 进程的三种状态
进程不是一直在运行的,它在三种状态之间反复切换:
| 状态 | 含义 | 什么时候进入 |
|---|---|---|
| 就绪 (Ready) | 准备好了,只等 CPU 安排 | 进程刚创建完,等待调度 |
| 运行 (Running) | 正在占用 CPU,执行指令 | 调度器选中了它 |
| 阻塞 (Blocked) | 在等待某件事,暂时无法继续 | 读文件、等键盘输入、等网络数据 |
状态切换流程:
就绪 --(被调度器选中)--> 运行
运行 --(时间片用完)--> 就绪
运行 --(等待 I/O)--> 阻塞
阻塞 --(I/O 完成)--> 就绪
🌰 举个例子:
你在终端运行./a.out,它先进入就绪队列。调度器选中它,进入运行状态。程序里有一行scanf,等待你输入——进程进入阻塞状态,让出 CPU。你按下回车,输入完成,进程回到就绪状态,等待下次被调度。
4.4 并发 vs 并行
这对概念经常被混淆,但区别很大:
| 概念 | 含义 | 生活类比 |
|---|---|---|
| 并发 (Concurrency) | 宏观上同时运行,微观上交替执行 | 一个人同时做多件事:写代码→回消息→写代码→回消息 |
| 并行 (Parallelism) | 真正的同时运行 | 多个人各做各的事:你写代码,我回消息 |
单核 CPU 只能并发,多核 CPU 才能并行。 你在单核机器上同时开了浏览器和终端,它们其实是在快速交替执行的,只是切换速度太快(毫秒级),你感觉不到。
4.5 调度算法
操作系统如何决定“下一个该轮到谁运行”?这就需要调度算法。调度的目标是:既要保证公平(每个进程都有机会运行),又要保证效率(CPU 不能闲着)。
常见的调度策略:
- 时间片轮转:每个进程分配一个时间片(如 10ms),时间用完就换下一个。
- 优先级调度:重要的事先做,不重要的排队等。
- 抢占式调度:高优先级进程来了,当前低优先级进程立刻让出 CPU。
Linux 使用的是抢占式多任务调度,保证系统随时响应重要任务,同时不让任何进程独占 CPU。
4.6 关键系统调用:fork 和 exec
在 Linux 中,创建新进程靠两个关键的系统调用:
| 系统调用 | 作用 | 生活类比 |
|---|---|---|
fork() |
复制当前进程,创建一个一模一样的子进程 | 克隆人——外表一样,但有自己的思想 |
exec() |
用新程序替换当前进程的内容(代码、数据全换) | 灵魂附体——外表不变,但内部完全换了一个人 |
fork + exec 是 Linux 创建新进程的标准组合:
- 你在终端敲
./a.out - shell 先调
fork()创建一个子进程(和自己一模一样) - 子进程再调
exec()把自己替换成a.out的内容 a.out开始运行,shell 在旁边等着
这就是你在终端里启动任何程序的底层原理。
5. 内存管理:空间怎么分配?
进程管理解决了“谁用 CPU”的问题,还有另一个问题:每个进程放在内存的哪个位置?
5.1 物理内存 vs 虚拟内存
| 概念 | 含义 |
|---|---|
| 物理内存 | 真实存在的内存条(硬件),容量有限 |
| 虚拟内存 | 操作系统给每个进程“假装”有的一大块连续内存 |
为什么需要虚拟内存?
- 物理内存可能被多个进程分得七零八碎,不连续。
- 每个进程都以为自己独占了一大块连续内存,编程更方便。
- 进程之间互相隔离,一个进程崩了不会破坏其他进程的数据。
5.2 页表:虚拟到物理的翻译官
页表是一张映射表,负责把虚拟地址翻译成物理地址。
- 内存被分成一块块固定大小的页,通常每页 4KB(4096 字节)。
- 进程看到的连续地址空间(如 0 到 1000),实际在物理内存中可能分布在完全不连续的位置。
- 页表记录着“虚拟第 0 页对应物理第 5 页,虚拟第 1 页对应物理第 8 页……”
这个翻译过程由 CPU 中的 MMU(内存管理单元) 硬件自动完成,对程序员透明。
5.3 单位换算速查
| 单位 | 换算 | 实际大小 |
|---|---|---|
| 1K | 1024 字节 | 约 1000 字节 |
| 1M | 1024K | 约 100 万字节 |
| 1G | 1024M | 约 10 亿字节 |
| 1T | 1024G | 约 万亿字节 |
💡 记忆点: 一张内存页的大小是 4K = 4096 字节,这个数字在学内存管理时会反复出现。
5.4 32 位系统的内存布局
32 位系统理论上能访问 4G 的地址空间(232=4G2^{32} = 4\text{G}232=4G)。操作系统通常把 4G 分成两部分:
| 区域 | 大小 | 用途 |
|---|---|---|
| 用户空间 | 0 ~ 3G | 给用户的程序使用(你的 a.out 就在这里) |
| 内核空间 | 3G ~ 4G | 给操作系统内核使用(管理硬件、调度进程) |
这就是为什么 32 位系统最大只支持 4G 内存——地址总线就那么宽,寻址范围就这么大。现在的 64 位系统理论上可以访问 16EB 的地址空间,足够用很多很多年。
6. Linux 开发工具链速查
理解了操作系统管什么之后,再回头看开发工具链,就能知道每个工具处于什么位置。
6.1 静态库 vs 共享库
| 特性 | 静态库 (.a) |
共享库 (.so) |
|---|---|---|
| 链接方式 | 把用到的函数拷贝到可执行程序中 | 不拷贝,运行时动态加载 |
| 空间占用 | 每个程序都有一份副本,较大 | 多个程序共享同一份内存中的代码 |
| 更新维护 | 库改了需重新编译主程序 | 库改了只需替换 .so 文件 |
💡 原理联系: 共享库节省内存,正是因为操作系统可以在物理内存中只存一份,多个进程通过页表映射到同一块物理内存。
6.2 文件存放位置速查
| 文件类型 | 常见位置 |
|---|---|
头文件 (.h) |
/usr/include |
静态库 (.a) |
/lib 或 /usr/lib |
共享库 (.so) |
/lib 或 /usr/lib |
| 可执行程序 | /bin 或 /usr/bin(如 ls、pwd、cp) |
6.3 GDB 调试命令速查
| 命令 | 全称 | 功能 |
|---|---|---|
l |
list | 列出源码 |
b |
break | 设置断点 |
r |
run | 运行程序 |
n |
next | 单步跳过(不进入函数内部) |
s |
step | 单步进入(进入函数内部) |
p |
打印变量值 | |
c |
continue | 继续运行到下一个断点 |
finish |
— | 跳出当前函数 |
q |
quit | 退出 gdb |
7. 总结:一条线串起来
把硬件、操作系统、进程、内存、开发工具链串起来,就得到一条完整的链路:
你写代码(vim hello.c)
↓
编译四步:预处理 → 编译 → 汇编 → 链接(gcc)
↓
生成 ELF 可执行文件(hello)
↓
在终端运行(./hello)
↓
shell 调用 fork() 创建子进程
↓
子进程调用 exec() 替换为 hello 的内容
↓
操作系统为 hello 进程分配虚拟内存,建立页表
↓
调度器将 hello 进程放入就绪队列
↓
调度器选中 hello 进程,CPU 开始执行
↓
进程等待 I/O 时进入阻塞状态,让出 CPU
↓
I/O 完成后回到就绪状态,等待下次调度
↓
程序执行完毕,进程结束,操作系统回收所有资源
理解了这条链路,你就理解了从硬件到操作系统、从编译到运行的完整流程。
8. 学习体会
以前觉得操作系统是纯理论课,进程、内存、调度这些概念和写代码没什么关系。直到我开始在 Linux 下做开发,用 gdb 调试段错误、用 ldd 查共享库依赖、看程序被 fork 和 exec 启动——才发现操作系统的每一个概念,都直接对应着程序的行为。
- 进程不是抽象的概念,它有一个实实在在的结构体
task_struct在管理着。 - 内存不是凭空分配的,页表在做着虚拟到物理的翻译。
- 并发不是魔法,是调度器在毫秒级的时间尺度上切换进程。
这些东西不是背下来就能理解的,只有在实践中遇到问题、排查问题、解决问题,才能把“知识点”变成“肌肉记忆”。这篇文章的整理,就是为了给后面学 Linux 进程系统调用打地基。地基稳了,后面学 fork、exec、进程间通信,就会顺畅很多。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)