linux php apche 吃透 Linux 内核,这 15 个关键数据结构一定要掌握
linux php apche 吃透 Linux 内核,这 15 个关键数据结构一定要掌握
很多朋友学 Linux 内核的时候,最大的感悟就是:看得懂每一行代码,却看不懂整个系统。
为什么会这样呢?因为 Linux 内核本质上就是一个巨大的数据结构集合。
调度器在管理数据结构。
内存管理在维护数据结构。
文件系统在组织数据结构。
网络协议栈更是在疯狂操作数据结构。
说得再直白一点:Linux 内核其实就是一群数据结构,加上一堆操作这些数据结构的函数。
所以今天这一篇我准备把 Linux 内核里最核心、最常见、最容易在内核源码阅读和高级面试里出现的 15 个关键数据结构,硬核总结出来给你们捋一遍。
不是为了让大家背定义,而是让你真正知道:内核到底是怎么管理进程、内存、文件和网络的。
毕竟内核代码动不动几万行,抓不住数据结构的核心,改个bug都能改到自闭,头发真的保不住啊。

一、内核基础通用数据结构(2 个)
内核所有高级功能的数据流转,都基于通用基础数据结构实现!这两个链表结构是内核的”基石工具”,贯穿内核所有模块,是所有内核开发者的入门必修课,没有任何例外。
1.1 双向循环链表

是内核里 使用频率最高的数据结构之一,进程调度队列、文件缓存、设备驱动链表……全是它。
传统的链表是在数据里塞 next、prev 指针, 的思路完全反着来——把链表节点塞进数据结构里。这种设计超级优雅——一个结构体想同时挂到多个链表里也完全没问题。
内核定义一个极其简洁的双向循环链表头:
struct list_head {
struct list_head *next, *prev;
};
光看这俩指针,你可能觉得”就这?”,但它的精髓全在 这个宏上。通过把 内嵌到你自己的结构体里,就能从链表节点反推出外层大结构的地址,操作一气呵成。
是 无锁化的设计基础,内核里各种 RCU 链表、哈希桶链表都是它的变种。它的接口也极其丰富:、、……拿来即用,不用重复造轮子。
给你们看个最经典的用法——遍历设备列表,找到对应设备:
struct my_device {
int id;
struct list_head node; // 内嵌链表节点
};
// 遍历链表
struct my_device *dev;
list_for_each_entry(dev, &device_list, node) {
if (dev->id == target_id) {
// 找到了,直接操作dev
break;
}
}
你看,根本不需要手写遍历逻辑,一个宏直接搞定。刚接触的时候可能觉得 绕脑,但等你手写几次驱动,就会发现这玩意儿简直是”内核版的乐高积木”。
新手常犯的错误是 它是双向循环链表,如果你不小心搞出死循环,内核直接当场卡死,连打印都来不及。所以刚入门的时候,一定要加遍历计数器,防止无限循环翻车。
1.2 / 哈希链表

既然有了双向链表,为什么还要搞个 hlist?
因为当哈希表(Hash Table)特别大时,如果每个哈希桶的表头都用 (占用2个指针空间),那光是空桶就会吃掉大量的内存。
如果说 是通用卡车,那 hlist 就是专为哈希表设计的轻量小跑车。
内核里的哈希表数量海了去了:页缓存(page cache)、inode 缓存、 缓存、网络连接跟踪表……几乎全用 hlist。为什么不用 ?因为哈希表的表头通常是指针数组,如果每个桶都用 ,桶头要占两个指针(next + prev),而 只用一个指针指向第一个节点,省掉了一半的内存开销。
结构体长这样:
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
注意这个 pprev,它是一个二级指针linux php apche,指向前一个节点的 next 指针的地址。这种设计让删除节点时不需要遍历找到前驱,直接通过 *pprev = next 就能把自己从链表中摘走,效率极高linux php apche,也方便 RCU 读端无锁遍历。
日常开发中,hlist 大量用在需要快速查找的场景,比如根据 inode 号快速定位 inode 缓存。面试时如果被问到”内核里哈希表怎么实现”,你能把 、pprev 的设计初衷讲清楚,基本就能拿捏住面试官了。
一个典型的内核 hlist 初始化 + 查找代码片段:
struct hlist_head hash_table[HASH_SIZE];
// 初始化每个桶的头
for (i = 0; i id == target_id)
return item;
}
hlist 的短板就是没办法直接反向遍历,且操作不当容易引发 pprev 悬空指针导致内核 panic。不过只要你老老实实用内核提供的 、,别自己瞎改指针,就没大事。
二、进程调度核心数据结构(3 个)
Linux 是分时多任务操作系统,所有进程的创建、管理、调度、切换,全靠这三个核心结构支撑,是进程子系统的骨架,面试进程调度必问,驱动多进程开发必懂。
2.1 进程控制块

