文件系統預讀【轉】

轉自:https://www.cnblogs.com/linhaostudy/p/16126723.html

 

正文

所謂預讀,是指文件系統爲應用程序一次讀出比預期更多的文件內容並緩存在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提交本次讀請求。形成的讀窗口如下圖所示:

image

圖中看到,應用程序申請訪問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)添加預讀標記。

image

由於上面的兩次順序讀,截至目前,該文件在操作系統中的page cache狀態如下:

image

Read 3

接下來應用程序進行第三次讀,順序讀,範圍是[page3, page6],上面的預讀其實已經將這些頁面讀入page cache了,但是由於page4被打上了PAGE_READAHEAD標記,因此,訪問到該頁面時會觸發一次異步預讀,預讀的過程與上面的步驟一致,當前預讀窗口爲(4,8,8),滿足順序性訪問特徵,根據特定算法計算本次預讀大小,更新預讀窗口爲(12,16,16),新的預讀窗口如下:

image

對該情境簡單總結下,由於三次的順序讀加上內核的預讀行爲,文件的page cache中的狀態當前如下圖所示:

image

情景2

這裏我們來看另外一種情境:單進程文件順序讀,讀大小爲256KB,看看預讀邏輯如何處理這種情況,照例首先給出事例代碼:

{
        ...
        f   = open("file", ....);
        ret = read(f, buf, 40 * 4096);
        ret = read(f, buf, 16 * 4096);
        ret = read(f, buf, 32 * 4096);
        ...
}

事例代碼中我們一共進行了三次讀,順序讀,且讀的大小不定,有超過最大預讀量的,也有低於最大預讀量的。

Read 1

毫無疑問,由於第一次讀肯定未在緩存命中,前一篇博客告訴我們需要進行一次同步預讀,需要初始化預讀窗口

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->size=32 pages,即使應用程序要讀的數量是40 pages,這樣ra->async_size = ra->size=32 pages,在readit邏輯判斷成立,因此會重設ra->async_size的值,根據計算應該是32 pages,而總的ra->size=初始值+ra->async_size=64 pages。形成的預讀窗口爲(0, 64, 32),如下圖:

image
由於應用程序本次訪問的實際頁面是PAGE0 ~PAGE40(由於同步預讀會全部在緩存命中),因此在訪問過程中會碰到page32,此時觸發一次異步預讀,並向前推進預讀窗口‘

