[搬运工]VFS虚拟文件系统

上文Linux文件系统中简单提了下为什么要有VFS?但并不知道VFS是什么原理?仅仅知道VFS是用户进程与各种类型的Linux文件系统之间的一个抽象接口层。

根据如下两篇文章学习总结,很多地方都不懂,先搬运到这,如有侵权,请通知删除。

1) VFS(一)虚拟文件系统概述

2) VFS(二)读文件的过程中发生了什么

一切皆文件

Linux下一切皆文件,共有七种文件类型,使用ls -l命令查看文件类型,输出的第一个字符代表文件类型。

-:普通文件        d:目录

c:字符设备      b:块设备

s:套接字          p:管道          l:软链

不同类型的文件底层解释方式并不相同。比如普通文件和套接字的读写逻辑就不一样。

而相同类型的文件的底层解释方式也有可能不同。比如都是普通文件/var/log/message和/proc/1/cmdline的读写逻辑也不一样等。 因为这些文件使用不同的文件系统实例来管理,比如/var/log/message可能是由ext4来管理的,而/proc/1/cmdline是由procfs管理,而socket是由sockfs来管理。所以不同文件的解释权在于特定的文件系统实例

VFS虚拟文件系统

为了支持各种各样文件系统,Linux在用户进程和文件系统实例中间引入了一个抽象层,对不同文件系统的访问都使用相同的方法,并提供了文件的统一视图。

不同的文件系统的底层实现方式可能有很大的差异,但VFS并不关心这些。通过提供公共组件和统一框架,VFS对上层系统调用屏蔽了具体文件系统实现之间的差异性,为所有文件的访问提供了相同的API,并遵循相同的调用语义。

在内核源码fs目录下,可以找到很多常用的文件系统实现,比如ext4,xfs,sysfs等。

这张图清晰的画出,用户进程在用户空间userspace,VFS在内核空间kernelspace,同时文件系统也在内核空间

VFS操作文件

1.  打开文件(open函数)

在操作文件的时候,在用户态看到的是文件描述符fd(一个整数)。

应用程序(进程)使用标准库的open函数,每打开一个文件,都会分配返回一个新的fd,之后通过read/write/close/ioctl等函数来对fd进行操作。fd只在当前进程内有效。

在x86_64架构上open函数会执行syscall指令从用户态转换到内核态,调用VFS层,并最终调用到do_sys_open函数。

1.1) do_sys_open的流程

1.2) do_filp_open的流程

2.  打开文件(read函数)

在Linux中读文件时,先通过open()函数打开文件得到文件描述符fd,然后使用read()系统调用来读取文件的内容。

read()系统调用有三个参数:文件描述符,保存数据的缓冲区,读取长度,函数为ssize_t read(int fd, void *buf, size_t count)

read()系统调用在内核中对应的入口是sys_read,定义在fs/read_write.c中:SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)

sys_read会调用vfs_read,进入VFS层。读取文件的流程如下:

每个task都有文件描述符表fdt。索引是各个文件描述符,值是file指针。这个表在单进程多线程中是共享的,在多进程中也可以共享,也可以不共享。

和很多内核对象一样,fdt用了一个引用计数来表明有多少个task共享它。在访问fdt时,需要进行同步。每个file也有一个引用计数,因为file也是共享的。

当一个task在读文件时,另外一个task可以写这个文件,第三个task可以同时执行关闭这个文件。使用引用计数可以防止在读写文件时,文件不会被错误的关闭掉。

文件打开/读写时,将引用计数加1,关闭/读写完成时,将引用计数减1。引用计数为0时,才真正执行关闭动作。

在读写文件获取file指针时,fdt使用RCU锁来保护。这里还有个优化,就是fdt由单个task独占时,获取file指针就不需要锁了。

3.  通用读函数generic_file_read_iter

vfs_read会调用file->f_op->read_iter函数。

file->f_op在文件打开时,在do_dentry_open中赋值为inode->i__fop。而inode->i_fop在初始化inode时,在ext4_iget中赋值为&ext4_file_operations。

file_operations的结构如下:

struct file_operations {

...

ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);

ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

int (*iopoll)(struct kiocb *kiocb, bool spin);

...

int (*mmap) (struct file *, struct vm_area_struct *);

unsigned long mmap_supported_flags;

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *, fl_owner_t id);

int (*release) (struct inode *, struct file *);

        ...   

} __randomize_layout;

read_iter函数指针负责读取文件数据。大部分文件系统的读取过程,都将read_iter置为generic_file_read_iter来实现的。或者在read_iter中做一些简单的处理,然后再调用generic_file_read_iter。

对大部分文件系统来说,读取文件数据的流程相差无几,所以VFS提供了一些通用的文件操作函数。

generic_file_read_iter是通用文件操作函数中的一个读函数,generic_file_buffered_read的包装。generic_file_buffered_read从页缓存中获取数据,如果页缓存中没有,就去块设备中读取。

