【转】006 mm/filemap.c

转自:http://sns.linuxpk.com/space.php?uid=15894&do=blog&id=15203

 

2005-11-25 11:50
mm/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; /* list of clean pages */
struct list_head dirty_pages; /* list of dirty pages */
struct list_head locked_pages; /* list of 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; /*由buddy或者inode queue使用*/
struct address_space *mapping;
unsigned long index;
struct page *next_hash; /*page 在hash表中时,指向下一个page*/
atomic_t count;
unsigned long flags;
struct list_head lru; /*lru cache 使用*/
unsigned long age;
wait_queue_head_t wait;
struct page **pprev_hash;/*page在hash表中时,指向上一个节点
*指向自己的指针
*/
struct buffer_head * buffers;
void *virtual; /* non-NULL if kmapped */
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 /*加入pache cache hash表*/
add_page_to_inode_queue /*加入inode queue即address_space*/
remove_page_from_inode_queue
remove_page_from_hash_queue
__remove_inode_page /*离开inode queue和hash 表*/
remove_inode_page /*同上*/
add_to_page_cache_locked /*加入inode queue,hash 和lru cache*/
__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->newNode */
page->next_hash = next; /* +-----+ */
page->pprev_hash = p; /* p--> |hashp|-->|oldNode| */
if (next) /* next----+ */
next->pprev_hash = &page->next_hash;
if (page->buffers)
PAGE_BUG(page); /*证明page 不会同时存在于page cache
和 buffer cache*/
/*2.6 已经与此不同了*/
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
+------<&lt;&lt;---------------&lt;------+
ext2_readpage->block_read_full_page(fs/buffer.c,buffer cache)
注意函数block_read_full_page,虽然位于buffer.c,但并没有使用buffercache. 但是确实使用了buffer:只是再指定page上创建
buffer提交底层驱动读取文件内容.这个流程有两个值得注意的地 方,一是普通file的read通过pagecache进行,二是page cache读取的时
候不和buffer cache进行同步,三是page cache的确使用了buffer,不过注意,buffer 不是buffer cache.

mmap也使用page cache 缓冲文件,流程如下:
do_mmap-&gt;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)
+-----&gt;先用函数getblk从buffer cache查找
+-----&gt;否则使用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)
+----&gt;do_try_to_free_pages (如果内存已经不够用)
+--&gt;page_launder
| +--&gt;扫描 <inactive_dirty_list>
| +--&gt;对dirty页启动回写(包括mapping和buffer cache)
+--&gt;refill_inactive
+--&gt;refill_inactive_scan
+--&gt;扫描<active_list>,选择合适页面移入
<inactive_dirty_list>
+--&gt;swap_out,对进程启动页面换出
+--&gt;try_to_swap_out将选中页面放入
<inactive_dirty_list>
+-----&gt;refill_inactive_scan

*****kreclaimd(mm/vmscan.c)
+-----&gt;遍历每个zone 用reclaim_page
扫描zone-&gt;inactive_clean_pages,找出可以释放的页面
脱离lru cache
+-----&gt;对reclaim_page找到的页面补充到buddy系统

*****bdflush
+----&gt;flush_dirty_buffers (提交buffer cache到驱动)
+-----&gt;如页面短缺,进行page_launder


5.buffer cache和lru队列
buffer cache 的确也使用了lru队列,grow_buffers调用lru_cache_add将页面加入lru队列.但是却没有加入到page cache.(请阅读代
码)
kreclaimd-&gt;reclaim_page将会尝试回收clean 页面到zone的buddy系统,如果page-&gt;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() ------------&gt;mm/vmscan.c
if (page-&gt;buffers) {
...
try_to_free_buffers
...
}
try_to_free_buffers是buffer cache提供给lru的函数,buffer cache自己从不使用,这证实了buffer cache的确利用lru cache回收内存.


第二部分 ---&gt; 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-&gt;inode-&gt;i_op-&gt;readpage**generic_readpage-&gt; 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://groups.google.com/group/fa.linux.kernel/browse_thread/thread/3d1be60ca2980479/0ca4533f7d0b73e4?hl=zh-CN&
在2.4中buffer cache自己维护了一套类似page cache和lru队列的机制,对buffer cache做lru 缓冲处理,的确不是一个什么好东西.


第三部分---&gt; mm/filemap.c


通过上面的讨论,已经涉及了本文件的诸多函数,这里对已经有说明的文件一笔带过,对感兴趣的,做个分析注解.
头六个函数就不多说了,见上面的分析.

