Archive for the ‘Linux’ category

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

进程控制踩过的坑

July 1st, 2015

1. fork()与vfork()非常相似,但是使用场景有一些不同,vfork()主要用来创建子进程,然后执行exec()一个新的程序,不会发生COW(fork()出来的子进程exec()会产生COW,所以vfork()更加快速),vfork()可以保证子进程先运行,调用exec()、exit之后才会被调度,如果子进程依赖父进程产生一些动作的话,可能产生死锁

2. vfork()在父进程空间中运行,这个导致子进程可以修改父进程的值!

3. 之前在C/S模型下Server 中fork()的健壮性中说过,fork()产生的子进程退出后,发送SIGCHLD信号,如果不及时使用wait方式处理的话,会产生僵尸进程。反过来,如果父进程先停止,那么子进程退出时,会向init进程发送SIGCHLD信号。

4. wait()与waitpid()都可以接受终止子进程发送的信号,wait()是waitpid()的简化版本,wait()返回任意一个终止子进程的状态,waitpid()可以接受特定子进程的信号。

5. 按照之前第3条所叙述的,我们可以利用这个init领养子进程规则让init管理孤儿进程,这里有一个技巧:fork()两次!

int main(void)
{
        pid_t pid;
        if ((pid = fork()) < 0) {
             err_sys("fork error");
        } else if (pid == 0) { /* first child */
             if ((pid = fork()) < 0)
                  err_sys("fork error");
             else if (pid > 0)
                  exit(0); /* parent from second fork == first child */
//这个exit(0)退出的就是第一次fork()出来的子进程,也是第二次fork()的
//父进程,当这个进程退出后,也就意味着第二次fork()出来的子进程变成
//孤儿进程,直接由init接管!
/*
* We’re the second child; our parent becomes init as soon
* as our real parent calls exit() in the statement above.
* Here’s where we’d continue executing, knowing that when
* we’re done, init will reap our status.
*/
//下面这段是第二次fork()出来子进程执行的代码段
            sleep(2);//必须保证第二次fork()出来的父进程先退出!
            printf("second child, parent pid = %ld\n", (long)getppid());
            exit(0);
        }
        if (waitpid(pid, NULL, 0) != pid) /* wait for first child */
            err_sys("waitpid error");
/*
* We’re the parent (the original process); we continue executing,
* knowing that we’re not the parent of the second child.
*/
        exit(0);
}

这个代码设计的很精巧,开始我没有看懂,仔细分析才可以。

6. 对于某些父子进程拥有竞争条件的代码,必须要使用信号机制或者管道机制实现父子进程同步,其中TELL_WAIT(),TELL_PARENT(),WAIT_PARENT(),TELL_CHILD(pid),WAIT_CHILD()可以使用不同的机制定义,从而实现父子进程的有序执行!

     TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx */
     if ((pid = fork()) < 0) {
      err_sys("fork error");
     } else if (pid == 0) { /* child */
     /* child does whatever is necessary ... */
     TELL_PARENT(getppid()); /* tell parent we’re done */
     WAIT_PARENT(); /* and wait for parent */
     /* and the child continues on its way ... */
     exit(0);
     }
    /* parent does whatever is necessary ... */
    TELL_CHILD(pid); /* tell child we’re done */
    WAIT_CHILD(); /* and wait for child */
    /* and the parent continues on its way ... */
    exit(0);

7. 使用信号机制来实现父子进程同步的话,可以自定义SIGUSR1,SIGUSR2的方式,在main()开始部位,设置中断处理函数,函数修改一个全局volatile sig_atomic类型的变量sigflag,然后在等待函数中,轮训挂起等待信号,直至进程处理信号,跳出这个循环:

while (sigflag == 0)
       sigsuspend(&zeromask); /* and wait for parent */
sigflag = 0;

8.使用pipe,可以在等待函数中读管道,在通知函数中写管道,达到父子进程的同步!

