实战内存管理:缺页异常和文件系统引发的宕机
采集列表 2023-06-01

1.问题描述

阿里工程师在Linux 5.0内核开发期间报告了一个宕机现象,

https://lore.kernel.org/linux-ext4/1540858969-75803-1-git-send-email-bo.liu@linux.alibaba.com/

从内核日志信息来看,有两个线程发生了死锁的情况。
下面是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。



声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
  • 相关技术文库
  • 硬件
  • 原理图
  • 信号完整性
  • EMI
下载排行榜
更多
评测报告
更多
广告