Linux内核编程——Linux内核导论

0 阅读13分钟

介绍

本章将介绍 Linux 内核架构的内部结构,并带领我们进行首次实践,内容包括获取内核源码、定义要构建的模块配置、构建内核本身,最后通过 QEMU 虚拟机执行内核。

结构

本章涵盖以下主题:

  • 准备工作环境
  • 内核简介
  • 下载 GNU/Linux 内核源码以重新编译
  • 从 kernel.org 下载官方压缩包
  • 配置自定义内核和模块
  • 构建自己的内核
  • 安装
  • 执行与启动流程
  • 代码风格

目标

完成本章后,你将能够理解 GNU/Linux 内核源码树的结构,以及 Linux 内核的生成流程,这将帮助我们开始实际的驱动开发工作。

准备工作环境

为了保持命令的一致性,本书中所有命令均在 Ubuntu 22.04.3 X86_64 LTS 版本的 Linux 发行版中验证通过。但通过少量适配,这些命令也能无障碍应用于其他 Linux 发行版。
本章后续需要安装若干实用软件包:

$ sudo apt install git build-essential curl qemu-system-gui qemu-system-utils qemu-system-x86 gnupg2 bison flex libncurses5-dev libglade2-dev libglade2-0 libgtk2.0-0 libgtk2.0-dev libglib2.0-0 libglib2.0-dev

本书聚焦于 GNU/Linux 内核,将逐步深入分析。但我们也需要一个 Linux 系统(通常称为 rootfs,即用户空间部分),以便整体验证内核。
为此,我们将使用 Buildroot 项目(buildroot.org,嵌入式领域常用)生成一个 ext2 格式的根文件系统镜像。
以下为项目源码默认分支(master/HEAD)拉取命令:

$ git clone --depth 1 https://git.busybox.net/buildroot/ && cd buildroot/
Cloning into 'buildroot'...
remote: Enumerating objects: 17248, done.
remote: Counting objects: 100% (17248/17248), done.
remote: Compressing objects: 100% (16360/16360), done.
remote: Total 17248 (delta 888), reused 11876 (delta 446), pack-reused 0
Receiving objects: 100% (17248/17248), 7.96 MiB | 8.20 MiB/s, done.
Resolving deltas: 100% (888/888), done.

查看分支:

$ git branch
* master

查看提交日志(示例):

$ git log
commit d797ecc6f026e1f0af17f91df33e8cd44731a97f (grafted, HEAD -> master, origin/master, origin/HEAD)
Author: Thomas Petazzoni <thomas.petazzoni@bootlin.com>
(...)

接下来需要定义 rootfs 配置,相当于构建的“身份证”,指明构建内容及方式。下面是一段适用于 x86_64 架构的配置示例:

$ nano .config
# 选择目标架构(例如 x86_64)
BR2_x86_64=y
# 设置系统主机名
BR2_TARGET_GENERIC_HOSTNAME="bpb"
# 配置根文件系统为 initramfs
BR2_TARGET_ROOTFS_INITRAMFS=y

# 跳过 Linux 内核构建(因为不打算构建内核)
BR2_LINUX_KERNEL=n
# 选择 ext2 格式作为目标镜像
BR2_TARGET_ROOTFS_EXT2=y
BR2_TARGET_ROOTFS_EXT2_2=y
BR2_TARGET_ROOTFS_EXT2_2r1=y
BR2_TARGET_ROOTFS_EXT2_GEN=2
BR2_TARGET_ROOTFS_EXT2_REV=1
BR2_TARGET_ROOTFS_EXT2_LABEL="rootfs"
BR2_TARGET_ROOTFS_EXT2_SIZE="250M"
BR2_TARGET_ROOTFS_EXT2_INODES=0
BR2_TARGET_ROOTFS_EXT2_INODE_SIZE=256
BR2_TARGET_ROOTFS_EXT2_RESBLKS=5
BR2_TARGET_ROOTFS_EXT2_MKFS_OPTIONS="-O ^64bit"
BR2_TARGET_ROOTFS_EXT2_NONE=y

在 Buildroot 中,defconfig(默认配置)是一个配置文件,指定构建嵌入式 Linux 系统根文件系统和交叉编译环境的默认设置与选项。该文件包含诸如包含哪些软件包、内核选项、目标架构设置等参数。
生成默认配置:

$ make defconfig

配置结果大致如下:

$ cat .config | grep -v "#" | grep -v '^$'
(...)
BR2_HAVE_DOT_CONFIG=y
BR2_HOST_GCC_AT_LEAST_4_9=y
BR2_HOST_GCC_AT_LEAST_5=y
BR2_HOST_GCC_AT_LEAST_6=y
BR2_HOST_GCC_AT_LEAST_7=y
BR2_HOST_GCC_AT_LEAST_8=y
BR2_HOST_GCC_AT_LEAST_9=y
BR2_USE_MMU=y
BR2_i386=y
BR2_ARCH_HAS_TOOLCHAIN_BUILDROOT=y
BR2_ARCH="i686"
BR2_NORMALIZED_ARCH="i386"
BR2_ENDIAN="LITTLE"
BR2_GCC_TARGET_ARCH="i686"
BR2_BINFMT_SUPPORTS_SHARED=y
BR2_READELF_ARCH_NAME="Intel 80386"
BR2_x86_i686=y
(...)

若想定制最终的 rootfs,添加更多工具,可启动基于 ncurses 的配置界面:

$ make menuconfig

接下来是构建步骤。构建所需时间取决于电脑性能和网络带宽。以现代电脑和光纤网络为例,大约需30分钟或更久:

$ time make
make 5540,18s user 583,48s system 308% cpu 33:07,71 total

构建完成后,会得到多种格式的 rootfs:

$ ls -alh output/images
total 12M
drwxr-xr-x 2 tgayet tgayet 4,0K sept. 30 22:01 .
drwxrwxr-x 6 tgayet tgayet 4,0K sept. 30 21:53 ..
-rw-r--r-- 1 tgayet tgayet  60M sept. 30 22:01 rootfs.ext2
-rw-r--r-- 1 tgayet tgayet 4,2M sept. 30 22:01 rootfs.tar

能够用仅约 60 MB 大小的镜像获得一个完整的 Linux 系统,这也解释了为什么 Buildroot 和 Yocto 构建在嵌入式领域广泛应用。
文件系统层次标准(Filesystem Hierarchy Standard,FHS)是一套规范和约定,定义了 Linux 系统中文件和目录的结构组织。下图展示了 Linux 发行版文件系统的层次结构细节:

image.png

