【Linux 系列·第 02 篇】操作系统原理:进程·内存·文件系统·I/O——Linux 怎么工作

系列回顾:第 01 篇我们绘制了 Linux 的全景图——四代演进、三层架构、三大哲学。本篇深入 Linux 的核心:操作系统原理——Linux 怎么工作?操作系统是计算机的"大管家"——它管理四大资源:CPU(进程管理)、内存(内存管理)、磁盘(文件系统)、设备(I/O 系统)。进程管理决定谁用 CPU、用多久——fork() 创建进程,CFS 调度器公平分配 CPU 时间,上下文切换在进程间切换。内存管理决定谁用内存、用多少——虚拟内存给每个进程 128TB 的地址空间,页表+TLB 加速地址翻译,按需分配+Swap 用磁盘扩展内存。文件系统决定数据怎么存、怎么找——VFS 统一所有文件系统接口,ext4 是默认文件系统,Page Cache 用内存加速磁盘访问。I/O 系统决定怎么和设备交互——中断+DMA 减少CPU参与,epoll 实现高并发 I/O。今天,我们从进程管理、内存管理到文件系统与 I/O,彻底拆解 Linux 操作系统的工作原理。


📑 文章目录


🔄 一、进程管理:CPU 的"调度员"

在这里插入图片描述

1.1 进程是什么?

进程(Process)是操作系统中最核心的抽象——一个正在运行的程序的实例。你在终端输入 ls,系统就创建了一个 ls 进程;你打开浏览器,系统就创建了一个浏览器进程。每个进程都有自己的地址空间、文件描述符表、信号处理等资源。

Linux 中进程的表示:task_struct(进程控制块,PCB)。每个进程在内核中都有一个 task_struct 结构体,记录了进程的所有信息:PID(进程 ID)、状态(运行/就绪/阻塞)、优先级、地址空间、打开的文件、信号处理、调度信息等。task_struct 是 Linux 进程管理的核心数据结构——内核通过它了解和控制每个进程。

Linux 的独特设计:进程和线程统一为 task_struct。在 Linux 中,线程只是"共享地址空间的进程"——它们用同样的 task_struct 表示,通过 clone() 系统调用的参数控制共享程度。这种统一抽象简化了内核设计,也使得 Linux 的线程创建比其他操作系统更高效。

1.2 进程的生命周期:fork → exec → exit

Linux 进程的创建遵循 Unix 的经典模式:fork() + exec()

fork():创建子进程。fork() 创建当前进程的副本——子进程获得父进程地址空间、文件描述符表、信号处理等的拷贝。fork() 的返回值:父进程返回子进程的 PID,子进程返回 0——通过返回值区分父子进程。

fork() 的关键优化:写时复制(Copy-on-Write, COW)。fork() 时并不真正复制父进程的内存页,而是让父子进程共享同一份物理页,标记为"只读"。当任一方尝试写入时,才触发缺页异常,内核复制该页——这就是"写时复制"。COW 大幅降低了 fork() 的开销:如果子进程立即 exec() 加载新程序,父进程的内存页根本不需要复制。

exec():加载新程序。exec() 用新的程序替换当前进程的代码和数据——保留 PID 和部分资源,但代码段、数据段、堆栈全部替换。这就是为什么 fork()+exec() 是创建新进程的标准模式:fork() 创建进程壳,exec() 加载新程序。

exit():终止进程。进程调用 exit() 终止自己,进入僵尸状态(Zombie),保留 task_struct 供父进程查询。父进程调用 wait() 回收子进程的资源——如果父进程不 wait,子进程就永远停留在僵尸状态,占用 PID 等资源。这就是"僵尸进程"问题——大量僵尸进程会耗尽 PID 资源。

1.3 进程调度:CFS 完全公平调度器

Linux 的默认调度器是 CFS(Completely Fair Scheduler, 2007)——它的目标很简单:公平地分配 CPU 时间给所有进程

CFS 的核心思想:vruntime(虚拟运行时间)。每个进程维护一个 vruntime——它已经运行了多少"虚拟时间"。权重高的进程(优先级高)vruntime 增长慢,权重低的进程 vruntime 增长快。CFS 每次选择 vruntime 最小的进程运行——这样,权重高的进程自然获得更多 CPU 时间,但不会饿死低权重进程。

CFS 的数据结构:红黑树。所有可运行进程按 vruntime 排列在红黑树中——选择下一个进程只需取最左节点 O(1),插入和删除 O(log N)。红黑树保证了调度的效率。

