2.1 简单的字符驱动程序
2.1.1 模块的初始化和退出
#include <linux/init.h>
#include <linux/module.h>
// 模块加载函数
static int __init simple_char_driver_init(void) {
// __init标记此函数仅在模块初始化时调用,减少内核内存占用
printk(KERN_INFO "Simple character driver loaded.\n");
// 初始化成功返回0,非0值表示错误
return 0;
}
// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
// __exit标记此函数仅在模块卸载时调用
printk(KERN_INFO "Simple character driver unloaded.\n");
}
// 声明模块的入口和出口函数
module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
// 声明模块的许可证
MODULE_LICENSE("GPL");
2.1.2 设备编号
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
// 定义设备号变量
dev_t dev_num;
// 模块加载函数
static int __init simple_char_driver_init(void) {
// 动态分配设备号,MAJOR(dev_num)和MINOR(dev_num)分别获取主设备号和次设备号
if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
printk(KERN_ERR "Failed to allocate device number.\n");
return -1;
}
printk(KERN_INFO "Allocated device number: major = %d, minor = %d\n", MAJOR(dev_num), MINOR(dev_num));
return 0;
}
// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
// 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple character driver unloaded.\n");
}
module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")
:动态分配字符设备号。dev_num
是用于存储分配的设备号的变量,0
表示起始次设备号,1
表示要分配的设备号数量,"simple_char_dev"
是设备名称。unregister_chrdev_region(dev_num, 1)
:释放之前分配的设备号,dev_num
是要释放的设备号,1
是设备号数量。
2.1.3 字符设备注册
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
// 定义设备号变量
dev_t dev_num;
// 定义文件操作结构体指针
struct file_operations simple_fops;
// 模块加载函数
static int __init simple_char_driver_init(void) {
// 动态分配设备号
if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
printk(KERN_ERR "Failed to allocate device number.\n");
return -1;
}
// 注册字符设备
if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
printk(KERN_ERR "Failed to register character device.\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
printk(KERN_INFO "Character device registered successfully.\n");
return 0;
}
// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
// 注销字符设备
unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
// 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple character driver unloaded.\n");
}
module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)
:注册字符设备。MAJOR(dev_num)
是主设备号,"simple_char_dev"
是设备名称,&simple_fops
是指向文件操作结构体的指针,目前simple_fops
未初始化,后续小节会完善。unregister_chrdev(MAJOR(dev_num), "simple_char_dev")
:注销字符设备,参数与注册时类似。
2.1.4 编写一个简单的read和write方法
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
// 定义设备号变量
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
.read = simple_read,
.write = simple_write,
};
// 简单的read方法实现
ssize_t simple_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
// filp:文件指针,用于访问文件相关信息
// buf:用户空间缓冲区,用于存储读取的数据
// count:请求读取的字节数
// off:文件偏移量
char kernel_buf[] = "Hello, world!";
size_t len = sizeof(kernel_buf) - 1;
if (count < len) {
len = count;
}
// 将内核缓冲区的数据复制到用户空间缓冲区
if (copy_to_user(buf, kernel_buf, len)) {
return -EFAULT;
}
return len;
}
// 简单的write方法实现
ssize_t simple_write(struct file *filp, const char __user *buf, size_t count, loff_t *off) {
// filp:文件指针,用于访问文件相关信息
// buf:用户空间缓冲区,包含要写入的数据
// count:请求写入的字节数
// off:文件偏移量
char kernel_buf[100];
size_t len = count;
if (len > sizeof(kernel_buf) - 1) {
len = sizeof(kernel_buf) - 1;
}
// 将用户空间缓冲区的数据复制到内核缓冲区
if (copy_from_user(kernel_buf, buf, len)) {
return -EFAULT;
}
kernel_buf[len] = '\0';
printk(KERN_INFO "Received data: %s\n", kernel_buf);
return len;
}
// 模块加载函数
static int __init simple_char_driver_init(void) {
// 动态分配设备号
if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
printk(KERN_ERR "Failed to allocate device number.\n");
return -1;
}
// 注册字符设备
if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
printk(KERN_ERR "Failed to register character device.\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
printk(KERN_INFO "Character device registered successfully.\n");
return 0;
}
// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
// 注销字符设备
unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
// 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple character driver unloaded.\n");
}
module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
simple_read
函数:从内核空间向用户空间复制数据。先判断请求读取的字节数是否小于内核缓冲区数据长度,然后使用copy_to_user
函数将数据复制到用户空间缓冲区。simple_write
函数:从用户空间向内核空间复制数据。先判断请求写入的字节数是否超过内核缓冲区大小,然后使用copy_from_user
函数将数据复制到内核缓冲区,并打印接收到的数据。
2.1.5 验证驱动程序
要验证驱动程序,可以通过以下步骤:
- 编译并加载驱动:使用
make
编译驱动代码,生成.ko
文件,然后使用sudo insmod <driver_name>.ko
加载驱动。 - 创建设备节点:加载驱动后,使用
mknod /dev/simple_char_dev c <major_number> <minor_number>
创建设备节点,其中<major_number>
和<minor_number>
是驱动分配的主设备号和次设备号。 - 测试读写操作:使用
cat /dev/simple_char_dev
测试读操作,会输出Hello, world!
;使用echo "test data" > /dev/simple_char_dev
测试写操作,内核日志会打印Received data: test data
。 - 卸载驱动:测试完成后,使用
sudo rmmod <driver_name>
卸载驱动。
2.2 全局变量和模块参数
2.2.1 定义和使用全局变量
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
// 定义设备号变量
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
.read = simple_read,
.write = simple_write,
};
// 定义全局变量
int global_variable = 42;
// 简单的read方法实现
ssize_t simple_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
char kernel_buf[20];
snprintf(kernel_buf, sizeof(kernel_buf), "Global var: %d", global_variable);
size_t len = strlen(kernel_buf);
if (count < len) {
len = count;
}
if (copy_to_user(buf, kernel_buf, len)) {
return -EFAULT;
}
return len;
}
// 简单的write方法实现
ssize_t simple_write(struct file *filp, const char __user *buf, size_t count, loff_t *off) {
return 0;
}
// 模块加载函数
static int __init simple_char_driver_init(void) {
if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
printk(KERN_ERR "Failed to allocate device number.\n");
return -1;
}
if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
printk(KERN_ERR "Failed to register character device.\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
printk(KERN_INFO "Character device registered successfully.\n");
return 0;
}
// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple character driver unloaded.\n");
}
module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
- 在
simple_read
函数中,将全局变量global_variable
的值格式化为字符串,然后复制到用户空间,展示了全局变量的使用。
2.2.2 模块参数
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
// 定义设备号变量
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
.read = simple_read,
.write = simple_write,
};
// 定义模块参数变量
int module_param_variable = 10;
module_param(module_param_variable, int, 0644);
MODULE_PARM_DESC(module_param_variable, "A sample module parameter");
// 简单的read方法实现
ssize_t simple_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
char kernel_buf[20];
snprintf(kernel_buf, sizeof(kernel_buf), "Module param: %d", module_param_variable);
size_t len = strlen(kernel_buf);
if (count < len) {
len = count;
}
if (copy_to_user(buf, kernel_buf, len)) {
return -EFAULT;
}
return len;
}
// 简单的write方法实现
ssize_t simple_write(struct file *filp, const char __user *buf, size_t count, loff_t *off) {
return 0;
}
// 模块加载函数
static int __init simple_char_driver_init(void) {
if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
printk(KERN_ERR "Failed to allocate device number.\n");
return -1;
}
if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
printk(KERN_ERR "Failed to register character device.\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
printk(KERN_INFO "Character device registered successfully.\n");
return 0;
}
// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple character driver unloaded.\n");
}
module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
module_param(module_param_variable, int, 0644)
:定义一个模块参数module_param_variable
,类型为int
,权限为0644
(表示所有者可读可写,组和其他用户可读)。MODULE_PARM_DESC(module_param_variable, "A sample module parameter")
:为模块参数添加描述信息,方便用户了解参数用途。
2.3 自动创建设备文件
2.3.1 类和设备
在Linux内核设备模型中,class
(类)是对设备的一种逻辑分组,它提供了一种将类似设备进行归类的方式,方便管理和用户空间识别。device
(设备)则代表具体的硬件设备实例,每个设备都属于某个类。通过这种机制,系统可以自动在 /dev
目录下创建设备文件,使得用户空间能够方便地访问设备。例如,所有的串口设备可能属于一个 serial
类,每个具体的串口设备就是该类下的一个设备实例。
2.3.2 编写代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
// 定义设备号变量
dev_t dev_num;
// 定义文件操作结构体
struct file_operations simple_fops = {
.read = simple_read,
.write = simple_write,
};
// 定义类指针
struct class *simple_class;
// 定义设备指针
struct device *simple_device;
// 简单的read方法实现
ssize_t simple_read(struct file *filp, char __user *buf, size_t count, loff_t *off) {
// filp:指向与文件相关的结构体,包含文件的状态、位置等信息
// buf:用户空间中用于存储读取数据的缓冲区指针
// count:用户请求读取的字节数
// off:文件当前的偏移量,用于指定从文件的哪个位置开始读取
char kernel_buf[] = "Hello, auto - created device!";
size_t len = sizeof(kernel_buf) - 1;
if (count < len) {
len = count;
}
// 将内核缓冲区中的数据复制到用户空间缓冲区
if (copy_to_user(buf, kernel_buf, len)) {
return -EFAULT;
}
return len;
}
// 简单的write方法实现
ssize_t simple_write(struct file *filp, const char __user *buf, size_t count, loff_t *off) {
// filp:指向与文件相关的结构体,包含文件的状态、位置等信息
// buf:用户空间中包含要写入数据的缓冲区指针
// count:用户请求写入的字节数
// off:文件当前的偏移量,用于指定从文件的哪个位置开始写入
return 0;
}
// 模块加载函数
static int __init simple_char_driver_init(void) {
// 动态分配设备号,0表示起始次设备号,1表示分配1个设备号
if (alloc_chrdev_region(&dev_num, 0, 1, "simple_char_dev")) {
printk(KERN_ERR "Failed to allocate device number.\n");
return -1;
}
// 注册字符设备,MAJOR(dev_num)获取主设备号,"simple_char_dev"是设备名称,&simple_fops是文件操作结构体指针
if (register_chrdev(MAJOR(dev_num), "simple_char_dev", &simple_fops)) {
printk(KERN_ERR "Failed to register character device.\n");
unregister_chrdev_region(dev_num, 1);
return -1;
}
// 创建类,THIS_MODULE表示当前模块,"simple_class"是类的名称
simple_class = class_create(THIS_MODULE, "simple_class");
if (IS_ERR(simple_class)) {
printk(KERN_ERR "Failed to create class.\n");
unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
unregister_chrdev_region(dev_num, 1);
return -1;
}
// 在类中创建设备,simple_class是所属类,NULL表示没有父设备,dev_num是设备号,NULL表示没有设备特定数据,"simple_char_dev"是设备名称
simple_device = device_create(simple_class, NULL, dev_num, NULL, "simple_char_dev");
if (IS_ERR(simple_device)) {
printk(KERN_ERR "Failed to create device.\n");
class_destroy(simple_class);
unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
unregister_chrdev_region(dev_num, 1);
return -1;
}
printk(KERN_INFO "Character device and related class/device created successfully.\n");
return 0;
}
// 模块卸载函数
static void __exit simple_char_driver_exit(void) {
// 删除设备
if (simple_device) {
device_destroy(simple_class, dev_num);
}
// 删除类
if (simple_class) {
class_destroy(simple_class);
}
// 注销字符设备
unregister_chrdev(MAJOR(dev_num), "simple_char_dev");
// 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple character driver unloaded.\n");
}
module_init(simple_char_driver_init);
module_exit(simple_char_driver_exit);
MODULE_LICENSE("GPL");
2.3.3 测试驱动程序
- 编译并加载驱动:
- 编写好驱动代码后,使用
make
命令进行编译,生成内核模块文件(.ko
文件)。 - 使用
sudo insmod <module_name>.ko
命令加载驱动模块。如果驱动加载成功,会在dmesg
日志中看到 “Character device and related class/device created successfully.” 的信息。
- 编写好驱动代码后,使用
- 验证设备文件创建:
- 驱动加载成功后,系统会自动在
/dev
目录下创建名为simple_char_dev
的设备文件。可以通过ls /dev/simple_char_dev
命令查看设备文件是否存在。
- 驱动加载成功后,系统会自动在
- 测试读写操作:
- 读操作:使用
cat /dev/simple_char_dev
命令,应该能看到输出 “Hello, auto - created device!”,这表明read
方法正常工作。 - 写操作:这里
write
方法只是简单返回0,实际应用中可以根据需求实现具体的写入逻辑。可以使用echo "some data" > /dev/simple_char_dev
命令进行测试,虽然当前代码不会对写入的数据做太多处理,但可以验证write
方法被调用。
- 读操作:使用
- 卸载驱动:
- 使用
sudo rmmod <module_name>
命令卸载驱动模块。卸载成功后,/dev
目录下的simple_char_dev
设备文件会被删除,同时在dmesg
日志中会看到 “Simple character driver unloaded.” 的信息。
- 使用
2.4 深入理解字符设备驱动
2.4.1 设备方法
设备方法是一组函数指针,定义在 struct file_operations
结构体中,用于实现对设备的各种操作。例如,read
方法用于从设备读取数据,write
方法用于向设备写入数据,open
方法用于打开设备,close
方法用于关闭设备等。这些方法是内核与设备驱动交互的接口,通过调用这些方法,内核可以实现对设备的控制和数据传输。
2.4.2 设备结构体
通常会定义一个设备结构体来存储与设备相关的信息,例如设备的状态、配置参数、私有数据等。这个结构体可以作为一个成员嵌入到 struct file
结构体中,通过 filp->private_data
指针来访问。这样,在设备的不同操作方法中(如 read
、write
等),可以方便地获取和修改设备的相关信息。
2.4.3 设备方法的实现
设备方法的实现需要根据设备的具体功能和需求来编写。例如,read
方法可能需要从设备的硬件寄存器中读取数据,然后将数据复制到用户空间;write
方法可能需要将用户空间的数据写入设备的硬件寄存器,以控制设备的行为。在实现过程中,要注意处理各种错误情况,如设备忙、数据传输错误等,并返回合适的错误码。
2.4.4 模块许可证声明
在Linux内核模块中,需要声明模块的许可证。常见的许可证是GPL(General Public License),通过 MODULE_LICENSE("GPL")
语句来声明。这表明模块的代码遵循GPL协议,保证了代码的开源性和共享性,同时也规定了使用、修改和分发模块代码的相关条款。
2.4.5 驱动程序中的错误处理
在驱动程序开发中,错误处理至关重要。例如,在分配设备号、注册字符设备、创建设备类和设备等操作中,都可能出现错误。当出现错误时,应该及时打印错误信息,使用合适的 printk
日志级别(如 KERN_ERR
表示错误),以便调试。同时,要根据错误情况进行相应的清理操作,如释放已分配的资源(设备号、类、设备等),避免内存泄漏和系统状态不一致。
2.4.6 调试技术
- 打印调试信息:使用
printk
函数输出调试信息,通过不同的日志级别(如KERN_INFO
、KERN_DEBUG
、KERN_ERR
等)来区分信息的重要性和类型。可以在关键代码位置添加printk
语句,观察内核日志(如通过dmesg
命令查看)来了解驱动程序的执行流程和状态。 - 内核调试器:如
kgdb
,它允许在内核代码中设置断点,单步执行代码,查看变量的值等,有助于深入调试复杂的内核问题。 - 动态调试:通过内核提供的动态调试机制,可以在不重新编译内核的情况下,动态开启或关闭调试信息的输出,提高调试的灵活性。
2.4.7 性能问题
在字符设备驱动程序中,性能问题可能出现在多个方面:
- 数据传输效率:例如,在
read
和write
方法中,数据在用户空间和内核空间之间的复制操作可能成为性能瓶颈。可以通过优化数据复制方式(如使用更高效的内存复制函数)或采用零拷贝技术来提高效率。 - 并发访问:如果多个进程同时访问字符设备,可能会出现竞争条件,影响性能。可以使用同步机制(如自旋锁、信号量等)来保证数据的一致性和操作的原子性。
- 中断处理:对于依赖中断的设备,中断处理时间过长可能会影响系统的整体性能。可以采用顶半部和底半部机制,将紧急但不耗时的操作放在顶半部处理,将耗时的操作放在底半部异步处理。
2.4.8 代码清理
在模块卸载时,需要进行代码清理工作,以释放所有分配的资源。这包括释放设备号(使用 unregister_chrdev_region
)、注销字符设备(使用 unregister_chrdev
)、删除设备(使用 device_destroy
)、删除类(使用 class_destroy
)等。确保在模块卸载后,系统不会残留任何未释放的资源,避免对后续系统运行造成影响。