FHS(文件系统层次结构标准)不是一个正式的标准,但被广泛被 Linux 发行版采用,以确保不同发行版之间的一致性和兼容性。这是因为 /boot 目录包含系统启动所必需的与引导加载程序相关的文件,例如内核 initrd(初始 RAM 磁盘镜像)、vmlinuz(Virtual Memory LINUx gzip 压缩的 Linux 内核可执行文件)和 grub(Grand Unified Bootloader,统一引导加载程序)。

注意:这里是 vmlinuz 而非 vmlinux。vmlinuz 是 Linux 虚拟内存的压缩内核可执行文件,而非未压缩的 vmlinux。

为了测试和验证我们的 rootfs,我们将使用当前发行版的内核:

$ ls -alh /boot | grep $(uname -r)
-rw-r--r--  1 root root 270K sept.  7 09:11 config-6.2.0-33-generic
lrwxrwxrwx  1 root root   27 sept. 19 13:30 initrd.img -> initrd.img-6.2.0-33-generic
-rw-r--r--  1 root root  72M sept. 19 13:31 initrd.img-6.2.0-33-generic
-rw-------  1 root root 7,7M sept.  7 09:11 System.map-6.2.0-33-generic
lrwxrwxrwx  1 root root   24 sept. 19 13:30 vmlinuz -> vmlinuz-6.2.0-33-generic
-rw-------  1 root root  14M sept.  7 10:18 vmlinuz-6.2.0-33-generic

当前运行的内核版本是 6.2.0-33-generic,可以通过 uname 命令查看:

$ uname -r
6.2.0-33-generic

借助 qemu,我们可以测试我们将要生成的内核,但现在也可以使用我们当前的 Linux 发行版内核进行测试。出于访问权限的考虑,测试前先将预构建的内核复制到临时目录:

$ sudo cp /boot/vmlinuz-$(uname -r) /tmp/vmlinuz-$(uname -r)
$ sudo chmod 777 /tmp/vmlinuz-$(uname -r)

然后使用 qemu 运行内核:

$ qemu-system-x86_64 -kernel /tmp/vmlinuz-$(uname -r) -hda output/images/rootfs.ext2 -nographic -append root="/dev/sda console=ttyS0" -net nic,macaddr=00:11:22:33:44:55 -net user

qemu 命令中参数说明:

  • -append:指定内核参数。本例中 root=/dev/hda 表示根文件系统使用支持的文件系统(ext2),console=ttyS0 指定内核的串口控制台输出。
  • -nographic:禁用图形输出,适合文本模式启动。
  • -net nic,macaddr=00:11:22:33:44:55:指定虚拟网络接口的 MAC 地址。
  • -net user:配置用户网络接口,使虚拟机可通过网络访问。

下图展示了使用先前生成的 rootfs 启动发行版内核的过程:

image.png

我们将使用这些 rootfs 以及 qemu 来测试我们将要生成的内核。
该 rootfs 的登录用户名为 root,且无密码。

我们可以确认所用内核版本与发行版内核版本一致,即 6.2.0-33-generic。此外,rootfs 已正确挂载在根目录 /,并使用 ext2 文件系统。

虽然我们详细介绍了如何生成自定义 rootfs,但本书关联的 GitHub 上也提供了一个可直接使用的 rootfs(rootfs.ext2)供下载。

:用于配置和构建的工具与 GNU/Linux 内核项目中使用的完全相同。

内核简介

如第一章《GNU/Linux 内核历史》所述,GNU/Linux 内核是一个包含多个层次和模块的宏内核,其架构随着时间不断演进。Linux 作为一个宏内核,采用了模块化且现代化的设计(支持在运行时插入和移除可加载内核模块),并且支持大多数以往仅存在于非自由操作系统封闭内核中的功能。
下图展示了 GNU/Linux 内核的架构:

image.png

以下是 GNU/Linux 内核的一些技术特性:

宏内核架构
Linux 内核采用宏内核架构,整个操作系统内核作为一个单一的、具有最高权限的进程运行在内核空间。
内核空间是内存中的受保护区域,用户空间进程无法访问。
支持在 SMP(对称多处理)和 NUMA(非统一内存访问)架构下,多个进程及其线程同时并发计算。

单一地址空间
在宏内核中,所有内核组件共享同一地址空间。
这意味着内核模块和子系统可以直接调用彼此的函数,访问彼此的数据结构,而无需通过定义良好的 API。

高权限级别
内核代码运行在 CPU 的最高权限级别,通常称为内核模式。
这使内核能够执行敏感且特权的操作,如管理硬件资源和控制用户空间进程的执行。

内核子系统
Linux 内核划分为多个子系统,负责操作系统的不同方面,包括进程管理、内存管理、文件系统、网络、设备驱动等。
每个子系统负责特定功能,包含自己的数据结构和函数集。

模块的动态加载
虽然核心内核是宏内核结构,但 Linux 支持动态加载和卸载内核模块。
内核模块是可加载的内核对象,能在内核运行时添加或扩展功能,无需重新编译内核。
模块机制提高了内核的模块化,支持广泛的硬件而不膨胀内核镜像。

同步与并发
由于所有内核代码运行在单一地址空间,宏内核必须谨慎管理并发和同步。
使用锁、信号量、自旋锁等机制保护共享数据结构,确保线程安全。

广泛的设备支持
宏内核通常内置大量设备驱动,支持各种硬件设备。
设备驱动作为内核的一部分,能够直接与硬件交互,实现高效访问。

高效的函数调用
内核函数因处于同一地址空间,函数调用开销极低。
内核内的函数调用类似用户空间程序中的普通函数调用。

内核崩溃(Kernel Panic)
遇到严重错误或不可恢复故障时,宏内核会触发内核崩溃,停止系统运行以防止进一步损害。
内核崩溃通常会生成调用栈和诊断信息,便于调试。

复杂性
宏内核因集成大量子系统和设备驱动,代码复杂度较高。
这增加了理解和维护内核代码的难度。

高性能
宏内核通常能提供较高性能,因其消除了微内核架构中进程间通信(IPC)的开销。
内核函数可针对底层硬件访问进行优化。

体积与启动时间
宏内核体积可能较大,因包含众多子系统和驱动。
内核体积和启动时间受启用功能和驱动数量影响。

设备驱动的权限与位置
设备驱动运行在内核空间(许多 CPU 架构中的环 0),拥有对硬件的完全特权访问权。

不过,也有部分模块设计为运行在用户空间,如基于 FUSE/CUSE 的文件系统模块,以及部分使用 libusb 库的 UIO 或 USB 模块。
Linux 中的窗口系统协议,如 X Window System 和 Wayland,不运行于内核。

