Posts Tagged ‘Exception’

MCA子系统分析

August 10th, 2015

Update 2015-6-15

machine check exception在Intel manual中是18号异常,主要用来处理硬件产生的异常,包括各种总线错误,内存错误,数据错误等。MCA子系统正是为了处理这个异常而设计的,由于这个子系统与寄存器紧密相关,所以我们需要阅读Intel manual chapter15后,才能理解本文章的一些函数与代码。

本文主要讨论SRAO,SRAR,UCNA类型的错误,其中SRAR错误错误等级最高,如果这个错误发生在kernel中,kernel默认panic。SRAO、UCNA等级不至于panic,但是当系统发生多次,仍有可能触发SRAR错误。

SRAO错误主要发生在Memory scrubbing 时,而Memory scrubbing主要通过定时ECC校验发现内存的单bit错误,但是对于多bit错误,导致内存错误失败,就会发生SRAO错误,发生这个错误意味着,系统检测到了内存错误,但是这个错误并没有被cpu使用,cpu的执行数据流仍然可以继续运行下去而不至于宕机。这就是RAS的目标。

下面函数就是MCA的初始化函数,__mcheck_cpu_ancient_init()初始化早期主板的MCA架构,类似于P4处理器。之后将do_machine_check()设定为MCE的异常处理函数,这里我不会展开。

__mcheck_cpu_init_generic()主要是用来初始化machine_check_poll(),这个函数主要用来处理UCNA/CE类型的错误,这个稍后进行介绍。__mcheck_cpu_init_vendor(c)主要是用来识别是Intel架构还是AMD架构,之后函数初始化了一个timer,用来定时调用machine_check_poll()轮询UCNA/CE错误。

最后又定义了一个工作队列mce_work,用来调用mce_process_work()函数,这个函数主要实现对SRAO类型错误的处理,最后并调用memory_failure()对错误进行恢复。(工作队列是处于进程上下文的的,这个也是memory_failure()要求的) mce_irq_work是一个信号队列,主要用来唤醒/dev/mcelog对于错误进行一个记录。

void mcheck_cpu_init(struct cpuinfo_x86 *c)
{
...
    if (__mcheck_cpu_ancient_init(c))
        return;
...
    machine_check_vector = do_machine_check;

    __mcheck_cpu_init_generic();
    __mcheck_cpu_init_vendor(c);
    __mcheck_cpu_init_timer();
    INIT_WORK(this_cpu_ptr(&mce_work), mce_process_work);
    init_irq_work(this_cpu_ptr(&mce_irq_work), &mce_irq_work_cb);
}

UCNA处理

下面我们来仔细说一下__mcheck_cpu_init_vendor(c);这个函数中完成了对于UCNA与Correctted Error的错误处理的初始化,通过下面初始化代码的函数调用关系,我们可以发现对于这两种类型的错误,handler就是intel_threshold_interrupt(),这个handler包括machine_check_poll()与mce_notify_irq()。这个函数本质上就是触发对于错误的记录,没有任何额外的操作,这就是叫UCNA(Uncorrected Error No Action)。machine_check_poll() 不对kernel产生任何影响,主要就是记录错误。

