天天看點

linux readahead預讀機制分析(linux 4.14)

作者:閃念基因

一、基本概念

設計背景

檔案通路類型一般是順序的,通路[A, B]範圍的資料後,接下來很可能通路[B+1, B+N]資料。由于通路磁盤、flash等存儲器件比較耗時,在通路 [A, B]的時候,如果提前把[B+1, B+N]資料從存儲器件讀取到ram中,那麼後繼需要用[B+1, B+N]資料時,就不需要通過耗時的disk io從存儲器件讀取資料了,進而提高性能。

預讀帶來的好處

對于順序讀,适量的預讀可以提升性能。原因如下:

1)提前讀取資料,避免耗時的disk io讀取資料

預讀資料緩存在page cache中(struct file->f_mapping->page_tree),用的時候,直接從緩存讀取,不需要從存儲器件讀取資料(這個操作耗時)。

2)提高存儲棧、器件處理效率

預讀本質是提前讀取即将通路的資料,以備将來使用,這意味着“目前資料”和“預讀資料”的邏輯位址是連續的,這兩個io請求在核心存儲棧中(filesytem-->block layer-->device)就可以進行合并(merge)處理。處理一個io請求,即可滿足原先的兩個io請求需求。

3)降低硬中斷、軟中斷帶來的負載

器件處理io請求完成時,核心通過硬中斷、軟中斷處理io完成時的工作,合并io請求,可降低中斷數量,減少中斷帶來的額外負載。

4)避免存儲棧擁塞導緻read響應不及時

器件的io數量是有上限的(預設值/sys/block/sdc/queue/nr_requests=128),當器件待處理的io數超過7/8 * nr_requests時,存儲棧進入擁塞狀态并限制生成io請求的速度;當待處理的io請求數達到最大值nr_requests時,read因申請不到struct request而阻塞等待,直至一些io請求處理完後被存儲棧回收,read、write才能轉換成io請求,然後送出io請求給器件處理。合并io請求,可減少io數量,降低存儲棧擁塞的機率。

5)避免磁盤磁頭來回移動

這一點是針對機械硬碟的。檔案資料[A,B]和[B+1, B+N]在邏輯位址上是連續的,磁頭隻需往一個方向移動。作為對比,通路[A,B],[A-N, A-1],[B+1, B+N],就會出現磁頭來回移動的情況,磁頭的機械運動是很耗時的。

預讀帶來的壞處

1)對于随機讀,預讀的資料不會被用到,白白占用了存儲器件帶寬及系統記憶體。

2)大量預讀可能導緻io負載增大。

3)大量預讀可能導緻記憶體壓力增大。

同步預讀& 異步預讀

源碼中有兩個函數page_cache_sync_readahead和page_cache_async_readahead,分别稱作同步預讀、異步預讀。這個地方容易讓人迷糊,需解釋一下二者的差別。

同步預讀:從存儲器件讀取多個page資料,一些page給read使用,一些page留給将來使用。在下圖,read檔案資料時,根據page->index在page cache中(struct file->f_mapping->page_tree)找需要的頁面(藍色頁面),如果找不到就執行同步預讀page_cache_sync_readahead。同步預讀根據待讀資料的邏輯位址、待讀page數量及位址資訊,封裝成bio,然後調用submit_bio送出給器件處理。不過要注意預讀送出bio後就傳回了,并不會等待page中的資料變成PageUptodate。

linux readahead預讀機制分析(linux 4.14)

異步預讀:本次讀的page純粹是為将來準備的,觸發異步預讀的read目前不需要這些資料。這些page提前從存儲器件讀取并将page加入到page cache中。

二、關鍵資料結構及原理

資料結構

預讀的核心資料結構是struct file_ra_state,定義在linux/fs.h中:

/*
 * Track a single file's readahead state
 */
/* 這個結構體是struct file相關的,描述了file的預讀狀态。
預讀按預讀視窗推進,預讀視窗中含有file_ra_state->size個page。
   read通路預讀視窗中的page,通路到第size-async_size個page,也
   即預讀視窗中還剩async_size個page沒有通路時,啟動異步預讀讀入一
   批新的page作為預讀視窗 */
