|
|
@@ -1,591 +1,592 @@
|
|
|
---
|
|
|
-title: "操作系统理论与Linux内核实现-原理与本质"
|
|
|
-tags:
|
|
|
- - source-summary
|
|
|
+tags: [source-summary]
|
|
|
type: source
|
|
|
+source: "操作系统理论与Linux内核实现-原理与本质"
|
|
|
+author: "AI助手"
|
|
|
+date: 2026-09-19
|
|
|
created: 2026-09-19
|
|
|
-source: "深度解析系列"
|
|
|
-description: "操作系统理论概念与Linux内核源码实现的深度对应,面向初学者的原理讲解"
|
|
|
-related:
|
|
|
- - "[[02-微机原理与操作系统硬件基础-原理与本质]]"
|
|
|
- - "[[01-从数电模电到计算机系统-原理与本质]]"
|
|
|
---
|
|
|
|
|
|
-# 操作系统理论与Linux内核实现-原理与本质
|
|
|
+# 操作系统理论与Linux内核实现:原理与本质
|
|
|
|
|
|
## 核心问题
|
|
|
|
|
|
-操作系统理论和Linux内核实现之间是什么关系?理论中的概念在Linux代码中是怎么落地的?
|
|
|
+本文档要解决什么问题?读完后你能回答:
|
|
|
|
|
|
-**一句话回答**:操作系统理论定义了"做什么",Linux内核实现了"怎么做"。每个理论概念都能在内核源码中找到对应的结构体、函数和算法。
|
|
|
+- Linux 进程的"身份信息"存在哪里?每个字段解决什么问题?
|
|
|
+- fork 为什么能做到"几乎零成本"?COW 的触发条件是什么?
|
|
|
+- CFS 调度器如何量化"公平"?vruntime 的计算公式背后的物理含义是什么?
|
|
|
+- 字符设备驱动的核心数据结构如何把用户态操作翻译成硬件操作?
|
|
|
+- 系统调用从用户态到内核态经历了哪些步骤?每一步发生了什么?
|
|
|
|
|
|
----
|
|
|
+## 1. task_struct里到底装了什么
|
|
|
+
|
|
|
+### M1 锚点与类比
|
|
|
|
|
|
-## 原理讲解
|
|
|
+**锚点**:`task_struct` 是 Linux 的 TCB(Task Control Block),定义在 `include/linux/sched.h`,包含进程运行所需的所有信息——PID、状态、优先级、内存映射、文件描述符表、信号处理等。它是内核中最大的数据结构之一(32 位默认配置下 1~2KB,随内核选项浮动)。
|
|
|
|
|
|
-### 第一部分:进程管理
|
|
|
+**类比**:`task_struct` 就像一个公司的完整运营手册——`pid` 是工号,`mm` 是办公空间平面图,`files` 是使用的工具清单,`stack` 是当前正在做的工作记录,`sig` 是紧急联络机制。
|
|
|
|
|
|
-#### 1. 理论中的进程/PCB → Linux的task_struct
|
|
|
+### M2 痛点与起源
|
|
|
|
|
|
-**理论概念**:进程是程序的一次执行实例,PCB(进程控制块)是操作系统用于管理进程的数据结构。
|
|
|
+没有 `task_struct`,内核无法区分并发执行的多个程序。每个进程需要独立的执行状态、独立的地址空间、独立的文件访问——这些信息必须被集中管理,否则上下文切换时无法保存和恢复。
|
|
|
|
|
|
-**Linux实现**:`task_struct` 是Linux内核中最核心的数据结构之一,定义在 `include/linux/sched.h`。
|
|
|
+### M3 关键字段机制链
|
|
|
|
|
|
-**task_struct的核心字段**:
|
|
|
+#### pid 与 tgid:为什么需要两个 ID
|
|
|
|
|
|
-| 字段 | 类型 | 作用 |
|
|
|
-| ---------- | ----------------------- | ---------------------------- |
|
|
|
-| `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相关上下文(寄存器等) |
|
|
|
+| 字段 | 含义 | 为什么存在 |
|
|
|
+| ------ | ------------------------ | -------------------------------------------------------- |
|
|
|
+| `pid` | 进程 ID(全局唯一) | 每个 task_struct 都有自己的 pid,线程也有独立 pid |
|
|
|
+| `tgid` | 线程组 ID(= 主线程 pid)| 用户态 `getpid()` 返回的是 tgid,用于区分进程和线程组 |
|
|
|
|
|
|
-**进程状态映射**:
|
|
|
+**关系**:主线程的 `pid == tgid`;同进程其他线程 `pid` 不同但 `tgid` 相同。
|
|
|
|
|
|
-| 理论状态 | Linux宏定义 | 含义 |
|
|
|
-| ---------------- | ---------------------- | ----------------------- |
|
|
|
-| 创建 | `TASK_NEW` | 新建进程,尚未准备好 |
|
|
|
-| 就绪/运行 | `TASK_RUNNING` | 在运行队列中,可被调度 |
|
|
|
-| 阻塞(可中断) | `TASK_INTERRUPTIBLE` | 等待事件,可被信号唤醒 |
|
|
|
-| 阻塞(不可中断) | `TASK_UNINTERRUPTIBLE` | 等待I/O,不响应信号 |
|
|
|
-| 停止 | `TASK_STOPPED` | 被信号暂停(如SIGSTOP) |
|
|
|
-| 终止 | `EXIT_ZOMBIE` | 已终止,等待父进程回收 |
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §1.2
|
|
|
|
|
|
-> 关键理解:Linux把"就绪"和"运行"合并为 `TASK_RUNNING`,因为调度器决定谁真正运行,进程本身只关心"我是否可以被调度"。
|
|
|
+#### state:进程状态机
|
|
|
+
|
|
|
+```mermaid
|
|
|
+stateDiagram-v2
|
|
|
+ [*] --> TASK_RUNNING: fork()
|
|
|
+ TASK_RUNNING --> TASK_INTERRUPTIBLE: schedule()
|
|
|
+ TASK_INTERRUPTIBLE --> TASK_RUNNING: 被唤醒/信号
|
|
|
+ TASK_RUNNING --> TASK_UNINTERRUPTIBLE: 等待I/O
|
|
|
+ TASK_UNINTERRUPTIBLE --> TASK_RUNNING: 硬件就绪
|
|
|
+ TASK_RUNNING --> TASK_STOPPED: SIGSTOP
|
|
|
+ TASK_STOPPED --> TASK_RUNNING: SIGCONT
|
|
|
+ TASK_RUNNING --> EXIT_ZOMBIE: exit()
|
|
|
+ EXIT_ZOMBIE --> [*]: 父进程wait()
|
|
|
+```
|
|
|
|
|
|
-#### 2. 进程创建:fork()的写时复制(COW)机制
|
|
|
+| 状态 | 含义 | 能否被信号唤醒 |
|
|
|
+| ----------------------- | ---------------------------------- | -------------- |
|
|
|
+| `TASK_RUNNING` | 就绪或正在运行 | — |
|
|
|
+| `TASK_INTERRUPTIBLE` | 可中断睡眠(等待事件) | 可以 |
|
|
|
+| `TASK_UNINTERRUPTIBLE` | 不可中断睡眠(等待硬件 I/O) | 不可以 |
|
|
|
+| `TASK_STOPPED` | 被 SIGSTOP 暂停(调试/Ctrl+Z) | SIGCONT 可恢复 |
|
|
|
|
|
|
-**传统fork**:创建子进程时,完全复制父进程的地址空间。问题:复制开销大,且子进程通常立即调用exec(),复制的内容完全浪费。
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §1.3
|
|
|
|
|
|
-**COW fork(Linux实现)**:
|
|
|
+#### 优先级三层体系
|
|
|
|
|
|
```
|
|
|
-fork()调用流程:
|
|
|
-1. 复制task_struct(轻量)
|
|
|
-2. 复制mm_struct(轻量)
|
|
|
-3. 页表标记为只读(关键!)
|
|
|
-4. 任一进程写入时 → 缺页中断 → 真正复制该页
|
|
|
+实时优先级 (0~99) ← SCHED_FIFO / SCHED_RR
|
|
|
+ ↓
|
|
|
+普通优先级 (100~139) ← SCHED_NORMAL / SCHED_BATCH
|
|
|
+ ↓
|
|
|
+ nice -20 ~ +19 → prio 100~139
|
|
|
```
|
|
|
|
|
|
-**页表级实现原理**:
|
|
|
+| 字段 | 作用 | 转换公式 |
|
|
|
+| ------------- | ----------------------------------------- | -------------------------------- |
|
|
|
+| `static_prio` | 静态优先级(由 nice 值映射) | `120 + nice` |
|
|
|
+| `normal_prio` | 普通优先级(继承或计算得出) | 基于 `static_prio` |
|
|
|
+| `prio` | 动态优先级(调度器实际使用) | 内核可动态调整 |
|
|
|
+
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §2.3
|
|
|
+
|
|
|
+#### mm、stack、files
|
|
|
+
|
|
|
+| 字段 | 指向 | 作用 |
|
|
|
+| ------- | ---------------------- | --------------------------------------------- |
|
|
|
+| `mm` | `struct mm_struct *` | 用户空间内存映射(页表、VMA 链表) |
|
|
|
+| `stack` | `void *` | 内核栈指针(通常 8KB/16KB),与用户栈分离 |
|
|
|
+| `files` | `struct files_struct *`| 进程打开的文件描述符表 |
|
|
|
+
|
|
|
+`task_struct` 这么大(几百个字段)是因为 Linux 需要管理的东西远比 FreeRTOS 多:完整的地址空间、文件系统、信号机制、网络协议栈、安全上下文等。FreeRTOS 的 TCB 只需要栈指针、优先级、状态和链表节点。
|
|
|
+
|
|
|
+#### task_struct 完整字段分组
|
|
|
|
|
|
```c
|
|
|
-// 简化的COW逻辑(mm/memory.c)
|
|
|
-static vm_fault_t do_wp_page(struct vm_fault *vmf) {
|
|
|
- // 1. 检测到写保护页被写入
|
|
|
- // 2. 判断是否为COW页
|
|
|
- // 3. 分配新物理页
|
|
|
- // 4. 复制原页内容
|
|
|
- // 5. 更新页表映射为可写
|
|
|
- // 6. 原页引用计数减1
|
|
|
-}
|
|
|
+struct task_struct {
|
|
|
+ /* --- 标识 --- */
|
|
|
+ pid_t pid; // 进程 ID (全局唯一)
|
|
|
+ pid_t tgid; // 线程组 ID (= 主线程 PID)
|
|
|
+ char comm[TASK_COMM_LEN]; // 进程名称 (16 字节)
|
|
|
+
|
|
|
+ /* --- 状态 --- */
|
|
|
+ volatile long state; // 进程状态 (TASK_RUNNING 等)
|
|
|
+ int exit_state; // 退出状态 (EXIT_ZOMBIE / EXIT_DEAD)
|
|
|
+ unsigned int flags; // 进程标志 (PF_KTHREAD, PF_USED_MATH 等)
|
|
|
+
|
|
|
+ /* --- 调度 --- */
|
|
|
+ int prio; // 动态优先级 (0~139)
|
|
|
+ int static_prio; // 静态优先级 (由 nice 值映射)
|
|
|
+ int normal_prio; // 普通优先级
|
|
|
+ unsigned int rt_priority; // 实时优先级 (0~99)
|
|
|
+ unsigned int policy; // 调度策略 (SCHED_NORMAL / SCHED_FIFO 等)
|
|
|
+ struct sched_entity se; // CFS 调度实体(vruntime 在这里)
|
|
|
+ struct sched_rt_entity rt; // 实时调度实体
|
|
|
+
|
|
|
+ /* --- 内存 --- */
|
|
|
+ struct mm_struct *mm; // 用户空间内存描述符
|
|
|
+ struct mm_struct *active_mm; // 当前活跃的内存描述符
|
|
|
+
|
|
|
+ /* --- 栈 --- */
|
|
|
+ void *stack; // 内核栈指针 (通常 8KB / 16KB)
|
|
|
+
|
|
|
+ /* --- 家族 --- */
|
|
|
+ struct task_struct *parent; // 父进程
|
|
|
+ struct list_head children; // 子进程链表
|
|
|
+ struct task_struct *group_leader; // 线程组领头进程
|
|
|
+
|
|
|
+ /* --- 文件与信号 --- */
|
|
|
+ struct fs_struct *fs; // 当前工作目录
|
|
|
+ struct files_struct *files;// 打开的文件表
|
|
|
+ struct signal_struct *sig; // 信号信息
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-**优势**:
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §1.2
|
|
|
|
|
|
-- fork()时间从O(n)降到O(1)(n为内存页数)
|
|
|
-- 实际复制只发生在写入时
|
|
|
-- 大量fork+exec场景性能提升显著
|
|
|
+### M8 易错点
|
|
|
|
|
|
-#### 3. 进程调度:CFS(完全公平调度器)
|
|
|
+| 误区 | 正确理解 |
|
|
|
+| --------------------------------- | ----------------------------------------------------- |
|
|
|
+| pid 和 tgid 总是相同 | 只有主线程的 tgid 等于 pid,其他线程 tgid 相同但 pid 不同 |
|
|
|
+| TASK_UNINTERRUPTIBLE 可被信号唤醒 | 前者不响应任何信号,后者可以被信号唤醒 |
|
|
|
+| task_struct 在内核栈上分配 | 通过 slab 分配器从专用缓存 `task_struct_cachep` 分配 |
|
|
|
+| vruntime 是 task_struct 的直接字段 | 实际在 `task_struct.se.vruntime`,属于 `sched_entity`|
|
|
|
|
|
|
-**核心思想**:虚拟运行时间(vruntime)保证每个进程获得公平的CPU时间。
|
|
|
+---
|
|
|
|
|
|
-**vruntime的含义**:
|
|
|
+## 2. fork为什么用COW(写时复制)
|
|
|
|
|
|
-- 每个进程维护自己的vruntime
|
|
|
-- 实际运行时,vruntime按实际时间流逝增加
|
|
|
-- nice值高的进程,vruntime增长慢(权重高)
|
|
|
-- 调度器选择vruntime最小的进程运行
|
|
|
+### M1 锚点与类比
|
|
|
|
|
|
-**红黑树数据结构的选择**:
|
|
|
+**锚点**:传统 fork 复制整个地址空间 → COW 共享物理页 → 写入时才复制。fork 性能提升的关键是"偷懒"——能不复制就不复制。
|
|
|
|
|
|
-| 数据结构 | 插入/删除 | 查找最小 | 选择原因 |
|
|
|
-| ---------- | ------------ | ------------ | ---------------------------- |
|
|
|
-| 链表 | O(n) | O(1) | 删除太慢 |
|
|
|
-| 堆 | O(log n) | O(1) | 无法快速更新vruntime |
|
|
|
-| **红黑树** | **O(log n)** | **O(log n)** | **平衡:支持高效更新和查找** |
|
|
|
+**类比**:COW 就像图书馆复印——fork 时给你一张"借书证"(页表映射),你可以看所有书(读取共享页);但如果你想在书上做笔记(写入),图书馆才会给你复印一本(复制该页)。
|
|
|
|
|
|
-**nice值到权重的映射**:
|
|
|
+### M2 痛点与起源
|
|
|
|
|
|
-```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时间越多
|
|
|
+传统 Unix 的 fork 每次都完整复制父进程地址空间。问题是:大多数 fork 后紧跟 exec,新程序会覆盖整个地址空间——复制的内存全浪费了。在内存 256MB 的嵌入式系统上,一次完整的地址空间复制代价极高。
|
|
|
+
|
|
|
+### M3 COW 机制链
|
|
|
+
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["进程写入共享页"] --> B["CPU 发现页是只读"]
|
|
|
+ B --> C["触发 page fault"]
|
|
|
+ C --> D["内核 do_page_fault()"]
|
|
|
+ D --> E["分配新物理页 + 复制内容"]
|
|
|
+ E --> F["更新页表指向新页 + 标记可写"]
|
|
|
+ F --> G["返回用户态继续执行"]
|
|
|
```
|
|
|
|
|
|
----
|
|
|
+**fork 时发生了什么**:
|
|
|
+
|
|
|
+1. `copy_page_range()` 仅复制页表项(虚拟→物理映射),不复制物理页面
|
|
|
+2. 父子进程页表项都标记为只读(`pte_set_wrprotect()`)
|
|
|
+3. 物理页面引用计数 +1
|
|
|
+
|
|
|
+**写入时发生了什么**:
|
|
|
|
|
|
-### 第二部分:内存管理
|
|
|
+1. CPU 检查页表发现只读 → 触发 page fault
|
|
|
+2. 内核分配新的物理帧 → 复制原页内容 → 更新页表指向新页 → 标记可写
|
|
|
|
|
|
-#### 1. 虚拟内存的实现
|
|
|
+**fork + exec 的典型场景**:fork 创建子进程 → exec 加载新程序 → COW 避免了无意义的完整复制。如果子进程立即 exec,整个 COW 过程只复制了页表(几十 KB),而非整个地址空间。
|
|
|
|
|
|
-**页表的多级结构**:
|
|
|
+#### do_fork 流程
|
|
|
|
|
|
```
|
|
|
-虚拟地址结构(以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] → 物理页框号 + 偏移
|
|
|
+用户态 fork()/clone()
|
|
|
+ │
|
|
|
+ ▼
|
|
|
+ do_fork() // 5.9+ 内核中名为 kernel_clone()
|
|
|
+ │
|
|
|
+ ├── copy_process() // 核心:复制进程描述符
|
|
|
+ │ ├── dup_task_struct() // 复制 task_struct + 内核栈
|
|
|
+ │ ├── copy_creds() // 复制权限信息
|
|
|
+ │ ├── copy_mm() // 复制内存映射 (COW)
|
|
|
+ │ ├── copy_files() // 复制文件描述符
|
|
|
+ │ ├── copy_fs() // 复制文件系统
|
|
|
+ │ ├── copy_sighand() // 复制信号处理
|
|
|
+ │ ├── copy_signal() // 复制信号信息
|
|
|
+ │ └── sched_fork() // 初始化调度相关字段
|
|
|
+ │
|
|
|
+ └── wake_up_new_task() // 将子进程加入调度器
|
|
|
```
|
|
|
|
|
|
-**缺页中断的处理流程**:
|
|
|
+`copy_page_range()` 中的关键操作:
|
|
|
|
|
|
```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信号
|
|
|
+static int copy_page_range(...) {
|
|
|
+ /* 仅复制页表项,设置 COW 标志 */
|
|
|
+ pte_t *src_pte = pte_offset_map(...);
|
|
|
+ if (pte_present(*src_pte)) {
|
|
|
+ pte_set_wrprotect(src_pte); // 父进程页表改为只读
|
|
|
+ copy_pte(dst, src_pte); // 子进程映射同一页,只读
|
|
|
+ }
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-**页表项(PTE)的结构**:
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §3.2
|
|
|
|
|
|
-| 位 | 含义 |
|
|
|
-| --------------- | ------------------------ |
|
|
|
-| Present | 页面是否在物理内存中 |
|
|
|
-| Read/Write | 读写权限 |
|
|
|
-| User/Supervisor | 用户/内核权限 |
|
|
|
-| Accessed | 页面是否被访问过 |
|
|
|
-| Dirty | 页面是否被修改过 |
|
|
|
-| PFN | 物理页框号(bit 12以上) |
|
|
|
+#### vfork 的存在意义
|
|
|
|
|
|
-#### 2. 物理内存分配
|
|
|
+| 对比项 | fork(COW) | vfork |
|
|
|
+| ---------- | ------------------------- | -------------------------- |
|
|
|
+| 地址空间 | 独立页表,共享物理页 | 完全共享父进程地址空间 |
|
|
|
+| 限制 | 无特殊限制 | 子进程不能修改父进程数据 |
|
|
|
+| 典型场景 | 需要独立执行流的场景 | fork 后立即 exec |
|
|
|
+| 性能 | 需复制页表 | 连页表都不复制 |
|
|
|
|
|
|
-**伙伴系统(Buddy System)——解决外部碎片**:
|
|
|
+glibc 的 `pthread_create()` 底层调用 `clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, ...)`,线程之间共享地址空间,本质就是"不复制"。
|
|
|
|
|
|
-```
|
|
|
-核心思想:
|
|
|
-- 物理内存按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的伙伴
|
|
|
-```
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §3.1
|
|
|
|
|
|
-**Slab/Slub分配器——高效分配小对象**:
|
|
|
+### M8 易错点
|
|
|
|
|
|
-```
|
|
|
-为什么需要slab?
|
|
|
-- 伙伴系统最小分配4KB,但内核频繁分配task_struct(几KB)、inode等小对象
|
|
|
-- 直接用伙伴系统浪费严重(内部碎片)
|
|
|
-
|
|
|
-Slab的工作方式:
|
|
|
-1. 向伙伴系统申请一整页(object cache)
|
|
|
-2. 将页划分为固定大小的对象槽
|
|
|
-3. 维护空闲/已用对象链表
|
|
|
-4. 分配时从空闲链表取出,释放时放回
|
|
|
-5. 对象类型专用(如task_struct_cachep),避免内存碎片
|
|
|
-```
|
|
|
+| 误区 | 正确理解 |
|
|
|
+| --------------------------------- | ------------------------------------------------------- |
|
|
|
+| fork 后父子进程虚拟地址相同 | 虚拟地址相同,但写入后映射到不同物理页 |
|
|
|
+| COW 在所有情况下都节省内存 | 如果 fork 后大量写入,反而多了一次复制开销 |
|
|
|
+| vfork 比 fork 快所以应该用 vfork | vfork 限制太多(子进程不能修改数据),现代 fork 已足够快 |
|
|
|
+| FreeRTOS 也能用 COW | FreeRTOS 没有 MMU,无法做页级保护 |
|
|
|
|
|
|
-#### 3. 用户空间与内核空间
|
|
|
+---
|
|
|
|
|
|
-**32位系统的地址空间划分(3G/1G)**:
|
|
|
+## 3. CFS的vruntime为什么这样算
|
|
|
|
|
|
-```
|
|
|
-虚拟地址空间布局(32位Linux):
|
|
|
-
|
|
|
-0xFFFFFFFF ┌───────────────────┐
|
|
|
- │ 内核空间(1GB) │ ← 所有进程共享同一份内核映射
|
|
|
-0xC0000000 ├───────────────────┤
|
|
|
- │ │
|
|
|
- │ 用户空间(3GB) │ ← 每个进程独立的虚拟地址空间
|
|
|
- │ │
|
|
|
-0x00000000 └───────────────────┘
|
|
|
-
|
|
|
-64位系统(以x86_64为例):
|
|
|
-- 用户空间:0x0000000000000000 - 0x00007FFFFFFFFFFF (128TB)
|
|
|
-- 内核空间:0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF (128TB)
|
|
|
-```
|
|
|
+### M1 锚点与类比
|
|
|
|
|
|
-**关键设计**:内核空间映射到每个进程的高端地址。切换进程时不需要切换内核地址映射,因为:
|
|
|
+**锚点**:CFS(完全公平调度器)的核心是 vruntime(虚拟运行时间)。公式:`vruntime += delta_exec × (NICE_0_LOAD / weight)`。nice 值越低 → 权重越高 → vruntime 增长越慢 → 获得的 CPU 时间越多。
|
|
|
|
|
|
-- 内核空间部分在所有进程中映射相同
|
|
|
-- 用户空间部分随进程切换而改变(通过切换CR3寄存器)
|
|
|
+**类比**:vruntime 就像"排队积分"——每次轮到你办事,根据你的 VIP 等级(nice 值)消耗不同积分。VIP 等级高(nice 低)的人消耗积分慢,所以排队时间长但每次办事时间也长;普通用户消耗积分快,分到的时间短。
|
|
|
|
|
|
----
|
|
|
+### M2 痛点与起源
|
|
|
|
|
|
-### 第三部分:文件系统
|
|
|
+早期 Linux 使用 O(1) 调度器——基于位图和优先级数组,查找下一个进程 O(1)。但它有两个致命问题:(1) 无法保证公平性,交互式进程可能饿死;(2) 需要启发式判断"交互式"和"批处理",判断错误导致系统卡顿。CFS 用 vruntime 彻底消除了"猜测"——谁的 vruntime 最小,谁就运行。
|
|
|
|
|
|
-#### 1. VFS(虚拟文件系统)
|
|
|
+### M3 CFS 机制链
|
|
|
|
|
|
-**一切皆文件的设计思想**:
|
|
|
+#### vruntime 计算公式
|
|
|
|
|
|
```
|
|
|
-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()
|
|
|
+vruntime 增量 = delta_exec × (NICE_0_LOAD / weight)
|
|
|
```
|
|
|
|
|
|
-**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 → 磁盘数据块
|
|
|
+| 变量 | 含义 | 示例值 |
|
|
|
+| ------------ | ------------------------ | -------------------------- |
|
|
|
+| `delta_exec` | 本次实际运行时间 | 10ms |
|
|
|
+| `NICE_0_LOAD`| nice=0 的权重(基准值) | 1024 |
|
|
|
+| `weight` | 当前进程的权重 | 由 nice 值通过 `prio_to_weight[]` 映射 |
|
|
|
+
|
|
|
+#### nice 值与权重对照
|
|
|
+
|
|
|
+| nice 值 | 权重 | vruntime 增长速度 | 含义 |
|
|
|
+| ------- | ------ | ----------------- | ------------------------- |
|
|
|
+| -20 | 88761 | 极慢 | 最高优先级,获得最多 CPU |
|
|
|
+| -10 | 9548 | 慢 | 较高优先级 |
|
|
|
+| 0 | 1024 | 基准 | 普通进程 |
|
|
|
+| 10 | 110 | 快 | 较低优先级 |
|
|
|
+| 19 | 15 | 极快 | 最低优先级,获得最少 CPU |
|
|
|
+
|
|
|
+> nice -20 的权重是 nice 19 的约 5900 倍(88761/15)。
|
|
|
+
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §2.2.1
|
|
|
+
|
|
|
+#### 红黑树组织
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph TB
|
|
|
+ A["vruntime=500"] --> B["vruntime=300"]
|
|
|
+ A --> C["vruntime=700"]
|
|
|
+ B --> D["vruntime=200"]
|
|
|
+ B --> E["vruntime=400"]
|
|
|
+ C --> F["vruntime=600"]
|
|
|
+ C --> G["vruntime=800"]
|
|
|
+ style D fill:#4caf50,color:#fff
|
|
|
```
|
|
|
|
|
|
-**file_operations如何将系统调用连接到具体文件系统**:
|
|
|
+- **最左节点**(vruntime 最小)始终是调度器下一个选择的进程
|
|
|
+- 插入/删除 O(log n),取最左节点 O(1)(缓存 `rb_leftmost` 指针)
|
|
|
+- 数千进程时红黑树性能远优于链表(链表排序插入 O(n))
|
|
|
|
|
|
-```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,
|
|
|
- // ...
|
|
|
-};
|
|
|
+```
|
|
|
+调度周期 (sysctl_sched_latency) = 6ms(默认)
|
|
|
+最小粒度 (sysctl_sched_min_granularity) = 0.75ms
|
|
|
|
|
|
-// 调用链:
|
|
|
-// sys_read() → vfs_read() → file->f_op->read()
|
|
|
-// 不同文件系统的read实现不同,但接口统一
|
|
|
+每个进程分配的时间片 = max(调度周期 / 可运行进程数, 最小粒度)
|
|
|
```
|
|
|
|
|
|
-#### 2. 文件描述符
|
|
|
+进程数超过 8 时,`6ms / 8 = 0.75ms` 触发最小粒度限制,CFS 退化为近似轮转。
|
|
|
|
|
|
-**进程的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
|
|
|
- └─ ...
|
|
|
-```
|
|
|
+Linux 内核通过调度类实现可插拔调度策略,五级调度类从高到低:
|
|
|
|
|
|
-**dup/dup2的实现原理**:
|
|
|
+1. **stop_sched_class** — 最高优先级,用于 CPU 热插拔、迁移
|
|
|
+2. **dl_sched_class** — Deadline 调度,`SCHED_DEADLINE`
|
|
|
+3. **rt_sched_class** — 实时调度,`SCHED_FIFO` / `SCHED_RR`
|
|
|
+4. **fair_sched_class** — CFS 公平调度,`SCHED_NORMAL` / `SCHED_BATCH`
|
|
|
+5. **idle_sched_class** — 空闲任务,每个 CPU 一个 idle 线程
|
|
|
|
|
|
-```c
|
|
|
-// dup2(oldfd, newfd) 的核心逻辑:
|
|
|
-// 1. 检查oldfd是否有效
|
|
|
-// 2. 如果newfd已打开,先关闭它
|
|
|
-// 3. 将fd[newfd]指向与fd[oldfd]相同的file对象
|
|
|
-// 4. file对象的引用计数+1
|
|
|
-// 5. 返回newfd
|
|
|
-
|
|
|
-// 效果:两个文件描述符指向同一个file结构
|
|
|
-// 修改任一fd的偏移量会影响另一个(共享file指针)
|
|
|
-```
|
|
|
+`pick_next_task()` 从最高级调度类开始遍历,直到找到可运行的进程。这就是为什么实时进程总是抢占普通进程。
|
|
|
|
|
|
----
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §2.5
|
|
|
|
|
|
-### 第四部分:系统调用
|
|
|
+#### 睡眠公平性
|
|
|
|
|
|
-#### 1. 用户态到内核态的切换
|
|
|
+进程睡眠醒来后 vruntime 可能远小于当前树中其他进程的 vruntime,导致它独占 CPU。CFS 的处理:`place_entity()` 函数中,`thresh = sysctl_sched_latency / 2`(GENTLE_FAIR_SLEEPERS),将 vruntime 设为 `min_vruntime - thresh`,确保睡眠进程获得合理份额但不会抢占过多 CPU。
|
|
|
|
|
|
-**ARM的SVC指令**:
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §2.2.2 ~ §2.2.4
|
|
|
|
|
|
-```armasm
|
|
|
-; ARM64系统调用示例
|
|
|
-; 用户程序调用 read(fd, buf, count)
|
|
|
+### M8 易错点
|
|
|
|
|
|
-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中
|
|
|
-```
|
|
|
+| 误区 | 正确理解 |
|
|
|
+| --------------------------------- | ------------------------------------------------------- |
|
|
|
+| vruntime 在 task_struct 直接字段中 | 在 `task_struct.se.vruntime`,属于 `sched_entity` |
|
|
|
+| nice 值越大优先级越高 | nice 值越大优先级越低(nice=-20 最高优先级) |
|
|
|
+| CFS 完全靠 vruntime 决策 | 还需考虑睡眠时间、唤醒抢占等因素 |
|
|
|
+| 红黑树是 O(1) 查找 | 取最左节点 O(1),插入/删除 O(log n) |
|
|
|
|
|
|
-**系统调用表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,
|
|
|
- ...
|
|
|
-};
|
|
|
+## 4. 字符设备驱动的file_operations
|
|
|
|
|
|
-// 伪代码:系统调用分发
|
|
|
-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);
|
|
|
-}
|
|
|
-```
|
|
|
+### M1 锚点与类比
|
|
|
|
|
|
-#### 2. 参数传递
|
|
|
+**锚点**:字符设备驱动的核心是 `file_operations` 结构体——把内核对设备的操作(open/read/write/ioctl)映射到用户空间的系统调用。定义在 `include/linux/fs.h`。
|
|
|
|
|
|
-**ARM的寄存器传参约定**:
|
|
|
+**类比**:`file_operations` 就像餐厅的菜单——顾客(用户程序)点菜(系统调用),服务员(VFS)把订单传给厨房(驱动),厨房按菜单上的函数(回调)做菜(操作硬件)。
|
|
|
|
|
|
-| 寄存器 | 用途 |
|
|
|
-| ------ | ----------------------- |
|
|
|
-| R0-R5 | 系统调用参数(最多6个) |
|
|
|
-| R7 | 系统调用号 |
|
|
|
-| R0 | 返回值 |
|
|
|
+### M2 痛点与起源
|
|
|
|
|
|
-```
|
|
|
-完整的系统调用流程:
|
|
|
-
|
|
|
-用户态 内核态
|
|
|
-─────────────────────────────────────────
|
|
|
-1. 设置R0-R5为参数
|
|
|
-2. 设置R7为调用号
|
|
|
-3. 执行SVC指令 ──────────→ 4. 保存用户态上下文
|
|
|
- 5. 切换到内核栈
|
|
|
- 6. 查sys_call_table[R7]
|
|
|
- 7. 执行对应的sys_xxx()
|
|
|
- 8. 结果写入R0
|
|
|
-9. 恢复用户态上下文 ←──── 10. 执行ERET返回用户态
|
|
|
-```
|
|
|
+Linux "一切皆文件"——硬件设备也暴露为 `/dev/` 下的文件节点。用户程序用标准 `open/read/write/close` 操作文件,但内核需要知道如何把这些操作翻译成对具体硬件的寄存器读写。`file_operations` 就是这个翻译层。
|
|
|
|
|
|
----
|
|
|
+### M3 机制链
|
|
|
|
|
|
-## 第五部分:为什么这样设计
|
|
|
+#### file vs inode 的生命周期
|
|
|
|
|
|
-### 1. 为什么选择红黑树做CFS
|
|
|
+| 结构体 | 生命周期 | 存放什么 |
|
|
|
+| ---------- | ------------------ | -------------------------------------- |
|
|
|
+| `inode` | 设备存在期间持久 | 设备全局状态(设备号、设备元数据) |
|
|
|
+| `file` | 每次 open 创建一个 | 单次打开的会话状态(`private_data`) |
|
|
|
|
|
|
-核心权衡:O(log n)的更新代价 vs O(n)扫描代价的折中。
|
|
|
+> 设备的全局状态应放在和 inode 相关的结构中;单次打开的会话状态应放在 `file.private_data` 中。
|
|
|
|
|
|
-| 方案 | 找最小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`),且红黑树的缓存局部性优于跳表。
|
|
|
+```mermaid
|
|
|
+flowchart TD
|
|
|
+ A["alloc_chrdev_region()"] --> B["cdev_init()"]
|
|
|
+ B --> C["cdev_add()"]
|
|
|
+ C --> D["class_create()"]
|
|
|
+ D --> E["device_create()"]
|
|
|
+ E --> F["用户空间 /dev/xxx 出现"]
|
|
|
+```
|
|
|
|
|
|
-### 2. 为什么fork用COW而不是直接复制
|
|
|
+| 步骤 | API | 作用 |
|
|
|
+| ---- | --------------------- | --------------------------------- |
|
|
|
+| 1 | `alloc_chrdev_region` | 动态分配设备号(避免冲突) |
|
|
|
+| 2 | `cdev_init` | 初始化 cdev,绑定 file_operations |
|
|
|
+| 3 | `cdev_add` | 注册到内核 |
|
|
|
+| 4 | `class_create` | 在 `/sys/class/` 下创建设备类 |
|
|
|
+| 5 | `device_create` | 在 `/dev/` 下自动创建设备节点 |
|
|
|
|
|
|
-- fork后90%+的进程会立即exec(),地址空间全部被替换
|
|
|
-- 直接复制:O(n)时间 + O(n)内存,然后exec()全部丢弃
|
|
|
-- COW:O(1)时间 + O(0)额外内存(直到首次写入)
|
|
|
-- 即使不exec(),COW也只在写入时付出代价,远优于无差别复制
|
|
|
+> 来源:`嵌入式Linux驱动开发实战/03-字符设备驱动核心/01-字符设备驱动基础` §3.1~§3.3
|
|
|
|
|
|
-### 3. 为什么VFS要三层抽象
|
|
|
+#### file_operations 关键回调
|
|
|
|
|
|
-``
|
|
|
-问题:支持20+种文件系统,系统调用只有一套read/write/open/close
|
|
|
+| 回调 | 对应用户态 | 作用 |
|
|
|
+| ----------------- | ---------- | ---------------------------------------- |
|
|
|
+| `open` | `open()` | 打开设备,初始化硬件,设置 private_data |
|
|
|
+| `release` | `close()` | 关闭设备,释放资源 |
|
|
|
+| `read` | `read()` | 从设备读取数据到用户空间 |
|
|
|
+| `write` | `write()` | 从用户空间写入数据到设备 |
|
|
|
+| `unlocked_ioctl` | `ioctl()` | 设备特定控制命令 |
|
|
|
+| `poll` | `select/poll/epoll` | 支持 I/O 多路复用 |
|
|
|
+| `mmap` | `mmap()` | 将设备内存映射到用户空间 |
|
|
|
|
|
|
-解决方案:三层抽象实现解耦
|
|
|
+#### ioctl 命令编码
|
|
|
|
|
|
-- inode层:屏蔽具体文件系统的元数据差异
|
|
|
-- dentry层:加速路径查找(dcache),解耦路径名和inode
|
|
|
-- file层:跟踪每次打开的状态(偏移、模式、标志)
|
|
|
+ioctl 命令通过宏自动编码方向、数据大小、magic 和序号,避免手动编码冲突:
|
|
|
|
|
|
-一个inode可以有多个dentry(硬链接)
|
|
|
-一个dentry可以有多个file(多次open同一文件)
|
|
|
+```c
|
|
|
+#include <linux/ioctl.h>
|
|
|
|
|
|
+#define LED_MAGIC 'L'
|
|
|
+#define LED_ON _IO(LED_MAGIC, 0) // 无数据传输
|
|
|
+#define LED_OFF _IO(LED_MAGIC, 1) // 无数据传输
|
|
|
+#define LED_SET _IOW(LED_MAGIC, 2, int) // 向内核写数据
|
|
|
+#define LED_GET _IOR(LED_MAGIC, 3, int) // 从内核读数据
|
|
|
```
|
|
|
|
|
|
-### 4. 为什么需要伙伴系统和slab两层内存分配
|
|
|
-
|
|
|
+| 宏 | 方向 | 含义 |
|
|
|
+| ------- | ------ | ---------------------- |
|
|
|
+| `_IO` | 无 | 仅命令,无数据传输 |
|
|
|
+| `_IOR` | 读 | 从内核读数据到用户态 |
|
|
|
+| `_IOW` | 写 | 从用户态写数据到内核 |
|
|
|
+| `_IOWR` | 读写 | 双向传输 |
|
|
|
+
|
|
|
+#### 用户态到内核态调用链
|
|
|
+
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ participant App as 用户程序
|
|
|
+ participant VFS as VFS
|
|
|
+ participant Fops as file_operations
|
|
|
+ participant HW as 硬件
|
|
|
+
|
|
|
+ App->>VFS: read(fd, buf, cnt)
|
|
|
+ VFS->>Fops: f_op->read(filp, buf, cnt, off)
|
|
|
+ Fops->>HW: 寄存器操作 / 数据传输
|
|
|
+ HW-->>Fops: 数据
|
|
|
+ Fops-->>App: copy_to_user() 返回数据
|
|
|
```
|
|
|
|
|
|
-问题:
|
|
|
+> 来源:`嵌入式Linux驱动开发实战/03-字符设备驱动核心/01-字符设备驱动基础` §1.3~§1.4
|
|
|
|
|
|
-- 伙伴系统以页为最小单位(4KB),但内核大量分配几十~几百字节的小对象
|
|
|
-- 直接用伙伴系统分配4KB只用几个字节,浪费99%+
|
|
|
+### M8 易错点
|
|
|
|
|
|
-解决方案(两层架构):
|
|
|
-slab/slub:
|
|
|
+| 误区 | 正确理解 |
|
|
|
+| --------------------------------- | ------------------------------------------------------- |
|
|
|
+| file_operations 可被多进程共享 | 结构体本身共享,但每次 open 创建新的 file 实例 |
|
|
|
+| 驱动中用 printf 打印调试信息 | 应该用 printk,且注意日志级别(KERN_DEBUG 等) |
|
|
|
+| 注册设备后设备节点自动出现 | 需要手动 mknod 或使用 udev(class_create + device_create)|
|
|
|
+| copy_to_user 返回负数表示失败 | 返回未拷贝的字节数(0 成功,非 0 失败) |
|
|
|
|
|
|
-- 管理小对象(几十字节~几KB)
|
|
|
-- 预分配、对象复用、类型专用缓存
|
|
|
-- 减少伙伴系统调用频率
|
|
|
+---
|
|
|
|
|
|
-伙伴系统:
|
|
|
+## 5. 系统调用怎么从用户态到内核态
|
|
|
|
|
|
-- 管理大块内存(4KB~4MB)
|
|
|
-- 处理外部碎片,提供连续物理页
|
|
|
+### M1 锚点与类比
|
|
|
|
|
|
-类比:
|
|
|
-slab ≈ 仓库里的零件盒(快速取小件)
|
|
|
-伙伴系统 ≈ 物流仓库(管理大件整托盘)
|
|
|
+**锚点**:系统调用 = 用户程序通过 SVC 指令陷入内核 → 查 `sys_call_table` → 执行内核函数 → 返回用户态。
|
|
|
|
|
|
-```
|
|
|
+**类比**:系统调用就像去政府办事——你(用户程序)填表(系统调用号)→ 到办事大厅(SVC 陷入)→ 取号排队(查 sys_call_table)→ 窗口办理(执行内核函数)→ 拿结果回家(返回用户态)。
|
|
|
|
|
|
----
|
|
|
+### M2 痛点与起源
|
|
|
|
|
|
-## 跨学科对应表
|
|
|
-
|
|
|
-| 操作系统理论概念 | 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` |
|
|
|
+用户程序运行在非特权模式,无法直接操作硬件或访问内核数据。但程序需要读文件、发网络包、分配内存——这些都必须请求内核代劳。系统调用是用户态和内核态之间唯一的合法入口。
|
|
|
|
|
|
----
|
|
|
+### M3 机制链
|
|
|
|
|
|
-## 常见误区
|
|
|
+#### ARM 的 SVC 指令
|
|
|
|
|
|
-### 误区1:fork是完整复制父进程的内存
|
|
|
+ARM Cortex-A7 使用 SVC 指令(以前叫 SWI)触发软中断:
|
|
|
|
|
|
-**真相**:fork只复制task_struct和页表(元数据),物理内存通过COW延迟分配。只有当任一进程写入某页时,才真正复制该页。
|
|
|
+1. CPU 从用户模式(USR)切换到管理模式(SVC)
|
|
|
+2. CPSR 保存到 SPSR_svc
|
|
|
+3. 返回地址保存到 LR_svc
|
|
|
+4. 跳转到异常向量表偏移 0x08 处的 `vector_swi`
|
|
|
|
|
|
-### 误区2:进程和线程在内核中是完全不同的东西
|
|
|
+#### 异常向量表布局
|
|
|
|
|
|
-**真相**:Linux不区分进程和线程。线程就是共享地址空间的task_struct。`clone()`系统调用通过标志位控制共享哪些资源(CLONE_VM共享内存,CLONE_FILES共享文件表等)。`pthread_create()`底层就是调用clone()。
|
|
|
+```asm
|
|
|
+/* arch/arm/kernel/entry-armv.S(Linux 4.1) */
|
|
|
+ .section .vectors, "ax", %progbits
|
|
|
+__vectors_start:
|
|
|
+ W(b) vector_rst /* 0x00: 复位 Reset */
|
|
|
+ W(b) vector_und /* 0x04: 未定义指令 */
|
|
|
+ W(ldr) pc, __vectors_start + 0x1000 /* 0x08: SWI/SVC */
|
|
|
+ W(b) vector_pabt /* 0x0C: 指令预取中止 */
|
|
|
+ W(b) vector_dabt /* 0x10: 数据访问中止 */
|
|
|
+ W(b) vector_addrexcptn /* 0x14: 保留 */
|
|
|
+ W(b) vector_irq /* 0x18: IRQ 中断 */
|
|
|
+ W(b) vector_fiq /* 0x1C: FIQ 快速中断 */
|
|
|
+```
|
|
|
|
|
|
-### 误区3:用户态和内核态是两套完全独立的地址空间
|
|
|
+> Cortex-A7 有 8 个异常入口,IRQ 占一个。SVC(系统调用)的向量在偏移 0x08,而 0x00 是复位向量——两者常被混为一谈。
|
|
|
|
|
|
-**真相**:在32位Linux中,内核空间(1GB)映射到每个进程虚拟地址空间的高端(0xC0000000-0xFFFFFFFF)。切换到内核态不需要切换页表,只是CPU特权级从Ring3提升到Ring0,可以访问内核空间的映射。
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §4.1
|
|
|
|
|
|
-### 误区4:文件描述符是文件的标识
|
|
|
+#### 系统调用表
|
|
|
|
|
|
-**真相**:文件描述符是进程级别的索引号,指向该进程file对象数组的下标。同一个文件被不同进程打开,会有不同的文件描述符和不同的file对象(但共享inode和dentry)。
|
|
|
+`sys_call_table` 是系统调用号 → 函数指针的映射表:
|
|
|
|
|
|
-### 误区5:CFS保证每个进程获得完全相等的CPU时间
|
|
|
+```
|
|
|
+系统调用号 0 → sys_restart_syscall
|
|
|
+系统调用号 1 → sys_exit
|
|
|
+系统调用号 3 → sys_read
|
|
|
+系统调用号 4 → sys_write
|
|
|
+...
|
|
|
+```
|
|
|
|
|
|
-**真相**:CFS保证的是按权重比例分配CPU时间。nice值为0的进程权重1024,nice值为-20的进程权重88761。高优先级进程获得更多CPU时间,但vruntime增长更慢,不会"饿死"低优先级进程。
|
|
|
+#### 参数传递与返回值
|
|
|
|
|
|
----
|
|
|
+- 参数通过 R0-R5 传递最多 6 个参数
|
|
|
+- 系统调用号通过 R7 传递(ARM)
|
|
|
+- 返回值在 R0
|
|
|
|
|
|
-## 面试要点
|
|
|
+#### 完整流程
|
|
|
|
|
|
-### Q1:请解释fork()的写时复制机制,以及它为什么比传统fork高效?
|
|
|
+```mermaid
|
|
|
+sequenceDiagram
|
|
|
+ participant App as 用户程序
|
|
|
+ participant SVC as SVC指令
|
|
|
+ participant KStk as 内核栈
|
|
|
+ participant SCT as sys_call_table
|
|
|
+ participant KFn as 内核函数
|
|
|
|
|
|
-**答题要点**:
|
|
|
-- fork()只复制task_struct和页表,不复制物理内存页
|
|
|
-- 页表项标记为只读(COW位)
|
|
|
-- 当任一进程尝试写入时,触发缺页中断
|
|
|
-- 内核在缺页处理中分配新物理页,复制内容,更新页表为可写
|
|
|
-- 效率提升:O(n) → O(1),因为大部分fork后紧跟exec(),物理页从未被复制
|
|
|
+ App->>SVC: SVC #0(系统调用号→R7)
|
|
|
+ SVC->>KStk: 保存用户态上下文(R0-R15, CPSR)到内核栈
|
|
|
+ KStk->>SCT: 用 R7 索引 sys_call_table
|
|
|
+ SCT->>KFn: 调用 sys_read/sys_write/...
|
|
|
+ KFn-->>KStk: 执行完毕,返回值→R0
|
|
|
+ KStk-->>App: 恢复用户态上下文,返回用户程序
|
|
|
+```
|
|
|
|
|
|
-### Q2:CFS调度器的vruntime是什么?红黑树在其中扮演什么角色?
|
|
|
+#### 内核栈与用户栈切换
|
|
|
|
|
|
-**答题要点**:
|
|
|
-- vruntime是虚拟运行时间,记录进程已获得的CPU时间(加权)
|
|
|
-- nice值高的进程权重低,vruntime增长快;nice值低的进程权重高,vruntime增长慢
|
|
|
-- 调度器每次选择vruntime最小的进程运行
|
|
|
-- 红黑树按vruntime排序,左子节点vruntime最小 → 调度器只需取最左节点
|
|
|
-- 红黑树支持O(log n)插入、删除和查找最小值,适合频繁更新的场景
|
|
|
+| 栈 | 运行模式 | 大小 | 用途 |
|
|
|
+| --------- | ---------- | -------- | ------------------------ |
|
|
|
+| 用户栈 | USR 模式 | 用户空间 | 用户程序局部变量、函数调用 |
|
|
|
+| 内核栈 | SVC/IRQ 模式 | 8KB/16KB | 系统调用、中断处理的内核函数调用 |
|
|
|
|
|
|
-### Q3:虚拟地址到物理地址的转换过程是什么?
|
|
|
+进入内核态时切换到该进程的内核栈(`task_struct.stack` 指向),返回用户态时切回用户栈。
|
|
|
|
|
|
-**答题要点**:
|
|
|
-- CPU发出虚拟地址,MMU查页表进行转换
|
|
|
-- x86_64使用四级页表:PGD→PUD→PMD→PTE
|
|
|
-- 每级页表根据虚拟地址的对应9位索引查找下一级
|
|
|
-- PTE包含物理页框号(PFN)和状态位(Present/Read-Write等)
|
|
|
-- 缺页中断:PTE的Present位为0 → 内核处理 → 分配物理页并填充PTE
|
|
|
+> 来源:`嵌入式Linux内核基础/04-进程调度与中断管理` §4.1
|
|
|
|
|
|
-### Q4:VFS为什么需要inode/dentry/file三层结构?
|
|
|
+#### 与 FreeRTOS 的区别
|
|
|
|
|
|
-**答题要点**:
|
|
|
-- inode存储文件元数据,一个文件唯一(硬链接共享inode)
|
|
|
-- dentry建立文件名到inode的映射,加速路径查找(dcache缓存)
|
|
|
-- file记录每次打开的状态(偏移、权限、标志),支持多次open同一文件
|
|
|
-- 三层解耦:同一inode可有多个dentry(硬链接),同一dentry可有多个file(多次open)
|
|
|
-- file_operations实现多态:不同文件系统的read/write实现不同,接口统一
|
|
|
+FreeRTOS 没有用户态/内核态的隔离,所有代码都在同一个特权级别运行。任务切换通过 PendSV 异常完成,没有系统调用的概念——任何函数调用都直接访问所有资源。Linux 的系统调用机制是安全隔离的基础。
|
|
|
|
|
|
-### Q5:Linux如何区分进程和线程?clone()和fork()有什么区别?
|
|
|
+### M8 易错点
|
|
|
|
|
|
-**答题要点**:
|
|
|
-- Linux不区分进程和线程,都是task_struct
|
|
|
-- fork():复制所有资源(通过COW)→ 独立进程
|
|
|
-- clone():通过标志位选择性共享 → 线程(CLONE_VM|CLONE_FS|CLONE_FILES...)
|
|
|
-- pthread_create()底层调用clone(),传入CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND
|
|
|
-- 进程切换开销大(切换页表、刷新TLB),线程切换只需切换栈和少量寄存器
|
|
|
+| 误区 | 正确理解 |
|
|
|
+| --------------------------------- | ------------------------------------------------------- |
|
|
|
+| 系统调用就是普通函数调用 | 涉及模式切换、栈切换、特权级变化,开销远大于函数调用 |
|
|
|
+| 所有系统调用都很快 | 有些系统调用可能阻塞(如 read 等待数据),进程被调度走 |
|
|
|
+| 系统调用号是固定的 | 不同架构的系统调用号可能不同,同一架构不同内核版本也可能变化 |
|
|
|
+| SVC 指令和 SWI 指令不同 | SVC 是 ARMv7+ 的名称,SWI 是旧名称,功能相同 |
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 参考资料
|
|
|
-
|
|
|
-### 相关文档
|
|
|
-- [[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内核最新动态和深度分析
|
|
|
+## 总结:Linux内核的设计特点 vs FreeRTOS
|
|
|
+
|
|
|
+| 维度 | FreeRTOS | Linux |
|
|
|
+| ---------- | --------------------------------- | ---------------------------------------- |
|
|
|
+| 有无 MMU | 无 | 有 |
|
|
|
+| 调度器 | 位图 + O(1) 查找 | CFS 红黑树 + vruntime |
|
|
|
+| 内存管理 | heap_1~5(静态分配/简单堆) | 伙伴系统 + slab + COW |
|
|
|
+| 上下文切换 | PendSV | 软中断 / schedule() |
|
|
|
+| IPC | 队列 / 信号量 / 任务通知 | 管道 / 共享内存 / 消息队列 / 信号 / socket |
|
|
|
+| 地址空间 | 所有任务共享同一地址空间 | 每个进程独立地址空间(mm_struct) |
|
|
|
+| 驱动模型 | 无统一框架,直接操作寄存器 | 字符设备 / 块设备 / 网络设备 + VFS |
|
|
|
+| 系统调用 | 无(所有代码同一特权级) | SVC 指令 + sys_call_table + 模式切换 |
|
|
|
+| 典型开销 | 上下文切换 < 10μs | 上下文切换 数百μs ~ 数ms |
|
|
|
+| 适用场景 | 确定性实时、资源受限 MCU | 通用计算、丰富外设、网络、GUI |
|
|
|
+
|
|
|
+```mermaid
|
|
|
+graph LR
|
|
|
+ subgraph FreeRTOS
|
|
|
+ A1["位图调度 O(1)"] --> A2["heap_1~5"]
|
|
|
+ A2 --> A3["无MMU共享地址空间"]
|
|
|
+ A3 --> A4["PendSV切换"]
|
|
|
+ end
|
|
|
+ subgraph Linux
|
|
|
+ B1["CFS红黑树+vruntime"] --> B2["伙伴系统+slab"]
|
|
|
+ B2 --> B3["每进程独立地址空间"]
|
|
|
+ B3 --> B4["SVC系统调用隔离"]
|
|
|
+ end
|
|
|
+ style A1 fill:#e8f5e9
|
|
|
+ style B1 fill:#e1f5fe
|
|
|
```
|
|
|
+
|
|
|
+> 来源:综合 `嵌入式Linux内核基础/04-进程调度与中断管理`、`FreeRTOS学习笔记/02-任务调度与状态管理`、`Linux+C++技术体系/字符设备驱动框架`
|