参考资料:
《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;
};
struct list_head{
struct list_head *next, *prev;
};
2)cdev与inode的关系
如图所示,cdev结构体的list成员连接到了inode结构体i_devices成员。其中,i_devices也是一个list_head结构。使cdev结构与inode节点组成一个双向链表。inode结构体表示/dev目录下的设备文件。
图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 **);
};
4、cdev结构体和file_operations结构体的关系:
一般来说,驱动开发人员会将特定设备的特定数据放到cdev结构体后,组成一个新的结构体。如下图所示,“自定义字符设备中就包含特定设备的数据。该“自定义设备”中有一个cdev结构体。cdev结构体中有一个指向file_operations的指针。这样就可以使用file_operations中的函数来操作字符设备或者“自定义字符设备”中的其他数据,从而起到控制设备的作用。
图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; /* 非常重要,用于存放转换后的设备描述结构指针 */
...
};
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()函数来操作设备。