void TELL_PARENT(pid_t pid)
{
    if (write(pfd2[1], "c", 1) != 1)
        err_sys("write error");
}
void WAIT_PARENT(void)
{
    char c;
    if (read(pfd1[0], &c, 1) != 1)
        err_sys("read error");
    if (c != ’p’)
        err_quit("WAIT_PARENT: incorrect data");
}

 

 

参考:
APUE P185,P270,P402

memtest86+与BadRAM使用

June 28th, 2015

一般情况下,DRAM的损坏是永久性的损坏,这个时候我们有三种方式解决这个问题:

  1. 买新的内存条
  2. 在kernel启动时加入mem参数,限制当前mem的使用,比如当前内存2GB,在800M地方,内存存在永久故障区域,那么我们可以在kernel的启动参数加入mem=780M,这样就可以限制当前内核分配800M的内存区域而触发MCE。但是这个也有一个明显的缺点:因为这个坏点导致大部分内存无法使用
  3. 在kernel启动加入badram 0x01000000,0xfffffffc即可。

我们这里使用第三种方式来绕过当前的内存坏块,而达到省钱的目的!badram 第一个参数 是出错的物理基地址,第二个参数是mask,来用标示这个掩码。当我们使用memtest86+测试出当前的出错内存物理地址,然后将这个错误地址加入到kernel 参数中即可。

其中memtest86+是一款离线内存检测工具,可以检测内存中的坏页。

下面我们来实验一下,比如当前系统没有出现内存坏块,那么使用iomem参看当前物理内存地址的分布:

00000000-00000fff : reserved
00001000-0009f3ff : System RAM
0009f400-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000dffff : PCI Bus 0000:00
  000c0000-000c7fff : Video ROM
  000cc000-000cd7ff : Adapter ROM
000e0000-000effff : pnp 00:08
000f0000-000fffff : reserved
  000f0000-000fffff : System ROM
00100000-7f6dffff : System RAM
  01000000-017b9cc4 : Kernel code
  017b9cc5-01d1e8ff : Kernel data
  01e86000-01fc8fff : Kernel bss
....

当我们在grub中加入badram 0x01000000,0xfffffffc参数,也就意味着Kernel代码区的地址出现错误,而且错误大小是2bit,这个时候我们重新启动,再查看当前系统物理内存分布,我们会发现:

01000000-010003ff : RAM buffer
01000400-7f6dffff : System RAM
  02000000-027b9cc4 : Kernel code
  027b9cc5-02d1e8ff : Kernel data

kernel 的代码段避过了问题内存区域,我们查看__pa(x)最终调用__phys_addr_nodebug,而其中phys_base则是在real_mode下面被调用。

static inline unsigned long __phys_addr_nodebug(unsigned long x)
{
    unsigned long y = x - __START_KERNEL_map;

    /* use the carry flag to determine if x was < __START_KERNEL_map */
    x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));

    return x;
}

22996709_13505212884HWu

Real-mode code 在X+0x8000开始,X就是badram传入的offset!

 

 

参考:

http://ubuntuforums.org/archive/index.php/t-1689890.html

http://ubuntuforums.org/showthread.php?t=2278744

https://help.ubuntu.com/community/BadRAM#BADRAM_setting_in_Grub2

http://blog.chinaunix.net/uid-22996709-id-3376998.html

如何杀死一个内核线程

June 23rd, 2015

首先明确杀死一个进程与杀死一个kthread是不同的,杀死进程的时机是进程从内核态返回到用户态检查_TIF_SIGPENDING标志位,进一步进入到处理信号的函数进行处理杀死这个进程。

内核线程运行在整个内核之上,,如果不返回,则不可能检查信号,所以内核的线程实质上的停止与启动必须由线程本身状态决定,不允许随意杀死。如果这个线程正在持有某个全局锁时,强制杀死kthread会造成整个内核的死锁。所以目前kernel对于内核线程的停止主要依赖于线程内部的停止。

