2005-11-25 11:50mm/filemap.c
page cache,buffer cache,lru cache,swap cache
第一部分-->综述:
首先概要介绍page cache和inode, page cache 和buffer cache,page cache
和swap cache,page cache和lru cache, buffer cache 和lru的相互关系.
0.page cache, buffer cache和lru cache的组成
filemap.c开头定义了一张hash表,是一个一维数组,每一项是一个指针,此指
针指向page结构.进入此hash表的page页面基本上就进入了page cache:
struct page **page_hash_table;
page cache 还包括 struct address_space 内的几个队列(inode queue):
struct list_head clean_pages;
struct list_head dirty_pages;
struct list_head locked_pages;
fs/buffer.c也有类似的hash数组,那是buffer cache.
mm/page_alloc.c定义了两个lru队列:
struct list_head active_list;
struct list_head inactive_dirty_list;
加上zone_t机构的
struct list_head inactive_clean_list;
构成lru cache.
page结构为这些cache 链表准备了几个成员变量:
typedef struct page {
struct list_head list;
struct address_space *mapping;
unsigned long index;
struct page *next_hash;
atomic_t count;
unsigned long flags;
struct list_head lru;
unsigned long age;
wait_queue_head_t wait;
struct page **pprev_hash;
struct buffer_head * buffers;
void *virtual;
struct zone_struct *zone;
} mem_map_t;
1. page cache 和 inode
page cache 在代码中又称 inode page cache, 足以显示page cache 和inode
紧密关联.加入page cache 和加入inode cache是同一个意思.加入page cache
意味着同时加入page cache hash表和inode queue(也建立了page和addr sapce
的关系). 见函数add_to_page_cache_locked,__add_to_page_cache即可取证.
从page cache 删除在程序中叫__remove_inode_page,再次显示inode 和page
cache的"一体化".
加入/离开page cache还涉及到如下几个函数:
add_page_to_hash_queue
add_page_to_inode_queue
remove_page_from_inode_queue
remove_page_from_hash_queue
__remove_inode_page
remove_inode_page
add_to_page_cache_locked
__add_to_page_cache
仅罗列函数add_page_to_hash_queue,以示完整:
static void add_page_to_hash_queue(struct page * page, struct page **p)
{
struct page *next = *p;
*p = page;
page->next_hash = next;
page->pprev_hash = p;
if (next)
next->pprev_hash = &page->next_hash;
if (page->buffers)
PAGE_BUG(page);
atomic_inc(&page_cache_size);
}
2. page cache 和buffer cache
page 不会同时存在于 buffer cache 和 page cache.add_page_to_hash_queue
将此思想显露无余.buffer_head 定义在fs.h,和文件系统有着更为紧密的关系.
从文件读写角度看buffer cache缓存文件系统的管理信息像root entry, inod等,
而page cache缓存文件的内容.看看read 一个普通文件的流程:
sys_read ->file->f_op->read(以ext2为例)
+
ext2_file_operations
+
generic_file_read->do_generic_file_read(this file,filemap.c)
+
从page cache寻找指定页__find_page_nolock
+
如果没有找到则从文件读取mapping->a_ops->readpage
+
ext2_aops
+------<<<---------------<------+
ext2_readpage->block_read_full_page(fs/buffer.c,buffer cache)
注意函数block_read_full_page,虽然位于buffer.c,但并没有使用buffer
cache. 但是确实使用了buffer:只是再指定page上创建buffer提交底层驱动读
取文件内容.这个流程有两个值得注意的地方,一是普通file的read通过page
cache进行,二是page cache读取的时候不和buffer cache进行同步,三是page
cache的确使用了buffer,不过注意,buffer 不是buffer cache.
mmap也使用page cache 缓冲文件,流程如下:
do_mmap->ext2_file_operations
+
generic_file_mmap
+
以共享映射为例file_shared_mmap
+
filemap_nopage(filemap,this file)先找page cache
+
ext2_aops 否则从文件读取
+
block_read_full_page
如果打开象/dev/hda1这种设备文件,其内容缓存于buffer cache,流程如下:
def_blk_fops
+
block_read(fs/block_dev.c)
+----->先用函数getblk从buffer cache查找
+----->否则使用ll_rw_block从驱动读取
注意到block_read和block_read_full_page都采用提交驱动的方式读取数据,
验证了page cache和buffer cache间的确没有数据同步.
buffer cache 提供了getblk和bread两个接口,从buffer cache获取数据
搜索调用者的话,可以看到ext2文件系统从buffer cache获取的内容没有普通
文件的数据,而是inod,dentry等数据.
3.swap cache和page cache
swap cache是一个特殊的page cache,不同之处在于address_space是swapper
_space.和page cache一样也挂入page cache hash queue.加入swap space的函
数add_to_swap_cache其实就是调用add_to_page_cache_locked.
4.page cache 和 lru cache
进入page cache的页面必然加入lru cache(lru_cache_add).通过函数
__add_to_page_cache和add_to_page_cache_locked即可确信这一点.从page
cache 删除的时候也同时从lru cache删除. 搜索对__lru_cache_del的调用,
即可发现filemap,shmem,swap cache在使用到page cache的时候都是如此操作.
注意,加入lru cache则不一定加入page cache,如 5)所述的buffer cache.
顺便述说一下lru cache相关的几个kthread和其大致作用:
*****kswapd (mm/vmscan.c)
+---->do_try_to_free_pages (如果内存已经不够用)
+-->page_launder
| +-->扫描 <inactive_dirty_list>
| +-->对dirty页启动回写(包括mapping和buffer cache)
+-->refill_inactive
+-->refill_inactive_scan
+-->扫描<active_list>,选择合适页面移入
<inactive_dirty_list>
+-->swap_out,对进程启动页面换出
+-->try_to_swap_out将选中页面放入
<inactive_dirty_list>
+----->refill_inactive_scan
*****kreclaimd(mm/vmscan.c)
+----->遍历每个zone 用reclaim_page
扫描zone->inactive_clean_pages,找出可以释放的页面
脱离lru cache
+----->对reclaim_page找到的页面补充到buddy系统
*****bdflush
+---->flush_dirty_buffers (提交buffer cache到驱动)
+----->如页面短缺,进行page_launder
5.buffer cache和lru队列
buffer cache 的确也使用了lru队列,grow_buffers调用lru_cache_add将
页面加入lru队列.但是却没有加入到page cache.(请阅读代码)
kreclaimd->reclaim_page将会尝试回收clean 页面到zone的buddy系统,如
果page->buffers不空,代表page被buffer cache 使用,那么reclaim_page只是将
页面转移到inactive_dirty_list.当reclaim_page发现buffer cache 的页面可
以回收时,因为此种页面不在page cache也不在swap cache, 只是从lru摘除,然
后直接释放.
buffer cache如此使用lru cache,作为自己的垃圾回收方式.
page_launder处理inactive_dirty_list将页面写入"硬盘",使页面可以释放
或者放入inactive_clean队列(大致描述).page_launder对buffer cache使用
的页面做特殊处理
page_launder() ------------>mm/vmscan.c
if (page->buffers) {
...
try_to_free_buffers
...
}
try_to_free_buffers是buffer cache提供给lru的函数,buffer cache自己从不
使用,这证实了buffer cache的确利用lru cache回收内存.
第二部分 ---> buffer cache vs page cache(page cache的演化)
在2.2x时期,page cache和buffer cache是两套cache系统,之间有同步.但是
linux不保证每个版本都如此.
如果现在/dev/hda1是根,如果hda1上有文件a.txt用dd dump /dev/hda1能够
得到和open a.txt一样的结果.
(见2.22:do_generic_file_read->inode->i_op->readpage**generic_readpage->
brw_page)
到了2.4.x事情已经变得不是这样了,dd if=/dev/hda1 从buffer cache中获取
数据,open打开的普通文件缓冲到page cache,两者没有任何同步机制(meta data
还是一致的). 合适的次序下,得到的结果不能保证正确性.
当然dump一个已经mount的,"live file system"是个愚蠢的做法,我们只是拿
来讨论问题.
到了2.5,文件的meta data也移到了page cache,事情进一步复杂了.在2.6的内
核中page cache和buffer cache进一步结合,从此buffer cache 消失,只有page
cache了. buffer cache退化为一个纯粹的io entry.随了linus的心愿.
可以看看linus的讨论
http: 在2.4中buffer cache自己维护了一套类似page cache和lru队列的机制,对
buffer cache做lru 缓冲处理,的确不是一个什么好东西.
第三部分---> mm/filemap.c
通过上面的讨论,已经涉及了本文件的诸多函数,这里对已经有说明的文件
一笔带过,对感兴趣的,做个分析注解.
头六个函数就不多说了,见上面的分析.
(1) page cache 初始化
void __init page_cache_init(unsigned long mempages)
{
unsigned long htable_size, order;
htable_size = mempages;
htable_size *= sizeof(struct page *);
for(order = 0; (PAGE_SIZE << order) < htable_size; order++)
;
do {
unsigned long tmp = (PAGE_SIZE << order) / sizeof(struct page *);
page_hash_bits = 0;
while((tmp >>= 1UL) != 0UL)
page_hash_bits++;
page_hash_table = (struct page **)
__get_free_pages(GFP_ATOMIC, order);
} while(page_hash_table == NULL && --order > 0);
printk("Page-cache hash table entries: %d (order: %ld, %ld bytes)\n",
(1 << page_hash_bits), order, (PAGE_SIZE << order));
if (!page_hash_table)
panic("Failed to allocate page hash table\n");
memset((void *)page_hash_table, 0, PAGE_HASH_SIZE * sizeof(struct page *));
}
(2) TryLockPage,lock_page和UnlockPage
static inline int sync_page(struct page *page)
逻辑简单,调用mapping->a_ops->sync_page(page),对于ext2就是ext2_aops
->block_sync_page->run_task_queue(&tq_disk)(fs/buffer.c).让磁盘有更多
机会运行回写,读入等任务.提供给 ___wait_on_page,__lock_page使用.
void ___wait_on_page(struct page *page)
{
struct task_struct *tsk = current;
DECLARE_WAITQUEUE(wait, tsk);
add_wait_queue(&page->wait, &wait);
do {
sync_page(page);
set_task_state(tsk, TASK_UNINTERRUPTIBLE);
if (!PageLocked(page))
break;
run_task_queue(&tq_disk);
schedule();
} while (PageLocked(page));
tsk->state = TASK_RUNNING;
remove_wait_queue(&page->wait, &wait);
}
也没有什么可以多说的,等待页面解锁时给页面同步相关的task queue多些运行
时间. void lock_page(struct page *page)和static void __lock_page(struct
page *page)同此函数.
include/linux/mm.h定义了
#define UnlockPage(page) do { \
smp_mb__before_clear_bit(); \
if (!test_and_clear_bit(PG_locked, &(page)->flags)) BUG(); \
smp_mb__after_clear_bit(); \
if (waitqueue_active(&page->wait)) \
wake_up(&page->wait); \
} while (0)
并且注释也说明了两个barrier的作用,
当用
TryLockPage
......
UnlockPage
组成一个临界区的时候,第一个barrier保证test_and_clear_bit在
test_and_set_bit之后执行,第二个barrier保证test_and_clear_bit和访问
wait_queue的次序.
问题是如何使用lock_page, UnlockPage,使用时机是什么?内核注释为"在进
行page上的IO操作时必须lock_page",这种解释有些简略.正在进行io的页面有如
下特征(几个典型情况):
1)如果页面归user space的进程使用,肯定是swap cache在进行io操作,并且页
面已经从用户的页表断开.
2)如果是user task进行文件读写操作,启动io的页面是page cache(normal file)
或者buffer cache.
3)如果是mmap,读写亦通过page cache进行.
4)首先page io在大部分情况下是一个异步操作,kernel不会"停下来"等待磁盘
操作的完成. 如典型的page fault需要换入时,新分配一个页面,加入swap
cache,启动io,最后当前进程wait on page.有可能内核处理swap的几个线程
会访问到此页,此种情况下需要进行互斥操作,不能在一个页面上启动两个io
操作.
5)或者SMP的情况下,一边进行io换入,另一个cpu也可以进行lru操作.
我相信作者一开始的时候准备用page lock这个机制防止对page io的重入.
但是此锁还同步了更多的东西:
看加入swap cache的情况:
void add_to_swap_cache(struct page *page, swp_entry_t entry)
{
unsigned long flags;
#ifdef SWAP_CACHE_INFO
swap_cache_add_total++;
#endif
if (!PageLocked(page)) BUG(); ..................
}
为何加入page cache需要上锁?看下面这个函数
void add_to_page_cache_locked(struct page * page, struct address_space
*mapping, unsigned long index)
{
if (!PageLocked(page))
BUG();
page_cache_get(page);
}
恩,对页面的引用计数增一,想一想还操作了page->mapping.所以我的结论是,在
以下情况下需要page lock:
1.对page进行io操作
2.某些特定目的情况下操作page->mapping和page引用计数的情形
为了验证这个结论,搜索对lock_page的引用,绝大多数在进行page io操作,还有
部分处理加入/离开page cache,这些容易理解. 然后挑一个例子看看为什么也
使用了lock_page,先看一个filemap.c中的函数
struct page * __find_lock_page (struct address_space *mapping,
unsigned long offset, struct page **hash)
{
struct page *page;
repeat:
spin_lock(&pagecache_lock); page = __find_page_nolock(mapping, offset, *hash);
if (page) {
page_cache_get(page);
spin_unlock(&pagecache_lock);
lock_page(page);
if (page->mapping)
return page;
UnlockPage(page);
page_cache_release(page);
goto repeat;
}
spin_unlock(&pagecache_lock);
return NULL;
}
使用page lock的原因已经写入注释,此函数返回一个保证还在page cache的页,
并增加页面引用计数,可以直接拿来使用,如shmem_nopage.总之,如果你要保证
page->mapping有效的话,必须lock_page然后进行判断,内核多处如此使用.
接着分析一个特殊的例子
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, pte_t *page_table, pte_t pte)
{
struct page *old_page, *new_page;
old_page = pte_page(pte);
if (!VALID_PAGE(old_page))
goto bad_wp_page;
switch (page_count(old_page)) {
case 2:
if (!PageSwapCache(old_page) || TryLockPage(old_page))
break;
if (is_page_shared(old_page)) {
UnlockPage(old_page);
break;
}
UnlockPage(old_page);
case 1:
flush_cache_page(vma, address);
establish_pte(vma, address, page_table, pte_mkyoung(pte_mkdirty(pte_mkwrite(pte))));
spin_unlock(&mm->page_table_lock);
return 1;
}
................
}
这个地方注释详尽,为了避免其他执行流从swap cache(only swap)共享此页
面,对页面加锁.但解锁之后设置pte可写是否正确呢?(解锁了,其他人即可共享啊)
我认为:
1)即使加锁后使pte可写,也无济于事,因为其他执行流照样可共享此页.
2)其他执行流共享此页后,不可能直接容许写,但到COW处理,重入此函数后
引用计数大于2,必须copy. 故不会出错.
3)如果计算是否是共享页面时不加锁则有可能两个进程同时拥有对此页面的
写权限.
(不能够是如此复杂的解释,到底应该怎样理解同步与互斥?2.22的确简单,这里有个
smp的大锁,lock kernel)这个锁锁定了一个临界区,保证计算一个确定的状态,同
时保证这个函数重入后不会的到相同的计算结果。
另一个类似函数是
static int do_swap_page(struct mm_struct * mm,
struct vm_area_struct * vma, unsigned long address,
pte_t * page_table, swp_entry_t entry, int write_access)
{
...........
lock_page(page);
swap_free(entry);
if (write_access && !is_page_shared(page))
pte = pte_mkwrite(pte_mkdirty(pte));
UnlockPage(page);
set_pte(page_table, pte);
...............
return 1;
}
(3) some func
static inline void set_page_dirty(struct page * page)
+
__set_page_dirty :标记页面为dirty,调整页面在page cache中(mapping)队列
的位置,并标记相关inode节点为dirty状态.调用者保证page在page cache之中.
void invalidate_inode_pages(struct inode * inode):
好像没有人用,正好也不看了.
(4)file truncate related
truncate_inode_pages ( service entry for file truncate in filemap.c)
+--->truncate_list_pages
+-->truncate_partial_page
+-->truncate_complete_page
这组函数和系统调用 truncate 相关(truncate file to specified len).入口
在 fs/open.c
asmlinkage long sys_truncate(const char * path, unsigned long length)
{
return do_sys_truncate(path, length);
}
经过一系列的函数周转到do_truncate->notify_change->inode_setattr(ext2文
件系统没有提供setattr,采用通用逻辑)->vmtruncate,最终利用truncate_inode
_pages清除page cache中相关的缓冲数据. 关于truncate不想再多说,只来看看:
static int truncate_list_pages(struct list_head *head, unsigned long
start, unsigned *partial)
{
.........
while (curr != head) {
unsigned long offset;
page = list_entry(curr, struct page, list);
curr = curr->next;
offset = page->index;
if ((offset >= start) || (*partial && (offset + 1) == start)) {
if (TryLockPage(page)) {
page_cache_get(page);
spin_unlock(&pagecache_lock);
wait_on_page(page);
page_cache_release(page);
return 1;
}
page_cache_get(page);
spin_unlock(&pagecache_lock);
.........
}
}
return 0;
}
(5)fsync, fdatasync
这两个系统调用将内核缓冲的文件数据同步到磁盘.系统调用的入口在buffer.c
sys_fsync,sys_fdatasync.区别在于sys_fsync将meta data也刷新到磁盘(atime
等),而sys_fdatasync只刷新"文件内容".两个系统调用都不保证包含他们的上级
目录的同步.如果需要,要明确的对对应目录调用fsync.
filemap.c中相关的函数是filemap_fdatasync,filemap_fdatawait.其作用是同
步page cache中的dirty页(mapping->dirty_pages)到磁盘.而inode meta data的
同步依赖于特定的文件系统(见buffer.c sys_fsync,注意page cache无meta数据).
filemap_fdatasync遍历dirty页面,提交系统驱动处理(mapping->a_ops->writepage
对ext2文件系统来讲就是ext2_aops -> ext2_writepage ->block_write_full_page
此函数也在buffer.c,请阅读此函数,注意page上的buffers并没有加入buffer cache)
filemap_fdatawait等待驱动完成page io操作.
不再列出相关代码,阅读时候体会一下加锁和增加页面引用计数的顺序.
(6)page cache: 数据读入
函数static inline int page_cache_read(struct file * file, unsigned
long offset)分配一个页面并提交磁盘驱动读入文件制定偏移的内容到page
cache, 同时考虑到了其他执行流先于我们读入的情况.仔细阅读此函数调用的
add_to_page_cache_unique->__add_to_page_cache,注意在__add_to_page_cache
中对page加了锁. 这个锁比较隐蔽,还以为page_cache_read在未加锁的情况下
启动了page io呢.
这是一个异步读取函数,应用于预读和其他需要异步读取的函数.
函数read_cluster_nonblocking调用page_cache_read异步读区整个cluster.
read_cache_page从mapping读取指定的内容到页面,所不同的是使用指定的方
式更新页面的内容.同样考虑到了各种race的情况.他使用用的函数有点拗口,来
看看:
static inline
struct page *__read_cache_page(struct address_space *mapping,
unsigned long index,
int (*filler)(void *,struct page*),
void *data)
{
struct page **hash = page_hash(mapping, index);
struct page *page, *cached_page = NULL;
int err;
repeat:
page = __find_get_page(mapping, index, hash);
if (!page) {
if (!cached_page) {
cached_page = page_cache_alloc();
if (!cached_page)
return ERR_PTR(-ENOMEM);
}
page = cached_page;
if (add_to_page_cache_unique(page, mapping, index, hash))
goto repeat;
cached_page = NULL;
err = filler(data, page);
if (err < 0) {
page_cache_release(page);
page = ERR_PTR(err);
}
}
if (cached_page)
page_cache_free(cached_page);
return page;
}
从语义上讲函数read_cache_page应该是"读取到page cache".
还有一个逻辑上比较类似的函数grab_cache_page,此函数只是锁定一个指定
区间的页面,返回给调用者.而不管是否update,也不提交给驱动读取页面.
(7)普通文件读写和预读
generic_file_read 负责普通文件的读取(系统调用read),即可以使用page
cache的一切文件系统。
系统调用read在文件fs/read_write.c中
asmlinkage ssize_t sys_read(unsigned int fd, char * buf, size_t count)
sys_read调用文件系统提供的read,我们以ext2为例就是
struct file_operations ext2_file_operations = {
llseek: ext2_file_lseek,
read: generic_file_read,
write: generic_file_write,
ioctl: ext2_ioctl,
mmap: generic_file_mmap,
open: ext2_open_file,
release: ext2_release_file,
fsync: ext2_sync_file,
};
一般来讲,文件读取通过 generic_file_read来进行.generic_file_read建立
一个read descriptor,然后交给do_generic_file_read,做真正的读取工作.调用
这个函数的时候传递了一个函数指针:file_read_actor,其作用是复制page内指定
偏移和长度的数据到用户空间.
先看看do_generic_file_read要处理的几个问题:
1) page cache: 普通文件缓存于内核的page cahce,引发linux读写文件时将
文件看作一个以page size为单位的逻辑页面.读取文件就是将用户读取的
位置和大小转换成逻辑的页面,从page cache找到内存对应的页面,并将内
容复制到用户缓冲区. 如果未缓存此文件的对应内容,就要从磁盘上的对应
文件以文件系统自己的方式读取到内存页面并将此页面加入到page cache.
2) 上面一条是将文件流切割成page 页,然后block_read_full_page(通常是
这个函数)还会将页面切割为此文件独立的线性block num,最后通过具体的
文件系统将文件线性的block转换成磁盘线性的block(硬件block num?).
3) 预读: 用户读取文件的时候内核极力猜测用户的意图,试图在用户使用数据
前就将数据准备好. 这样可以早期启动磁盘的io操作,以dma方式并行处理.
并且成批的io操作可以提高吞吐量.linux内核的预读对于顺序读取模式应改
很有效果.
4) 隔离各种文件系统读取文件内容的方式. 就是通过给定文件关联的inode,利
用函数指针mapping->a_ops->readpage读取文件内容. 具体的例子可以看ext2
struct address_space_operations ext2_aops = {
readpage: ext2_readpage,
writepage: ext2_writepage,
sync_page: block_sync_page,
prepare_write: ext2_prepare_write,
commit_write: generic_commit_write,
bmap: ext2_bmap
};
ext2_readpage直接调用block_read_full_page(page,ext2_get_block).就
是将文件内线性编址的page index 转换为文件线性编址的block(逻辑块).
其中 ext2_get_block(*inode,iblock,*bh_result,create)将文件的逻辑块
号转换为块设备的逻辑块号(块设备上线性编址的block num),最后提交设备
驱动读取指定物理块.(驱动将设备块号转换为扇区编号..^_^)读写文件页面
的过程仅做此简析,以后分析buffer相关的文件时再细细品味一下.
{[写到这里时候,发生了一些事情,耽搁了两周. 顺便看了看devfs.. go on]}
具体再分析do_generic_file_read的时候就逻辑清晰了.
void do_generic_file_read(struct file * filp, loff_t *ppos, read_descriptor_t * desc, read_actor_t actor)
{
struct inode *inode = filp->f_dentry->d_inode;
struct address_space *mapping = inode->i_mapping;
unsigned long index, offset;
struct page *cached_page;
int reada_ok;
int error;
int max_readahead = get_max_readahead(inode);
cached_page = NULL;
index = *ppos >> PAGE_CACHE_SHIFT;
offset = *ppos & ~PAGE_CACHE_MASK;
if (index > filp->f_raend || index + filp->f_rawin < filp->f_raend) {
reada_ok = 0;
filp->f_raend = 0;
filp->f_ralen = 0;
filp->f_ramax = 0;
filp->f_rawin = 0;
} else {
reada_ok = 1;
}
if (!index && offset + desc->count <= (PAGE_CACHE_SIZE >> 1)) {
filp->f_ramax = 0;
} else {
unsigned long needed;
needed = ((offset + desc->count) >> PAGE_CACHE_SHIFT) + 1;
if (filp->f_ramax < needed)
filp->f_ramax = needed;
if (reada_ok && filp->f_ramax < MIN_READAHEAD)
filp->f_ramax = MIN_READAHEAD;
if