Bootstrap

《Linux设备驱动程序》(第三版)第2章 字符设备驱动程序

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 验证驱动程序

要验证驱动程序,可以通过以下步骤:

  1. 编译并加载驱动:使用 make 编译驱动代码,生成 .ko 文件,然后使用 sudo insmod <driver_name>.ko 加载驱动。
  2. 创建设备节点:加载驱动后,使用 mknod /dev/simple_char_dev c <major_number> <minor_number> 创建设备节点,其中 <major_number><minor_number> 是驱动分配的主设备号和次设备号。
  3. 测试读写操作:使用 cat /dev/simple_char_dev 测试读操作,会输出 Hello, world!;使用 echo "test data" > /dev/simple_char_dev 测试写操作,内核日志会打印 Received data: test data
  4. 卸载驱动:测试完成后,使用 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 测试驱动程序
  1. 编译并加载驱动
    • 编写好驱动代码后,使用 make 命令进行编译,生成内核模块文件(.ko 文件)。
    • 使用 sudo insmod <module_name>.ko 命令加载驱动模块。如果驱动加载成功,会在 dmesg 日志中看到 “Character device and related class/device created successfully.” 的信息。
  2. 验证设备文件创建
    • 驱动加载成功后,系统会自动在 /dev 目录下创建名为 simple_char_dev 的设备文件。可以通过 ls /dev/simple_char_dev 命令查看设备文件是否存在。
  3. 测试读写操作
    • 读操作:使用 cat /dev/simple_char_dev 命令,应该能看到输出 “Hello, auto - created device!”,这表明 read 方法正常工作。
    • 写操作:这里 write 方法只是简单返回0,实际应用中可以根据需求实现具体的写入逻辑。可以使用 echo "some data" > /dev/simple_char_dev 命令进行测试,虽然当前代码不会对写入的数据做太多处理,但可以验证 write 方法被调用。
  4. 卸载驱动
    • 使用 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 指针来访问。这样,在设备的不同操作方法中(如 readwrite 等),可以方便地获取和修改设备的相关信息。

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 调试技术
  1. 打印调试信息:使用 printk 函数输出调试信息,通过不同的日志级别(如 KERN_INFOKERN_DEBUGKERN_ERR 等)来区分信息的重要性和类型。可以在关键代码位置添加 printk 语句,观察内核日志(如通过 dmesg 命令查看)来了解驱动程序的执行流程和状态。
  2. 内核调试器:如 kgdb,它允许在内核代码中设置断点,单步执行代码,查看变量的值等,有助于深入调试复杂的内核问题。
  3. 动态调试:通过内核提供的动态调试机制,可以在不重新编译内核的情况下,动态开启或关闭调试信息的输出,提高调试的灵活性。
2.4.7 性能问题

在字符设备驱动程序中,性能问题可能出现在多个方面:

  1. 数据传输效率:例如,在 readwrite 方法中,数据在用户空间和内核空间之间的复制操作可能成为性能瓶颈。可以通过优化数据复制方式(如使用更高效的内存复制函数)或采用零拷贝技术来提高效率。
  2. 并发访问:如果多个进程同时访问字符设备,可能会出现竞争条件,影响性能。可以使用同步机制(如自旋锁、信号量等)来保证数据的一致性和操作的原子性。
  3. 中断处理:对于依赖中断的设备,中断处理时间过长可能会影响系统的整体性能。可以采用顶半部和底半部机制,将紧急但不耗时的操作放在顶半部处理,将耗时的操作放在底半部异步处理。
2.4.8 代码清理

在模块卸载时,需要进行代码清理工作,以释放所有分配的资源。这包括释放设备号(使用 unregister_chrdev_region)、注销字符设备(使用 unregister_chrdev)、删除设备(使用 device_destroy)、删除类(使用 class_destroy)等。确保在模块卸载后,系统不会残留任何未释放的资源,避免对后续系统运行造成影响。

;