字符设备驱动开发基础—静态/动态注册设备号,使用cdev注册驱动

发布于:2024-08-02 ⋅ 阅读:(44) ⋅ 点赞:(0)

主次设备号介绍

在Linux和类Unix操作系统中,设备文件用于表示各种硬件设备和虚拟设备。每个设备文件通过一个唯一的设备号进行标识,该设备号由主设备号次设备号组成。设备号帮助操作系统将设备文件与实际的设备驱动程序关联起来,以便正确处理对设备的操作请求。

主设备号 (Major Number)

主设备号是设备号的高位部分,用于标识设备的类型或类别。它决定了操作系统使用哪个设备驱动程序来处理设备文件的操作请求。每种设备类型(如磁盘、终端、网络设备等)通常都有一个唯一的主设备号。

  • 主设备号的分配是由操作系统维护的,确保每个设备驱动程序都有一个唯一的主设备号。
  • 内核使用主设备号来查找与之关联的设备驱动程序。驱动程序注册时会声明其支持的主设备号。
  • 例如,主设备号为8的设备通常表示SCSI磁盘设备。

次设备号 (Minor Number)

次设备号是设备号的低位部分,用于标识同一类设备中的具体实例或单个设备。例如,多个硬盘驱动器或分区可能使用相同的主设备号,但有不同的次设备号。

  • 次设备号允许同一设备驱动程序管理多个设备实例。例如,多个硬盘、多个串口等。
  • 对于字符设备和块设备,次设备号的使用方式可能有所不同。字符设备的次设备号通常用于区分不同的设备,而块设备的次设备号通常用于区分不同的分区或设备上的逻辑单元。

设备号的表示与注册

设备号在Linux内核中通常使用dev_t类型表示,这是一个32位的整数,其中高12位表示主设备号,低20位表示次设备号。

设备号的获取

设备号可以通过以下方式获取:

  1. 静态分配:在驱动程序中直接指定主设备号和次设备号。

    int major = 240; // 静态指定主设备号
    dev_t dev = MKDEV(major, 0);
    
  2. 动态分配:通过内核提供的API函数动态分配主设备号。

    dev_t dev;
    int result = alloc_chrdev_region(&dev, 0, 1, "my_device");
    if (result < 0) {
        // 处理错误
    }
    int major = MAJOR(dev); // 获取分配的主设备号
    int minor = MINOR(dev); // 获取次设备号
    
设备号的注册

在驱动程序中,设备号的注册通常涉及以下步骤:

  1. 申请设备号:使用register_chrdev_region()alloc_chrdev_region()函数来申请设备号范围。

  2. 注册设备:在cdev结构体中设置设备号,并调用cdev_add()函数将设备注册到内核。

设备文件的创建

设备文件通常位于/dev目录下,通过mknod命令创建,指定主设备号和次设备号。例如:

mknod /dev/my_device c 240 0

这里,c表示字符设备,240是主设备号,0是次设备号。

实际应用中的主次设备号

  1. 块设备:如硬盘、分区等。主设备号区分设备类型,次设备号区分不同的硬盘或分区。
  2. 字符设备:如串口、终端等。主设备号区分设备类型,次设备号区分不同的端口或终端。

cdev 结构体介绍

在Linux内核中,cdev结构体是字符设备驱动程序的核心数据结构之一,用于表示和管理字符设备。字符设备是通过字符设备文件接口与用户空间进行交互的设备,例如终端、串口、鼠标等。cdev结构体帮助将字符设备与设备文件关联起来,并定义了设备的操作方法。

cdev 结构体

cdev结构体位于内核源码的include/linux/cdev.h文件中,其主要字段和作用如下:

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};
主要字段
  1. kobject kobj

    • 这是cdev在内核中注册为对象的基础。kobject是内核中的通用对象结构,提供了内核对象的基础设施,包括引用计数和sysfs接口支持。
    • 通过kobject,可以将字符设备对象与内核其他部分进行关联和管理。
  2. module *owner

    • 指向拥有该字符设备的内核模块。通常,这个字段被设置为模块宏THIS_MODULE,用于表示当前的加载模块。
    • 通过该字段,内核可以管理模块的引用计数,防止在模块被卸载时还有未完成的操作。
  3. const struct file_operations *ops

    • 指向文件操作结构体(file_operations),该结构体定义了字符设备的各种操作函数,如openreadwriteioctl等。
    • 这是字符设备的核心,所有的用户空间操作请求都会通过这些函数实现。
  4. struct list_head list

    • 用于将cdev结构体链接到一个链表中。这在内部管理多个字符设备或设备实例时非常有用。
  5. dev_t dev

    • 设备号,包括主设备号和次设备号。设备号唯一标识一个字符设备文件,使其能够与特定的设备关联。
    • 主设备号用于标识设备类型,次设备号用于区分同一类型的不同设备实例。
  6. unsigned int count

    • 指定字符设备的设备号范围,通常为1,表示一个cdev结构体实例管理一个设备号。

使用 cdev 结构体的步骤

  1. 定义和初始化 cdev 结构体

    • 通过静态分配或动态分配定义cdev结构体。
    • 使用cdev_init()函数初始化cdev结构体,将file_operations结构体的指针赋给ops字段。
    struct cdev my_cdev;
    cdev_init(&my_cdev, &my_fops);
    
  2. 分配和注册设备号

    • 使用alloc_chrdev_region()函数动态分配设备号,或使用register_chrdev_region()函数注册已知设备号。
    • 将获得的设备号存储在dev_t类型的变量中。
    dev_t dev;
    alloc_chrdev_region(&dev, 0, 1, "my_device");
    
  3. 添加 cdev 到内核

    • 使用cdev_add()函数将初始化后的cdev结构体添加到内核中,关联到之前分配的设备号。
    cdev_add(&my_cdev, dev, 1);
    
  4. 清理 cdev 结构体

    • 在模块卸载或不再使用设备时,使用cdev_del()函数从内核中移除cdev结构体,并释放设备号。
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev, 1);
    

