Linux之内核中的文件系统


文件描述符

一般说起文件和文件系统的时候,都会下意识的想到它们存在于磁盘上。然而,磁盘只能被动的存储,却不能主动去处理文件;要处理文件,只能把它们的数据复制到内存中,交给CPU处理,处理好的数据先写入内存,再传回到磁盘。那么,操作系统如何在内存中管理各种文件呢?即运行时文件系统在内核中的表示。
我们知道,进程是操作系统分配资源的基本单位,文件也是在进程中被处理的。比如用vim写代码,那么vim这个程序就成为操作系统中的一个进程,vim处理的对象就是一个代码文件。操作系统用PCB来管理进程,PCB从代码的角度看就是task_struct结构体,这个结构体中有一个指针指向files_struct结构体,这就是所谓的文件描述符表,其代码如下:

struct files_struct {
         atomic_t count;        /* 共享该表的进程数 */
         rwlock_t file_lock;     /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
         int max_fds;           /*当前文件对象的最大数*/
         int max_fdset;        /*当前文件描述符的最大数*/
         int next_fd;          /*已分配的文件描述符加1*/
         struct file ** fd;      /* 指向文件对象指针数组的指针 */
         fd_set *close_on_exec;  /*指向执行exec(  )时需要关闭的文件描述符*/
         fd_set *open_fds;     /*指向打开文件描述符的指针*/
         fd_set close_on_exec_init;/* 执行exec(  )时需要关闭的文件描述符的初值集合*/
         fd_set open_fds_init;  /*文件描述符的初值集合*/
         struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};

这个表是每个进程私有的,每个进程都有一个,它也称为用户打开文件表,这是为了区别系统打开文件表。我们主要关注最后一个成员:struct file * fd_array[32];,这是一个指针数组,数组的每个成员都指向一个file结构体,file结构体中保存着该进程已经打开的文件的信息。由于一个进程可以打开多个文件,所以用一个指针数组来保存它们的信息。对于在数组中有入口地址的每个文件来说,数组的索引就是文件描述符(file descriptor)。通常,数组的第一个元素(索引为0)是进程的标准输入文件,数组的第二个元素(索引为1)是进程的标准输出文件,数组的第三个元素(索引为2)是进程的标准错误文件。这三个文件是操作系统为每个进程默认打开的,如果进程需要打开其他文件,那么进程描述符将从3开始。文件描述符是系统的一个重要资源,虽然说系统内存有多少就可以打开多少的文件描述符,但是在实际实现过程中内核是会做相应的处理的,一般最大打开文件数会是系统内存的10%(以KB来计算)(称之为系统级限制),查看系统级别的最大打开文件数可以使用sysctl -a | grep fs.file-max命令查看。与此同时,内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024,使用ulimit -n命令可以查看。
在Linux中,进程是通过文件描述符(file descriptors,简称fd)而不是文件名来访问文件的,文件描述符实际上是一个整数。

虚拟文件系统
下面来看一下file结构体,代码如下:
struct file
{
 struct list_head        f_list;    /*所有打开的文件形成一个链表*/
 struct dentry           *f_dentry; /*指向相关目录项的指针*/
 struct vfsmount         *f_vfsmnt; /*指向VFS安装点的指针*/
 struct file_operations  *f_op;     /*指向文件操作表的指针*/
 mode_t f_mode;                     /*文件的打开模式*/
 loff_t f_pos;                      /*文件的当前位置*/
 unsigned short f_flags;            /*打开文件时所指定的标志*/
 unsigned short f_count;            /*使用该结构的进程数*/
 unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; 
/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/
 int f_owner;                       /* 通过信号进行异步I/O数据的传送*/
 unsigned int         f_uid, f_gid; /*用户的UID和GID*/
 int                 f_error;       /*网络写操作的错误码*/
 unsigned long f_version;          /*版本号*/
 void *private_data;              /* tty驱动程序所需 */
};

