在Linux和类Unix操作系统中,设备文件用于表示各种硬件设备和虚拟设备。每个设备文件通过一个唯一的设备号进行标识,该设备号由主设备号和次设备号组成。设备号帮助操作系统将设备文件与实际的设备驱动程序关联起来,以便正确处理对设备的操作请求。
主设备号是设备号的高位部分,用于标识设备的类型或类别。它决定了操作系统使用哪个设备驱动程序来处理设备文件的操作请求。每种设备类型(如磁盘、终端、网络设备等)通常都有一个唯一的主设备号。
次设备号是设备号的低位部分,用于标识同一类设备中的具体实例或单个设备。例如,多个硬盘驱动器或分区可能使用相同的主设备号,但有不同的次设备号。
设备号在Linux内核中通常使用dev_t类型表示,这是一个32位的整数,其中高12位表示主设备号,低20位表示次设备号。
设备号可以通过以下方式获取:
静态分配:在驱动程序中直接指定主设备号和次设备号。
int major = 240; // 静态指定主设备号 dev_t dev = MKDEV(major, 0); 动态分配:通过内核提供的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); // 获取次设备号 在驱动程序中,设备号的注册通常涉及以下步骤:
申请设备号:使用register_chrdev_region()或alloc_chrdev_region()函数来申请设备号范围。
注册设备:在cdev结构体中设置设备号,并调用cdev_add()函数将设备注册到内核。
设备文件通常位于/dev目录下,通过mknod命令创建,指定主设备号和次设备号。例如:
mknod /dev/my_device c 240 0 这里,c表示字符设备,240是主设备号,0是次设备号。
在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; }; kobject kobj:
cdev在内核中注册为对象的基础。kobject是内核中的通用对象结构,提供了内核对象的基础设施,包括引用计数和sysfs接口支持。kobject,可以将字符设备对象与内核其他部分进行关联和管理。module *owner:
THIS_MODULE,用于表示当前的加载模块。const struct file_operations *ops:
file_operations),该结构体定义了字符设备的各种操作函数,如open、read、write、ioctl等。struct list_head list:
cdev结构体链接到一个链表中。这在内部管理多个字符设备或设备实例时非常有用。dev_t dev:
unsigned int count:
cdev结构体实例管理一个设备号。cdev 结构体的步骤定义和初始化 cdev 结构体:
cdev结构体。cdev_init()函数初始化cdev结构体,将file_operations结构体的指针赋给ops字段。struct cdev my_cdev; cdev_init(&my_cdev, &my_fops); 分配和注册设备号:
alloc_chrdev_region()函数动态分配设备号,或使用register_chrdev_region()函数注册已知设备号。dev_t类型的变量中。dev_t dev; alloc_chrdev_region(&dev, 0, 1, "my_device"); 添加 cdev 到内核:
cdev_add()函数将初始化后的cdev结构体添加到内核中,关联到之前分配的设备号。cdev_add(&my_cdev, dev, 1); 清理 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 #include #include #include #include #include #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 #include #include #include #include #include #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
