mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
6037 字
16 分钟
内核架构全景
2025-01-28

你每天都在使用 Linux——敲命令、写代码、部署服务。但你是否想过:当你输入 ls 按下回车的那一刻,从键盘驱动捕获按键,到 Shell 进程被唤醒,再到文件系统遍历目录项、最终将结果渲染到终端——这短短几十毫秒之间,内核究竟做了多少事?

本章是整个系列的”地图”,不深入任何一个子系统的细节(那是后续章节的任务),而是从宏观视角俯瞰 Linux 内核的整体架构:它是如何把用户空间和内核空间隔离开的?它由哪些子系统组成?内核代码在什么上下文中执行?它使用了哪些通用数据结构?以及——它如何通过 /proc/sys 向用户空间”开口说话”?

理解了这幅全景图,后续每一章的学习就有了锚点。

一、用户空间与内核空间:一道看不见的墙#

1.1 为什么需要隔离?#

想象你住在一栋公寓楼里。你的房间是用户空间——你可以随意布置家具、看书、休息,但你不能去动大楼的供电系统、电梯控制、消防管道——那是内核空间,只有物业(内核)才有权操作。

这种隔离不是”建议”,而是 CPU 硬件强制的。x86 架构定义了多个特权级(Privilege Ring),Linux 只使用其中两个:

  • Ring 0(内核态):完全的硬件访问权限,可以执行任何指令、访问任何内存地址
  • Ring 3(用户态):受限的执行环境,不能直接访问硬件、不能访问内核内存
Note

x86 架构实际上定义了 Ring 0 ~ Ring 3 四个特权级,但 Linux 从诞生之初就只使用 Ring 0 和 Ring 3 两个级别。中间的 Ring 1 和 Ring 2 在 Linux 中从未被使用——这是”机制而非策略”哲学的体现:硬件提供了四级的机制,Linux 选择了最简的策略。

1.2 地址空间划分#

在 32 位 x86 系统上,Linux 采用经典的 3G/1G 划分

  • 虚拟地址 0x00000000 ~ 0xBFFFFFFF(低 3 GB):用户空间,每个进程独占
  • 虚拟地址 0xC0000000 ~ 0xFFFFFFFF(高 1 GB):内核空间,所有进程共享

在 64 位 x86_64 系统上,地址空间大幅扩展,但划分逻辑不变:

  • 用户空间:虚拟地址的低位部分(典型为 128 TB)
  • 内核空间:虚拟地址的高位部分(典型为 128 TB)
  • 两者之间有巨大的”空洞”(canonical hole),任何访问都会触发段错误
Warning

用户空间的代码绝对不能直接读写内核空间的内存。任何越界访问都会被 MMU(内存管理单元)拦截,CPU 产生一个页错误(Page Fault),内核随后向违规进程发送 SIGSEGV 信号——这就是你常见的”段错误(Segmentation Fault)“。

1.3 用户态与内核态的切换#

用户程序需要访问硬件资源时(读写文件、创建进程、网络通信等),必须通过系统调用(System Call) 主动陷入内核态。切换过程如下:

  1. 用户程序执行 syscall 指令(x86_64)或 int 0x80(x86 32 位)
  2. CPU 从 Ring 3 切换到 Ring 0,切换到内核栈
  3. 内核根据系统调用号查找 sys_call_table,执行对应的内核函数
  4. 执行完毕后,通过 sysretiret 返回用户态

这个切换是有代价的:涉及寄存器保存/恢复、TLB 刷新、CPU 流水线冲刷等。将在第 2 章:系统调用中详细分析这一过程的开销。

二、内核的七大子系统概览#

Linux 内核是一个庞大的工程——6.x 版本的源码已超过 3000 万行。但万变不离其宗,它由七大核心子系统组成:

