设备号
通过访问设备节点可以调用字符设备驱动程序,设备节点通常位于 /dev 目录下。 ls -l 命令输出权限字符串的首字母 c 来标记字符设备,同时在原本显示文件大小的位置,会显示出该设备的主设备号和次设备号。 主设备号与特定的字符设备驱动程序绑定,用户空间对设备节点进行的打开、读写等操作,最终会被内核路由调用该驱动程序对应的函数。 次设备号由驱动程序内部用于区分同一驱动管理下的不同设备实例,驱动通过次设备号来定位具体的物理或逻辑设备。
手动创建设备节点
#1.加载模块 sudo insmod scull.ko #2.自动获取主设备号并赋值给变量 scull_major=$(awk'$2=="scull" {print $1}'/proc/devices)#3.创建设备节点 sudo mknod/dev/scull0 c $scull_major0#4.修改节点权限,让普通用户也能读写,否则默认只有 root 能访问 sudo chmod666/dev/scull0 #5.手动创建的节点也需要手动删除 sudo rmmod scull sudo rm/dev/scull0file_operations file inode
1、struct file_operations(驱动的能力清单)
核心定位:它是驱动向内核暴露功能的接口,定义了驱动程序支持哪些操作(如 open、read、write 等)。
本质:这是一个函数指针的集合。用面向对象编程的术语来说,我们可以将 file 视为“对象”,而 file_operations 中的函数就是作用于该对象的“方法”。
关键机制:
结构体中的每个字段必须指向驱动中实现特定操作的函数。
对于不支持的操作,可以将其设为 NULL。内核在遇到 NULL 指针时,会根据具体函数执行不同的默认行为(例如 read 为 NULL 时会返回 -EINVAL 错误)。
第一个成员通常是 owner,一般被初始化为 THIS_MODULE,用于防止模块在操作还在被使用时被意外卸载。
2、inode(设备文件元信息载体)
当用户程序调用 fd = open(“/dev/mydev”, O_RDWR|O_NONBLOCK) :
1、VFS(虚拟文件系统)拿到字符串 “/dev/mydev”,在目录树中找到对应的目录项并提取出该文件对应的 inode 结构体。
2、VFS 发现 inode 属于字符设备(通过 inode->i_mode),然后从 inode 中读取之前存好的设备号( dev_t dev = inode->i_rdev);
3、VFS 拿着提取出来的 dev_t,通过内核全局字符设备映射表(kobj_map) 中进行查找对应的 cdev 指针。
4、VFS 从 cdev 中取出 file_operations,将其赋值给当前打开的文件结构体(file->f_op = cdev->ops)。
如果 fops 里定义了 open 函数,VFS 就会调用驱动的 open 函数,并且把第 1 步找到的 inode 和当前的 file 作为参数传递进去:
driver_open(struct inode *inode, struct file *filp)
核心链路:节点文件 ➔ inode ➔ 设备号 ➔ cdev ➔ 操作方法 (fops)
3、struct file (用户与设备的交互会话)
当用户程序调用 fd = open(“/dev/mydev”, O_RDWR|O_NONBLOCK) ,内核不仅会找到对应的 inode,还会在内存中动态分配一个全新的 struct file 。file 只关心进程 “怎么操作这个设备”。 它记录了这次打开操作的所有状态信息。
结构体核心字段
1、f_mode 和 f_flags
f_flags:直接来自用户态 open(fd, flags) 中的 flags 参数(如 O_NONBLOCK 供驱动开发者使用,用于判断用户的具体操作意图和状态)。
f_mode:内核 VFS 拿到 f_flags 后解析、转换生成的内部标志(如 FMODE_READ 供内核 VFS 使用,用于底层权限校验。)。
2、f_pos(文件偏移量)
记录当前读写操作到了设备的哪个位置(偏移量)。
作用:在 read 和 write 函数中,必须根据 filp->f_pos 来决定从设备的哪个地址开始读/写数据,操作完后还要更新 f_pos。
3、private_data
在 open 函数中,我们通过 inode 找到了具体的硬件设备结构体(比如 struct my_dev *dev),然后把它赋值给 filp->private_data = dev;。这样,在后续的 read/write/release 函数中,只需通过 filp->private_data 就能瞬间找回这个设备的上下文。
4、f_op(操作方法指针)
VFS 在权限校验通过并创建 file 结构体后,会将底层 cdev 中绑定的 file_operations 指针,赋值给 file->f_op。VFS 最终通过 file->f_op->open(inode, file) 这一函数指针,将控制权正式移交给驱动开发者编写的 open 函数。驱动 open 函数接收的两个参数,正是 VFS 一路解析得到的 inode(包含设备号)和刚刚实例化的 file(包含操作上下文)。
open(“/dev/mydev”, O_RDWR|O_NONBLOCK) 完整链路
路径解析与权限校验(VFS 层)
VFS 根据路径 /dev/mydev 逐层解析,最终找到设备节点对应的 inode。
VFS 提取 flags 中的读写意图转换为 f_mode(FMODE_READ | FMODE_WRITE),并与 inode 的权限位(i_mode)进行严格校验。校验失败则直接返回 -EACCES。实例化 file 与设备映射(VFS 层)
校验通过后,内核分配并创建 file 结构体,将原始的 flags(含 O_NONBLOCK)存入 file->f_flags,将 f_mode 存入 file->f_mode。
VFS 从 inode->i_rdev 中取出完整的设备号(dev_t),通过字符设备映射表(kobj_map)查找到对应的 cdev 指针。
VFS 将 cdev->ops(即驱动注册的 file_operations)赋值给 file->f_op。回调驱动接口(驱动层)
VFS 最终通过函数指针 file->f_op->open(inode, filp),将找到的 inode 和刚创建的 file(filp)作为参数,将控制权正式移交给驱动开发者编写的 open 函数,完成整个调用闭环。
#include<linux/module.h>#include<linux/init.h>#include<linux/types.h>#include<linux/kdev_t.h>#include<linux/cdev.h>#include<linux/fs.h>#include<linux/device.h>#include<linux/mutex.h>#include<linux/uaccess.h>#include<linux/slab.h>#include<linux/version.h>#include<linux/kernel.h>#defineSCULL_DEVICES4#defineSCULL_NAME"scull"#defineQSET_SIZE1000#defineQUANTUM4000structscull_qset{void**data;structscull_qset*next;};structscull_dev{structscull_qset*qset_head;unsignedintquantum;unsignedintqset_size;unsignedsize;structmutexlock;dev_tdevno;structcdevcdev;};staticunsignedintscull_major=0;staticstructclass*scull_class;structscull_devscull_devs[SCULL_DEVICES];staticvoidscull_trim(structscull_dev*dev){inti;structscull_qset*tmp_next,*dptr;for(dptr=dev->qset_head;dptr!=NULL;dptr=tmp_next){tmp_next=dptr->next;for(i=0;i<dev->qset_size;i++){if(dptr->data[i]!=NULL){kfree(dptr->data[i]);}}kfree(dptr->data);kfree(dptr);}dev->qset_head=NULL;dev->size=0;printk(KERN_INFO"scull_trim successed!\n");}staticintscull_open(structinode*inode,structfile*filp){structscull_dev*dev=container_of(inode->i_cdev,structscull_dev,cdev);filp->private_data=dev;if(filp->f_flags&(O_RDWR|O_WRONLY)){if(filp->f_flags&O_TRUNC){mutex_lock(&dev->lock);scull_trim(dev);mutex_unlock(&dev->lock);}}printk(KERN_INFO"scull_open succeed!\n");return0;}staticintscull_release(structinode*inode,structfile*filp){printk(KERN_INFO"scull_release successed!\n");return0;}staticssize_tscull_read(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos){structscull_dev*dev=filp->private_data;structscull_qset*dptr;unsignedintitem_size=dev->quantum*dev->qset_size;intitem,s_pos,q_pos,rest;ssize_tretval=0;if(mutex_lock_interruptible(&dev->lock))return-EINTR;if(*f_pos>dev->size)gotoout;if(count>dev->size-*f_pos)count=dev->size-*f_pos;item=*f_pos/item_size;rest=*f_pos%item_size;q_pos=rest/dev->quantum;s_pos=rest%dev->quantum;dptr=dev->qset_head;while(item--&&dptr){dptr=dptr->next;}if(count>dev->quantum-s_pos){count=dev->quantum-s_pos;}if(dptr&&dptr->data&&dptr->data[q_pos]){if(copy_to_user(buf,dptr->data[q_pos]+s_pos,count)){retval=-EFAULT;gotoout;}*f_pos+=count;retval=count;}else{gotoout;}out:mutex_unlock(&dev->lock);returnretval;}staticssize_tscull_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_pos){structscull_dev*dev=filp->private_data;structscull_qset*dptr;unsignedintitem_size=dev->quantum*dev->qset_size;intitem,s_pos,q_pos,rest;ssize_tretval=0;if(mutex_lock_interruptible(&dev->lock))return-EINTR;item=*f_pos/item_size;rest=*f_pos%item_size;q_pos=rest/dev->quantum;s_pos=rest%dev->quantum;dptr=dev->qset_head;if(dptr==NULL){dptr=kmalloc(sizeof(structscull_qset),GFP_KERNEL);if(!dptr)gotoout;memset(dptr,0,sizeof(structscull_qset));dev->qset_head=dptr;}while(item-->0){if(!dptr->next){dptr->next=kmalloc(sizeof(structscull_qset),GFP_KERNEL);if(!dptr->next)gotoout;memset(dptr->next,0,sizeof(structscull_qset));}dptr=dptr->next;}if(!dptr->data){dptr->data=kmalloc(dev->qset_size*sizeof(char*),GFP_KERNEL);if(!dptr->data)gotoout;memset(dptr->data,0,dev->qset_size*sizeof(char*));}if(!dptr->data[q_pos]){dptr->data[q_pos]=kmalloc(dev->quantum,GFP_KERNEL);if(!dptr->data[q_pos])gotoout;memset(dptr->data[q_pos],0,dev->quantum);}if(count>dev->quantum-s_pos){count=dev->quantum-s_pos;}if(copy_from_user(dptr->data[q_pos]+s_pos,buf,count)){retval=-EFAULT;gotoout;}*f_pos+=count;retval=count;if(dev->size<*f_pos){dev->size=*f_pos;}out:mutex_unlock(&dev->lock);returnretval;}staticconststructfile_operationsscull_fops={.owner=THIS_MODULE,.open=scull_open,.release=scull_release,.read=scull_read,.write=scull_write,};staticint__initscull_init(void){intret,i;dev_tdevno;ret=alloc_chrdev_region(&devno,0,SCULL_DEVICES,SCULL_NAME);if(ret){printk(KERN_ERR"alloc_chrdev_region failed \n");returnret;}scull_major=MAJOR(devno);printk(KERN_INFO"scull_major = %d",scull_major);#ifLINUX_VERSION_CODE>=KERNEL_VERSION(6,4,0)scull_class=class_create(SCULL_NAME);#elsescull_class=class_create(THIS_MODULE,SCULL_NAME);#endifif(IS_ERR(scull_class)){ret=PTR_ERR(scull_class);printk(KERN_ERR"scull failed create class ret = %d \n",ret);gotoerr_unregister_region;}for(i=0;i<SCULL_DEVICES;i++){structscull_dev*dev=&scull_devs[i];dev->qset_size=QSET_SIZE;dev->quantum=QUANTUM;dev->size=0;dev->devno=MKDEV(scull_major,i);mutex_init(&dev->lock);dev->qset_head=NULL;cdev_init(&dev->cdev,&scull_fops);dev->cdev.owner=THIS_MODULE;device_create(scull_class,NULL,dev->devno,NULL,"scull%d",i);ret=cdev_add(&dev->cdev,dev->devno,1);if(ret){while(i--){cdev_del(&scull_devs[i].cdev);device_destroy(scull_class,scull_devs[i].devno);}printk(KERN_ERR"failed to add scull_cdev %d\n",ret);gotoerr_create_class;}}printk(KERN_INFO"scull_init succeed!\n");return0;err_create_class:class_destroy(scull_class);err_unregister_region:unregister_chrdev_region(devno,SCULL_DEVICES);returnret;}staticvoid__exitscull_exit(void){inti;dev_tdevno=MKDEV(scull_major,0);for(i=0;i<SCULL_DEVICES;i++){structscull_dev*dev=&scull_devs[i];device_destroy(scull_class,dev->devno);cdev_del(&dev->cdev);scull_trim(dev);}class_destroy(scull_class);unregister_chrdev_region(devno,SCULL_DEVICES);printk(KERN_INFO"scull module unloaded!");}MODULE_LICENSE("GPL");module_init(scull_init);module_exit(scull_exit);字符设备驱动程序通过以下步骤实现驱动程序的 init
分配设备号 创建设备实例 将设备实例与 file_operations 绑定 将设备实例与设备号绑定,登记内核映射表 创建设备类 根据设备号与设备类创建设备节点