linux虚拟文件系统-文件的打开

本质上,一个文件打开的过程就是建立fd,file,dentry,inode,address_space的关联过程。关联过程中关键的一个过程是如何根据路径名寻找到对应的dentry,对于用户程序来说路径只是一个特殊的字符串来表示全路径,但是内核中不是以全路径来管理文件的,而是分解成多个dentry,他们之间相互组成树的形式,从该dentry到根文件系统的root dentry之间所有的dentry的名称组合到一起并且在其中加上“/”组成了全路径字符串;中间有一个映射过程,需要将用户程序中路径名映射到某个dentry上。
linux系统支持两种路径:绝对路径和相对路径,绝对路径就是以根文件系统的root dentry为起点的路径名,例如/mnt/test/a,相对路径以当前进程所在的工作目录为起点的路径名,例如./a/b
另外系统还支持软链接文件和挂载,在查找路径的时候还需要对其进行转换,该问题在下面路径查找的时候进一步说明。

文件对象关联

文件打开的过程中主要涉及到几种对象,fd,file,dentry,inode,address_space,最后建立的映射关系如下图。
linux虚拟文件系统-文件的打开_第1张图片

  • files_struct中默认内嵌有一个fdtable和file的指针数组fd_array,一般进程打开的文件很少,所以内嵌的对象一般就能满足使用。对于进程打开大量文件的情况,它会新申请fdtable对象并且对file指针数组进行扩张来容纳更多的文件信息,并且将旧的fdtable信息和file指针数组拷贝到新的对象上去。fd代表索引,对应着fdtable->fd指针数组的下标和fdtable中open_fds和close_on_exec的两个bitmap的位置。

  • fdtable中分别有open_fds和close_on_exec两个指针,分别指向两个bitmap,前者标识文件描述符是否可用,而后者描述了在进程exec时是否将文件描述符进行关闭。当进程exec时默认是继承原有的fdtable信息的,这会造成两种情况,一个是文件描述符的泄露,另一个是文件打开异常。例如当程序hello启动时通过flock文件锁的形式来检查是否可以运行,第一次打开文件对他进行上锁,之后通过exec启动它自身,有进行文件打开和上锁,此时就上锁失败了,这种情况时需要在open时指定O_CLOEXEC,在exec时自动关闭这个描述符。

  • 文件的各种信息都是拿到path之后进行实际关联的,path中指向具体的dentry,进而关联到inode和address_space。找到对应的dentry之后在do_dentry_open进一步关联,file可以直接到inode和address_space,file的file_operations的对象更是原来的inode的i_fop,这样接下来的文件读写就基本上可以直接面向address_space和它管理的page cache了。不同类型的文件file的file_operations是不同的,例如普通文件有readdir就毫无意义,反过来目录文件有llseek操作也很奇怪,所以在根据文件系统上信息创建inode对象的时候根据文件类型为他指定了不同的file_operations,这样在和file关联的时候就能得到正确的文件操作方法。

路径查找过程

path:文件路径,内核中一个文件所在的路径使用vfsmount,dentry的形式即挂载点下的某个dentry来表示唯一路径。主要是一个分区可能被多次挂载到不同的路径,文件路径虽然不同但是都是同一个文件。
nameidata来源于name to inode文件路径到inode的解析,也可以理解为路径解析(name interprets),表示一次路径查找的过。主要有几个重要的成员:struct path path,root;int last_type;struct qstr last。在查找之前会对其进行初始化,root成员指向进程的根路径,根据路径是否以/开头来决定path成员指向当前进程的工作目录还是根路径,这样nameidata就能处理绝对路径和相对路径两种形式了。path指向当前解析到的路径,当查找结束的时候指向目标路径。last_type表示上一次路径的类型,总共有LAST_NORM, LAST_ROOT, LAST_DOT,LAST_DOTDOT几种类型。struct qstr last表示下一次要查找的文件名称。

