linux字符设备驱动的基本运行流程和一些基础

Linux字符设备驱动的基本运行流程
笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正!!!
Linux驱动大致可以分为字符设备驱动、块设备驱动和网络设备驱动。
故名思义,字符设备就是以字符或者字节来读写数据的设备,通常没有缓冲器,如串口等。块设备就是通常支持随机存取和寻址,并使用缓存器的设备,如硬盘等。网络设备就是用于网络数据的收发。
字符设备驱动最重要的部分有include/linux/cdev.h的struct cdev和include/linux/fs.h文件中的struct file_operation, struct inode,struct file。次重要的部分有include/linux/dcache.h文件中的struct dentry。

先知道dev_t类型的变量是一个设备号,包括了主设备号和次设备号,一般主设备号是12位,次设备号是20位。但实际应用中主设备号其实是0到255。

首先Struct cdev是字符设备对象。
在这里插入图片描述

最重要的是ops,这是file_operations类型的。这个类型的结构体会存储很多后续自己填写实现的功能。Owner一般写THIS_MODULE表示这个字符设备属于当前模块(这里的模块是你在哪个.c文件(驱动模块)里写THIS_MODULE,它就代表这个.c文件编译生成的内核模块)。List会把当前模块所有的cdev连在一起成为链表(和probe不同,probe是间接连接,list是直接连接)。Dev是设备号(主+次)。Count是当前cdev所覆盖的次设备数量(因为多个次设备可能共用一个cdev,因为他们的功能可能差不多)。

这是file_operation结构体,owner基本上依旧写THIS_MODULE,然后里面就是一些我们写驱动需要实现功能的函数,给这个file_operation的函数指针赋值就ok了。
在这里插入图片描述

1、llseek: 用于修改文件的当前读写位置,并返回偏移后的位置。参数file传入了对应的文件指针,我们可以看到以上代码中所有的函数都有该形参,通常用于读取文件的信息,如文件类型、读写权限;参数loff_t指定偏移量的大小;参数int是用于指定新位置指定成从文件的某个位置进行偏移,SEEK_SET表示从文件起始处开始偏移;SEEK_CUR表示从当前位置开始偏移;SEEK_END表示从文件结尾开始偏移。
2、read: 用于读取设备中的数据,并返回成功读取的字节数。该函数指针被设置为NULL时,会导致系统调用read函数报错,提示“非法参数”。该函数有三个参数:file类型指针变量,char__user*类型的数据缓冲区,__user用于修饰变量,表明该变量所在的地址空间是用户空间的。内核模块不能直接使用该数据,需要使用copy_to_user函数来进行操作。size_t类型变量指定读取的数据大小。
3、write: 用于向设备写入数据,并返回成功写入的字节数,write函数的参数用法与read函数类似,不过在访问__user修饰的数据缓冲区,需要使用copy_from_user函数。
4、unlocked_ioctl: 提供设备执行相关控制命令的实现方法,它对应于应用程序的fcntl函数以及ioctl函数。在 kernel 3.0 中已经完全删除了 struct file_operations 中的 ioctl 函数指针。
5、open: 设备驱动第一个被执行的函数,一般用于硬件的初始化。如果该成员被设置为NULL,则表示这个设备的打开操作永远成功。
6、release: 当file结构体被释放时,将会调用该函数。与open函数相反,该函数可以用于释放。
上面,我们提到read和write函数时,需要使用copy_to_user函数以及copy_from_user函数来进行数据访问,写入/读取成 功函数返回0,失败则会返回未被拷贝的字节数。

讲完file_operation结构体讲一下file结构体。
在这里插入图片描述

1、 struct inode *f_inode指向该文件对应的索引节点struct inode,inode 是文件的 “持久化元数据载体”(存储文件的权限、大小、物理存储位置、设备号等静态信息,后面讲)。inode 是文件的唯一标识,同一个文件只有一个 inode,而 struct file 是文件的打开实例,多次打开同一个文件会有多个 struct file 指向同一个 inode。
2、 const struct file_operations *f_op指向操作函数集(我们自己在file_operation结构体实现的函数)。为什么之前已经写了file_operation这个file结构体又有,最本质就是为了提高代码复用性,方便用户使用,驱动开发者把file结构体中的f_op指向我们自己写的cdev中的ops就行。如果出现功能改动我们也只需要把file_operation结构体实现的函数改动或者是把file结构体中的f_op指向我们自己写的其他cdev中的ops就行。说了这么多就是好用就完了,可以减少过多的改动。
3、 atomic_long_t f_count是引用计数。当 dup()、fork() 等操作共享该 struct file 时,引用计数加 1;当 close() 时,引用计数减 1,只有计数为 0 时,内核才会销毁该 struct file 对象。可以避免在还有进程使用时释放 struct file,防止野指针问题。

