详解字符设备驱动框架及操作函数的实现原理

字符设备驱动框架解析

设备的操作函数如果比喻是桩的话(性质类似于设备操作函数的函数,在一些场合被称为桩函数),则:

驱动实现设备操作函数 ----------- 做桩

insmod调用的init函数主要作用 --------- 钉桩

rmmod调用的exitt函数主要作用 --------- 拔桩

应用层通过系统调用函数间接调用这些设备操作函数 ------- 用桩

结构体说明

struct file_operations 结构体

struct file_operations 是 Linux 内核中定义的一个结构体,用于描述文件和设备的操作接口。每个文件或设备都关联一个 struct file_operations 对象,其中包含了对文件或设备进行操作的函数指针。这个设计使得内核可以通过统一的接口调用不同设备驱动程序的具体实现函数。

/**
 * struct file_operations - 文件操作函数集,用于描述文件和设备的操作接口
 * @owner:           指向拥有该结构的模块的指针,用于模块引用计数
 * @llseek:          用于设置文件读写位置的函数指针
 * @read:            从文件中读取数据的函数指针
 * @write:           向文件中写入数据的函数指针
 * @read_iter:       通过迭代方式从文件中读取数据的函数指针
 * @write_iter:      通过迭代方式向文件中写入数据的函数指针
 * @iterate:         用于读取目录项的函数指针
 * @poll:            用于轮询文件状态的函数指针
 * @unlocked_ioctl:  用于处理设备控制命令的函数指针
 * @compat_ioctl:    用于处理32位兼容设备控制命令的函数指针
 * @mmap:            用于内存映射的函数指针
 * @open:            用于打开文件或设备的函数指针
 * @flush:           用于刷新文件或设备的函数指针
 * @release:         用于关闭文件或设备的函数指针
 * @fsync:           用于同步文件或设备的函数指针
 * @fasync:          用于异步通知的函数指针
 * @lock:            用于文件锁定的函数指针
 * @sendpage:        用于发送页面的函数指针
 * @get_unmapped_area: 用于获取未映射区域的函数指针
 * @check_flags:     用于检查标志的函数指针
 * @flock:           用于文件加锁的函数指针
 * @splice_write:    用于写入管道的函数指针
 * @splice_read:     用于从管道读取的函数指针
 * @setlease:        用于设置文件租约的函数指针
 */
struct file_operations {
    struct module *owner;  // 模块所有者
    loff_t (*llseek) (struct file *, loff_t, int);  // 设置文件读写位置
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  // 读取文件数据
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);  // 写入文件数据
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);  // 迭代读取数据
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);  // 迭代写入数据
    int (*iterate) (struct file *, struct dir_context *);  // 读取目录项
    unsigned int (*poll) (struct file *, struct poll_table_struct *);  // 轮询文件状态
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);  // 设备控制命令
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);  // 兼容设备控制命令
    int (*mmap) (struct file *, struct vm_area_struct *);  // 内存映射
    int (*open) (struct inode *, struct file *);  // 打开文件或设备
    int (*flush) (struct file *, fl_owner_t id);  // 刷新文件或设备
    int (*release) (struct inode *, struct file *);  // 关闭文件或设备
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);  // 同步文件或设备
    int (*fasync) (int, struct file *, int);  // 异步通知
    int (*lock) (struct file *, int, struct file_lock *);  // 文件锁定
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);  // 发送页面
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);  // 获取未映射区域
    int (*check_flags)(int);  // 检查标志
    int (*flock) (struct file *, int, struct file_lock *);  // 文件加锁
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);  // 写入管道
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);  // 从管道读取
    int (*setlease)(struct file *, long, struct file_lock **, void **);  // 设置文件租约
};

inode结构体
文件元信息(File Metadata)是描述文件属性和结构的信息,而不是文件内容本身。包括文件名、类型、大小、创建和修改时间I(时间戳)、权限等,帮助操作系统和应用程序管理和使用文件。
​
内核中记录文件元信息的结构体inode,文件在文件系统(外存)中都有一个对应的inode,它包含了文件的重要属性和位置信息。inode本身并不包含文件名信息,文件名与inode之间的映射由文件系统中的目录数据结构(例如目录项)管理。
struct inode
{
    //....
    dev_t  i_rdev;//设备号。通过设备号在内核的哈希链表数据结构里面查找到对应的struct cdev对象,将该对象的地址赋值给下面的struct cdev  *i_cdev
    struct cdev  *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象
    //....
}
/*
    1. 内核中每个该结构体对象对应着一个实际文件,一对一
    2. open一个文件时如果内核中该文件对应的inode对象已存在则不再创建,不存在才创建
    3. 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)
*/
file结构体
在内核中是通过很多个数组来管理的,每一个进程对应着一个文件描述符数组,这些数组里面都填了一个指向struct file结构体对象的地址,通过file对象操作文件。
    
file 结构体包含了文件的状态信息以及文件操作的相关方法,是读写文件内容过程中用到的一些控制性数据组合而成的对象(在调用open时创建)------文件操作引擎(文件操控器)
    
open函数返回的文件描述符fd就是文件描述符数组的下标。(描述符的本质就是下标)
    
