記憶體管理實戰案例分析1:缺頁異常和檔案系統引發的當機

rlk8888發表於2022-03-16

微信公眾號: [奔跑吧linux社群]
本文節選自《奔跑吧linux核心》第二版卷1第6.3章

1.問題描述

阿里工程師在Linux 5.0核心開發期間報告了一個當機現象,

從核心日誌資訊來看,有兩個執行緒發生了死鎖的情況。
下面是task1程式的函式呼叫關係。

task1:

[<ffffffff811aaa52>] wait_on_page_bit+0x82/0xa0
[<ffffffff811c5777>] shrink_page_list+0x907/0x960
[<ffffffff811c6027>] shrink_inactive_list+0x2c7/0x680
[<ffffffff811c6ba4>] shrink_node_memcg+0x404/0x830
[<ffffffff811c70a8>] shrink_node+0xd8/0x300
[<ffffffff811c73dd>] do_try_to_free_pages+0x10d/0x330
[<ffffffff811c7865>] try_to_free_mem_cgroup_pages+0xd5/0x1b0
[<ffffffff8122df2d>] try_charge+0x14d/0x720
[<ffffffff812320cc>] memcg_kmem_charge_memcg+0x3c/0xa0
[<ffffffff812321ae>] memcg_kmem_charge+0x7e/0xd0
[<ffffffff811b68a8>] __alloc_pages_nodemask+0x178/0x260
[<ffffffff8120bff5>] alloc_pages_current+0x95/0x140
[<ffffffff81074247>] pte_alloc_one+0x17/0x40
[<ffffffff811e34de>] __pte_alloc+0x1e/0x110
[<ffffffffa06739de>] alloc_set_pte+0x5fe/0xc20
[<ffffffff811e5d93>] do_fault+0x103/0x970
[<ffffffff811e6e5e>] handle_mm_fault+0x61e/0xd10
[<ffffffff8106ea02>] __do_page_fault+0x252/0x4d0
[<ffffffff8106ecb0>] do_page_fault+0x30/0x80
[<ffffffff8171bce8>] page_fault+0x28/0x30
[<ffffffffffffffff>] 0xffffffffffffffff

下面是task2程式的函式呼叫關係。

task2:

[<ffffffff811aadc6>] __lock_page+0x86/0xa0
[<ffffffffa02f1e47>] mpage_prepare_extent_to_map+0x2e7/0x310 [ext4]
[<ffffffffa08a2689>] ext4_writepages+0x479/0xd60
[<ffffffff811bbede>] do_writepages+0x1e/0x30
[<ffffffff812725e5>] __writeback_single_inode+0x45/0x320
[<ffffffff81272de2>] writeback_sb_inodes+0x272/0x600
[<ffffffff81273202>] __writeback_inodes_wb+0x92/0xc0
[<ffffffff81273568>] wb_writeback+0x268/0x300
[<ffffffff81273d24>] wb_workfn+0xb4/0x390
[<ffffffff810a2f19>] process_one_work+0x189/0x420
[<ffffffff810a31fe>] worker_thread+0x4e/0x4b0
[<ffffffff810a9786>] kthread+0xe6/0x100
[<ffffffff8171a9a1>] ret_from_fork+0x41/0x50
[<ffffffffffffffff>] 0xffffffffffffffff

2.問題分析

從task1程式的函式呼叫關係來看,CPU在處理缺頁異常時,do_fault()函式為PT分配一個物理頁面。在分配頁面的路徑上正好觸及memcg的上限值,導致進入直接頁面回收函式do_try_to_free_pages()。在頁面回收中掃描不活躍頁面連結串列,若頁面正在處於回寫狀態,即設定PG_Writeback標誌位,那麼有兩種處理情況。

  1. 當前系統有大量的回寫頁面,若當前程式是kswapd核心執行緒,且這個頁面設定了PG_PageReclaim標誌位,就會繼續掃描下一個頁面,而不用等待這個頁面回寫完成。

  2. 系統等待這個頁面回寫完成,見wait_on_page_writeback()。
    相關程式碼見shrink_page_list()函式,它實現在mm/vmscan.c檔案中,其程式碼片段如下。

<mm/vmscan.c>


static unsigned long shrink_page_list()
{
        ...
        if (PageWriteback(page)) {
             if (current_is_kswapd() &&
                PageReclaim(page) &&
                test_bit(PGDAT_WRITEBACK, &pgdat->flags)) {
                nr_immediate++;
                goto activate_locked;
            }  else {
                unlock_page(page);
                wait_on_page_writeback(page);
                list_add_tail(&page->lru, page_list);
                 continue;
            }
        }
        ...
}

