暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Linux中的VFS实现[二]

术道经纬 2020-06-23
623

基础操作

内核不能感知一个文件的内容和结构,它只是把文件简单地看做字节的集合,并提供了对其内容的字节流访问。对一个文件的常规操作包括创建、删除和读写等。

【打开文件】

一个进程想要读取或是写入一个文件,必须先建立和文件inode之间的通道,方式是通过open()函数。

int open (const char *pathname, int flags, mode_t mode);

这里传入的参数是文件所在的路径明(即"pathname"),那如何根据这个"pathname"找到对应的inode呢?这就要依靠内核提供的dentry(directory entry)了。

dentry

dentry用于建立路径名和inode之间的关联,和super_block以及inode不同,dentry是一个内存结构,并没有对应的磁盘数据。

通常会觉得一个文件只有一个路径是吧?别忘了上文介绍的hard link,对一个inode每增加一个hard link,该inode的路径指向就增加一个。因此,一个inode会对应多个dentry(通过"i_dentry"链表组织),而一个dentry只会对应一个inode(即"d_inode")。

struct inode -> struct hlist_head i_dentry
struct dentry -> struct inode *d_inode; /* Where the name belongs to */

dentry并不等同于directory,但确实和directory存在着相当的关系。虽然directory被视作文件,但directory本身需要一些特殊操作,比如路径名的查找,而dentry概念的提出就是为了使查找的过程更加便利。比如对"."表示的同级目录就跳过解析,对".."表示的上级目录就使用"dentry->d_parent"。

一层一层的解析路径是非常耗时的,如果每次打开文件都重新解析一次路径,实在是效率堪忧。所以啊,已经解析过的路径会存在一个通过hash表组织的dentry cache(简称dcache)里。

dcache的每一项的内容是一个路径到inode的映射关系,即便是上次查找不存在的路径,也会以"negative entry"的形式记录在dcache里,这样下次在试图访问这个不存在的路径时,可以立即返回错误,不用再去磁盘瞎折腾一番。

此外,还有一个inode cache(简称icache),icache的每一项内容是一个已挂载的文件系统中的文件inode。因为文件的访问在时间上和空间上都体现出明显的局部性,因此使用dcache和icache能极大地提高访问效率。

file

每打开一次文件,就会生成一个代表已打开文件的struct file对象。

struct file {
struct path f_path;
struct inode *f_inode; /* cached value */
...
}

"file"结构体借助"f_path->dentry"和对应的inode关联起来,由于文件可以被多次打开,因此通过同一路径打开的"file"会关联到同一dentry上。

fd table

此外,内核还会向调用open()的进程返回一个per-process的文件描述符(file descriptor,简称fd),代表该进程与文件的一个独立会话,由struct file对象保存着该会话的内容,包括打开文件的方式(即"f_flags"和"f_mode",对应open函数中的第二和第三个参数)和下一次读写时的偏移位置(即"f_pos")等。

unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;

fd是一个从0开始的非负整数,同一进程获取的所有fd构成了这个进程的文件描述符表的数组索引,而数组元素就是指向struct file对象的指针。

根据POSIX标准,当获取一个新的fd时,要返回数值最小的可用描述符。内核会使用bitmap来记录每个进程的fd的分配情况,并据此找到这个最小的可用描述符。

task_struct->files->fdt->fd[fd];

在文件已经打开后,其名称就没什么用处了,它现在由其fd唯一标识,之后的系统调用都将以这个fd为参数。这样,在一次open()执行完路径名的解析后,接下来的每次文件访问都不再需要重复这一过程,使用fd即可快速定位到已打开文件的对象instance。

在其他一些类Unix系统中,还存在一个全局的Open File Table(简称OFT),每打开一个文件,OFT中将增加一个entry,指向全局的inode table中对应的inode。

文件描述符和OFT中的entry通常是一一对应的,但是在两种情况下,OFT中的一个entry会被多个文件描述符共享。

  • 一是使用fork()创建子进程后,子进程将共享父进程在OFT中的所有entries。

  • 二是使用dup(),它允许为进程创建一个新的描述符,指向已经打开且已经有了描述符的文件,其结果就是这两个描述符指向了OFT中的同一个entry。

int fd = open("README", O_RDONLY);
int fd2 = dup(fd);

dup()主要用在输出重定向中,共享同一entry的文件描述符("fd"和"fd2")可互换使用。

【读写文件

通过dentry可以找到inode,而inode记录了文件属性和文件数据的关联(参考这篇文章),因而最终可以找到文件的user data,实现对文件数据的读写。

文件打开后,起始的访问位置是文件的开头。因为文件默认是按顺序访问的,所以偏移位置会随着文件的读写被自动更新,具体的offset数值记录在struct file对象的"f_pos"域中。

调用read()读取一个文件的内容后除了文件的offset,inode的atime也会被更新。

当读到一个文件的末尾时,将返回0,但返回0不一定是读到末尾了(参考这个回答)。最后,vfs_read()实际调用的是文件所在的文件系统事先注册的read函数(称为“回调”)。

ssize_t __vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
if (file->f_op->read)
return file->f_op->read(file, buf, count, pos);
...
}

Linux内核虽然主要是用C语言编写的,但是其实现充分借鉴了面向对象的思想,使得结构和逻辑看起来更加简洁,VFS的设计就是一个很好的例证。每个结构体都包含若干的function pointers,对于一个具体的文件系统,这些function pointer指向文件系统具体的实现。

除了默认的顺序访问,还可使用lseek()强制改变offset的位置,以实现文件的随机访问。需要注意的是,文件offset只是一个软件的概念,因而lseek并不会引起任何真正的I/O操作。

如果同一进程(或者不同进程)打开同一个文件两次,会生成两个不同的文件描述符,它们有各自独立的offset,互不影响。

write()用于写入,它会更新一个inode的mtime,还可能涉及到磁盘上block的分配。

ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos)

下一次的读写默认从当前的offset位置开始,如果需要显式地传入偏移量,可使用pread()和pwrite(),这两个函数相当于lseek()加上read()/write()的组合。如果需要多个buffer的内容被依次传送,应使用scatter-gather模式的readv()和writev()。

为了加快读写文件的的速度,磁盘上文件的内容会在内存中形成一份copy,且最近访问过的部分会驻留在内存中,也就是page cache。

struct inode中与page cache相关的部分包括:管理了一个文件在RAM中缓存的所有pages的"address_space",用于page writeback的dirty时间记录的"dirtied_when"和后备存储器管理的"i_wb_list"等。

struct inode {
struct address_space *i_mapping;

unsigned long dirtied_when; /* jiffies of first dirtying */
struct list_head i_wb_list; /* backing dev writeback list */
...
}

page cache可以提高对文件内容的访问速度,但由于其驻留在内存,因此存在一点掉电时数据丢失的风险,所以writeback机制除了用于释放并回收内存,也可视作是一种durability和performace之间的trade-off。

【关闭文件

调用close()关闭文件,将释放文件描述符,同时将inode的引用计数减1。如上文所讨论的,当引用计数为0时,意味着一个文件的所有hard link都被移除,并且没有一个进程正在使用该文件,则文件内容和inode才会被真正删除。那如果进程在退出时忘记了关闭会怎样呢?

对于普通进程,这没有关系,因为进程退出时Linux内核会自动关闭文件,释放内存。但对于一个常驻进程来说,如果文件描述符始终不释放,其个数迟早会达到上限。用于文件管理的内存结构没有被释放,也将造成资源的泄露。一个解决办法是通过"lsof"命令查看当前系统中存在哪些未正确关闭的文件。



文章转载自术道经纬,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论