struct file_ra_state {
    //從start這個page開始讀
    pgoff_t start;            /* where readahead started */
//讀取size個page放入預讀視窗。
如記憶體不足無法申請page,則預讀小于size個page
    unsigned int size;        /* # of readahead pages */
    //預讀視窗中還剩async_size個page時,啟動異步預讀
    unsigned int async_size;    /* do asynchronous readahead when
                       there are only # of pages ahead */
    //預讀視窗上限值(機關: page).
      預設等于struct backing_dev_info->ra_pages, 可通過fadvise調整。
如果read需要讀的page數量小于ra_pages,最多讀取ra_pages個頁面.
      如果read需要讀的page數量大于ra_pages,最多讀取 min { read的page數量,存儲器件單次io最大page數量 }個頁面.
      預讀視窗中目前有多少個頁面由size成員變量表示.
    unsigned int ra_pages;        /* Maximum readahead window */
    unsigned int mmap_miss;        /* Cache miss stat for mmap accesses */
    //最後一次的讀位置(機關:位元組)
    loff_t prev_pos;        /* Cache last read() position */
};           

關鍵字段意義如下圖:

linux readahead預讀機制分析(linux 4.14)

原理

read請求N 個page資料,通過預讀從存儲器件讀取M個page資料(M > N),并對其中的第a個page設定PageReadahead标記(0 ≤ a < N)。PageReadahead标記起到一個辨別作用,表示預讀視窗中剩餘的page不多了,需要啟動異步預讀再讀入一批page存放page cache。

如果是順序讀,肯定會讀到PageReadahead标記的頁(單線程讀的場景),或者讀到預讀視窗的最後一個頁(多線程穿插讀的場景),是以代碼作者認為,如果通路到這兩種頁就是順序讀(注意:這樣的判斷不是很準,但代碼寫起來簡單高效),否則認為是随機通路。

如果是順序通路,預讀視窗在之前的基礎上擴大2倍或者4倍(上限值不超過struct file_ra_state->ra_pages),然後從存儲器件讀入資料填充這些page,并緩存在page cache中,最後,在新讀入的這批page中標明一個page設定PageReadahead标記(一般是新讀入的這批page中的第一個page)。

如果是随機通路,預讀機制僅從存儲器件中讀取read函數需要的資料,read請求幾個page,就讀幾個page,并緩存在page cache中。可以看出随機讀不會預讀多餘的page,另外注意随機讀不更改預讀視窗。

三、預讀示例

按4k順序通路檔案的[0, 8*4K]的資料(順序通路),然後lseek到108*4k處通路檔案(随機通路)。過程如下:

1)通路第0個page資料

page->index=0的頁面在page cache中找不到,觸發同步預讀page_cache_sync_readahead,一次性讀了4個page(read需要1個page,預讀3個page。預讀page數量與實際請求的page數量、file_ra_state->ra_pages有關,通過get_init_ra_size計算)。本次預讀建立的預讀視窗如下:

linux readahead預讀機制分析(linux 4.14)

注意,預讀視窗中第ra->size - ra->async_size = 1個page,即page->index=1設定了PageReadahead标記。

2)通路第1個page資料

page->index=1的頁面在page cache中能找到(預讀命中),不需要從存儲器件中讀取資料。又因該page有PageReadahead标記,觸發異步預讀page_cache_async_readahead,預讀頁面數量由get_next_ra_size計算得到,因為本次請求的資料起始位置與上一次讀結束位置相同,屬于順序讀,get_next_ra_size加大預讀量,預讀量從之前的4 page增大到8 page。本次預讀建立的預讀視窗如下:

linux readahead預讀機制分析(linux 4.14)

注意,預讀視窗中第ra->size - ra->async_size = 0個page,即page->index=4設定了PageReadahead标記。

3)通路第2、3個page資料

這兩個page在page cache中可以找到(預讀命中),直接從page cache中讀取資料。

4)通路第4個page資料

page->index=4的頁面在page cache中可以找到(預讀命中),直接從page cache中讀取資料。不過這個page設定了PageReadahead标記,觸發異步預讀page_cache_async_readahead,由于是順序讀,get_next_ra_size将預讀量從8 page增大到16 page,本次預讀建立的預讀視窗如下:

linux readahead預讀機制分析(linux 4.14)

後繼的順序通路流程重複上面過程,遇到page被标記成PageReadahead,增大預讀量(最大不超過struct file_ra_state->ra_pages)後啟動異步預讀。

5)lseek跳到108*4k處通路第108個page資料