graph TB subgraph 用户空间 APP[用户进程] end subgraph 内核空间 SYSCALL[系统调用接口] PROC[进程管理] MEM[内存管理] FS[文件系统] NET[网络协议栈] DRV[设备驱动] IPC[进程间通信] SEC[安全子系统] end HW[硬件] APP -->|系统调用| SYSCALL SYSCALL --> PROC SYSCALL --> MEM SYSCALL --> FS SYSCALL --> NET SYSCALL --> IPC PROC --- MEM FS --- MEM NET --- MEM PROC --- IPC SEC -.->|强制访问控制| PROC SEC -.->|文件安全| FS SEC -.->|网络过滤| NET FS --> DRV NET --> DRV DRV --> HW MEM --> DRV style SYSCALL fill:#4CAF50,color:#fff style SEC fill:#FF5722,color:#fff style HW fill:#607D8B,color:#fff

2.1 进程管理(Process Management)#

进程是操作系统资源分配的基本单位。Linux 内核的进程管理子系统负责:

  • 进程的创建与销毁fork()exec()exit() 系统调用
  • 进程调度:决定哪个进程在哪个 CPU 核心上运行、运行多久
  • 进程状态管理:运行、就绪、阻塞、僵尸等状态的转换
  • 进程间关系:父子关系、进程组、会话

核心数据结构是 task_struct(定义在 include/linux/sched.h),每个进程(和线程)都对应一个 task_struct 实例。它包含了内核管理进程所需的全部信息:PID、状态、优先级、打开的文件列表、内存描述符、信号处理等。

Note

在 Linux 中,线程和进程使用相同的 task_struct 结构——线程本质上是共享地址空间的进程。这种统一设计被称为”轻量级进程(LWP)“模型,是 Linux 内核的一大特色。

2.2 内存管理(Memory Management)#