CFS 的时间分配:sched_period。CFS 保证在一个调度周期内,每个可运行进程至少运行一次。时间分配 = sched_period × (进程权重 / 总权重)。权重由 nice 值决定:nice 值越低,权重越高,CPU 时间越多。nice 值范围 -20 到 +19,默认 0。

1.4 进程状态与上下文切换

Linux 进程的五种状态:Running(正在 CPU 上运行或等待 CPU)、Interruptible(可中断阻塞,等待事件,可被信号唤醒)、Uninterruptible(不可中断阻塞,等待磁盘 I/O 等,不可被信号唤醒)、Stopped(被暂停,如 SIGSTOP)、Zombie(已终止,等待父进程 wait 回收)。

上下文切换(Context Switch):从一个进程切换到另一个进程——保存当前进程的寄存器、栈指针等 CPU 状态,恢复下一个进程的 CPU 状态。上下文切换的开销:1-10 微秒——看起来很短,但频繁切换(如大量进程竞争 CPU)会显著影响性能。减少上下文切换是性能调优的重要方向。

1.5 进程 vs 线程 vs 协程

进程:独立地址空间,隔离性好,创建开销大(fork 需要复制页表等),切换开销大(需要切换地址空间)。适用于需要隔离的任务。

线程:共享地址空间,通信高效(直接共享变量),创建开销中(pthread_create 需要分配栈等),切换开销中(不需要切换地址空间)。适用于并行计算。

协程:用户态调度,创建开销极低(只需分配栈),切换开销极低(不需要内核参与)。适用于高并发 I/O(如 Go 的 goroutine、Python 的 asyncio)。

Linux 的统一抽象:进程和线程都是 task_struct,通过 clone() 的参数控制共享程度。这种设计使得 Linux 的线程创建比 Windows 更高效——Windows 线程是专门的内核对象,而 Linux 线程只是"共享地址空间的进程"。


🧠 二、内存管理:内存的"分配器"

在这里插入图片描述

2.1 虚拟内存:每个进程的"独栋别墅"

虚拟内存(Virtual Memory)是现代操作系统最重要的发明之一——给每个进程一个独立的、连续的、私有的地址空间。32 位系统每个进程 4GB,64 位系统每个进程 128TB。进程以为自己独占整个内存空间,实际上物理内存可能远小于虚拟地址空间。

虚拟内存的三大好处:隔离(每个进程的地址空间独立,一个进程崩溃不会影响其他进程)、简化编程(程序员不需要关心物理内存布局,直接使用虚拟地址)、扩展容量(物理内存不够时,用磁盘(Swap)补充,让每个进程都能使用超过物理内存的空间)。

进程地址空间的布局(从低到高):代码段(.text,可执行代码,只读)→ 数据段(.data,已初始化全局变量;.bss,未初始化全局变量)→ (Heap,向上增长,malloc/new 分配)→ 内存映射区(mmap,共享库、文件映射)→ (Stack,向下增长,局部变量、函数调用)→ 内核空间(高地址,用户不可访问)。

2.2 页表与 TLB:虚拟地址到物理地址的翻译

虚拟内存的核心问题:虚拟地址怎么翻译为物理地址? 答案是页表(Page Table)

Linux 采用多级页表:64 位系统使用 4 级页表(PGD → PUD → PMD → PTE)。虚拟地址被分为 5 部分:页全局目录索引、页上级目录索引、页中间目录索引、页表项索引、页内偏移。每次地址翻译需要 4 次内存访问——太慢了!

TLB(Translation Lookaside Buffer):页表的缓存。TLB 存储最近使用的虚拟地址→物理地址映射,CPU 先查 TLB,命中则直接获得物理地址(1 个时钟周期),未命中则查页表(4 次内存访问)。TLB 命中率通常在 95-99%,是内存性能的关键。

大页(Huge Pages):默认页大小 4KB,大页 2MB 或 1GB。大页减少页表项数量,减少 TLB Miss,提高性能。深度学习训练常用大页——因为模型参数占用大量内存,4KB 页导致频繁 TLB Miss。

2.3 按需分配与缺页中断

Linux 的内存分配策略:按需分配(Demand Paging)——malloc() 只分配虚拟地址,不分配物理内存。当进程首次访问该地址时,触发缺页中断(Page Fault),内核才分配物理页。

缺页中断的处理流程:CPU 访问虚拟地址 → MMU 查页表 → 页表项标记"不存在" → 触发缺页中断 → 内核分配物理页 → 更新页表 → 重新执行指令。这种"用到了才分配"的策略避免了浪费——malloc(1GB) 不会立即占用 1GB 物理内存,只有实际访问的页才分配。

