Archive for August, 2015

阻塞,非阻塞访问与异步通知的比较

August 25th, 2015

最近在编写字符设备驱动,在使用场景上面存在不同的实现:阻塞I/O,非阻塞I/O和异步通知三种,之前都是朦朦胧胧知道三者区别,而没有认真的学习三者不同,这这篇文章中我会仔细的比较三者的区别。

设备的阻塞访问

指的是执行设备操作时如果无法回去资源,那么挂起进程,挂起的进程进入休眠状态,kernel将其从rq中移出,直到条件满足,示例代码:

char buf;
fd = open("/dev/ttyS1",O_RDWR);
...
res = read(fd,&buf,1);
if(res == 1)
   printf("%c\n",buf);

20150825112857

阻塞访问的优点就是节省CPU资源,资源没有得到满足,那么挂起即可,进程进入休眠状态,将cpu资源让给其他进程(当然如果进入休眠,那么当资源满足,我们需要一种方式唤醒这个休眠进程,可以使用信号)。阻塞I/O 一般使用等待队列来实现。

设备的非阻塞访问

指的是如果得不到资源,那么立即返回,并不挂起这个进程,我们可以不断的轮训这个设备,直到这个设备满足资源。

char buf;
fd = open("/dev/ttyS1",O_RDWR | O_NONBLOCK);
...
while(read(fd,&buf,1)!= 1)
   printf("%c\n",buf);

20150825112920

非阻塞访问的最大缺点是因为要不停的轮训设备,会浪费大量的cpu时间,但是我们可以借助sigaction通过异步通知的方式访问串口提高cpu利用率,说到非阻塞,通常会用到select() poll() 系统调用,这两个调用最后都会调用到驱动设备中的poll函数。

poll函数原型是unsigned int (* poll)(struct file *filp,struct poll_table *wait),在驱动里面,调用poll_wait() 向poll_table注册等待队列,当字符设备中存在数据时,return POLLIN,POLLRDNORM,POLLOUT。这里我们要注意:设备驱动的poll函数本身并不会阻塞,但是poll和select()系统调用会阻塞等待文件描述符集合中的至少一个可访问或者超时。

异步通知

异步通知的全程是“信号驱动的异步I/O”,也就是说一旦设备准备就绪,主动通知应用程序,这样应用程序根本就不需要查询设备状态。

20150825112911

我们可以使用信号来通知设备处理,其中STDIN_FILENO是int类型,不同于STDIN 的FILE * 类型,使用signal添加信号处理函数,使用fcntl()设置SIGIO信号被STDIN_FILENO接收,之后使用O_ASYNC 使得IO具有异步特性。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>

#define MAX_LEN 100

void input_handler(int num)
{
        char data[MAX_LEN];
        int len;

        len = read(STDIN_FILENO,&data,MAX_LEN);
        data[len] = 0;
        printf("input:%s\n",data);
}

int main()
{
        int oflags;
        signal(SIGIO,input_handler);
        fcntl(STDIN_FILENO,F_SETOWN,getpid());
        oflags = fcntl(STDIN_FILENO,F_GETFL);
        fcntl(STDIN_FILENO,F_SETFL,oflags | O_ASYNC);

        while(1);
}

 

 

[1] UNIX 高级编程

[2] Linux 设备驱动开发

[3] http://stackoverflow.com/questions/15102992/what-is-the-difference-between-stdin-and-stdin-fileno

[4] http://www.c4learn.com/c-programming/c-reference/fread-function/

MCE与mcelog之前的交互

August 21st, 2015

mcelog是在用户空间实现记录解码MCA报告的硬件错误信息的工具,而MCA则是一个内核机制,用来收集硬件错误信息。但是这个时候仅仅是一系列的错误代码,需要依靠用户空间的mcelog进行解码。二者是如何协调的呢?

通过查看相关代码,二者交互的接口是/dev/mcelog ,而mcelog在这个字符设备上休眠,直到mcelog被唤醒,读取这个字符设备中的信息,谁来唤醒这个daemo呢?

我们看到在mce代码初始化的时候,初始化了一个工作队列和一个irq队列,二者本质上调用的内容是一样的

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

...
    INIT_WORK(this_cpu_ptr(&mce_work), mce_process_work);
    init_irq_work(this_cpu_ptr(&mce_irq_work), &mce_irq_work_cb);
}

在do_machine_check()最后的代码调用了mce_report_event(),而这个函数包括两个部分,一个是通知mcelog读取字符设备,一个是记录通知等待队列mce_work,调用mce_process_work()记录这个MCE错误,通常这个错误是SRAO等级。