是 Linux 内核里最大、最复杂、最重要的数据结构,没有之一。
内核用”任务”的概念管理所有执行流——进程、线程、内核线程全部用 表示。一个进程的 PID、状态、内存映射、文件描述符表、信号处理、调度信息……统统塞在这个大结构里。
它的体积非常庞大,在64位系统上通常有几千字节,包含数百个字段。我们不需要全记住,但要能抓住核心的几个区域:
在项目开发里,你通过 宏就能直接拿到当前进程的 。内核模块开发中,读取进程 PID、判断进程状态、遍历子进程,全靠它。
不仅字段多,还牵扯到内核栈的巧妙设计。内核为每个进程分配一个内核栈(通常 8KB 或 16KB),而 被放在栈底(或通过 关联)linux php apche 吃透 Linux 内核,这 15 个关键数据结构一定要掌握,这样内核通过栈指针就能快速计算出 地址,效率极高。
是内核 OOM 和调度延迟的敏感根源——调度器、内存回收、 等子系统几乎全在读写它,一处改错,整个内核都可能崩。
面试常问:”内核是怎么区分进程和线程的?”答案就藏在 的 、tgid 和共享资源里。
2.2 线程栈结构

虽然名气没 大,但进程切换时的 CPU 上下文,全靠它保存和恢复。
它和硬件架构强相关,x86 下结构定义在
arch/x86//asm/.h 中,保存了内核态和用户态的寄存器快照、浮点寄存器(fpu)、调试寄存器等。每次进程切换时,内核先保存当前进程的 CPU 上下文到它的 中,再从新进程的 里恢复上下文,这就是所谓的”上下文切换”。
很多人对进程切换只停留在”保存寄存器”这个笼统概念上,但实际上,FPU 状态的延迟保存(eager fpu 和 lazy fpu)优化,就是围绕 展开的。进程如果没有用过浮点指令,切换时就不必保存 FPU 上下文,能省掉大量时间。
在内核调试中,如果遇到莫名其妙的段错误或浮点计算异常,经常就是 保存不完整或恢复错误导致的。了解它的字段,对分析 crash dump 非常有帮助。
2.3 调度实体


以前的 Linux 调度器谁优先级高跑谁,后来发现不公平。于是出了 CFS(完全公平调度器),而它调度的基本单位不是进程,而是调度实体。
这是设计思想的一次飞跃:调度器不再直接对着 干活,而是抽象出一个调度实体, 里嵌着一个 。如果是组调度(),一个调度实体还能代表一组进程。
里最有名的字段就是 (虚拟运行时间)。CFS 用红黑树把所有调度实体按 排序,每次调度时挑出 最小的那个实体运行。 的增长速度受进程优先级(nice 值)的影响,优先级越高, 走得越慢,就能获得更多的 CPU 时间。
struct sched_entity {
struct load_weight load; // 负载权重
struct rb_node run_node; // 挂在CFS红黑树上的节点
u64 vruntime; // 虚拟运行时间,CFS核心排序键
// ...
};
项目里做性能优化时,如果发现某些进程饥饿,大概率是 计算出了问题,或者该进程的调度实体没被正确加入到红黑树里。理解 ,是理解 CFS 调度器内部运作的钥匙。
三、内存管理核心数据结构(3 个)
Linux 的内存管理是出了名的复杂,但只要你抓住下面三个结构,主线逻辑就瞬间清晰了。
3.1 page物理页结构体

Linux 以页为基本单位管理物理内存,每一个物理页框对应一个 page 结构体。
page 在内核里无处不在,它是 buddy 分配器、slab 分配器、页回收、页面迁移、文件缓存的基础。一个 page 既可能代表一段物理内存,也可能关联一个文件缓存页,甚至还能代表一个设备映射页,这种多义性(union 复用)让 page 结构体非常”精分”。
关键字段包括:flags(页状态标记,比如脏、锁定、回写)、(引用计数)、(关联的地址空间,用于文件页)、index(在映射中的偏移)等。
开发驱动或者优化内存时,通过 page 可以查到页面是否被锁定、是否有活跃引用,从而分析内存泄漏或高昂的缺页异常。不过要小心,page 结构本身也消耗内存,在大内存服务器上,所有 page 结构体的总大小可达 GB 级,这是物理内存的一笔固定开销。
3.2 虚拟内存区域(VMA)