file结构体并不与实际的文件一一对应,例如,当一个进程多次打开同一文件的时候,都会分配一个不同的file结构体以及相应的文件描述符,尽管这些file结构体最终都指向同一个实际物理文件。可以看出,内存中的文件和磁盘中的文件实现方式还是不太一样的,内存中的文件是动态的,因为要不停的读写,所以只是一份拷贝,所有的操作针对的只是这份拷贝,操作完成后,把结果写回到磁盘文件中;写回这个动作执行之前,所有的改动只存在与内存中,并不会实时反应到磁盘上,可以用vim和cat同时操作一个文件来验证一下。懂了这个道理之后,再看为啥每次打开文件要分配不同的file结构体:因为每次打开意味着要对同一个文件进行不同的操作,并希望不同操作的结果都能写回到磁盘文件中;如果使用同一个file,那么就有可能后面的操作覆盖掉前面的操作。
覆盖的可能性来自于file结构体的成员loff_t f_pos;它表示文件当前的读写位置。每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。每次打开一个文件,除非明确要求,否则文件位置都被置为0,即文件的开始处,此后的读或写操作都将从文件的开始处执行,可以通过执行系统调用LSEEK(随机存储)对这个文件位置进行修改。所以,如果两次操作是针对同一文件的不同位置,使用同一个file结构体将使它们拥有同样的读写位置,这显然无法达到目的。这还是同一进程的不同操作,更何况不同进程的不同操作呢?于是,每次打开文件几乎都要分配一个新的file结构体。可以说,file结构体主要保存的就是这个读写位置。
上面使用了“几乎”,那就表示有例外情况:生成一个新进程时,子进程要共享父进程的所有信息,包括file结构体,其成员unsigned short f_count;就表示了当前有多少进程在使用这个结构体,只有当它的值为0时,才从内存中销毁这它。
file结构体中的第一个成员是 struct list_head f_list;它使file结构形成一个双链表,称为系统打开文件表,其最大长度是NR_FILE,在fs.h中定义为8192。
那么文件描述符表、系统打开文件表和实际文件之间的关系可以如下图:
这里写图片描述
然后我们看看file结构体如何指向最终的实际文件,这要涉及到第二个成员:struct dentry *f_dentry; 其代码如下:

struct dentry {
atomic_t d_count;        /* 目录项引用计数器 */
unsigned int d_flags;    /* 目录项标志 */
struct inode  * d_inode;   /* 与文件名关联的索引节点 */
struct dentry * d_parent;       /* 父目录的目录项 */
struct list_head d_hash;        /* 目录项形成的哈希表 */
struct list_head d_lru;         /*未使用的 LRU 链表 */
struct list_head d_child;       /*父目录的子目录项所形成的链表 */
struct list_head d_subdirs;     /* 该目录项的子目录所形成的链表*/
struct list_head d_alias;       /* 索引节点别名的链表*/
int d_mounted;                  /* 目录项的安装点 */
struct qstr d_name;             /* 目录项名(可快速查找) */
unsigned long d_time;           /* 由 d_revalidate函数使用 */
struct dentry_operations  *d_op; /* 目录项的函数集*/
struct super_block * d_sb;      /* 目录项树的根 (即文件的超级块)*/
unsigned long d_vfs_flags;  
void * d_fsdata;                /* 具体文件系统的数据 */
unsigned char d_iname[DNAME_INLINE_LEN]; /* 短文件名 */
};

这个结构体保存了路径信息,包括文件名。我在介绍ln命令时说过,操作系统根据inode号寻找文件,而人类根据文件名,故需要从这个结构体中找到二者之间的对应关系。我们可以利用这两个成员:struct inode * d_inode;和struct qstr d_name;。前者指向了与真正的文件相关的inode结构体,它保存着从磁盘分区的inode读上来信息。至此,进程中文件描述符指向了最终的磁盘文件,我猜file结构体最终要依靠inode结构体写回磁盘。
完。

相关内容