从块设备中读取数据是异步的,但在没有获取到数据前,task会进入睡眠,释放CPU。数据读取完毕后,会唤醒task,将数据拷贝到用户态的buffer。

generic_file_buffered_read在一个大的循环中,将线性的文件读转换为page读。

1).将文件的读写位置和读取长度转化为page tree的index。

2).根据index,使用find_get_page找到对应的page。

2.1).如果page不存在,就进行同步预读。同步预读成功后,再次使用find_get_page得到page。

2.2).如果预读关闭或者block拥塞,导致同步预读失败,那么会转向使用mapping->a_ops->readpage进行单页读取。

3).如果page设置了PG_readahead标记,则启动一个异步预读。

4).如果page在同步预读中分配的,那么会锁住page,并阻塞在和page关联的waitqueue上(page_wait_table的一个bucket)。

异步的块层IO结束后,IO完成处理函数会解锁该page,并唤醒之前在waitqueue上睡眠的task。但这里可能会唤醒多个task (thundering herd)。因为多个page (PageLocked pages and PageWriteback pages)可以在一个waitqueue上等待。

5).如果page是之前已经读取过的,那么判断page是否是最新的。如果不是,则使用mapping->a_ops->readpage再次读取。

6).拷贝page数据到用户空间。如果拷贝了足够的字节数,或者发生错误,或者收到kill signal,这里就不再循环,而是返回已经拷贝到用户空间的字节数。

7).循环读取page,回到第1)步继续执行。

4.文件预读

文件预读机制,假设文件会被顺序读取

如果程序打开文件读入第一页,那么它接下来会有很大概率会继续读取后面的页。而且文件系统也会为相邻的数据尽量分配相邻的块儿,所以顺序读能从中受益。大量的顺序读,通过预读,只产生少量的和底层硬件的交互,从而降低延迟。

但对于随机读,事情就变得不确定了。这时候预读可能就没什么帮助,甚至会引发性能下降。因为都进来的数据可能根本用不到,还占内存。

内核提供了一个参数,/sys/block /<devname>/queue/read_ahead_kb,用来控制设备预读的最大KB数。在顺序读场景中可以调大,在随机读的场景调小一些,然后根据反馈来做进一步的优化。

用户态的文件读和mmap映射文件导致的缺页处理中,都要调用预读函数,预读函数最终会汇总到ondemand_readahead上。在ondemand_readahead中,拿到和file上关联的一个file_ra_state结构,来进行预读控制。

文件进行预读时,会形成一个预读窗口{start, size, asyn_size}。

start指定窗口中开始预读的位置。size指定预读页数。async_size指定一个阈值,预读窗口剩余这么多页时,就开始异步预读。

ra_pages是窗口可能的最大值,和 /sys/block/<devname>/queue/read_ahead_kb的值对应。如果后者是4096,那么ra_pages就是1024。

如果程序从0开始顺序读文件,每次4k。那么在ondemand_readahead中,首先会调用get_init_ra_size初始化一个小的窗口,读入一定量的数据。

如果程序是顺序读,后续的顺序4k读会慢慢的扩大窗口,读入更多的数据,直到窗口达到最大值。

如果程序是随机读,导致窗口失效,那么就要重新初始化。如果遇到预读标记,但和之前的预读窗口不符,那么也要重新设置,以适应并发的随机读取。

程序从0开始顺序读文件,一共读5次,每次读4k:

1).第1次读4k,page不在cache中,进行同步预读,预读窗口初始化为 {0, 4, 3},读4页(0-3),第1页设置readahead标志

2).第2次读4k,page在cache中,命中readahead,进行异步预读,预读窗口扩大为 {4, 8, 8},读8页(4-11),第4页设置readahead标志

3).第3/4次读4k,page在cache中,不命中readahead

4).第5次读4k,page在cache中, 命中readahead,进行异步预读,预读窗口扩大为 {12, 16, 16},读16页(12-27),第12页设置readahead标志。

5. 通用块层的处理

ondemand_readahead会调用pagecache层的关键函数mapping->a_ops->readpages(在ext4中是ext4_readpages,进一步会调用ext4_mpage_readpages)。

在ext4_mpage_readpages中将page读转化为文件的block读。函数通过BIO来标识IO请求的多个段(通过bi_io_vec数组)。

每个biovec的数组项包含用于IO的page(bv_page),页内偏移(bv_offset)和IO大小(bv_len)。这些pages可以是不连续的,这简化了DMA的scatter/gather操作。

submit_bio是向块层提交bio的关键,最终该函数会使用make_request_fn将bio加入块设备的请求队列上。同时,IO调度层的工作也会在这里完成,通过指定的调度算法对IO进行排序和合并。

在IO完成后,块设备通过中断通知cpu。在中断处理函数中,触发BLOCK_SOFTIRQ。在软中断处理中,回调最终会触发bio->bi_end_io(对ext4来说是mpage_end_io),解锁之前在锁定的页面。

至此之前在该page的waitqueue上阻塞的task就可以继续执行了,从而是read函数返回,整个调用流程也就全部结束了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容