进程的地址空间并不是一整块内存,而是由一段段虚拟内存区域组成的,每一段就是一个 ,简称 VMA。
每个 VMA 描述了进程地址空间里一段连续虚拟地址的属性:起始/结束地址、访问权限(可读/可写/可执行)、映射类型(文件映射/匿名映射)。当进程 mmap 一个文件,或者 一块大内存时,内核就会创建或扩展对应的 VMA。
VMA 用链表和红黑树两种方式组织: 里一个链表链接所有 VMA,同时用红黑树加速查找。当发生缺页异常时,内核必须在 VMA 树里快速定位到发生异常的地址所属的 VMA,才能决定如何处理这次缺页。
日常开发中,如果一个进程 段错误,你查看 /proc/pid/maps 看到的每一行,就是一个 VMA。有了 VMA,才实现了 、写时复制、内存锁等高级特性。
3.3 进程内存描述符

一个进程的整个虚拟地址空间linux php apche 吃透 Linux 内核,这 15 个关键数据结构一定要掌握,全封装在 里。 中的 mm 指针指向它。
里面装了进程的内存全局信息:VMA 链表头和红黑树根、页表指针(pgd)、mmap 锁、内存映射段基地址、堆栈区间、VSZ 和 RSS 统计、引用计数等。
在创建进程时,fork() 会利用写时复制(COW)复制父进程的 ;而线程则共享同一个 ,这正是线程共享地址空间的根本原因。
做内存压力分析和性能剖析时,经常需要看 中的 、、,来判断进程用了多少虚拟内存、锁定了多少物理页。如果系统 OOM, 的 也会直接影响哪个进程被优先杀掉。
四、文件系统核心数据结构(3 个)
Linux 里一切皆文件,下面这三个结构体,就是文件系统的”铁三角”,直接决定了 VFS “一切皆文件”的抽象能力。管你是磁盘上的 txt,还是鼠标、网卡、甚至是一个进程,在内核里统统被抽象成文件。
4.1 file文件结构体

file 是文件打开后的”内核级句柄”。用户态拿到的 fd,内核里就对应着一个 file 结构。
file 里记录当前读写位置(f_pos)、打开标志(,如 )、指向 的指针(里面包含 read、write、mmap 等函数指针),以及指向对应 和 inode 的指针。
一个文件可以被多次打开,每次 open 都生成一个新的 file 结构体,但多个 file 可以共享同一个 和 inode。这也是父子进程打开同一个文件时,文件偏移量相互独立的原因,因为它们各自有一个 file 结构体。
在内核态开发中,你会经常使用 打开文件,直接拿到 file 指针,然后通过 / 进行读写。但要牢记,file 结构体的引用计数必须正确管理,fget / fput 配对,否则会导致文件无法关闭甚至内核资源泄漏。
4.2 inode索引节点

inode 是文件系统里的唯一标识,代表一个真实的文件或目录。它和 file 不同:一个 inode 对应磁盘上一个实实在在的对象,而 file 只是打开该对象产生的一个访问实例。
inode 里存了文件的几乎所有元数据:文件大小、访问时间、修改时间、权限、所有者、块映射信息等。内核在读写文件时,必须先把 inode 加载到内存中,然后通过 inode 中的地址空间()来缓存文件数据。
inode 的生命周期比 file 长得多。即使文件没有被任何进程打开,inode 只要在缓存中,就能加速下一次访问。当系统内存紧张时,内核会回收未使用的 inode 缓存。做文件系统优化时,调整
/proc/sys/vm/ 可以控制 inode / 缓存的回收倾向。
4.3 目录项结构体

( entry)是 VFS 实现路径名到 inode 映射的核心结构。
Linux 的文件系统都有复杂的路径树,如果每次路径查找都从头解析,性能直接拉胯。 缓存()就是专门干这个的:内核把最近访问过的目录项组织成树状结构,每个 记录其父 、文件名、以及指向对应 inode 的指针。以后再用路径访问文件时,大部分解析工作都能在内存中的 树上完成,根本不用访问磁盘。
还可以是”负值”( ),表示查找失败的文件名,这样可以避免对不存在的文件反复向底层文件系统发查询。
一个经典的内核调试场景:文件明明存在,但 open 报 ,很可能就是 缓存出现问题,或者挂载点覆盖导致 树指向了错误位置。理解 ,才能搞懂 VFS 的 mount 、 等机制。
五、内核同步与阻塞数据结构(3 个)
内核里有很多并行的任务,抢占、中断、多核争抢随时发生。为了防止数据被写烂,或者为了让进程在没数据时乖乖睡觉,就需要这些同步与阻塞结构。
下面这三个,覆盖了 90% 的同步和阻塞场景。
5.1 等待队列

