【Linux内核】设备驱动之字符设备介绍

目录

一、字符设备的基本概念

二、字符设备驱动的核心结构

1. struct cdev结构体的主要成员

2. 设备号的组成

3. 怎样分配设备号

三、字符设备驱动的注册与注销

四、设备操作函数集

五、实际开发产品实例

六、总结


一、字符设备的基本概念

        字符设备是Linux设备驱动中的一种类型,通常用于处理以字节流形式进行数据传输的设备。与块设备不同,字符设备不支持随机访问,数据只能按顺序读取或写入。常见的字符设备包括键盘、鼠标、串口等。

        字符设备驱动的主要任务是为用户空间提供访问硬件设备的接口。这些接口通常通过文件系统暴露给用户,用户可以通过标准的文件操作函数(如openreadwriteclose等)来与设备进行交互。

二、字符设备驱动的核心结构

        在Linux内核中,字符设备驱动的核心结构是struct cdev,它代表一个字符设备。struct cdev结构体包含了设备号、设备操作函数集等信息。设备号由主设备号和次设备号组成,主设备号用于标识设备类型,次设备号用于标识具体的设备实例。

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

1. struct cdev结构体的主要成员

  • dev_t dev:设备号,由主设备号和次设备号组成。主设备号用于标识设备类型,次设备号用于标识具体的设备实例。设备号可以通过MKDEV(major, minor)宏来生成,其中major是主设备号,minor是次设备号。

  • struct file_operations *ops:指向设备操作函数集的指针。file_operations结构体定义了设备的各种操作函数,如openreadwriterelease等。这些函数是用户空间与内核空间交互的接口。

2. 设备号的组成

  • 主设备号(Major Number):用于标识设备类型。例如,所有硬盘设备可能共享同一个主设备号,而所有串口设备可能共享另一个主设备号。主设备号的范围通常是0到255。

  • 次设备号(Minor Number):用于标识同一类型设备中的具体实例。例如,如果有多个串口设备,每个串口设备将有一个唯一的次设备号。次设备号的范围通常是0到255。

3. 怎样分配设备号

        在Linux设备驱动开发中,分配设备号是一个关键步骤,它用于在内核中唯一标识一个字符设备。设备号由主设备号和次设备号组成,主设备号用于标识设备类型,次设备号用于标识具体的设备实例。

分配设备号可以通过以下两种方式实现:

  1. 动态分配:使用alloc_chrdev_region函数

    • 该函数会自动分配一个可用的主设备号,并返回给调用者。这种方式适用于开发者不确定使用哪个主设备号,或者希望避免与其他设备冲突的情况。

    • 函数原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

    • 参数说明:

      • dev:用于存储分配的设备号。

      • baseminor:请求的起始次设备号。

      • count:请求的次设备号数量。

      • name:设备名称,通常出现在/proc/devices中。

    • 示例:
      dev_t dev;
      int ret = alloc_chrdev_region(&dev, 0, 1, "my_device");
      if (ret < 0) {
          printk(KERN_ERR "Failed to allocate device number\n");
          return ret;
      }
      
  2. 静态分配:使用register_chrdev_region函数

    • 该函数允许开发者指定一个特定的主设备号。这种方式适用于开发者已经知道要使用的主设备号,并且确保该设备号未被其他设备占用。

    • 函数原型:int register_chrdev_region(dev_t from, unsigned count, const char *name);

    • 参数说明:

      • from:指定的设备号。

      • count:请求的次设备号数量。

      • name:设备名称,通常出现在/proc/devices中。

    • 示例:
      dev_t dev = MKDEV(240, 0); // 主设备号为240,次设备号为0
      int ret = register_chrdev_region(dev, 1, "my_device");
      if (ret < 0) {
          printk(KERN_ERR "Failed to register device number\n");
          return ret;
      }
      

        在实际开发中,通常建议使用alloc_chrdev_region进行动态分配,以避免设备号冲突。分配成功后,设备号可以通过MAJORMINOR宏来提取主设备号和次设备号。设备号分配完成后,还需要通过cdev_initcdev_add等函数将设备与文件操作结构体关联,并注册到内核中。在设备驱动卸载时,应使用unregister_chrdev_region函数释放已分配的设备号,以避免资源泄漏。