与传统宏内核不同,Linux 设备驱动支持以模块形式灵活配置,运行时可加载或卸载,且在某些条件下支持抢占,以便更好地处理硬件中断和支持对称多处理。

因此,Linux 并无稳定的设备驱动应用二进制接口(ABI)。

内存管理
Linux 通常启用内存保护和虚拟内存管理,也能处理非统一内存访问(NUMA)。

μClinux 项目支持在无虚拟内存的微控制器上运行 Linux。

硬件与文件系统映射
硬件设备在 ramfs 文件层级中表示。
用户应用通常通过约定,通过 /dev 或 /sysfs 目录中的条目与设备驱动交互。

进程信息则映射至 /procfs 文件系统。

image.png

这种模块化视角对我们后续非常有用,尤其是在配置阶段需要牢记,以便选择内核的具体内容。
构建 Linux 内核的步骤如下:

  1. 获取源码,如果是压缩包,还需验证归档文件的真实性。
  2. 配置构建,确定生成的静态部分和动态模块。
  3. 编译内核及动态模块。
  4. 安装内核。
  5. 使用预构建的 rootfs(rootfs.ext2)和 qemu 测试内核。

我们将逐一详细讲解这些步骤。

下载 GNU/Linux 内核源码以重新编译

本节将介绍如何通过多种方式获取 GNU/Linux 内核源码。

使用发行版的软件包

修改当前内核配置的第一种方法是,从系统中已安装的源码包开始,这些源码一般存放于 /usr/src/ 目录下。
每个内核版本对应一个以 linux-headers 为前缀、后接版本号和额外版本号的目录。
针对某个版本,通常有多个相关软件包,包括运行时内核和其源码(linux-headers)。
例如,使用以下命令搜索与当前内核版本对应的软件包:

$ apt-cache search `uname -r`
linux-buildinfo-6.2.0-33-generic - Linux kernel buildinfo for version 6.2.0 on 64 bit x86 SMP
linux-cloud-tools-6.2.0-33-generic - Linux kernel version specific cloud tools for version 6.2.0-33
linux-headers-6.2.0-33-generic - Linux kernel headers for version 6.2.0 on 64 bit x86 SMP
linux-image-6.2.0-33-generic - Signed kernel image generic
linux-image-unsigned-6.2.0-33-generic - Linux kernel image for version 6.2.0 on 64 bit x86 SMP
linux-modules-6.2.0-33-generic - Linux kernel extra modules for version 6.2.0 on 64 bit x86 SMP
linux-modules-extra-6.2.0-33-generic - Linux kernel extra modules for version 6.2.0 on 64 bit x86 SMP
linux-modules-ipu6-6.2.0-33-generic - Linux kernel ipu6 modules for version 6.2.0-33
linux-modules-ivsc-6.2.0-33-generic - Linux kernel ivsc modules for version 6.2.0-33
(...)

安装源码包后,源码位于 /usr/src/linux-headers-<version> 目录:

$ ls -ald /usr/src/linux-headers*
drwxr-xr-x  7 root root 4096 sept.  5 13:41 /usr/src/linux-headers-6.2.0-32-generic
drwxr-xr-x  7 root root 4096 sept. 19 13:26 /usr/src/linux-headers-6.2.0-33-generic

进入对应版本源码目录:

$ cd /usr/src/linux-headers-$(uname -r)
$ pwd
/usr/src/linux-headers-6.2.0-33-generic
$ ls -alh
total 2,3M
drwxr-xr-x  7 root root 4,0K sept. 19 13:26 .
drwxr-xr-x 17 root root 4,0K sept. 19 14:11 ..
drwxr-xr-x  3 root root 4,0K sept. 19 13:26 arch
lrwxrwxrwx  1 root root   39 sept.  7 09:11 block -> ../linux-hwe-6.2-headers-6.2.0-33/block
lrwxrwxrwx  1 root root   39 sept.  7 09:11 certs -> ../linux-hwe-6.2-headers-6.2.0-33/certs
-rw-r--r--  1 root root    0 sept.  7 09:11 .checked-atomic-arch-fallback.h
-rw-r--r--  1 root root    0 sept.  7 09:11 .checked-atomic-instrumented.h
-rw-r--r--  1 root root    0 sept.  7 09:11 .checked-atomic-long.h
-rw-r--r--  1 root root 270K sept.  7 09:11 .config
lrwxrwxrwx  1 root root   40 sept.  7 09:11 crypto -> ../linux-hwe-6.2-headers-6.2.0-33/crypto
lrwxrwxrwx  1 root root   47 sept.  7 09:11 Documentation -> ../linux-hwe-6.2-headers-6.2.0-33/Documentation
lrwxrwxrwx  1 root root   41 sept.  7 09:11 drivers -> ../linux-hwe-6.2-headers-6.2.0-33/drivers
lrwxrwxrwx  1 root root   36 sept.  7 09:11 fs -> ../linux-hwe-6.2-headers-6.2.0-33/fs
-rw-r--r--  1 root root   39 sept.  7 09:11 .gitignore
drwxr-xr-x  4 root root 4,0K sept. 19 13:26 include
lrwxrwxrwx  1 root root   38 sept.  7 09:11 init -> ../linux-hwe-6.2-headers-6.2.0-33/init
lrwxrwxrwx  1 root root   42 sept.  7 09:11 io_uring -> ../linux-hwe-6.2-headers-6.2.0-33/io_uring
lrwxrwxrwx  1 root root   37 sept.  7 09:11 ipc -> ../linux-hwe-6.2-headers-6.2.0-33/ipc
lrwxrwxrwx  1 root root   40 sept.  7 09:11 Kbuild -> ../linux-hwe-6.2-headers-6.2.0-33/Kbuild
lrwxrwxrwx  1 root root   41 sept.  7 09:11 Kconfig -> ../linux-hwe-6.2-headers-6.2.0-33/Kconfig
drwxr-xr-x  2 root root 4,0K sept. 19 13:26 kernel
lrwxrwxrwx  1 root root   37 sept.  7 09:11 lib -> ../linux-hwe-6.2-headers-6.2.0-33/lib
-rw-r--r--  1 root root  71K sept.  7 09:11 Makefile
-rw-r--r--  1 root root 1,3K sept.  7 09:11 .missing-syscalls.d
lrwxrwxrwx  1 root root   36 sept.  7 09:11 mm -> ../linux-hwe-6.2-headers-6.2.0-33/mm
-rw-r--r--  1 root root 1,9M sept.  7 09:11 Module.symvers
lrwxrwxrwx  1 root root   37 sept.  7 09:11 net -> ../linux-hwe-6.2-headers-6.2.0-33/net
lrwxrwxrwx  1 root root   47 sept.  7 09:11 rust -> ../linux-hwe-6.2-lib-rust-6.2.0-33-generic/rust
lrwxrwxrwx  1 root root   41 sept.  7 09:11 samples -> ../linux-hwe-6.2-headers-6.2.0-33/samples
drwxr-xr-x  7 root root  12K sept. 19 13:26 scripts
lrwxrwxrwx  1 root root   42 sept.  7 09:11 security -> ../linux-hwe-6.2-headers-6.2.0-33/security
lrwxrwxrwx  1 root root   39 sept.  7 09:11 sound -> ../linux-hwe-6.2-headers-6.2.0-33/sound
drwxr-xr-x  4 root root 4,0K sept. 19 13:26 tools
lrwxrwxrwx  1 root root   40 sept.  7 09:11 ubuntu -> ../linux-hwe-6.2-headers-6.2.0-33/ubuntu
lrwxrwxrwx  1 root root   37 sept.  7 09:11 usr -> ../linux-hwe-6.2-headers-6.2.0-33/usr
lrwxrwxrwx  1 root root   38 sept.  7 09:11 virt -> ../linux-hwe-6.2-headers-6.2.0-33/virt

