tags: [source-summary] type: source source: "操作系统理论与Linux内核实现-原理与本质" author: "AI助手" date: 2026-09-19
本文档要解决什么问题?读完后你能回答:
锚点:task_struct 是 Linux 的 TCB(Task Control Block),定义在 include/linux/sched.h,包含进程运行所需的所有信息——PID、状态、优先级、内存映射、文件描述符表、信号处理等。它是内核中最大的数据结构之一(32 位默认配置下 1~2KB,随内核选项浮动)。
类比:task_struct 就像一个公司的完整运营手册——pid 是工号,mm 是办公空间平面图,files 是使用的工具清单,stack 是当前正在做的工作记录,sig 是紧急联络机制。
没有 task_struct,内核无法区分并发执行的多个程序。每个进程需要独立的执行状态、独立的地址空间、独立的文件访问——这些信息必须被集中管理,否则上下文切换时无法保存和恢复。
| 字段 | 含义 | 为什么存在 |
|---|---|---|
pid |
进程 ID(全局唯一) | 每个 task_struct 都有自己的 pid,线程也有独立 pid |
tgid |
线程组 ID(= 主线程 pid) | 用户态 getpid() 返回的是 tgid,用于区分进程和线程组 |
关系:主线程的 pid == tgid;同进程其他线程 pid 不同但 tgid 相同。
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§1.2
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()
| 状态 | 含义 | 能否被信号唤醒 |
|---|---|---|
TASK_RUNNING |
就绪或正在运行 | — |
TASK_INTERRUPTIBLE |
可中断睡眠(等待事件) | 可以 |
TASK_UNINTERRUPTIBLE |
不可中断睡眠(等待硬件 I/O) | 不可以 |
TASK_STOPPED |
被 SIGSTOP 暂停(调试/Ctrl+Z) | SIGCONT 可恢复 |
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§1.3
实时优先级 (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 |
struct mm_struct * |
用户空间内存映射(页表、VMA 链表) |
stack |
void * |
内核栈指针(通常 8KB/16KB),与用户栈分离 |
files |
struct files_struct * |
进程打开的文件描述符表 |
task_struct 这么大(几百个字段)是因为 Linux 需要管理的东西远比 FreeRTOS 多:完整的地址空间、文件系统、信号机制、网络协议栈、安全上下文等。FreeRTOS 的 TCB 只需要栈指针、优先级、状态和链表节点。
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
| 误区 | 正确理解 |
|---|---|
| pid 和 tgid 总是相同 | 只有主线程的 tgid 等于 pid,其他线程 tgid 相同但 pid 不同 |
| TASK_UNINTERRUPTIBLE 可被信号唤醒 | 前者不响应任何信号,后者可以被信号唤醒 |
| task_struct 在内核栈上分配 | 通过 slab 分配器从专用缓存 task_struct_cachep 分配 |
| vruntime 是 task_struct 的直接字段 | 实际在 task_struct.se.vruntime,属于 sched_entity |
锚点:传统 fork 复制整个地址空间 → COW 共享物理页 → 写入时才复制。fork 性能提升的关键是"偷懒"——能不复制就不复制。
类比:COW 就像图书馆复印——fork 时给你一张"借书证"(页表映射),你可以看所有书(读取共享页);但如果你想在书上做笔记(写入),图书馆才会给你复印一本(复制该页)。
传统 Unix 的 fork 每次都完整复制父进程地址空间。问题是:大多数 fork 后紧跟 exec,新程序会覆盖整个地址空间——复制的内存全浪费了。在内存 256MB 的嵌入式系统上,一次完整的地址空间复制代价极高。
flowchart TD
A["进程写入共享页"] --> B["CPU 发现页是只读"]
B --> C["触发 page fault"]
C --> D["内核 do_page_fault()"]
D --> E["分配新物理页 + 复制内容"]
E --> F["更新页表指向新页 + 标记可写"]
F --> G["返回用户态继续执行"]
fork 时发生了什么:
copy_page_range() 仅复制页表项(虚拟→物理映射),不复制物理页面pte_set_wrprotect())写入时发生了什么:
fork + exec 的典型场景:fork 创建子进程 → exec 加载新程序 → COW 避免了无意义的完整复制。如果子进程立即 exec,整个 COW 过程只复制了页表(几十 KB),而非整个地址空间。
用户态 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() 中的关键操作:
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); // 子进程映射同一页,只读
}
}
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§3.2
| 对比项 | fork(COW) | vfork |
|---|---|---|
| 地址空间 | 独立页表,共享物理页 | 完全共享父进程地址空间 |
| 限制 | 无特殊限制 | 子进程不能修改父进程数据 |
| 典型场景 | 需要独立执行流的场景 | fork 后立即 exec |
| 性能 | 需复制页表 | 连页表都不复制 |
glibc 的 pthread_create() 底层调用 clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, ...),线程之间共享地址空间,本质就是"不复制"。
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§3.1
| 误区 | 正确理解 |
|---|---|
| fork 后父子进程虚拟地址相同 | 虚拟地址相同,但写入后映射到不同物理页 |
| COW 在所有情况下都节省内存 | 如果 fork 后大量写入,反而多了一次复制开销 |
| vfork 比 fork 快所以应该用 vfork | vfork 限制太多(子进程不能修改数据),现代 fork 已足够快 |
| FreeRTOS 也能用 COW | FreeRTOS 没有 MMU,无法做页级保护 |
锚点:CFS(完全公平调度器)的核心是 vruntime(虚拟运行时间)。公式:vruntime += delta_exec × (NICE_0_LOAD / weight)。nice 值越低 → 权重越高 → vruntime 增长越慢 → 获得的 CPU 时间越多。
类比:vruntime 就像"排队积分"——每次轮到你办事,根据你的 VIP 等级(nice 值)消耗不同积分。VIP 等级高(nice 低)的人消耗积分慢,所以排队时间长但每次办事时间也长;普通用户消耗积分快,分到的时间短。
早期 Linux 使用 O(1) 调度器——基于位图和优先级数组,查找下一个进程 O(1)。但它有两个致命问题:(1) 无法保证公平性,交互式进程可能饿死;(2) 需要启发式判断"交互式"和"批处理",判断错误导致系统卡顿。CFS 用 vruntime 彻底消除了"猜测"——谁的 vruntime 最小,谁就运行。
vruntime 增量 = delta_exec × (NICE_0_LOAD / weight)
| 变量 | 含义 | 示例值 |
|---|---|---|
delta_exec |
本次实际运行时间 | 10ms |
NICE_0_LOAD |
nice=0 的权重(基准值) | 1024 |
weight |
当前进程的权重 | 由 nice 值通过 prio_to_weight[] 映射 |
| 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
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
rb_leftmost 指针)调度周期 (sysctl_sched_latency) = 6ms(默认)
最小粒度 (sysctl_sched_min_granularity) = 0.75ms
每个进程分配的时间片 = max(调度周期 / 可运行进程数, 最小粒度)
进程数超过 8 时,6ms / 8 = 0.75ms 触发最小粒度限制,CFS 退化为近似轮转。
Linux 内核通过调度类实现可插拔调度策略,五级调度类从高到低:
SCHED_DEADLINESCHED_FIFO / SCHED_RRSCHED_NORMAL / SCHED_BATCHpick_next_task() 从最高级调度类开始遍历,直到找到可运行的进程。这就是为什么实时进程总是抢占普通进程。
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§2.5
进程睡眠醒来后 vruntime 可能远小于当前树中其他进程的 vruntime,导致它独占 CPU。CFS 的处理:place_entity() 函数中,thresh = sysctl_sched_latency / 2(GENTLE_FAIR_SLEEPERS),将 vruntime 设为 min_vruntime - thresh,确保睡眠进程获得合理份额但不会抢占过多 CPU。
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§2.2.2 ~ §2.2.4
| 误区 | 正确理解 |
|---|---|
| vruntime 在 task_struct 直接字段中 | 在 task_struct.se.vruntime,属于 sched_entity |
| nice 值越大优先级越高 | nice 值越大优先级越低(nice=-20 最高优先级) |
| CFS 完全靠 vruntime 决策 | 还需考虑睡眠时间、唤醒抢占等因素 |
| 红黑树是 O(1) 查找 | 取最左节点 O(1),插入/删除 O(log n) |
锚点:字符设备驱动的核心是 file_operations 结构体——把内核对设备的操作(open/read/write/ioctl)映射到用户空间的系统调用。定义在 include/linux/fs.h。
类比:file_operations 就像餐厅的菜单——顾客(用户程序)点菜(系统调用),服务员(VFS)把订单传给厨房(驱动),厨房按菜单上的函数(回调)做菜(操作硬件)。
Linux "一切皆文件"——硬件设备也暴露为 /dev/ 下的文件节点。用户程序用标准 open/read/write/close 操作文件,但内核需要知道如何把这些操作翻译成对具体硬件的寄存器读写。file_operations 就是这个翻译层。
| 结构体 | 生命周期 | 存放什么 |
|---|---|---|
inode |
设备存在期间持久 | 设备全局状态(设备号、设备元数据) |
file |
每次 open 创建一个 | 单次打开的会话状态(private_data) |
设备的全局状态应放在和 inode 相关的结构中;单次打开的会话状态应放在
file.private_data中。
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 出现"]
| 步骤 | API | 作用 |
|---|---|---|
| 1 | alloc_chrdev_region |
动态分配设备号(避免冲突) |
| 2 | cdev_init |
初始化 cdev,绑定 file_operations |
| 3 | cdev_add |
注册到内核 |
| 4 | class_create |
在 /sys/class/ 下创建设备类 |
| 5 | device_create |
在 /dev/ 下自动创建设备节点 |
来源:
嵌入式Linux驱动开发实战/03-字符设备驱动核心/01-字符设备驱动基础§3.1~§3.3
| 回调 | 对应用户态 | 作用 |
|---|---|---|
open |
open() |
打开设备,初始化硬件,设置 private_data |
release |
close() |
关闭设备,释放资源 |
read |
read() |
从设备读取数据到用户空间 |
write |
write() |
从用户空间写入数据到设备 |
unlocked_ioctl |
ioctl() |
设备特定控制命令 |
poll |
select/poll/epoll |
支持 I/O 多路复用 |
mmap |
mmap() |
将设备内存映射到用户空间 |
ioctl 命令通过宏自动编码方向、数据大小、magic 和序号,避免手动编码冲突:
#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) // 从内核读数据
| 宏 | 方向 | 含义 |
|---|---|---|
_IO |
无 | 仅命令,无数据传输 |
_IOR |
读 | 从内核读数据到用户态 |
_IOW |
写 | 从用户态写数据到内核 |
_IOWR |
读写 | 双向传输 |
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
| 误区 | 正确理解 |
|---|---|
| file_operations 可被多进程共享 | 结构体本身共享,但每次 open 创建新的 file 实例 |
| 驱动中用 printf 打印调试信息 | 应该用 printk,且注意日志级别(KERN_DEBUG 等) |
| 注册设备后设备节点自动出现 | 需要手动 mknod 或使用 udev(class_create + device_create) |
| copy_to_user 返回负数表示失败 | 返回未拷贝的字节数(0 成功,非 0 失败) |
锚点:系统调用 = 用户程序通过 SVC 指令陷入内核 → 查 sys_call_table → 执行内核函数 → 返回用户态。
类比:系统调用就像去政府办事——你(用户程序)填表(系统调用号)→ 到办事大厅(SVC 陷入)→ 取号排队(查 sys_call_table)→ 窗口办理(执行内核函数)→ 拿结果回家(返回用户态)。
用户程序运行在非特权模式,无法直接操作硬件或访问内核数据。但程序需要读文件、发网络包、分配内存——这些都必须请求内核代劳。系统调用是用户态和内核态之间唯一的合法入口。
ARM Cortex-A7 使用 SVC 指令(以前叫 SWI)触发软中断:
vector_swi/* 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 快速中断 */
Cortex-A7 有 8 个异常入口,IRQ 占一个。SVC(系统调用)的向量在偏移 0x08,而 0x00 是复位向量——两者常被混为一谈。
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§4.1
sys_call_table 是系统调用号 → 函数指针的映射表:
系统调用号 0 → sys_restart_syscall
系统调用号 1 → sys_exit
系统调用号 3 → sys_read
系统调用号 4 → sys_write
...
sequenceDiagram
participant App as 用户程序
participant SVC as SVC指令
participant KStk as 内核栈
participant SCT as sys_call_table
participant KFn as 内核函数
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: 恢复用户态上下文,返回用户程序
| 栈 | 运行模式 | 大小 | 用途 |
|---|---|---|---|
| 用户栈 | USR 模式 | 用户空间 | 用户程序局部变量、函数调用 |
| 内核栈 | SVC/IRQ 模式 | 8KB/16KB | 系统调用、中断处理的内核函数调用 |
进入内核态时切换到该进程的内核栈(task_struct.stack 指向),返回用户态时切回用户栈。
来源:
嵌入式Linux内核基础/04-进程调度与中断管理§4.1
FreeRTOS 没有用户态/内核态的隔离,所有代码都在同一个特权级别运行。任务切换通过 PendSV 异常完成,没有系统调用的概念——任何函数调用都直接访问所有资源。Linux 的系统调用机制是安全隔离的基础。
| 误区 | 正确理解 |
|---|---|
| 系统调用就是普通函数调用 | 涉及模式切换、栈切换、特权级变化,开销远大于函数调用 |
| 所有系统调用都很快 | 有些系统调用可能阻塞(如 read 等待数据),进程被调度走 |
| 系统调用号是固定的 | 不同架构的系统调用号可能不同,同一架构不同内核版本也可能变化 |
| SVC 指令和 SWI 指令不同 | SVC 是 ARMv7+ 的名称,SWI 是旧名称,功能相同 |
| 维度 | 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 |
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++技术体系/字符设备驱动框架