1.1 什么是设备驱动程序
1.1.1 驱动程序的角色
驱动程序充当硬件设备与操作系统内核之间的桥梁。它使得内核能够与特定硬件进行交互,负责将内核的抽象指令转换为硬件可理解的操作,同时将硬件的状态和数据反馈给内核。
1.1.2 字符设备、块设备和网络设备
- 字符设备:以字符为单位顺序处理数据的设备,如串口、键盘等。字符设备驱动程序通常实现
read
和write
等方法,以字节流的形式处理数据。 - 块设备:以数据块为单位进行数据传输的设备,如硬盘、闪存等。块设备通常支持随机访问,数据的读写以块(通常为512字节或其倍数)为单位。
- 网络设备:用于网络通信的设备,如以太网卡。网络设备驱动主要处理网络数据包的发送和接收,与字符设备和块设备的接口方式有所不同。
1.2 为什么要写驱动程序
1.2.1 为新硬件写驱动程序
当出现新的硬件设备时,操作系统内核原生可能不支持该设备,需要编写驱动程序,使内核能够识别并控制该硬件,发挥其功能。
1.2.2 访问内核服务
驱动程序处于内核空间,可以访问内核提供的各种服务和资源,如内存管理、进程调度等,以实现更高效的设备控制和数据处理。
1.2.3 与内核开发者交流
参与驱动程序开发有助于与内核开发者社区互动,分享经验、获取反馈,共同推动内核及驱动生态的发展。
1.3 设备驱动程序的模块形式
1.3.1 什么是内核模块
内核模块是一种可动态加载和卸载的内核代码,它允许在不重新编译内核的情况下,向内核添加功能,如设备驱动、文件系统等。以下是一个简单的内核模块示例代码:
#include <linux/init.h>
#include <linux/module.h>
// 模块加载函数
static int __init simple_module_init(void) {
// 这里开始模块初始化工作
printk(KERN_INFO "Simple module loaded.\n");
return 0;
}
// 模块卸载函数
static void __exit simple_module_exit(void) {
// 这里进行模块卸载前的清理工作
printk(KERN_INFO "Simple module unloaded.\n");
}
// 模块入口和出口声明
module_init(simple_module_init);
module_exit(simple_module_exit);
// 模块许可证声明
MODULE_LICENSE("GPL");
在上述代码中:
#include <linux/init.h>
和#include <linux/module.h>
引入了编写内核模块所需的头文件。static int __init simple_module_init(void)
定义了模块的加载函数。__init
标记此函数仅在模块初始化时使用,减少内核内存占用。函数内部使用printk
打印一条信息,表示模块已加载。printk
是内核空间的打印函数,KERN_INFO
是日志级别,表明这是一条普通信息。static void __exit simple_module_exit(void)
定义了模块的卸载函数。__exit
标记此函数仅在模块卸载时使用。函数内部同样使用printk
打印信息,表示模块已卸载。module_init(simple_module_init)
和module_exit(simple_module_exit)
声明了模块的入口和出口函数,告诉内核在加载和卸载模块时分别调用哪个函数。MODULE_LICENSE("GPL")
声明了模块的许可证为GPL,这是内核模块常见的许可证要求。
1.3.2 模块的优点
- 动态加载与卸载:无需重启系统或重新编译内核,即可根据需要加载或卸载模块,方便设备的管理和调试。
- 减少内核体积:将不常用的功能以模块形式存在,避免内核镜像过于庞大,提高内核的启动速度和资源利用率。
- 便于开发和维护:不同的模块可以独立开发、调试和维护,降低开发难度,提高代码的可维护性。
1.4 为什么要关心内核版本
1.4.1 内核版本号
Linux内核版本号由三部分组成,如 x.y.z
。x
为主版本号,y
为次版本号(偶数表示稳定版本,奇数表示开发版本),z
为修订号,每次内核代码修改都会增加修订号。
1.4.2 内核版本的差异
不同内核版本在API、功能特性、驱动支持等方面可能存在差异。新的内核版本可能引入新的设备驱动接口、优化性能或修复安全漏洞。编写驱动程序时,需要确保代码与目标内核版本兼容,否则可能导致驱动无法正常工作。
1.5 本书的示例代码
1.5.1 获得示例代码
通常可以从本书的官方网站、相关代码托管平台(如GitHub)或随书附带的资源中获取示例代码。
1.5.2 安装示例代码
获取代码后,根据具体的代码结构和目标系统,可能需要将代码解压到合适的目录,如 /usr/src/
下的自定义目录中,以便后续编译和使用。
1.5.3 编译示例代码
假设示例代码是一个简单的内核模块,编写如下 Makefile
:
# KERNELDIR指向内核源码目录,需根据实际情况修改
KERNELDIR := /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
# 目标
obj - m := simple_module.o
# 编译规则
all:
$(MAKE) - C $(KERNELDIR) M=$(PWD) modules
# 清理规则
clean:
$(MAKE) - C $(KERNELDIR) M=$(PWD) clean
在上述 Makefile
中:
KERNELDIR := /lib/modules/$(shell uname -r)/build
定义了内核源码目录,$(shell uname -r)
获取当前系统的内核版本号。PWD := $(shell pwd)
获取当前工作目录。obj - m := simple_module.o
表示要编译的模块为simple_module.o
。all
目标用于编译模块,$(MAKE) - C $(KERNELDIR) M=$(PWD) modules
命令进入内核源码目录并在当前目录下编译模块。clean
目标用于清理编译生成的文件,$(MAKE) - C $(KERNELDIR) M=$(PWD) clean
命令进入内核源码目录并在当前目录下执行清理操作。
编译时,在示例代码目录下执行 make
命令即可。
1.5.4 示例代码的使用说明
编译成功后,会生成对应的 .ko
文件(如 simple_module.ko
)。使用 sudo insmod simple_module.ko
命令加载模块,使用 sudo rmmod simple_module
命令卸载模块。同时,可以通过查看内核日志(如 dmesg
命令)来查看模块加载和卸载过程中的打印信息,以了解模块的运行情况。