static void __mcheck_cpu_init_vendor(struct cpuinfo_x86 *c)
{
    switch (c->x86_vendor) {
    case X86_VENDOR_INTEL:
        mce_intel_feature_init(c);
....
}
void mce_intel_feature_init(struct cpuinfo_x86 *c)
{
    intel_init_thermal(c);
    intel_init_cmci();
}
static void intel_init_cmci(void)
{
...
    mce_threshold_vector = intel_threshold_interrupt;
...
}
static void intel_threshold_interrupt(void)
{
    if (cmci_storm_detect())
        return;
    machine_check_poll(MCP_TIMESTAMP, this_cpu_ptr(&mce_banks_owned));
    mce_notify_irq();
}

在最新的4.0.4代码里,machine_check_poll()包括下面的代码,所以对于UCNA类型的错误也只是将他记录在mce_ring中,之后使用memory_failure()进行处理,比如标记为HWPoison页框。

if (severity == MCE_DEFERRED_SEVERITY && memory_error(&m))
{
      if (m.status & MCI_STATUS_ADDRV) {
          mce_ring_add(m.addr >> PAGE_SHIFT);
          mce_schedule_work();
      }
}

SRAR/SRAO错误处理

综上所述do_machine_check()主要处理Fetal与SRAR/SRAO类型的错误,他会通过查表mce_severity()判断错误等级。找到这个SRAR类型错误发生位置,内核空间直接panic,用户空间杀死当前进程(进入machine check exception是一种NMI类型的异常,处于进程上下文),(对于发生在用户空间SRAR错误处理的时机就是把错误记录在mce_info结构体中,给当前进程设置TIF_MCE_NOTIFY标示,在返回用户空间时,调用mce_notify_process()-找出之前记录在struct mce_info的错误信息,进一步调用memory_failure()进行错误恢复处理)在最新的主线内核提交中,Luck, Tony <[email protected]>提交了一个commit。在最新代码中,他删除了mce_info,mce_save_info(),mce_find_info(),mce_clear_info(),mce_notify_process()和位于do_notify_resume()中的mce_notify_process(),也就是说SRAR不在返回用户态前处理。

x86, mce: Get rid of TIF_MCE_NOTIFY and associated mce tricks

We now switch to the kernel stack when a machine check interrupts
during user mode. This means that we can perform recovery actions
in the tail of do_machine_check()

他改变了SRAR发生在用户空间时,通过设置fiag并调度的方式,直接在do_machine_check()最后加入对于这种错误的处理,并在末位加入memory_failure()的错误恢复,这里指出如果恢复失败,那么直接使用force_sig(SIGBUS, current)。

对于SRAO类型的错误主要通过记录在mce_ring中,然后通过工作队列的方式调用mce_process_work()方式调用memory_failure()进行错误处理。下面代码来自于do_machine_check()。

      if (severity == MCE_AO_SEVERITY &amp;&amp; mce_usable_address(&amp;m))
            mce_ring_add(m.addr >> PAGE_SHIFT);

do_machine_check() 在最后会通过调用mce_report_event()->mce_irq_work->wake up /dev/mcelog 记录SRAR/SRAO错误。 所有内存问题,最后都会调用memory_failure()函数,这个函数就是对于问题页框进行标记,然后解除与进程的关系映射等。

 

参考:

Memory scrubbing

中断下半部的两种实现方式

http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

MCA 中的 Monarch’s region

July 27th, 2015

在do_machine_check()中,扫描banks的前后存在mce_start() 与 mce_end() 。这两个函数可以使得原有kernel中cpus 从并行执行转变为串行执行,先进入这个mce_start()函数的cpu便开始同步,这个函数主要等待所有的cpu都进入mce_start()后,才开始逐个扫描banks。

首先我们必须知道do_machine_check()中声明的order,no_way_out含义,order主要用来标示所有CPU进入handler的顺序。

no_way_out 大于0时,意味着kernel无法找到安全的方式恢复MCE(初始当前cpu no_way_out=0),而且会在进入mce_start()前首先通过mce_no_way_out()赋值一次no_way_out,判断出一次panic,当前cpu的no_way_out会被赋值为1。global_nwo标示一个全局的值,每个cpu都会有一个no_way_out,而global_nwo只有一个。

void do_machine_check(struct pt_regs *regs, long error_code)
{
... 
        int order;
        int no_way_out = 0; 
....
        no_way_out = mce_no_way_out(&m, &msg, valid_banks, regs);

        order = mce_start(&no_way_out);
        for (i = 0; i < cfg->banks; i++) {
             .... 
         }
 
        if (!no_way_out)
             mce_clear_state(toclear);
 
        if (mce_end(order) < 0)
             no_way_out = worst >= MCE_PANIC_SEVERITY;
....

根据代码,我们看到每个cpu都有超时限制,并会将当前的no_way_out加到全局global_nwo上(最后退出君主区域,正常系统应该为0,如果不为0,意味着有panic事件,总的来说global_nwo就是为了维持no_way_out的全局一致性),而mce_callin初始为0,首先到达此处的cpu将order+1。然后等待其他cpu自增,直到等于当前系统cpu的总数。如果等待超时,意味着出现不确定的问题,然后会在

if (mce_end(order) < 0)
        no_way_out = worst >= MCE_PANIC_SEVERITY;

直接赋值将no_way_out = 1,然后panic。

所有cpu都进入到mce_start()后,判断order(order=1的是Monarch ,order其他的是仆人,听命于Monarch),order=1时,这个君主将mce_executing置1,其他的cpu等待。

static int mce_start(int *no_way_out)
{
        int order;
        int cpus = num_online_cpus();
        u64 timeout = (u64)mca_cfg.monarch_timeout * NSEC_PER_USEC;

        if (!timeout)
                return -1;

        atomic_add(*no_way_out, &global_nwo);

        smp_wmb();
        order = atomic_inc_return(&mce_callin);

        while (atomic_read(&mce_callin) != cpus) {
                if (mce_timed_out(&timeout)) {
                        atomic_set(&global_nwo, 0);
                        return -1;
                }
                ndelay(SPINUNIT);
        }

        smp_rmb();

        if (order == 1) {
                /*
                 * Monarch: Starts executing now, the others wait.
                 */
                atomic_set(&mce_executing, 1);
        } else {
                while (atomic_read(&mce_executing) < order) {
                        if (mce_timed_out(&timeout)) {
                                atomic_set(&global_nwo, 0);
                                return -1;
                        }
                        ndelay(SPINUNIT);
                }
        }

        *no_way_out = atomic_read(&global_nwo);
        return order;
}

扫描完君主的banks后,君主cpu进入mce_end(),mce_executing增加,君主cpu等待其他仆人cpu扫描banks,直到所有的mce_executing都执行完毕(典型的cpu同步操作),此刻mce_executing==order==cpu的个数,最后由君主cpu再次确认no_way_out的值,我们可以认为no_way_out就是panic事件的个数。(此处我们要将理解为每个cpu都有一份独立的order,但是所有cpu靠原子mce_executing来做到cpu的同步。)

在int mce_end(int order)中,仆人cpu直接等待君主cpu的裁决(也就是return),然后君主cpu进入mce_reign(),这个函数就是根据所有cpu扫描的结果,做一次全面的统计,统计出global_worst,然后返回0,如果order出错,直接返回-1。

static int mce_end(int order)
{
        int ret = -1;
        u64 timeout = (u64)mca_cfg.monarch_timeout * NSEC_PER_USEC;

        if (!timeout)
                goto reset;
        if (order < 0)
                goto reset;

        atomic_inc(&mce_executing);

        if (order == 1) {

                int cpus = num_online_cpus();

                while (atomic_read(&mce_executing) <= cpus) {
                        if (mce_timed_out(&timeout))
                                goto reset;
                        ndelay(SPINUNIT);
                }

                mce_reign();
                barrier();
                ret = 0;
        } else {

                while (atomic_read(&mce_executing) != 0) {
                        if (mce_timed_out(&timeout))
                                goto reset;
                        ndelay(SPINUNIT);
                }
                return 0;
        }
reset:
        atomic_set(&global_nwo, 0);
        atomic_set(&mce_callin, 0);
        barrier();

        atomic_set(&mce_executing, 0);
        return ret;
}

退出君主区域,也就是MCA裁决当前系统应该采取哪种策略,是kill当前进程还是panic系统等,详细请看

do_machine_check() 函数分析

do_machine_check() 函数分析

July 20th, 2015

do_machine_check()函数是MCA架构最为核心的函数,在之前的 一篇博文 我们分析了现有MCA 对于现有错误的处理流程,但是没有对于do_machine_check函数进行分析。这里我们会深入分析这个异常处理函数,下面分析的代码是基于Linux-3.14.38,在今天一月份Andi Kleen 提交了最新的补丁,这个会在最后进行说明。

do_machine_check()异常处理函数是由18号异常触发,它执行在NMI上下文,不适用已有的kernel service服务和机制,甚至无法正常打印信息。开头主要是声明一些变量,mca_config 主要用来声明当前系统mca的基本配置,包括是否选择通过mcelog记录CE错误,CMCI中断使能等。

struct mca_config {
        bool dont_log_ce;
        bool cmci_disabled;
        bool ignore_ce;
        bool disabled;
        bool ser;
        bool bios_cmci_threshold;
        u8 banks;
        s8 bootlog;
        int tolerant;
        int monarch_timeout;
        int panic_timeout;
        u32 rip_msr;
};

severity 主要记录错误等级,order主要用于后来进入Monarch’s region的顺序,no_way_out 用来快速检查当前cpu是否需要panic,mce_gather_info(&m, regs);用来收集当前cpu MCA寄存器中的数据,保存到struct mce结构体中。

mces_seen是每个cpu都有存在的struct mce变量,当需要保存该值时,就将其保存在final中,final<=>mces_seen等价。

void do_machine_check(struct pt_regs *regs, long error_code)
{
        struct mca_config *cfg = &mca_cfg;
        struct mce m, *final;
        int i;
        int worst = 0;
        int severity;

        int order;
        int no_way_out = 0; 

        int kill_it = 0;
        DECLARE_BITMAP(toclear, MAX_NR_BANKS);
        DECLARE_BITMAP(valid_banks, MAX_NR_BANKS);
        char *msg = "Unknown";
        atomic_inc(&mce_entry);

        this_cpu_inc(mce_exception_count);

        if (!cfg->banks)
                goto out;

        mce_gather_info(&m, regs);

        final = &__get_cpu_var(mces_seen);
        *final = m;

        memset(valid_banks, 0, sizeof(valid_banks));
        no_way_out = mce_no_way_out(&m, &msg, valid_banks, regs);

        barrier();

检查 mcgstatus 寄存器中的MCG_STATUS_RIPV是否有效,无效,将kill_it置1,后续配合SRAR事件使用。

mce_start()与mce_end()可以使得所有cpu进入Monarch’s region,第一个进入handler的是Monarch,之后的cpu听从Monarch的指挥。之后按照cfg配置信息,扫描当前cpu所有bank,将当前MSR_IA32_MCx_STATUS()信息读取到m.status中,判断这个status是否有效,对于machine_check_poll() 处理的事件跳过处理。

判断当前bank内严重等级,如果severity错误等级是MCE_KEEP_SEVERITY 和 MCE_NO_SEVERITY ,忽略这次扫描,然后mce_read_aux()继续读取ADDR与MISC相关信息到当前struct mce m中。

这时当前severity等级是MCE_AO_SEVERITY 时,kernel会将其保存到ring buffer中,在之后的work queue中进行处理。然后调用mce_log(&m)记录到/dev/mcelog中。如果此次错误等级最高,那么更新 worst ,并struct mce写入到当前cpu。

扫描完毕之后,保存struct mce 信息,清除寄存器内容。再次判断worst 等级,高于MCE_PANIC_SEVERITY的话,稍后会panic。然后mce_end()退出 Monarch。

        if (!(m.mcgstatus & MCG_STATUS_RIPV))
                kill_it = 1;

        order = mce_start(&no_way_out);
        for (i = 0; i < cfg->banks; i++) {
                __clear_bit(i, toclear);
                if (!test_bit(i, valid_banks))
                        continue;
                if (!mce_banks[i].ctl)
                        continue;

                m.misc = 0;
                m.addr = 0;
                m.bank = i;

                m.status = mce_rdmsrl(MSR_IA32_MCx_STATUS(i));
                if ((m.status & MCI_STATUS_VAL) == 0)
                        continue;

                if (!(m.status & (cfg->ser ? MCI_STATUS_S : MCI_STATUS_UC)) &&
                        !no_way_out)
                        continue;

                add_taint(TAINT_MACHINE_CHECK, LOCKDEP_NOW_UNRELIABLE);

                severity = mce_severity(&m, cfg->tolerant, NULL);

                if (severity == MCE_KEEP_SEVERITY && !no_way_out)
                        continue;
                __set_bit(i, toclear);
                if (severity == MCE_NO_SEVERITY) {
                        continue;
                }

                mce_read_aux(&m, i);

                if (severity == MCE_AO_SEVERITY && mce_usable_address(&m))
                        mce_ring_add(m.addr >> PAGE_SHIFT);

                mce_log(&m);

                if (severity > worst) {
                        *final = m;
                        worst = severity;
                }
        }
        m = *final;

        if (!no_way_out)
                mce_clear_state(toclear);

        if (mce_end(order) < 0)
                no_way_out = worst >= MCE_PANIC_SEVERITY;

tolerant 在mca机制中可调, 0最严格,3最宽松。

/*
* Tolerant levels:
* 0: always panic on uncorrected errors, log corrected errors
* 1: panic or SIGBUS on uncorrected errors, log corrected errors
* 2: SIGBUS or log uncorrected errors (if possible), log corr. errors
* 3: never panic or SIGBUS, log all errors (for testing only)
*/

只要小于3,如果no_way_out等于1 ,直接panic,如果不是,worst等级是SRAR,那么标记这个进程,在返回用户态时处理,具体处理的函数就是memory_failure();如果不是SRAR错误,而且RIPV无效,那么只能杀死当前进程。

        if (cfg->tolerant < 3) {
                if (no_way_out)
                        mce_panic("Fatal machine check on current CPU", &m, msg);
                if (worst == MCE_AR_SEVERITY) {
                        /* schedule action before return to userland */
                        mce_save_info(m.addr, m.mcgstatus & MCG_STATUS_RIPV);
                        set_thread_flag(TIF_MCE_NOTIFY);
                } else if (kill_it) {
                        force_sig(SIGBUS, current);
                }
        }

        if (worst > 0)
                mce_report_event(regs);
        mce_wrmsrl(MSR_IA32_MCG_STATUS, 0);
out:
        atomic_dec(&mce_entry);
        sync_core();
}

在最新代码 Linux-4.0.4 中,Andi Kleen 删除了mce_info,mce_save_info(),mce_find_info(),mce_clear_info(),mce_notify_process()和位于do_notify_resume()中的mce_notify_process(),也就是说SRAR不在返回用户态前处理。

x86, mce: Get rid of TIF_MCE_NOTIFY and associated mce tricks
We now switch to the kernel stack when a machine check interrupts
during user mode. This means that we can perform recovery actions
in the tail of do_machine_check()

他改变了SRAR发生在用户空间时,通过设置fiag并调度的方式,直接在do_machine_check()最后加入对于这种错误的处理,并在末位加入memory_failure()的错误恢复,这里指出如果恢复失败,那么直接使用force_sig(SIGBUS, current)。

最后指出:do_machine_check()只处理SRAR、SRAO类型的错误,对于UCNA类型错误由machine_check_poll()处理,下篇博文介绍machine_check_poll()。

 

参考:

http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html

从用户态到内核态的切换

June 14th, 2015

我们都知道用户态向内核态切换有三种方式:1:系统调用 2:中断  3:异常 。之前只是知道这个切换的原理,但是并没有仔细的从代码上面学习切换的过程。之前我学习了页表,但是内核页表与进程页表是割裂的,信号处理也是割裂的。这里我会结合用户态到内核态从代码进行分析。

这部分切换的函数是用汇编代码写的。详细路径存在于arch/x86/kernel/entry_32.S文件中。

内核栈

内核栈:在Linux中每个进程有两个栈,分别用于用户态和内核态的进程执行(类似于内核页表与用户页表),其中的内核栈就是用于内核态的栈,它和进程的thread_info结构一起放在两个连续的页框大小的空间内。

在kernel 源代码中使用C语言定义了一个联合结构thread_union 方便地表示一个进程的thread_info和内核栈(定义在定义在include/linux/sched.h)

union thread_union {
     struct thread_info thread_info;
     unsigned long stack[THREAD_SIZE/sizeof(long)];
};

结合一个图可以看的更加清楚,其中栈是从高地址向低地址生长的,其中esp寄存器是CPU栈指针,存放内核栈栈顶地址。从用户态刚切换到内核态时,进程的内核栈总是空的(非常类似于进程页表切换到内核态页表,惰性传值),此时esp指向这个栈的顶端。

在x86中调用int指令系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。所以在进入内核态之前要保存进程的上下文,中断结束后恢复进程上下文,那靠的就是内核栈
Screenshot1
thread_info结构的定义如下

25 struct thread_info {
 26         struct task_struct      *task;          /* main task structure */
 27         struct exec_domain      *exec_domain;   /* execution domain */
 28         __u32                   flags;          /* low level flags */
 29         __u32                   status;         /* thread synchronous flags */
 30         __u32                   cpu;            /* current CPU */
 31         int                     saved_preempt_count;
 32         mm_segment_t            addr_limit;
 33         struct restart_block    restart_block;
 34         void __user             *sysenter_return;
 35 #ifdef CONFIG_X86_32
 36         unsigned long           previous_esp;   /* ESP of the previous stack in
 37                                                    case of nested (IRQ) stacks
 38                                                 */
 39         __u8                    supervisor_stack[0];
 40 #endif
 41         unsigned int            sig_on_uaccess_error:1;
 42         unsigned int            uaccess_err:1;  /* uaccess failed */
 43 };

内核栈保存用户态的esp,eip等寄存器的值,首先得知道内核栈的栈指针,那在进入内核态之前,通过TSS才能获得内核栈的栈指针。

TSS

TSS反映了CPU上的当前进程的特权级。linux为每一个cpu提供一个tss段,并且在tr寄存器中保存该段。在从用户态切换到内核态时,可以通过获取TSS段中的esp0来获取当前进程的内核栈 栈顶指针,从而可以保存用户态的cs,esp,eip等上下文。

注:linux中之所以为每一个cpu提供一个tss段,而不是为每个进程提供一个tss段,主要原因是tr寄存器永远指向它,在任务切换的适合不必切换tr寄存器,而且进程切换的时候只会切换新的esp0。结合scheduler()中的switch_to宏,next_p->thread.esp0装入对应于本地CPU的TSS的esp0字段(其实,任何由sysenter汇编指令产生的从用户态到内核态的特权级转换将把这个地址拷贝到esp寄存器中)。

内核代码中TSS结构的定义位于arch/x86/include/asm/processor.h文件。

其中主要的内容是:

  • 硬件状态结构:          x86_hw_tss(arch/x86/include/asm/processor.h)
  • IO权位图:     io_bitmap
  • 备用内核栈:        stack

linux的tss段中只使用esp0和iomap等字段,并不用它的其他字段来保存寄存器,在一个用户进程被中断进入内核态的时候,从tss中的硬件状态结构中取出esp0(即内核栈栈顶指针),然后切到esp0,其它的寄存器则保存在esp0指的内核栈上而不保存在tss中。

下面,我们看看INIT_TSS定义,其中init_stack是宏定义,指向内核栈 #define init_stack (init_thread_union.stack)

824 #define INIT_TSS  {                                                       \
825         .x86_tss = {                                                      \
826                 .sp0            = sizeof(init_stack) + (long)&init_stack, \
827                 .ss0            = __KERNEL_DS,                            \
828                 .ss1            = __KERNEL_CS,                            \
829                 .io_bitmap_base = INVALID_IO_BITMAP_OFFSET,               \
830          },                                                               \
831         .io_bitmap              = { [0 ... IO_BITMAP_LONGS] = ~0 },       \
832 }

内核栈栈顶指针、内核代码段、内核数据段赋值给TSS中的相应项。从而进程从用户态切换到内核态时,可以从TSS段中获取内核栈栈顶指针,进而保存进程上下文到内核栈中。

 

综上所述:

  • 读取tr寄存器,访问TSS段
  • 从TSS段中的esp0获取进程内核栈的栈顶指针
  • 由控制单元在内核栈中保存当前eflags,cs,ss,eip,esp寄存器的值。
  • 由SAVE_ALL保存其寄存器的值到内核栈
  • 把内核代码选择符写入CS寄存器,内核栈指针写入ESP寄存器,把内核入口点的线性地址写入EIP寄存器

此时,CPU已经切换到内核态,根据EIP中的值开始执行内核入口点的第一条指令。

 

参考:

http://guojing.me/linux-kernel-architecture/posts/process-switch/