Linux 字符设备驱动(一)---cdev、file_operations、inode、file结构体详述,上层应用如何访问到底层驱动

参考资料:

      《Linux驱动开发入门与实战》,概念及源码主要参考《Linux驱动开发入门与实战》,务求准确。同时衷心感谢其他网友的分享。大部分内容都是手敲的,错漏之处望指正,谢谢!

 linux设备驱动之字符设备驱动
 https://www.linuxprobe.com/linux-device-driver.html

 Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析
 https://blog.csdn.net/zqixiao_09/article/details/50839042


前言


    不积硅步无以至千里,不积小流无以成江河。
    

1、字符设备与块设备的概念
    字符设备:是指只能一个字节一个字节读写数据的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

    块设备:是指可以从设备的任意位置读取一定长度数据的设备。其读取数据不必按照先后顺序,可以定位到设备的某一具体位置。块设备包括硬盘、磁盘、U盘和SD卡等。

    每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

2、cdev结构体
    在Linux内核中使用cdev结构体描述字符设备。该结构体是所有字符设备的抽象,其包含了大量字符设备所共有的特性。通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。

    1)cdevj结构体:
    struct cdev {
        struct kobject kobj;    
        struct module *owner; 
        struct file_operations *ops;
        struct list_head list;    
        dev_t dev;
        unsigned int count;                
    };

  •     kobj:内嵌的kobject结构,用于内核设备驱动模型的管理;一般不使用。
  •     owner:指向包含该结构的模块的指针,用于引用计数。通常为THIS_MODULE;owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct module是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE。
  •     ops:指向字符设备操作集的指针。该操作集中定义了字符设备驱动提供给VFS的接口函数,如open()、read()、write()等。在cdev_init()这个函数里面与cdev结构联系起来。
  •     dev:该字符设备的起始设备号,一个设备可能有多个设备号。
  •     count:使用该字符设备驱动的设备数量;当使用rmmod卸载模块时,如果count成员不为0,那么系统不允许卸载模块。dev成员和count成员则在cdev_add中才会赋上有效的值;
  •     list:该结构将使用该驱动的字符设备连接成一个链表。list结构是一个双向链表,用于将其他结构体连接成一个双向链表。该结构在Linux内核中广泛应用,需要掌握。

    struct list_head{
        struct list_head *next, *prev;
    };

    2)cdev与inode的关系
    如图所示,cdev结构体的list成员连接到了inode结构体i_devices成员。其中,i_devices也是一个list_head结构。使cdev结构与inode节点组成一个双向链表。inode结构体表示/dev目录下的设备文件。

                                                    Linux 字符设备驱动(一)---cdev、file_operations、inode、file结构体详述,上层应用如何访问到底层驱动_第1张图片

                                                                       图1  cdev与inode的关系
    每个字符设备在/dev目录下都有一个设备文件,打开设备文件就相当于打开相应的字符设备。例如应用程序打开设备文件A,那么系统会产生一个inode节点。这样可以通过inode结点的i_cdev字段找到cdev字符结构体。通过cdev的ops指针,就能找到设备A的操作函数。
    
    例子:构造cdev设备结构体
    struct xxx_dev{
        struct cdev cdev;
        char *data;
        struct semaphore sem;
        ...
    };
    
    
3、file_operations结构体

    这个结构体是字符设备当中最重要的结构体之一,file_operations 结构体中的成员函数指针是字符设备驱动程序设计的主体内容,这些函数实际上在应用程序进行Linux 的 open()、read()、write()、close()、seek()、ioctl()等系统调用时最终被调用。

struct file_operations {
    /*拥有该结构的模块计数,一般为THIS_MODULE*/
    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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
   
    /*轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
    unsigned int (*poll) (struct file *, struct poll_table_struct *);

    /*执行设备的I/O命令*/
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

    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 *, struct dentry *, int datasync);
    int (*aio_fsync) (struct kiocb *, 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 **);
};

  • owener成员是一个指向拥有这个结构的模块的指针。这个成员用来维持模块的引用计数,当模块还在使用时,不能用rmmod卸载模块。一般初始化为THIS_MODULE。
  • llseek()函数用来改变文件中的当前读/写位置,并将新位置返回。loff_t参数是"long long"类型。"long long"类型"long long"类型即使在32位机上也是64位宽,这是为了与64位机兼容而定的。
  • read()函数用来从设备中获取数据,成功时函数返回读取的字节数,失败时返回一个负的错误码。
  • write()函数用来写数据到设备中。成功时该函数返回写入的字节数,失败时返回一个负的错误码。
  • ioctl()函数提供了一种执行设备特定命令的方法。例如使设备复位,这既不是读操作也不是写操作,不适合用read()和write()方法来实现。如果在应用程序中给ioctl传入没有定义的命令,那么将返回-ENOTTY的错误,表示该设备不支持这个命令。
  • open()用来打开一个设备,在该函数中可以对设备进行初始化。如果这个函数被复制NULL,那么设备打开永远成功,并不会对设备产生影响。
  • release()函数用来释放open()函数中申请的资源,将在文件引用计数为0时,被系统调用。其对应引用程序的close()方法,但并不是每一次调用close()方法,都会出发release()函数,在对设备文件的所有打开都释放后,才会被调用。


4、cdev结构体和file_operations结构体的关系:
    一般来说,驱动开发人员会将特定设备的特定数据放到cdev结构体后,组成一个新的结构体。如下图所示,“自定义字符设备中就包含特定设备的数据。该“自定义设备”中有一个cdev结构体。cdev结构体中有一个指向file_operations的指针。这样就可以使用file_operations中的函数来操作字符设备或者“自定义字符设备”中的其他数据,从而起到控制设备的作用。                      

                        Linux 字符设备驱动(一)---cdev、file_operations、inode、file结构体详述,上层应用如何访问到底层驱动_第2张图片    

                                                                                 图2