(1) page cache 初始化
/*
* mempages: 物理页面个数
*/
void __init page_cache_init(unsigned long mempages)
{
unsigned long htable_size, order;

/*计算要为hash 表分配多少内存, 及其order值(power of 2)*/
htable_size = mempages;
htable_size *= sizeof(struct page *);
for(order = 0; (PAGE_SIZE <&lt; order) &lt; htable_size; order++)
;

/*计划分配一个能容下所有物理页的hash表,就看又没内存*/
do {
/*这个order能够容下的page个数数*/
unsigned long tmp = (PAGE_SIZE &lt;&lt; order) / sizeof(struct page *);

/*计算这么大的表对应的hash值(hash表下标)最多有多少位*/
page_hash_bits = 0;
while((tmp >&gt;= 1UL) != 0UL)
page_hash_bits++;

page_hash_table = (struct page **) /*看看有没有这么多连续内存*/
__get_free_pages(GFP_ATOMIC, order);
} while(page_hash_table == NULL && --order &gt; 0);/*没有的话尝试少分点*/

printk("Page-cache hash table entries: %d (order: %ld, %ld bytes)/n",
(1 <&lt; page_hash_bits), order, (PAGE_SIZE &lt;&lt; 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-&gt;sync_page(page),对于ext2就是ext2_aops
-&gt;block_sync_page-&gt;run_task_queue(&tq_disk) (fs/buffer.c).让磁盘有更多
机会运行回写,读入等任务.提供给 ___wait_on_page,__lock_page使用.
/*
* Wait for a page to get unlocked.
*
* This must be called with the caller "holding" the page,
* ie with increased "page-&gt;count" so that the page won't
* go away during the wait..
*/
void ___wait_on_page(struct page *page)
{
struct task_struct *tsk = current;
DECLARE_WAITQUEUE(wait, tsk);

add_wait_queue(&page-&gt;wait, &wait);
do {
sync_page(page); /*给磁盘(may be other dev)一点运行机会
*说不定就不用再等了
*/
set_task_state(tsk, TASK_UNINTERRUPTIBLE);
if (!PageLocked(page))
break;
run_task_queue(&tq_disk);/**/
schedule();
} while (PageLocked(page));
tsk-&gt;state = TASK_RUNNING;
remove_wait_queue(&page-&gt;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)-&gt;flags)) BUG(); /
smp_mb__after_clear_bit(); /
if (waitqueue_active(&page-&gt;wait)) /
wake_up(&page-&gt;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)) //如果页面未锁,禁止加入swap cache
BUG(); //出现此种情况是内核的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-&gt;mapping.所以我的结论是,在
以下情况下需要page lock:
1.对page进行io操作
2.某些特定目的情况下操作page-&gt;mapping和page引用计数的情形

为了验证这个结论,搜索对lock_page的引用,绝大多数在进行page io操作,还有
部分处理 加入/离开page cache,这些容易理解. 然后挑一个例子看看为什么也
使用了lock_page,先看一个 filemap.c中的函数
/*
* Get the lock to a page atomically.
*/
struct page * __find_lock_page (struct address_space *mapping,
unsigned long offset, struct page **hash)
{
struct page *page;

/*
* We scan the hash list read-only. Addition to and removal from
* the hash-list needs a held write-lock.
*/
repeat:
spin_lock(&pagecache_lock); //操作page cache的锁
page = __find_page_nolock(mapping, offset, *hash);
if (page) {
page_cache_get(page);
spin_unlock(&pagecache_lock);

lock_page(page); //判断page-&gt;mapping以求
//返回一个肯定在page cache
//的页面,必须锁定页面,否则
//可能被page cache清除
/* Is the page still hashed? Ok, good.. */
if (page-&gt;mapping)
return page;

/* Nope: we raced. Release and try again.. */
UnlockPage(page);
page_cache_release(page);
goto repeat;
}
spin_unlock(&pagecache_lock);
return NULL;
}
使用page lock的原因已经写入注释,此函数返回一个保证还在page cache的页,
并增加页面引用计数,可以直接拿来使用,如shmem_nopage.总之,如果你要保证
page-&gt;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;

/*
* We can avoid the copy if:
* - we're the only user (count == 1)
* - the only other user is the swap cache,
* and the only swap cache user is itself,
* in which case we can just continue to
* use the same swap cache (it will be
* marked dirty).
*/
switch (page_count(old_page)) {
case 2:
/*
* Lock the page so that no one can look it up from
* the swap cache, grab a reference and start using it.
* Can not do lock_page, holding page_table_lock.
*/
if (!PageSwapCache(old_page) || TryLockPage(old_page))
break;
if (is_page_shared(old_page)) {
UnlockPage(old_page);
break;
}
UnlockPage(old_page); //解锁后如果有人从swap cache共享了页面呢?
/* FallThrough */
case 1:
flush_cache_page(vma, address);
establish_pte(vma, address, page_table, pte_mkyoung(pte_mkdirty(pte_mkwrite(pte))));
spin_unlock(&mm-&gt;page_table_lock);
return 1; /* Minor fault */
}

................
}
这个地方注释详尽,为了避免其他执行流从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)
{
...........

/*
* Freeze the "shared"ness of the page, ie page_count + swap_count.
* Must lock page before transferring our swap count to already
* obtained page count.
*/
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; /* Minor fault */
}



(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)
+---&gt;truncate_list_pages
+--&gt;truncate_partial_page
+--&gt;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-&gt;notify_change-&gt;inode_setattr(ext2文
件系统没有提供setattr,采用通用逻辑)-&gt;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-&gt;next;
offset = page-&gt;index;

