【轉】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.

發佈了20 篇原創文章 · 獲贊 1 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章