|
|
@@ -0,0 +1,591 @@
|
|
|
+---
|
|
|
+title: "操作系统理论与Linux内核实现-原理与本质"
|
|
|
+tags:
|
|
|
+ - source-summary
|
|
|
+type: source
|
|
|
+created: 2026-09-19
|
|
|
+source: "深度解析系列"
|
|
|
+description: "操作系统理论概念与Linux内核源码实现的深度对应,面向初学者的原理讲解"
|
|
|
+related:
|
|
|
+ - "[[02-微机原理与操作系统硬件基础-原理与本质]]"
|
|
|
+ - "[[01-从数电模电到计算机系统-原理与本质]]"
|
|
|
+---
|
|
|
+
|
|
|
+# 操作系统理论与Linux内核实现-原理与本质
|
|
|
+
|
|
|
+## 核心问题
|
|
|
+
|
|
|
+操作系统理论和Linux内核实现之间是什么关系?理论中的概念在Linux代码中是怎么落地的?
|
|
|
+
|
|
|
+**一句话回答**:操作系统理论定义了"做什么",Linux内核实现了"怎么做"。每个理论概念都能在内核源码中找到对应的结构体、函数和算法。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 原理讲解
|
|
|
+
|
|
|
+### 第一部分:进程管理
|
|
|
+
|
|
|
+#### 1. 理论中的进程/PCB → Linux的task_struct
|
|
|
+
|
|
|
+**理论概念**:进程是程序的一次执行实例,PCB(进程控制块)是操作系统用于管理进程的数据结构。
|
|
|
+
|
|
|
+**Linux实现**:`task_struct` 是Linux内核中最核心的数据结构之一,定义在 `include/linux/sched.h`。
|
|
|
+
|
|
|
+**task_struct的核心字段**:
|
|
|
+
|
|
|
+| 字段 | 类型 | 作用 |
|
|
|
+| ---------- | ----------------------- | ---------------------------- |
|
|
|
+| `pid` | `pid_t` | 进程ID,唯一标识 |
|
|
|
+| `state` | `long` | 进程状态 |
|
|
|
+| `prio` | `int` | 优先级 |
|
|
|
+| `mm` | `struct mm_struct *` | 内存描述符,管理虚拟地址空间 |
|
|
|
+| `stack` | `void *` | 内核栈指针 |
|
|
|
+| `files` | `struct files_struct *` | 打开的文件描述符表 |
|
|
|
+| `parent` | `struct task_struct *` | 父进程指针 |
|
|
|
+| `children` | `struct list_head` | 子进程链表 |
|
|
|
+| `thread` | `struct thread_struct` | CPU相关上下文(寄存器等) |
|
|
|
+
|
|
|
+**进程状态映射**:
|
|
|
+
|
|
|
+| 理论状态 | Linux宏定义 | 含义 |
|
|
|
+| ---------------- | ---------------------- | ----------------------- |
|
|
|
+| 创建 | `TASK_NEW` | 新建进程,尚未准备好 |
|
|
|
+| 就绪/运行 | `TASK_RUNNING` | 在运行队列中,可被调度 |
|
|
|
+| 阻塞(可中断) | `TASK_INTERRUPTIBLE` | 等待事件,可被信号唤醒 |
|
|
|
+| 阻塞(不可中断) | `TASK_UNINTERRUPTIBLE` | 等待I/O,不响应信号 |
|
|
|
+| 停止 | `TASK_STOPPED` | 被信号暂停(如SIGSTOP) |
|
|
|
+| 终止 | `EXIT_ZOMBIE` | 已终止,等待父进程回收 |
|
|
|
+
|
|
|
+> 关键理解:Linux把"就绪"和"运行"合并为 `TASK_RUNNING`,因为调度器决定谁真正运行,进程本身只关心"我是否可以被调度"。
|
|
|
+
|
|
|
+#### 2. 进程创建:fork()的写时复制(COW)机制
|
|
|
+
|
|
|
+**传统fork**:创建子进程时,完全复制父进程的地址空间。问题:复制开销大,且子进程通常立即调用exec(),复制的内容完全浪费。
|
|
|
+
|
|
|
+**COW fork(Linux实现)**:
|
|
|
+
|
|
|
+```
|
|
|
+fork()调用流程:
|
|
|
+1. 复制task_struct(轻量)
|
|
|
+2. 复制mm_struct(轻量)
|
|
|
+3. 页表标记为只读(关键!)
|
|
|
+4. 任一进程写入时 → 缺页中断 → 真正复制该页
|
|
|
+```
|
|
|
+
|
|
|
+**页表级实现原理**:
|
|
|
+
|
|
|
+```c
|
|
|
+// 简化的COW逻辑(mm/memory.c)
|
|
|
+static vm_fault_t do_wp_page(struct vm_fault *vmf) {
|
|
|
+ // 1. 检测到写保护页被写入
|
|
|
+ // 2. 判断是否为COW页
|
|
|
+ // 3. 分配新物理页
|
|
|
+ // 4. 复制原页内容
|
|
|
+ // 5. 更新页表映射为可写
|
|
|
+ // 6. 原页引用计数减1
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**优势**:
|
|
|
+
|
|
|
+- fork()时间从O(n)降到O(1)(n为内存页数)
|
|
|
+- 实际复制只发生在写入时
|
|
|
+- 大量fork+exec场景性能提升显著
|
|
|
+
|
|
|
+#### 3. 进程调度:CFS(完全公平调度器)
|
|
|
+
|
|
|
+**核心思想**:虚拟运行时间(vruntime)保证每个进程获得公平的CPU时间。
|
|
|
+
|
|
|
+**vruntime的含义**:
|
|
|
+
|
|
|
+- 每个进程维护自己的vruntime
|
|
|
+- 实际运行时,vruntime按实际时间流逝增加
|
|
|
+- nice值高的进程,vruntime增长慢(权重高)
|
|
|
+- 调度器选择vruntime最小的进程运行
|
|
|
+
|
|
|
+**红黑树数据结构的选择**:
|
|
|
+
|
|
|
+| 数据结构 | 插入/删除 | 查找最小 | 选择原因 |
|
|
|
+| ---------- | ------------ | ------------ | ---------------------------- |
|
|
|
+| 链表 | O(n) | O(1) | 删除太慢 |
|
|
|
+| 堆 | O(log n) | O(1) | 无法快速更新vruntime |
|
|
|
+| **红黑树** | **O(log n)** | **O(log n)** | **平衡:支持高效更新和查找** |
|
|
|
+
|
|
|
+**nice值到权重的映射**:
|
|
|
+
|
|
|
+```c
|
|
|
+// kernel/sched/core.c
|
|
|
+const int prio_to_weight[40] = {
|
|
|
+ /* -20 */ 88761, 71755, 56483, 46273, 36291,
|
|
|
+ /* -15 */ 29154, 23254, 18705, 14949, 11916,
|
|
|
+ /* -10 */ 9548, 7620, 6100, 4904, 3906,
|
|
|
+ /* -5 */ 3121, 2501, 1991, 1586, 1277,
|
|
|
+ /* 0 */ 1024, 820, 655, 526, 423,
|
|
|
+ /* 5 */ 335, 272, 215, 172, 137,
|
|
|
+ /* 10 */ 110, 87, 70, 56, 45,
|
|
|
+ /* 15 */ 36, 29, 23, 18, 15,
|
|
|
+};
|
|
|
+// nice -20 权重88761,nice 0 权重1024,nice 19 权重15
|
|
|
+// 权重越高,vruntime增长越慢,获得CPU时间越多
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 第二部分:内存管理
|
|
|
+
|
|
|
+#### 1. 虚拟内存的实现
|
|
|
+
|
|
|
+**页表的多级结构**:
|
|
|
+
|
|
|
+```
|
|
|
+虚拟地址结构(以x86_64为例,48位虚拟地址):
|
|
|
++---------+---------+---------+---------+-------------+
|
|
|
+| PGD索引 | PUD索引 | PMD索引 | PTE索引 | 页内偏移 |
|
|
|
+| 9 bits | 9 bits | 9 bits | 9 bits | 12 bits |
|
|
|
++---------+---------+---------+---------+-------------+
|
|
|
+
|
|
|
+四级页表遍历过程:
|
|
|
+CR3寄存器 → PGD[pgd_index] → PUD[pud_index] → PMD[pmd_index] → PTE[pte_index] → 物理页框号 + 偏移
|
|
|
+```
|
|
|
+
|
|
|
+**缺页中断的处理流程**:
|
|
|
+
|
|
|
+```c
|
|
|
+// arch/x86/mm/fault.c(简化)
|
|
|
+void do_page_fault(struct pt_regs *regs, unsigned long error_code) {
|
|
|
+ // 1. 解析虚拟地址(cr2寄存器)
|
|
|
+ // 2. 查找VMA(虚拟内存区域)
|
|
|
+ // 3. 检查访问权限
|
|
|
+ // 4. 根据原因分发处理:
|
|
|
+ // - 缺页(页面不在物理内存)→ 分配物理页并映射
|
|
|
+ // - 写保护(COW)→ 复制页面
|
|
|
+ // - 段错误(非法访问)→ 发送SIGSEGV信号
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**页表项(PTE)的结构**:
|
|
|
+
|
|
|
+| 位 | 含义 |
|
|
|
+| --------------- | ------------------------ |
|
|
|
+| Present | 页面是否在物理内存中 |
|
|
|
+| Read/Write | 读写权限 |
|
|
|
+| User/Supervisor | 用户/内核权限 |
|
|
|
+| Accessed | 页面是否被访问过 |
|
|
|
+| Dirty | 页面是否被修改过 |
|
|
|
+| PFN | 物理页框号(bit 12以上) |
|
|
|
+
|
|
|
+#### 2. 物理内存分配
|
|
|
+
|
|
|
+**伙伴系统(Buddy System)——解决外部碎片**:
|
|
|
+
|
|
|
+```
|
|
|
+核心思想:
|
|
|
+- 物理内存按2^order分为块(order 0=4KB, 1=8KB, ... 10=4MB)
|
|
|
+- 每个order维护一个空闲链表
|
|
|
+- 分配:找到最小满足需求的块,若没有则拆分更大的块
|
|
|
+- 释放:检查相邻伙伴是否空闲,若是则合并(递归向上合并)
|
|
|
+
|
|
|
+示例:分配12KB(需要order=2,即16KB)
|
|
|
+当前有:order0×3, order1×1, order2×0, order3×1
|
|
|
+→ 拆分order3 → 得到2个order2 → 用掉1个,返回1个order3的伙伴
|
|
|
+```
|
|
|
+
|
|
|
+**Slab/Slub分配器——高效分配小对象**:
|
|
|
+
|
|
|
+```
|
|
|
+为什么需要slab?
|
|
|
+- 伙伴系统最小分配4KB,但内核频繁分配task_struct(几KB)、inode等小对象
|
|
|
+- 直接用伙伴系统浪费严重(内部碎片)
|
|
|
+
|
|
|
+Slab的工作方式:
|
|
|
+1. 向伙伴系统申请一整页(object cache)
|
|
|
+2. 将页划分为固定大小的对象槽
|
|
|
+3. 维护空闲/已用对象链表
|
|
|
+4. 分配时从空闲链表取出,释放时放回
|
|
|
+5. 对象类型专用(如task_struct_cachep),避免内存碎片
|
|
|
+```
|
|
|
+
|
|
|
+#### 3. 用户空间与内核空间
|
|
|
+
|
|
|
+**32位系统的地址空间划分(3G/1G)**:
|
|
|
+
|
|
|
+```
|
|
|
+虚拟地址空间布局(32位Linux):
|
|
|
+
|
|
|
+0xFFFFFFFF ┌───────────────────┐
|
|
|
+ │ 内核空间(1GB) │ ← 所有进程共享同一份内核映射
|
|
|
+0xC0000000 ├───────────────────┤
|
|
|
+ │ │
|
|
|
+ │ 用户空间(3GB) │ ← 每个进程独立的虚拟地址空间
|
|
|
+ │ │
|
|
|
+0x00000000 └───────────────────┘
|
|
|
+
|
|
|
+64位系统(以x86_64为例):
|
|
|
+- 用户空间:0x0000000000000000 - 0x00007FFFFFFFFFFF (128TB)
|
|
|
+- 内核空间:0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF (128TB)
|
|
|
+```
|
|
|
+
|
|
|
+**关键设计**:内核空间映射到每个进程的高端地址。切换进程时不需要切换内核地址映射,因为:
|
|
|
+
|
|
|
+- 内核空间部分在所有进程中映射相同
|
|
|
+- 用户空间部分随进程切换而改变(通过切换CR3寄存器)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 第三部分:文件系统
|
|
|
+
|
|
|
+#### 1. VFS(虚拟文件系统)
|
|
|
+
|
|
|
+**一切皆文件的设计思想**:
|
|
|
+
|
|
|
+```
|
|
|
+Linux中,所有I/O资源都通过文件接口抽象:
|
|
|
+- 普通文件:/home/user/file.txt
|
|
|
+- 目录:/home/user/
|
|
|
+- 设备文件:/dev/sda, /dev/tty0
|
|
|
+- 管道:pipe文件描述符
|
|
|
+- 套接字:socket文件描述符
|
|
|
+- proc文件系统:/proc/cpuinfo, /proc/[pid]/status
|
|
|
+
|
|
|
+统一接口:open() / read() / write() / close() / ioctl()
|
|
|
+```
|
|
|
+
|
|
|
+**inode/dentry/file三层结构**:
|
|
|
+
|
|
|
+```
|
|
|
+三层抽象:
|
|
|
+
|
|
|
+1. inode(索引节点)—— 文件的"身份"
|
|
|
+ - 存储元数据:大小、权限、时间戳、数据块指针
|
|
|
+ - 每个文件一个inode,由inode号标识
|
|
|
+ - 定义在 include/linux/fs.h: struct inode
|
|
|
+
|
|
|
+2. dentry(目录项)—— 文件的"名字"
|
|
|
+ - 建立文件名 → inode的映射
|
|
|
+ - 维护目录树结构(parent/children)
|
|
|
+ - 有dentry cache加速路径查找
|
|
|
+ - 定义在 include/linux/dcache.h: struct dentry
|
|
|
+
|
|
|
+3. file(打开的文件)—— 进程的"文件句柄"
|
|
|
+ - 记录打开模式、当前偏移量、引用计数
|
|
|
+ - 指向dentry和inode
|
|
|
+ - 每次open()创建一个新的file对象
|
|
|
+ - 定义在 include/linux/fs.h: struct file
|
|
|
+
|
|
|
+关系图:
|
|
|
+进程 → files_struct → fd[fd_number] → file → dentry → inode → 磁盘数据块
|
|
|
+```
|
|
|
+
|
|
|
+**file_operations如何将系统调用连接到具体文件系统**:
|
|
|
+
|
|
|
+```c
|
|
|
+// include/linux/fs.h
|
|
|
+struct file_operations {
|
|
|
+ struct module *owner;
|
|
|
+ ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
|
|
|
+ ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
|
|
|
+ int (*open)(struct inode *, struct file *);
|
|
|
+ int (*release)(struct inode *, struct file *);
|
|
|
+ // ... 其他操作
|
|
|
+};
|
|
|
+
|
|
|
+// ext4文件系统的实现(fs/ext4/file.c):
|
|
|
+static const struct file_operations ext4_file_operations = {
|
|
|
+ .read = ext4_file_read_iter,
|
|
|
+ .write = ext4_file_write_iter,
|
|
|
+ .open = ext4_file_open,
|
|
|
+ // ...
|
|
|
+};
|
|
|
+
|
|
|
+// 调用链:
|
|
|
+// sys_read() → vfs_read() → file->f_op->read()
|
|
|
+// 不同文件系统的read实现不同,但接口统一
|
|
|
+```
|
|
|
+
|
|
|
+#### 2. 文件描述符
|
|
|
+
|
|
|
+**进程的files_struct → fdtable → file指针数组**:
|
|
|
+
|
|
|
+```
|
|
|
+数据结构关系:
|
|
|
+
|
|
|
+task_struct
|
|
|
+ └→ files_struct (fs_struct)
|
|
|
+ └→ fdtable
|
|
|
+ └→ struct file *fd[] // 文件描述符数组
|
|
|
+ │
|
|
|
+ ├─ [0] → file → stdin
|
|
|
+ ├─ [1] → file → stdout
|
|
|
+ ├─ [2] → file → stderr
|
|
|
+ ├─ [3] → file → /home/user/data.txt
|
|
|
+ └─ ...
|
|
|
+```
|
|
|
+
|
|
|
+**dup/dup2的实现原理**:
|
|
|
+
|
|
|
+```c
|
|
|
+// dup2(oldfd, newfd) 的核心逻辑:
|
|
|
+// 1. 检查oldfd是否有效
|
|
|
+// 2. 如果newfd已打开,先关闭它
|
|
|
+// 3. 将fd[newfd]指向与fd[oldfd]相同的file对象
|
|
|
+// 4. file对象的引用计数+1
|
|
|
+// 5. 返回newfd
|
|
|
+
|
|
|
+// 效果:两个文件描述符指向同一个file结构
|
|
|
+// 修改任一fd的偏移量会影响另一个(共享file指针)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 第四部分:系统调用
|
|
|
+
|
|
|
+#### 1. 用户态到内核态的切换
|
|
|
+
|
|
|
+**ARM的SVC指令**:
|
|
|
+
|
|
|
+```armasm
|
|
|
+; ARM64系统调用示例
|
|
|
+; 用户程序调用 read(fd, buf, count)
|
|
|
+
|
|
|
+mov x8, #63 ; 系统调用号(__NR_read = 63)
|
|
|
+mov x0, x19 ; 参数1:fd
|
|
|
+mov x1, x20 ; 参数2:buf
|
|
|
+mov x2, x21 ; 参数3:count
|
|
|
+svc #0 ; 触发异常,切换到EL1(内核态)
|
|
|
+; 内核执行sys_read(),返回值在x0中
|
|
|
+```
|
|
|
+
|
|
|
+**系统调用表sys_call_table**:
|
|
|
+
|
|
|
+```c
|
|
|
+// arch/arm64/kernel/sys.c(简化)
|
|
|
+const syscall_fn_t sys_call_table[__NR_syscalls] = {
|
|
|
+ [0] = sys_io_setup,
|
|
|
+ [1] = sys_io_destroy,
|
|
|
+ ...
|
|
|
+ [63] = sys_read,
|
|
|
+ [64] = sys_write,
|
|
|
+ ...
|
|
|
+};
|
|
|
+
|
|
|
+// 伪代码:系统调用分发
|
|
|
+void el0_svc_handler(struct pt_regs *regs) {
|
|
|
+ unsigned long syscall_no = regs->regs[7]; // R7存储调用号
|
|
|
+ unsigned long a0 = regs->regs[0]; // R0-R5存储参数
|
|
|
+ unsigned long a1 = regs->regs[1];
|
|
|
+ ...
|
|
|
+ regs->regs[0] = sys_call_table[syscall_no](a0, a1, a2, a3, a4, a5);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2. 参数传递
|
|
|
+
|
|
|
+**ARM的寄存器传参约定**:
|
|
|
+
|
|
|
+| 寄存器 | 用途 |
|
|
|
+| ------ | ----------------------- |
|
|
|
+| R0-R5 | 系统调用参数(最多6个) |
|
|
|
+| R7 | 系统调用号 |
|
|
|
+| R0 | 返回值 |
|
|
|
+
|
|
|
+```
|
|
|
+完整的系统调用流程:
|
|
|
+
|
|
|
+用户态 内核态
|
|
|
+─────────────────────────────────────────
|
|
|
+1. 设置R0-R5为参数
|
|
|
+2. 设置R7为调用号
|
|
|
+3. 执行SVC指令 ──────────→ 4. 保存用户态上下文
|
|
|
+ 5. 切换到内核栈
|
|
|
+ 6. 查sys_call_table[R7]
|
|
|
+ 7. 执行对应的sys_xxx()
|
|
|
+ 8. 结果写入R0
|
|
|
+9. 恢复用户态上下文 ←──── 10. 执行ERET返回用户态
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第五部分:为什么这样设计
|
|
|
+
|
|
|
+### 1. 为什么选择红黑树做CFS
|
|
|
+
|
|
|
+核心权衡:O(log n)的更新代价 vs O(n)扫描代价的折中。
|
|
|
+
|
|
|
+| 方案 | 找最小vruntime | 更新vruntime(调度tick) | 综合评估 |
|
|
|
+| ---------- | -------------- | ------------------------ | ---------------------------------- |
|
|
|
+| 排序链表 | O(1) | O(n) 插入排序 | 100个进程,每tick O(100),不可接受 |
|
|
|
+| 最小堆 | O(1) | O(log n) 但需要额外索引 | 需要hash表定位节点,内存开销大 |
|
|
|
+| **红黑树** | **O(log n)** | **O(log n)** | **无额外开销,常数因子小** |
|
|
|
+
|
|
|
+Linux选择红黑树还因为:内核已有成熟的红黑树实现(`lib/rbtree.c`),且红黑树的缓存局部性优于跳表。
|
|
|
+
|
|
|
+### 2. 为什么fork用COW而不是直接复制
|
|
|
+
|
|
|
+- fork后90%+的进程会立即exec(),地址空间全部被替换
|
|
|
+- 直接复制:O(n)时间 + O(n)内存,然后exec()全部丢弃
|
|
|
+- COW:O(1)时间 + O(0)额外内存(直到首次写入)
|
|
|
+- 即使不exec(),COW也只在写入时付出代价,远优于无差别复制
|
|
|
+
|
|
|
+### 3. 为什么VFS要三层抽象
|
|
|
+
|
|
|
+``
|
|
|
+问题:支持20+种文件系统,系统调用只有一套read/write/open/close
|
|
|
+
|
|
|
+解决方案:三层抽象实现解耦
|
|
|
+
|
|
|
+- inode层:屏蔽具体文件系统的元数据差异
|
|
|
+- dentry层:加速路径查找(dcache),解耦路径名和inode
|
|
|
+- file层:跟踪每次打开的状态(偏移、模式、标志)
|
|
|
+
|
|
|
+一个inode可以有多个dentry(硬链接)
|
|
|
+一个dentry可以有多个file(多次open同一文件)
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+### 4. 为什么需要伙伴系统和slab两层内存分配
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+问题:
|
|
|
+
|
|
|
+- 伙伴系统以页为最小单位(4KB),但内核大量分配几十~几百字节的小对象
|
|
|
+- 直接用伙伴系统分配4KB只用几个字节,浪费99%+
|
|
|
+
|
|
|
+解决方案(两层架构):
|
|
|
+slab/slub:
|
|
|
+
|
|
|
+- 管理小对象(几十字节~几KB)
|
|
|
+- 预分配、对象复用、类型专用缓存
|
|
|
+- 减少伙伴系统调用频率
|
|
|
+
|
|
|
+伙伴系统:
|
|
|
+
|
|
|
+- 管理大块内存(4KB~4MB)
|
|
|
+- 处理外部碎片,提供连续物理页
|
|
|
+
|
|
|
+类比:
|
|
|
+slab ≈ 仓库里的零件盒(快速取小件)
|
|
|
+伙伴系统 ≈ 物流仓库(管理大件整托盘)
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 跨学科对应表
|
|
|
+
|
|
|
+| 操作系统理论概念 | Linux内核数据结构 | Linux内核函数/机制 | 源码位置 |
|
|
|
+|------------------|-------------------|-------------------|----------|
|
|
|
+| 进程控制块(PCB) | `task_struct` | `copy_process()`, `do_fork()` | `include/linux/sched.h` |
|
|
|
+| 进程状态 | `task_struct.state` | `set_current_state()` | `include/linux/sched.h` |
|
|
|
+| 进程调度 | `rq`(运行队列), `cfs_rq` | `schedule()`, `pick_next_task()` | `kernel/sched/core.c` |
|
|
|
+| 虚拟内存 | `vm_area_struct`(VMA) | `do_mmap()`, `do_page_fault()` | `include/linux/mm_types.h` |
|
|
|
+| 页表 | `pgd_t`, `pud_t`, `pmd_t`, `pte_t` | `walk_page_range()` | `include/pgtable.h` |
|
|
|
+| 伙伴系统 | `free_area[MAX_ORDER]` | `__alloc_pages()`, `__free_pages()` | `mm/page_alloc.c` |
|
|
|
+| Slab分配器 | `kmem_cache`, `slab` | `kmem_cache_alloc()`, `kmem_cache_create()` | `mm/slub.c` |
|
|
|
+| VFS inode | `struct inode` | `iget()`, `iput()` | `include/linux/fs.h` |
|
|
|
+| VFS dentry | `struct dentry` | `d_alloc()`, `d_lookup()` | `include/linux/dcache.h` |
|
|
|
+| VFS file | `struct file` | `fget()`, `fput()` | `include/linux/fs.h` |
|
|
|
+| 系统调用 | `sys_call_table[]` | `do_sys_open()`, `sys_read()` | `arch/x86/entry/syscalls/syscall_64.tbl` |
|
|
|
+| 文件描述符 | `files_struct` → `fdtable` | `alloc_fd()`, `fd_install()` | `include/linux/fdtable.h` |
|
|
|
+| 写时复制 | 页表PTE的写保护位 | `do_wp_page()` | `mm/memory.c` |
|
|
|
+| 上下文切换 | `thread_struct`(寄存器) | `context_switch()`, `switch_to()` | `kernel/sched/core.c` |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 常见误区
|
|
|
+
|
|
|
+### 误区1:fork是完整复制父进程的内存
|
|
|
+
|
|
|
+**真相**:fork只复制task_struct和页表(元数据),物理内存通过COW延迟分配。只有当任一进程写入某页时,才真正复制该页。
|
|
|
+
|
|
|
+### 误区2:进程和线程在内核中是完全不同的东西
|
|
|
+
|
|
|
+**真相**:Linux不区分进程和线程。线程就是共享地址空间的task_struct。`clone()`系统调用通过标志位控制共享哪些资源(CLONE_VM共享内存,CLONE_FILES共享文件表等)。`pthread_create()`底层就是调用clone()。
|
|
|
+
|
|
|
+### 误区3:用户态和内核态是两套完全独立的地址空间
|
|
|
+
|
|
|
+**真相**:在32位Linux中,内核空间(1GB)映射到每个进程虚拟地址空间的高端(0xC0000000-0xFFFFFFFF)。切换到内核态不需要切换页表,只是CPU特权级从Ring3提升到Ring0,可以访问内核空间的映射。
|
|
|
+
|
|
|
+### 误区4:文件描述符是文件的标识
|
|
|
+
|
|
|
+**真相**:文件描述符是进程级别的索引号,指向该进程file对象数组的下标。同一个文件被不同进程打开,会有不同的文件描述符和不同的file对象(但共享inode和dentry)。
|
|
|
+
|
|
|
+### 误区5:CFS保证每个进程获得完全相等的CPU时间
|
|
|
+
|
|
|
+**真相**:CFS保证的是按权重比例分配CPU时间。nice值为0的进程权重1024,nice值为-20的进程权重88761。高优先级进程获得更多CPU时间,但vruntime增长更慢,不会"饿死"低优先级进程。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 面试要点
|
|
|
+
|
|
|
+### Q1:请解释fork()的写时复制机制,以及它为什么比传统fork高效?
|
|
|
+
|
|
|
+**答题要点**:
|
|
|
+- fork()只复制task_struct和页表,不复制物理内存页
|
|
|
+- 页表项标记为只读(COW位)
|
|
|
+- 当任一进程尝试写入时,触发缺页中断
|
|
|
+- 内核在缺页处理中分配新物理页,复制内容,更新页表为可写
|
|
|
+- 效率提升:O(n) → O(1),因为大部分fork后紧跟exec(),物理页从未被复制
|
|
|
+
|
|
|
+### Q2:CFS调度器的vruntime是什么?红黑树在其中扮演什么角色?
|
|
|
+
|
|
|
+**答题要点**:
|
|
|
+- vruntime是虚拟运行时间,记录进程已获得的CPU时间(加权)
|
|
|
+- nice值高的进程权重低,vruntime增长快;nice值低的进程权重高,vruntime增长慢
|
|
|
+- 调度器每次选择vruntime最小的进程运行
|
|
|
+- 红黑树按vruntime排序,左子节点vruntime最小 → 调度器只需取最左节点
|
|
|
+- 红黑树支持O(log n)插入、删除和查找最小值,适合频繁更新的场景
|
|
|
+
|
|
|
+### Q3:虚拟地址到物理地址的转换过程是什么?
|
|
|
+
|
|
|
+**答题要点**:
|
|
|
+- CPU发出虚拟地址,MMU查页表进行转换
|
|
|
+- x86_64使用四级页表:PGD→PUD→PMD→PTE
|
|
|
+- 每级页表根据虚拟地址的对应9位索引查找下一级
|
|
|
+- PTE包含物理页框号(PFN)和状态位(Present/Read-Write等)
|
|
|
+- 缺页中断:PTE的Present位为0 → 内核处理 → 分配物理页并填充PTE
|
|
|
+
|
|
|
+### Q4:VFS为什么需要inode/dentry/file三层结构?
|
|
|
+
|
|
|
+**答题要点**:
|
|
|
+- inode存储文件元数据,一个文件唯一(硬链接共享inode)
|
|
|
+- dentry建立文件名到inode的映射,加速路径查找(dcache缓存)
|
|
|
+- file记录每次打开的状态(偏移、权限、标志),支持多次open同一文件
|
|
|
+- 三层解耦:同一inode可有多个dentry(硬链接),同一dentry可有多个file(多次open)
|
|
|
+- file_operations实现多态:不同文件系统的read/write实现不同,接口统一
|
|
|
+
|
|
|
+### Q5:Linux如何区分进程和线程?clone()和fork()有什么区别?
|
|
|
+
|
|
|
+**答题要点**:
|
|
|
+- Linux不区分进程和线程,都是task_struct
|
|
|
+- fork():复制所有资源(通过COW)→ 独立进程
|
|
|
+- clone():通过标志位选择性共享 → 线程(CLONE_VM|CLONE_FS|CLONE_FILES...)
|
|
|
+- pthread_create()底层调用clone(),传入CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND
|
|
|
+- 进程切换开销大(切换页表、刷新TLB),线程切换只需切换栈和少量寄存器
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 参考资料
|
|
|
+
|
|
|
+### 相关文档
|
|
|
+- [[02-微机原理与操作系统硬件基础-原理与本质]] — CPU特权级、中断机制、地址转换的硬件基础
|
|
|
+- [[01-从数电模电到计算机系统-原理与本质]] — 计算机系统的底层基础
|
|
|
+
|
|
|
+### 内核源码目录
|
|
|
+| 目录/文件 | 内容 |
|
|
|
+|-----------|------|
|
|
|
+| `kernel/sched/` | 调度器核心代码(CFS、RT调度) |
|
|
|
+| `mm/` | 内存管理(页分配、slab、缺页处理) |
|
|
|
+| `fs/` | 文件系统(VFS、ext4、proc等) |
|
|
|
+| `include/linux/sched.h` | task_struct定义 |
|
|
|
+| `include/linux/fs.h` | VFS核心结构(inode、file、file_operations) |
|
|
|
+| `include/linux/mm_types.h` | 内存管理核心结构 |
|
|
|
+| `arch/x86/entry/` | x86系统调用入口 |
|
|
|
+| `arch/arm64/kernel/` | ARM64系统调用入口 |
|
|
|
+
|
|
|
+### 推荐阅读
|
|
|
+- 《Linux内核设计与实现》(LKD) — Robert Love,入门必读
|
|
|
+- 《深入理解Linux内核》(ULK) — 深入理解数据结构和算法
|
|
|
+- 《Linux设备驱动程序》(LDD3) — 理解内核编程实践
|
|
|
+- [LWN.net](https://lwn.net/) — Linux内核最新动态和深度分析
|
|
|
+```
|