linux 字符设备 一、cdev结构体在linux内核中使用cdev结构体来描述一个字符设备include/linux/cdev.hstructcdev{structkobjectkobj;//内嵌的内核对象.structmodule*owner;//该字符设备所在的内核模块的对象指针.conststructfile_operations*ops;//该结构描述了字符设备所能实现的方法是极为关键的一个结构体.structlist_headlist;//用来将已经向内核注册的所有字符设备形成链表.dev_tdev;//字符设备的设备号由主设备号和次设备号构成.unsignedintcount;//隶属于同一主设备号的次设备号的个数.};二、dev_t在内核中dev_t用来表示设备编号。三个函数申请设备号2.1 register_chrdev_region函数register_chrdev_region函数用于静态地为一个字符设备申请一个或多个设备编号。函数原型如下所示。register_chrdev_region函数intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name)函数参数和返回值如下参数fromdev_t类型的变量用于指定字符设备的起始设备号如果要注册的设备号已经被其他的设备注册了那么就会导致注册失败。count指定要申请的设备号个数count的值不可以太大否则会与下一个主设备号重叠。name用于指定该设备的名称我们可以在/proc/devices中看到该设备。返回值 返回0表示申请成功失败则返回错误码2.2 alloc_chrdev_region函数使用register_chrdev_region函数时都需要去查阅内核源码的Documentation/admin-guide/devices.txt文件 这就十分不方便。因此内核又为我们提供了一种能够动态分配设备编号的方式alloc_chrdev_region。调用alloc_chrdev_region函数内核会自动分配给我们一个尚未使用的主设备号。 我们可以通过命令“cat /proc/devices”查询内核分配的主设备号。alloc_chrdev_region函数原型内核源码/fs/char_dev.cintalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name)函数参数和返回值如下参数dev指向dev_t类型数据的指针变量用于存放分配到的设备编号的起始值baseminor次设备号的起始值通常情况下设置为0count、name同register_chrdev_region类型用于指定需要分配的设备编号的个数以及设备的名称。返回值 返回0表示申请成功失败则返回错误码2.3 register_chrdev函数除了上述的两种内核还提供了register_chrdev函数用于分配设备号。该函数是一个内联函数它不 仅支持静态申请设备号也支持动态申请设备号并将主设备号返回函数原型如下所示。staticinlineintregister_chrdev(unsignedintmajor,constchar*name,conststructfile_operations*fops){return__register_chrdev(major,0,256,name,fops);}我们从以上代码中可以看到使用register_chrdev函数向内核申请设备号同一类字 符设备即主设备号相同会在内核中申请了256个通常情况下我们不需要用到这么多个设备这就造成了极大的资源浪费。三、字符设备驱动程序框架四、示例代码#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h#includelinux/types.h// 1. 宏定义#defineDEV_NAMEchrdev_demo// /dev/下设备文件名#defineCLASS_NAMEchr_class// 设备类名#defineBUF_LEN128// 内核缓冲区大小// 2. 私有设备结构体存放设备私有数据structchrdev_private{structcdevcdev;// 字符设备核心结构体charkbuf[BUF_LEN];// 内核缓冲区intbuf_len;// 当前有效数据长度};// 全局变量dev_tdevno;// 存放分配的主次设备号staticstructclass*dev_class;staticstructchrdev_privatedev_priv;// 文件操作接口 // open打开设备文件staticintchrdev_open(structinode*inode,structfile*file){file-private_datadev_priv;// 将私有数据绑定到file结构体printk(KERN_INFOchrdev open success\n);return0;}// release关闭设备文件staticintchrdev_release(structinode*inode,structfile*file){printk(KERN_INFOchrdev release\n);return0;}// read用户层读内核数据staticssize_tchrdev_read(structfile*file,char__user*ubuf,size_tcount,loff_t*ppos){structchrdev_private*privfile-private_data;intret;// 拷贝内核缓冲区数据到用户空间retcopy_to_user(ubuf,priv-kbuf,priv-buf_len);if(ret!0){printk(KERN_ERRcopy_to_user failed\n);return-EFAULT;}printk(KERN_INFOread %d bytes from kernel buf\n,priv-buf_len);returnpriv-buf_len;}// write用户层写数据到内核staticssize_tchrdev_write(structfile*file,constchar__user*ubuf,size_tcount,loff_t*ppos){structchrdev_private*privfile-private_data;intcopy_lencountBUF_LEN?BUF_LEN:count;// 用户空间 - 内核空间if(copy_from_user(priv-kbuf,ubuf,copy_len)){return-EFAULT;}priv-buf_lencopy_len;printk(KERN_INFOwrite %d bytes to kernel: %s\n,copy_len,priv-kbuf);returncopy_len;}// 文件操作函数集staticstructfile_operationschr_fops{.ownerTHIS_MODULE,.openchrdev_open,.releasechrdev_release,.readchrdev_read,.writechrdev_write,};// 驱动加载入口 staticint__initchrdev_demo_init(void){intret;// 1. 动态分配设备号自动分配主设备号次设备0你的主设备号就会写入 /proc/devicesretalloc_chrdev_region(devno,0,1,DEV_NAME);if(ret0){printk(KERN_ERRalloc devno failed!\n);returnret;}printk(KERN_INFOalloc dev success, major:%d minor:%d\n,MAJOR(devno),MINOR(devno));// 2. 初始化cdev结构体绑定文件操作集cdev_init(dev_priv.cdev,chr_fops);dev_priv.cdev.ownerTHIS_MODULE;// 3. 向内核注册字符设备 绑定设备号与文件操作接口retcdev_add(dev_priv.cdev,devno,1);if(ret0){unregister_chrdev_region(devno,1);printk(KERN_ERRcdev add failed\n);returnret;}// 4. 创建设备类 /sys/class/CLASS_NAMEdev_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(dev_class)){cdev_del(dev_priv.cdev);unregister_chrdev_region(devno,1);returnPTR_ERR(dev_class);}// 5. 自动在/dev生成设备文件 /dev/DEV_NAMEdevice_create(dev_class,NULL,devno,NULL,DEV_NAME);printk(KERN_INFOchrdev driver install ok, /dev/%s ready\n,DEV_NAME);return0;}// 驱动卸载入口 staticvoid__exitchrdev_demo_exit(void){// 逆序释放资源和init流程相反device_destroy(dev_class,devno);class_destroy(dev_class);cdev_del(dev_priv.cdev);unregister_chrdev_region(devno,1);printk(KERN_INFOchrdev driver unload\n);}module_init(chrdev_demo_init);module_exit(chrdev_demo_exit);MODULE_LICENSE(GPL);MODULE_AUTHOR(Driver Demo);MODULE_DESCRIPTION(Standard Char Device Driver Framework);MODULE_VERSION(1.0);核心结论分步拆解全过程1. alloc_chrdev_region / register_chrdev_region仅占用设备号会刷新 /proc/devices这一步只是在内核的设备号注册表标记一段主/次设备号为已占用完成两件事分配/锁定 dev_t 号段保存你传入的驱动名字会刷新 /proc/devices。此时 cat /proc/devices 看你的驱动。2. cdev_init()仅绑定 file_operations无任何 proc 动作只是把struct cdev和操作函数集绑定完全不接触 /proc。3. cdev_add()真正注册到字符设备核心写入 /proc/devicescdev_add做关键两件事将struct cdev对象挂载到内核字符设备哈希链表把「主设备号 驱动名」注册到/proc/devices的展示链表。调用成功后立刻 cat /proc/devices 就能看到你的主设备号和名称。补充/proc/devices是内核动态生成的虚拟文件不是磁盘实体文件每次 cat 时内核实时遍历已注册的字符设备链表输出内容。关键区分各个接口分别影响哪些文件函数/proc/devices/sys/class/xxx/dev/xxxalloc_chrdev_region✅ 出现主设备号❌ 无❌ 无cdev_add✅ 出现主设备号❌ 无❌ 无class_create✅ 已有条目✅ 创建类目录❌ 无device_create✅ 已有条目✅ 生成设备子目录✅ udev/mdev自动生成反向什么时候从 /proc/devices 消失调用cdev_del(chr_dev)时内核会把该 cdev 从字符设备链表移除再次 cat /proc/devices你的驱动条目就消失。完整卸载顺序device_destroy()删 /dev 节点class_destroy()销毁 sysfs 类cdev_del()→ 从 /proc/devices 移除条目unregister_chrdev_region()释放设备号补充易错点只分配设备号、不调用cdev_add/proc/devices 永远看不到你的驱动class_create/device_create只是管理 sysfs 和 /dev不影响 /proc/devices有没有 class 不影响 proc 里是否显示/proc/devices 只展示主设备号不区分次设备号同一主设备号只显示一行。