字符设备驱动

在 Linux 内核中,字符设备是一种常见的设备类型,广泛用于与用户空间进行数据交互。以下是详细的步骤,教你如何编写一个简单的 Linux 字符设备驱动,并在系统中生成对应的设备文件节点。


1. 字符设备驱动的基本框架

一个基本的字符设备驱动需要以下几个关键部分:

  • 设备号的分配和注册
  • 字符设备结构的初始化和注册
  • 文件操作(file_operations)的实现
  • 设备节点的自动或手动创建
  • 模块的初始化和退出函数

以下是一个完整的示例代码,逐步讲解每个部分。


2. 示例代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME  "my_char_class"

static int major;                    // 主设备号
static struct cdev my_cdev;          // 字符设备结构
static struct class *my_class;       // 设备类
static struct device *my_device;     // 设备

// 打开设备
static int my_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device opened\n");
    return 0;
}

// 关闭设备
static int my_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed\n");
    return 0;
}

// 读取设备
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off)
{
    char *data = "Hello from kernel\n";
    size_t data_len = strlen(data);

    if (*off >= data_len)
        return 0; // 已读取到文件末尾

    if (len > data_len - *off)
        len = data_len - *off;

    if (copy_to_user(buf, data + *off, len))
        return -EFAULT;

    *off += len;
    return len;
}

// 写入设备
static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *off)
{
    char kbuf[128];
    if (len > sizeof(kbuf) - 1)
        len = sizeof(kbuf) - 1;

    if (copy_from_user(kbuf, buf, len))
        return -EFAULT;

    kbuf[len] = '\0';
    printk(KERN_INFO "Received: %s\n", kbuf);
    return len;
}

// 文件操作结构体
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};

// 模块初始化函数
static int __init my_init(void)
{
    dev_t dev;
    int ret;

    printk(KERN_INFO "Initializing the character device driver\n");

    // 动态分配设备号
    ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate device number\n");
        return ret;
    }
    major = MAJOR(dev);

    // 初始化字符设备
    cdev_init(&my_cdev, &fops);
    my_cdev.owner = THIS_MODULE;

    // 注册字符设备
    ret = cdev_add(&my_cdev, dev, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev, 1);
        printk(KERN_ERR "Failed to register character device\n");
        return ret;
    }

    // 创建设备类
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        printk(KERN_ERR "Failed to create device class\n");
        return PTR_ERR(my_class);
    }

    // 创建设备节点
    my_device = device_create(my_class, NULL, dev, NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        printk(KERN_ERR "Failed to create device\n");
        return PTR_ERR(my_device);
    }

    printk(KERN_INFO "Device registered with major number %d\n", major);
    return 0;
}

// 模块退出函数
static void __exit my_exit(void)
{
    dev_t dev = MKDEV(major, 0);

    printk(KERN_INFO "Removing the character device driver\n");

    // 销毁设备节点
    device_destroy(my_class, dev);
    // 销毁设备类
    class_destroy(my_class);
    // 删除字符设备
    cdev_del(&my_cdev);
    // 释放设备号
    unregister_chrdev_region(dev, 1);

    printk(KERN_INFO "Device driver removed\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");

3. 代码说明

3.1 设备号的分配
  • 使用 alloc_chrdev_region() 动态分配设备号,主设备号存储在 major 中。
  • 也可以使用 register_chrdev_region() 手动指定设备号,但动态分配更灵活。
3.2 字符设备的初始化和注册
  • 使用 cdev_init() 初始化字符设备结构体 my_cdev,并绑定文件操作 fops
  • 使用 cdev_add() 将字符设备注册到内核。
3.3 文件操作的实现
  • my_open() 和 my_release() 是设备打开和关闭的回调函数。
  • my_read() 实现从内核向用户空间读取数据,使用 copy_to_user() 传递数据。
  • my_write() 实现从用户空间向内核写入数据,使用 copy_from_user() 接收数据。
3.4 设备节点的自动创建
  • 使用 class_create() 创建设备类。
  • 使用 device_create() 创建设备节点,设备节点会自动出现在 /dev/ 目录下(例如 /dev/my_char_dev)。
  • 设备节点依赖于 udev 或 mdev,确保系统支持自动创建设备节点。
3.5 模块的初始化和退出
  • 在 my_init() 中完成设备号分配、字符设备注册、设备节点创建等操作。
  • 在 my_exit() 中清理资源,包括销毁设备节点、设备类、字符设备和释放设备号。

4. 编译和测试

4.1 编写 Makefile

obj-m += my_char_dev.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
4.2 编译和加载模块
  1. 编译模块:

    make
    

    编译后会生成 my_char_dev.ko 文件。

  2. 加载模块:

    sudo insmod my_char_dev.ko
    
  3. 查看设备节点:

    ls /dev/my_char_dev
    

    如果一切正常,设备节点 /dev/my_char_dev 应该已经创建。

  4. 查看内核日志:

    dmesg | tail
    
4.3 测试设备
  1. 读取设备:

    cat /dev/my_char_dev
    

    输出:Hello from kernel

  2. 写入设备:

    echo "Test message" > /dev/my_char_dev
    

    查看内核日志(dmesg),可以看到写入的内容。

  3. 卸载模块:

    sudo rmmod my_char_dev
    

5. 注意事项

  1. 权限问题

    • 设备节点默认权限可能需要调整,可以使用 chmod 修改:
      sudo chmod 666 /dev/my_char_dev
      
    • 或者在代码中通过 device_create() 的参数设置权限。
  2. 调试

    • 使用 printk() 输出调试信息,查看内核日志(dmesg)。
    • 如果设备节点未创建,检查 udev 或 mdev 是否正常工作。
  3. 错误处理

    • 确保在初始化失败时正确清理资源,避免内存泄漏或设备号冲突。
  4. 内核版本兼容性

    • 不同内核版本的 API 可能有变化,建议参考内核文档或使用 compat.h 处理兼容性问题。

6. 手动创建设备节点(可选)

如果 device_create() 无法自动创建设备节点,可以手动创建:

  1. 查看主设备号:

    cat /proc/devices | grep my_char_dev
    

    假设主设备号为 250

  2. 手动创建节点:

    sudo mknod /dev/my_char_dev c 250 0
    sudo chmod 666 /dev/my_char_dev
    

7. 总结

通过以上步骤,你可以:

  • 编写一个简单的字符设备驱动。
  • 实现基本的读写功能。
  • 自动或手动创建设备节点。
  • 测试设备的读写功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值