一种方式

发送信号,对于内核线程默认是对于信号是忽略的,所以我们要想停止一个线程必须在线程内部使用allow_signal(SIGKILL)方式,然后在内核线程代码的某个部位处理这个信号。所以发送信号的时机非常重要,如果当前kthread正在进行某些业务逻辑,那么发送SIGKILL无效。

另外一种方式

使用目前kernel提供工具函数int kthread_stop(struct task_struct *k) 用来对某个kthread进行停止。这个函数仅仅限于kthread_create()创建的内核线程,通过这个函数创建的内核线程都会被挂在kthreadd 内核线程树上。这种方式也可以被看作是一种发送信号的方式,但是这些函数已经被提供出来供编写者用来停止内核线程。线程内部必须显式的检查THREAD_SHOULD_STOP信号,从而使得线程return或者使用do_exit()退出线程[1]。否则无法停止内核线程。

当kthread_create()创建的内核线程时:

kthread_create
  -> kthread_create_on_node                              // in kthead.c
      -> adds your thread request to kthread_create_list
          -> wakes up the kthreadd_task

当唤醒kthreadd_task时,这个函数会运行kthreadd()。

pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
...
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);

kthreadd()这个函数会调用kthread()函数。kthread()函数 调用用户定义的内核线程函数。

kthreadd                                                 // all in kthread.c
  -> create_kthread
      -> kernel_thread(kthread, your_kthread_create_info, ...)

kthread()函数会调用我们自己创建的内核线程函数,当需要停止的时候,检查KTHREAD_SHOULD_STOP位,当返回后会将ret值传递到do_exit(ret),这个也就是我们不用显示调用do_exit()的原因。

kthread
  -> initialization stuff
    -> schedule() // allows you to cancel the thread before it's actually started
      -> if (!should_stop)
          -> ret = your_thread_function()
            -> do_exit(ret)

注意:内核线程return时,默认调用do_exit(ret),如果直接使用do_exit()退出线程,那么必须保证task_struct不被释放否则当继续执行kthread_stop()会释放一个无效的task_struct,导致发生Oops。[4]

当需要停止目标内核线程,kernel会获取当前描述目标内核线程状态的结构体kthread,设置KTHREAD_SHOULD_STOP标示位,然后唤醒这个目标线程,当前进程调用wake_for_completion(&kthread->exited)睡眠,被唤醒的条件其实就是这个目标内核线程的task_struct 上的vfork_done完成,这个标志位在do_exit()中被设置。当前进程/内核线程等待目标内核线程结束的过程时不可中断的,直到目标内核线程退出,最后释放task_struct结构体,这样就可以安全的停止当前线程。

int kthread_stop(struct task_struct *k)
{
        struct kthread *kthread;
        int ret;

        trace_sched_kthread_stop(k);

        get_task_struct(k);
        kthread = to_live_kthread(k);
        if (kthread) {
            set_bit(KTHREAD_SHOULD_STOP, &kthread->flags);
            __kthread_unpark(k, kthread);
            wake_up_process(k);
            wait_for_completion(&kthread->exited);
        }
        ret = k->exit_code;
        put_task_struct(k);

        trace_sched_kthread_stop_ret(ret);
        return ret;
}

上面的代码必须确保task_struct有效,如果无效,调用这个函数会发生Oops。

在内核线程中的业务处理逻辑外使用kthread_should_stop()检查当前线程的KTHREAD_SHOULD_STOP标志位,如果被设置,退出循环,就要执行线程的退出操作。

do {
        //do business
} while(!kthread_should_stop());

[1] http://v4l.videotechnology.com/dwg/kernelthreads/kernelthreads.html
[2] http://lwn.net/Articles/65178/
[3] http://blog.csdn.net/chinayangbo2011/article/details/8923731

[4] http://stackoverflow.com/questions/10177641/proper-way-of-handling-threads-in-kernel