Linux中的VFS实现 [二]

本文深入探讨了Linux虚拟文件系统(VFS)的实现细节,包括如何通过路径名找到inode、dentry的作用及其缓存机制、vfsmount和path的定义、以及文件打开、读写和关闭的操作流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux中的VFS实现 [二]

Linux中的VFS实现[一]

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

【打开文件】

一个进程想要读取或是写入一个文件,必须先建立和文件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是一个内存结构,并没有对应的磁盘数据。

struct dentry {
struct dentry d_parent; / parent directory /
struct list_head d_child; /
child of parent list /
struct list_head d_subdirs; /
our children */

}
dentry并不等同于directory,但确实和directory存在着相当的关系。虽然directory被视作文件,但directory本身需要一些特殊操作,比如路径名的查找,而dentry概念的提出就是为了使查找的过程更加便利。

比如在上图所示的这样一个结构中,inode一共有4个,包括2个目录"/", “foo"和2个普通文件"bar”, “bar2”。而dentry有3个,一个将"bar"链接到"foo",一个将"bar2"链接到"foo",还有一个将"foo"链接到根目录节点。

查找的起点通常是根目录("/")或者当前目录,一级一级往下。对".“表示的同级目录就跳过解析,对”…“表示的上级目录就使用"dentry->d_parent”。有访问"parent"的权限,才能进入其对应的目录,然后校验了"child"的访问权限后,将其转变为下轮查找的"parent"。如果遇到了symbol link,就跳到其指向的目录继续查找。

通常会觉得一个文件只有一个路径是吧?别忘了上文介绍的hard link,对一个inode每增加一个hard link,该inode的路径指向就增加一个。

struct inode d_inode; / Where the name belongs to /
struct hlist_node d_alias; /
inode alias list */
因此,一个inode会对应多个dentry(通过"i_dentry"链表组织),而一个dentry只会对应一个inode(即"d_inode")。

一层一层的解析路径是非常耗时的,如果每次打开文件都重新解析一次路径,实在是效率堪忧。所以啊,已经解析过的路径会存在dentry cache(简称dcache)里,关于dcache的详细介绍将放在这篇文章中。

vfsmount 和 path
对于一个文件系统来说,其挂载点也是一个路径,因此将表示路径的"dentry"和表示文件系统实体的"super_block"结合起来,就成了描述“挂载点”的数据结构"vfsmount"。

struct vfsmount {
struct dentry mnt_root; / root of the mounted tree */
struct super_block mnt_sb; / pointer to superblock */

}
而对于一个非文件系统挂载点的普通路径来说,其必然处于一个文件系统的子路径中,因而诞生了一个名为"path"的数据结构,它既拥有路径"dentry"本身的信息,还包含了其所在文件系统的挂载点"vfsmount"的信息。

struct path {
struct vfsmount *mnt;
struct dentry *dentry;
}
比如在"/mnt/opt"目录下挂载一个新的文件系统,而后在该目录下创建一个名为"foo"的文件夹,进入这个文件夹并创建一个名为"bar"的文件,那么对于这个新建文件的路径来说,其"dentry->d_name"是"/mnt/opt/foo/bar",而"vfsmount"中的"dentry->d_name"则是"/mnt/opt"。

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"命令查看当前系统中存在哪些未正确关闭的文件。

参考:

https://www.kernel.org/doc/Documentation/filesystems/vfs.txt
Linux内核文件描述符表的演变
http://www.fieldses.org/~bfields/kernel/vfs.txt

原创文章,转载请注明出处。

使用freemarker生成word ,并集成struts2 同时生成及下载文档 资料附有Java源代码和自己总结的使用说明及注意事项 大至预览如下: 1、用word编辑好模板 普通字符串替换为 ${string} 表格循环用标签 姓名:${user.userName} , 性别:${user.sex} 2、将word模板另存为xml格式 3、将xml模板文件后缀名改为.ftl 4、编辑ftl文件 注意 编辑word模板时,${string} 标签最好是手动一次性输入完毕,或者使用记事本统一将整个${string}编辑好之后,粘贴至word里边。 也就是说,不要在word里首先打完 ${ } 之后,又从其它地方把 string 字符串粘贴至 { } 之间,这样在 word 转化为 xml时,解析会有问题,freemarker解析时,会报错。 /** * @Desc:生成word文件 * @Author:张轮 * @Date:2014-1-22下午05:33:42 * @param dataMap word中需要展示的动态数据,用map集合来保存 * @param templateName word模板名称,例如:test.ftl * @param filePath 文件生成的目标路径,例如:D:/wordFile/ * @param fileName 生成的文件名称,例如:test.doc */ @SuppressWarnings("unchecked") public static void createWord(Map dataMap,String templateName,String filePath,String fileName){ try { //创建配置实例 Configuration configuration = new Configuration(); //设置编码 configuration.setDefaultEncoding("UTF-8"); //ftl模板文件统一放至 com.lun.template 包下面 configuration.setClassForTemplateLoading(WordUtil.class,"/com/lun/template/"); //获取模板 Template template = configuration.getTemplate(templateName); //输出文件 File outFile = new File(filePath+File.separator+fileName); //如果输出目标文件夹不存在,则创建 if (!outFile.getParentFile().exists()){ outFile.getParentFile().mkdirs(); } //将模板和数据模型合并生成文件 Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile),"UTF-8")); //生成文件 template.process(dataMap, out); //关闭流 out.flush(); out.close(); } catch (Exception e) { e.printStackTrace(); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值