【linux驱动】最基本的驱动框架

🏀最简单的驱动框架

⚽一些重要的数据结构

在linux驱动开发中,大部分基本的驱动程序都涉及操作到三个重要的内核数据结构体,分别是file_operationsfileinode。在学习驱动框架之前需要对上述结构体有一个基本的认识。

🥎file结构体

在linux系统中,file结构体表示打开的文件描述符,它由内核在open文件时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。在文件的所有实例都被关闭之后,内核会释放这个数据结构。

📓 在内核源码中,指向struct file的指针通常被称为file或filp(“文件指针”),为了不至于将指针和这个结构体本身相混淆,我们常将将该指针称为filp。

linux系统中,一个文件可能会被多个线程打开,所以一个文件可能会有多个文件描述符file与之对应。当然,在一个linux线程中也可能会打开多个文件,所以在这个线程的 ** 控制块(PCB)**中,会维护一个列表(本质上就是一个数组),用来保存该进程所有打开的文件。而所谓的文件句柄,即使这个数组的下标,每打开一个文件,就在数组中放入指向这个文件的描述符的指针filp,并返回数组的下标。如下如所示:
在这里插入图片描述
可以看出,在使用函数open打开文件时,传入的参数flagsmode等参数都会被记录在file结构体中。读写文件时,文件当前的偏移也会保存至成员f_pos中。

此外,字符设备节点也是一类特殊的文件,在Linux中打开时也会创建一个对应file结构体。
在这里插入图片描述
如上图所示,当应用层使用open打开字符设备节点时,内核中会创建一个file结构体,并将传入的参数记录至该结构体中,而且会调用结构体成员file_operations *f_op中的open函数。同样,如果应用层使用read/write来读写时,也会调用结构体成员file_operations *f_op中的read/write函数来实现读写的目的。

🥎inode结构体

linux内核使用inode结构体在内部表示文件,它和file结构不同,file结构表示打开的文件描述符。对于单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个的inode结构(即一个文件只有一个inode与之对应,但可能会有多个文件描述符file)。如下图所示,为inode结构体的原型:

//include/linux/fs.h
 /*
  * Keep mostly read-only and often accessed (especially for
  * the RCU path lookup and 'stat' data) fields at the beginning
  * of the 'struct inode'
  */
 struct inode {
         umode_t                 i_mode;
         kuid_t                  i_uid;
         kgid_t                  i_gid;
         unsigned int            i_flags;
         union {
                 const unsigned int i_nlink;
                 unsigned int __i_nlink;
         };
         dev_t                   i_rdev;
         loff_t                  i_size;
         struct timespec         i_atime;
         struct timespec         i_mtime;
         struct timespec         i_ctime;
         union {
                 struct hlist_head       i_dentry;
                 struct rcu_head         i_rcu;
         };
         u64                     i_version;
         atomic_t                i_count;
         atomic_t                i_dio_count;
         atomic_t                i_writecount;
         const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
         struct address_space    i_data;
         struct list_head        i_devices;
         union {
                 struct pipe_inode_info  *i_pipe;
                 struct block_device     *i_bdev;
                 struct cdev             *i_cdev;
                 char                    *i_link;
                 unsigned                i_dir_seq;
         };
         void                    *i_private; /* fs or device private pointer */
 };    

🥎cdev结构体

在linux内核中使用struct cdev结构体来抽象字符设备。在内核调用设备的驱动程序之前,必须分配并注册一个或者多个该结构体,通过其成员dev_t来定义设备号,以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给CFS的接口函数(open/read/write等)。如下图所示,为cdev结构体的原型:

struct cdev {
        struct kobject kobj;   //内嵌的kobject结构,用于内核设备驱动模型的管理,一般不使用。 
        struct module *owner; 	//指向包含该结构的模块的指针,用于有引用计数,通常为THIS_MODULE。
        struct file_operations *fps;	//指向字符设备操作集的指针。
        struct list_head list;    //该结构将使用该驱动程序的字符设备连接成一个链表。
        dev_t dev;		//该字符设备的起始设备号,一个设备可能有多个设备号。
        unsigned int count;        //使用该字符设备驱动的设备数量。
    };

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/f8d33d13167b4aef80b75d70ac97b1c5.png
如上图所示,是cdevinode的关系。每个字符设备在/dev目录下都有一个设备文件,打开设备文件就相当于打开相应的字符设备。例如应用程序打开设备文件A,那么系统会产生一个inode节点。这样可以通过inode结点的i_cdev字段找到cdev字符结构体。通过cdevops指针,就能找到设备A的操作函数。