/* Is one of the pages to truncate? */
if ((offset &gt;= 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-&gt;dirty_pages)到磁盘.而inode meta data的
同步依赖于特定的文件系统(见buffer.c sys_fsync,注意page cache无meta数据).
filemap_fdatasync遍历dirty页面,提交系统驱动处理 (mapping-&gt;a_ops-&gt;writepage
对ext2文件系统来讲就是 ext2_aops -&gt; ext2_writepage -&gt;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-&gt;__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;
/*
*add_to_page_cache_unique-&gt;__add_to_page_cache对页面进行了加锁
*/
if (add_to_page_cache_unique(page, mapping, index, hash))
goto repeat;/*新页面加入cache的时候发现cache已经有了指定页面*/
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为例就是
/*
* We have mostly NULL's here: the current defaults are ok for
* the ext2 filesystem.
*/
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-&gt;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的时候就逻辑清晰了.
/*
* This is a generic file read routine, and uses the
* inode-&gt;i_op-&gt;readpage() function for the actual low-level
* stuff.
*
* This is really ugly. But the goto's actually try to clarify some
* of the logic when it comes to error handling etc.
*/
void do_generic_file_read(struct file * filp, loff_t *ppos, read_descriptor_t * desc, read_actor_t actor)
{
struct inode *inode = filp-&gt;f_dentry-&gt;d_inode;
struct address_space *mapping = inode-&gt;i_mapping;
unsigned long index, offset;
struct page *cached_page; /*不存在于page cache的时候分配的页面,可能用不到
*因为获取锁的时候可能等待,被其他执行流抢了先.
*/
int reada_ok;
int error;
int max_readahead = get_max_readahead(inode);

/*
* 在字节流内的位置转换成线性的文件页面流索引
*/
cached_page = NULL;
index = *ppos &gt;&gt; PAGE_CACHE_SHIFT;
offset = *ppos & ~PAGE_CACHE_MASK;

/*
* 看看预读是否有效,及时调整预读量.
* 如果还未曾预读或者被重置,调整read-ahead max的过程就是预读的初始化
*/
/*
* If the current position is outside the previous read-ahead window,
* we reset the current read-ahead context and set read ahead max to zero
* (will be set to just needed value later),
* otherwise, we assume that the file accesses are sequential enough to
* continue read-ahead.
*/
if (index &gt; filp-&gt;f_raend || index + filp-&gt;f_rawin < filp->f_raend) {
/*index < filp->raend - filp-&gt;rawin*/
/*如果用户读取范围超出预读窗口则重新计算预读量和起始位置*/
reada_ok = 0;
filp-&gt;f_raend = 0;
filp-&gt;f_ralen = 0;
filp-&gt;f_ramax = 0;
filp-&gt;f_rawin = 0;
} else {
reada_ok = 1;
}
/*
* Adjust the current value of read-ahead max.
* If the read operation stay in the first half page, force no readahead.
* Otherwise try to increase read ahead max just enough to do the read request.
* Then, at least MIN_READAHEAD if read ahead is ok,
* and at most MAX_READAHEAD in all cases.
*/
if (!index && offset + desc-&gt;count <= (PAGE_CACHE_SIZE >&gt; 1)) {
/*读取文件的前半个页面,不进行预读*/
filp-&gt;f_ramax = 0;
} else {
unsigned long needed;
/*计算需要读入的页面个数, 注*ppos在页面index的offset位置*/
needed = ((offset + desc-&gt;count) &gt;&gt; PAGE_CACHE_SHIFT) + 1;

if (filp-&gt;f_ramax < needed)
filp->f_ramax = needed; /*预读量至少要满足这次读取请求*/

if (reada_ok && filp-&gt;f_ramax < MIN_READAHEAD)
filp->f_ramax = MIN_READAHEAD;
if (filp-&gt;f_ramax &gt; max_readahead)
filp-&gt;f_ramax = max_readahead;
}

/*
* 根据用户要求读取所有请求的页面
*/
for (;;) {
struct page *page, **hash;
unsigned long end_index, nr;

/*nr:本页面读取的字节数*/
end_index = inode-&gt;i_size &gt;&gt; PAGE_CACHE_SHIFT;
if (index &gt; end_index)
break;
nr = PAGE_CACHE_SIZE;
if (index == end_index) {
nr = inode-&gt;i_size & ~PAGE_CACHE_MASK;
if (nr <= offset)
break;
}

nr = nr - offset;

/*
* Try to find the data in the page cache..
*/
/* (先在page cache寻找指定文件页) */
hash = page_hash(mapping, index);

spin_lock(&pagecache_lock);
page = __find_page_nolock(mapping, index, *hash);
if (!page)
goto no_cached_page; /*分配页面加入page cache 跳转到 page_ok*/
/*如果睡眠后其他执行流将文件的page块加入到
*page cache就跳转到 found_page
*/
found_page:
page_cache_get(page); /*先get页面*/
spin_unlock(&pagecache_lock);/*后解锁page cache*/

if (!Page_Uptodate(page))
goto page_not_up_to_date; /*预读,读取本页,然后返回到page_ok*/
generic_file_readahead(reada_ok, filp, inode, page);
page_ok:
/* If users can be writing to this page using arbitrary
* virtual addresses, take care about potential aliasing
* before reading the page on the kernel side.
*/
if (mapping->i_mmap_shared != NULL)
flush_dcache_page(page);

/*
* Ok, we have the page, and it's up-to-date, so
* now we can copy it to user space...
*
* The actor routine returns how many bytes were actually used..
* NOTE! This may not be the same as how much of a user buffer
* we filled up (we may be padding etc), so we can only update
* "pos" here (the actor routine has to update the user buffer
* pointers and the remaining count).
*/
nr = actor(desc, page, offset, nr);
offset += nr;
/*计算下一个要读的页面和偏移*/
index += offset &gt;&gt; PAGE_CACHE_SHIFT;
offset &= ~PAGE_CACHE_MASK;

page_cache_release(page);
if (nr && desc-&gt;count) /*需要继续*/
continue;
break; /*读取结束*/
/*
* for 循环的主流程结束
*/
/*
* 页面没有含有有效数据的情况
*/
/*
* Ok, the page was not immediately readable, so let's try to read ahead while we're at it..
*/
page_not_up_to_date:
generic_file_readahead(reada_ok, filp, inode, page);

if (Page_Uptodate(page))
goto page_ok;

/* Get exclusive access to the page ... */
lock_page(page);

/* Did it get unhashed before we got the lock? */
if (!page-&gt;mapping) {
UnlockPage(page);
page_cache_release(page);
continue;
}

/* Did somebody else fill it already? */
if (Page_Uptodate(page)) {
UnlockPage(page);
goto page_ok;
}

readpage:/* 无有效数据和页面不在page cache 的情况也许都要read page (no_cached_page)*/
/* ... and start the actual read. The read will unlock the page. */
error = mapping-&gt;a_ops-&gt;readpage(filp, page);

if (!error) {
if (Page_Uptodate(page))
goto page_ok;

/* Again, try some read-ahead while waiting for the page to finish.. */
generic_file_readahead(reada_ok, filp, inode, page);
wait_on_page(page);
if (Page_Uptodate(page))
goto page_ok;
error = -EIO;
}

/* UHHUH! A synchronous read error occurred. Report it */
desc-&gt;error = error;
page_cache_release(page);
break;
/*
* 未在page cache 发现指定页面,只有分配一个了
*/

no_cached_page:
/*
* Ok, it wasn't cached, so we need to create a new
* page..
*
* We get here with the page cache lock held.
*/
if (!cached_page) {
spin_unlock(&pagecache_lock);
cached_page = page_cache_alloc();
if (!cached_page) {
desc-&gt;error = -ENOMEM;
break;
}

/*
* Somebody may have added the page while we
* dropped the page cache lock. Check for that.
*/
spin_lock(&pagecache_lock);
page = __find_page_nolock(mapping, index, *hash);
if (page)
goto found_page;
}

/*
* Ok, add the new page to the hash-queues...
*/
page = cached_page;
__add_to_page_cache(page, mapping, index, hash);
spin_unlock(&pagecache_lock);
cached_page = NULL;

goto readpage;
} /*end for*/

*ppos = ((loff_t) index <&lt; PAGE_CACHE_SHIFT) + offset;
filp->f_reada = 1;
if (cached_page)
page_cache_free(cached_page);
UPDATE_ATIME(inode);
}

函数的分析就是上面的注释.另外一个问题就是预读. do_generic_file_read
当然是进行文件的预读的最好的时机.在这里建立预读的context(一直在想 contex
的最佳译法),检查预读是否有效.

为了搞清楚预读的各个变量我们分三遍读do_generic_file_read,分别对应:
第一次读取文件,第二次读取文 件顺序读取,所以预读命中,第三次读取文件,超出
预读窗口. 来看看和generic_file_readahead如何配合.
条件:
1) 假设读取不是从0字节开始,比如从8k的地方读
2) 假设读取的时候进行加锁都比较快,io没有很快完成(这应该是一般
情况,ide硬盘怎么会有那么快)


