赞
踩
【推荐阅读】
内核为块设备提供了两种通用的缓存方案:
缓冲区曾经是块设备进行I/O操作的传统方法,目前只用于支持很小的读取操作,对于块传输的标准数据结构变为struct bio,这种方式可以合并统一请求中后续的块,加速处理,但是对于单个块的操作,缓冲区仍然是首选,例如经常按块读取元数据的系统。
很多场合下,页缓存和块缓存结合使用(一个缓存的页在写操作期间划分为不同的缓冲区,在更细的粒度识别出被修改的部分,在数据写回时,只需要回写被修改的部分,不需要整页传输)
Linux采用基数树的数据结构管理页缓存中的页,如下图
树的根有一个简单的数据结构表示,包含了树的高度(所包含节点的最大层次数目)和一个指针,指向组成树的第一个节点的数据结构
树的节点具备两种搜索标记,二者用于指定给定页当前是否为脏(即页的内容和后备存储器的数据不同)或该页是否正在向底层块设备回写。标记会一直向上设置到根节点,如果某个层次 n+1 的节点设置了某个标记,那么 n 层次的父节点也会设置该标记。好处是内核可以判断在某个范围内是否有一页或多页设置了某个标记位
由于页缓存的存在,写操作不是直接对块设备进行,而是在内存中进行,修改的数据首先被收集起来,然后被传输到更低的内核层,在那里对写操作进一步优化,具体的优化过程详见 通用块设备层。这里从页缓存的视角来看,需要确定何时回写?
内核提供如下几种同步方案:
为管理可以按整页处理和缓存各种不同对象,内核使用了**“地址空间”**抽象,将内存中的页和特定的块设备关联起来,每个地址空间都有一个“宿主”,所谓其数据来源,一般用inode表示
一般修改文件或者按页缓存的对象时,只会修改页的一部分,为节约时间,在写操作期间,会将缓存中的每一页划分为较小的单位,称为缓冲区,回写过程中以缓冲区为单位来操作
Linux早期版本只包含块缓存,用于加速文件操作和系统性能,底层块设备的块缓存存在内存的缓冲区中,可以加速读写(实现部分包含在fs/buffers.c中)
与内存页相比,块比较小而且长度可变(依赖于使用的块设备或者文件系统)
文件系统在处理元数据时,一般会使用块缓存;而裸数据的传输则按页进行
缓冲区的实现基于页缓存,Linux 2.6之前,缓冲区使用缓冲区头buffer head结构实现,在2.6之后,不再使用缓冲区头结构,而是使用bio结构
地址空间建立了缓存数据与后备存储器之间的关联,实现了两个单元之间的转换机制
地址空间的基础是address_struct结构,定义如下:
主要成员:
地址空间与内核其他部分的关联如下图
page_cache_alloc用于为一个即将加入页缓存的新页分配数据结构,加上后缀_cold的函数是获取一个冷页
内核还提供了另一个可选的参数add_to_page_cache_lru,他首先调用add_to_page_cache向地址空间的页缓存添加一页,然后使用lru_cache_add函数将该页加入到系统的LRU缓存
使用基数树判断给定页是否已经缓存,使用find_get_page实现该功能
其中radix_tree_lookup用于查找位于给定偏移量的页,在找到页之后,page_cache_get将页的引用计数加1
内核经常要在页上等待,直至其状态改变为预期值。例如,数据同步时需要确保对于某页的回写已经结束,此时内存页的内容和底层块设备是相同的。处于回写过程中的页会设置PG_writeback标志位
内核提供了wait_on_page_writeback函数,用于等待页的该标志位清除
wait_on_cahe_bit安装一个等待队列,进程可以在上面睡眠,直至PG_writeback标志位清除
另外也有等待页解锁的需求,使用函数wait_on_page_locked实现
内核在块设备和内存之间传输数据时,相关的算法和数据结构都是以页为基本单位,而逐个缓冲区/块的传输会对性能产生负面影响。因此,在再重新设计块层的过程中,内核版本2.5引入BIO,来替代缓冲区,用于处理与块设备的数据传输。内核添加了4个函数,来支持读写一页或多页
其中,writeback_control用于精确控制回写操作的选项
这4个函数共同之处:都是构建一个BIO实例,用于对块层进行传输
以mpage_readpages为例,该函数需要nr_pages个page实例,以链表的形式通过参数pages传递进去,mapping是相关的地址空间,get_block用于查找匹配的块地址
该函数首先遍历所有的page实例,在循环的每一遍,首先将该页添加到地址空间相关的页缓存中,然后创建一个bio请求,从块层读取所需的数据
在do_mpage_readpage建立bio请求时,会包含此前各页的BIO数据,以构造一个合并的请求(将几页的读取合并到一个请求,而不是每页一个请求)
如果在循环结束时,do_mpage_readpage留下一个未处理的BIO请求,则提交该请求
页缓存预读不是由页缓存独立完成的,还需要VFS和内存管理层的支持
预读在内核的几处都有涉及
这两个函数在读取文件时会调用,其中通过系统调用read()正常读文件时,一般会调用do_generic_mapping_read函数,而内存映射时第一次读取文件内容时,会触发缺页中断调用filemap_fault函数
代码详细实现见 深入Linux内核架构 P458
下面以do_generic_mapping_read为例,来考察预读的具体过程
假定进程已经打开了一个文件,准备读取第一页,该页尚未读入页缓存,此时不会只读入一页,而是顺序读取多页,内核调用page_cache_sync_readahead读取一行中的8页(数字8是具体说明),第一页对于do_generic_mapping_read来说是立即可用的,而在实际需要之前就被读入页缓存的页,处于预读窗口中
之后进程继续读取接下来的页,在访问第6页(6是举例说明)时,内核检测到在该页设置了PG_Readahead标志。此时触发了一个异步操作,会在后台读取若干页(由于还有两页可用,所以不需要同步读取,但在后台进行的I/O操作需要确保进一步读取文件时,相关页已经读入页缓存)。page_cache_async_read函数负责发出异步读请求,它又会将窗口中的一页标记为PG_Readahead,在进程遇到该页时,又会触发异步读取,以此类推
最重要的问题在于预测预读窗口的最优长度。因此,内核会记录每个文件上一次的设置,使用file_ra_state关联到每个file实例
主要成员:
预读机制的实现涉及如下几个函数
其中,ondemand_readahead例程负责实现预读策略(即判断预读多少当前不需要的页),在确定预读窗口的长度之后,调用ra_submit,将技术性问题委托给__do_page_cache_readhead函数,其中页是在页缓存中分配的,而后由块层填充
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。