🥎file_operations结构体

file_operations结构体是字符设备中最重要的结构体之一,它是字符设备的的操作集,原型如下:

/*
 * NOTE:
 * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
 * can be called without the big kernel lock held in all filesystems.
 */
 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 (*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 *);
    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 (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
    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 (*dir_notify)(struct file *filp, unsigned long arg);
    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);
};

🥎上层应用访问底层驱动的过程

在这里插入图片描述

⚽字符设备驱动程序框架

在这里插入图片描述
如上图所示,是字符设备的驱动程序框架。在应用层调用open/read/write/ioctl等系统调用接口时,最终会调用驱动层的drv_open/drv_read/drv_write/drv_ioctl等函数,这些函数的则是由驱动工程师来编写的。为什么要采用这样的框架呢?通常来说,应用开发人员对底层的硬件操作方法不甚了解,直接操作底层硬件,稍有不甚就会损毁硬件,所以由更了解硬件的驱动工程师来实现硬件控制部分的代码,并提供接口给应用层,这样应用开发人员不直接接触硬件,就会避免这样的问题。那么,如何编写驱动程序呢?步骤如下:

1. 确定主设备号,或由内核来分配。

  • 主设备号和次设备号

在使用ls -l命令查看文件的详细信息时,可在文件的的最后修改日期前看到两个数(用逗号分隔),这个位置通常显示的是文件的长度;而对于设备文件,这两个数则是相应设备的主设备号次设备号。通常,主设备号标识设备对应的驱动程序,次设备号则用于确定具体的设备。
在linux内核中,dev_t类型用来保存设备编号。dev_t是一个32位的数,其中的12位用来表示主设备号,而其余20位用来表示次设备号。有几个相关的宏,如下:

MAJOR(dev_t dev);					//dev_t	——> 主设备号
MINOR(dev_t dev);					//dev_t	——> 次设备号
MKDEV(int major, int minor);		//主设备号 + 次设备号 ——> dev_t
  • 分配和释放设备编号

在建立一个字符设备之前,我们的驱动程序首先要做的事情就是获得一个或者多个设备编号。有两种情况,第一种是我们提前明确知道所需要的设备编号,此时我们使用内核提供的函数register_chrdev_region来获得设备编号,该函数原型如下:

/**
* @brief	分配设备编号
* @param	first,表示要分配的设备编号范围的起始值。
* @param	count,表示所请求的连续设备编号的个数。
* @param	name,表示和该设备范围关联的设备名称。
* return	0,分配成功;负值,分配诗失败。
* ********************************************************************************************************************/
int register_chrdev_region(dev_t first,unsigned int count, char *name);

第二种情况是我们不知道设备将要使用哪些设备编号,此时我们使用函数alloc_chrdev_region,内核会为我们分配恰当的设备编号。,该函数的原原型如下:

/**
* @brief	分配设备编号
* @param	dev,出参,在成功完成调用后将保存已分配范围的第一个编号。
* @param	firstminor,表示要使用的被请求的第一个次设备号,它通常是0。
* @param	count,表示所请求的连续设备编号的个数。
* @param	name,表示和该设备范围关联的设备名称。
* return	0,分配成功;负值,分配失败。
* ********************************************************************************************************************/
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

📓 在用户空间程序可访问上述设备编号之前,驱动程序需要将设备编号和内部函数连接起来。

2. 定义file_operations结构体(核心步骤)。

迄今为止,我们已经为自己分配了一些设备编号,但是还未将任何驱动程序操作连接到这些编号,所以,我们接下来需要先实现驱动程序的操作函数,将其统一封装在file_operation结构体中。
在这里插入图片描述
如上图所示,实在hello驱动hello_drv.c文件中定义的file_operations结构体变量,并且进行了初始化,即给函数指针赋值相应的函数。