2.4 Swap 与 OOM Killer

Swap(交换空间):当物理内存不足时,内核将不常用的内存页写入磁盘的 Swap 空间,释放物理内存给需要的进程。Swap 让系统可以使用超过物理内存的内存——但代价是速度:磁盘访问比内存慢 1000 倍以上。Swap 频繁使用(Swap Thrashing)会导致系统极度缓慢。

OOM Killer(Out-Of-Memory Killer):当物理内存和 Swap 都耗尽时,内核启动 OOM Killer——选择一个进程杀死,释放内存。OOM Killer 的选择标准:oom_score 越高越容易被杀——占用内存多、优先级低、子进程多的进程得分高。可以通过 /proc/[pid]/oom_adj 调整分数,保护关键进程不被杀。

2.5 内存分配:brk/mmap vs kmalloc

用户空间内存分配:brk() 调整堆顶指针,用于小内存分配(malloc < 128KB);mmap() 在内存映射区分配,用于大内存分配(malloc >= 128KB)。free() 时,brk 分配的内存可能不会立即归还内核(内存池),mmap 分配的内存会立即归还。

内核空间内存分配:kmalloc() 分配物理连续的内存(DMA 等需要),vmalloc() 分配虚拟连续但物理不连续的内存(大块内存)。kmalloc 速度快但有碎片风险,vmalloc 灵活但需要页表映射。


💾 三、文件系统与 I/O:数据的"管家"

3.1 VFS:一切文件系统的统一接口

VFS(Virtual File System,虚拟文件系统)是 Linux 文件系统的核心抽象层——为所有文件系统提供统一的接口。无论底层是 ext4、XFS、Btrfs、NTFS 还是 /proc,用户程序都用同样的 open()/read()/write()/close() 操作它们。这就是"一切皆文件"的实现基础。

VFS 的四大核心对象:Superblock(超级块,文件系统的元信息——大小、类型、状态)、Inode(索引节点,文件的元数据——权限、大小、数据块位置、时间戳)、Dentry(目录项,文件名到 Inode 的映射——加速路径查找)、File(打开的文件实例——读写偏移、访问模式、引用计数)。

VFS 的工作流程:用户调用 open(“/home/user/file.txt”) → VFS 逐级解析路径(/ → home → user → file.txt)→ 每级查找 Dentry 缓存,未命中则读取磁盘 Inode → 找到文件的 Inode → 创建 File 对象 → 返回文件描述符 fd。后续 read(fd)/write(fd) 都通过 fd 找到 File 对象,再找到 Inode,最终操作数据块。

3.2 ext4:Linux 的默认文件系统

ext4(Fourth Extended Filesystem)是 Linux 的默认文件系统——自 2008 年引入以来,一直是大多数 Linux 发行版的默认选择。ext4 的前身:ext(1992)→ ext2(1993)→ ext3(2001,添加日志)→ ext4(2008,大文件支持、延迟分配)。

ext4 的磁盘布局:磁盘被划分为块组(Block Group),每个块组包含:超级块备份(文件系统元信息的副本)、块位图(记录哪些数据块空闲)、Inode 位图(记录哪些 Inode 空闲)、Inode 表(存储所有 Inode)、数据块(存储文件内容)。

ext4 的三大关键特性:日志(JBD2)——写操作先记录日志,再写入数据块。如果写入过程中崩溃,重启后可以通过日志恢复,保证文件系统一致性。延迟分配(Delayed Allocation)——写数据时先写入 Page Cache,不立即分配磁盘块,等到刷回磁盘时才分配。这减少了碎片,提高了写入性能。Extent——用起始块+长度表示连续的数据块,而不是逐块映射。一个大文件可能只需要几个 Extent,而不是几千个块映射。

3.3 Page Cache:用内存加速磁盘

Page Cache 是 Linux 文件系统性能的关键——将磁盘数据缓存在内存中。读操作先查 Page Cache,命中则直接返回(纳秒级),未命中则读磁盘(微秒级)并缓存。写操作先写 Page Cache,标记为"脏页"(Dirty Page),后台线程(pdflush)定期刷回磁盘,或 fsync() 强制刷回。

Page Cache 的关键特性:空闲内存就是缓存——Linux 会将所有空闲内存用于 Page Cache,当应用程序需要内存时自动回收。这意味着 Linux 的"可用内存"= 空闲内存 + 可回收缓存——不要看到"内存使用率高"就慌,大部分可能是 Page Cache。

