操作系统内核原理学习笔记
从进程调度到内存管理,深入理解操作系统底层运作机制。
1 · 操作系统概述
1.1 什么是操作系统
操作系统是管理硬件资源、为上层应用提供统一接口的系统软件。它负责:
- 进程/线程管理
- 内存管理
- 文件系统
- 设备驱动
- 网络协议栈
- 安全与权限控制
没有操作系统,每个程序都要直接和硬件打交道,开发效率和稳定性都会极低。
1.2 内核架构分类
宏内核(Monolithic Kernel)
- 所有核心功能运行在同一个内核地址空间
- 性能高,模块间通信快
- 代表:Linux、Windows NT(混合内核偏宏内核)
微内核(Microkernel)
- 内核只保留最基本功能(调度、IPC、内存管理)
- 文件系统、设备驱动等运行在用户态
- 更稳定、更安全,但 IPC 开销大
- 代表:Minix、QNX、seL4
混合内核
- 介于宏内核和微内核之间
- 代表:Windows NT、macOS(XNU)
1.3 内核态 vs 用户态
CPU 有多个特权级别(Ring 0 ~ Ring 3):
- Ring 0:内核态,可以执行特权指令、访问所有硬件
- Ring 3:用户态,普通应用程序运行在此
系统调用(System Call) 是用户态进入内核态的唯一合法入口。例如:
read() / write()
open() / close()
fork() / exec()
mmap() / brk()
上下文切换有开销:保存/恢复寄存器、切换地址空间、刷新 TLB。
2 · 进程与线程
2.1 进程
进程是程序的一次执行实例,拥有独立的地址空间。
进程控制块(PCB)包含:
- PID(进程 ID)
- 状态(就绪、运行、阻塞、僵尸)
- CPU 寄存器快照
- 内存映射
- 打开的文件描述符表
- 调度信息
进程状态转换:

创建
↓
就绪 ← 时间片到
↓ 调度
运行
↓ I/O 请求 / 等待事件
阻塞
↓ I/O 完成
就绪
↓ 执行完成
终止
2.2 线程
线程是进程内的执行单元,多个线程共享进程的地址空间和资源。
一个线程有独享的:
共享的:
线程模型:
- 1:1 模型:一个用户线程对应一个内核线程(Linux pthread)
- N:1 模型:多个用户线程映射到一个内核线程(早期 Green Thread)
- M:N 模型:M 个用户线程映射到 N 个内核线程(Go goroutine 近似)
2.3 进程间通信(IPC)
| 方式 |
特点 |
适用 |
| 管道 |
单向,父子进程 |
简单数据流 |
| 命名管道 |
任意进程,文件系统可见 |
无亲缘关系进程 |
| 消息队列 |
内核维护消息链表 |
异步消息 |
| 共享内存 |
最快,需要同步 |
大数据量 |
| 信号量 |
同步与互斥 |
配合共享内存 |
| 信号 |
异步通知 |
中断处理 |
| Socket |
可跨网络 |
分布式进程 |
共享内存为什么最快?
因为数据不需要在内核和用户空间之间拷贝,多个进程直接映射同一块物理内存。
2.4 协程
协程是用户态轻量级线程,由运行时调度。
特点:
- 切换无需进入内核态
- 极低的切换开销
- 适合高并发 I/O 密集型场景
代表:
- Go goroutine
- Python asyncio
- Rust async/await
- C++20 coroutine
Go 的 M:P:G 模型:
- G:Goroutine
- M:操作系统线程
- P:逻辑处理器,M 需要绑定 P 才能执行 G
3 · CPU 调度
3.1 调度目标
- 公平性:每个进程都能获得 CPU 时间
- 吞吐量:单位时间完成的进程数
- 响应时间:交互式系统关注(用户点击后多久有反应)
- 周转时间:批处理系统关注(提交到完成的总时间)
- 实时性:实时系统满足截止时间
3.2 经典调度算法
FCFS(先来先服务)
- 按到达顺序执行
- 优点:简单
- 缺点:护航效应(长任务阻塞短任务)
SJF(短作业优先)
时间片轮转(RR)
- 每个进程分配固定时间片
- 时间片太大 → 响应差;太小 → 切换开销大
- 通常 10ms ~ 100ms
优先级调度
- 高优先级先执行
- 需要防饿死:老化(Aging)机制,等待时间越长优先级越高
多级反馈队列(MLFQ)
- 多个队列,不同优先级
- 新进程进入高优先级队列,时间片短
- 用完时间片降级到低优先级队列
- 兼顾交互式和批处理任务
3.3 Linux CFS
Linux 的 Completely Fair Scheduler 目标是让每个进程获得"公平"的 CPU 时间。
核心思想:
- 每个进程维护 vruntime(虚拟运行时间)
- vruntime 增长越快,说明它占用 CPU 越多
- 调度时选择 vruntime 最小的进程运行
数据结构:
- 使用 Red-Black Tree 维护可运行进程
- vruntime 最小的节点在最左边
nice 值影响权重:
- nice 越低,权重越高,实际运行时间增长越慢
- nice 越高,权重越低,实际运行时间增长越快
4 · 内存管理
4.1 虚拟内存
每个进程有独立的虚拟地址空间,大小由硬件决定(x86-64 通常 48 位有效地址,256TB)。
虚拟地址通过页表映射到物理地址。
为什么需要虚拟内存?
- 进程隔离:一个进程不能直接访问另一个进程的内存
- 内存超分配:进程的虚拟地址空间可以比物理内存大
- 简化编程:每个进程都以为自己独占内存
- 共享内存:不同进程可以映射到同一物理页
4.2 分页机制
内存被划分成固定大小的页(Page),通常 4KB。
页表记录虚拟页到物理页的映射。
x86-64 使用多级页表(4 级):
CR3 → PGD → PUD → PMD → PTE → Physical Page
如果页表项不存在或权限不足,会触发缺页异常(Page Fault)。
4.3 TLB
TLB(Translation Lookaside Buffer)是 CPU 内部的高速缓存,缓存最近用过的虚拟→物理映射。
TLB 命中:地址翻译几乎无开销。
TLB 未命中:需要查页表,可能多级内存访问。
进程切换时 TLB 会被刷新(或 ASID 标记)。
4.4 页面置换算法
物理内存不足时,需要把某些页换出到磁盘。
OPT:替换最长时间不会被访问的页(理论最优,无法实现)
FIFO:先进先出,简单但效果差
LRU:替换最久未使用的页
Clock(二次机会):近似 LRU,用访问位实现
Linux Active/Inactive 链表:近似 LRU 的实现
4.5 内存分配
Buddy System(伙伴系统)
- 按 2 的幂次分配物理页
- 分配和合并都高效
- 用于内核大块内存分配
Slab / Slub
- 内核小对象分配器
- 预分配固定大小的对象池
- 减少碎片和分配开销
用户态 malloc
- ptmalloc(glibc 默认)
- jemalloc(多线程友好,Redis、Facebook 用)
- tcmalloc(Google)
malloc 不一定每次都向内核申请内存,而是通过 brk 或 mmap 申请大块,内部切分管理。
4.6 mmap
mmap 把文件或设备映射到进程地址空间。
用途:
- 文件映射 I/O
- 共享内存
- 动态库加载
- 进程间通信
相比 read/write,mmap 可以减少一次内核到用户空间的拷贝。
5 · 文件系统
5.1 VFS
VFS(Virtual File System)是 Linux 提供的统一文件系统接口。
四个核心对象:
- superblock:文件系统整体信息
- inode:文件元数据(大小、权限、时间戳、数据块位置)
- dentry:目录项,缓存路径到 inode 的映射
- file:打开的文件实例
5.2 inode
inode 不包含文件名,文件名存在目录的数据块中。
目录项 = 文件名 + inode 号
硬链接:多个文件名指向同一个 inode
软链接:独立文件,内容是目标路径
5.3 ext4 文件系统
ext4 的关键特性:
- 块组(Block Group):把磁盘分成多个块组,每个块组相对独立
- 延迟分配:写入时先缓存,刷新时再分配磁盘块
- 日志(JBD2):先写日志再写数据,崩溃后可恢复
- Extents:连续存储大文件,替代间接块映射
5.4 页缓存(Page Cache)
读文件时,先查页缓存:
- 命中:直接返回
- 未命中:从磁盘读入页缓存,再返回给用户
写文件时:
- 先写到页缓存,标记为脏页
- 由 pdflush / flush 线程异步刷盘
fsync() 强制刷盘,保证持久化
数据库通常用 O_DIRECT 绕过页缓存,自己管理缓冲池。
6 · 中断与异常
6.1 中断
中断是 CPU 暂停当前任务,去处理异步事件的机制。
硬件中断:由外设触发
软件中断:由指令触发
int 0x80(传统 Linux 系统调用)
syscall / sysenter(现代 x86 系统调用)
中断处理流程:
- CPU 收到中断信号
- 保存当前上下文(寄存器、程序计数器)
- 查中断描述符表(IDT),找到处理函数
- 执行中断处理
- 恢复上下文,返回原任务
6.2 上半部与下半部
上半部(Top Half)
- 紧急、必须快速完成
- 通常关闭中断执行
- 只做最少工作,如保存数据、调度下半部
下半部(Bottom Half)
- 可以稍后执行
- 开中断执行,不阻塞其他中断
- 实现方式:softirq、tasklet、workqueue
例如网卡收包:
- 上半部:把数据从网卡寄存器复制到内核缓冲区
- 下半部:协议栈处理(IP、TCP)
6.3 异常
异常是 CPU 执行指令时产生的同步事件:
- 除零错误
- 缺页异常
- 非法指令
- 栈溢出
- 断点(int 3)
内核会向进程发送信号:
- SIGSEGV:段错误
- SIGFPE:浮点异常
- SIGBUS:总线错误
- SIGILL:非法指令
7 · 内核同步机制
7.1 为什么需要同步
多核 CPU 并发执行,多个 CPU 可能同时访问同一个内核数据结构(链表、计数器、页表)。需要同步原语保护共享数据。
7.2 自旋锁(Spinlock)
- 拿不到锁时忙等待(自旋)
- 不会睡眠
- 适合临界区极短的场景
- 不能递归获取
spin_lock(&my_lock);
// 临界区
spin_unlock(&my_lock);
7.3 互斥锁(Mutex)
- 拿不到锁时睡眠,让出 CPU
- 适合临界区较长或可能阻塞的场景
- 可以睡眠
7.4 信号量(Semaphore)
- 计数器,允许多个线程同时访问
- P 操作:减 1,为负则等待
- V 操作:加 1,唤醒等待线程
7.5 RCU
Read-Copy-Update,适合读多写少的场景。
- 读不加锁
- 写时复制一份数据,修改后替换指针
- 老数据延迟释放,等待所有读者退出
Linux 内核大量使用 RCU,如进程链表、路由表。
7.6 内存屏障
现代 CPU 和编译器会乱序执行指令以优化性能。内存屏障防止特定类型的乱序:
smp_mb():全屏障
smp_rmb():读屏障
smp_wmb():写屏障
无锁编程中经常使用。
8 · 系统调用
8.1 常见系统调用
| 类别 |
系统调用 |
| 进程 |
fork / vfork / clone / exec / wait / exit |
| 文件 |
open / read / write / close / lseek / stat |
| 目录 |
mkdir / rmdir / chdir / getcwd |
| 网络 |
socket / bind / listen / accept / connect / send / recv |
| 内存 |
brk / mmap / munmap / mprotect |
| 信号 |
kill / sigaction / sigprocmask |
| 时间 |
gettimeofday / clock_gettime / nanosleep |
8.2 系统调用流程(x86-64)
- 用户程序把系统调用号放入
rax 寄存器
- 参数放入
rdi、rsi、rdx 等寄存器
- 执行
syscall 指令
- CPU 切换到 Ring 0,查 MSR 寄存器找到入口函数
- 根据系统调用号调用
sys_call_table 中的函数
- 执行内核逻辑
- 返回值放入
rax,执行 sysret 返回用户态
8.3 用 strace 调试
# 跟踪指定系统调用
strace -e trace=open,read,write ./myapp
# 统计系统调用次数和耗时
strace -c ./myapp
# 附加到运行中的进程
strace -p <pid>
strace 是排查程序问题的重要工具,比如:
- 程序卡在某个系统调用上
- 配置文件路径不对
- 权限问题导致 open 失败