通路page->index=108的page,不符合順序讀的條件,是以代碼判斷成随機讀。如果是随機讀,則不做預讀,read請求幾個頁的資料,就從存儲器件中讀幾個頁資料(ondemand_readahead --> __do_page_cache_readahead)。

注意,這次随機讀,不會更改預讀視窗狀态。

四、關鍵代碼分析

同步預讀generic_file_buffered_read --> page_cache_sync_readahead 與 異步預讀generic_file_buffered_read --> page_cache_async_readahead都會調用ondemand_readahead,在這個函數中計算預讀量,然後ra_submit送出io請求。

/* @offset: 從哪個page開始讀 
   @req_size: 需要讀多少個page,這是read需要的數量
   如果是順序讀,ondemand_readahead讀取至少req_size個page。
   如果是随機讀,ondemand_readahead僅讀取req_szie個page。
*/
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)
{
  struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
  unsigned long max_pages = ra->ra_pages;
  unsigned long add_pages;
  pgoff_t prev_offset;


  /*
   * If the request exceeds the readahead window, allow the read to
   * be up to the optimal hardware IO size
   */
/* read請求的page大于預讀視窗大小,預讀的page數量上調至存儲器件單次io最大的page數量 */
  if (req_size > max_pages && bdi->io_pages > max_pages)
    max_pages = min(req_size, bdi->io_pages);


  /*
   * start of file
   */
  if (!offset)
    goto initial_readahead;


  /*
   * It's the expected callback offset, assume sequential access.
   * Ramp up sizes, and push forward the readahead window.
   */
/*  條件1:offset == ra->start + ra->size - ra->async_size


             /         同步建立的預讀視窗         \
            /                                      \
           |----------------------------------------|
           |    |   |   | ^ |   |   |   |   |   |   |
           |--------------|-------------------------|
                          |
             ra->start + ra->size - ra->async_size
             該page設定了PageReadahead


           上圖是同步預讀建立的預讀視窗,從第0個page到第ra->size - ra->async_size
           個page,是read需要用的,後面的page是預留給下次read用的。
           讀到第ra->size - ra->async_size個pag,表明預讀視窗已經開始使用,
           需要啟動異步預讀,讀入一批page建立一個新的預讀視窗,為下下次read做準備。


    條件2:offset == ra->start + ra->size
              
               前一次     /      異步建立的預讀視窗      \
             預讀視窗   /                                \
           -----------|-----------------------------------|
               |  |  | ^ |  |  |  |  |  |  |  |  |  |  |  |   
          ------------|-----------------------------------|
                     |
          ra->start + ra->size(前一次預讀用完了,執行下一個預讀視窗的第一個page)
          該page設定了PageReadahead


          異步預讀建立的預讀視窗中,第一個page标記為PageReadahead,通路到這個标記的
          頁面,表明新預讀視窗已經開始使用,需要啟動異步預讀,讀入一批page建立一個新的
          預讀視窗,為下下次read做準備。


    滿足這兩個條件,可認為本次read預上一次read是順序讀類型 (見原理部分描述)。
    這個條件是不準确的,但是代碼實作簡單高效。*/
  if ((offset == (ra->start + ra->size - ra->async_size) ||
       offset == (ra->start + ra->size))) {
    ra->start += ra->size;
        
/*既然是順序讀,就擴大預讀視窗:
  如果目前預讀視窗大小ra->size < 1/16 * 允許讀的最多page數
  1)則預讀視窗在原基礎上擴大4倍。否則擴大2倍。
  2)擴大後的視窗大小,不能超過“允許讀的最多page數量”
*/
    ra->size = get_next_ra_size(ra, max_pages);
    ra->async_size = ra->size;
    goto readit;
  }


  /*
   * Hit a marked page without valid readahead state.
   * E.g. interleaved reads.
   * Query the pagecache for async_size, which normally equals to
   * readahead size. Ramp it up and use it as the new readahead size.
   */
/* 多線程順序讀一個檔案(interleaved read),破壞了file->ra_state,是以沒法根據前面的條件
   判斷出是順序讀類型。如果是順序讀,最終一定會通路到預讀視窗中的PageReadahead标記的page。
   是以,反過來推,通路到PageReadahead标記的page,就認為是順序讀。當然這樣的判斷是不準确的,
   但準确率高,代碼簡單。*/
  if (hit_readahead_marker) {
    pgoff_t start;


/* offset + 1開始,找到第一個不在page cache中的檔案資料頁,作為預讀的開始 */
    rcu_read_lock();
    start = page_cache_next_hole(mapping, offset + 1, max_pages);
    rcu_read_unlock();


    if (!start || start - offset > max_pages)
      return 0;


    ra->start = start;
    ra->size = start - offset;  /* old async_size */
    ra->size += req_size;
    ra->size = get_next_ra_size(ra, max_pages);
    ra->async_size = ra->size;
    goto readit;
  }


  /*
   * oversize read
   */
  if (req_size > max_pages)
    goto initial_readahead;


  /*
   * sequential cache miss
   * trivial case: (offset - prev_offset) == 1
   * unaligned reads: (offset - prev_offset) == 0
   */
/* 上面的場景根據下面3個條件判斷是否是順序讀:
   offset == ra->start + ra->size - ra->async_size
   offset == ra->start + ra->size
   hit_readahead_marker
   代碼執行到這裡,屬于随機讀場景。
*/
  prev_offset = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
  if (offset - prev_offset <= 1UL)
    goto initial_readahead;


  /*
   * Query the page cache and look for the traces(cached history pages)
   * that a sequential stream would leave behind.
   */
  if (try_context_readahead(mapping, ra, offset, req_size, max_pages))
    goto readit;


  /*
   * standalone, small random read
   * Read as is, and do not pollute the readahead state.
   */
/* read請求req_size,__do_page_cache_readahead隻讀req_size個page。注意2點:
   1)__do_page_cache_readahead-->read_pages條件io就傳回了,不會等待page變成PageUptodate。
   2)__do_page_cache_readahead不會更改 struct file_ra_state。
*/
  return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);