然后到struct inode 结构体。
在这里插入图片描述

1、 dev_t i_rdev存的是设备号。
2、 struct cdev *i_cdev:指向的是字符设备对象cdev结构体。但是这个存cdev的前提是通过i_rdev找到了对应的cdev,然后让i_cdev存起来。

接下来是struct dentry。
在这里插入图片描述

1、 struct inode *d_inode指向该路径分量对应的struct inode,如果非 NULL,表示 dentry 是 “有效” 的(已关联 inode,如普通文件、目录);如果是 NULL,表示 dentry 是 “负的”(negative dentry,如路径不存在、刚被删除,用于缓存路径解析失败的结果,避免重复查询磁盘)。
2、 struct dentry *d_parent是当前 dentry 指向其父目录 dentry 的指针,它的核心使命是明确文件 / 目录在路径中的层级归属,并将所有 dentry 串联成与实际文件系统一致的内存目录树。
3、 struct qstr d_name是struct dentry中用于存储当前 dentry 对应的路径分量名称。

接下来是cdev_map。内核通过一个散列表(哈希表)cdev_map来记录设备编号。 哈希表由数组和链表组成,吸收数组查找快,链表增删效率高,容易拓展等优点。
在这里插入图片描述

可以知道probes本质上是probe结构体类型的一个指针数组,然后probe其实是个链表。这意味着同一个主设备号下,可以挂接多个probe节点。里面重要的部分有range和data。Range写的是次设备号的范围,data写的是指向该设备号范围对应的 struct cdev对象。
以主设备号(major)为cdev_map编号,使用哈希函数f(major)=major%255来计算组数下标(使用哈希函数是为了链表节点尽量平均分布在各个数组元素中,提高查询效率); 主设备号冲突,则以次设备号为比较值来排序链表节点(一个主设备号多个次设备的情况)。 如下图所示,内核用struct cdev结构体来描述一个字符设备,并通过struct kobj_map类型的 散列表cdev_map来管理当前系统中的所有字符设备。
在这里插入图片描述

第一个问题:一个主设备号就对应一个probes,probes里面又会有对应范围的次设备号所对应的cdev,那存在一个主设备号但是存在多个次设备号范围的情况下怎么办,怎么图中每个probes中只连接了一个cdev?
答:之前说到probes的类型是probe 链表,所以可以通过链表间接连接不同次设备号的cdev。

总体的基本运行流程:
一、前置基础:核心对象的定位(先明确角色,流程中会反复调用)

  1. 用户态程序:发起open/read系统调用的主体,运行在低权限地址空间,无法直接访问内核数据。

  2. 文件描述符fd:用户态唯一标识打开的设备/文件的整数,是进程文件描述符表的下标(内核会为每个进程维护一张 “文件描述符表”,这张表就是一个数组,数组的下标就是 fd比如下标 0 对应标准输入,下标 1 对应标准输出,数组的每个元素,都指向内核中真正管理文件的 struct file 对象)。

  3. task_struct:进程控制块(PCB),内核中描述进程的核心结构体,包含进程的所有资源信息(如文件描述符表、内存空间、PID等)。

  4. files_struct:进程的文件描述符表,隶属于task_struct,核心是数组fd[](下标为fd,元素为struct file *)。
    在这里插入图片描述

  5. struct file:内核中描述“打开的文件/设备”的结构体,保存打开状态(如读写模式、当前偏移、关联的inode和操作函数集)。

  6. struct dentry:VFS目录项,内核内存中的路径名缓存,记录文件名、父目录dentry及指向inode的指针。

  7. struct inode:文件/设备的物理属性载体,记录设备号、类型、所属文件系统操作函数等核心物理信息。

  8. struct cdev:字符设备驱动核心对象,绑定驱动的file_operations操作函数集。

  9. file_operations:驱动实现的设备操作函数集(如open/read/write),是内核与驱动交互的接口。

  10. VFS(虚拟文件系统):内核抽象层,统一管理不同文件系统的访问逻辑,承接路径解析、inode查找等核心操作。虚拟文件系统的 “虚拟”,核心是它构建了一层抽象的、统一的软件层 **,屏蔽了底层不同物理文件系统(如 ext4、NTFS、tmpfs、sysfs)和设备文件(如 /dev 下的字符 / 块设备)的实现差异,让用户态和内核上层代码无需关心底层具体类型,就能用统一的接口操作所有 “文件”—— 这种 “对下封装差异、对上提供统一抽象” 的设计,就是 “虚拟” 的本质。
    11、系统调用表是Linux 内核中核心的全局数据结构,本质是一个以「系统调用号」为下标的函数指针数组,它建立了系统调用号(整数)与对应内核服务函数(如sys_open、sys_read)之间的一一映射关系,是用户态程序请求内核服务的 “导航目录”