第一次读取文件:(假设page cache 无此页面)
+----do_generic_file_read()
{
.......
if (index &gt; filp-&gt;f_raend ||....) {..}
reada_ok = 0; //read 8k,so exceed reada context
else{ }

if (!index && offset ...) {
}
else {
unsigned long needed;

needed = ....;

if (filp-&gt;f_ramax < needed)
filp->f_ramax = needed; //f_ramax init
}

readpage:
假设第一次读取,所以page cache没有此页面,需要从hd读入,页面已
锁.
if (!error) {
if (Page_Uptodate(page))
goto page_ok; //我们假设读取没有很快完成也是很
//合理的,哪有那么快
//所以进行预读的时候页面是加了锁的,reada_ok为0
generic_file_readahead(reada_ok, filp, inode, page);
wait_on_page(page);
if (Page_Uptodate(page))
goto page_ok;
error = -EIO;
}

}

+--generic_file_readahead()
{
raend = filp-&gt;f_raend; /*=0 */
max_ahead = 0; /*本次要启动io的页面之数量*/

if (PageLocked(page)) { //第一次读取文件所以filp-&gt;f_ralen 为0
if (!filp-&gt;f_ralen || index &gt;= raend || index + filp-&gt;f_rawin < raend) {
//重新建立预读窗口
raend = index; //假设"上次预读"结束于当前页面(正在读取的页面)
//即,当前锁定的页面是在"预读"
if (raend &lt; end_index)
max_ahead = filp->f_ramax; //本次预读filp-&gt;f_ramax个页面,
//在do_generic_file_read 中已经初始化
filp-&gt;f_rawin = 0; //预读窗口为0,因为还没有预读过(或重新建立预读)
filp-&gt;f_ralen = 1; //上次"预读"了1个页面
if (!max_ahead) {
filp-&gt;f_raend = index + filp-&gt;f_ralen;/*上次预读窗口外的第一个页面*/
filp-&gt;f_rawin += filp-&gt;f_ralen;/*连续有效预读的总个数*/
}
}
}else if (reada_ok ...)
}
ahead = 0; /*本次预读的页面个数*/
while (ahead < max_ahead) {
在max_ahead个页面上启动预读
} /*ahead 保持为0*/
if (ahead) {
if (reada_ok == 2) {//我们这次reada_ok为0}
filp->f_ralen += ahead; //f_ralen代表上次预读的个数,这里为此记录
filp-&gt;f_rawin += filp-&gt;f_ralen; //f_rawin代表所有连续有效预读的总量
filp-&gt;f_raend = raend + ahead + 1;//f_raend是预读窗口外第一个页面
filp-&gt;f_ramax += filp-&gt;f_ramax;//预读有效,下次预读量加倍
.....
}
}

分析: 第一次读取文件page 2,offset 0,filep各项为0,do_generic_file_read将
reada_ok 置0. 将filp-&gt;f_ramax置为用户读取的页面个数(有上限).
generic_file_readahead 为第一次读取文件建立预读档案并预读一定数量的页面.


