一、用标题户级、内核级和系统调用
Linux /Unix系统下的进程运行分为用户态和进程态两种状态。我们的应用程序通常仅在用户态下运行,出于保护内核资源的需要,用户态下运行的程序在只能访问有限的资源,例如不能访问内核的数据结构和程序。
内核的一个重要功能就是协调和管理硬件资源,包括CPU、内存、I/O设备等,从而为上层运行的诸多应用程序提供更好的运行环境。因此,驱动程序通常都是在内核态下运行。我们的进程大多数时间都是运行在用户态下的,一旦它需要操作系统帮助它完成一些自己没有权限和能力完成的工作,就会切换到内核态。而系统调用就是进程主动申请从用户态切换到内核态的方式。
二、虚拟文件系统(VFS)
1. 什么是虚拟文件系统
我们在Unix/Linux系统下用户程序操作普通文件,即txt、pdf、mp4等,使用open函数打开文件,使用read、write函数对文件进行读写,使用close函数关闭文件。但除了普通文件外,Unix/Linux的创作者提出“一切皆文件”的思想,希望将目录、字符设备、块设备、套接字等也被等同于文件对待,使用普通文件使用相同的文件操作接口,如open、read、write,对这些设备进行操作。
虚拟文件系统(Virtual File System,VFS)是Linux系统中的一个软件抽象层,它就是实现上述功能的关键。它为用户空间的程序提供了文件系统接口,同时定义了所有文件系统都支持的基本的、概念上的接口和数据结构。例如,读写普通的文本文件和读写I/O设备的具体实现方法必然是不同的,但VFS提供了统一的接口read和write,开发人员需要编写这些接口的具体的不同的实现。因此,在VFS层和内核的其他部分看来,所有文件都只需调用read和write函数就可以完成读写功能,具体的实现过程它们并不关心。
2.文件接口结构体file_operation
struct file_operations {
struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES
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);//仅用于读取目录,对于设备文件,该字段为NULL
unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替
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); //通知设备FASYNC标志发生变化
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 **);
};
用户层程序利用在对/dev路径设备进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。
下面介绍几个主要指针函数,如下:
1、struct module *owner
第一个 file_operations 成员根本不是一个操作,它是一个指向拥有这个结构的模块的指针。
这个成员用来在它的操作还在被使用时阻止模块被卸载。几乎所有时间中,它被简单初始化为THIS_MODULE,一个在 <linux/module.h>中定义的宏。这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。
2、int (*open) (struct inode * inode , struct file * filp ) ;
尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法。如果这个项是 NULL,设备打开一直成功,但是你的驱动不会得到通知。与open()函数对应的是release()函数。
(inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
3、ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);
这个函数用来从设备中获取数据。在这个位置的一个空指针导致 read 系统调用以 -EINVAL(“Invalid argument”)失败。一个非负返回值代表了成功读取的字节数(返回值是一个"signed size"类型,常常是目标平台本地的整数类型)。指针参数 filp 为进行读取信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址),参数size为要读取的信息长度,参数p为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值.
4、ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos);
发送数据给设备。如果NULL,-EINVAL返回给调用write系统调用的程序。如果非负,返回值代表成功写的字节数。
(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)
5、int (*release) (struct inode *, struct file *);
release()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:
三、总结
struct file_operations结构体实现将用户层对于设备的open()、read()、write()和close()等操作映射到内核层的设备驱动操作函数,驱动开发重点实现此函数中指向的操作函数,从而实现系统用户层操作程序对于硬件设备的操作。
参考文章:Linux字符设备驱动程序(设备号、cdev、设备节点、file_operations)
参考文章:Linux字符驱动设备file_operations结构体分析
参考文章:linux驱动开发(二):Linux字符设备驱动程序(设备号、cdev、设备节点、file_operations)

1万+

被折叠的 条评论
为什么被折叠?



