使用PageCache读取文件元数据块
Linux 2.4的最大贡献是统一的PageCache与BufferCache,准确来说,它是讲所有数据都保存在了PageCache中,但是仍然保留了BufferCache的接口,以供如superblocks,bitmap,inode table,block table等文件元数据读写的使用。也即,与块设备交互时,我们依然使用bh抽象,但是,bh不会有自己的空间,其使用的是PageCache中对应页的空间。具体文件系统内部使用时,会在页上提供Buffer head抽象,从而,具体文件系统可以使用block抽象。
那么问题来了,我们提到过,PageCache缓存的是文件页,它将每个文件的内容抽象为一个地址空间(address_space),并在此空间上读写,可以元数据并不在文件上啊?为了解决这个问题,内核将一个块设备也看作了一个文件,这个块设备上的数据是一个地址空间。于是乎,当我们需要读写superblock时,我们只需要读写这个特殊文件的第一个页的第一个块。
例子,ext2文件系统的read_super函数,最终会调用bread(dev, 1, block_size),熟悉BufferCache的同学应该了解,bread是BufferCache的一个非常重要的接口。虽然BufferCache的接口依然保留,但是其内部实现已经化归到PageCache上了。
struct buffer_head* bread(kdev_t dev, int block, int size)
{
struct buffer_head* bh;
bh = getblk(dev, block, size);
if (buffer_uptodate(bh))
return bh;
set_bit(BH_Sync, &bh->b_state);
ll_rw_block(READ, 1, &bh);
wait_on_buffer(bh);
if (buffer_uptodate(bh))
return bh;
brelse(bh);
return NULL;
}
对比之前的代码,之个函数可以说是毫无变化,但是,我们提到,BufferCache构建于PageCache之上,所以,这里的getblk是关键。默认的,它会直接查找BufferCache的hash表,如果存在,直接返回,如果不存在,它会调用grow_buffers(dev, block, size)来分配为此dev的block来分配Page,并创建配置buffer_head。具体过程如下(可以把getblk和grow_buffers一路看下去)。
- 如果bh已经在BufferCache中了,直接返回,否则
- 从PageCache中取出或者新建block所在页
- 锁PageCache
- 从dev的特殊inode的mapping中获取block所在的页
- 放锁PageCache
- 如果页不存在,分配一个新页,对PageCache加锁,将其放入PageCache
- 如果页存在,锁定它
- 注意,此时,页的引用计数为二,一是PageCache的,一是当前操作持有的
- 为页分配若干buffer_head,将buffer_head的data指向页上的空间(一般的,一个页是4k,因此,我们分配4个bh,将其data指针分别页上的对应区域)
- 将页上的bh link起来,并将页引用加一,此时,引用为三!
- 将页上的bh放入BufferCache的hash表中
- 解除页锁定
- 释放页引用:此时,页的引用为二,一是PageCache所持有的,一是BufferCache所持有的
至此,我们成功地将所有缓存空间都由PageCache统一化管理了,但是,我们依然保留有bh的接口,因为目前对块设备进行读写时,我们依赖使用bh抽象。BufferCache提供了对bh对象(类似一个句柄吧,其实际空间在PageCache的页上)的管理,bh对象不对单独出现,它一定属于某个具体的文件页,无论是文件系统文件的页,还是表现块设备的特殊的文件的页。
是不是觉得这很绕?没错,对于内核的改动是渐进的,我们无法一下子完成去掉BufferCache,因此这一方面涉及到块设备的改动,一方面涉及到所有具体文件系统的改动,内核维护者只能以这样一个渐进的方式来处理此问题。
文件地址空间操作的实现
不同具体文件系统对address_space_operations的实现有所不同,这里,我们将分析默认版本,即基于块设备的文件系统的实现。相关代码主要在fs/buffer.c中。
readpage
基于块设备的readpage实现是block_read_full_page,它的参数是一个page,即要读的页;另一个是get_block策略函数,即,将页所对应的文件块的逻辑块index,转换成块在块设备中的index。每个文件系统会提供不同的get_block策略函数,因属于文件系统对文件的空间分配的部分。
- 操作开始时
- 页的引用至少为二,一是PageCache的,一是执行操作的线程的
- 页处于锁定状态
- 如果页当前没有配置bh,为其创建bh(创建好后,引用会额外加一,表示此页已经被BufferCache使用了)
- 根据get_block策略,设置block的物理index
- 接下来的操作就没有什么了,因此主要是将bh提交给块设备层,进行读操作
writepage
注意,writepage只被mmap相关函数所使用,VFS读写时,使用的是另一套函数。其实现是block_write_full_page,第一个参数为page,第二个参数为get_block策略函数。
如果需要写的页在文件最后一个页之内,则调用__block_write_full_page,其过程与block_read_full_page类似。
如果需要写的页在文件正好为文件的最后一个页,我们还需要做很多额外的工作。因为实际上,最后一个页可能不完整。我们知道,文件系统是以块为粒度管理空间的,但是VFS读写时,是以页为粒度的,这就意味着,如果读文件最后一个页,它可能只有一个块。此时,操作退化成prepare_write与commit_write。
prepare_write/commit_write
一个困扰我们的问题是,writepage与prepare_write/commit_write有什么区别?区别在于,writepage写的是一整页,而prepare_write/commit_write写得是部分页。这也是为什么前者用于mmap,后者用于VFS的原因。
prepare_write/commit_write的参数都是(page, from, to, get_block_t),没错,它也需要一个get_block策略来将逻辑块index转换成物理块index。
先看block_prepare_write:
- 如果页无bh,为其准备bh
- 对于[from, to)范围内涉及到的partial bh(即bh所对应的空间不完全被[from, to)所包含),,发起读请求,以确保之后的写是对齐的。
- 等待读完成
再看block_commit_write:
- 注意,执行commit_write时,要写的内容已经在page上了,我们需要commit此page上的最新的写
- 看了下,没有做什么大不了的事情,不分析了
BufferCache与PageCache的联系与区别
之前提到,一个页一旦被分配了bh,它的引用计数会加一,那么问题来了,这个引用计数是什么时候减少的呢?代码见kswapd,简单来说,当kswapd尝试要回收一个页,但发现页有bh,它会检查bh是为非lock非dirty,如果页的所有bh都是非lock非dirty,则回收bh,页的引用减少一。