第二次读取 文件:上次进行了预读,假设page cache 已经有此页面,并且是顺序读
取,命中了预读窗口.
+----do_generic_file_read()
{
.......
if (index &gt; filp-&gt;f_raend ||....) {..}
else{ //命中预读窗口
reada_ok = 1;
}

if (!index && offset ...) {/*读取文件的前半个页面,不进行预读*/
我们早就不是读前半个页面了
}
else {
unsigned long needed;

needed = ....;
//假设上次预读量已经足够了,所以这次f_ramax没有被重置
//是上次读取量的两倍
if (filp-&gt;f_ramax < needed)
filp->f_ramax = needed;
}

for (;;) {
//我们已经假设page cache存在此页面
found_page:
....
if (!Page_Uptodate(page))
goto page_not_up_to_date; /*假设预读已经完成(没有完成也一样)*/
//所以进行预读的时候页面是没有加锁的,reada_ok为1
generic_file_readahead(reada_ok, filp, inode, page);
............
}
}
// reada_ok =1 代表此次读取命中预读窗口(但不一定命中上次预读窗)
+--generic_file_readahead()
{
raend = filp-&gt;f_raend; /*=0 */
max_ahead = 0; /*本次要启动io的页面之数量*/

if (PageLocked(page)) {
//这次没有加锁,^_^
}else if (reada_ok && filp-&gt;f_ramax && raend &gt;= 1 &&
index <= raend && index + filp->f_ralen &gt;= raend) {
/*命中预读窗口,并且命中上次预读的那部分页面,用户真是步
*步紧逼啊.我们这次读取如果不是如此,就不会再进行任何预读
*临时决定就假设如此吧.
*/
/*页面未锁,或许读取完成,或许还没有开始---&gt;*/
raend -= 1; /*见注释,保持和同步预读有着同样的io max size*/
if (raend < end_index)
max_ahead = filp->f_ramax + 1;
if (max_ahead) {
filp-&gt;f_rawin = filp-&gt;f_ralen;
filp-&gt;f_ralen = 0; /*将上次预读长度(即"上次预读"窗口)清空*/
reada_ok = 2; /*---&gt;所以或许要督促一下,尽快开始读取*/
}
}

ahead = 0; /*本次预读的页面个数*/
while (ahead < max_ahead) {
.....
if (page_cache_read(filp, raend + ahead) &lt; 0)
break;
} /*ahead 保持为0*/
if (ahead) {
if (reada_ok == 2) { /*强制unplug*/
run_task_queue(&tq_disk);
}
filp->f_ralen += ahead; //f_ralen代表上次预读的个数,这里为此记录
filp-&gt;f_rawin += filp-&gt;f_ralen; //f_rawin代表所有连续有效预读的总量
filp-&gt;f_raend = raend + ahead + 1;//f_raend是预读窗口外第一个页面
filp-&gt;f_ramax += filp-&gt;f_ramax;//预读有效,下次预读量加倍
.....
}
}

分析: 第二次读取文件如果用户命中上次预读的那几个页面,证明预读有效,极有
可能是顺序读取,故进 行预读(预读量是上次的两倍),并再次加倍预读量.(当然有
上限).
第三次读取:未 命中预读窗口. 和第一次预读类似. 这里不再列举.

预读分为两种: 同步预读和异步预读.从磁盘读取数据如果是DMA方式,总是异步
的.这里应该是数和用户读取文件同时进行的意思,也就是当前 页面已经开始io的
情况之下,页面已经上锁,叫做同步.
异步读取的时候,调用run_task_queue(&tq_disk), 到底干了些啥?
drivers/block/ll_rw_blk.c 函数generic_plug_device,将request_queue_t放入
task queue :tq_disk.块驱动的task queue里都是什么请求?当然是我们的读/写
啦. 印证一下: 同一个文件的函数
void blk_init_queue(request_queue_t * q, request_fn_proc * rfn)
{
INIT_LIST_HEAD(&q-&gt;queue_head);
INIT_LIST_HEAD(&q-&gt;request_freelist[READ]);
INIT_LIST_HEAD(&q-&gt;request_freelist[WRITE]);
elevator_init(&q-&gt;elevator, ELEVATOR_LINUS);
blk_init_free_list(q);
q-&gt;request_fn = rfn; /*note 0*/
q-&gt;back_merge_fn = ll_back_merge_fn;
q-&gt;front_merge_fn = ll_front_merge_fn;
q-&gt;merge_requests_fn = ll_merge_requests_fn;
q-&gt;make_request_fn = __make_request;
q-&gt;plug_tq.sync = 0;
q-&gt;plug_tq.routine = &generic_unplug_device; /*note 1*/
q-&gt;plug_tq.data = q;
q-&gt;plugged = 0;
/*
* These booleans describe the queue properties. We set the
* default (and most common) values here. Other drivers can
* use the appropriate functions to alter the queue properties.
* as appropriate.
*/
q-&gt;plug_device_fn = generic_plug_device; /*note 2*/
q-&gt;head_active = 1;
}
负责初始化blk驱动的请求队列. 对于ide:见drivers/ide/ide-probe.c
static void ide_init_queue(ide_drive_t *drive)
{
request_queue_t *q = &drive-&gt;queue;

q-&gt;queuedata = HWGROUP(drive);
blk_init_queue(q, do_ide_request);
}
ide 请求队列中的
q-&gt;request_fn = do_ide_request,
q-&gt;plug_tq.routine = &generic_unplug_device;
q-&gt;plug_device_fn = generic_plug_device;
q-&gt;make_request_fn = __make_request;
首先我们请求读入:
submit_bh-&gt;generic_make_request-&gt; q-&gt;make_request_fn**__make_request:
__make_request()
{....
if (list_empty(head)) { //如果当前驱动无其他pending的请求
//就将队列plug到task queue,这样,可以在一连串的请求都放入
//请求队列后再开始io,从而可以将连续请求合并到一起
q-&gt;plug_device_fn(q, bh-&gt;b_rdev); /* is atomic */ /*generic_plug_device*/
goto get_rq;
}
....
add_request-&gt; 将读写请求放入q.
out:
if (!q-&gt;plugged) /*如果plug了就不再直接调用request_fn*/
(q-&gt;request_fn)(q); /* do_ide_request*/

}