struct file
{
    //...
    mode_t f_mode;//不同用户的操作权限,驱动一般不用
    loff_t f_pos;//position 文件操作的位置指示器,需要控制数据开始读写位置的设备有用
    unsigned int f_flags;//open时的第二个参数flags(打开文件的权限)存放在此,驱动中常用
    struct file_operations *f_op;//open时从struct inode中i_cdev的对应成员获得地址,指向一个包含文件操作函数指针的结构体struct file_operation,这个结构体定义了对应于打开文件的操作函数。驱动开发中用来协助理解工作原理,内核中使用
    void *private_data;//本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据
    struct dentry *f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode
    int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象。例如在创建子进程时,父子进程共用一个文件操作引擎操作同一个文件。如果某个进程关闭文件操作引擎,并不会真的关闭引擎,只有引用计数为0时才会销毁file对象。
    //...
};
/*
    1. open函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作
    2. open同一个文件多次,每次open都会创建一个该类型的对象
    3. 文件描述符数组中存放的地址指向该类型的对象
    4. 每个文件描述符都对应一个struct file对象的地址
*/

字符设备驱动程序框架分析

open函数的实现原理

在Linux内核中,open() 函数是用来打开文件或者创建新文件的系统调用。它的实现原理涉及以下几个关键步骤:

  1. 参数验证和权限检查

    内核首先会验证传递给 open() 函数的参数,包括文件名、打开模式(如只读、读写)、权限标志等。同时,内核还会检查当前进程是否有足够的权限执行所请求的操作。
  2. 查找文件

    根据传入的文件名,内核会查找文件系统中对应的文件。这个过程通常包括在文件系统的目录层次结构中查找文件名,并获取文件的inode号码。
  3. 分配文件描述符

    如果文件查找成功,内核会为这个打开的文件分配一个文件描述符(file descriptor),这个描述符在进程中唯一标识了这个文件。文件描述符是一个非负整数,通常是当前进程中尚未使用的最小整数值。
  4. 分配并初始化文件结构体

    内核会为这个打开的文件分配一个 file 结构体,用于管理文件状态和文件操作。这个 file 结构体包含了文件的状态信息(如文件位置、访问模式、状态标志等)以及指向文件操作函数指针的结构体(struct file_operations *f_op)。
  5. 设置文件操作函数指针

    根据文件类型和打开模式,内核会将适当的文件操作函数指针集合(file_operations 结构体)赋值给 file 结构体的 f_op 指针。这些函数指针定义了如何执行对文件的读取、写入、定位、控制和关闭等操作。
  6. 返回文件描述符

    最后,open() 系统调用会返回分配的文件描述符给用户空间程序。用户程序可以使用这个文件描述符来进行后续的文件操作,如读取、写入等。
驱动实现端(驱动程序)
  1. 设备注册

    • 驱动程序通常会在初始化阶段向系统注册设备。

    • 内核中,每个字符设备都会注册一个 struct cdev 对象。

    • 这个对象包含设备号(dev_t devno)、指向文件操作函数集的指针(struct file_operations *fops)等。

    • struct cdev 对象通过哈希链表或其他方式链接,管理多个字符设备。

  2. 实现操作函数

    • 驱动程序需要实现 struct file_operations 结构体中的操作函数,包括 openreadwriteclose 等,并将这些函数的地址存入 struct file_operations 结构体对象,构成一个操作函数集。这些函数通常由设备驱动程序的作者编写,并在注册设备时指定。

    • 每个字符设备 struct cdev 都关联一个 struct file_operations 对象(struct file_operations *fops),该对象包含了设备的具体操作实现。当应用程序对设备进行操作时,内核会调用相应的操作函数。

驱动使用端(应用程序或其他内核组件)
  1. 系统调用接口

    • 用户空间的应用程序通过系统调用(如 openreadwriteclose)来操作设备文件。

    • 内核提供了对应的系统调用处理函数(如 syscall_opensyscall_readsyscall_writesyscall_close),应用程序调用open等函数实际上就是调用这些函数。

    • 当应用程序调用 open 系统调用时,内核会通过open函数传入的参数(文件名/路径)查找struct inode链表找到对应的inode对象(如果没有对象则创建对象)。通过inode对象找到设备文件的元信息,包括设备号和struct cdev *i_cdev存放的struct cdev对象地址。

    • 内核根据设备文件的inode提供的设备号devno查找管理设备的哈希链表,找到所关联的 struct cdev 对象,并通过 struct cdev 对象获取 struct file_operations 对象的地址*fops,并赋值给 struct file 对象的struct file_operations *f_op

  2. 内核空间和用户空间交互

    • 每次调用 open系统调用都会创建一个 struct file 对象,该对象包含了文件描述符、设备状态和指向 struct file_operations (操作函数集)的指针(f_op)。

    • 通过 f_op,内核能够找到相应的设备操作函数集,并调用相应的操作函数。例如,调用 f_op->open 时,实际会执行驱动程序中定义的 drv_open 函数。

    • 创建完成file对象后,会将该对象的地址存入进程对应的文件描述符数组,所对应的数组的下标就是open函数返回的文件描述符。

  3. 调用驱动程序对应的操作函数

    1. 系统调用接口

      • 用户进程调用系统调用,如 openreadwriteclose 等。

      • 系统调用入口函数会根据文件描述符找到对应的 file 结构体。

    2. 找到 file_operations 结构体

      • file 结构体中的 f_op 指针指向对应的 file_operations 结构体。

      • file_operations 结构体中包含了所有操作函数的指针。

    3. 调用具体的驱动程序函数

      • 内核通过调用 file_operations 结构体中的函数指针,执行具体的文件操作。

      • 这些函数指针指向驱动程序中实现的具体操作函数,如 drv_opendrv_readdrv_writedrv_release 等。

    4. 示例

      • read系统调用。根据read函数传入的文件描述符参数,在文件描述符数组中找到对应的file对象的地址,从而访问file结构体对象,内核通过 file 结构体中的 f_op 指针找到对应的 file_operations 结构体,从而调用 file_operations 结构体中的 read 函数指针。这些函数指针指向驱动程序中实现的具体操作函数,如 drv_opendrv_readdrv_writedrv_release 等。

  • 30
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值