关于内核本身的配置,存在两个位置:
一是在上述源码目录中的 .config 文件(如 /usr/src/linux-headers-$(uname -r)/.config);
二是在 /boot 目录下的配置文件:

$ ls -alh /boot/config-`uname -r`*
-rw-r--r-- 1 root root 270K sept.  7 09:11 /boot/config-6.2.0-33-generic

从 kernel.org 下载官方压缩包

如果你想编译当前内核的不同版本,最简单的方法是从官方 kernel.org 网站获取对应内核版本的源码:

image.png

该网站目前只支持四种协议,而过去支持八种:HTTPS、GIT 和 RSYNC。网站上列出了多个内核版本:稳定版(stable)、生命周期结束版(EOL)或长期支持版(LTS)。
当一个内核版本从主线分支(mainline)进入稳定分支时,可能发生以下两种情况:

  • 主线内核(Mainline kernels) :由 GNU/Linux 内核开发社区发布的最新版本,通常支持周期较短,很快会被新版本替代。
  • 长期支持内核(Long Term Support, LTS) :部分内核版本被标记为 LTS,意味着它们拥有更长的支持周期。LTS 版本会得到数年的维护与安全补丁更新,通常为 2 至 6 年甚至更长,具体视内核版本而定。
  • 生命周期结束内核(End of Life, EOL) :经过若干次错误修复后,维护者不再发布该版本的补丁。如果内核被标记为 EOL,需计划升级至下一个主要稳定版本。

注意每次内核发布都会更新的 XML 和 JSON 文件:

下载源码压缩包可以用 curl 命令:

$ curl -OL https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.5.5.tar.xz

下载输出示例:

100  132M  100  132M    0     0  5208k      0  0:00:26  0:00:26 --:--:-- 3713k

下载 Linux 内核源码后,通常需要确认源码来源以保证可追溯性,这在 DevSecOps 中非常重要,有助于防范恶意代码。为此,kernel.org 提供了 GPG 指纹以验证压缩包:

$ curl -OL https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.5.5.tar.sign

下载签名文件示例:

100   987  100   987    0     0   3005      0 --:--:-- --:--:-- --:--:--  3009

接下来需要导入与 GNU/Linux 项目官方维护者相关的两个 GPG 公钥:

$ gpg2 --locate-keys torvalds@kernel.org gregkh@kernel.org

导入结果示例:

gpg: key 38DBBDC86092693E: public key "Greg Kroah-Hartman <gregkh@kernel.org>" imported
gpg: key 79BE3E4300411886: public key "Linus Torvalds <torvalds@kernel.org>" imported

你会注意到签名是针对未压缩的归档文件完成的,因此压缩版(.gz 和 .xz)只需一个签名文件即可。
所以要先解压归档文件(这里以 unxz 为例):

$ unxz linux-6.5.5.tar.xz

然后用签名文件验证 .tar 文件:

$ gpg2 --verify linux-6.5.5.tar.sign

验证输出示例:

gpg: Good signature from "Greg Kroah-Hartman <gregkh@kernel.org>"

也可以将解压和验证合并成一条命令:

$ xz -cd linux-6.5.5.tar.xz | gpg2 --verify linux-6.5.5.tar.sign -

注意:有一套脚本可以帮助你验证官方内核,地址:git.kernel.org/pub/scm/lin…

克隆官方 Git 仓库
另一种备受开发者青睐的方法是从官方 GIT 仓库获取源码:

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

克隆过程示例:

Receiving objects: 100% (9721192/9721192), 2.69 GiB | 16.76 MiB/s, done.

查询所有可用标签列表:

$ git tag --list

示例标签:

v6.5
v6.5-rc1
v6.5-rc2
...
v6.6-rc1
v6.6-rc2
...

可以搜索与你的 Ubuntu 版本内核最接近的标签,例如 6.2.0-33-generic,对应 v6.2 发布版本:

$ git tag --list | grep 6.2

切换到 v6.2 标签并创建分支:

$ git checkout -b linux_6.2 v6.2

至此,我们已准备好进入 Linux 内核配置步骤。

自定义内核和模块的配置

配置步骤的目的是定义 GNU/Linux 内核功能的身份映射。这将使得许多功能可以静态地集成在宏内核中(静态部分),或以动态模块的形式加载(动态部分)。
配置工具的作用是生成一个 .config 文件,该文件总结了所有需要激活的功能。.config 文件就像构建的身份证。
配置工具通过向主 Makefile 传递特定目标名称来调用。这个配置模块与许多其他项目中使用的类似,如 Buildroot、Ptxdist、crosstool-ng 等。它由 Yann Morin 于数年前重新开发。

配置工具的类型包括:

  • config:使用文本界面更新配置
  • nconfig:使用 ncurses 界面更新配置(简洁黑白版)
  • menuconfig:使用 ncurses 界面更新配置(彩色版)
  • xconfig:使用 QT 界面更新配置
  • gconfig:使用 GTK 界面更新配置
  • oldconfig:使用内核源码中已有的 .config 文件更新配置
  • localmodconfig:通过禁用未加载的动态模块更新当前配置
  • localyesconfig:通过将动态模块整合到内核静态部分来更新配置
  • silentoldconfig:类似 oldconfig,但静默更新依赖关系
  • defconfig:使用特定架构的默认设置更新配置
  • savedefconfig:将当前配置保存为最小化的配置文件
  • allnoconfig:创建所有选项均为“否”的新配置
  • allyesconfig:创建所有选项均为“是”的新配置
  • allmodconfig:创建所有选项均为模块的配置
  • alldefconfig:创建所有请求选项均为默认值的配置
  • randconfig:通过随机选择启用或禁用选项定义新配置
  • listnewconfig:列出新增选项
  • olddefconfig:类似 silentoldconfig,但将新增符号设置为默认值

