Linux内核配置、构建、模块管理与测试全解析
1. 内核配置与构建基础
1.1 配置文件扩展
在“System Type”菜单行上方有这样一行代码:
source init/Kconfig
“source”关键字用于扩展配置菜单,以纳入额外的模块化特性,其作用类似于C语言中的
#include
。在主Kconfig文件中,会分散着许多这样的“source”行。配置语言实际上比这个简单示例所展示的要复杂得多,更多详细信息可查看
linux/Documentation/kbuild/kconfig-language.txt
。
1.2 构建内核
构建内核的实际过程会因目标不同而略有差异,下面分别介绍为目标板和工作站构建内核的过程。
1.2.1 为目标板构建内核
前三个步骤普通用户即可执行:
1.
make clean
:删除上一次构建生成的所有中间文件,确保所有内容都根据当前配置选项进行构建。实际上,几乎所有Linux的Makefile都有“clean”目标。如果是首次构建,由于没有中间文件需要清理,所以可以不执行此命令。
2.
make
:这是构建的核心步骤,它会构建可执行的内核映像和所有内核模块,此过程通常需要一些时间。最终生成的压缩内核映像位于
arch/$(ARCH)/boot/bzImage
。
3.
make uImage
:目标板上的U-boot引导加载器要求处理的任何文件前面都有一个64字节的头部。“uImage”这个Makefile目标会在“bzImage”前面添加该头部,从而在
arch/$(ARCH)/boot
目录下创建“uImage”文件。
64字节头部包含以下信息:
| 信息 | 描述 |
| ---- | ---- |
| 目标操作系统 | 目标板所使用的操作系统 |
| 目标CPU架构 | 目标板的CPU架构类型 |
| 映像类型 | 如内核、文件系统、独立程序等 |
| 压缩类型 | 内核映像的压缩方式 |
| 加载地址 | 内核映像加载到内存的地址 |
| 入口点 | 内核开始执行的地址 |
| 映像名称 | 内核映像的名称 |
| 映像时间戳 | 内核映像的创建时间 |
之前在第4章,我们将一个名为“mkimage”的可执行文件复制到了ARM交叉工具链的
bin/
目录下,这个工具用于构建头部,它由“uImage”的Makefile目标调用。实际上,“mkimage”是U-boot发行版的一部分。
接下来的步骤需要root用户权限:
4.
make modules_install
:
INSTALL_MOD_PATH = /home/your_user_name/root_qtopia
此命令将模块复制到
$(INSTALL_MOD_PATH)/lib/modules/kernel_version
目录下,其中“kernel_version”是标识正在构建的特定内核的字符串。需要注意的是,为目标板构建时,必须明确指定根文件系统,否则模块会被复制到工作站文件系统的
/lib/modules
目录下。严格来说,在这种情况下,此步骤并非必需,因为启动内核并不需要这些模块。
整个构建过程是递归的,内核源代码树中的每个子目录都有自己的Makefile来处理该目录中的源文件,顶层Makefile会递归调用所有子Makefile。
1.2.2 为工作站构建内核
为工作站构建内核的过程与为目标板构建略有不同:
1. 不需要执行“make uImage”步骤。
2. 执行“make modules_install”时无需指定“INSTALL_MOD_PATH”参数。
3. 以root用户身份执行“make install”,此命令会完成以下几件事:
- 将x86架构下名为“vmlinuz”的内核可执行文件和链接映射文件“System.map”复制到
/boot
目录。
- 在
/boot/grub/grub.conf
中添加一个新条目,以便GRUB可以将新内核作为引导选项。
- 在
/boot
目录下创建一个初始RAM磁盘“initrd”。
1.3 工作站相关说明
1.3.1 GRUB引导加载器
如今,大多数工作站安装都会使用名为GRUB的引导加载器来选择要引导的特定内核或其他操作系统。能够引导多个内核映像非常有用,例如,当你构建了一个新内核但无法正常启动时,可以回退到已知能正常工作的映像,然后尝试找出新内核的问题所在。你可以查看工作站上的
/boot/grub/grub.conf
文件来了解相关信息。
1.3.2 初始RAM磁盘(initrd)
大多数Linux内核都配置为使用“initrd”(初始RAM磁盘)。它是一个非常小的Linux文件系统,由引导加载器加载到RAM中,并在内核启动时、主根文件系统挂载之前进行挂载。使用“initrd”的常见原因是,在挂载根分区之前需要加载一些内核模块,这些模块通常用于支持根分区使用的文件系统(如ext3)或硬盘所连接的控制器(如SCSI或RAID)。
1.4 引导新内核
构建好新内核后,可以通过以下两种方式进行测试:
1. 将新映像加载到闪存中,可以与现有内核映像并存,也可以替换现有内核映像。
2. 使用TFTP通过网络引导新映像,这种方式在开发环境中特别有利,因为无需进行耗时的闪存烧录过程,就可以快速测试新内核映像。
具体操作步骤如下:
1. 将
arch/arm/boot/uImage
移动到
/var/lib/tftpboot
目录(或者你选择的其他TFTP目录)。
2. 将目标板引导到U-boot,然后执行以下命令:
tftpboot 32000000 uImage
bootm 32000000
“tftpboot”命令将“uImage”下载到RAM中的
0x32000000
位置,“bootm”命令会解释该位置的U-boot头部,将映像复制到内存中的正确位置(必要时进行解压缩),并将控制权转移给它。内核启动后,执行
uname -a
命令,你会发现内核映像的时间戳与构建时间相对应。
需要注意的是,U-boot总是将数字解释为十六进制。此外,还有一个U-boot环境变量“boot_tftp”,它包含上述两个命令,运行该变量可以通过TFTP引导内核。
1.5 资源推荐
-
关于内核配置和构建过程的更多详细信息,可查看
/usr/src/arm/linux/Documentation/kbuild目录下的文件。 -
以下是一些可能感兴趣的指南:
- Config-HOWTO :主要关注系统构建完成后的配置方法。
- Kernel-HOWTO :提供本章所涵盖主题的额外信息。
- www.linuxfromscratch.org :该项目提供了从源代码开始逐步构建自定义Linux系统的详细说明,还有一个子项目涉及为嵌入式环境交叉构建Linux。
- www.yaffs.net :YAFFS的主页。
2. 内核模块与设备驱动
2.1 内核模块概述
可安装的内核模块是扩展基本Linux内核功能、添加新特性而无需重新构建内核的有效方式。其关键特性是可以在需要时动态加载,不再需要时卸载,你可以将模块看作是内核层面的用户空间进程。模块在设备驱动和文件系统等方面特别有用。
由于Linux支持的硬件种类繁多,构建一个包含所有可能设备驱动的内核映像是不切实际的。因此,内核仅包含引导设备和其他常见硬件(如串行和并行端口、IDE和SCSI驱动器等)的驱动程序,其他设备则以可加载模块的形式支持,并且在特定系统中只加载所需的模块。
在生产嵌入式环境中,通常没有很好的技术理由使用可加载模块,因为我们事先知道系统必须支持的硬件,所以可以直接将支持功能构建到内核映像中。不过,后面会提到使用模块可能存在“商业”原因。在测试新驱动时,模块仍然很有用,因为每次在驱动代码中发现并修复问题时,无需重新构建新的内核映像,只需将其作为模块加载并进行测试即可。
需要注意的是,模块在内核空间的特权级别0下执行,因此有可能导致整个系统崩溃。
2.2 模块示例
2.2.1 创建项目
在
home/src/hello/
目录下创建一个新的Eclipse Makefile项目,打开
hello.c
文件。该文件是一个可加载内核模块的简单示例,包含两个非常简单的函数:
hello_init()
和
hello_exit()
。文件的最后两行代码如下:
module_init(hello_init);
module_exit(hello_exit);
这两行代码将这两个函数告知模块加载器和卸载器实用程序。每个模块都必须包含一个初始化函数和一个退出函数。
module_init()
宏指定的函数(这里是
hello_init()
)会在使用
insmod
命令安装模块时被调用,
module_exit()
指定的函数(
hello_exit()
)会在使用
rmmod
命令移除模块时被调用。
2.2.2 处理Eclipse错误
你可能会注意到Eclipse报告了一些语义、语法错误和代码分析问题,但实际上项目可以正确构建。Eclipse CDT团队承认静态代码分析“远非完美”,会产生虚假错误。如果你觉得这些消息烦人,可以通过打开项目属性对话框,进入“C/C++ General -> Code Analysis”,选择“Use project settings”,并取消勾选“Potential Programming Problems”和“Syntax and Semantic Errors”来关闭静态分析。当然,还需要添加适当的包含路径,即
/absolute_path_to_kernel_source/include
。
2.2.3 打印信息
在这个示例中,两个函数都使用
printk()
函数在控制台打印消息,
printk()
是内核中与
printf()
等效的函数。像
printf()
这样依赖于操作系统重定向等特性的C库函数,在内核代码中不可用。
printk()
不会直接将消息写入控制台,而是将其写入一个循环缓冲区,然后唤醒
klogd
守护进程,将消息写入系统日志,并可能在控制台打印。
在
printk()
格式字符串的开头有
KERN_ALERT
,这是日志级别的符号表示,用于确定消息是否会显示在控制台。日志级别范围从0到7,数字越小优先级越高。如果日志级别在数值上小于内核整数变量
console_loglevel
,则消息会显示在控制台。无论日志级别如何,
printk
消息总会出现在
/var/log/messages
文件中。
2.2.4 模块参数
“hello”示例还展示了如何指定模块参数,即可以在加载模块的命令行中输入值的局部变量,这通过
module_param()
宏实现。
module_param()
的第一个参数是变量名,第二个是变量类型,第三个是“权限标志”,用于控制对sysfs中模块参数表示的访问。sysfs是2.6内核的新特性,比
/proc
文件系统更方便访问驱动内部信息。目前一个安全的值是
S_IRUGO
,表示该值为只读。变量类型可以是
charp
(字符字符串指针)、
int
、
short
、
long
等各种大小的整数,或者在前面加“u”表示无符号整数。
2.2.5 测试模块
构建项目并进行测试,注意,尽管项目位于目标板的根文件系统中,但它是在工作站上运行的。在shell窗口中,以root用户身份在
module/
目录下输入以下命令:
insmod hello.ko my_string = "name" my_int = 47
如果你最初以普通用户身份登录,然后使用
su
切换到root用户,由于
/sbin
通常不是普通用户路径的一部分,因此必须在所有模块命令前加上
/sbin/
。在X窗口下运行的shell窗口中,无论日志级别如何,
printk
消息都不会显示,因为X窗口运行的是“虚拟”终端。你可以以root用户身份执行以下命令查看
printk
的输出:
tail /var/log/messages
该命令会打印消息日志文件的最后几行。一个有用的变体是:
tail -f /var/log/messages
“f”表示“跟随”,此版本会持续运行并输出发送到消息文件的新文本,该命令也可写成“tailf”。通常我会打开第二个shell窗口来监控消息文件。
模块是特定于内核版本的,也就是说,
insmod
只会加载为当前运行的内核版本编译的模块,因为内核API会随时间变化。例如,为3.1.5版本编译的模块,如果加载到3.4.4版本的内核中,可能会因为内核函数的参数列表发生变化而崩溃。
insmod
提供了一个选项(
-f
)来强制加载为其他版本编译的模块,在很多情况下可能会起作用。
执行
lsmod
命令,它会按加载的逆序列出当前加载的模块,“hello”应该是第一个条目。
lsmod
还会给出每个模块的“使用计数”以及显示哪些模块依赖于其他模块,相同的信息也可以在
/proc/modules
文件中找到。
执行
rmmod hello
命令,你会看到
hello_exit()
函数打印的消息。最后再次执行
lsmod
命令,会发现“hello”已不再列出。需要注意的是,
rmmod
命令不需要“
.ko
”扩展名,模块加载后仅通过基本名称来识别。
2.2.6 外部符号解析
模块通常会包含对外部符号(如
printk
)的引用,这些外部引用是如何解析的呢?
insmod
会根据内核的符号表来解析这些引用,内核符号表在内核启动过程中加载到内存中。此外,模块中定义的任何导出符号都会添加到内核符号表中,供后续加载的模块使用。因此,模块可以引用的外部符号仅限于内建在内核映像中的符号或之前加载的模块中的符号,内核符号表可在
/proc/ksyms
中查看。
2.3 “污染”内核
安装“hello”模块时,在
hello_init()
消息之前,你可能会看到另一条消息:
hello: module license ‘unspecified’ taints kernel
这意味着什么呢?显然,内核维护者厌倦了处理涉及无可用源代码的内核模块的错误报告,即那些未根据开源许可证(如GPL)发布的模块。他们的解决方案是发明了
MODULE_LICENSE()
宏,通过该宏可以声明模块确实是开源的,其格式如下:
MODULE_LICENSE ("approved string")
“approved string”是
linux/include/linux/module.h
中找到的ASCII文本字符串之一,其中包括“GPL”。如果你按照开源许可证(如GPL)的条款分发模块,就可以在代码中包含相应的
MODULE_LICENSE()
调用,加载模块时就不会产生任何警告。
如果你安装了一个产生上述警告的模块,并且系统随后崩溃,崩溃文档(核心转储)将显示加载了一个非开源模块,你的内核就被“污染”了,因为有人无法访问相关代码。如果你将崩溃文档提交给内核维护者,将不会得到处理。
在
hello.c
文件中,在两个
module_param()
语句下方添加以下行:
MODULE_LICENSE("GPL");
重新构建“hello”模块并验证,安装新版本时不会再产生警告。不过,内核仍然处于“污染”状态,“污染”标志会一直设置,直到你重新启动系统。“污染”标志可在
/proc/sys/kernel/tainted
中查看,它实际上是一个位掩码,揭示了内核状态的许多信息,相关文档可在
linux/Documentation/sysctl/kernel.txt
中找到。
2.4 内核模块与GPL
内核模块与GPL有“特殊”关系。众所周知,Linux是根据GNU GPL(具体是GPL版本2)发布的。
大家都认同,在用户空间运行且仅使用已发布内核API的应用程序不属于GPL定义的“衍生作品”,因此可以保持专有。另一方面,任何静态链接到内核中的代码都在使用GPL代码,根据定义,它是衍生作品,必须根据GPL发布并提供源代码。
动态加载的模块处于中间状态,尽管它们也在使用内核中的GPL代码,但模块不一定是开源的,不过如前所述,如果运行非开源模块,内核至少会给出警告。然而,有些内核开发者希望禁止非开源模块,甚至一度讨论不允许此类模块加载。
对此,Linus认为禁止专有模块会损害Linux的商业可行性,因为硬件供应商有合理的理由保持其驱动程序的专有性,发布驱动源代码可能会泄露其专有硬件的细节。如果被迫发布源代码,他们可能不会支持Linux。
这对你处理自己的设备驱动有影响。从技术上讲,在嵌入式环境中通常没有太多理由使用模块,因为硬件是固定且明确的,所以将驱动程序构建到内核映像中是合理的。但如果你想将这些驱动程序保持为专有,就只能将它们构建并发布为模块。
2.5 构建内核模块
内核模块的构建过程比我们之前遇到的Makefile要复杂一些,这是因为模块必须在特定内核的上下文中构建,特别是你打算运行该模块的内核。因此,如果你像现在这样在核外构建模块,Makefile需要切换到内核源代码的顶级目录并调用那里的Makefile。实际的构建过程在内核源代码树的顶级目录中进行,结果会放回包含模块源代码的目录。
以“hello”模块为例,其Makefile的第一行如下:
obj-m := hello.o
简单来说,这告诉内核构建系统从
hello.o
创建一个模块。接下来根据需要指定一些调试标志。
真正有趣的部分在第20行,我们对“modules”目标调用
make
:
-C /lib/modules/$(shell uname -r)/build
这是
make
切换到内核源代码树的方式。执行
uname -r
命令,会得到一个表示内核版本的数字字符串,在
/lib/modules
目录下会找到同名的子目录,其中有一个名为“build”的链接,它实际上指向当前正在执行的内核的源代码树。“-C”表示切换目录。最后,
M=$(shell pwd)
将当前目录作为环境变量“M”传递。
内核源代码树不需要完整的内核源代码,具体来说,
.c
文件不是必需的,所需的是头文件、配置文件和Makefile,这是流行桌面发行版的常见格式。你可以查看自己内核的源代码树,了解其中包含的内容。
最终结果是将
hello.c
编译为
hello.o
,然后将其转换为可加载模块
hello.ko
。在这个过程中,会创建一些临时文件和一个目录,因此有“clean”目标。如果一个模块由两个或多个源文件构建而成,Makefile的第一行将扩展为类似以下的内容:
obj-m := module.o
module-objs := file1.o file2.o
在创建任何类型项目的Makefile时,无论是内核模块还是用户空间应用程序,通常最好从现有模型开始,这里描述的Makefile是其他内核模块项目的良好起点。
最后,在创建“hello”的Eclipse项目时,我们忽略了包含路径的问题。内核模块从内核源代码树中获取头文件,具体来说是从内核源代码树的
include
目录。
综上所述,通过上述步骤,你可以完成内核的配置、构建、模块的管理以及新内核的测试等操作,深入了解Linux内核的开发和使用。在实际应用中,你可以根据具体需求灵活运用这些知识,开发出适合不同场景的Linux系统。
2.6 内核模块构建流程总结
为了更清晰地展示内核模块的构建流程,下面使用 mermaid 格式的流程图进行说明:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(创建Eclipse Makefile项目):::process
B --> C(编辑hello.c文件):::process
C --> D(设置obj - m := hello.o):::process
D --> E(设置调试标志):::process
E --> F(调用make modules):::process
F --> G{-C /lib/modules/$(shell uname -r)/build}:::decision
G -->|切换目录| H(执行make):::process
H --> I(M=$(shell pwd)传递当前目录):::process
I --> J(编译hello.c为hello.o):::process
J --> K(将hello.o转换为hello.ko):::process
K --> L([结束]):::startend
从这个流程图可以看出,构建内核模块需要经过多个步骤,从项目创建到最终模块生成,每个环节都紧密相连,需要严格按照流程进行操作。
2.7 常见问题及解决方法
在进行内核配置、构建以及模块管理的过程中,可能会遇到一些常见问题,下面为大家总结并提供相应的解决方法:
| 问题描述 | 可能原因 | 解决方法 |
|---|---|---|
make clean
报错
| 权限不足或文件损坏 | 以 root 用户身份执行命令,检查文件是否存在损坏 |
make
过程中出现编译错误
| 缺少依赖库、代码语法错误 | 安装所需依赖库,检查代码语法 |
insmod
无法加载模块
| 模块版本与内核不匹配、权限问题 |
使用
-f
选项强制加载(不推荐),检查模块编译时使用的内核版本;以 root 用户身份执行命令
|
| 模块加载后系统崩溃 | 模块代码存在严重错误、与内核不兼容 | 检查模块代码,确保其正确性;确保模块与内核版本兼容 |
2.8 设备驱动开发初探
在了解了内核模块的相关知识后,我们可以进一步探讨设备驱动开发。设备驱动是内核与硬件设备之间的桥梁,它负责将硬件设备的功能暴露给内核和用户空间。下面以一个简单的 GPIO 驱动为例,介绍设备驱动开发的基本步骤:
- 确定硬件信息 :了解 GPIO 引脚的编号、电气特性等信息。
- 编写驱动代码 :实现驱动的初始化、释放、读写等功能。
- 注册驱动 :将驱动注册到内核中,使其能够被内核识别和管理。
- 测试驱动 :编写测试程序,验证驱动的功能是否正常。
以下是一个简单的 GPIO 驱动示例代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#define GPIO_PIN 123
static int __init gpio_driver_init(void)
{
int ret;
ret = gpio_request(GPIO_PIN, "my_gpio");
if (ret) {
printk(KERN_ALERT "Failed to request GPIO %d\n", GPIO_PIN);
return ret;
}
gpio_direction_output(GPIO_PIN, 0);
printk(KERN_ALERT "GPIO driver initialized\n");
return 0;
}
static void __exit gpio_driver_exit(void)
{
gpio_free(GPIO_PIN);
printk(KERN_ALERT "GPIO driver exited\n");
}
module_init(gpio_driver_init);
module_exit(gpio_driver_exit);
MODULE_LICENSE("GPL");
2.9 设备驱动开发流程
同样,我们使用 mermaid 格式的流程图来展示设备驱动开发的流程:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A([开始]):::startend --> B(确定硬件信息):::process
B --> C(编写驱动代码):::process
C --> D(注册驱动到内核):::process
D --> E(编写测试程序):::process
E --> F(测试驱动功能):::process
F --> G{测试是否通过}:::process
G -->|是| H([结束]):::startend
G -->|否| C(编写驱动代码):::process
从这个流程图可以看出,设备驱动开发是一个迭代的过程,需要不断地测试和修改,直到驱动功能正常为止。
2.10 总结与展望
通过本文的介绍,我们详细了解了 Linux 内核的配置、构建、模块管理以及设备驱动开发的相关知识。从内核配置文件的扩展到内核模块的构建,再到设备驱动的开发,每个环节都有其独特的特点和要求。
在实际应用中,我们可以根据具体需求灵活运用这些知识,开发出适合不同场景的 Linux 系统。例如,在嵌入式系统中,我们可以根据硬件资源的限制,选择合适的内核配置选项,构建精简的内核;在开发新的硬件设备时,我们可以编写相应的设备驱动,将硬件设备的功能集成到系统中。
未来,随着 Linux 内核的不断发展和硬件技术的不断进步,内核开发和设备驱动开发将会面临更多的挑战和机遇。我们需要不断学习和掌握新的知识和技术,以适应不断变化的需求。同时,我们也应该积极参与开源社区,与其他开发者分享经验和成果,共同推动 Linux 技术的发展。
希望本文能够对大家有所帮助,让大家在 Linux 内核开发和设备驱动开发的道路上迈出坚实的一步。
超级会员免费看

被折叠的 条评论
为什么被折叠?