initial_readahead:
/* 第一次讀檔案,初始化預讀視窗 */
  ra->start = offset;
  ra->size = get_init_ra_size(req_size, max_pages);
  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.
   * Take care of maximum IO pages as above.
   */
/* hit_readahead_marker預期建立了一個預讀視窗A,待通路offset剛好是這個預讀視窗的第一個page,
   這說明通路A視窗,後面沒有預留的預讀視窗了,需建立下一個預讀視窗B備用,既然A、B都還沒預讀,
   就合并後一起預讀。*/
  if (offset == ra->start && ra->size == ra->async_size) {
    add_pages = get_next_ra_size(ra, max_pages);
    if (ra->size + add_pages <= max_pages) {
      ra->async_size = add_pages;
      ra->size += add_pages;
    } else {
      ra->size = max_pages;
      ra->async_size = max_pages >> 1;
    }
  }


/* ra_submit --> __do_page_cache_readahead盡量配置設定ra->size個page,
   配置設定失敗就停止配置設定,這些page加入page_pool連結清單。


   read-->page_cache_sync_readahead,假設read需要讀取nr_to_read個page,
   預讀M個page,則第nr_to_read個page(從0開始計數)設定PageReadahead标記。


   read-->page_cache_async_readahead,則預讀的第0個page(從0開始計數)設定
   PageReadahead标記。


   ra_submit -->__do_page_cache_readahead --> read_pages調用
   mapping->a_ops->readpages或者mapping->a_ops->readpage依次讀入page_pool
   連結清單中的page。


   ra_submit送出io後就傳回,不會等待page變成PageUptodate。
*/
  return ra_submit(ra, mapping, filp);
}           

五、代碼待優化的地方

場景

generic_file_buffered_read -->page_cache_sync_readahead或page_cache_async_readahead --> ondemand_readahead --> ra_submit --> __do_page_cache_readahead 會把剛剛申請的一批page(用于預讀)加入到page cache中,如果讀失敗了,這些page并不會從page cache中清除,隻是标記成PageError。

問題

generic_file_buffered_read -->find_get_page到page,但不是PageUptodate狀态,則認為io錯誤導緻的問題,就通過mapping->a_ops->readpage重新讀取單個page資料。是以預讀發生io err後,後繼read不會批量讀入這些page了,隻會單個page讀,性能較差。

解決

對于這個問題,5.18核心版本已經修複。

linux readahead預讀機制分析(linux 4.14)

作者:Vier

來源-微信公衆号:酷派技術團隊

出處:https://mp.weixin.qq.com/s/vYWWwnkxpyffrHlqAMdBlQ