在内存trace中经常遇到线程uninterruptable sleep,而打出来的trace可以看到是如下:
wait_on_page_bit_killable+0xb0/0xcc
__lock_page_or_retry+0xb8/0xf4
filemap_fault+0x4cc/0x630
ext4_filemap_fault+0x34/0x48
__do_fault+0x88/0x110
handle_mm_fault+0x854/0xb68
do_page_fault+0x2a4/0x3b4
do_DataAbort+0x84/0x158
认识的可能只有do_page_fault()开始到do_fault()结束,而被卡在了wait_on_page_bit_killable()函数,通过分析得知这主要和PG_locked flag有关。
我们知道发生do_page_fault主要是为了给虚拟地址分配物理内存,但是这个发现卡在了wait_on_page_bit_killable()函数,导致此问题的原因在于PG_locked被长期置为1导致,
在__do_fault()函数中:
static int __do_fault(struct vm_area_struct *vma, unsigned long address,
pgoff_t pgoff, unsigned int flags,
struct page *cow_page, struct page **page)
{
struct vm_fault vmf;
int ret;
vmf.virtual_address = (void __user *)(address & PAGE_MASK);
vmf.pgoff = pgoff;
vmf.flags = flags;
vmf.page = NULL;
vmf.cow_page = cow_page;
ret = vma->vm_ops->fault(vma, &vmf);------------------(1)
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
if (!vmf.page)
goto out;
if (unlikely(PageHWPoison(vmf.page))) {
if (ret & VM_FAULT_LOCKED)
unlock_page(vmf.page);
page_cache_release(vmf.page);
return VM_FAULT_HWPOISON;
}
if (unlikely(!(ret & VM_FAULT_LOCKED)))
lock_page(vmf.page);------------------------------(2)
else
VM_BUG_ON_PAGE(!PageLocked(vmf.page), vmf.page);
out:
*page = vmf.page;
return ret;
}
(1)此处为执行vm_ops的fault函数,也就是ext4_filemap_fault()
(2)此处如果没有返回VM_FAULT_LOCKED,则调用lock_page()设置PG_locked标志位,而此处的lock_page在拿不到PG_locked flag时会导致系统睡眠,将进程设置为UNINTERRUPTABLE sleep
ext4_filemap_fault()->filemap_fault()->lock_page_or_retry():
static inline int lock_page_or_retry(struct page *page, struct mm_struct *mm,
unsigned int flags)
{
might_sleep();
return trylock_page(page) || __lock_page_or_retry(page, mm, flags);
}
以上是该函数的源码, 最后一行中, 先执行 trylock_page(page) , 当其返回为 true 的时候, 则不再执行 __lock_page_or_retry . 那我们先来看一下 trylock_page(page):
static inline int trylock_page(struct page *page)
{
return (likely(!test_and_set_bit_lock(PG_locked, &page->flags)));
}
它是先判断 page 的 PG_locked 标志位, 然后再设置该标志位, 即, 如果该 PG_locked 标志位没被设置, 那么 trylock_page 返回true , 同时把该标志位置1.所以, 如果该 page 的 PG_locked 标志位当前没被置位, 即该 page 的 IO 操作被执行完了, 那么直接返回, 不会执行 __lock_page_or_retry ,也不会执行 wait_on_page_locked_killable 的阻塞.
相反, 如果该 page 的 PG_locked 标志位当前已经被置位了, 那么则会执行到 wait_on_page_locked_killable, 一直阻塞, 直到 PG_locked 被清除.显然, wait_on_page_locked_killable 阻塞的时间长, 说明该 page 的 PG_locked 标志位长时间处于1状态, 没被清除.该 PG_locked 标志位是在回写开始时和 IO 读完成时才会被清除.
一个 page 才 4KB 大小, IO 操作不至于很长时间的, 那又是为什么会出现卡在这么长时间呢?
PG_locked 是 page 的一个标志位, 而这个标志位又跟 IO 有关, 那么就肯定了这个 page 一定是 page cache 中的, 也就是说, 当磁盘的内容被读到内存之前是会设置 PG_locked 的标志位的, 然后等待磁盘的内容读出到 该 page 后才清除该标志位.
整个流程如下:
-
先从内存中分配一个 page.
-
把该 page 放入 page_cache 的 lru 里(即存放 page cache 的链表)
-
设置 page 的 PG_locked 标志位, 这里应该要注意的是: 此时并没有填充该 page, 即是一个空内容的 page, 用户拿到后是不可以使用的,所以这时需要设置该标志位做保护.
-
调用 block 层的 readpage 回调函数, 最终会调用 ext4_mpage_readpages.
-
最后会调用 submit_bio 申请一个 io 操作, 随后直接返回做别的事去了, 注意: 这里只是提交的 io 读操作的异步申请, 并没执行真正的读操作, 所以该 page 仍然处于空内容状态,即 PG_locked 仍然被置位.
-
block 层收到以上申请之后开始真正的把磁盘中的内容读到 page 中去.
-
当 io 操作完成后, 会回调 mpage_end_io 的函数.
- 在该函数中调用 unlock_page 这时才真正地把 PG_locked 标志位清除.
在以上流程中, 我们可以注意到, linux 系统的 page cache 链表中有时会出现一些还没准备好的 page(即还没把磁盘中的内容完全地读出来) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了. 只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长.