Linux内核三大数据结构——file_operations、file & inode

来源:互联网 发布:matlab 2016a mac 编辑:程序博客网 时间:2024/05/28 01:35

大部分驱动程序操作都涉及三个重要的内核数据结构,分别是file_operations,file,inode。

file_operations:(文件操作)

定义#include <linux/fs.h>

头文件如果没包含,编译会出现以下错误:error: variable 'test_fops' has initializer but incomplete type

    该结构是将系统调用 和驱动程序连接起来,这个结构的每一成员都对应着一个系统调用。当用户进程利用系统调用对设备进行读写操作的时候,这些系统调用通过设备节点中的主设备号和次设备号来确定相应的驱动程序,而每一个字符驱动在linux内核中又是由cdev结构体来描述的,其中cdev结构体中含有成员fops结构体,然后就可以读取file_operations结构体中相应的函数指针,接着把控制权交给函数,从而完成linux设备驱动程序的工作。

[Action]:

file_operations 结构或者指向这类结构的指针称为fops,这个结构中每个字段都必须指向驱动中实现特定操作的函数。

对于不支持的操作,可以对应的字段设置为NULL。
struct file_operations{struct module *owner//第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.loff_t (*llseek) (struct file *, loff_t, int);//llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);//初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);//初始化设备上的一个异步写.int (*readdir) (struct file *, void *, filldir_t);//对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.unsigned int (*poll) (struct file *, struct poll_table_struct *);//poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);//ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.int (*mmap) (struct file *, struct vm_area_struct *);//mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.int (*open) (struct inode *, struct file *);//尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.int (*flush) (struct file *);//flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.int (*release) (struct inode *, struct file *);//在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.int (*fsync) (struct file *, struct dentry *, int);//这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.int (*aio_fsync)(struct kiocb *, int);//这是 fsync 方法的异步版本.int (*fasync) (int, struct file *, int);//这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.int (*lock) (struct file *, int, struct file_lock *);//lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);//这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);//这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);//sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);//这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.[10]int (*check_flags)(int)//这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.int (*dir_notify)(struct file *, unsigned long);//这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify.};

一般的设备驱动程序通常用到以下5种操作: 

struct file_operations  {      ssize_t (*read)(struct file *,charchar *, size_t, loff_t *);//从设备同步读取数据      ssize_t (*write)(struct file *,const charchar *, size_t, loff_t *);      int (*ioctl) (struct  inode *,  struct file *, unsigned int,  unsigned long);//执行设备IO控制命令      int (*open) (struct inode *, struct file *);//打开      int (*release)(struct inode *, struct file *);//关闭  }; 

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
具体到应用程序中:
fd=open("/dev/hello",O_RDWR)
通过系统调用open()来打开设备文件,此设备节点对应有一个设备号。
打开 /dev/hello时,通过主次设备号找到相应的字符驱动程序。即在cdev链表中找到cdev这个结构体,
cdev里面又包含了file_operations结构体,含有对设备的各种操作,打开时即调用里面的.open 函数指针指向的open函数。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


File:

file是一个内核结构,不会出现在用户程序中。它不仅仅代表一个打开的文件。它由内核在open时创建,并传递给该文件上进行操作的所有函数,知道最后的close函数,在文件的所有实例都被关闭后,内核会释放这个数据结构。
struct file{mode_t f_mode;//表示文件是否可读或可写,FMODE_READ或FMODE_WRITEdev_ t  f_rdev ;// 用于/dev/ttyoff_t  f_ops;//当前文件位移unsigned short f_flags;//文件标志,O_RDONLY,O_NONBLOCK和O_SYNCunsigned short f_count;//打开的文件数目unsigned short f_reada;struct inode *f_inode;//指向inode的结构指针struct file_operations *f_op;//文件索引指针};

  

Inode:
    在linux中inode结构体用于表示文件,file结构体表示打开的文件的描述,因为对于单个文件而言可能会有许多个表示打开的文件的描述符,因而就可能会的对应有多个file结构,但是都指向单个inode结构。


该结构里面包含了很多信息,但是,驱动开发者只关心里面两个重要的域:
dev_t i_rdev;  //含有真正的设备号
struct cdev *i_cdev;  //struct cdev是内核内部表示字符设备的结构.
 

【三者之间关系】:

    struct file结构体中包含有struct file_operations结构体,struct file_operations是struct file的一个域;我们在使用系统调用open()打开一个设备节点struct inode时,我们会得到一个文件struct file,同时返回一个文件描述符,该文件描述符是一个整数,我们称之为句柄,通过访问句柄我们能够访问设备文件struct file,描述符是一个有着特殊含义的整数,特定位都有一定的意义或属性。

inode结构它表示打开的文件描述符。他包含了大量有关文件的信息。而只有dev_t i_rdev; struct cdev *i_cdev与驱动程序代码有关用。
前者表示了设备文件的inode结构,包含了真正的设备编号,而后者表示字符设备的内核的内部结构,当其指向一个字符设备文件时,则包含了指向struct cdev结构的指针。


【驱动中】:

(1)、我们先找到一个设备号devno,可以动态申请,也可以静态设定,假设静态设定为major,minor,通过宏MKDEV(major,minor)来生成devno
(2)、构建对设备的操作函数file_opreation结构体,里面包含了的设备的操作:open、read、write、release、ioctl等
(3)、构建cdev结构体,里面填充两个主要成员dev(设备号)、file_operation(对设备的操作)
(4)、把cdev添加的cdev链表中:cdev_init、cdev_add


【应用程序中】:
fd=open("/dev/hello",O_RDWR)来打开设备文件,此设备节点对应有一个设备号,这是我们识别驱动和设备的桥梁。
打开 /dev/hello时,根据设备号,在cdev链表中找到cdev这个结构体,cdev里面包含了file_operation结构体,有设备的各种操作,打开时就调用里面的.open 函数。在这里要完成几件事:
(1)inode节点 每一个文件都对应有一个inode节点,inode结构体里.i_fop由cdev的file_operation填充,i_dev由cdev的设备号填充
(2)file结构体中的file_operation也同样由cdev中对应项填充,还有一项fd,对应于打开文件的文件描述符,fd和file一一对应,文件每打开一次,就有一个file结构it。所以file里面的.private就很重要,下面会说到。


还有一个问题,那就是多个相同的设备,会公用同一个驱动,所以要把每一个设备的私有数据封装起来,构成一个私有数据结构体。对设备的每一次读写,都通过操作设备的私有数据结构体中的资源来完成。也就是说,驱动在加载的时候,会申请多个设备私有资源结构体,每个结构体中包含了设备的所有私有资源,虽然公用一个驱动,可是通过设备号找到此设备号对应设备的私有资源,说的有点拗口。这可以通过file结构体的.private来指向。


例如封装私有数据的结构体为:
struct hello_device{
char buf[128]; //设备的私有资源,譬如buf
struct cdev cdev;//设备结构体,里面有devno和file_operation
……
};
前面应经提到inode中的i_cdev会指向cdev结构,所以可以由container宏来得到hello_device的地址。
所以,在驱动的open函数中有两个参数,inode和file
int open(structc inode *inode,struct file *file){
struct hello_device   *p =container(inode->i_cdev,hello_struct,cdev)
file->private=p;
}
这样file中就包含了设备的私有数据。
驱动read函数中:
ssize_t read(fd,char __user *buf,count)
fd和file一一对应,每打开一次设备,虽然有不同的fd,但他们的file.private是一样的。


前面主要说了一个驱动如何可以支持多个设备的问题,以及应用层和驱动之间的联系。还有一个问题就是,如何处理过个进程访问同一个设备的问题。



Reference:

http://blog.csdn.net/dreaming_my_dreams/article/details/8272586

http://liurugongzi.blog.sohu.com/104834407.html

http://blog.csdn.net/u010944778/article/details/45077565



阅读全文
0 0