static void mce_report_event(struct pt_regs *regs)
{
         if (regs->flags & (X86_VM_MASK|X86_EFLAGS_IF)) {
                 mce_notify_irq();

                 mce_schedule_work();
                 return;
         }
         irq_work_queue(this_cpu_ptr(&mce_irq_work));
}

irq_work_queue()也是通过irq队列唤醒mce_irq_work_cb()函数,这个函数实质上还是mce_notify_irq()与mce_schedule_work()。

static void mce_irq_work_cb(struct irq_work *entry)
{
         mce_notify_irq();
         mce_schedule_work();
}

所以mce与mcelog最最核心的两个函数就是mce_notify_irq()与mce_schedule_work(),我们看到mce_notify_irq()首先唤醒了mce_chrdev_wait,这个正是mce_chrdev_poll()所等待的事件,/dev/mcelog字符驱动poll函数。

int mce_notify_irq(void)
{
...
         if (test_and_clear_bit(0, &mce_need_notify)) {
                 /* wake processes polling /dev/mcelog */
                 wake_up_interruptible(&mce_chrdev_wait);

                 if (mce_helper[0])
                         schedule_work(&mce_trigger_work);
...
}
static unsigned int mce_chrdev_poll(struct file *file, poll_table *wait)
{
         poll_wait(file, &mce_chrdev_wait, wait);
...
}

然后又唤醒mce_trigger_work工作队列,这个工作队列唤醒了mce_do_trigger工作函数call_usermodehelper(),这个函数非常神奇的地方在于可以从内核空间直接调用用户空间进程!

static void mce_do_trigger(struct work_struct *work)
{
         call_usermodehelper(mce_helper, mce_helper_argv, NULL, UMH_NO_WAIT);
}

第二个核心函数就是mce_schedule_work(),通过工作队列mce_work最终还是mce_process_work()->memory_failure()。

static void mce_schedule_work(void)
{
         if (!mce_ring_empty())
                 schedule_work(this_cpu_ptr(&mce_work));
}

这里代码逻辑其实很简单,但是使用了两种内核机制,最终还是记录到ring_buffer,memory_failure()修复,唤醒mcelog解码硬件错误信息,并将其记录到/var/log/mcelog。

 

具体查看 中断下半部的两种实现方式 中工作队列使用方式:

1. 通过下述宏动态创建一个工作:INIT_WORK(struct work_struct *work,void(*func)(void*),void *data);

2.每个工作都有具体的工作队列处理函数,原型如下:void work_handler(void *data)

3.将工作队列机制对应到具体的中断程序中,即那些被推后的工作将会在func所指向的那个工作队列处理函数中被执行。实现了工作队列处理函数后,就需要schedule_work函数对这个工作进行调度,就像这样:schedule_work(&work);

Linux 跨模块函数调用

August 20th, 2015

在编写模块的时候,我们经常会同时编写多个模块,模块中的函数,难免会有相互调用的需求,这个时候我们需要修改调用这个函数的Makefile文件,使其可以找到要调用的函数。

我们可以举例Module A 与 Module B,Module A中含有Module B要调用的函数,模块A中使用EXPORT_SYMBOL或EXPORT_SYMBOL_GPL将要提供给B模块的函数导出,具体Module A的代码形式如下:

void A_function(void)
{
        printk("A function");
        return;
}
EXPORT_SYMBOL(A_function);

这个时候我们通过make 可以生成一系列的中间文件,这里面包括Module.symvers,如果我们打开看这个符号表,可以发现这个文件包含函数虚拟地址,函数名,模块路径,导出形式EXPORT_SYMBOL。

然后我们在Module B中使用这个A_function,需要首先声明extern void A_function(void);然后才可以使用。

extern void A_function(void);

static int __init B_init(void)
{
        printk("B_func module init!\n");
        A_function();
        return 0;
}

我们已经把函数主体编写完毕,但是当我们使用insmod插入这个模块时,我们会发现系统提示Unknwon symbol,所以这个时候我们要向Module A的Makefile中加入Module B的Module.symvers,这样kernel在插入Module A时才知道Module B的位置。

obj-m:= module-B.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/lib/modules/$(LINUX_KERNEL)/build

KBUILD_EXTRA_SYMBOLS +=/home/dslab/kmod/huawei_hook/Module.symvers
export KBUILD_EXTRA_SYMBOLS

all:
 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
 make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

还有一种方式,就是在kernel源码 编译的时候加入到源码树中的Module.symers,然后编译内核,不过显然这种方式过于笨重,推荐使用修改Module A的Module.symvers这种方式。

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-&gt;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 &amp;&amp; memory_error(&amp;m))
{
      if (m.status &amp; 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 <tony.luck@intel.com>提交了一个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