struct nameidata {
    struct path path; 	//当前路径,随着路径名的解析它会逐渐的变化,最后指向路径名的path
    struct qstr last;	//在路径名解析过程中,会根据“/”将路径名分解成目录项,例如“/mnt/test”则会分解成mnt和test两个目录项;代表当前目录下要查找的文件名
    struct path root; 	//进程的根文件路径
    struct inode    *inode; /* path.dentry.d_inode */ 
    unsigned int    flags; 
    unsigned    seq, m_seq;
    int     last_type; 
    unsigned    depth; 
    struct file *base;  
    char *saved_names[MAX_NESTED_LINKS + 1];          //主要是防止软链接的不断循环嵌套
}; 

在查找的过程中,它将文件路径以/为分隔符来分解成一串路径,我们先不考虑特殊的路径查找,以普通的路径查找为例。
linux虚拟文件系统-文件的打开_第2张图片上图中是文件打开的基本过程:

  1. 分配空闲的文件描述符,当文件成功打开之后就能在最后的fd_install进行关联
  2. 分配空闲的file,当路径查找成功的时候可以进行和对应的dentry,inode进行关联
  3. 文件路径的分解link_path_walk,它将一个路径分解成两部分:最后一级文件名和其他部分,例如在图中/mnt/test/a文件,分解成/mnt/test/a两部分,当然这是一个最简单的实例,实际的路径更加复杂:最后一级文件可以是普通文件,目录文件,软连接文件,其他部分可能只是普通的路径,也可能包含软连接文件,还有可能经过挂载点。对于查找一个/mnt/test/a文件,它将路径名拆解成mnt,test,通过walk_component循环查找每一级路径并且更新last_type和last:查找/下是否有mnt,之后查找mnt下test,一直到剩下最后一级a。
  4. 最后do_last处理最后一级路径a的问题,即真正开始进行文件打开的操作,从文件系统中读取信息创建对应的dentry和inode,address_space信息并且关联起来。

walk_component是路径查找的主要实现,它里面需要处理很多琐碎的问题,最主要的就是并发问题。加入正在将a/b通过rename操作变成a/c/b,另一个进程正在查找a/b/..它可能最终返回的是a/c目录文件。rename本身的操作应该是原子性的,查找的结果应该有两种,当rename操作之前返回的是a,操作之后应该这个路径就是非法的,无论如何也不应该返回给一个a/c目录文件。上面的查找过程总体而言没啥难度,但是这种并发操作让这个过程的实现太晦涩了,而且也绕不开这个问题,所以里面会使用各种的锁和引用来防止并发。

首先说一下普通的慢速查找lookup_slow,对于/mnt/test/a路径查找,例如查找/时是否有mnt目录项,它首先需要获取到(/)dentry->d_inode->i_mutex,之后通过目录的inode->i_op->lookup进行查找,这个可能是需要进行实际读取文件系统数据来进行查找的,文件系统可能是基于块设备的也可能是网络文件系统类型,意味着它需要发起磁盘IO或者网络查询来得到确切的答案。
内核中除了使用dcache来缓存文件系统的信息,避免进行底层文件系统操作之外,还通过dentry_hashtable哈希表来维护dentry,这样可以通过父dentry和当前文件名信息struct qstr来计算hash值来查找当前文件的dentry,这样的速度是最快的了。为了应对并发问题,它使用RCU技术来保证对象不会在使用中被释放掉,整个的link_path_walk过程都处于read临界区,rcu_read_lock();link_path_walk();rcu_read_unlock()

目前的查找过程中,优先使用RCU walk的形式,当失败之后再次使用slow的方式去文件系统中获取对应的路径。

软链接文件

follow_link处理软链接文件的路径问题,通过dentry->d_inode->i_op->follow_link将软链接文件的内容读取到nameidata->save_names[nameidata->depth]中,之后根据链接路径来重新初始化path。软链接形式是绝对路径时即以/开头,复位nameidata->path到nameidata->root再次通过link_path_walk进行查找。
对于路径中间存在软链接形式的文件时,walk_component负责跳转到软链接中。
对于最后一级文件是软链接时,当open时允许跟踪软链接文件时,do_last尝试打开时会返回错误并且should_follow_link返回真,之后它会通过trailing_symlink再次进行查找和打开。