顯然,本場景通過wait_on_page_writeback()函式來等待這個頁面回寫完成,因為它是通過直接頁面回收路徑來呼叫的。
接下來分析task2程式的函式呼叫關係。task2執行在核心執行緒裡,這個核心執行緒使用工作佇列機制實現重新整理回寫功能。核心回寫執行緒會定期選擇髒的檔案進行回寫。回寫過程中呼叫檔案系統中的writepages回撥函式把髒頁面寫回磁碟,對於ext4檔案系統,該回撥函式是do_writepages()。在mpage_prepare_extent_to_map()函式中掃描這個檔案所有的頁面,首先尋找髒頁面(設定了PG_Dirty標誌位的頁面),然後給這個頁面設定PG_Writeback標誌位,呼叫ext4_io_submit()提交I/O到塊層。在這個掃描過程中要短暫地為每個頁面加一個頁鎖(即設定PG_locked標誌位)。
一個可能的場景如下。

  1. 假設訪問一個檔案,首先通過mmap方式把整個檔案對映到了使用者空間。這個檔案的前半段已經被寫入過,因此這個檔案產生了髒頁,即有髒的內容快取頁面還沒有寫回磁碟。
    對於CPU1,因為這個檔案中有內容快取頁面是髒的,所以把這個檔案的inode新增到了回寫連結串列裡(wb->b_dirty)。核心回寫執行緒會定期從wb->b_dirty連結串列中取髒的inode進行回寫處理。此時,flash核心執行緒正在處理這個檔案的inode。在回寫執行緒中,ext4_writepages()-> mpage_prepare_extent_to_map()函式會遍歷整個檔案去查詢哪些頁面是髒頁(判斷是否設定了PG_dirty標誌位)。掃描時會先去申請 頁面的鎖,然後判斷其是否為髒頁。在本場景中,首先,Page_A會被先掃描,因為它在檔案的前半段,而且這個頁面是髒頁。Page_A成功申請了頁鎖,然後設定PG_Writeback標誌位並且通過ext4_io_submit()提交I/O到塊層。
    CPU0訪問這個檔案的後半段,檔案後半段還沒有建立對映關係,因此產生了缺頁異常。在__do_fault()函式中,vma->vm_ops->fault()會呼叫檔案的fault()回撥函式把檔案的內容讀取到內容快取裡,並通過lock_page(vmf->page)給這個頁面加上鎖,我們假設這個頁面稱為Page_B。

  2. 接下來,在finish_fault()函式裡,發現Page_B對應的PT是空的(還沒建立),因此呼叫pte_alloc_one()函式分配一個頁面來作為PT。我們把這個頁面稱為Page_C,它不屬於這個檔案的內容快取。在alloc_pages()裡,當把這個頁面加入memcg時達到了上限值,因此呼叫直接頁面回收函式do_try_to_free_pages。在shrink_page_list()裡等待另外一個頁面回寫完成,“無巧不成書”,這個回寫的頁面正是前面提到的Page_A,它是這個檔案前半段的某個髒頁。因為Page_A已經在前面設定了PG_Writeback標誌位。

  3. 這個時候,CPU1正好掃描到了Page_B,使用lock_page()嘗試給Page_B新增頁鎖。
    這樣,CPU1嘗試為Page_B申請鎖,但是Page_B的鎖已經被CPU0持有了。CPU0持有了Page_B的鎖,在鎖的臨界區裡,它又等待另外一個頁面Page_A的回寫完成。因此,典型的ABBA型別的死鎖發生了。
    整個死鎖過程如圖6.6所示。


3.解決方案

最早的解決方案是在ext4檔案系統的mpage_prepare_extent_to_map()函式中對申請的頁鎖進行判斷。若頁面的鎖已經被其他物件持有,那麼先提交I/O到塊層,然後使用lock_page()嘗試申請頁鎖。但是社群的核心開發者都不同意這個方案,因為其他的檔案系統(如xfs等)都可能存在類似的問題。

後來核心開發者從缺頁異常方向來修復這個問題,最後SUSE核心工程師Michal Hocko提交的補丁被合併到Linux 5.0核心中。在缺頁異常過程中有一個提前預先分配頁表的機制。若PT不存在,那麼在為Page_B申請鎖之前提前分配好PT需要的頁面,這樣就可以規避這個問題。vm_fault資料結構中有一個prealloc_pte成員,它是提前分配好的頁表需要的頁面。修復好的流程如圖6.7所示,在缺頁異常處理中,在為Page_B申請鎖之前,若發現PT為空,則提前分配一個頁面,將其作為PT。

新書預告

《奔跑吧linux核心》第二版卷1已經上架了。

《奔跑吧linux核心》第二版卷2上架!


金色年華,流金歲月,奔二入門篇上架!

圖片


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70005277/viewspace-2871479/,如需轉載,請註明出處,否則將追究法律責任。

相關文章