内存管理子系统是内核中最复杂的部分之一,负责:

  • 物理内存管理:通过伙伴系统(Buddy System)和 Slub 分配器管理物理页帧
  • 虚拟内存管理:为每个进程维护独立的虚拟地址空间(mm_struct + vm_area_struct
  • 页缓存(Page Cache):缓存磁盘数据,减少 I/O 操作
  • 内存回收kswapd 内核线程在内存不足时回收页面
  • OOM Killer:内存耗尽时选择”牺牲”进程

2.3 文件系统(Filesystem)#

Linux 的文件系统子系统通过 VFS(Virtual File System,虚拟文件系统) 实现了”一切皆文件”的 Unix 哲学:

  • VFS 抽象层:定义了 super_blockinodedentryfile 四大核心对象
  • 具体文件系统:ext4、XFS、Btrfs 等磁盘文件系统;tmpfs、procfs、sysfs 等内存文件系统
  • 页缓存与脏页写回:文件 I/O 的性能优化核心

2.4 网络协议栈(Network Stack)#

Linux 内核的网络子系统实现了完整的 TCP/IP 协议栈:

  • 套接字层socket() 系统调用的内核端实现
  • 协议处理:TCP、UDP、ICMP、IP 等协议的实现
  • 数据包收发sk_buff 结构贯穿整个协议栈
  • Netfilter/iptables:网络包过滤与 NAT 框架
  • NAPI:中断与轮询结合的高性能网络驱动接口

2.5 设备驱动(Device Drivers)#

设备驱动是内核中代码量最大的部分(约占 60%+),负责与具体硬件交互:

  • 字符设备:按字节流访问(串口、终端)
  • 块设备:按块访问(硬盘、SSD)
  • 网络设备:数据包收发(网卡)
  • 设备驱动模型kobjectsysfs 提供统一的设备管理框架

2.6 进程间通信(IPC)#

Linux 支持多种 IPC 机制,满足不同场景的通信需求:

  • 管道(Pipe):父子进程间的字节流通信
  • 消息队列:System V 和 POSIX 消息队列
  • 共享内存:最快的 IPC 方式,进程直接读写同一块内存
  • 信号量:用于 IPC 的同步与互斥
  • 信号(Signal):异步通知机制
  • Unix 域套接字:本地进程间的全双工通信

2.7 安全子系统(Security)#

安全子系统贯穿内核各层,提供多层次的访问控制:

  • DAC(自主访问控制):传统的文件权限(rwx)和属主/属组
  • Capabilities:将 root 权限细分为数十种独立能力
  • LSM(Linux Security Module):安全模块框架,SELinux、AppArmor 均基于此
  • Seccomp-BPF:限制进程可使用的系统调用集合
  • Audit:安全审计框架,记录敏感操作

三、内核代码的执行上下文#

理解内核代码在什么上下文中执行,是理解内核编程约束的关键。Linux 内核代码主要在两种上下文中运行:

graph LR subgraph 进程上下文 A[用户进程] -->|系统调用| B[内核代进程执行] B -->|可睡眠| C[调度器可切换] B -->|可阻塞| D[等待 I/O 等] end subgraph 中断上下文 E[硬件中断] -->|中断处理| F[Top Half] F -->|唤醒| G[Softirq/Tasklet] G -->|延迟处理| H[Workqueue<br/>回到进程上下文] end style B fill:#4CAF50,color:#fff style F fill:#FF5722,color:#fff style G fill:#FF9800,color:#fff style H fill:#4CAF50,color:#fff

3.1 进程上下文(Process Context)#

当用户进程通过系统调用进入内核时,内核代码代表该进程执行——这就是进程上下文。此时:

  • 有明确的 current 指针,指向当前进程的 task_struct
  • 可以睡眠(调用 schedule() 让出 CPU、等待信号量等)
  • 可以访问用户空间地址(通过 copy_from_user() / copy_to_user()
  • 可以被调度器抢占(CONFIG_PREEMPT)

进程上下文是内核中最”舒适”的执行环境——你拥有进程的完整上下文信息,可以使用大部分内核 API。

3.2 中断上下文(Interrupt Context)#

当硬件中断触发时,CPU 立即打断当前执行流,跳转到中断处理函数——此时内核代码在中断上下文中执行。此时:

  • 没有进程上下文current 指针虽然存在,但不指向与中断相关的进程
  • 绝对不能睡眠:因为没有一个”进程”可以被调度出去——如果睡眠,调度器无法恢复执行
  • 不能访问用户空间:没有用户地址空间的映射
  • 执行时间必须尽可能短
Warning

在中断上下文中调用任何可能睡眠的函数(如 kmalloc(GFP_KERNEL)mutex_lock()schedule())都是严重错误,会导致内核死锁或崩溃。这是内核编程中最常见的陷阱之一。

3.3 可睡眠 vs 不可睡眠——一张速查表#

操作进程上下文中断上下文
kmalloc(GFP_KERNEL)可以禁止
kmalloc(GFP_ATOMIC)可以可以
mutex_lock()可以禁止
spin_lock()可以可以(但需谨慎)
schedule()可以禁止
copy_from_user()可以禁止
printk()可以可以
complete()可以可以

3.4 从中断上下文回到进程上下文#

Linux 内核将中断处理分为两半:

  • Top Half(上半部):在中断上下文中执行,只做最紧急的硬件操作(如从网卡 DMA 缓冲区复制数据),然后立刻唤醒下半部
  • Bottom Half(下半部):延迟处理,通过 Softirq、Tasklet 或 Workqueue 实现。其中 Workqueue 运行在内核线程中,回到了进程上下文,因此可以睡眠

这种”上半部/下半部”的分割设计,是内核在响应速度处理完整性之间的精妙平衡。

四、Linux 内核的设计哲学#

理解 Linux 内核的设计哲学,能帮助你理解很多”为什么是这样”的问题。

4.1 宏内核架构(Monolithic Kernel)#

操作系统内核的架构之争由来已久。宏内核与微内核是两种根本不同的设计理念:

维度宏内核(Linux)微内核(MINIX、seL4)
设计所有子系统运行在同一地址空间只保留最核心功能,其余作为用户态服务
性能函数直接调用,开销极小服务间通过 IPC 通信,上下文切换开销大
可靠性一个子系统崩溃,整个内核崩溃服务崩溃可重启,系统更健壮
代码量庞大(3000 万行+)精简(seL4 约 1 万行)

Linux 选择了宏内核架构,理由很务实:性能。在宏内核中,进程管理调用内存管理函数只是一个函数调用;而在微内核中,这需要一次 IPC——涉及上下文切换、消息序列化/反序列化,开销可能高出一个数量级。

description: ”### 4.2 动态加载模块——宏内核的”逃生舱”

纯宏内核的一个痛点是:所有功能编译进内核,导致内核镜像臃肿、扩展性差。Linux 通过可加载内核模块(Loadable Kernel Module, LKM) 解决了这个问题:

// 一个最简内核模块示例
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, kernel world!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, kernel world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Explorer");
MODULE_DESCRIPTION("A minimal kernel module");

模块在运行时通过 insmod 加载、rmmod 卸载,运行在内核空间,拥有完整的内核权限——这使得 Linux 兼具宏内核的性能和微内核的灵活性。设备驱动、文件系统、网络协议都可以作为模块动态加载。

4.3 “机制而非策略”#

这是 Unix/Linux 最重要的设计原则之一,Linus Torvalds 反复强调:

内核应该提供机制(Mechanism),而不是强制策略(Policy)。

具体含义:

  • 进程调度:内核提供 CFS、实时调度等多种调度器(机制),但让用户通过 nicechrtcgroup 决定如何使用(策略)
  • 文件系统:内核提供 VFS 框架(机制),但支持 ext4、XFS、Btrfs 等多种文件系统(策略由用户选择)
  • I/O 调度:内核提供 mq-deadline、bfq、kyber 等调度器(机制),用户可根据存储设备类型选择(策略)
  • 安全模块:内核提供 LSM 框架(机制),SELinux、AppArmor、Smack 是不同的安全策略实现

这种设计让 Linux 内核保持了极强的通用性——从嵌入式设备到超级计算机,同一套内核代码可以适应截然不同的使用场景。

五、内核数据结构巡礼#

Linux 内核不使用标准 C 库(glibc),因此也不能使用 glibc 提供的数据结构。内核自己实现了一套精巧、高效的通用数据结构,它们是内核各子系统的”基础设施”。

5.1 list_head——侵入式双向链表#

这是内核中使用最广泛的数据结构。与用户态链表不同,list_head 采用侵入式设计——链表节点嵌入到数据结构内部,而不是将数据结构包在链表节点里:

include/linux/list.h
struct list_head {
struct list_head *next, *prev;
};
// 使用方式:将 list_head 嵌入到自定义结构中
struct task_struct {
int pid;
char comm[16];
struct list_head tasks; // 嵌入链表节点!
struct list_head children; // 可以嵌入多个链表节点
// ... 更多字段
};

侵入式设计的优势:

  • 零额外分配:链表节点已经嵌入数据结构中,不需要额外分配内存
  • 一个对象可以同时属于多个链表:只需嵌入多个 list_head
  • 缓存友好:遍历链表时,链表节点与数据在同一个缓存行中

内核提供了丰富的链表操作宏:

// 初始化
INIT_LIST_HEAD(&task->tasks);
// 插入
list_add(&new_task->tasks, &prev_task->tasks); // 头插
list_add_tail(&new_task->tasks, &prev_task->tasks); // 尾插
// 遍历
struct task_struct *pos;
list_for_each_entry(pos, &task_list, tasks) {
printk("PID: %d\n", pos->pid);
}
// 通过链表节点获取包含它的结构体
struct task_struct *t = list_entry(ptr, struct task_struct, tasks);
Note

list_entry 宏是侵入式链表的核心魔法——它通过 container_of 宏,从结构体成员的地址反推出结构体本身的地址。container_of 是 Linux 内核中最著名的宏之一,后续你会反复遇到它。

5.2 hlist——散列表的链表#

hlist(哈希链表)是 list_head 的变体,专门用于散列表的桶链表。与 list_head 的双向循环链表不同,hlist 的设计目标是减少散列表的内存开销

include/linux/list.h
struct hlist_head {
struct hlist_node *first; // 只有一个指针!
};
struct hlist_node {
struct hlist_node *next, **pprev; // pprev 是指向前一个节点 next 字段的指针
};

为什么 hlist_head 只有一个指针?因为散列表的桶数组可能非常大(如 PID 散列表可能有数万个桶),每个桶省一个指针就能节省大量内存。pprev 是指向指针的指针——这样在删除节点时不需要判断是否是链表头,统一了操作逻辑。

5.3 rbtree——红黑树#

红黑树在内核中用于需要快速查找、插入、删除的场景,保证最坏情况下操作复杂度为 O(logn)O(\log n)

include/linux/rbtree.h
struct rb_root {
struct rb_node *rb_node;
};
struct rb_node {
unsigned long __rb_parent_color; // 父节点指针 + 颜色位(巧妙复用)
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));

内核中红黑树的典型应用:

子系统用途
进程调度(CFS)vruntime 为键组织可运行进程
内存管理管理 VMA(虚拟内存区域)
I/O 调度(CFQ)按请求扇区号排序 I/O 请求
Timer管理定时器
ext4 文件系统目录项的快速查找
Note

__rb_parent_color 字段是一个经典的内核优化——它将父节点指针和颜色位打包在一个 unsigned long 中。因为 rb_node 结构体是对齐到 sizeof(long) 的,其地址的最低两位永远是 0,所以可以用最低位来存储红黑树的颜色(0 = 红,1 = 黑)。这种”位窃取”技巧在内核中随处可见。

5.4 Radix Tree——基数树#

基数树是一种空间优化的前缀树,在内核中主要用于从整型索引快速定位指针

// include/linux/xarray.h(现代内核用 XArray 替代了 Radix Tree 的部分用途)
// 传统 Radix Tree API 仍在 lib/radix-tree.c 中可用

基数树在内核中的典型应用:

  • 页缓存(Page Cache):从文件偏移量(页索引)快速查找对应的 page 结构——这是页缓存最核心的查找结构
  • IDR 机制:分配和管理唯一的整型 ID,并将 ID 映射到指针
  • 网络子系统:路由表的快速查找

与红黑树相比,基数树在”以整数为键”的场景下更高效,因为不需要逐位比较,而是按固定步长(如 6 位)直接索引,缓存局部性更好。

六、/proc 和 /sys 文件系统:内核向用户空间暴露信息的窗口#

内核是一个”黑盒”——它的数据结构都在内核空间,用户程序无法直接访问。但系统管理员和开发者需要观察内核的运行状态。Linux 的解决方案是:用文件系统作为接口

6.1 /proc——进程与内核信息#

/proc 是一个伪文件系统(procfs),它不占用磁盘空间,文件内容由内核动态生成。主要提供两类信息:

进程相关信息(每个进程一个目录):

# 查看当前 Shell 进程的信息
ls /proc/$$/
# 输出:
# cmdline comm cwd environ exe fd
# maps mem mountinfo ns oom_score root
# sched smaps stat statm status ...
# 查看进程的命令行参数
cat /proc/$$/cmdline | tr '\0' ' '
# 输出:/bin/zsh
# 查看进程的内存映射
head -5 /proc/$$/maps
# 输出:
# 55a1c2000000-55a1c2027000 r--p 00000000 08:01 262210 /usr/bin/zsh
# 55a1c2027000-55a1c20de000 r-xp 00027000 08:01 262210 /usr/bin/zsh
# ...
# 查看进程的状态
cat /proc/$$/status | head -10
# 输出:
# Name: zsh
# State: S (sleeping)
# Tgid: 12345
# Pid: 12345
# PPid: 1
# ...

系统全局信息

# 内核版本
cat /proc/version
# 输出:Linux version 6.12.7-generic ...
# CPU 信息
cat /proc/cpuinfo | head -15
# 内存使用情况
cat /proc/meminfo | head -5
# 输出:
# MemTotal: 16384000 kB
# MemFree: 8234567 kB
# MemAvailable: 12000000 kB
# Buffers: 234567 kB
# Cached: 3456789 kB
# 中断统计
head -5 /proc/interrupts
# 文件系统挂载信息
cat /proc/mounts | head -5
# 内核命令行参数
cat /proc/cmdline

6.2 /sys——设备与驱动模型#

/syssysfs 文件系统的挂载点,它将内核的设备驱动模型以目录树的形式暴露给用户空间:

# 查看 sysfs 的顶层结构
ls /sys/
# 输出:
# block bus class dev devices firmware fs kernel module power
# 查看块设备信息
ls /sys/block/
# 输出:sda sr0 ...
# 查看某个块设备的详细信息
ls /sys/block/sda/
# 输出:
# alignment_offset capability device ext_range hidden inflight
# power queue range removable ro size slaves stat subsystem uevent
# 查看已加载的内核模块
ls /sys/module/ | head -10
# 查看某个模块的参数
ls /sys/module/nvme/parameters/
# 查看内核热插拔事件
udevadm monitor --environment

6.3 /proc 与 /sys 的分工#

维度/proc(procfs)/sys(sysfs)
定位进程信息 + 内核统计设备/驱动/总线模型
结构相对扁平,历史遗留较多严格的层次结构
规则每个 PID 一个目录 + 杂项文件按 bus/class/device 组织
写入部分文件可写(如 /proc/sys/大部分属性可读写
趋势新信息优先放 /sys内核开发者的推荐选择
Note

/proc/sys/ 目录下的文件对应 sysctl 参数——你可以通过 sysctl 命令或直接写入这些文件来在运行时修改内核参数。例如 echo 1 > /proc/sys/vm/drop_caches 可以清空页缓存,sysctl -a 可以查看所有可调参数。

七、内核启动流程概览#

从按下电源键到出现登录提示符,Linux 内核经历了一个精密的启动序列。这里给出概览,详细分析将在第 19 章:Linux 启动流程中展开。

sequenceDiagram participant BIOS as BIOS/UEFI participant Boot as Bootloader<br/>(GRUB) participant Kernel as 内核解压与初始化 participant Init as PID 1<br/>(systemd) BIOS->>Boot: POST 自检,加载引导程序 Boot->>Kernel: 加载 vmlinuz,传递内核命令行 Note over Kernel: start_kernel()<br/>→ 各子系统初始化<br/>→ rest_init() Kernel->>Init: 启动 /sbin/init (PID 1) Note over Init: systemd 读取 unit 文件<br/>挂载文件系统<br/>启动用户服务

关键步骤简述:

  1. BIOS/UEFI 阶段:硬件自检(POST),找到启动设备,加载引导程序
  2. Bootloader 阶段:GRUB 加载内核镜像(vmlinuz)和 initramfs 到内存,传递内核命令行参数
  3. 内核初始化start_kernel()(定义在 init/main.c)是内核的 C 语言入口,依次完成:
    • 架构相关初始化(setup_arch()
    • 内存管理初始化(mm_init()
    • 调度器初始化(sched_init()
    • 中断和时钟初始化
    • 设备驱动初始化
  4. rest_init():创建内核线程 kernel_init(最终执行 /sbin/init,成为 PID 1)和 kthreadd(内核线程守护者)
  5. PID 1:通常是 systemd,负责挂载最终根文件系统、启动用户空间服务

八、内核线程(kthread):内核自己的”进程”#

内核不只是被动地响应系统调用和中断——它也有自己的”后台任务”需要持续运行。这些任务以内核线程(Kernel Thread) 的形式存在。

8.1 什么是内核线程?#

内核线程是只在内核空间运行的线程——它没有用户空间地址空间(mm_structNULL 或共享 init_mm),永远在 Ring 0 执行。从调度器的角度看,内核线程和用户进程没有本质区别——它们都是 task_struct,参与同样的调度。

8.2 常见的内核线程#

# 查看系统中的内核线程(方括号标记的进程)
ps -eo pid,comm | grep '\[' | head -15
# 输出:
# 2 [kthreadd]
# 3 [rcu_gp]
# 4 [rcu_par_gp]
# 5 [slub_flushwq]
# 6 [netns]
# 8 [kworker/0:0-events]
# 10 [kworker/0:1-events]
# 11 [ksoftirqd/0]
# 12 [rcuc/0]
# 13 [migration/0]
# 14 [idle_inject/0]
# 15 [cpuhp/0]
# 17 [kdevtmpfs]
# 18 [inet_frag_wq]

几个重要的内核线程:

内核线程职责
kthreadd内核线程守护者,所有内核线程的父进程(PID 2)
ksoftirqd/N每个 CPU 一个,处理延迟的软中断
kworker/N工作队列的worker线程,执行延迟工作
kswapd内存回收守护线程,在内存不足时回收页面
jbd2/sdX-Yext4 文件系统的日志线程
migration/N负责在 CPU 间迁移进程(负载均衡)

8.3 创建内核线程#

内核通过 kthread_create()kthread_run() 创建内核线程:

include/linux/kthread.h
// 创建但不启动
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[], ...);
// 创建并启动(最常用)
struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,
const char namefmt[], ...);
// 示例:创建一个内核线程
struct task_struct *my_thread;
my_thread = kthread_run(my_thread_fn, NULL, "my_kernel_thread");
if (IS_ERR(my_thread)) {
pr_err("Failed to create kernel thread\n");
return PTR_ERR(my_thread);
}
// 线程函数的实现
static int my_thread_fn(void *data)
{
while (!kthread_should_stop()) {
// 执行内核工作...
set_current_state(TASK_INTERRUPTIBLE);
schedule(); // 主动让出 CPU,可睡眠
}
return 0;
}
// 停止内核线程
kthread_stop(my_thread);
Note

内核线程虽然运行在内核空间,但它可以睡眠——因为它有完整的 task_struct,调度器可以正常地切换它。这与中断上下文形成鲜明对比。

九、动手实践#

本章的实践操作不需要任何编程——只需一个 Linux 终端。通过这些操作,你将亲手”触摸”内核暴露给用户空间的接口。

实践 1:观察用户/内核空间划分#

# 查看内核版本和编译信息
cat /proc/version
# 查看进程的虚拟内存映射,观察用户空间和内核空间的分界
cat /proc/self/maps | tail -5
# 高地址区域就是内核空间的映射
# 查看系统的物理内存和虚拟内存配置
cat /proc/meminfo | grep -E "MemTotal|MemFree|VmallocTotal|VmallocUsed"

实践 2:探索 /proc 文件系统#

# 查看当前 Shell 进程的详细信息
ls -la /proc/$$/
# 查看进程状态
cat /proc/$$/status
# 查看进程打开的文件描述符
ls -la /proc/$$/fd/
# 查看进程的内存映射(理解 VMA)
cat /proc/$$/maps | head -10
# 查看进程的命令行参数和环境变量
cat /proc/$$/cmdline | tr '\0' ' '
cat /proc/$$/environ | tr '\0' '\n' | head -10
# 查看系统中所有进程的 PID 和命令
ls /proc/ | grep -E '^[0-9]+$' | head -10

实践 3:探索 /sys 文件系统#

# 查看 sysfs 的顶层结构
ls /sys/
# 查看块设备信息
ls /sys/block/
# 查看第一个 CPU 的信息
cat /sys/devices/system/cpu/cpu0/topology/thread_siblings_list
# 查看已加载的内核模块
ls /sys/module/ | wc -l
ls /sys/module/ | head -10
# 查看某个模块的详细信息(以 ext4 为例)
ls /sys/module/ext4/
cat /sys/module/ext4/parameters/inode_readahead_blks 2>/dev/null || echo "参数不可读"

实践 4:观察内核线程#

# 列出所有内核线程
ps -eo pid,ppid,comm | grep '\[' | head -20
# 观察 kthreadd(所有内核线程的父进程)
ps -eo pid,ppid,comm | grep kthreadd
# PID 2, PPID 0 —— kthreadd 是内核直接创建的
# 观察内核线程的树状关系
pstree -p 2 | head -20
# 查看 ksoftirqd 的运行统计
cat /proc/softirqs

实践 5:通过 sysctl 修改内核参数#

# 查看所有可调内核参数
sysctl -a | wc -l
# 查看与内存相关的参数
sysctl -a | grep vm | head -10
# 查看内核的 PID 最大值
cat /proc/sys/kernel/pid_max
# 查看文件描述符限制
cat /proc/sys/fs/file-max
# 临时修改一个内核参数(重启后失效)
sudo sysctl vm.swappiness=10
cat /proc/sys/vm/swappiness

实践 6:观察系统调用(预告第 2 章)#

# 使用 strace 跟踪一个简单命令的系统调用
strace -c ls /tmp 2>&1 | tail -20
# 观察系统调用的详细过程
strace -e trace=openat,read,write cat /proc/version

小结#

本章从宏观视角俯瞰了 Linux 内核的整体架构:

  1. 用户空间与内核空间通过 CPU 特权级(Ring 0 / Ring 3)和地址空间划分实现隔离,系统调用是两者之间的唯一桥梁
  2. 七大子系统(进程管理、内存管理、文件系统、网络协议栈、设备驱动、IPC、安全)各司其职,又相互协作
  3. 执行上下文决定了内核代码能做什么、不能做什么——进程上下文可睡眠,中断上下文绝对不能
  4. 宏内核 + 可加载模块的设计让 Linux 兼顾性能与灵活性,“机制而非策略”的哲学贯穿始终
  5. 侵入式数据结构list_headhlistrbtree、radix tree)是内核的基础设施,理解它们是阅读内核源码的前提
  6. /proc/sys 是内核向用户空间暴露信息的窗口,也是观察和调优内核的利器
  7. 内核线程是内核的”后台工人”,在进程上下文中执行内核的持续性任务

参考资料#

经典教材#

  • 《Linux 内核设计与实现》(Robert Love)第 2 章——对 Linux 内核架构的精炼概述
  • 《深入理解 Linux 内核》(Daniel P. Bovet 等)第 1 章——从体系结构角度剖析内核设计
  • 《Linux 设备驱动程序》(Jonathan Corbet 等)第 2 章——内核模块与执行上下文的权威讲解
  • 《操作系统导论》(OSTEP)(Remzi H. Arpaci-Dusseau)——建立操作系统宏观认知的最佳入门书

内核源码#

  • include/linux/sched.h —— task_struct 结构体定义
  • include/linux/list.h —— list_headhlist 的完整实现
  • include/linux/rbtree.h —— 红黑树接口定义
  • include/linux/xarray.h —— XArray(radix tree 的现代替代)
  • init/main.c —— start_kernel()rest_init(),内核的 C 语言入口
  • kernel/kthread.c —— 内核线程的创建与管理

在线资源#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

内核架构全景
https://blog.souloss.com/posts/linux-internals/kernel-architecture-overview/
作者
Souloss
发布于
2025-01-28
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时