ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核五大子系统详解:从openEuler视角构建整体认知

Linux内核五大子系统详解:从openEuler视角构建整体认知 1. 从 openEuler 出发重新认识 Linux 内核的“骨架”写这个系列的时候我一直在想一个问题为什么很多朋友学内核啃了几个月《深入Linux内核架构》最后还是觉得 Linux 像一团迷雾——每个机制都认识但连不成一个整体后来我想明白了问题不是大家不够努力而是缺少一张“地图”。Linux 内核几千万行代码如果你不知道它由哪几块构成、每块解决什么问题、块与块之间怎么协作那你读源码的时候就只有“树木”没有“森林”。这也是我写 openEuler 内核解读系列的初衷从一个国产主流发行版的实际内核出发把 Linux 内核的骨架讲清楚再慢慢往里面填血肉。openEuler 的内核版本跟随上游非常紧目前常用的 5.10、6.6 内核都属于当前维护的主力版本代码结构与上游 Linux 基本一致所以你拿着 openEuler 的源码学到的知识放到任何主流 Linux 发行版上都是通用的。这篇是第四篇我给它的定位是“总览中的总览”——先把 Linux 内核的五大子系统梳理明白。哪五个进程管理、内存管理、文件系统、网络协议栈、设备驱动。这不是官方给出的孤本分类而是业界解读内核时最经典、最适合作为学习路线的切分方式。把五个子系统的职责、核心数据结构、互相之间的接口抓住后续你再去看调度器、看 slab、看 VFS、看 netfilter、看驱动模型都会有一个明确的坐标系。这一篇适合什么读者如果你已经会写 Linux 下的 C 代码了解系统调用、中断、内核态用户态这些基本概念但还没有系统梳理过内核的整体框架或者你工作中遇到性能问题能从 top、vmstat 看到现象但说不清楚现象背后是哪一段内核代码在扛活——那这篇文章就是给你准备的。2. 五大子系统划分的内在逻辑2.1 为什么是这五个不是四个也不是六个要理解这个划分不妨想一个问题操作系统存在的意义是什么往大了说是管理软硬件资源、给上层应用提供稳定高效的运行环境。拆细一点它必须处理五件躲不开的事第一CPU 怎么分配。系统里同时跑着几百上千个线程谁先用 CPU、用多久、切换的时候现场怎么保存这必须有一个专门的角色负责。这就是进程管理子系统核心对象是 task_struct核心机制是调度器、进程切换、进程通信。第二内存怎么分配。每个进程都觉得自己拥有连续的、私有的地址空间但物理内存就那么几条 DIMM还要被所有进程共享。谁来负责地址映射、谁来决定缺页时怎么处理、谁来做页换出换入这就是内存管理子系统核心对象是 mm_struct、page、zone核心机制是缺页异常、页面回收、slab 分配器。第三数据怎么存储。文件不是磁盘上的裸扇区而是一套有目录、有权限、有缓存、有日志的抽象。你在应用层调用 read() 读一个文件从虚拟文件系统VFS到具体文件系统 ext4再到块设备层每一层都要有人接管。这就是文件系统子系统核心对象是 inode、dentry、file、super_block核心机制是页缓存、VFS 四件套。第四数据怎么传输。网络数据从网卡进来要经过协议栈层层解包再从 socket 交给应用发出去要层层封装。这就是网络协议栈核心对象是 sk_buff、sock核心机制是 TCP/IP 协议处理、NAPI、netfilter。第五硬件怎么接入。键盘、鼠标、显卡、NVMe 盘、USB 设备它们千奇百怪内核不可能为每个设备写死代码所以必须有一套统一设备模型让驱动可以“插拔式”地挂进内核。这就是设备驱动子系统核心对象是 device、driver、bus核心机制是 platform 总线、设备树、中断处理。你看这五项正好对应了一台电脑“算、存、传、收发、接外设”的五大职能。每一个子系统都庞大到值得用一本书去讲但它们的边界非常清晰。学习的时候按子系统逐个击破不会产生“看 A 的时候惦记 B”的割裂感。2.2 openEuler 对子系统划分的影响桌面与服务器内核的视角如果你接触过 openEuler会发现它的内核配置和桌面发行版有明显差异。原因在于openEuler 主要面向服务器和云基础设施场景它的内核在配置阶段就砍掉了很多桌面相关驱动打开了更多服务器特性。比如更注重 NUMA 调度优化、CPU 隔离、内存热插拔、容器隔离相关的 cgroup 能力。这意味着用 openEuler 学内核有一个天然好处你看到的内核配置相对“干净”很多嵌入式或桌面场景的旁支被收敛了更容易抓住主干。虽然五大子系统是 Linux 内核通用的骨架但 openEuler 的选择会让你在看代码时少碰到“岔路”。顺便说一句我建议初学者直接看 openEuler 提供的内核源码 RPM 包解压后的代码而不是在 GitHub 上乱逛。原因是发行版的内核源码带有完整的 config 文件你能对照自己的运行内核查看实际编译选项这样学习时是“对着自己的系统学”而不是对着“某个陌生人的内核”学。3. 子系统之外所有机制的“地基”思维在逐个拆开五大子系统之前有一个比具体子系统更底层的概念需要先铺垫——中断。因为无论是进程切换、缺页处理、网卡收包还是磁盘请求完成最终都是靠中断来“叫醒”内核的。中断处理分上半部和下半部上半部要求快只做必要登记下半部softirq、tasklet、workqueue处理真正耗时的工作。五大子系统几乎每一个都离不开中断的配合。有这个概念打底你去读任何子系统的代码都会习惯性地先问一句这里是中断上下文吗能睡眠吗能拿锁吗这三个问题其实是内核开发者每天都会问自己的问题。4. 第一大子系统进程管理——一切运行的起点4.1 task_struct 与“进程就是一棵树”进程管理最核心的数据结构是 task_struct常被称为进程描述符。它大到什么程度在 6.6 内核里task_struct 的字段数以百计包含进程状态、调度信息、打开的文件表、内存描述符、信号处理、时间统计、cgroup 关联等等。从面向对象的角度理解task_struct 就是进程这个“对象”的全部状态。Linux 里的进程不是散落的而是有严格的树形关系每个进程都有父进程init 进程PID 1是树根用 fork() 创建子进程时子进程复制父进程的大部分内容再通过 execve() 加载新程序映像。内核里维护着这样一棵进程树ps 命令展示的 PPID 列就是这棵树的直接体现。调度器方面openEuler 5.10 内核使用的还是 CFS 调度器其核心思想是用 vruntime虚拟运行时间来衡量每个进程的运行“代价”调度器总是选择 vruntime 最小的进程运行。openEuler 6.6 内核跟进了上游新的 EEVDF 调度算法原理从维护一个红黑树变成了维护一个按虚拟 deadline 排序的队列更适合现代混合负载场景。此外 openEuler 自己还维护了 AMP 调度器特性用于 ARM 大小核架构下的优化调度这是它在服务器领域落地的一个重要差异化能力。4.2 进程管理涉及的关键接口与应用开发最直接相关的系统调用是 fork/vfork/clone、execve、exit、wait4。值得展开的是 clone它通过 flags 参数控制父子进程共享什么、复制什么。线程本质上是共享地址空间、共享文件表的 clone 调用产物——所以 Linux 里线程和进程没有本质区别只是共享资源的粒度不同。很多面试题问“Linux 线程的实现”答案的核心就在 clone 的 flags 上。进程管理这块还有一个高频考点是写时复制COW。fork 时子进程不真正复制父进程的物理内存页而是让页表指向同一物理页再把页设为只读。任一方写入时触发缺页异常内核才分配新页并复制数据。这种惰性复制极大地提高了 fork 的效率也让“fork 一个进程”在 Linux 上非常轻量。5. 第二大子系统内存管理——系统的“总账房”5.1 虚拟地址与物理页的映射关系内存管理是五大子系统里最抽象、但也最需要优先搞懂的一个。核心概念是虚拟地址空间每个用户态进程都有自己独立的 3GB 地址空间32 位下或巨大的用户态地址空间64 位下它们通过页表映射到物理内存。内核态也有自己的地址空间通常占据最高位的地址区域。为什么要坚持虚拟地址拆开讲有两个直接收益第一让每个进程可以独立、连续地使用地址空间不用关心物理内存碎片第二天然实现了进程隔离——进程 A 不可能通过地址猜到并修改进程 B 的数据。页表在多级结构中逐级查询x86_64 下是 4 级页表ARM64 在 4KB 页时也是 4 级。每次访问内存都需要硬件 MMU 做页表查询为了加速CPU 引入了 TLB页表缓存。这也是为什么大页内存HugePage能提升性能——减少页表项数量提高 TLB 命中率。5.2 分配器三件套物理内存的分配有三个层次从大到小页分配器Buddy System以页通常 4KB为单位的伙伴系统解决大块物理页的分配与回收同时尽力减少外部碎片。slab 分配器内核里到处都是小对象如 task_struct、inode、dentry每个都按页分配太浪费。slab 将页切成同尺寸的小对象缓存分配和释放都非常快。vmalloc用于需要连续虚拟地址但不要求连续物理地址的场景比如内核模块加载、某些驱动的临时映射。理解这三个层次你才能真正听懂“内存碎片”“kmalloc 和 vmalloc 的区别”“slab 缓存膨胀”这些问题背后在说什么。5.3 缺页异常与页面回收malloc() 分配的内存并不会立刻获得物理页只是建立了虚拟地址映射。真正访问时触发缺页异常page fault内核才分配物理页。省内存靠的就是这种“用到才给”的惰性分配。页面回收则负责在内存紧张时挑出一些页写回磁盘或丢弃。回收算法在 5.10 内核里是基于 LRU 链表改进的openEuler 也维护着一些针对匿名页回收和文件页回收的参数优化。你可能注意到一个容易被误解的参数swappiness。它并不是“交换倾向有多高”而是“匿名页与文件页回收比例的偏向”默认 60调小可以让系统更倾向于回收文件页而不是交换到 swap。6. 第三大子系统文件系统——从 read() 看一条完整链路6.1 VFS 与四件套super_block、inode、dentry、file文件系统子系统的灵魂是 VFS虚拟文件系统。VFS 的意义在于它定义了一套统一的抽象接口让上层的系统调用open/read/write不用关心底层到底是 ext4、xfs、btrfs 还是 NFS。你可以把 VFS 理解成“文件操作的多态接口层”。VFS 有四个核心对象super_block描述一个已挂载文件系统的整体信息。inode描述一个文件或目录的元数据权限、大小、时间戳、数据块位置。dentry描述目录项是路径解析过程中的一个节点。file描述进程打开的一个文件实例保存当前读写位置等状态。四者的关系可以用一次 open(/etc/passwd) 来串联内核沿着路径逐级解析 dentry最终找到对应的 inode然后创建一个 file 实例挂到进程的文件描述符表上。每次读写时通过 file 找到 dentry、再找到 inode最后通过 inode 的地址空间操作读数据。6.2 页缓存缓冲背后的功臣读文件时数据不会每次都从磁盘来。内核把最近读过的文件页缓存在内存里形成页缓存page cache。再次读同一个文件时直接命中缓存性能高出几个数量级。写文件时也是先写进页缓存标记为脏页再择机由内核线程写回磁盘。这就是热词里“内核缓冲”对应的机制。很多人用 free 命令看到 cached 很高就以为内存不够用其实恰恰相反这是内核在用空闲内存加速文件访问。真正需要关注的是 available 一列它表示在不发生明显回收的情况下可分配给新程序的内存。6.3 openEuler 里的文件系统相关特性openEuler 在文件系统层的动作主要集中在高性能和容器镜像场景比如 EROFS增强型只读文件系统用于容器镜像加速在内核态直接解压和读取镜像文件以及 iSER 等存储协议相关的块层优化。对学习内核的读者来说理解 VFS 和页缓存比追这些特性更重要但知道 openEuler 在往哪些方向使劲有助于建立“内核技术如何服务产业”的直觉。7. 第四大子系统网络协议栈——数据包的“物流中心”7.1 分层与 sk_buff网络子系统的复杂程度不亚于内存但学习路径更清晰从链路层、网络层、传输层到应用层网络数据包一路向上收包或一路向下发包。贯穿全过程的核心数据结构是 sk_buff它描述一个数据包的所有信息包括指针链、数据长度、协议头指针、校验和状态等。你可以把 sk_buff 理解成一个“物流包裹面单”面单记录数据从哪来、到哪去、用什么规则处理包裹里的数据可能被协议栈各层反复拆分或封装但 sk_buff 结构本身把这些操作都管理起来了。收包时网卡驱动把数据从 DMA 缓冲区挂到 sk_buff 上送入协议栈协议栈各层通过调整指针来剥掉头部而不是反复拷贝数据。7.2 收包路径中断、NAPI 与软中断网络收包的高性能路径是一个经典重点网卡收到包后产生硬件中断驱动在中断上半部只是标记一下然后触发软中断NET_RX_SOFTIRQ真正的收包处理在软中断里完成——从 ring buffer 取包、构造 sk_buff、调用协议栈各层处理函数。为了降低高流量场景下的中断开销内核使用 NAPI 机制第一次中断后开启轮询模式后续包不再触发中断而是由软中断批量拉取直到队列空了才重新开启中断。这就是高性能网络包括 openEuler 上跑 DPDK 或 AF_XDP 之外的默认内核路径能吃掉高吞吐的关键之一。7.3 连接跟踪与安全框架网络子系统还包含 netfilter 框架它在协议栈的关键路径上预留了钩子点iptables/nftables 就是挂在这个框架上的规则引擎。openEuler 也维护了一些针对 conntrack连接跟踪表的调优比如增大哈希表大小来支撑大规模长连接服务器场景。学习网络子系统时我最推荐的办法是“从一次 ping 和一次 TCP 握手看起”用 tcpdump 抓包同时打开内核源码跟着 sk_buff 从驱动到协议栈逐行走一遍。走完两遍你对网络子系统的整体感会比读十篇文章都强。8. 第五大子系统设备驱动——连接硬件与内核的“翻译官”8.1 设备模型device、driver、bus设备驱动子系统的核心是一套设备模型设备device描述硬件本身驱动driver描述如何控制该类设备总线bus描述设备和驱动如何匹配。三者通过 bus 上的 match 函数建立联系。嵌入式场景下常用的 platform 总线就是一种虚拟总线用于匹配片上设备与 platform 驱动。设备树Device Tree是 ARM 平台上描述硬件信息的标准板级厂商把内存大小、外设地址、中断号等信息写在 dts 文件里内核启动时解析设备树把节点变成 device再与驱动匹配。openEuler 在 ARM 服务器上的广泛支持依赖的就是这套机制的成熟。字符设备驱动则是最常见的驱动类型通过 register_chrdev 注册主设备号用户在应用层用 open/read/write/ioctl 访问设备节点驱动层对应的 file_operations 函数指针会被依次调用。很多热词里的“嵌入式内核源码”学习其实主要就是在学设备驱动子系统这一块。8.2 驱动开发的三个建议如果你将来要写驱动我建议先分清两种上下文进程上下文可以睡眠可以调用可能阻塞的函数和中断上下文不能睡眠不能拿可能阻塞的锁。驱动 80% 的 bug 都出在这上面。另外现代内核强烈推荐使用 devm_ 系列资源管理函数如 devm_kzalloc、devm_request_irq它能把资源的释放注册到设备生命周期上避免在 probe 失败或 remove 时忘记释放导致的内存泄漏。这些都写在《Linux Device Drivers》这本书里但真正落地的经验还是要在 openEuler 等真实环境里反复调试才能体会。9. 五大子系统如何协作用一次 read() 走完全局讲了这么多如果你觉得每个子系统都懂了一点但还串不起来那很正常。让我用一次最简单的磁盘文件读取把五大子系统全部串一遍应用调用 read()陷入内核。首先是进程管理子系统介入当前进程从用户态切换到内核态保存用户态寄存器现场通过系统调用号分发到 VFS 层的 read 实现。文件系统子系统接手根据 fd 找到 file定位到页缓存如果命中则直接把数据拷贝到用户缓冲区整个流程在微秒级完成。如果不命中则要发起磁盘 I/O文件系统生成 bio 请求块设备层通过 io 调度器排队最终由设备驱动子系统把请求写入 NVMe 或 SATA 控制器。进程因为等待 I/O 进入睡眠状态调度器进程管理子系统的一块核心会切换到其他可运行进程。磁盘完成后硬件中断唤醒驱动驱动处理中断并通知块层I/O 完成唤醒等待的进程。此时数据已经从磁盘到了页缓存缺页机制内存管理子系统确保目标用户的物理页已就绪并完成映射内核把数据从页缓存拷贝过去进程恢复运行read() 返回。网络方向同理网卡收包后设备驱动子系统把包交给网络子系统协议栈处理时如果涉及 socket 缓冲区分配就要向内存管理子系统申请 sk_buff 和相关内存如果 socket 上有进程在等待数据进程管理子系统会负责唤醒它。你会发现每一个“完整的功能”都是五个子系统接力完成的。这就是为什么我总建议读者不要孤立地学某一个子系统——只有把它们的接口安全边界摸清楚你才算真正认识了内核。10. 常见排查场景与工具速查这里结合我自己的经验整理几个高频场景帮助你把子系统知识落地到问题定位中。现象可能涉及的子系统常用排查工具CPU 使用率高但业务吞吐低进程管理调度失衡、spinlock 争用top、pidstat、perf sched内存充足但频繁 swap内存管理回收策略、cgroup 限制vmstat、sar -r、/proc/zoneinfofree 命令 cached 很高文件系统页缓存正常现象free -h、cat /proc/meminfo网络小包吞吐上不去网络NAPI、中断合并ethtool -S、top 看软中断占比磁盘 I/O 延迟抖动文件系统设备驱动IO 调度、队列深度iostat -x、blktrace排查思路有一个不变的原则先确认现象发生在哪个子系统再进子系统内部找原因。不要一上来就怀疑调度器或者网络栈先压住疑问用工具把证据收集齐。一个容易被忽视的细节openEuler 上跑 perf 或 bpftrace 系列工具时注意内核是否开启了对应的 CONFIG_PERF_EVENTS、CONFIG_BPF 等选项默认安装的内核通常都已经打开但如果你自己裁剪了内核这些功能会被悄悄关掉。11. 从通识走向深入的学习路径建议如果你把五大子系统整体过了一遍下一步该怎么走我根据自己的经验和踩过的坑给三条路径参考。第一条是“源码树目录法”直接打开内核源码的顶层目录你会发现五个子系统的代码天然对应着几个核心目录——kernel/进程管理、调度、mm/内存管理、fs/文件系统、net/网络、drivers/设备驱动。按目录逐个“扫”一遍每个目录只看核心文件不求细节两周左右就能形成整体地图感。第二条是“特性牵引法”给自己定一个待解决的实际问题比如“为什么我的服务在 openEuler 上延迟偶尔飙高”然后顺藤摸瓜——查调度延迟、查中断、查网络收包路径、查内存回收每查一层都会加深一整片知识。这是比纯读书高效得多的方式。第三条是“动手实验法”挑一个 openEuler 虚拟机自己编译内核、裁剪配置、加打印或 kprobe验证你对某个子系统的猜测。你可以试试修改调度器的某个参数观察负载变化的反应也可以自己写一个简单的字符设备驱动体会设备模型的运作。实验失败很正常但每次失败都会让你记住一个“为什么”。我个人体会最深的是只读书不实验总觉得自己懂了一到线上出问题还是慌反过来边做边查刻意弱化“必须读完某一章”的执念内核的轮廓反而越来越清晰。希望这篇通识能帮你把这五块骨架立起来后面的路虽然长但有地图就不怕走偏。
返回列表