linux文件系统和设备驱动+file结构体

1,文件系统和设备驱动之间的关系

 1)应用程序和VFS之间是系统调用;

2)VFS与文件系统以及设备文件之间的接口是file_operations结构体成员函数,这个结构体可以对文件进行打开,读写,定位,控制等操作;如下图所示:

 3)由于字符设备的上层没有类似磁盘的ext2等文件系统,所以字符设备的file_opterations成员函数就直接由设备驱动提供了

 

 

2,file结构体

1)2.6源码在linux/inlcude/linux/fs.h文件中

 1 struct file {
 2     union {
 3         struct llist_node    fu_llist;
 4         struct rcu_head     fu_rcuhead;
 5     } f_u;
 6     struct path        f_path;
 7     struct inode        *f_inode;    /* cached value */
 8     const struct file_operations    *f_op;
 9 
10     /*
11      * Protects f_ep_links, f_flags.
12      * Must not be taken from IRQ context.
13      */
14     spinlock_t        f_lock;
15     atomic_long_t        f_count;
16     unsigned int         f_flags;
17     fmode_t            f_mode;
18     struct mutex        f_pos_lock;
19     loff_t            f_pos;
20     struct fown_struct    f_owner;
21     const struct cred    *f_cred;
22     struct file_ra_state    f_ra;
23 
24     u64            f_version;
25 #ifdef CONFIG_SECURITY
26     void            *f_security;
27 #endif
28     /* needed for tty driver, and maybe others */
29     void            *private_data;
30 
31 #ifdef CONFIG_EPOLL
32     /* Used by fs/eventpoll.c to link all the hooks to this file */
33     struct list_head    f_ep_links;
34     struct list_head    f_tfile_llink;
35 #endif /* #ifdef CONFIG_EPOLL */
36     struct address_space    *f_mapping;
37 } __attribute__((aligned(4)));    /* lest something weird decides that 2 is OK */

网上找的带注释的版本:

 1 struct file {
 2 
 3   union {
 4 
 5         struct list_head fu_list; //文件对象链表指针linux/include/linux/list.h
 6 
 7         struct rcu_head fu_rcuhead; //RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
 8 
 9    } f_u;
10 
11   struct path f_path; //包含dentry和mnt两个成员,用于确定文件路径
12 
13   #define f_dentry f_path.dentry //f_path的成员之一,当前文件的dentry结构
14 
15   #define f_vfsmnt f_path.mnt //表示当前文件所在文件系统的挂载根目录
16 
17   const struct file_operations *f_op; //与该文件相关联的操作函数
18 
19   atomic_t f_count; //文件的引用计数(有多少进程打开该文件)
20 
21   unsigned int f_flags; //对应于open时指定的flag
22 
23   mode_t f_mode; //读写模式:open的mod_t mode参数
24 
25        loff_t     f_pos;//当前文件指针位置
26 
27   off_t f_pos; //该文件在当前进程中的文件偏移量
28 
29   struct fown_struct f_owner; //该结构的作用是通过信号进行I/O时间通知的数据。
30 
31   unsigned int f_uid, f_gid;// 文件所有者id,所有者组id
32 
33   struct file_ra_state f_ra; //在linux/include/linux/fs.h中定义,文件预读相关
34 
35   unsigned long f_version;//记录文件的版本号,每次使用之后递增
36 
37   #ifdef CONFIG_SECURITY
38 
39        void *f_security;
40 
41   #endif
42 
43   /* needed for tty driver, and maybe others */
44 
45   void *private_data;//使用这个成员来指向分配的数据
46 
47   #ifdef CONFIG_EPOLL
48 
49   /* Used by fs/eventpoll.c to link all the hooks to this file */
50 
51       struct list_head f_ep_links;
52 
53       spinlock_t f_ep_lock;
54 
55   #endif /* #ifdef CONFIG_EPOLL */
56 
57   struct address_space *f_mapping;
58 
59   };

struct file(file结构体):
  struct file结构体定义在include/linux/fs.h中定义。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。

它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,

struct file的指针通常被命名为file或filp。其有两个非常重要的字段:文件描述符和缓冲区。

 文件描述符fd:

  fd只是一个小整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。

文件描述符的操作(如: open)返回的是一个文件描述符,内核会在每个进程空间中维护一个文件描述符表, 所有打开的文件都将通过此表中的文件描述符来引用; 
而流(如: fopen)返回的是一个FILE结构指针, FILE结构是包含有文件描述符的,FILE结构函数可以看作是对fd直接操作的系统调用的封装, 它的优点是带有I/O

缓存。

  每个进程在PCB(Process Control Block)即进程控制块中都保存着一份文件描述符表,文件描述符就是这个表的索引,文件描述表中每个表项都有一个指

向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。

 

缓冲区:

A)缓冲区机制

根据应用程序对文件的访问方式,即是否存在缓冲区,对文件的访问可以分为带缓冲区的操作和非缓冲区的文件操作:

  a) 带缓冲区文件操作:高级标准文件I/O操作,将会在用户空间中自动为正在使用的文件开辟内存缓冲区。

  b) 非缓冲区文件操作:低级文件I/O操作,读写文件时,不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),当然用于可以在自己

