资讯动态

Linux内核学习:先看目录,用地图思维驾驭3000万行源码

发布时间:2026/10/8 20:41:38 来源:尧图企业网站定制
1. 为什么内核学习的第二步是看懂目录——地图思维的价值如果给所有准备踏入内核源码世界的初学者一个建议我会说先别急着打开某个.c文件从头读起先把源码树逛一遍。内核学习之路【2/100】这一篇我要分享的就是这个目录环节的完整思路。内核源码的规模摆在那里——Linux 内核主线的代码量早已超过 3000 万行文件数以万计。没有哪个正常人能靠顺序阅读把它读完也不需要读完。真正决定学习效率的是你脑子里有没有一张准确的地图。目录结构就是这张地图本身。我见过太多人兴致勃勃下载了源码然后一头扎进kernel/fork.c读进程创建读了两天连task_struct的来龙去脉都没搞清原因很简单——他不知道这个文件在整个内核里处于什么位置、依赖谁、被谁依赖。先看目录就是在给自己建立坐标系。1.1 内核源码不是一本书而是一座城市很多人把源码当成一本书觉得从头翻到尾就能懂这是最大的误区。内核源码更像一座城市有行政中心kernel有工业区drivers有交通枢纽net有仓储物流fs有市政规划arch有公共设施lib还有档案馆Documentation。你学习一座城市不会从某条巷子的门牌号开始逛你会先看地图知道哪片区域是什么功能然后再按需要深入。这套比喻不是随便打的。内核各目录间的耦合关系本质上就是城市各功能区之间的协作关系。比如你发起一次文件读取用户态调用read()进入内核后会经过虚拟文件系统层fs再走到具体文件系统实现如 ext4也在 fs 下然后通过块设备层block调用驱动drivers期间涉及内存分配mm、进程调度kernel/sched、可能还有网络如果文件在 NFS 上。整个过程跨越多个目录如果没有地图你会迷失在函数调用链里。1.2 目录结构承载的三种关键信息看目录绝不只是认个路它至少承载三类信息这些信息直接决定你的学习路线第一是功能边界。内核把什么功能放在哪个目录是有清晰逻辑的比如进程调度、信号、时间管理这类内核核心服务一定在kernel/下虚拟内存、页缓存、伙伴系统一定在mm/下各种文件系统实现一定在fs/下。这道边界是你后续查代码时的第一层定位。第二是依赖关系。通过目录的架构你能感知到分层。比如include/目录下的头文件被所有目录依赖arch/目录下的代码只为特定硬件架构服务drivers/则大量调用内核提供的公共 API。理解这种依赖方向比记住某个具体函数更重要。第三是设计层次。内核不是扁平的。从init/的启动流程到kernel/的核心服务到mm、fs、net的子系统再到drivers的具体设备支持是一个从抽象到具体、从核心到外沿的层次结构。你学内核应该先学层次靠内的部分再往外走。目录结构天然为你标好了这个顺序。看到这里你应该已经明白目录不是给你背的是给你用的。下面我从顶层开始把每个目录拆开讲一遍重点说清楚每个目录的职责、你该什么时候碰它、以及它和其他目录的关系。2. 顶层目录逐个拆解——每个目录到底在干什么Linux 内核源码树的顶层目录通常有二十个左右。不同版本略有增减比如早期有firmware/后来移除了crypto/、virt/等则稳定存在但主体结构非常稳定。我用当前主线的布局为准逐个讲。2.1 与你天天打交道的核心目录kernel、mm、fs、netkernel/—— 这是内核的行政中枢。进程创建与回收fork.c、exit.c、调度器sched/、信号机制signal.c、定时器与时钟管理time/、模块加载module.c、内核线程kthread.c都在这。初学者面对的很多硬核问题入口都在这里。注意kernel/目录下还有power/、irq/、locking/、cgroup/等子目录各管一摊。mm/—— 内存管理子系统全内核最考验抽象能力的部分之一。伙伴系统分配页page_alloc.c、slab/slub 分配小对象slub.c、虚拟地址空间管理mmap.c、mprotect.c、缺页异常处理fault.c、页缓存filemap.c、内存回收vmscan.c全在这里。你在用户态每次malloc、mmap最终都会沉到这里。fs/—— 文件系统大本营。顶层有很多文件系统和框架代码ext4/、btrfs/、xfs/、overlayfs/这类具体实现虚拟文件系统层VFS的核心代码则在根目录下直接以*.c形式存在比如open.c、read_write.c、namei.c。fs/还包含buffer.c块缓冲、dcache.c目录项缓存、inode.c索引节点管理等通用机制。如果你关心文件到底是怎么存下来的这几个文件是必读。net/—— 网络协议栈全家桶。ipv4/、ipv6/是 TCP/IP 协议的实现packet/是原始包接口netfilter/是防火墙框架core/放了一些 socket 层的公共逻辑mac80211/管无线bluetooth/自然是蓝牙。注意设备驱动本身不在net/下而是放在drivers/net/协议栈与驱动的边界就在这里。这四个目录是内核学习的主战区任何一条稍微深入的学习路线都绕不开它们。有时间应该先在这四个目录里泡一段时间。2.2 硬件相关的 arch 与 driversarch/—— 按 CPU 架构划分的目录内部结构一般是arch/架构名/比如x86/、arm/、arm64/、riscv/、powerpc/、s390/等。每个架构目录下又有kernel/、mm/、lib/等子目录存放该架构特有的启动代码、汇编入口、页表管理、中断控制器驱动等。x86 下的entry_64.S、head_64.Sarm64 下的entry.S都是理解系统启动和异常入口的关键文件。绝大多数应用级驱动开发者不需要深入arch/但如果你想理解系统调用怎么从用户态陷入内核态x86 下的entry_64.S和syscall_64.tbl值得看。drivers/—— 这是内核中体积最大的目录在源码树里占了相当可观的比例。它按设备类型分子目录gpu/、net/、mmc/、usb/、i2c/、spi/、gpio/、clk/、regulator/、input/、media/、sound/、thermal/、firmware/等等。写驱动的人长年泡在这里但学内核核心机制的人初期不建议深入这块这里的代码量太大、太杂很容易一头扎进去出不来。我的建议是先把驱动框架如driver-core/、base/下的总线、设备、驱动模型搞明白再按需进入具体子目录。2.3 容易被忽略但关键时刻救命的目录include/—— 头文件总目录。它几乎被所有源码文件引用重要性怎么强调都不过分。include/linux/放的是通用内核 API 的头文件include/uapi/放的是用户态可见的接口定义系统调用号、结构体、ioctl 命令等include/arch/架构/是架构相关的头文件。初学阶段遇到一个结构体不知道在哪定义十有八九在include/linux/下能找到。init/—— 内核启动的入口。main.c里的start_kernel()是内核的 C 语言入口函数从它开始内核完成一堆初始化工作最终启动第一个用户态进程init。想从宏观上把握内核启动流程这是必读的一站。block/—— 块设备层。它位于文件系统和具体磁盘驱动之间负责 I/O 调度mq-deadline、bfq等 I/O 调度器都在这、请求队列管理、bio 处理等。你读文件系统代码时看 bio 怎么下发到块设备就要来这里。ipc/—— 进程间通信。System V 消息队列、共享内存shm.c、信号量sem.c都在这。虽然现代应用大多改用其他 IPC 方式但在内核里这套机制仍是老牌经典。crypto/—— 内核加密 API。不是只给用户用文件系统加密、网络协议加密IPsec 等都会调用这里的接口。想了解内核怎么抽象算法struct crypto_alg、skcipher、akcipher 等可以来这看。security/—— 安全框架包括 SELinux、AppArmor 等 LSM 钩子的实现。它与include/linux/lsm_hooks.h配合为内核提供一个安全策略的插槽机制。sound/—— 音频栈重点是 ALSA。sound/core/是核心sound/soc/是嵌入式常用的 ASoC 框架。virt/—— 内核虚拟化相关的代码比如 KVM 的部分实现。对虚拟化感兴趣的人会常来。lib/—— 内核的公共工具库比如字符串操作、链表、红黑树、crc 校验、KASAN相关的部分支持等。自己写内核模块时很多工具函数要来这里找。scripts/—— 构建与辅助脚本。Kconfig、Kbuild的解析逻辑、make menuconfig的配置界面脚本、各种代码格式检查工具都在这。不搞构建系统的人可以不深究但编译内核出问题时来这里看 Makefile 相关工具能帮上忙。tools/—— 用户态的辅助工具比如perf性能分析、bpftool、objtool、selftests等。这些工具本身不在运行的内核里却对内核开发和调试极有用。建议早点学会perf和bpftool后面排查问题会非常顺手。usr/—— 主要是 initramfs 相关的生成逻辑里面有个gen_init_cpio之类的东西。一般不用碰。Documentation/—— 内核文档。老一辈内核开发者常说代码即文档但Documentation/下的核心机制文档、子系统说明、API 描述仍然很有价值。比如你要写驱动Documentation/driver-api/值得先看想理解内存管理Documentation/mm/有不错的入门材料。顶层目录大致就这些。接下来我挑几个核心子系统的内部结构再做一次深井式拆解因为光知道顶层目录还不够真正读代码时你需要更细的坐标系。3. 核心子系统的目录细节与代码阅读顺序如果你已经能把顶层目录画出来下一步是把目光沉到几个核心子系统的内部。我按学习高频程度排序来讲。3.1 kernel 目录内部调度、信号、时间各在哪kernel/目录在主线内核里顶层文件不少但更关键的是几个子目录kernel/sched/调度器。core.c是调度核心fair.c是 CFS 公平调度器新版本已演化为 CFS 的基础部分又加入了 EEVDF 等新策略rt.c是实时调度器。要理解Linux 怎么决定谁先跑这一子目录就是主战场。kernel/time/时钟与定时器。hrtimers.c是高精度定时器tick-sched.c是 tick 调度clockevents.c是时钟事件设备管理。系统调时间、定时器精度问题、低延迟相关优化都跟这里有关。kernel/irq/中断子系统。irqdesc.c、handle.c、manage.c、chip.c等文件实现了中断描述符、中断处理流程、中断线程化等。设备驱动开发者要理解中断请求request_irq背后的机制来这。kernel/locking/同步原语。mutex.c、spinlock.c、rwsem.c、qspinlock.c等。想真正理解锁的实现细节而不是停留在 API 层面这些代码值得啃。kernel/power/电源管理框架。main.c处理 suspend/resume 流程。嵌入式、移动设备的开发者会关心。kernel/cgroup/cgroup 核心实现。容器技术、资源隔离的底层就在这。kernel/bpf/BPF 子系统。现代内核里bpf/已经是热门方向。里面是 BPF 指令解释器、验证器verifier.c、BPF 系统调用实现syscall.c。这学期如果你对 eBPF 感兴趣这里是必经之路。顶层还有些直接文件比如fork.c和exit.c进程生命周期、signal.c信号处理、module.c模块加载、kthread.c内核线程、workqueue.c工作队列、panic.c内核崩溃处理、kallsyms.c内核符号表等。这些文件虽然不是目录但地位重要按需阅读即可。3.2 mm 目录内部内存分配器与地址空间管理mm/目录的文件多且彼此穿插。我按一次内存分配的旅程来组织阅读顺序用户进程要内存先触发缺页异常进入mm/fault.c的do_page_fault路径arch/下有架构相关的前半段。内核要分配物理页调用伙伴系统的alloc_pages实现在mm/page_alloc.c。合作伙伴把物理页按 2 的幂次阶数管理这里能看到冷热页、每 CPU 页列表pcplist、内存结点和区域的层级关系。小对象分配走 slab/slub核心代码在mm/slub.c。它从伙伴系统拿大块内存再切分成固定大小的对象并用空闲链表管理。kmalloc、kmem_cache_create 最终都到这里。如果涉及文件映射会遇到mm/filemap.c页缓存的读入、写回、预读逻辑都在这里。它把物理页与文件内容关联起来。内存不够时要回收mm/vmscan.c中的 shrink 机制负责扫描 LRU 链表选择匿名页还是文件页进行回收或交换。额外的调试与统计还包括mm/kasan.c、mm/slub.c里的 debug 逻辑、mm/oom_kill.c内存耗尽后的裁决者等。初学者容易在 mm 里迷路我建议先抓两条主线物理页的分配路径伙伴系统和虚拟地址到物理页的映射过程缺页异常。两线汇合你就能理解 Linux 内存管理的八成功效。3.3 fs 目录内部VFS 与具体文件系统的层次关系fs/目录结构上非常有层次感。顶层一部分文件属于 VFS 框架层另一部分是具体文件系统子目录。VFS 框架层的核心文件包括fs/namei.c路径解析path walk是open()系统调用的前半生把路径字符串一步步解析成目录项和 inode。fs/dcache.c目录项缓存dentry cache它把路径组件缓存起来加速路径解析。fs/inode.cinode 的分配、回收、哈希管理。inode 是文件元数据在内存中的代表。fs/file.c文件描述符表的管理。fd_install、close_fd等都在这里。fs/open.c、fs/read_write.c系统调用的直接实现比如do_sys_open、vfs_read、vfs_write。fs/super.c超级块管理每个已挂载的文件系统有一个超级块对象。fs/mount.c挂载与命名空间管理容器技术里的 mount namespace 与此密切相关。具体文件系统则以目录形式存在ext4/、btrfs/、xfs/、f2fs/、overlayfs/、tmpfs/在mm/里特例等。它们实现 VFS 定义的各种操作接口比如ext4_file_operations、ext4_dir_inode_operations、ext4_inode_operations。阅读fs/的方法是先从 VFS 框架入手理解struct file、struct dentry、struct inode、struct super_block四大对象的协作再挑一个简单的文件系统ramfs、tmpfs或小型的minix看具体实现。不要一上来就啃 ext4。3.4 net 目录内部协议栈的经典分层net/目录下有core/和很多协议子目录。简单梳理net/core/socket 层的公共设施比如sock.csocket 核心结构、skbuff.csk_buff 缓冲区管理、dev.c网络设备层核心、neighbour.c邻居子系统。sk_buff 是整个网络栈的快递包裹搞清楚它的head、data、tail、end指针含义后面读任何协议代码都会轻松很多。net/ipv4/TCP/UDP/ICMP 等实现。tcp.c、tcp_input.c、tcp_output.c是 TCP 的三驾马车udp.c相对简单适合入门。net/ipv6/IPv6 协议栈。net/netfilter/Netfilter 框架和 iptables/nftables 在内核侧的实现。NAT、防火墙规则匹配的机制都在这。net/sched/流量控制。HTB、TBF、fq_codel、qdisc 等排队规则都在这。net/packet/AF_PACKET 套接字抓包工具tcpdump的底层就靠它。net/unix/Unix domain socket本地进程通信。net/wireless/、net/mac80211/WiFi 相关无线网卡的协议支持。网络栈的阅读建议是先看net/core/skbuff.c再跟一次一个数据包从网卡进来经过协议栈到达 socket 接收队列的完整路径。这条路径会把net/目录里大半的关键文件串起来。4. 带着问题读目录——如何用目录结构反推内核行为章节讲到这里目录的地图已经铺开了。但地图存在的意义是导航。这一章我把思路反过来用三个实际追踪案例演示怎么利用目录知识快速定位内核代码。4.1 案例一一个系统调用从用户态到内核态的路径追踪假设你想搞清楚在用户态调用open()打开一个文件内核内部到底做了一串什么事。你不该盲搜open因为匹配结果成百上千。目录地图给出的路径是这样的先从架构侧找系统调用入口arch/x86/entry/entry_64.S里能看到do_syscall_64的汇编入口它根据系统调用号跳转到对应的内核 C 函数。系统调用号对照表在arch/x86/entry/syscalls/syscall_64.tbl。从系统调用号找到内核实现的入口函数在fs/open.c里是do_sys_open。do_sys_open会调用路径解析逻辑去fs/namei.c找path_openat。路径解析过程中要用目录项缓存去fs/dcache.c找相关实现。解析到最终文件后要分配文件描述符和struct file去fs/file.c、fs/open.c里继续追。最终会调用具体文件系统的open回调比如 ext4在fs/ext4/namei.c或fs/ext4/file.c里。你发现没有一次open()把arch/、fs/下的多个目录串成了一条链。懂目录你就能快速划出这条链而不是靠运气在代码里乱撞。4.2 案例二一个驱动模块从加载到工作的完整流程假设你在写一个字符设备驱动模块加载后要创建设备节点。从目录视角看模块加载的入口函数由module_init指定底层机制在kernel/module.c——它负责把模块的二进制段搬运到内存、解析符号、调用初始化函数。驱动要注册设备注册路径的核心在drivers/base/设备和总线模型的核心实现以及driver-core/下的bus.c、device.c、driver.c、platform.c等。如果是平台设备drivers/base/platform.c提供platform_driver_register如果是 PCI 设备drivers/pci/下有一整套枚举、配置空间读取逻辑。设备节点如果依赖 devtmpfs相关实现会落到drivers/base/devtmpfs.c和fs/devtmpfs.c之间的配合上。如果你用了中断注册中断处理函数走request_irq实现在kernel/irq/manage.c。如果设备要睡一会儿或定时轮询那又指向kernel/sched/和kernel/time/。从写一个 hello world 驱动出发沿着目录走一圈你能把设备模型、中断、内核线程、定时器全部串起来。这种地图导航式的学习远比孤立地读某个文件有效。4.3 从目录的变化看内核版本的演进内核目录不是一成不变的。举个典型例子kernel/bpf/目录在较老的内核里几乎不存在但从 4.x 时代开始快速膨胀drivers/gpu/下新增的drm/子目录体系伴随着显卡渲染管线的复杂化不断生长fs/下新增文件系统如erofs/针对只读容器镜像反映的是新的应用场景。你跟踪新版本内核的更新可以只看目录层的 diff某个子系统新增了文件通常意味着新功能某个目录被重构通常意味着设计思路的调整。目录的变化史就是内核的进化史这一点在读 git log 时尤其有感触。5. 实操心得目录级学习的工具与方法最后一章我分享几个自己在目录学习阶段实际用到的工具和落地方法其中一些是踩过坑之后才明白的。5.1 源码阅读环境的搭建从 grep 到 ctags 再到专用工具早期我读内核就是vim grep硬搜函数定义。后来发现效率太低转向了 ctags。给内核源码建索引的命令大致是cd /path/to/linux make tags这会生成tags文件在 vim 里用Ctrl]就能跳转到函数或结构体定义。类似地make cscope生成 cscope 数据库用来查谁调用了这个函数这类反向引用效率比 grep 高很多。如果你是 VSCode 用户也可以用clangd基于compile_commands.json来做符号跳转但内核的编译数据库生成稍微麻烦一点。老实用 ctags/cscope 也能把大部分工作做完关键是不要让工具折腾占用学习时间。5.2 我给初学者的三阶段目录学习路线第一阶段叫逛目录不需要读懂代码只需要把顶层目录名称、职责、大致关系记住。给自己一个小目标——能做一张手绘的内核目录思维导图。第二阶段叫按主题深挖比如以进程调度为主题只逛kernel/sched/目录配合include/linux/sched.h把结构体、关键函数、子目录如kernel/sched/fair.c、rt.c之间的关系理清。这一阶段不要贪多一次只吃透一个主题贵在精。第三阶段叫跨目录追踪回到第四章那三个案例尝试自己设计一个场景比如网络包从进来到被应用读取涉及哪些目录哪些文件。这一阶段的核心是建立跨目录的联系感理解子系统之间的接口边界。三阶段走完你对内核源码的把握会从见过很多文件名变成知道代码在哪、大概长什么样、和谁协作。这时你才真正具备自己读代码的能力。5.3 踩过的坑不要在这些地方浪费太多时间我见过不少人和我当年一样在几个地方浪费了大量时间总结成三条第一别在 drivers 里迷路。drivers/目录体量巨大且大部分代码是配置设备寄存器 调用内核 API的模式学习价值有但前期投入产出比很低。除非你已经确定未来做某个方向比如网卡驱动、GPU否则先远离。第二别在 include 里死磕。头文件里的结构体、宏定义多如牛毛。遇到一个查一个就够了不要试图通读include/linux/。你真正需要精读的头文件不多比如sched.h、fs.h、skbuff.h这几个核心其他都是用到再看。第三别忽略 Documentation。很多初学者觉得内联注释少就放弃 Documentation这是可惜的。尤其当你要理解某个子系统比如 BPF、KVM、设备模型Documentation/下往往有一篇很好的 overview 文章读完再进代码事半功倍。这三个月我自己的习惯是每学一个子系统先在目录里建立文件清单标注哪些是主路径、哪些是辅助、哪些先跳过。这个习惯帮我省了大量时间也推荐给你。内核学习之路【2/100】的目录篇就写到这里。下一篇我会从这个地图中最常用的一条路线出发带大家真正深入一段内核代码把目录学到的坐标系用起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