下图展示了不同类型配置工具的部分界面截图:

image.png

GNU/Linux 内核的配置是定制内核行为和功能以满足特定需求的关键步骤。
每个菜单项最多有三种状态:选为内核静态部分(y)、选为动态模块(m)、未选中。
下图展示了使用 QT 配置工具的示例:

image.png

每个功能激活项可以包含于内核的静态部分,或在可能的情况下单独作为可动态加载的模块:

  • ISO9660 CDROM 文件系统支持:将作为与内核分离的动态 .ko 模块包含;如果此配置被选中,则其子依赖项也会被启用。
  • UDF 文件系统支持:将包含在 Linux 内核的静态部分。

生成的配置文件中,每个功能对应一行,遵循以下规则:

  • 以数字开头且以“is not set”结尾的行表示该功能未被启用,不会被采纳。
  • =m 结尾的行表示该功能将生成动态模块。
  • =y 结尾的行表示该功能将包含在内核静态部分。

示例 .config 文件摘录:

## CD-ROM/DVD 文件系统
#CONFIG_ISO9660_FS=m
CONFIG_JOLIET=y
CONFIG_ZISOFS=y
CONFIG_UDF_FS=y
CONFIG_UDF_NLS=y

## DOS/FAT/NT 文件系统
## CONFIG_MSDOS_FS is not set
#CONFIG_VFAT_FS is not set
CONFIG_NTFS_FS=m
# CONFIG_NTFS_DEBUG is not set
CONFIG_NTFS_RW=y

内核配置包含选项之间的依赖关系。例如,启用某个网络驱动会自动激活网络协议栈。依赖关系有两种类型:

  • 依赖的依赖:当选项 B 依赖于选项 A 时,若 B 未激活,则 A 不会显示。
  • 依赖选择:当选项 A 激活时,自动激活其依赖的选项 B。

可以使用 xconfig 显示所有选项(包括未激活的),未激活选项会被灰显。

配置阶段对选择内核驱动至关重要:

  • 应避免包含不必要的驱动,否则会增加内核体积。
  • 静态编译驱动与动态模块驱动的比例应考虑内核加载时间和初始化时间。
  • 静态驱动过多会导致较长加载时间,除非使用 XIP(执行就地)和 AXFS。

内核配置一般使用工具如 make menuconfigmake nconfigmake xconfig 等,这些工具提供用户友好的界面来修改配置选项。

定制 Linux 内核时可配置的关键主题包括:

  • 代码成熟度:显示或隐藏仍处于开发阶段的选项(不稳定)。若想使用最新内核特性,通常应启用。
  • 通用设置:系统的一般选项,如内核版本、架构(32 位或 64 位)、压缩方法、命令行、定时器频率、抢占模型等。
  • 块层:主板输入输出相关,可针对嵌入式设备移除。
  • 可加载模块支持:控制内核模块管理(默认配置适用于常规使用)。
  • 处理器类型和特性:处理器架构(如 x86、Sparc)、超线程、双核、SMP 等。
  • 电源管理和 ACPI:省电、待机及高级配置和电源接口支持。
  • 总线选项:管理 PCI、USB、I2C 等各种总线和控制器。
  • 设备驱动:支持各种硬件设备,如网络卡、存储设备、声卡、显卡等。
  • 文件系统:支持的文件系统类型,如 ext4、Btrfs、XFS,网络文件系统等。
  • 内核特性:实时支持、调度器、POSIX 兼容性、调试追踪、安全选项等。
  • 网络支持:协议栈和网络功能配置。
  • 安全选项:强制访问控制、加密选项、安全增强等。
  • 性能监控和剖析支持
  • 内核文档:是否启用内核文档支持(对开发有用)

以上只是部分关键配置主题,具体选项和可用性会因内核版本和硬件而异。配置时应根据系统需求和限制,精心挑选功能,避免不必要的模块,以获得定制化且高效的内核。

与发行版打包版本(如 .deb.rpm)不同,从 kernel.org 下载的源码压缩包默认不含配置文件。
在 Linux 发行版中,通常需要获取当前内核的配置文件以作为起点。

首先,用 uname 命令查看当前内核版本:

$ uname -a
Linux tgayet-DS87D 6.2.0-33-generic #33~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep  7 10:33:52 UTC  x86_64 x86_64 x86_64 GNU/Linux

然后,将当前内核配置文件复制到内核源码根目录,路径可来自 /boot

$ cp /boot/config-`uname -r`* .config

或者:

$ cp /usr/src/linux-headers-$(uname -r)/.config .config

要使该 .config 文件生效,运行:

$ make oldconfig

如果当前内核启用了伪文件系统 /proc/config.gz,也可以直接从当前运行内核导出配置文件:

$ zcat /proc/config.gz > .config

但前提是内核配置启用了:

CONFIG_IKCONFIG=y
CONFIG_IKCONFIG_PROC=y

对应内核菜单中设置:

General setup ---> 
  [*] Kernel .config support
  [*] Enable access to .config through /proc/config.gz

完成配置阶段后,.config 文件应位于源码根目录。但需要注意,这一功能在当前内核中越来越少被启用。

开始编译前,可以查看 Makefile 顶部的内核版本变量,用于标识内核:

VERSION = 6
PATCHLEVEL = 2
SUBLEVEL = 0
EXTRAVERSION = -bpb
NAME = Kernel linux for bpb book

还可以通过在 .config 文件中添加 CONFIG_LOCALVERSION 来追加内核额外版本号,需以连字符开头,用于版本号与自定义字符串的分隔:

CONFIG_LOCALVERSION = -custom

例如,若设置 -custom,则使用 uname -r 查看时,内核版本可能显示为 5.14.0-custom

构建自定义内核

内核配置完成后,就需要进行构建。

在进入编译阶段前,请确认已安装以下必要工具:

软件最低版本检查命令
Gnu C2.95.3gcc --version
Gnu make3.78make --version
binutils2.12ld -v
util-linux2.10ofdformat --version
module-init-tools0.9.10depmod -V
e2fsprogs1.29tune2fs
jfsutils1.1.3fsck.jfs -V
reiserfsprogs3.6.3reiserfsck -V
xfsprogs2.1.0xfs_db -V
pcmcia-cs3.1.21cardmgr -V
quota-tools3.09quota -V
PPP2.4.0pppd --version
isdn4k-utils3.1pre1`isdnctrl 2>&1
nfs-utils1.0.5showmount --version
procps3.1.13ps --version
oprofile0.5.3oprofiled --version

(表 2.1:构建内核所需工具)

GNU/Linux 内核的生成分两步进行:

  1. 内核静态部分(如 vmlinux、bzImage 等)
  2. 驱动/模块/内核对象的动态部分(*.ko 文件)

从命令行源码生成内核的步骤如下:

首先生成内核脚本(最基本的目标):

$ make scripts

会编译生成各种辅助工具,如 genksyms、selinux 工具等。

接着直接运行 make 命令即可开始编译,make 会依据 .config 文件中定义的配置:

$ make

默认情况下,编译过程比较简洁,只有在发生严重错误时才输出详细信息。

如果想切换到详细模式,可以通过 V 参数控制:

$ make V=1  # 详细模式
$ make V=0  # 静默模式(默认)

还存在 V=2 等其他参数,另有类似 CW 等参数可用。

若编译机器拥有多个处理器或核心,可利用 -j--jobs)和 -l--load-average)参数实现并行编译,提升效率:

  • -j 设置允许同时运行的最大任务数。
  • -l 限制在系统负载低于指定值时才启动新任务。
    一般建议将并行任务数设置略高于核心数,以充分利用 CPU。

可以用如下命令计算核心数:

$ export NBCORE=$(($(grep -c processor /proc/cpuinfo)+1))
# 或者
$ export NBCORE=$(($(cat /proc/cpuinfo | grep processor | wc -l)+1))

比如 Intel Core i7 结果为 9,Intel Core i3 结果为 5。

结合实际核心数进行编译示例:

$ export NBCORE=$(($(cat /proc/cpuinfo | grep processor | wc -l)+1))
$ make --jobs=$((NBCORE+1)) --load-average=${NBCORE}

即使不在源码目录,也可通过 -C 参数指定源码路径:

$ make -j9 -C ~/linux_src/

构建完成后,主要会生成以下文件:

  • ./vmlinux:未压缩的 Linux 内核,用于调试。
  • ./System.map:静态部分符号表。
  • ./arch/<ARCH>/boot/bzImage:压缩版 Linux 内核镜像。

vmlinux 是 ELF 格式文件,常用于调试和性能分析,但通常不会直接加载到内存,因内核启动时偏好加载压缩的 bzImage

$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=..., not stripped

$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 6.2.0-bpb-dirty ..., SMP PREEMPT_DYNAMIC ...

注意,i386 架构被重定向至 x86:

arch/i386/boot/bzImage -> ../../x86/boot/bzImage (符号链接)

构建完成后,即可使用 qemu 和前面生成的 rootfs 测试刚编译的内核:

$ qemu-system-x86_64 -kernel ./arch/x86/boot/bzImage -hda ./rootfs.ext2 -nographic -append root="/dev/sda console=ttyS0" -net nic,macaddr=00:11:22:33:44:55 -net user

image.png

我们可以通过 EXTRAVERSION 提供的后缀确认这是我们的内核,同时也可以通过查看传递给内核的参数(可通过 /proc/cmdline 访问)来确认。

在 Linux 内核上下文中,“dirty”一词并不指内核代码或数据的“脏”与否,而是具有特定的技术含义,涉及内存管理和缓存机制。

在 Linux 内核中,“dirty”状态表示某块内存页(通常是 RAM 中的页)自从上次从存储设备(如磁盘)读取后已被修改。当内存中的数据被更改时,该页会被标记为 dirty,意味着它与磁盘上对应的数据不再一致。这一信息很重要,原因包括:

  • 写入效率:内核通过只回写被标记为 dirty 的内存页来优化存储写入操作,从而减少写入次数,提高整体 I/O 性能。
  • 缓存机制:内核会将频繁访问的数据保存在内存缓存中以加快访问速度。当缓存中的数据被修改时,标记为 dirty,内核随后会将其写回存储,确保数据一致性。
  • 数据完整性:跟踪 dirty 页对于保证数据完整性至关重要,确保内存中的数据修改最终被持久保存到存储中,从而在系统崩溃或断电时能恢复关键数据。

“dirty”这一术语在计算机科学和操作系统中广泛使用,用来描述需要与非易失性存储同步的被修改数据。它本质上是一个标志,指示内存中的某些数据需要写回磁盘以维护数据一致性。

相对地,“clean”页指自读取以来未被修改且与磁盘数据同步的内存页。内核的内存管理系统会跟踪这些状态,以高效管理数据并保证系统中的数据完整性。

为 Ubuntu/Debian/Mint 生成 DEB 包

为 Ubuntu 或 Debian 构建内核包涉及多个步骤,包括内核配置、编译、生成 Debian 包以及安装。具体步骤如下:

  1. 开始前,请确保已安装必要的构建工具和依赖:
$ sudo apt-get install build-essential fakeroot kernel-package libncurses5-dev bison flex

2. 获取内核源码。可从官方 kernel.org 网站下载,或使用包管理器获取源码包。
例如,下载当前运行内核的源码包:

$ sudo apt-get source linux-image-$(uname -r)

3. 进入内核源码目录:

$ cd linux-<version>  # 将 <version> 替换为实际内核版本

4. 使用文本界面配置内核:

$ make menuconfig

5. 使用图形界面配置内核:

$ make xconfig

6. 根据需求配置内核,包括自定义设置和功能。 7. 编译内核并生成 Debian 包:

$ fakeroot make-kpkg --initrd --append-to-version=<custom_version> kernel_image kernel_headers

<custom_version> 替换为你希望追加到内核版本号的标签。

  1. 成功构建内核包后,安装它:
$ sudo dpkg -i ../linux-image-<custom_version>_amd64.deb  # 替换为实际包名

9. 可能还需安装对应的 linux-headers 包:

$ sudo dpkg -i ../linux-headers-<custom_version>_amd64.deb  # 替换为实际包名

10. 更新引导加载器配置以识别新内核(以 GRUB 为例):

$ sudo update-grub

11. 重启系统以加载新内核:

$ sudo reboot

12. 系统重启后,验证新内核是否生效:

$ uname -r

应显示带有你自定义版本标签的内核版本。

为 Fedora Core/Red Hat/SUSE 生成 RPM 包

为 Fedora Core、Red Hat 或 SUSE Linux 构建内核包通常需创建自定义的 RPM 包。不同发行版有各自的打包和构建流程,下面分别介绍:

Fedora Core / Red Hat:

  1. 安装必要的开发工具:
$ sudo dnf install rpm-build redhat-rpm-config gcc make

2. 从官方仓库下载内核源码:

$ sudo dnf download --source kernel

3. 设置 RPM 构建环境:

$ rpmdev-setuptree

该命令会创建 rpmbuild 目录结构。

  1. 将下载的源码 RPM 复制到 SOURCES 目录:
$ cp kernel*.src.rpm ~/rpmbuild/SOURCES/

5. 解压源码 RPM:

$ rpm -i ~/rpmbuild/SOURCES/kernel*.src.rpm

此时可对内核源码应用自定义补丁或修改。

  1. 配置内核:
$ cd ~/rpmbuild/SPECS
$ rpmbuild -bp kernel.spec

7. 构建内核 RPM 包:

$ rpmbuild -bb kernel.spec

8. 安装生成的 RPM 包:

$ sudo dnf install ~/rpmbuild/RPMS/<architecture>/kernel-*.rpm

9. 更新 GRUB 配置并重启系统加载新内核:

$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg  # GRUB2
$ sudo reboot

SUSE Linux:

  1. 安装开发工具:
$ sudo zypper install rpm-build gcc make

2. 从官方仓库下载内核源码。 3. 设置 RPM 构建环境:

$ rpmdev-setuptree

4. 将源码 RPM 复制到 SOURCES:

$ cp kernel*.src.rpm ~/rpmbuild/SOURCES/

5. 解压源码 RPM:

$ rpm -i ~/rpmbuild/SOURCES/kernel*.src.rpm

此时可应用自定义补丁。

  1. 配置内核:
$ cd ~/rpmbuild/SPECS
$ rpmbuild -bp kernel.spec

7. 构建内核 RPM 包:

$ rpmbuild -bb kernel.spec

8. 安装生成的 RPM 包:

$ sudo zypper install ~/rpmbuild/RPMS/<architecture>/kernel-*.rpm

9. 更新 GRUB 配置并重启系统:

$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg  # GRUB2
$ sudo reboot

请将 <architecture> 替换为你的系统架构(如 x86_64)。
构建和安装自定义内核可能较为复杂,操作前务必做好充分备份和恢复准备,确保系统关键组件安全。

安装

生成阶段完成后,各部分可以分别进行安装。

安装可以使用默认路径,也可以根据下表中参数重新定义安装路径:

Makefile 参数名说明默认路径关联目标
INSTALL_PATHS指定内核及系统映射文件的安装位置/bootinstall
INSTALL_MOD_PATH指定内核源码中本地动态模块的安装位置/lib/modules/$(KERNELRELEASE)/kernelmodules_install
INSTALL_MOD_DIR指定额外动态模块的安装位置/lib/modules/$(KERNELRELEASE)/extramodules_install
INSTALL_FW_PATH指定动态模块固件的安装位置$(INSTALL_MOD_PATH)/lib/firmwarefirmware_install

(表 2.2:GNU/Linux 内核 Makefile 参数)

可以通过指定 rootfs 根目录的前缀来修改安装路径。这对于安装内核本身非常有用,通常安装在引导加载程序所在路径:

$ make INSTALL_PATH=<kernel_path> install

对于动态模块的安装:

$ make INSTALL_MOD_PATH=<dynamic_modules_path> modules_install

对于固件的安装:

$ make INSTALL_FW_PATH=<dynamic_modules_path> firmware_install

内核安装完成后,需要请求 grub 更新配置:

$ sudo update-grub2

系统会提示输入密码,并输出类似如下内容:

Generating grub.cfg ...
Found linux image: /boot/bzImage-6.2.0-bpb
Found initrd image: /boot/bzImage-6.2.0-bpb

如果一切正常,内核会被添加到启动时的可启动内核列表中。

执行与启动流程

vmlinux 文件是一个静态链接的可执行文件,包含以 ELF/COFF 格式存储的 Linux 内核。它可能用于内核调试、生成 System.map 符号表或其他操作,但在作为操作系统内核启动前,必须添加多重引导头(multiboot header)、引导扇区和配置例程,使其可引导。在较早的 UNIX 平台上,内核镜像被命名为 /unix。随着虚拟内存的引入,支持该功能的内核通常以 vm 作为前缀进行区分。vmlinux 名称来源于 vmunix 的合成,而 vmlinuz 则在末尾加了字母 z,表示该内核是压缩过的(例如使用 gzip 压缩)。

传统上,制作可引导内核镜像时,内核会使用 gzip 进行压缩,或自 Linux 2.6.30 起采用 LZMA 或 bzip2 压缩,这需要在生成的镜像中包含一个极小的解压缩代码段(stub)。该解压缩代码负责解包内核代码,在某些系统上会在控制台打印点状进度提示,然后继续启动过程。后续还支持了 LZO、xz、LZ4 和 zstd 等压缩算法。

解压缩例程对启动时间的影响可以忽略不计。在 bzImage 出现之前,某些架构(尤其是 x86)对内核大小的限制极为严格,因此压缩成为必需。

启动时对压缩 Linux 内核的解压缩流程:

image.png

启动镜像文件名本身并不重要,但大多数发行版采用 vmlinuz 作为启动镜像名称。

随着 Linux 内核的发展,用户自定义内核的体积已超出部分架构对压缩内核代码存储空间的限制。为解决这一问题,bzImage(big zImage)格式被设计出来,通过将内核划分为非连续的内存区域来突破空间限制。

image.png

在 Linux 2.6.30 之前,bzImage 使用 gzip 进行压缩,之后引入了更多压缩算法。尽管前缀“bz”可能让人误以为使用了 bzip2 压缩,但事实并非如此。(bzip2 通常配套一些以“bz”为前缀的工具,如 bzless、bzcat 等。)

编译 Linux 内核时,还会生成 System.map 文件,它是内核使用的符号表。该表用于在符号名与内存地址之间进行查找。符号名可以是变量名或函数名。调试时需要通过符号名查找地址或反向查找,System.map 文件尤为重要,尤其是在调试内核崩溃(panic)和错误时。开启 CONFIG_KALLSYMS 选项后,内核自身可以完成地址到符号名的转换,因而无需使用如 ksymoops 之类的工具。

bzImage 文件结构由以下部分连接组成:bootsect.o + setup.o + misc.o + piggy.o,其中 piggy.o 的数据段包含了 gzip 压缩的 vmlinux 文件。内核源码中的 scripts/ 目录下提供了 extract-vmlinux 脚本,用于提取 GNU/Linux 内核镜像。

某些发行版会提供包含对应内核 RPM 的 vmlinux 文件的 kernel-debuginfo 包,该文件通常安装在以下路径之一:

  • /usr/lib/debug/lib/modules/$(uname -r)/vmlinux
  • /usr/lib/debug/lib64/modules/$(uname -r)/vmlinux
  • /boot

image.png

从 BIOS、主引导记录(MBR)、GRUB2 到初始 RAM 磁盘(initrd)启动 Linux 内核的过程涉及一系列系统启动时依次执行的步骤。

在之前使用 qemu 运行时,qemu 替代了 BIOS 和引导加载程序(可能是 qemu 自带的或其他引导器)。

以下是 Linux 操作系统启动流程的概览:

BIOS 初始化
当计算机开机或重置时,最先运行的软件是基本输入输出系统(BIOS)。
BIOS 会执行硬件检测,初始化设备,并寻找可启动设备以加载引导加载程序。
BIOS 通常按照预设顺序查找可启动设备,如硬盘、光驱、USB 设备或网络启动,具体顺序依 BIOS 设置而定。

主引导记录(MBR)
如果可启动设备是硬盘,BIOS 会查找该设备的主引导记录(MBR),即存储设备的第一个扇区(512 字节)。
MBR 包含主要引导加载程序代码,这是一段小程序,负责加载二级引导加载程序,如 GRUB2。

二级引导加载程序(GRUB2)
GRUB2(Grand Unified Boot Loader)是许多 Linux 发行版广泛使用的二级引导加载程序。
MBR 代码执行后,会加载位于特定分区(通常是 /boot 分区)的二级引导核心镜像。
GRUB2 会向用户展示启动菜单(如果已配置),允许选择具体的内核启动。
GRUB2 还能加载并执行配置文件,如 grub.cfg,其中包含可用内核及其启动参数的信息。
在嵌入式领域,qemu 自带的引导加载程序被广泛采用。

加载 Linux 内核
选定内核后,GRUB2 将 Linux 内核镜像(如 vmlinuz)加载至内存。
GRUB2 同时传递内核命令行参数,包括根文件系统位置和 initrd 镜像位置。

Initrd(初始 RAM 磁盘)
initrd 是包含必要驱动和工具的初始 RAM 磁盘,用于挂载根文件系统。
内核加载 initrd 到内存并解压其内容。
initrd 对于根文件系统存放在复杂存储设备(如 LVM 或软件 RAID)上的系统尤为重要,能让内核在挂载真实根文件系统前加载必需驱动。

挂载根文件系统
在 initrd 协助下,内核挂载根文件系统,通常位于硬盘上。
根文件系统挂载后,内核将控制权交给 init 进程,init 是首个用户空间进程,也是所有其他进程的祖先。
init 进程负责初始化剩余的用户空间组件、服务和应用。
initrd 往往作为临时解决方案,确保复杂存储或设备配置下系统能够顺利启动。一旦根文件系统挂载完成,内核便不再需要 initrd。

具体启动细节可能因发行版和配置不同而异,但以上概述为从 BIOS 到 initrd 的 Linux 内核启动过程提供了基本理解。

用户可选择启动内核,甚至添加额外内核参数。也可以选择命令行 shell,以便手动更精细地控制启动过程。

当二级引导加载程序已加载到内存后,文件系统被访问,默认内核镜像和 initrd 镜像被载入内存。镜像准备好后,二级引导加载程序调用内核镜像。

接下来进入 GNU/Linux 内核阶段。此时内核尚未真正执行,首先执行内核配置相关操作,如内存管理等。完成这些准备工作后,内核将被解包,最终开始运行。

代码风格

Linux 内核社区遵循明确的代码风格规范,以保持代码库的一致性和可读性。在向 Linux 内核贡献代码时,遵守这些编码规范至关重要。

主要的代码风格规则详见 Linux 内核源码树中的文档:Documentation/process/coding-style.rst

以下是 Linux 内核代码编写中的一些关键编码规范:

缩进
使用制表符(tab)进行缩进,不使用空格。每个制表符等同于八个空格。

行长度
代码和注释的行长度限制为 80 个字符。
例外:解释性注释可延长至 100 个字符。

大括号
采用 Kernighan 和 Ritchie 风格,即左大括号与语句或函数声明在同一行:

if (condition) {
    /* 代码 */
} else {
    /* 代码 */
}

函数和变量命名
函数名使用小写字母和下划线(例如 function_name())。
变量名也使用小写字母和下划线(例如 variable_name)。
使用具有描述性的名称。

结构体
结构体名及字段名均使用小写字母和下划线(例如 struct my_struct { int field_name; })。

注释
使用 /* ... */ 形式的注释,避免使用 //
注释应简明扼要,侧重说明“为什么”,而非“做了什么”。
对函数参数、返回值及全局变量进行文档说明。
注释放置于所描述代码之前。

头文件和包含顺序
按字母顺序组织 #include 语句,系统头文件优先,其次是内核头文件,最后是本地头文件。
避免在头文件中使用 #include,以减少依赖问题。

空白和空行
逗号后保留一个空格。
行尾不允许有多余空白。
使用空行分隔代码逻辑区块,但不要过多。

switch 语句
case 标签与 switch 语句对齐,不额外缩进。

return 语句
避免在 return 语句中使用复杂表达式。
非必要时不在返回值周围加括号。

错误处理
使用负数错误码(如 -ENOMEM)表示错误。
避免用 goto 进行错误处理,推荐使用结构化错误处理方式。

宏定义
尽量少用宏,简单任务优先使用内联函数。
宏命名使用大写字母和下划线(如 #define MACRO_NAME)。

枚举和常量
常量优先使用枚举而非 #define
常量命名使用大写字母和下划线(如 #define CONSTANT_NAME)。

类型转换
避免不必要的类型转换。
适当时使用 C99 标准类型(如 int32_tuint64_t)。

许可和版权头文件
源文件中应包含适当的许可和版权声明头。

以上是 Linux 内核中主要的编码规范。遵守这些规范有助于维护代码的可读性和一致性,便于开发者协作和内核维护。

在向 Linux 内核提交代码时,务必遵守这些代码风格规则,以确保代码被主线内核接受。

从一开始养成良好习惯,对我们来说尤为重要。

总结

本章是理解 GNU/Linux 内核各组成部分的重要一步,同时也帮助我们了解了定制内核生成的不同阶段。

到目前为止,我们还未编写任何代码或进行修改,只是原样获取了源码。然而,在下一章,我们将开始真正动手,从零开发内核的第一个驱动程序,深入系统核心。