Linux内核作为一个特殊的程序,同样需要经过编译、链接之后才能运行,仅仅是它执行时拥有特殊的权限,位于特定的空间,并且不会也不可能信赖其他的软件罢了。
Linux发展至今,其内核的组织结构日渐清晰,层次日渐分明。一旦基本系统安装完毕,具有系统管理员权限的用户即可编译内核。一般来说,Linux系统内核的源码放置在/usr/src/linux目录下,它将依赖于体系结构的代码和独立于体系结构的代码分离开来(前者仅占5%,通常是一些汇编代码和局部的、针对于不同体系结构的局部处理细节,如时钟定时器频率)。
一、简介
Linux 内核是一种单内核模式的系统,内核中所有的程序几乎都紧密联系在一起。相互之间的依赖和调用关系非常紧密,了解其源码目录结构有助于更好地理解和研究内核。
Linux 内核的源码采用树形结构,功能相关的文件被放置在不同的子目录下,使得程序更具可读性。Linux 内核主要由多个模块构成,各个模块之间存在着复杂的依赖关系。
子系统之间的依赖关系使得内核的各个部分能够协同工作,为用户提供稳定、高效的操作系统环境。例如,进程调度与内存管理之间相互依赖,在多道程序环境下,程序运行必须创建进程,而创建进程的第一步就是将程序和数据装入内存。
进程间通信与内存管理也有紧密联系,共享内存机制是进程间通信的一种方法,需要内存管理的支持。虚拟文件系统与网络接口以及内存管理之间也存在依赖关系,虚拟文件系统利用网络接口支持网络文件系统,同时也利用内存管理支持 RAMDISK 设备。内存管理与虚拟文件系统相互依赖,内存管理利用虚拟文件系统支持交换,当一个进程存取的内存映射被换出时,内存管理向文件系统发出请求,并挂起当前正在运行的进程。
Linux 内核中的所有子系统还依赖于一些共同的资源,包括所有子系统都用到的例程,如分配和释放内存空间的函数、打印警告或错误信息的函数及系统提供的调试例程等。
Linux kernel 成功的两个原因在于灵活的架构设计和良好的可扩展性。灵活的架构设计使得大量的志愿开发者能够很容易加入到开发过程中,而每个子系统尤其是那些需要改进的子系统都具备良好的可扩展性,这使得 Linux kernel 可以不断进化和改进。
Linux 内核在整个计算机系统中处于核心位置,它通过虚拟化将计算机硬件抽象为一台虚拟机,供用户进程使用。进程运行时完全不需要知道硬件是如何工作的,只要调用 Linux kernel 提供的虚拟接口即可。同时,Linux 内核还负责多任务处理,实际上是多个任务在并行使用计算机硬件资源,内核的任务是仲裁对资源的使用,制造每个进程都以为自己是独占系统的错觉。
进程上下文切换需要换掉程序状态字、换掉页表基地址寄存器的内容、换掉 current 指向的 task_struct 实例、换掉 PC,也就换掉了进程打开的文件和进程内存的执行空间。
Linux 内核的整体架构以进程调度器为中心,所有其余的子系统都依赖于进程调度器,因为其余子系统都需要阻塞和恢复进程。当一个进程需要等待一个硬件动作完成时,相应子系统会阻塞这个进程;当这个硬件动作完成时,子系统会将这个进程恢复,这个阻塞和恢复动作都要依赖于进程调度器完成。进程调度器依赖内存管理器,当进程恢复执行时,需要依靠内存管理器分配供它运行的内存。IPC 子系统依赖于内存管理器,共享内存机制是进程间通信的一种方法,运行两个进程利用同一块共享的内存空间进行信息传递。VFS 依赖于网络接口,支持 NFS 网络文件系统;VFS 依赖于内存管理器,支持 ramdisk 设备。内存管理器依赖于 VFS,因为要支持交换,可以将暂时不运行的进程换出到磁盘上的交换分区,进入挂起状态。
Linux 内核采用高度模块化设计,利于分工合作。只有极少数的程序员需要横跨多个模块开展工作,这种情况仅发生在当前系统需要依赖另一个子系统时。硬件设备驱动、文件系统模块、网络设备驱动和网络协议模块这四个模块的可扩展性最高。
Linux 内核中的数据结构包括任务列表、内存映射、I-nodes 等。系统中每个进程的数据结构 task_struct 中有一个指针 mm 指向它的内存映射信息,也有一个指针 files 指向它打开的文件,还有一个指针指向该进程打开的网络套接字。所有的数据结构的根都在进程调度器维护的任务列表链表中。
Linux 内核的子系统架构包括进程调度器、内存管理器、IPC 子系统、VFS 等。进程调度器是 Linux kernel 中最重要的子系统,它控制对 CPU 的访问,不仅包括用户进程对 CPU 的访问,也包括其余子系统对 CPU 的访问。进程调度器由调度策略模块、体系结构相关模块、体系结构无关模块和系统调用接口模块组成。调度策略模块决定哪个进程获得对 CPU 的访问权,应该让所有进程尽可能公平地共享 CPU。体系结构相关模块设计一组统一的抽象接口来屏蔽特定体系接口芯片的硬件细节,与 CPU 交互以阻塞和恢复进程。
体系结构无关模块与调度策略模块交互将决定下一个执行的进程,然后调用体系结构相关的代码去恢复那个进程的执行,同时还会调用内存管理器的接口来确保被阻塞的进程的内存映射信息被正确保存起来。系统调用接口模块允许用户进程访问 Linux Kernel 明确暴露给用户进程的资源,通过一组定义合适的基本上不变的接口将用户应用程序和 Linux 内核解耦,使得用户进程不会受到内核变化的影响。
二、主要目录结构详解
⑴ arch
Linux 内核的 arch 目录包含和硬件体系结构相关的代码。每种平台占一个相应的目录,例如和 32 位 PC 相关的代码存放在 i386 目录下。这个目录中比较重要的部分包括 kernel(内核核心部分)、mm(内存管理)、math-emu(浮点单元仿真)、lib(硬件相关工具函数)、boot(引导程序)、pci(PCI 总线)和 power(CPU 相关状态)等。正如参考资料中提到的,arch 目录是针对不同 CPU 体系架构平台的代码,将与平台相关的代码放在该目录中。
每种体系结构都有相应的子目录,如 arm、arm64 分别是 arm 32 位和 64 位平台。同时,在 Linux 内核源码中,arch 目录存放了与体系结构相关的代码,用于支持不同硬件体系结构的实现,其下的子目录主要根据不同的架构进行分类,如 x86、arm、mips 等。对于特定的架构,如 x86,又分为几个子目录,包括 boot(包含引导启动代码)、include/asm(包含与汇编语言相关的头文件)、kernel(包含特定于 x86 架构的内核代码)和 mm(包含特定于 x86 架构的内存管理代码)等。
⑵block
Linux 内核的 block 目录包含部分块设备驱动程序。在 Linux 内核源码中,block 目录是用于处理块设备的子系统,它包含了与块设备驱动和 I/O 调度相关的代码。块设备是以固定大小的块(通常为 512 字节)进行读写的设备,例如硬盘、固态硬盘(SSD)等。block 目录中有一些重要的文件和目录,如 blk-core.c(实现了块层核心功能,包括请求队列管理、I/O 请求处理等)、bio.c(包含了生物结构体和与之相关的函数,用于描述和操作数据块的输入输出)、ll_rw_blk.c(定义了低级别的读写函数接口,用于与底层设备交互)等。
此外,block 目录下还有各种与特定块设备驱动相关的文件或目录,例如 drivers/(包含了各种不同类型的块设备驱动程序代码)和 iosched/(包含不同 I/O 调度算法的实现代码,例如 CFQ、Deadline、NOOP 等)。
⑶ crypto
Linux 内核的 crypto 目录包含常用加密和散列算法(如 AES、SHA 等),还有一些压缩和 CRC 校验算法。在 Linux 内核源码中,crypto 目录是用于实现各种密码学功能的子系统,包含了对称加密、非对称加密、哈希函数以及其他密码学算法的实现代码。
例如 algif_skcipher.c(实现了内核级别的对称加密接口)、rsa.c(实现了 RSA 算法相关的函数和数据结构)、sha256_generic.c(实现了 SHA-256 哈希算法相关的函数和数据结构)、kpp.c(实现了内核级别的密钥协商协议接口)、digest.c(包含了通用消息摘要处理相关的函数和数据结构)等。此外,crypto 目录下还有其他子目录,如 cipher/(包含不同对称加密算法,如 AES、DES 的实现代码)和 asymmetric_keys/(包含非对称密钥管理相关的代码)。
⑷documentation
Linux 内核的 documentation 目录下面没有内核代码,只有很多质量参差不齐的文档,但往往能够给我们提供很多的帮助。这个目录包含了大量关于内核开发和使用的文档资源,提供了对不同子系统、功能和接口的详细说明,以及编程指南、配置选项等信息。
其中一些重要的子目录和文件包括 admin-guide/(包含了管理员相关的指南,如设备管理、调试技术等)、filesystems/(提供了文件系统相关的文档,包括各种文件系统类型,如 ext4、NTFS 的说明)、networking/(包含网络子系统相关的文档,包括协议栈、驱动程序等)、security/(提供了与内核安全性相关的文档,例如 SELinux 和 AppArmor 的配置指南)、gpio/(包含有关 GPIO 子系统的文档,涵盖 GPIO 控制和驱动程序开发)、sysfs-rules.txt(详细描述了 sysfs 规则的语法和用法)。
⑸drivers
Linux 内核的 drivers 目录中是系统中所有的设备驱动程序。它又进一步划分成几类设备驱动,每一种有对应的子目录,如声卡的驱动对应于 drivers/sound,还包括 USB 总线驱动程序、PCI 总线驱动程序、显卡驱动程序、网卡驱动程序等。在 Linux 内核源码中,drivers 目录是一个非常重要的目录,它包含了各种设备驱动程序的源代码。Linux 内核通过这些驱动程序与硬件设备进行交互和管理。
drivers 目录下的结构是多层级的,不同类型的设备驱动程序被组织到各自对应的子目录中。常见的 drivers 子目录有 char/(包含了字符设备驱动程序,如串口、终端、键盘等)、block/(包含了块设备驱动程序,用于访问硬盘、SSD 等存储设备)、net/(包含了网络设备驱动程序,涵盖以太网卡、Wi-Fi 卡等)、gpu/(包含了图形处理单元相关的驱动程序)、sound/(包含了声音子系统相关的驱动程序)、usb/(包含了 USB 设备驱动程序)等。
⑹fs
Linux 内核的 fs 目录包含各种支持的文件系统,如 ext、fat、ntfs 等。每一个子目录支持一个文件系统,如 Ext3 文件系统对应的就是 /fs/ext3 目录。在 Linux 内核源码中,fs 目录包含了与文件系统相关的代码,是整个文件系统层次结构的核心部分,提供了与文件和目录操作、磁盘管理、文件系统接口等相关的功能。例如 ext2 / 子目录包含了 ext2 文件系统的实现代码。每个逻辑文件系统都在 fs 目录下有对应的目录。同时,fs 目录中存储了虚拟文件系统相关代码。
⑺include
Linux 内核的 include 目录头文件,其中与系统相关的头文件被放置在 linux 子目录下。与 intel cpu 相关的头文件在 include/asm-i386 子目录下,而 include/scsi 目录则是有关 scsi 设备的头文件目录。在 Linux 内核源码中,include 目录中存储了内核源码依赖的大部分头文件代码,也存储了虚拟文件系统相关代码。
include 目录按照下面的子目录进行分组,如 include/asm-*/ 每一个对应着一个 arch 的子目录,比如 include/asm-alpha、Include/asm-arm 等。每个子目录中的文件都定义了支持给定体系结构所必要的预处理函数和内联函数,这些内联函数多数都是全部或者部分的汇编语言实现。include/linux 与平台无关的头文件都在这个目录下,它通常会被链接到目录 /usr/include/linux(或者它里面的所有文件都会被复制到 /usr/include/linux 目录下边)。
⑻ init
Linux 内核的 init 目录包含内核初始化代码,包括 main.c 和 Version.c 两个文件。这是研究核心如何工作的好起点,该初始化代码关联到了内存的各个组件的入口。init 目录中的代码是内核启动代码,通过解压程序调入 C 语言入口。其中的文件有 do_mounts.h,该文件实现在 Linux 下挂载文件系统。
⑼ipc
Linux 内核的 ipc 目录包含进程间通信的代码。如信号量、共享内存等。在 Linux 内核源码中,ipc 目录中存储了进程间通信相关代码,包含了共享内存、信号量及其他形式的 IPC 代码。
⑽kernel
Linux 内核的 kernel 目录包含内核管理的核心代码,此目录下的文件实现了大多数 linux 系统的内核函数,其中最重要的文件当属 sched.c;同时与处理器结构相关代码都放在 arch/*/kernel 目录下。在 Linux 内核源码中,kernel 目录中存储了内核核心代码,其中包含了进程管理、IRQ 中断等模块。kernel 目录会调用 arch/arm/kernel。
⑾lib
Linux 内核的 lib 目录包含库文件代码,与处理器相关的库函数位于 arch/*/lib/ 目录下。在 Linux 内核源码中,lib 目录此目录包含了核心的库代码。实现了一个标准 C 库的通用子集,包括字符串和内存操作的函数(strlen、mmcpy 等)以及有关 sprintf 和 atoi 系列函数。与 arch/lib 下的代码不同,这里的库代码都是 C 编写的,在内核新的移植版本中可以直接使用。与处理器结构相关库代码被放在 arch/mm 中。
⑿mm
Linux 内核的 mm 目录包含内存管理代码,和平台相关的一部分代码放在 arch//mm 目录下。在 Linux 内核源码中,mm 目录此目录包含了与体系无关的部分内存管理代码。与体系结构相关的内存管理代码位于 arch//mm 下。mm 目录包含内存相关的页表和内存管理。
⒀net
Linux 内核的 net 目录包含与网络相关的代码,实现了各种常见的网络协议,其每个子目录对应于网络的一个方面。在 Linux 内核源码中,net 目录里是核心的网络部分代码,其每个子目录对应于网络的一个方面,包含了网络子系统代码和一些驱动代码。
⒁scripts
Linux 内核的 scripts 目录包含用于配置内核文件的脚本文件等。
⒂security
Linux 内核的 security 目录包括了不同的 Linux 安全模型的代码,对计算机免于受到病毒和黑客的侵害很重要。在 Linux 内核源码中,security 目录下包含了不同的 Linux 安全模型的代码,提供了与内核安全性相关的文档,例如 SELinux 和 AppArmor 的配置指南。
⒃sound
Linux 内核的 sound 目录包含常用音频设备的驱动程序等。在 Linux 内核源码中,sound / 包含了声音子系统相关的驱动程序。
⒄usr
Linux 内核的 usr 目录实现了一个 cpio。用于打包和压缩的 cpio 归档文件,initrd 的镜像,它可以作为内核启动后挂载的第一个文件系统。
⒅virt
Linux 内核的 virt 目录包含了虚拟化代码,它允许用户一次运行多个操作系统。通过虚拟化,客户机操作系统就像任何其他运行在 Linux 主机的应用程序一样运行。
三、主要文件说明
根目录下的单文件比较重要的是 Kbuild,Kconfig,Makefile 等文件。
3.1Config 文件
Make oldconfig 和 make defconfig 会把生成的默认的 configuration,执行 Make 之后会根据 Makefile 上下层级依赖关系编译整个系统,.config 作为编译各个模块的依据。在内核编译时,主 Makefile 会调用这个.config 文件,它记录了编译 Linux 内核时的配置选项,以及用户或者系统管理员选择的配置信息。这些配置选项包括了是否开启某些功能、是否支持某些硬件设备等等。
config 文件通常位于 Linux 内核源码的根目录下,用户可以通过命令行或者图形化界面的方式进行配置和保存。通过配置.config 文件,用户可以实现对 Linux 内核的个性化定制,以满足不同用户和系统的需求。对于系统管理员来说,.config 文件是非常重要的工具。通过配置.config 文件,管理员可以定制 Linux 内核,去掉不需要的功能,保持系统的精简和高效。同时,管理员也可以根据实际应用场景,选择适合的配置选项,以提升系统的性能和稳定性。
另外,对于 Linux 爱好者和开发者来说,.config 文件也具有重要的意义。他们可以通过配置.config 文件,定制 Linux 内核以支持特定的硬件设备或者功能,实现更广泛的应用。此外,通过分析.config 文件,还可以了解 Linux 内核所支持的功能和硬件设备,为后续的开发和定制提供参考。
Kbuild 文件:是 kernel build 的意思,就是内核编译的意思,设置一些内核设定的脚本。打个比方,这个脚本设定一个 ARCH 变量,这是开发者想要生成的内核支持的处理器类型。
3.2Kconfig 文件
这个脚本会在开发人员配置内核的时候用到。分布在各目录下的 Kconfig 构成了一个分布式的内核配置数据库,每个 Kconfig 分别描述了所属目录源文件相关的内核配置菜单。在内核配置 make menuconfig (或 xconfig 等) 时,从 Kconfig 中读出配置菜单,用户配置完后保存到.config (在顶层目录下生成) 中。
每个 config 菜单项都有类型定义,如 bool(布尔类型)、tristate(三态:内建、模块、移除)、string(字符串)、hex(十六进制)、integer(整型)。
作用:决定 make menuconfig 时展示的菜单项,Tristate 表示该项是否编进内核、编成模块。显示为<>,假如选择编译成内核模块,则会在.config 中生成一个 CONFIG_HELLO_MODULE=m 的配置,选择 Y 就是直接编进内核,会在.config 中生成一个 CONFIG_HELLO_MODULE=y 的配置项。Tristate 后的字符串是 make menuconfig 时显示的配置项名称。
bool 类型只能选中或不选中,make menuconfig 时显示为 [ ],即无法配置成模块。depend on 指该选项依赖于另一个选项,只有当依赖项被选中时,当前配置项的提示信息才会出现,才能设置当前配置项。select 是反向依赖关系,该选项选中时,同时选中 select 后面定义的那一项。help 是帮助信息。目录层次迭代:Kconfig 中有类似语句:source "drivers/usb/Kconfig",用来包含(或嵌套)新的 Kconfig 文件,使得各个目录管理各自的配置内容,不必把那些配置都写在同一个文件里,方便修改和管理。
3.3Makefile 文件
这个脚本是编译内核的主要文件。这个文件将编译参数和编译所需的文件和必要的信息传给编译器。顶层 Makefile 定义并向环境中输出了许多变量,为各个子目录下的 Makefile 传递一些信息。有些变量,比如 SUBDIRS,不仅在顶层 Makefile 中定义并且赋初值,而且在 arch//Makefile 还作了扩充。
常用的变量有版本信息(VERSION,PATCHLEVEL,SUBLEVEL,EXTRAVERSION,KERNELRELEASE)、CPU 体系结构(ARCH)、路径信息(TOPDIR,SUBDIRS)、内核组成信息(HEAD,CORE_FILES,NETWORKS,DRIVERS,LIBS)、编译信息(CPP,CC,AS,LD,AR,CFLAGS,LINKFLAGS)等。
Makefile 中的变量顶层 Makefile 定义并向环境中输出了许多变量,为各个子目录下的 Makefile 传递一些信息。有些变量,比如 SUBDIRS,不仅在顶层 Makefile 中定义并且赋初值,而且在 arch//Makefile 还作了扩充。常用的变量有版本信息(VERSION,PATCHLEVEL,SUBLEVEL,EXTRAVERSION,KERNELRELEASE)、CPU 体系结构(ARCH)、路径信息(TOPDIR,SUBDIRS)、内核组成信息(HEAD,CORE_FILES,NETWORKS,DRIVERS,LIBS)、编译信息(CPP,CC,AS,LD,AR,CFLAGS,LINKFLAGS)等。
Makefile 的作用是根据配置的情况,构造出需要编译的源文件列表,然后分别编译,并把目标代码链接到一起,最终形成 linux 内核二进制文件。由于 Linux 内核源代码是按照树形结构组织的,所以 Makefile 也被分布在目录树中。
Linux 内核中的 Makefile 以及与 Makefile 直接相关的文件有:Makefile(顶层 Makefile,是整个内核配置、编译的总体控制文件)、config(内核配置文件,包含由用户选择的配置选项,用来存放内核配置后的结果)、arch/*/Makefile(位于各种 CPU 体系目录下的 Makefile,是针对特定平台的 Makefile)、各个子目录下的 Makefile(负责所在子目录下源代码的管理)、Rules.make(规则文件,被所有的 Makefile 使用,定义了所有 Makefile 共用的编译规则)。
四、总结
Linux 内核源码的目录结构体现了其模块化和分层设计的理念。每个目录都有其特定的功能和用途,各个子系统之间相互协作,共同构建了一个强大的操作系统内核。通过深入了解内核源码的目录结构,开发者可以更高效地理解内核的工作原理,进行内核的开发和维护工作。