2、深入理解Linux内核开发与设备驱动基础
深入理解Linux内核开发与设备驱动基础
1. Linux内核开发简介
Linux最初是芬兰学生Linus Torvalds在1991年作为一个业余项目开始的。随着时间的推移,这个项目逐渐发展壮大,如今全球约有1000名贡献者参与其中。现在,Linux在嵌入式系统和服务器领域都不可或缺。内核是操作系统的核心部分,但它的开发并非易事。
Linux相较于其他操作系统具有诸多优势:
- 免费使用。
- 有大量社区文档支持。
- 可在不同平台间移植。
- 提供源代码访问。
- 有大量免费的开源软件。
1.1 开发环境搭建
在开始任何开发之前,需要搭建开发环境。在基于Debian的系统上,Linux开发环境的搭建相对简单,可使用以下命令:
$ sudo apt-get update
$ sudo apt-get install gawk wget git diffstat unzip texinfo \
gcc-multilib build-essential chrpath socat libsdl1.2-dev \
xterm ncurses-dev lzop
如果代码部分要兼容ARM片上系统(SoC)解决方案,还需安装gcc - arm:
sudo apt-get install gcc-arm-linux-gnueabihf
1.2 获取内核源代码
早期的内核版本(直到2003年)采用奇偶版本编号方式,奇数版本为稳定版,偶数版本为不稳定版。2.6版本发布后,版本编号方案切换为X.Y.Z:
- X:实际的内核版本,也称为主版本,当出现向后不兼容的API更改时递增。
- Y:次版本,以向后兼容的方式添加功能后递增。
- Z:也称为PATCH,代表与bug修复相关的版本。
这种版本编号方式称为语义版本编号,一直使用到2.6.39版本。2011年,Linus Torvalds决定将版本提升到3.0,这也意味着语义版本编号的结束,随后采用了X.Y方案。到3.20版本时,Linus认为无法再增加Y,于是切换到任意版本编号方案,当Y变得过大难以计数时就递增X,这就是版本从3.20直接跳到4.0的原因。现在,内核使用任意的X.Y版本编号方案,与语义版本编号无关。
获取源代码可使用以下命令:
git clone https://github.com/torvalds/linux
git checkout v4.1
ls
1.3 内核源代码组织
内核源代码的主要目录及其功能如下:
| 目录 | 功能 |
| ---- | ---- |
| arch/ | 包含特定架构的代码,如alpha/、arm/、mips/等子目录。 |
| block/ | 包含块存储设备的调度算法代码。 |
| crypto/ | 包含加密API和加密算法代码。 |
| Documentation/ | 包含不同内核框架和子系统使用的API描述。 |
| drivers/ | 包含所有设备驱动,按不同子目录组织。 |
| fs/ | 包含内核支持的不同文件系统的实现,如NTFS、FAT等。 |
| include/ | 包含内核头文件。 |
| init/ | 包含初始化和启动代码。 |
| ipc/ | 包含进程间通信(IPC)机制的实现,如消息队列、信号量和共享内存。 |
| kernel/ | 包含与架构无关的基础内核部分。 |
| lib/ | 包含库例程和一些辅助函数。 |
| mm/ | 包含内存管理代码。 |
| net/ | 包含网络协议代码。 |
| scripts/ | 包含内核开发期间使用的脚本和工具。 |
| security/ | 包含安全框架代码。 |
| sound/ | 包含音频子系统代码。 |
| usr/ | 目前包含initramfs实现。 |
内核必须保持可移植性,任何特定架构的代码应位于arch目录中。与用户空间API相关的内核代码不会改变,以免破坏现有程序。
1.4 内核配置
Linux内核是一个基于makefile的项目,有数千个选项和驱动。可使用
make menuconfig
(基于ncurses的界面)或
make xconfig
(基于X的界面)来配置内核。配置选项将存储在源代码树根目录的
.config
文件中。
大多数情况下,无需从头开始配置。每个arch目录中都有默认且有用的配置文件,可作为起始点。例如,对于ARM CPU,配置文件位于
arch/arm/configs/
;对于i.MX6处理器,默认配置文件是
arch/arm/configs/imx_v6_v7_defconfig
;对于x86处理器,在
arch/x86/configs/
中有
i386_defconfig
和
x86_64_defconfig
分别用于32位和64位版本。
以下是x86系统的配置和构建示例:
make x86_64_defconfig
make zImage -j16
make modules
make INSTALL_MOD_PATH </where/to/install> modules_install
对于基于i.MX6的开发板,可按以下步骤操作:
ARCH=arm make imx_v6_v7_defconfig
ARCH=arm make menuconfig
如果使用
make xconfig
遇到Qt4错误,可使用以下命令解决:
sudo apt-get install qt4-dev-tools qt4-qmake
1.5 内核构建
构建内核需要指定架构和编译器,但对于本地构建并非必需。以下是基于ARM的构建示例:
ARCH=arm make imx_v6_v7_defconfig
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make zImage -j16
构建完成后,结果将是一个位于
arch/arm/boot/
的单个二进制映像。
构建模块可使用以下命令:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules
安装模块可使用以下命令:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules_install
modules_install
目标期望一个环境变量
INSTALL_MOD_PATH
来指定模块的安装位置。如果未设置,模块将安装在
/lib/modules/$(KERNELRELEASE)/kernel/
。
i.MX6处理器支持设备树,可使用以下命令编译所有ARCH设备树:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs
对于构建独立的设备树blob(DTB),可使用:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6d-sabrelite.dtb
1.6 内核编码风格
在深入了解内核编码之前,应参考内核源代码树中
Documentation/CodingStyle
的内核编码风格手册。一些重要的编码规则如下:
- 始终使用8个字符的制表符缩进,每行应保持80列宽。可使用
scripts/cleanfile
脚本检查和调整缩进和行宽:
scripts/cleanfile my_module.c
也可使用
indent
工具正确缩进代码:
sudo apt-get install indent
scripts/Lindent my_module.c
-
未导出的每个函数/变量应声明为
static。 -
括号表达式内部不应添加空格,例如
s = sizeof (struct file);是可以接受的,而s = sizeof( struct file );则不行。 -
禁止使用
typedef。 -
始终使用
/* this */注释风格,而不是// this。 - 宏应大写,但函数式宏可以小写。
- 注释不应替代难以理解的代码,应优先重写代码而非添加注释。
1.7 内核结构分配与初始化
内核为其数据结构和功能提供了两种可能的分配机制。一些常见的数据结构包括工作队列、列表、等待队列、小任务、定时器、完成量、互斥锁和自旋锁等。动态初始化器都是宏,因此始终大写,如
INIT_LIST_HEAD()
、
DECLARE_WAIT_QUEUE_HEAD()
、
DECLARE_TASKLET()
等。
表示框架设备的数据结构总是动态分配的,每个都有自己的分配和释放API。这些框架设备类型包括网络设备、输入设备、字符设备、IIO设备、类、帧缓冲、调节器、PWM设备和RTC等。静态对象的作用域在整个驱动程序以及该驱动程序管理的每个设备中可见,而动态分配的对象仅对实际使用该模块实例的设备可见。
1.8 内核的类、对象与面向对象编程
内核通过设备和类实现面向对象编程(OOP)。内核子系统通过类进行抽象,
/sys/class/
下的目录数量几乎与子系统数量相同。
struct kobject
结构是这种实现的核心,它甚至引入了引用计数器,以便内核知道实际使用该对象的用户数量。每个对象都有一个父对象,并且在sysfs(如果已挂载)中有一个条目。属于给定子系统的每个设备都有一个指向操作(ops)结构的指针,该结构暴露了可以在该设备上执行的操作。
2. 设备驱动基础
设备驱动是一种软件,旨在控制和管理特定的硬件设备。从操作系统的角度来看,驱动程序可以位于内核空间(以特权模式运行)或用户空间(具有较低的特权),这里主要讨论内核空间的Linux设备驱动。编写设备驱动需要具备C编程技能,熟悉指针和一些操作函数,同时也需要一定的硬件知识。
2.1 用户空间与内核空间
用户空间和内核空间的概念与内存和访问权限有关。现代CPU支持在特权或非特权模式下运行,这使得内核具有更高的权限,而用户程序受到限制。
- 内核空间:是内核驻留和运行的地址集合。内核内存(或内核空间)是内核拥有的内存范围,受访问标志保护,防止用户程序意外或故意干扰内核。在内核模式下,CPU可以访问整个系统内存(包括内核空间和用户空间)。
- 用户空间:是普通程序(如gedit等)受限运行的地址集合。可以将其视为沙箱或监狱,以防止用户程序干扰其他程序的内存或资源。在用户模式下,CPU只能访问标记为用户空间访问权限的内存。用户程序进入内核空间的唯一途径是通过系统调用,如read、write、open、close、mmap等。用户空间代码以较低的优先级运行,当进程执行系统调用时,会向内核发送软件中断,内核开启特权模式让进程在内核空间运行,系统调用返回后,内核关闭特权模式,进程再次受限。
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{执行系统调用}:::decision
B -->|是| C(发送软件中断):::process
C --> D(内核开启特权模式):::process
D --> E(进程在内核空间运行):::process
E --> F(系统调用返回):::process
F --> G(内核关闭特权模式):::process
G --> H([用户程序继续运行]):::startend
B -->|否| H
2.2 模块的概念
模块对于Linux内核就像插件(附加组件)对于用户软件(如Firefox)一样。它可以动态扩展内核功能,而无需重启计算机。大多数情况下,内核模块是即插即用的,插入后即可使用。为了支持模块,内核必须在构建时启用以下选项:
CONFIG_MODULES=y
2.3 模块依赖
在Linux中,一个模块可以提供函数或变量,并使用
EXPORT_SYMBOL
宏将它们导出,供其他模块使用,这些被称为符号。模块B对模块A的依赖意味着模块B正在使用模块A导出的一个符号。
2.4 depmod工具
depmod是在内核构建过程中运行的工具,用于生成模块依赖文件。它通过读取
/lib/modules/<kernel_release>/
中的每个模块,确定其应导出的符号和需要的符号,结果写入
modules.dep
文件及其二进制版本
modules.dep.bin
,这类似于模块索引。
2.5 模块的加载与卸载
要使模块可用,需要将其加载到内核中,有两种方法:
-
开发阶段
:通常使用
insmod
并指定模块路径:
insmod /path/to/mydrv.ko
这是一种低级的模块加载方式,是其他模块加载方法的基础。
-
生产系统
:通常使用
modprobe
,它会解析
modules.dep
文件,先加载依赖项,然后再加载指定的模块:
modprobe mydrv
能否使用
modprobe
取决于
depmod
是否知道模块的安装情况。
如果希望模块在系统启动时加载,可以创建
/etc/modules-load.d/<filename>.conf
文件,并在每行添加要加载的模块名称。例如
/etc/modules-load.d/mymodules.conf
:
#this line is a comment
uio
iwlwifi
通过以上内容,我们对Linux内核开发和设备驱动基础有了较为全面的了解。从环境搭建、代码获取到内核配置、构建,再到设备驱动的相关概念和模块操作,这些知识为进一步深入学习和实践Linux内核开发奠定了坚实的基础。
深入理解Linux内核开发与设备驱动基础
2. 设备驱动基础(续)
2.6 驱动骨架与调试消息管理
在编写设备驱动时,构建一个合理的驱动骨架是非常重要的。驱动骨架为驱动程序提供了基本的结构和流程,使得开发者能够更清晰地组织代码。
在调试驱动程序时,有效的调试消息管理可以帮助开发者快速定位问题。可以使用内核提供的打印函数,如
printk
来输出调试信息。例如:
#include <linux/kernel.h>
// 输出调试信息
printk(KERN_INFO "My driver: Initializing...\n");
printk
函数的第一个参数是日志级别,常见的日志级别如下表所示:
| 日志级别 | 描述 |
| ---- | ---- |
| KERN_EMERG | 系统紧急情况,需要立即处理 |
| KERN_ALERT | 需要立即采取行动的情况 |
| KERN_CRIT | 严重的关键情况 |
| KERN_ERR | 错误情况 |
| KERN_WARNING | 警告情况 |
| KERN_NOTICE | 正常但值得注意的情况 |
| KERN_INFO | 一般信息 |
| KERN_DEBUG | 调试信息 |
在开发过程中,可以根据实际情况选择合适的日志级别输出调试信息。
2.7 错误处理
在设备驱动开发中,错误处理是必不可少的环节。因为硬件设备可能会出现各种异常情况,如设备初始化失败、数据传输错误等。合理的错误处理可以提高驱动程序的稳定性和可靠性。
以下是一个简单的错误处理示例:
#include <linux/errno.h>
int my_driver_init(void) {
int ret;
// 初始化设备
ret = device_init();
if (ret != 0) {
printk(KERN_ERR "My driver: Device initialization failed with error code %d\n", ret);
return ret;
}
// 其他初始化操作
// ...
return 0;
}
在这个示例中,如果设备初始化失败,会输出错误信息并返回错误码,避免后续操作继续执行。
3. 总结
通过前面的介绍,我们对Linux内核开发和设备驱动基础有了较为全面的认识。下面通过一个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(搭建开发环境):::process
B --> C(获取内核源代码):::process
C --> D(配置内核):::process
D --> E(构建内核):::process
E --> F(构建模块):::process
F --> G(加载模块):::process
G --> H(开发设备驱动):::process
H --> I{是否有错误?}:::decision
I -->|是| J(错误处理):::process
J --> H
I -->|否| K([结束]):::startend
整个开发流程可以概括为以下几个主要步骤:
1.
搭建开发环境
:在基于Debian的系统上,使用
apt-get
命令安装必要的开发工具。
2.
获取内核源代码
:使用
git
命令从GitHub上克隆内核源代码,并切换到指定版本。
3.
配置内核
:使用
make menuconfig
或
make xconfig
配置内核选项,并将配置保存到
.config
文件中。
4.
构建内核
:使用
make
命令构建内核和模块。
5.
加载模块
:使用
insmod
或
modprobe
命令将模块加载到内核中。
6.
开发设备驱动
:编写设备驱动代码,包括驱动骨架、调试消息管理和错误处理等。
7.
错误处理
:在开发过程中,及时处理各种错误情况,确保驱动程序的稳定性和可靠性。
在实际开发中,还需要遵循内核编码风格,合理分配和初始化内核结构,理解内核的面向对象编程实现方式等。同时,对于设备驱动的开发,要深入了解硬件设备的特性和工作原理,结合操作系统的相关知识,才能编写出高质量的设备驱动程序。
通过不断学习和实践,逐步掌握Linux内核开发和设备驱动开发的技能,为开发更复杂的系统和应用打下坚实的基础。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)