3.4 I/O 系统:中断、DMA 与 epoll

中断(Interrupt):设备通过中断通知 CPU 事件——“数据准备好了”、“操作完成了”。CPU 收到中断后,暂停当前任务,执行中断处理程序,然后恢复。中断让 CPU 不需要轮询设备状态,大幅提高效率。

DMA(Direct Memory Access):让设备直接读写内存,不需要 CPU 参与。没有 DMA:CPU 发指令 → 设备处理 → CPU 拷贝数据到内存。有 DMA:CPU 发指令 → 设备处理 → DMA 直接将数据写入内存 → 中断通知 CPU 完成。DMA 解放了 CPU,让它做更有用的事。

I/O 多路复用:一个线程同时监控多个文件描述符的 I/O 事件。演进:select(O(n),最多 1024 个 fd)→ poll(O(n),无限制但遍历)→ epoll(O(1),事件驱动,只返回就绪的 fd)。epoll 是高并发的基石——Nginx、Redis、Node.js 都基于 epoll 实现高并发。

epoll 的两种触发模式:LT(Level Triggered,水平触发)——只要 fd 有数据可读,epoll_wait 就返回。ET(Edge Triggered,边缘触发)——只有 fd 状态变化时才返回,需要一次性读完所有数据。ET 模式效率更高但编程更复杂。

3.5 /proc 与 /sys:内核信息的窗口

/proc 和 /sys 是"一切皆文件"哲学的极致体现——虚拟文件系统,不占磁盘空间,读取时动态生成

/proc:进程和系统信息。/proc/[pid]/status(进程状态)、/proc/[pid]/maps(内存映射)、/proc/[pid]/fd/(打开的文件)、/proc/cpuinfo(CPU 信息)、/proc/meminfo(内存信息)、/proc/loadavg(负载均值)。

/sys:设备和内核参数。/sys/class/(设备分类)、/sys/devices/(设备树)、/sys/kernel/(内核参数)。通过读写 /sys 的文件可以动态调整内核参数——不需要重启。


📊 总结对比

进程 vs 线程 vs 协程

维度 进程 线程 协程
地址空间 独立(开销大) 共享(开销小) 共享
创建成本 高(fork) 中(pthread) 低(用户态)
切换成本 高(1-10us) 中(0.5-5us) 低(0.1-0.5us)
通信 IPC(管道/共享内存) 共享变量 共享变量
调度 内核调度 内核调度 用户调度
适用 隔离任务 并行计算 高并发I/O

I/O 多路复用

维度 select poll epoll
最大连接 1024 无限制 无限制
复杂度 O(n) O(n) O(1)
内存拷贝 每次拷贝 每次拷贝 mmap共享
触发方式 水平触发 水平触发 水平+边缘
适用场景 少量连接 中量连接 海量连接

一句话总结

Linux 操作系统四大核心原理:进程管理(进程是运行中的程序实例,task_struct统一表示进程和线程/fork()+exec()创建新进程写时复制优化/CFS完全公平调度器vruntime+红黑树O(logN)/五种状态Running/Interruptible/Uninterruptible/Stopped/Zombie/上下文切换1-10us是性能杀手/进程vs线程vs协程:隔离性递减并发性递增)、内存管理(虚拟内存每个进程128TB独立地址空间隔离+简化编程+扩展容量/4级页表PGD→PUD→PMD→PTE+TLB缓存加速/按需分配Demand Paging用到了才分配/Swap用磁盘扩展内存但慢1000x/OOM Killer内存耗尽时杀进程保命/brk小内存mmap大内存kmalloc物理连续vmalloc虚拟连续)、文件系统(VFS四大对象Superblock/Inode/Dentry/File统一所有文件系统接口/ext4默认文件系统日志+延迟分配+Extent/Page Cache用内存加速磁盘空闲内存=缓存/proc和/sys虚拟文件系统内核信息的窗口)、I/O系统(中断通知CPU事件+DMA解放CPU/epoll O(1)事件驱动高并发基石Nginx/Redis都用它/LT水平触发ET边缘触发)。进程管理=公平分配CPU时间/内存管理=用空间换时间用磁盘换内存/文件系统=一切皆文件+缓存加速/I/O系统=中断+DMA+事件驱动。


参考链接

系列预告:第 03 篇将深入常用命令——文件操作·进程管理·网络工具·文本处理,掌握 Linux 的日常使用。

Logo

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

更多推荐