然后当我们直接调用 run_task_queue(&tq_disk)-&gt;__run_task_queue-&gt;
tq_disk-&gt;routine**generic_unplug_device-&gt;__generic_unplug_device-&gt;
q-&gt;request_fn**do_ide_request.

分析完了这些,就可以理解下面的注释了
generic_file_readahead ()
{
..........
/*
* .............
* If we tried to read ahead asynchronously,
* Try to force unplug of the device in order to start an asynchronous
* read IO request.
* ........
*/
if (ahead) {
if (reada_ok == 2) { /*强制unplug,真正开始异步io操作*/
run_task_queue(&tq_disk);
}
....
}
}


(8)sys_sendfile和普通文件的写操作
sys_sendfile :内核空间的文件拷贝. 系统完成从一个文件拷贝指定数据到另
一个 文件的功能.不过这次使用
do_generic_file_read(in_file, ppos, &desc, file_send_actor);
file_send_actor顺势就写入指定文件了.使用的函数是
written = file-&gt;f_op-&gt;write(file, kaddr + offset, size, &file-&gt;f_pos);
对于ext2,就是generic_file_write.(不幸,也在这个文件内,too long):
和 generic_file_read类似,写操作也要转换文件字节流pos到文件页面index,同样
在具体的文件系统和 vfs层有一个隔离. 我们只关心一下write和read的不同之处,
忽略一些和read类似的细节:
ssize_t
generic_file_write(struct file *file,const char *buf,size_t count,loff_t *ppos)
{
.......
cached_page = NULL;

down(&inode-&gt;i_sem);

pos = *ppos;
... // check something
status = 0;
if (count) {
remove_suid(inode);
inode-&gt;i_ctime = inode-&gt;i_mtime = CURRENT_TIME;
mark_inode_dirty_sync(inode); /*将inode移入super block的dirty队列*/
}

while (count) {

/*计算页面索引(流地址到页面地址转换)*/
offset = (pos & (PAGE_CACHE_SIZE -1)); /* Within page */
index = pos &gt;&gt; PAGE_CACHE_SHIFT;
bytes = PAGE_CACHE_SIZE - offset;
....
/*
* Bring in the user page that we will copy from _first_.
* Otherwise there's a nasty deadlock on copying from the
* same page as we're writing to, without it being marked
* up-to-date.
*/
{ volatile unsigned char dummy; /*用户空间内可能跨两个页面
存储同一个文件页面数据,故需要尝试访问两个页面*/
__get_user(dummy, buf);
__get_user(dummy, buf+bytes-1);
/*为何先访问一下,待会再续*/
}


/*看看page cache有无此页面,若无则分配一个并加入pache cache*/
status = -ENOMEM; /* we'll assign it later anyway */
page = __grab_cache_page(mapping, index, &cached_page);
if (!page)
break;

/* We have exclusive IO access to the page.. */
if (!PageLocked(page)) { /*防止我们操作的时候回写页面*/
PAGE_BUG(page);
}

status = mapping-&gt;a_ops-&gt;prepare_write(file, page, offset, offset+bytes);
if (status)
goto unlock;
kaddr = page_address(page);
status = copy_from_user(kaddr+offset, buf, bytes);
flush_dcache_page(page);
if (status)
goto fail_write;
status = mapping-&gt;a_ops-&gt;commit_write(file, page, offset, offset+bytes);
if (!status)
status = bytes;

.........
unlock:
/* Mark it unlocked again and drop the page.. */
UnlockPage(page);
if (deactivate) /*deactive 可以促使更快的回写dirty page.另外有可能是
get_user所作的操作将页面swap in,用完后deactive很合理*/
deactivate_page(page);
page_cache_release(page);

if (status < 0)
break;
}
*ppos = pos;

if (cached_page)
page_cache_free(cached_page);

/* For now, when the user asks for O_SYNC, we'll actually
* provide O_DSYNC. */
if ((status >= 0) && (file-&gt;f_flags & O_SYNC))
status = generic_osync_inode(inode, 1); /* 1 means datasync */

err = written ? written : status;
out:

up(&inode-&gt;i_sem);
return err;
fail_write:
status = -EFAULT;
ClearPageUptodate(page);
kunmap(page);
goto unlock;
}