📓 owner是一个指向模块所有者的指针,是必须设置的。
📓 gcc编译器中增加了使用 .结构体成员=xxx来给成员变量赋值的语法。

3. 实现对应的drv_open/drv_read/drv_write等驱动操作函数。
在这里插入图片描述
如上图所示,为hello驱动程序的几个操作函数。

4. 把file_operations结构体注册到内核。

做完了以上几个步骤,接下来就需要将驱动程序操作和已经分配号的设备编号连接起来。我们可以理解为内核中存在一个chardevs[]数组,数组的下标就是主设备号,当使用某一类字符设备时,根据下标(主设备号)来找到对应设备的file_operations结构体对象。这样,就可以将驱动程序操作和设备编号连接起来,而所谓注册,就是将file_operations结构体填写到chardevs[]数组中去。

在早期的linux内核中,提供了函数register_chrdev用来注册file_operations结构体,该函数的原型如下:

/**
* @brief	注册file_operations结构体。
* @param	major,设备的主设备号,值为0时,内核自主分配设备号。
* @param	name,驱动程序的名称。
* @param	fops, 表示将要注册的file_operations结构体。
* return	申请的主设备号。
* ********************************************************************************************************************/
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

/**
* @brief	注销file_operations结构体。
* @param	major,设备的主设备号。
* @param	name,驱动程序的名称。
* return	0,成功;负值,失败。
* ********************************************************************************************************************/
int unregister_chrdev(unsigned int major, const char *name);

在新的linux内核中,我们首先需要分配一个struct cdev结构体来表示字符设备,如下:

struct cdev *my_cdev = cdev_alloc();

然后,初始化该结构体(将file_operations结构体嵌入进struct cdev结构体),相关函数如下:

/**
* @brief	分配并初始化struct cdev结构体。
* @param	cdev,struct cdev结构体对象。
* @param	fops,需要注册的file_operations结构体。
* return	void。
* ********************************************************************************************************************/
void cdev_init(struct cdev *cdev, struct file_operations *fops);

最后,告诉内核struct cdev结构体的信息。相关函数如下:

/**
* @brief	告诉内核struct cdev结构体的信息。
* @param	cdev,struct cdev结构体对象。
* @param	num,该设备对应的第一个设备编号。
* @param	count,和设备关联的设备编号的数量,通常取1。
* return	0,成功;负值,失败。
* ********************************************************************************************************************/
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);

5. 定义入口函数,安装驱动程序时就会调用这个入口函数。

我们上面步骤4中注册file_operations结构体到内核是在入口函数中完成的,入口函数使用宏__init修饰。如下所示,是hello驱动的入口函数:
在这里插入图片描述
如上图所示,我们在使用insmod安装驱动程序的时候,内核会自动调用hello_init函数,在该函数中完成:

  • 申请主设备号
  • 注册file_operations结构体到内核中。
  • 创建设备类,使用函数class_create实现。
  • 创建设备节点,使用函数device_create实现。

6. 定义出口函数,卸载驱动程序时就会调用这个出口函数。

有入口函数就有出口函数,使用宏__exit来修饰。在使用remmod卸载驱动时,内核会自动调用hello_exit函数。
在这里插入图片描述
如上图所示,在出口函数中的动作与入口函数中的动作相反,主要完成以下操作:

  • 销毁设备节点,使用函数device_destroy实现
  • 销毁设备类,使用函数classs_destroy实现
  • 从内核中注销file_operations结构体

7. 完善设备信息
在这里插入图片描述
其中,module_init告诉内核hello_init是入口函数;module_exit告诉内核hello_exit是出口函数;使用MOUDULE_LICENSE表示遵循GPL协议。

⚽参考文档

【Linux驱动】最基本的驱动框架|LED驱动
cdev/file_operations/inode/file之家的联系
《Linux设备驱动程序 第三版》(魏永明 耿岳 钟书毅 译)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值