二、流程
阶段1:用户态调用open("/dev/ my_dev0", O_RDWR)触发系统调用

  1. 用户态程序执行int fd = open("/dev/my_dev0", O_RDWR);,C库封装的open函数触发软中断,陷入内核态。
  2. 内核根据系统调用表,跳转到sys_open执行,这是系统调用的入口点。

阶段2:内核sys_open调用do_sys_open,分配fd与空file结构体

  1. 分配未使用的fd:内核调用get_unused_fd_flags(),遍历当前进程files_structfd[]数组,找到第一个未使用的下标(即fd,如3),然后标记为已使用。
  2. 创建空struct file:内核调用get_empty_filp(),从内核file缓存池分配一个新的struct file结构体,初始化默认值。

阶段3:do_filp_open执行VFS路径解析,找到dentryinode

  1. 路径层级解析:VFS(虚拟文件系统)从根目录/开始,逐层解析路径/dev/my_dev0
    先获取根目录的dentry,然后通过根目录的dentry找到对应根目录的inode
    调用根目录inodei_op->lookup函数,查找子目录devdentry
    再调用dev目录inodei_op->lookup函数,查找文件my_dev0dentry
  2. 获取inode:从my_dev0dentry->d_inode获取对应的struct inode结构体:
    检查inode->i_mode,确认是字符设备(S_IFCHR);
    提取inode->i_rdev中的设备号(主设备号+次设备号,如200:0)。

阶段4:do_dentry_open关联cdev,初始化file并调用驱动open

  1. 查找cdev对象:内核遍历散列表cdev_map(设备号到cdev的映射表),根据inode->i_rdev的设备号在散列表中找到对应的struct cdev对象。
  2. 创建并初始化struct file
    file->f_inode = inode:绑定设备的inode;(inode是文件 / 设备的持久化元数据载体。将file绑定到inode,意味着这个 “打开实例” 关联到了具体的文件 / 设备实体)
    file->f_dentry = dentry:绑定路径的dentry
    file->f_op = cdev->ops:将file的操作函数集指向驱动的file_operations
    file->f_mode = O_RDWR:设置打开模式为读写(这个是一开始调用open函数填写的一个参数)。
  3. 关联file到进程文件描述符表:
    内核通过current宏获取当前进程的task_struct
    file指针赋值给task_struct->files->fd[fd]
  4. 调用驱动open回调:内核执行file->f_op->open(inode, file),即驱动实现的open函数:
    驱动在open中完成硬件初始化、内存申请、中断设置等操作;
    open返回0则成功,返回负数则终止流程并向用户态返回错误。

阶段5:内核返回fd给用户态程序

  1. 若一切正常,则内核从sys_open系统调用返回,然后返回了之前分配得到fd(如3),后续可通过该fd执行read/write等等操作。

阶段6:用户态调用read(fd, buf, size)的核心流程(假如需要)

  1. 用户态执行read(fd, buf, sizeof(buf)),触发sys_read系统调用,陷入内核态。
  2. 根据fd查找file
    内核获取当前进程的task_struct,从task_struct->files->fd[fd]取出file指针(数组直接查找,O(1)效率)。
  3. 调用驱动read回调:内核执行file->f_op->read(file, buf, size, &file->f_pos)
    驱动在read中从硬件读取数据,通过copy_to_user将数据拷贝到用户态的buf
    驱动返回读取的字节数,内核更新file->f_pos(字符设备可忽略,普通文件/块设备需记录偏移)。
  4. 返回结果给用户态:内核从sys_read返回,用户态的read函数得到读取的字节数,完成数据读取。

阶段7:后续清理流程(close/进程退出/驱动卸载)(假如需要)

  1. 用户态调用close(fd):触发sys_close系统调用,内核找到file,调用file->f_op->release(驱动释放函数),将files_struct->fd[fd]置为NULL,减少file引用计数,计数为0时释放file结构体。
  2. 进程退出:内核遍历进程的files_struct,关闭所有未关闭的file,释放files_structtask_struct
  3. 驱动卸载:内核从cdev_map中移除cdev,释放驱动占用的资源(例如设备号),如果cdev的引用计数>0(有进程正在使用设备),则卸载失败。

感谢大家阅读到最后,我整理的可能不是很好,希望对大家有帮助。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值