/* 如果:
      ** 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))) {
	      // 設置本次讀的
		ra->start += ra->size; 
		ra->size = get_next_ra_size(ra, max);
		ra->async_size = ra->size;
		goto readit;
	}

更新後的當前預讀窗口爲(64, 32, 32),如下:

image

因此,經過第一次讀以後,該文件在內存中page cache狀態如下圖所示:

image

Read 2

由於第二次讀只需讀出page40 ~ page55,直接在page cache中命中,也不會觸發一次異步預讀,預讀窗口也不會更新,因此,該過程非常簡單。本次讀完以後,文件在內存page cache的狀態如下:

image

Read3

應用程序第三次讀的範圍爲page56 ~ page87,由上圖可知,這些均可以在page cache中命中,但是由於訪問了PAGE64,因此會觸發一次異步預讀,且當前的預讀窗口爲(64, 32, 32),根據上面的算法更新預讀窗口爲(96, 32, 32),因此,本次預讀完成以後,文件在page cache中的緩存狀態如下:

image

情景三

所謂的交織讀指的是多線程(進程)讀同一個打開的文件描述符,單個線程的順序讀在操作系統看來可能會變成隨機讀。同樣我們還是結合實例來分析。

事例代碼

{
        ......
        f = open("file", ......)
        pthread_create(read_file_1, f, ...)
        pthread_create(read_file_2, f, ...)
        ......
}
 
read_file_1(f)
{
        lseek(f, 0, SEEK_SET);
        read(f, ..., 2 * 4096);
        read(f, ..., 4 * 4096)
        read(f, ..., 16 * 4096)
}
 
read_file_2(f)
{
        lseek(f, 128 * 4096, SEEK_SET);
        read(f, ..., 2 * 4096);
        read(f, ..., 4 * 4096)
        read(f, ..., 16 * 4096)
}

事例代碼中創建了兩個線程同時讀文件file,每個線程均是順序讀,讓我們看看操作系統的預讀是如何處理這種情況的。因爲多線程的執行順序可能是多種多樣的,我們只列舉一種執行流並解釋,線程1 read 1,線程2 read 1,線程2 read 2,線程1 read 2,線程1 read 3,線程2 read3。

線程1 Read 1

線程1讀文件的前兩個頁面,由於尚未緩存命中,因此會觸發文件系統的一次同步預讀,確定預讀窗口爲(ra->start, ra->size, ra->async_size) = (0, 4, 2),形成的預讀窗口如下

image

線程2 Read 1

線程2讀文件的128和129兩個頁面,由於這兩個頁面也尚未緩存在page cache中,也必須啓動一次同步預讀,這裏會更改上面的預讀窗口爲(128, 4, 2),更新後的預讀窗口如下:

由於本次讀和上次讀是順序讀,且本次訪問的4個頁面有兩個緩存命中,但由於訪問了PAGE 130,而該頁面又被打上了異步預讀標記,因此在訪問頁面130的時候會觸發一次異步預讀,更新預讀窗口爲(132, 8, 8),如下:

image

由於本次會訪問4個頁面,因此PAGE 132也會被訪問,從而又觸發一次異步預讀,更新預讀窗口爲(140, 16, 16),最終形成的預讀窗口如下:

image

線程2兩次讀 read1 和read 2以後形成的page cache狀態如下所示:

image

線程1 Read 2

接下來線程1進行第二次讀,範圍是PAGE 2 ~ PAGE 5,由於線程1 read 1將PAGE 2 和PAGE 3已經預讀進page cache,因此可直接命中,但在訪問PAGE 2的時候會觸發一次異步預讀,所以這裏會更新預讀窗口,但很不幸,預讀窗口保存的是線程2的預讀狀態,因此本次訪問和之前的預讀窗口並不連續,因此我們必須想辦法來恢復線程1的之前的預讀狀態,會觸發下面的執行邏輯:

if (hit_readahead_marker) {
		pgoff_t start;
 
		rcu_read_lock();
		//計算本次應該從哪個頁面開始讀
		// 計算的方法是:從上次的offset開始查找,找到第一個沒有緩存在page cache 的頁面
		start = radix_tree_next_hole(&mapping->page_tree, offset+1,max);
		rcu_read_unlock();
 
		// 如果沒有找到或者與本次讀的偏移相差甚大,那麼其實無需再讀了
		if (!start || start - offset > max)
			return 0;
 
		ra->start = start;
		ra->size = start - offset;	/* old async_size */
		ra->size += req_size;
		ra->size = get_next_ra_size(ra, max);
		//既然是異步預讀,那讀出的所有頁面均是提前讀的,因此設置async_size = size
		ra->async_size = ra->size;
		goto readit;
	}

這裏恢復線程1的預讀窗口方法也比較簡單:從本次預讀的頁面開始向後搜索,找到第一個沒有緩存在page cache的頁面,本例中是page4,然後以此爲本次預讀的起始頁面號,並可以計算出上次的預讀窗口大小(page 4 - page 2 = 2),根據這兩個值便可確定本次預讀窗口爲(4, 8, 8)。

更新後的預讀窗口如下圖所示:

image

在訪問頁面4時,會再次出發異步預讀,更新預讀窗口爲(8, 8, 8),如下圖所示:

image

因此,線程1經過read 1 和read 2,形成的page cache狀態如下:

image

線程1 Read 3

線程1第三次讀的頁面是PAGE 6 ~ PAGE 13,全部在緩存命中,但在訪問PAGE 8的時候會觸發一次異步預讀,更新預讀窗口爲(16, 16, 16)。

在線程1經歷了三次讀以後,page cache的狀態如下圖所示:

image

線程2 Read 3

線程2第三次讀頁面是PAGE 134 ~ PAGE 141,這些全在緩存中命中,但是訪問PAGE 140時會觸發一次異步預讀。更新預讀窗口,但是很不幸,之前的預讀窗口是線程1的,因此我們必須搜尋才能恢復線程2的預讀窗口,搜尋過程之前已經描述,這裏不再囉嗦,恢復出線程2的預讀窗口爲(156, 32,32)。因此,總的來看,由於線程2的三次讀形成的page cache狀態如下:

image

 


如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章