5、inode结构体
    内核使用inode结构体在内部表示文件,它是实实在在的表示物理硬件上的某一个文件,且一个文件仅有一个inode与之对应。inode一般作为file_operations结构中函数的参数传递过来。例如,open()函数将传递一个inode指针进来,表示目前打开的文件节点。需要注意的是,inode的成员已经被系统赋予了合适的值,驱动程序只需要使用该结点中的信息,而不用更改。
    open()函数为:int (*open) (struct inode *, struct file *);
    inode结构中包含大量的有关文件的信息。这里只对编写驱动程序有用的字段进行介绍。
struct inode{
    dev_t i_rdev; /*设备编号*/
    struct cdev *i_cdev; /*cdev 是表示字符设备的内核的内部结构*/


    struct list_head i_devices;
};
    i_rdev:表示设备文件对应的设备号。
    i_devices:如图1所示,该成员指向其他inode结点的i_devices成员。构成struct list_head双向链表。
    i_cdev:该成员指向cdev结构体。
    
    
6、file结构体
    file 结构代表一个打开的文件,它的特点是一个文件可以对应多个file结构。它由内核再open时创建,并传递给在该文件上操作的所有函数,直到最后close函数,在文件的所有实例都被关闭之后,内核才释放这个数据结构。
    在内核源代码中,指向 struct file 的指针通常被称为filp,file结构有以下几个重要的成员:

struct file{
    mode_t fmode; /* 文件模式,如FMODE_READ,FMODE_WRITE */
    ...
    loff_t f_pos; /* loff_t f_pos 表示文件读写位置。loff_t 是一个64位的数,需要时,须强制转换为32位。*/
    unsigned int f_flags; /*文件标志,如:O_NONBLOCK */
    struct file_operations *f_op;
    void *private_data; /* 非常重要,用于存放转换后的设备描述结构指针 */
    ...
};

  •   mode_t f_mode:此文件模式通过FMODE_READ, FMODE_WRITE识别了文件为可读的,可写的,或者是二者。在open或ioctl函数中可能需要检查此域以确认文件的读/写权限,你不必直接去检测读或写权限,因为在进行octl等操作时内核本身就需要对其权限进行检测。
  •   loff_t f_pos:当前读写文件的位置。为64位。如果想知道当前文件当前位置在哪,驱动可以读取这个值而不会改变其位置。对read,write来说,当其接收到一个loff_t型指针作为其最后一个参数时,他们的读写操作便作更新文件的位置,而不需要直接执行filp ->f_pos操作。而llseek方法的目的就是用于改变文件的位置。
  •   unsigned int f_flags:文件标志, 例如 O_RDONLY, O_NONBLOCK和O_SYNC。驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作; 其他标志很少使用。 特别地, 当检查读/写许可, 使用 f_mode而不是f_flags。所有的标志在头文件 中定义。
  •     struct file_operations *f_op:当文件需要迅速进行各种操作时,内核分配这个指针作为它实现文件打开,读,写等功能的一部分。filp->f_op 其值从未被内核保存作为下次的引用,即你可以改变与文件相关的各种操作,这种方式效率非常高。
  •   void *private_data:在驱动调用open方法之前,open系统调用设置此指针为NULL值。你可以很自由的将其做为你自己需要的一些数据域或者不管它,如,你可以将其指向一个分配好的数据,但是你必须记得在file struct被内核销毁之前在release方法中释放这些数据的内存空间。private_data用于在系统调用期间保存各种状态信息是非常有用的
  •     struct dentry *f_dentry:关联到文件的目录入口(dentry)结构。设备驱动编写者一般不需要关心dentry结构, 除了作为 filp->f_dentry->d_inode存取inode结构。

 

7、上层应用如何访问到底层驱动?
    1)每一个字符设备xxx都对应一个设备文件/dev/xxx。当我们使用open(“/dev/xxx”,O_RDWR)函数打开一个设备文件的时候。Linux系统在VFS层都会分配一个struct file结构体描述打开的这个设备文件。
    2)Linux系统中,每个文件都有一个struct inode结构体来描述,这个结构体里面记录了文件的所有信息。open打开设备文件/dev/xxx时,Linux文件系统会根据文件名找到该文件对应的struct inode结构体。该结构体中记录了xxx设备的设备号dev_t i_rdev、设备类型mode_t imode、描述字符设备的结构体struct cdev i_cdev。如果imode是字符设备类型,则去字符设备cdev map中根据设备号i_rdev找到对应的描述该字符设备的结构体i_cdev。把i_cdev结构体的地址首地址返回给inode结构体中的i_cdev成员。
    3)在该字符设备的结构体i_cdev中记录了字符设备操作函数集合,即struct file_operations xxx_ops结构体。该结构体中定义了字符设备对应的所有操作函数。拷贝const struct file_operations xxx_ops结构体的地址,将其赋值给VFS层struct file结构体的const struct file_operations *f_op结构体指针。
    4)VFS层给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就可以通过fd来找到struct file,然后由struct file找到操作字符设备的函数接口。
    5)应用层可以使用fd来调用read() 、write()、ioctl()函数来操作设备。

Linux 字符设备驱动(一)---cdev、file_operations、inode、file结构体详述,上层应用如何访问到底层驱动_第3张图片

 

你可能感兴趣的:(linux)