file_operations 结构体

file_operations结构体定义了字符设备的操作接口,是用户空间和设备之间的桥梁。常见的操作包括:

  • open:打开设备文件时调用。
  • release:关闭设备文件时调用。
  • read:从设备读取数据。
  • write:向设备写入数据。
  • ioctl:控制设备的操作。

示例:

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

通过使用cdev结构体和file_operations结构体,字符设备驱动程序可以在Linux内核中注册和管理设备,从而实现设备与用户空间的交互。

静态注册设备号

在驱动程序中直接指定主设备号和次设备号。

示例代码:

#include <linux/init.h>
#include <linux/module.h>

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

#define DEVICE_NAME "my_char_device"
#define MAJOR 200
#define MINJOR 0
static struct cdev mydev;
static dev_t dev; //存储设备号
static int my_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "my_char_device: open()\n");
    return 0;
}
static int my_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "my_char_device: release()\n");
    return 0;
}
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {
    printk(KERN_INFO "my_char_device: read()\n");
    return 0;
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    printk(KERN_INFO "my_char_device: write()\n");
    return count;
}
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};
//在模块加载到内核程序中被执行一次
int __init test_init(void)
{
    printk("module init success\n");
    int retval;
    //使用cdev 接口来注册字符设备驱动
    //1. 注册 /分配 主次设备驱动号
    dev = MKDEV(MAJOR,MINJOR);
    retval = register_chrdev_region(dev,1,DEVICE_NAME);
    if(retval){
        //说明注册失败了,可能已经被占用了
        printk(KERN_ERR "Unable to register minors for %s\n",DEVICE_NAME);
        return retval;
    }
    printk(KERN_INFO "staic register minors success\n");
    //2.注册字符设备驱动
    //传入cdev 和 文件操作结构体
    cdev_init(&mydev,&fops);
    retval = cdev_add(&mydev,dev,1);  //注册一个字符设备驱动
    if(retval<0){
        printk(KERN_ERR "Failed to add cdev\n");  
        unregister_chrdev_region(dev, 1);  //注销设备号
        return retval;
    }
    printk(KERN_INFO "my_char_device: module loaded\n");
    return 0;
}
//在模块从内核驱动中卸载时执行一次
void __exit test_exit(void)
{
    cdev_del(&mydev);
    unregister_chrdev_region(dev, 1);
}
module_init(test_init);		//注册此模块加载的回调函数
module_exit(test_exit);		//注册此模块卸载的回调函数
MODULE_LICENSE("GPL");		//声明遵循协议

其中直接指定了 主设备号200 次设备号为0

效果演示

使用dmesg 可以查看输出提示

在这里插入图片描述

查看当前注册的设备号

cat /proc/devices

可以看到200 主设备号的驱动程序已经注册成功了

在这里插入图片描述

动态注册设备号

示例代码

#include <linux/init.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#define DEVICE_NAME "my_char_device"
static struct cdev mydev;
int major_number;
static int my_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "my_char_device: open()\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "my_char_device: release()\n");
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    printk(KERN_INFO "my_char_device: read()\n");
    return 0;
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    printk(KERN_INFO "my_char_device: write()\n");
    return count;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};

// 在模块加载到内核程序中被执行一次
int __init test_init(void)

{
    printk("module init success\n");
    int retval;

    // 使用cdev 接口来注册字符设备驱动
    // 1. 动态分配 主次设备驱动号
    dev_t dev;
    retval = alloc_chrdev_region(&dev, 0, 1, "my_device");
    if (retval < 0)
    {
        printk(KERN_ERR "Failed to allocate major number\n");
        return retval;
        // 处理错误
    }
    int major = MAJOR(dev); // 获取分配的主设备号
    int minor = MINOR(dev); // 获取次设备号
    printk(KERN_INFO "major number is:%d,  minor number is:%d\n",major,minor);
    major_number = MAJOR(dev);
    // 2.注册字符设备驱动
    // 传入cdev 和 文件操作结构体
    cdev_init(&mydev, &fops);
    retval = cdev_add(&mydev, dev, 1); // 注册一个字符设备驱动
    if (retval < 0)
    {
        printk(KERN_ERR "Failed to add cdev\n");
        unregister_chrdev_region(dev, 1); // 注销设备号
        return retval;
    }
    
    printk(KERN_INFO "my_char_device: module loaded\n");
    return 0;
}
// 在模块从内核驱动中卸载时执行一次
void __exit test_exit(void)
{
    cdev_del(&mydev);
    dev_t  dev = MKDEV(major_number,0);
    unregister_chrdev_region(dev, 1);
}
module_init(test_init); // 注册此模块加载的回调函数
module_exit(test_exit); // 注册此模块卸载的回调函数

MODULE_LICENSE("GPL"); // 声明遵循协议

与静态注册不同的是使用的函数为 retval = alloc_chrdev_region(&dev, 0, 1, "my_device");

推荐使用动态分配的方式,更加灵活

效果演示

动态分配了主设备号为235

在这里插入图片描述