内核里进程阻塞的基石,就是等待队列。当进程需要等待某个条件(如设备可读、IO 完成、定时器到期)时,它就通过等待队列进入睡眠状态,条件满足后再被唤醒。
一个典型的等待队列使用流程长这样:
DECLARE_WAIT_QUEUE_HEAD(my_queue);
// 等待方
wait_event(my_queue, condition);
// 唤醒方
condition = 1;
wake_up(&my_queue);
看着简单,但背后涉及进程状态切换、() 调度、竞态处理等一堆细节。等待队列可以配置为互斥等待,以避免”惊群”问题(多个进程被同时唤醒,但只有一个能处理条件)。
在字符设备驱动、信号处理、 机制中,等待队列都扮演核心角色。如果你想理解阻塞 IO 和 epoll 的内部原理,先把 吃透。
5.2 信号量


信号量是是内核经典的计数同步机制,支持多个持有者。二进制信号量退化为互斥锁,计数信号量则能控制并发访问资源的最大数量。
在内核中,用 表示,通过 down() 获取(如果计数为 0 则阻塞),up() 释放。它的实现底层仍然依赖等待队列和原子操作。
信号量允许在中断上下文中使用 up() 唤醒等待者,但不允许在中断中调用可能阻塞的 down(),这点在写驱动时必须时刻警惕。常见的死锁场景就是进程持有一个信号量然后睡眠,导致其他进程永久等待。
5.3 mutex互斥锁

mutex 是信号量的轻量级替代品,专门用于一对一互斥保护。它的语义更简单:同一时间只允许一个拥有者linux php apche,不上计数,不支持递归加锁。
与自旋锁相比,mutex 在竞争时会睡眠让出 CPU,所以只能用在线程上下文(中断上下文绝不能碰 mutex)。与信号量相比,mutex 有严格的 lock owner 检查,有死锁检测()等调试支持,性能也更好。
现代内核里,大部分临界区保护都用 mutex,只有需要在中断上下文或者需要极短持锁的场景才用自旋锁。写内核代码有一个黄金法则:能用 mutex 就别用 ,语义清晰、调试友好。
六、网络栈核心数据结构(1 个)
Linux 网络性能为什么强?大名鼎鼎的 居头顶功。
6.1 网络套接字缓冲区

Linux 网络栈里最核心的数据结构,没有之一。 贯穿网络数据包的整个生命周期,从网卡驱动接收到最后交给用户态 ,全链路都是用 承载数据。
一个 不仅包含数据包本身,还携带大量元信息:头部指针(、、)、网络设备信息(dev)、套接字关联(sk)、协议类型、校验和、分段信息等。
它采用了分段存储与非线性缓冲区设计:数据存储在线性区( + data + )和分片页(frags)中,可以高效地添加/剥离协议头部,而无需复制整个数据包。这也是为什么内核网络性能如此之高的原因之一。
开发网络驱动、 钩子或者 eBPF 程序时,你几乎无处不在地和 打交道:克隆、释放、线性化、调整头部空间……一定要小心 的引用计数,一次疏忽就可能导致内存泄漏或者 free,直接内核崩溃。
七、核心数据结构选型 & 调试7.1 15 个核心结构选型对照表
唠到这儿,你一定也发现了,内核数据结构没有谁天下第一,只有在特定上下文中谁最合适:
数据结构
主要职责
所在子系统
关键设计点
典型误解/坑
通用双向循环链表,无处不在
通用
内嵌,大量宏封装
死循环、忘记用 safe
哈希链表,省内存、快速删除
通用
pprev
二级指针,省掉表头 prev
手动修改指针导致 pprev 悬空
进程/线程总控块
进程调度
字段极多, 快速获取, 区分进程/线程
混淆 tgid 和 pid,忘记 RCU 保护读取
硬件上下文保存/恢复
进程调度
架构相关,FPU 状态延迟加载
切换时 FPU 未保存导致浮点异常
CFS
调度实体, 为核心
进程调度
红黑树排序,组调度
直接修改 导致调度混乱
page
物理页框描述符
内存管理
多义性 union,引用计数
page
1. 本站所有资源来源于用户上传和网络,如有侵权请联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系站长处理!
6. 本站不售卖代码,资源标价只是站长收集整理的辛苦费!如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
7. 站长QQ号码 2205675299
资源库 - 资源分享下载网 » linux php apche 吃透 Linux 内核,这 15 个关键数据结构一定要掌握
常见问题FAQ
- 关于资源售价和售后服务的说明?
- 代码有没有售后服务和技术支持?
- 有没有搭建服务?
- 链接地址失效了怎么办?
- 关于解压密码