首先看mapping-&gt;a_ops-&gt;prepare_write都做了些什么:(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_prepare_write,此函数只是简单调用
block_prepare_write(page,from,to,ext2_get_block)
+---&gt;__block_prepare_write(........,ext2_get_block)
准备写这个页面的时候,其实是先从磁盘装入页面,这样可以保证你只写一部分页面的时候可以有办法回写整个页面.
看看__block_prepare_write的代码,可以注意到他调用ext2_get_block的时候其最后一个参数 create为1.代表如果未能在磁盘上找着指定的
block,就分配一个给这个文件.ext2_get_block在说文 件读取的时候已经见过了,他将文件内连续编址的block,通过ext2的一到三级block索引
转换成在磁盘内连续编址的 block. __block_prepare_write 和ext2_get_block在阅读buffer.c和ext2系统相关文件的时候再来仔细讨论.
mapping-&gt;a_ops-&gt;commit_write就是 generic_commit_write, 只是mark对应的所有bh为dirty状态,关于此函数的其他细节这里不再讨论.
此函数还处理O_SYNC,立即回写所有的数据.其细节以后再讨论,不过那个函数蛮简单,多自己看.
至于为什么要deactive写入的页面,代码中已经注释上了,不知道作者和我想的是否一样:为了更快的回写数据,因为只有在inactive dirty队
列的页面才会为page_launder回写.

另外一个问题就是为啥需要__get_user(dummy, buf):
暂举一例(fix me:是否有更通用的例子?):将文件mmap到用户空间,然后用户copy文件中某页的前半部分到后半部分,而此页面被写回了文件.
这样:generic_file_write-&gt;__grab_cache_page分配了一个新的页面,并加锁,然 后mapping-&gt;a_ops-&gt;prepare_write从文件中读入,但是没
有置为 uptodate.紧接者copy_from_user从用户页拷贝数据发生page fault(见fault.c我们已经分析过了),于是又在page cache查找页面,当
然应该命中我们刚刚分配的已经 加锁的这个page.但是因为没有置uptodate, 异常处理就再次试图读入页面,进行加锁操作,这样一个死锁就发生
了. 而用__get_user(dummy, buf)处理一下,就模拟了用户的操作,触发了一个页面异常,强行将页面swap in,就不会死锁了.


(9)sys_msync和mmap的COW操作

msync:
将mmap的内存页面回写到对应文件.有三个功能(man msync):
a)MS_ASYNC , 异步回写只是调度一个回写流程.linux中只需要mark dirty.
b)MS_SYNC,同步回写,等待回写完成.
c)MS_INVALIDATE,通知对同一个文件做了映射的进程,使其mapping获得新的数据. 对于linux,共享的映射使用相同的pages,无需考虑.
而对于PRIVATE(MAP_PRIVATE)的映射,写入 mapping页面不会回写到文件,无需和其他maping保持一直(不管是share还是private,参考man
mmap).

顺便分析一下mmap对文件映射的几个重要行为: MAP_SHARED: 和其他进程共享文件的映射页,只有这种映射,写入映射的数据才回写到文
件. 首先是映射建立,VM的属性VM_XXX继承自用户设置MAP_XXX,PROT_XXX. 参考mm/mmap.c 函数do_mmap_pgoff.
unsigned long do_mmap_pgoff(......)
{
............
if (file) {
VM_ClearReadHint(vma);
vma-&gt;vm_raend = 0;

if (file-&gt;f_mode & FMODE_READ)
vma-&gt;vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_SHARED) {
vma-&gt;vm_flags |= VM_SHARED | VM_MAYSHARE;

/* This looks strange, but when we don't have the file open
* for writing, we can demote the shared mapping to a simpler
* private mapping. That also takes care of a security hole
* with ptrace() writing to a shared mapping without write
* permissions.
*
* We leave the VM_MAYSHARE bit on, just to get correct output
* from /proc/xxx/maps..
*/
if (!(file-&gt;f_mode & FMODE_WRITE))
vma-&gt;vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
}
} else {
vma-&gt;vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_SHARED)
vma-&gt;vm_flags |= VM_SHARED | VM_MAYSHARE;
}

.......
if (flags & VM_LOCKED) {
mm-&gt;locked_vm += len &gt;&gt; PAGE_SHIFT;
make_pages_present(addr, addr + len); /*就是handle_mm_fault*/
}
return addr;
........
}
注意这个函数的两个地方: VM_SHARED属性的设置,和VM_LOCKED. 只有VM_LOCKED属性的页面才立即分配页面.并且分配的方式是模拟缺页
中断.所以总是以如下的方式分配内存页面个 mmap, 以前已经分析过fault.c了,这里结合mmap再看看函数
static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
unsigned long address, int write_access, pte_t *page_table)
{
struct page * new_page;
pte_t entry;

if (!vma-&gt;vm_ops || !vma-&gt;vm_ops-&gt;nopage)
return do_anonymous_page(mm, vma, page_table, write_access, address);

/*
* The third argument is "no_share", which tells the low-level code
* to copy, not share the page even if sharing is possible. It's
* essentially an early COW detection.
*/
new_page = vma-&gt;vm_ops-&gt;nopage(vma, address & PAGE_MASK,
(vma-&gt;vm_flags & VM_SHARED)?0:write_access);
if (new_page == NULL) /* no page was available -- SIGBUS */
return 0;
.........(在下面列出)
}

在分析fault.c的时候已经说过了,对于mmap建立的 vm,vma-&gt;vm_ops-&gt;nopage就是
filemap_nopage.

