首先感谢Intel的陈功给予的指导,在其帮助下才梳理里了PR_MCE_KILL_EARLY 的相关流程。这里写的比较粗,是主要流程,主要针对于如何选出要发送SIGBUS的任务的流程,其他的细节以后博客慢慢挖
1. 背景描述
先讲一下背景,在MCE的处理中,分为SRAO和SRAR两种。对应SRAR来说处理是紧急的,必须要current的上下文完成MCE的纠正处理;但对于SRAO来说,可以将纠正错误的行为放到一个特定的线程中进行处理而不用打段当前线程指令流。也就是说,可以做一个多线程的业务模型,主线程处理业务,另外特定线程处理SRAO错误,当发生SRAO的时候,SIGBUS会发送个特定线程进行处理而不影响主线程。
错误处理一般就是杀死被影响的进程(当然Intel从芯片角度期待不影响进程,但是从操作系统层面很难做到)而保证系统可以正常运行。在QEMU里面里面,这种恢复行为会有特殊处理的处理,以后博客提及。我们管必须在current上下文对错误处理完成的行为叫做KILL_EARLY,可以在特定线程进行处理的情况叫做KILL_LATE
好了,知道了上面的背景后,我们来看一下内核是如何处理的
2. 系统调用用户接口
首先看PR_MCE_KILL_EARLY的设定
SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
unsigned long, arg4, unsigned long, arg5)
{
......
case PR_MCE_KILL_SET:
current->flags |= PF_MCE_PROCESS;
if (arg3 == PR_MCE_KILL_EARLY) // 必须在current上下文执行完成的,设定PF_MCE_EARLY标志
current->flags |= PF_MCE_EARLY;
else if (arg3 == PR_MCE_KILL_LATE)
current->flags &= ~PF_MCE_EARLY;
else if (arg3 == PR_MCE_KILL_DEFAULT)
current->flags &= ~(PF_MCE_EARLY|PF_MCE_PROCESS);
else
return -EINVAL;
break;
......
}
可以看到,其实真正受影响的就是task_struct中的PF_MCE_EARLY标志位,这个标志位会在设定PR_MCE_KILL_EARLY的时候被设定到task的标志里面,这个标志位会在MCE的处理函数中使用
3. MCE处理主流程
接着看MCE的处理流程
void do_machine_check(struct pt_regs *regs, long error_code)
{
......
//一般所有的处理都会走到这里,memory_failure是标记内存有毒和错误恢复处理的主要函数
out:
......
if (memory_failure(recover_paddr >> PAGE_SHIFT, MCE_VECTOR, flags) < 0) {
pr_err("Memory error not recovered");
force_sig(SIGBUS, current);
}
......
}
/*memory_failure是标记内存有毒和错误恢复处理的主要函数 */
int memory_failure(unsigned long pfn, int trapno, int flags)
{
......
//错误处理, 标记内存有毒和错误恢复处理
if (hwpoison_user_mappings(p, pfn, trapno, flags, &hpage)
!= SWAP_SUCCESS) {
action_result(pfn, MF_MSG_UNMAP_FAILED, MF_IGNORED);
res = -EBUSY;
goto out;
}
......
}
/*核心处理流程*/
static int hwpoison_user_mappings(struct page *p, unsigned long pfn,
int trapno, int flags, struct page **hpagep)
{
......
/*STEP1: 收集有哪些进程需要被发送SIGBUS,放在一个链表tokill里面*/
//一般都会走到这里,注意这里的flags参数表示了是否是AR,如果是AR的情况传入的flags & MF_ACTION_REQUIRED不为0,也就是force_early的参数不为0
if (kill)
collect_procs(hpage, &tokill, flags & MF_ACTION_REQUIRED);
......
/*STEP2: 向收集到的tokill链表中的进程全部发送SIGBUS*/
forcekill = PageDirty(hpage) || (flags & MF_MUST_KILL);
kill_procs(&tokill, forcekill, trapno, ret != SWAP_SUCCESS, p, pfn, flags);
......
}
4. 收集内存错误相关的进程/线程
我们看看如何收集相关的进程
static void collect_procs(struct page *page, struct list_head *tokill, int force_early)
{
......
if (PageAnon(page))
/*以这个为例进行说明,下面的流程相似,只是要文件对应的内存页面*/
collect_procs_anon(page, tokill, &tk, force_early);
else
collect_procs_file(page, tokill, &tk, force_early);
......
}
static void collect_procs_anon(struct page *page, struct list_head *to_kill, struct to_kill **tkc, int force_early)
{
......
/*遍历所有的进程任务(注意不包括线程)*/
for_each_process (tsk) {
......
/*
* STEP1: 确定检查对象
*
* 确定检查对象是当前遍历到的进程还是进程下一个用来特定处理SIGBUS的线程
* 后面的处理会过滤掉没有使用错误页面内存的进程/线程
*/
struct task_struct *t = task_early_kill(tsk, force_early);
......
anon_vma_interval_tree_foreach(vmac, &av->rb_root, pgoff, pgoff) {
/*
* STEP2: 检查目标task对象是否是收到错误页面影响的任务
*
只有被遍历到的任务t使用了出错的内存,才会加入到to_kill链表里面。如,两个进程共享一块内存就会出现两个进程都加入到tokill里面的情况
一般情况下,只有当前进程(或其中的线程)会使用到当前出错的内存页,也就是说这里的t可以理解为current,或者current下的一个线程
注意vma来自于遍历av->rb_root,av来自于错误页面page(av = page_lock_anon_vma_read(page);), 所以vma->vm_mm == t->mm就表示了task使用了错误的页面
*/
if (vma->vm_mm == t->mm)
add_to_kill(t, page, vma, to_kill, tkc);
......
}
}
......
}
/*
* 查找要检查是否加入tokill的task对象
* force_early 不为0,也就是SRAR的情况,返回任务本身, 因为SRAR要求必须在当前任务上下文进行处理,后面需要检查当前任务是否加入tokill链表
* force_early 为0,也就是SRAO,
* 查看是否有标记了PF_MCE_EARLY 的子线程,如果有就返回该子线程,因为这个线程是处理MCE的特定线程 ,后面需要检查特定线程是否加入tokill链表
* 查看是否有标记了PF_MCE_EARLY 的子线程或者没有子线程,就返回进程本身,后面需要检查当前任务是否加入tokill链表
*/
static struct task_struct *task_early_kill(struct task_struct *tsk, int force_early)
{
......
/*
如果是SRAR,必须在当前上下文处理
如果是SRAR,那么force_early不为空,前面也说过,SRAR置位的时候,该值不为空
注意这里:如果是force_early, 就直接返回当前任务task,也就是前面说的,外面遍历所有的进程task,这里就是返回所有的进程task,但是在外层函数,一般只会将current加入tokill链表里面
所以这里可以简单的认为返回的是current
*/
if (force_early)
return tsk;
/*如果不是force_early 就要查找了,找到进程下第一个被标记了PF_MCE_EARLY 的子线程返回发送SIGBUS*/
t = find_early_kill_thread(tsk);
if (t)
return t;
......
}
/*查找给定任务下第一个被标记了PF_MCE_EARLY的子线程,如果没有返回NULL*/
static struct task_struct *find_early_kill_thread(struct task_struct *tsk)
{
......
/*返回第一个标记为PF_MCE_EARLY的task*/
for_each_thread(tsk, t)
if ((t->flags & PF_MCE_PROCESS) && (t->flags & PF_MCE_EARLY))
return t;
......
}
5. 发送SIGBUS给特定任务
那么我们看看如何向被影响的进程发送一个SIGBUS
static void kill_procs(struct list_head *to_kill, int forcekill, int trapno, int fail, struct page *page, unsigned long pfn, int flags)
{
......
/*遍历tokill链表,逐个发送SIGBUS*/
list_for_each_entry_safe (tk, next, to_kill, nd) {
/*forcekill表示页面被进程使用了,没法unmap,所以必须对任务发送SIGBUS*/
if (forcekill) {
......
if (fail || tk->addr_valid == 0) {
......
force_sig(SIGKILL, tk->tsk);
}
/*大部分走到这里,向特定的用户态进程发送SIGBUS*/
else if (kill_proc(tk->tsk, tk->addr, trapno, pfn, page, flags) < 0)
......
}
/*填写信号信息后,向给定的进程发送SIGBUS*/
static int kill_proc(struct task_struct *t, unsigned long addr, int trapno, unsigned long pfn, struct page *page, int flags)
{
......
si.si_signo = SIGBUS;
si.si_errno = 0;
si.si_addr = (void *)addr;
#ifdef __ARCH_SI_TRAPNO
si.si_trapno = trapno;
#endif
si.si_addr_lsb = compound_order(compound_head(page)) + PAGE_SHIFT;
......
/*SRAR的,如果要发送SIGBUS的task和当前Task拥有相同的MM,也就是共享一套内存,那么就说明内存错误可以在发送的任务里面处理,就想task发送SIGBUS,并且是AR*/
if ((flags & MF_ACTION_REQUIRED) && t->mm == current->mm) {
si.si_code = BUS_MCEERR_AR;
ret = force_sig_info(SIGBUS, &si, current);
} else {
/* 如果是SRAR不是共享mm,也就是说特定线程并没有涉及的错误内存,那么发送SRAO
* 如果是SRAO,也发送SRAO,
* 注意这里是异步发送信号的
*/
si.si_code = BUS_MCEERR_AO;
ret = send_sig_info(SIGBUS, &si, t); /* synchronous? */
}
......
}
6. 总结
PR_MCE_KILL_EARLY表示用户态进程是否开启一个线程(子线程/主线程)用来专门处理MCE的SIGBUS信号;
SRAR情况下,系统要求必须在当前任务上下文Action完成,所以SIGBUS会发给当前运行的进程/线程current
SRAO情况下,如果有标记了PR_MCE_KILL_EARLY的,用来专门处理MCE的SIGBUS信号的线程,那么就将SIGBUS发送给这个特定的线程
参考文章