挂载点

当一个目录被挂载之后,它和下面所有的文件都被隐藏掉了,后续的文件操作是看不到这些文件的,直至被卸载掉。当路径查找的过程经过挂载点时,即当前dentry->d_flags|DCACHE_MOUNTED时表明当前目录是一个挂载点,需要转换到挂载点中的路径也就是当前的可见路径。
linux虚拟文件系统-文件的打开_第3张图片挂载点mount通过mount_hashtable表管理,它的hash值是通过挂载点test所在的vfsmount(test)和dentry(test)计算出来的,之后通过mount->mnt_parent->mnt == vfsmount(test) && mount->mnt_mountpoint == dentry(test)来确定mount;内核中使用lookup_mnt来查找一个挂载点dentry对应的mount信息,linux允许一个目录可以被多次挂载,第一次查询返回最后被挂载的信息,以后逐次返回上一次挂载的信息,最后返回空。

 * mount /dev/sda1 /mnt                                                             
 * mount /dev/sda2 /mnt                                                             
 * mount /dev/sda3 /mnt                                                             
 *                                                                                  
 * Then lookup_mnt() on the base /mnt dentry in the root mount will                 
 * return successively the root dentry and vfsmount of /dev/sda1, then              
 * /dev/sda2, then /dev/sda3, then NULL.   
 struct vfsmount *lookup_mnt(const struct path *path)

当经过挂载点路径时,通过follow_managed来切换路径,查找到对应的mount信息之后,改变nameidata->path中的信息:

path->mnt = mounted;
path->dentry = dget(mounted->mnt_root);

link_path_walk

link_path_walk只是循环查找中间的路径名而不处理最后一级的路径问题,除了普通的open之外还有不同的场景也会进行文件名解析,例如通过stat或者chmod系统调用也需要进行文件名解析,它只需在最后一级文件存在的情况下返回对应的dentry就足够了。而当通过open系统调用进行文件打开时,它允许添加各种参数里,例如O_CREAT,O_NOFOLLOW时,O_CREAT在最后一级dentry没有对应的inode时进行创建inode动作,O_NOFOLLOW在最后一级dentry是个软连接文件时不再进行follow link而是直接打开文件,此时读到的就是软链接文件的内容:它的链接路径:/home/linux/project/linux/scripts/gdb/vmlinux-gdb.py,不带O_NOFOLLOW时会根据它的链接路径继续查找link_path_walk找到最终的普通文件。
softlink

dcache的精确性问题

内核使用dentry和inode cache来保存底层文件系统的信息,这样就不需要频发查找磁盘了,但是对于网络文件系统,dcache完全不能保证和实际文件系统的一致性,实际的文件系统在另外一台机器上,可能被几个机器网络共享访问,dcache不能精确代表当前的状态,所以在查找的过程需要重新revalidate操作。

文件的关闭

因为每一层存在一对多的关系,例如多个fd引用同一个file,多个file引用同一个inode,它通过引用计数来表示当前的使用状态,对于文件的关闭操作直接触发的就非常简单,仅仅是解除fd和file的关联,清除fdtable中的bitmap相关位,并对file引用计数减一。fput时当file的引用计数为零时准备释放file对象,它这里使用了RCU异步延迟释放的方式进行释放,真正干活的是__fput,对dentry减一,之后dput触发inode的引用计数,一环扣一环,环环相扣来实现了精准引用统计,避免内存泄露和错误释放。

VFS本身看起来比较简单,实际上它非常复杂,处理的细节问题非常多,fs/namei.c文件就有5000行,还有几个主要的问题没有解决,留待下一篇吧

  1. dentry的各种锁及应用场景分析,VFS本身支持多场景并发,并且它要处理各种底层文件系统的特性,这就有了各种锁和引用计数
  2. 在查找过程中如何防止循环软链接的
  3. RCU形式的查找和slow形式查找的切换

你可能感兴趣的:(linux)