一文看懂 什麼是頁緩存(Page Cache)

我們知道文件一般存放在硬盤(機械硬盤或固態硬盤)中,CPU 並不能直接訪問硬盤中的數據,而是需要先將硬盤中的數據讀入到內存中,然後才能被 CPU 訪問。

由於讀寫硬盤的速度比讀寫內存要慢很多(DDR4 內存讀寫速度是機械硬盤 500 倍,是固態硬盤的 200 倍),所以爲了避免每次讀寫文件時,都需要對硬盤進行讀寫操作,Linux 內核使用 頁緩存(Page Cache) 機制來對文件中的數據進行緩存。

本文使用的 Linux 內核版本爲:Linux-2.6.23

什麼是頁緩存

爲了提升對文件的讀寫效率,Linux 內核會以頁大小(4KB)爲單位,將文件劃分爲多數據塊。當用戶對文件中的某個數據塊進行讀寫操作時,內核首先會申請一個內存頁(稱爲 頁緩存)與文件中的數據塊進行綁定。如下圖所示:

如上圖所示,當用戶對文件進行讀寫時,實際上是對文件的 頁緩存 進行讀寫。所以對文件進行讀寫操作時,會分以下兩種情況進行處理:

頁緩存的實現

前面主要介紹了頁緩存的作用和原理,接下來我們將會分析 Linux 內核是怎麼實現頁緩存機制的。

1. address_space

在 Linux 內核中,使用 file 對象來描述一個被打開的文件,其中有個名爲 f_mapping 的字段,定義如下:

從上面代碼可以看出,f_mapping 字段的類型爲 address_space 結構,其定義如下:

struct address_space {
    struct inode           *host;      /* owner: inode, block_device */
    struct radix_tree_root page_tree;  /* radix tree of all pages */
    rwlock_t               tree_lock;  /* and rwlock protecting it */
    ...
};

address_space 結構其中的一個作用就是用於存儲文件的 頁緩存,下面介紹一下各個字段的作用:

從 address_space 對象的定義可以看出,文件的 頁緩存 使用了 radix樹 來存儲。

radix樹:又名基數樹,它使用鍵值(key-value)對的形式來保存數據,並且可以通過鍵快速查找到其對應的值。內核以文件讀寫操作中的數據 偏移量 作爲鍵,以數據偏移量所在的 頁緩存 作爲值,存儲在 address_space 結構的 page_tree 字段中。

下圖展示了上述各個結構之間的關係:

如果對 radix樹 不太瞭解,可以簡單將其看成可以通過文件偏移量快速找到其所在 頁緩存 的結構,有機會我會另外寫一篇關於 radix樹 的文章。

2. 讀文件操作

現在我們來分析一下讀取文件數據的過程,用戶可以通過調用 read 系統調用來讀取文件中的數據,其調用鏈如下:

從上面的調用鏈可以看出,read 系統調用最終會調用 do_generic_mapping_read 函數來讀取文件中的數據,其實現如下:

void
do_generic_mapping_read(struct address_space *mapping,
                        struct file_ra_state *_ra,
                        struct file *filp,
                        loff_t *ppos,
                        read_descriptor_t *desc,
                        read_actor_t actor)
{
    struct inode *inode = mapping->host;
    unsigned long index;
    struct page *cached_page;
    ...

    cached_page = NULL;
    index = *ppos >> PAGE_CACHE_SHIFT;
    ...

    for (;;) {
        struct page *page;
        ...

find_page:
        // 1. 查找文件偏移量所在的頁緩存是否存在
        page = find_get_page(mapping, index);
        if (!page) {
            ...
            // 2. 如果頁緩存不存在, 那麼跳到 no_cached_page 進行處理
            goto no_cached_page; 
        }
        ...

page_ok:
        ...
        // 3. 如果頁緩存存在, 那麼把頁緩存的數據拷貝到用戶應用程序的內存中
        ret = actor(desc, page, offset, nr);
        ...
        if (ret == nr && desc->count)
            continue;
        goto out;
        ...

readpage:
        // 4. 從文件讀取數據到頁緩存中
        error = mapping->a_ops->readpage(filp, page);
        ...
        goto page_ok;
        ...

no_cached_page:
        if (!cached_page) {
            // 5. 申請一個內存頁作爲頁緩存
            cached_page = page_cache_alloc_cold(mapping);
            ...
        }

        // 6. 把新申請的頁緩存添加到文件頁緩存中
        error = add_to_page_cache_lru(cached_page, mapping, index, GFP_KERNEL);
        ...
        page = cached_page;
        cached_page = NULL;
        goto readpage;
    }

out:
    ...
}

do_generic_mapping_read 函數的實現比較複雜,經過精簡後,上面代碼只留下最重要的邏輯,可以歸納爲以下幾個步驟:

add_to_page_cache_lru 函數主要完成兩個工作:

總結

本文主要介紹了 頁緩存 的作用和原理,並且介紹了在讀取文件數據時對頁緩存的處理過程。本文並沒有介紹寫文件操作對應的頁緩存處理和當系統內存不足時怎麼釋放頁緩存,有興趣的話可以自行閱讀相關的代碼實現。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/IvbkVt5tuwezRbKjQQt1RA