三、字符设备驱动的注册与注销

字符设备驱动的注册通常通过cdev_initcdev_add函数完成。cdev_init函数用于初始化struct cdev结构体,并关联设备操作函数集。cdev_add函数将字符设备添加到内核中,使其对用户空间可见。

struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops);
cdev_add(&my_cdev, dev, 1);

字符设备的注销通过cdev_del函数完成,通常在模块卸载时调用。

cdev_del(&my_cdev);

在Linux内核中,字符设备驱动的注册与注销通常通过以下步骤完成:

  1. 分配设备号:使用alloc_chrdev_regionregister_chrdev_region函数分配设备号。

  2. 初始化cdev结构体:使用cdev_init函数初始化cdev结构体,并设置file_operations

  3. 添加cdev到系统:使用cdev_add函数将cdev添加到系统中,使其对用户空间可见。

  4. 注销设备:使用cdev_del函数从系统中删除cdev,并使用unregister_chrdev_region函数释放设备号。

四、设备操作函数集

        设备操作函数集struct file_operations定义了用户空间与设备交互的接口。常见的操作函数包括openreleasereadwriteioctl等。

struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
    .ioctl = my_ioctl,
};

file_operations结构体定义了设备的各种操作函数,这些函数是用户空间与内核空间交互的接口。常见的操作函数包括:

  • int (*open)(struct inode *, struct file *):打开设备时调用的函数。

  • ssize_t (*read)(struct file *, char __user *, size_t, loff_t *):从设备读取数据时调用的函数。

  • ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *):向设备写入数据时调用的函数。

  • int (*release)(struct inode *, struct file *):关闭设备时调用的函数。

五、实际开发产品实例

        假设开发一个简单的LED控制设备驱动,该设备通过字符设备接口控制LED的开关。首先,定义设备操作函数集。

static int led_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "LED device opened\n");
    return 0;
}

static int led_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "LED device closed\n");
    return 0;
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    char val;
    if (copy_from_user(&val, buf, 1))
        return -EFAULT;
    if (val == '1')
        printk(KERN_INFO "LED ON\n");
    else if (val == '0')
        printk(KERN_INFO "LED OFF\n");
    return count;
}

static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .release = led_release,
    .write = led_write,
};

接下来,初始化并注册字符设备。

static dev_t dev;
static struct cdev led_cdev;

static int __init led_init(void) {
    int ret;
    dev = MKDEV(240, 0);
    ret = register_chrdev_region(dev, 1, "led");
    if (ret < 0) {
        printk(KERN_ERR "Failed to register device\n");
        return ret;
    }
    cdev_init(&led_cdev, &led_fops);
    ret = cdev_add(&led_cdev, dev, 1);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add cdev\n");
        unregister_chrdev_region(dev, 1);
        return ret;
    }
    printk(KERN_INFO "LED device registered\n");
    return 0;
}

static void __exit led_exit(void) {
    cdev_del(&led_cdev);
    unregister_chrdev_region(dev, 1);
    printk(KERN_INFO "LED device unregistered\n");
}

module_init(led_init);
module_exit(led_exit);

用户空间可以通过以下命令控制LED设备。

echo 1 > /dev/led  # 打开LED
echo 0 > /dev/led  # 关闭LED

六、总结

        字符设备驱动是Linux设备驱动中的重要组成部分,通过文件系统接口为用户空间提供访问硬件设备的能力。开发字符设备驱动需要理解struct cdevstruct file_operations等核心结构,并掌握设备的注册、注销以及操作函数的实现。通过实际开发产品实例,可以更好地理解字符设备驱动的具体技术要点和细节。

你可能感兴趣的:(Linux内核,linux,Linux内核,字符设备,Linux设备驱动)