/*
* filemap_nopage() is invoked via the vma operations vector for a
* mapped memory region to read in file data during a page fault.
*
* The goto's are kind of ugly, but this streamlines the normal case of having
* it in the page cache, and handles the special cases reasonably without
* having a lot of duplicated code.
*/
struct page * filemap_nopage(struct vm_area_struct * area,
unsigned long address, int no_share)
{
/*
* 注意do_no_page调用时候no_share的设置:(vma-&gt;vm_flags & VM_SHARED)?0:write_access
* 1.如果是VM_SHARED,no_share置0,代表共享文件的映射页面
* 2.否则(private),看是否是写操作,如果是写操作,no_share置1,指示分配
* 新页面给这个进程.
*/
.....................
if (no_share) {
struct page *new_page = page_cache_alloc();
if (new_page) {
copy_user_highpage(new_page, old_page, address);
flush_page_to_ram(new_page);
} else
new_page = NOPAGE_OOM;
page_cache_release(page);
return new_page;
......................
}
如果是private映射,此函数执COW, copy一个页面个这个进程.如果进程只是
read就还返回现有的页面, do_no_page 函数对此页面建立一个COW的pte:
VM属 性是VM_WRITE,pte却不可写,等进程写的时候再copy.看看do_no_page的剩余
部分:
static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma,
unsigned long address, int write_access, pte_t *page_table)
{
............
flush_page_to_ram(new_page);
flush_icache_page(vma, new_page);
entry = mk_pte(new_page, vma-&gt;vm_page_prot);
if (write_access) {
entry = pte_mkwrite(pte_mkdirty(entry));
} else if (page_count(new_page) &gt; 1 &&
!(vma-&gt;vm_flags & VM_SHARED)) /*非共享映射,且页面已经有多于一个
entry = pte_wrprotect(entry); *进程使用,禁止写,为cow埋下伏笔.
*/

set_pte(page_table, entry);
/* no need to invalidate: a not-present page shouldn't be cached */
update_mmu_cache(vma, address, entry);
return 2; /* Major fault */
}

说了这么多,差点忘了msync,sys_msync是入口,逻辑简单,找到对应的vma调用
msync_interval, 阅读这个函数的时候注意只有对VM_SHARED,msync才有意义.
并且MS_ASYNC是隐含完成的,没有对应的 case或者判断中出现这个宏.
static int msync_interval(struct vm_area_struct * vma,
unsigned long start, unsigned long end, int flags)
{
struct file * file = vma-&gt;vm_file;
if (file && (vma-&gt;vm_flags & VM_SHARED)) {
int error;
/*最终filemap_sync_pte将page 标记为dirty,完成 MS_ASYNC*/
error = filemap_sync(vma, start, end-start, flags);

if (!error && (flags & MS_SYNC)) {
struct inode * inode = file-&gt;f_dentry-&gt;d_inode;
down(&inode-&gt;i_sem);
filemap_fdatasync(inode-&gt;i_mapping);/*启动page io,参考ext2_writepage*/

/*可以参考ext2_sync_file,回写文件系统的meta信息*/
if (file-&gt;f_op && file-&gt;f_op-&gt;fsync)
error = file-&gt;f_op-&gt;fsync(file, file-&gt;f_dentry, 1);

/*等待文件回写完成*/
filemap_fdatawait(inode-&gt;i_mapping);
up(&inode-&gt;i_sem);
}
return error;
}
return 0;
}
filemap_sync-&gt;filemap_sync_pmd_range-&gt;filemap_sync_pte_range-&gt;
filemap_sync_pte只是调用set_page_dirty,不再罗列.filemap_fdatasync
前面有简述,不再分析.


(10) 有关mmap的几个函数
经过这么激烈的讨论,关于filemap(mmap)自己经没有什么好说的了.并且我们暂时关注filemap(mmap一个文件到用户进程).
调用mmap,在用户内存中找到一个空闲的虚拟空间,建立一个vma,设置好vm_ops并建立vma和文件的关系,当用户去访问对应的页面时再分配
页面,从文件读入数据.用户通过msync回写数据到文件.几个进程可以同时mmap同一个文件,需要进程自己处理好访问的互斥. 就说这么多.
系统调用sys_mmap2(或者old_mmap)-&gt;do_mmap2-&gt;do_mmap_pgoff(简单分析):
unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,
unsigned long len,ulong prot, ulong flags, unsigned long pgoff)
{
........
/*建立vma*/
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!vma)
return -ENOMEM;

vma-&gt;vm_mm = mm;
vma-&gt;vm_start = addr;
vma-&gt;vm_end = addr + len;
vma-&gt;vm_flags = vm_flags(prot,flags) | mm-&gt;def_flags;

.........
if (file) {
if (vma-&gt;vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma-&gt;vm_file = file; /*和文件建立关系*/
get_file(file);
error = file-&gt;f_op-&gt;mmap(file, vma); /*ext2就是generic_file_mmap
*就是设置vma-&gt;vm_ops从而使
*vma-&gt;vm_ops-&gt;readpage为函
*数filemap_nopage
*/
if (error)
goto unmap_and_free_vma;
} else if (flags & MAP_SHARED) {
..........
}
.............

}

generic_file_mmap 如此简单....
filemap的缺页处理前面已经详细讨论过了,包括share/private之间的区别,以及filemap如何利用COW处理share /private映射.不再赘述.

(11)sys_madvise/sys_mincore
首先应该man一下madvise.此调用设置一段用户的虚拟内存属性,以便内核在这段虚拟内存上启用合适的cache/read ahead 算法. 如此
种种.看起来代码挺多其实没有什么技术含量的,只是在修改vma结构,用户可能将一个vma的一段的属性改变,所以有可 能要将vma拆分成两个.
MADV_WILLNEED就调度预读,MADV_DONTNEED就将指定段的已分配页面全部释放.

sys_mincore获取指定长度上那些页面已经在内存了,无需磁盘操作即可读取,代码不复杂.


对filemap.c的分析,到此终于"草草"结束.filemap_nopage 是唯一一个比较重要但是没有完整分析的函数,阅读的时候可以看到
sys_madvise所做的建议被用作内核处理缺页的依 据,注意一下nopage_sequential_readahead对标记了顺序读取的vma进行预读.

good luck.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值