理解操作系统:进程管理、内存管理与 Linux 开发基础

1. 前言:为什么学 Linux 进程之前,要先补操作系统?

前两篇博客我分别整理了 Linux 环境搭建和 C 语言的编译调试流程。掌握了 vimgccgdb 之后,我开始接触一个更核心的模块:Linux 进程

但很快我就发现一个问题:老师课上讲的 forkexecPCB进程状态,听起来全是抽象的概念。如果不理解操作系统到底是怎么管理硬件、怎么调度程序的,这些知识点就只是零散的“名词解释”,很难串起来。

于是我暂停了进程的系统调用学习,先把操作系统的核心理论补了一遍。这篇文章就是这次学习整理的笔记,聚焦两个核心问题:

  • CPU 怎么分配时间?(进程管理)
  • 内存怎么分配空间?(内存管理)

理解了这两件事,后面再学 forkexec、进程间通信,就会有“原来如此”的感觉。


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,本质上都是操作系统。

它的核心职责就三件事:

  1. 提供交互接口:让用户和程序能方便地使用硬件(命令行、图形界面)。
  2. 管理内存:决定哪个程序使用哪块内存,各进程互不侵犯。
  3. 管理进程:决定哪个程序什么时候用 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 创建新进程的标准组合:

  1. 你在终端敲 ./a.out
  2. shell 先调 fork() 创建一个子进程(和自己一模一样)
  3. 子进程再调 exec() 把自己替换成 a.out 的内容
  4. 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(如 lspwdcp

6.3 GDB 调试命令速查

命令 全称 功能
l list 列出源码
b break 设置断点
r run 运行程序
n next 单步跳过(不进入函数内部)
s step 单步进入(进入函数内部)
p print 打印变量值
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 进程系统调用打地基。地基稳了,后面学 forkexec、进程间通信,就会顺畅很多。

Logo

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

更多推荐