的程序中为每个文件设定缓冲区。

两种文件操作的解释和比较:

  1、非缓冲的文件操作访问方式,每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉

及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。

  2、ANSI标准C库函数 是建立在底层的系统调用之上,即C函数库文件访问函数的实现中使用了低级文件I/O系统调用,ANSI标准C库中的文件处理函数为了减

少使用系统调用的次数,提高效率,采用缓冲机制,这样,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需

要再使用系统调用了,即需要少量的CPU状态切换,提高了效率。

B)缓冲类型

标准I/O提供了3种类型的缓冲区。

  1、全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。第一次执行I/O操作时,ANSI标

准的文件管理函数通过调用malloc函数获得需要使用的缓冲区,默认大小为8192。

  2、行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准

输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区

的大小为1024。

  3、无缓冲区:

无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

注:

  1、标准输入和标准输出设备:当且仅当不涉及交互作用设备时,标准输入流和标准输出流才是全缓冲的。

  2、标准错误输出设备:标准出错绝不会是全缓冲方式的。

  3、对于任何一个给定的流,可以调用setbuf()和setvbuf()函数更改其缓冲区类型。

下面我们通过如下程序来进一步了解缓冲区:

 1 #include<stdio.h>
 2 #include<unistd.h>
 3 #include<sys/types.h>
 4 #include<fcntl.h>
 5 #include<sys/stat.h>
 6 #include<errno.h>
 7 #include<string.h>
 8 #include<stdlib.h>
 9 
10 char* msg="hello bit\n";
11 char* msg1="hello world\n";
12 
13 int main()
14 {
15     printf("printf\n");
16     fwrite(msg,1,strlen(msg),stdout);
17     write(1,msg1,strlen(msg1));
18 
19     pid_t id=fork();
20     if(id<0){
21         printf("fork,%s\n",strerror(errno));
22         exit(0);
23     }else if(id==0) {
24         printf("child:pid=%d,ppid=%d\n",getpid(),getppid());
25     }else{
26         printf("father:pid=%d,ppid=%d\n",getpid(),getppid());
27         sleep(3);
28     }
29     return 0;
30 }

使用命令行 gcc buffer.c ,

2.1)执行 ./a.out 

printf
hello bit
hello world
father:pid=19502,ppid=13555
child:pid=19503,ppid=19502

2.2)执行 ./a.out > testfile 

testfile内容如下所示:

hello world
printf
hello bit
child:pid=22995,ppid=22994
printf
hello bit
father:pid=22994,ppid=13555

那么为什么输出到屏幕只有5条输出命令而输出到文件有7条输出命令呢?

根据输出结果我们可以看出printf和fwrite重复写了两次,没有重复打印的是write.。


printf和fwrite都是库函数,而write则是系统直接调用的:结合已有知识,我们了解到当使用库函数命令时,打印消息并没有直接写到输出位置上,而是先把数据写到输出缓冲区,在刷新至输出位置。

    1、当输出目标位置为输出到显示器时,则刷新方式是行刷新;

    2、当输出目标位置为输出到文件中时,刷新方式由行缓冲变为全缓冲,全缓冲是指当把缓冲区写满后才能刷新。(或者强制刷新)代码中printf和fwrite第一次打印在fork操作之前,第二次打印则在fork操作之后,原因是在fork操作前,printf和fwrite的输出命令将数据先写到缓冲区中,此时只执行了这两条命令,由于是全缓冲的刷新方式,所以这两条命令并不足以将缓存写满,所以数据暂存在缓冲区中;然后进行fork创建子进程,由于fork创建出的父子进程代码共享,而数据不共享,各自私有一份,缓冲区中的数据都属于数据,因为父进程的残留数据还在缓冲区中,所以fork完毕后,父子进程将缓存中的数据各自存一份有父进程残留数据的数据,所以当父子进程各自刷新时,父子进程会各自执行一次printf和fwrite的输出命令,所以命令就从原来的两条变为四条。

struct file 的其他重要成员有:.
  1.mode_t f_mode;
      文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写

许可, 但是不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.

  2.loff_t f_pos;
      当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ). 驱动可以读这个值,如果它需要知道文件中的当前位置, 但是正常地不应该改变它; 读

和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于 filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.

  3.unsigned int f_flags;
      这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作; 其他标志很少使用. 特别地, 

应当检查读/写许可, 使用 f_mode 而不是f_flags. 所有的标志在头文件<linux/fcntl.h> 中定义.

  4.struct file_operations *f_op;
      和文件关联的操作. 内核安排指针作为它的open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味

着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替

代 filp->f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销. 替换文件操作的能力是面向对象编程的"方法重载"的内核对

等体.

  5.void *private_data;
      open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须

记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.

  6.struct dentry *f_dentry;
关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关心 dentry 结构, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.

 

 

 

 

 

posted @ 2023-06-25 17:04  burlingame  阅读(191)  评论(0编辑  收藏  举报