圖解 Linux 文件預讀原理
概述
本文主要闡述內核 (linux-3.12) 的文件系統預讀設計和實現。
所謂預讀,是指文件系統爲應用程序一次讀出比預期更多的文件內容並緩存在 page cache 中,這樣下一次讀請求到來時部分頁面直接從 page cache 讀取即可。當然,這個細節對應用程序透明,應用程序可能的感覺就是下次讀的速度會更快,當然這是好事。文中我們會通過設置幾個情境(順序讀、隨機讀、多線程交織讀)來分析預讀的邏輯。
情境 1:順序讀
// 事例代碼
{
...
f = open("file", ....);
ret = read(f, buf, 4096);
ret = read(f, buf, 2 * 4096);
ret = read(f, buf, 4 * 4096);
...
}
該場景非常簡單:打開文件,共進行三次讀(且是順序讀),那讓我們看看操作系統是如何對文件進行預讀的。
Read 1
第一次進入內核讀處理流程時,在 page cache 中查找該 offset 對應的頁面是否緩存,因爲首次讀,緩存未命中,觸發一次同步預讀:
static void do_generic_file_read(struct
file *filp, loff_t *ppos,
read_descriptor_t *desc,
read_actor_t actor)
{
......
for (;;) {
......
cond_resched();
find_page:
// 如果沒有找到,啓動同步預讀
page = find_get_page(mapping, index);
if (!page) {
page_cache_sync_readahead(
mapping, ra, filp,
index,
last_index - index
);
該同步預讀邏輯最終進入如下預讀邏輯:
// 注意: 這裏offset 和req_size其實是頁面數量
static unsigned long ondemand_readahead(
struct address_space *mapping,
struct file_ra_state *ra,
struct file *filp,
bool hit_readahead_marker,
pgoff_t offset,
unsigned long req_size)
{
unsigned long max =
max_sane_readahead(ra->ra_pages);
// 第一次讀文件,直接初始化預讀窗口即可
if (!offset)
goto initial_readahead;
......
initial_readahead:
ra->start = offset;
ra->size = get_init_ra_size(req_size, max);
// ra->size 一定是>= req_size的,這個由get_init_ra_size保證
// 如果req_size >= max,那麼ra->async_size = ra_size
ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;
readit:
/*
* Will this read hit the readahead marker made by itself?
* If so, trigger the readahead marker hit now, and merge
* the resulted next readahead window into the current one.
*/
if (offset == ra->start &&
ra->size == ra->async_size) {
ra->async_size = get_next_ra_size(ra, max);
ra->size += ra->async_size;
}
return ra_submit(ra, mapping, filp);
}
讀邏輯會爲該文件初始化一個預讀窗口:
(ra->start, ra->size, ra->async_size)
本例中的預讀窗口爲 (0,4,3),初始化該預讀窗口後調用 ra_submit 提交本次讀請求。形成的讀窗口如下圖所示:
圖中看到,應用程序申請訪問 PAGE 0,內核一共讀出 PAGE0 ~PAGE3,後三個屬於預讀頁面,而且 PAGE_1 被標記爲 PAGE_READAHEAD,當觸發到該頁面讀時,操作系統會進行一次異步預讀,這在後面我們會仔細描述。
等這四個頁面被讀出時,第一次讀的頁面已經在 pagecache 中,應用程序從該 page 中拷貝出內容即可。
Read 2
接下來應用程序進行第二次讀:offset=4096, size=8192。內核將其轉化爲以 page 爲單位計量,offset=1,size=2。即讀上面的 PAGE1 和 PAGE2。
感謝第一次的預讀,PAGE1 和 PAGE2 目前已經在內存中了,但由於 PAGE1 被打上了 PAGE_AHEAD 標記,讀到該頁面時會觸發一次異步預讀:
find_page:
......
page = find_get_page(mapping, index);
if (!page) {
page_cache_sync_readahead(
mapping, ra, filp,
index,
last_index - index);
page = find_get_page(mapping, index);
if (unlikely(page == NULL))
goto no_cached_page;
}
if (PageReadahead(page)) {
page_cache_async_readahead(
mapping, ra, filp,
page,index,
last_index - index);
}
static unsigned long
ondemand_readahead(
struct address_space *mapping,
struct file_ra_state *ra,
struct file *filp,
bool hit_readahead_marker,
pgoff_t offset,
unsigned long req_size)
{
unsigned long max =
max_sane_readahead(ra->ra_pages);
........
/* 如果:
* 1. 順序讀(本次讀偏移爲上次讀偏移 (ra->start) + 讀大小(ra->size,包含預讀量) -
* 上次預讀大小(ra->async_size))
* 2. offset == (ra->start + ra->size)???
*/
if ((offset == (ra->start + ra->size - ra->async_size) ||
offset == (ra->start + ra->size))) {
// 設置本次讀的offset,以page爲單位
ra->start += ra->size;
ra->size = get_next_ra_size(ra, max);
ra->async_size = ra->size;
goto readit;
}
經歷了第一次預讀,文件的預讀窗口狀態爲
(ra->start,ra->size, ra->async_size)=(0, 4, 3)
本次的請求爲 (offset,size)=(1, 2),上面代碼的判斷條件成立,因此我們會向前推進預讀窗口,此時預讀窗口變爲
(ra->start,ra->size, ra->async_size) = (4, 8, 8)
由於本次是異步預讀,應用程序可以不等預讀完成即可返回,只要後臺慢慢讀頁面即可。本次預讀窗口的起始以及大小以及預讀大小可根據前一次的預讀窗口計算得到,又由於本次是異步預讀,因此,預讀大小就是本次讀的頁面數量,因此將本次預讀的第一個頁面 (PAGE 4) 添加預讀標記。
由於上面的兩次順序讀,截至目前,該文件在操作系統中的 page cache 狀態如下:
Read 3
接下來應用程序進行第三次讀,順序讀,範圍是 [page3, page6],上面的預讀其實已經將這些頁面讀入 page cache 了,但是由於 page4 被打上了 PAGE_READAHEAD 標記,因此,訪問到該頁面時會觸發一次異步預讀,預讀的過程與上面的步驟一致,當前預讀窗口爲 (4,8,8) ,滿足順序性訪問特徵,根據特定算法計算本次預讀大小,更新預讀窗口爲 (12,16,16) ,新的預讀窗口如下:
對該情境簡單總結下,由於三次的順序讀加上內核的預讀行爲,文件的 page cache 中的狀態當前如下圖所示:
原文連接:https://zhuanlan.zhihu.com/p/41307290
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HBqHU0jKqQRWWjxCj1hEWA