Interrupt Handling [LDD3 10]

尽管device的IO region可以用来控制device,但是还不够。一个device,往往会有外界有交互,当外界发生了某个事件,需要device做出某种响应,driver也需要做处理。CPU不可能一直等device的某个event,而应该在event发生时,由device通知CPU。因此那就要有一种机制,让device能够通知到driver,这就是中断的作用。

所谓中断,就是硬件在需要CPU做出反应的时候发出的signal,kernel处理硬件中断的方式方法和user space处理signal是类似的。

driver能做的,其实就是实现一个interrupt handler,并注册给kernel,当device中断产生时,handler能够正确处理这个中断就可以了。在讲中断之前,需要强调一点,interrupt handler是和其他的code并发运行的,因此handler里一定要考虑竞争和并发的问题。

10.1. Preparing the Parallel Port


准备并口设备这里不讲了,因为没有并口设备。

10.2. Installing an Interrupt Handler


如果想在device driver中接收并处理设备产生的中断,得需要device driver告诉kernel怎么把中断传递给driver。如果没有匹配的handler,设备的中断会被忽略。

中断线(interrupt line)是kernel里很宝贵又很稀有的资源,一共只有15/16个,driver在使用中断之前要申请中断线资源,使用完后释放。在很多情况下,kernel中有些module driver可能要与别的drivershare同样的中断线。

获取中断资源的函数:

<linux/interrupt.h>
int request_irq(unsigned int irq, 
                irqreturn_t (*handler)(int, void *, struct pt_regs *),
                unsigned long flags,
                const char *dev_name,
                void *dev_id);

void free_irq(unsigned int irq, void *dev_id);

request_irq的返回值,如果是0,表明成功,如果返回负值,说明获取irq失败。下面是参数的说明:

unsigned int irq : driver请求的设备中断号

irqreturn_t (*handler)(int, void *, struct pt_regs *) : 中断发生时用来处理中断的handler

unsigned long flags  : 和中断管理相关的一些bitmask,包含SA_INTERRUPT,SA_SHIRQ,SA_SAMPLE_RANDOM,在下面会详细说明。

const char *dev_name  : 中断的owner,设备名字,在/proc/interrupts下会显示。

void *dev_id : 对于shared  interrupt line有用。一般是device的structure,或者一些private data,用来表明是谁的中断。如果不是shared interrupt line,可以设为NULL。

关于参数flags,有SA_INTERRUPT,SA_SHIRQ,SA_SAMPLE_RANDOM这几种:

SA_INTERRUPT: 如果设这个flag,说明是一个Fast的handler,Fast的handler运行的时候会关闭当前CPU的中断。

SA_SHIRQ:如果设这个flag,说明中断可以在设备之间share。

SA_SAMPLE_RANDOM: 如果你的device产生的中断比较随机,那么可以设这个flag,这样的话,OS上层需要产生随机数的时候,就可以把你的中断作为参考因素之一。

interrupt的handler可以在module driver初始化的时候注册,也可以在device被open的时候注册。LDD3推荐后者,也就是在device被open的时候才注册,因为中断线是有限的资源,有些device driver初始化的时候注册,但是后续可能并不会使用,这就造成了浪费,因此推荐在driver被open的时候才去请求中断,并注册handler。

一般来说,在device被open,但是还没有让device产生中断之前request_irq,在所有的device都被close之后再free_irq,这种方式的缺点在于,需要device driver自己保存device的计数,来确定什么时候关闭了所有的device。

一个使用request_irq的例子:

if (short_irq >= 0) {
    result = request_irq(short_irq, short_interrupt,
            SA_INTERRUPT, "short", NULL);
   if (result) {
        printk(KERN_INFO "short: can't get assigned irq %i\n",
                short_irq);
        short_irq = -1;
    }
    else { /* actually enable it -- assume this *is* a parallel port */
        outb(0x10,short_base+2);
    }
}

有些架构比如X86,有一个query irq是否可用的接口:

int can_request_irq(unsigned int irq, unsigned long flags);

如果irq当前没有人使用,就返回非零值。

10.2.1. The /proc Interface

每当hardware产生中断,并被CPU接收时,kernel就会把中断计数加1,这个计数可以用来check hardware是否正常工作。中断的计数可以通过/proc/interrupts来读取。另外,/proc/stat也可以出去interrupt的数量,区别在于stat里是自系统起来获取的中断数;而/proc/interrupts只统计当前active的中断数。比如这里有一个读取中断计数的例子:

root@montalcino:/bike/corbet/write/ldd3/src/short# m /proc/interrupts
           CPU0       CPU1       
  0:    4848108         34    IO-APIC-edge  timer
  2:          0          0          XT-PIC  cascade
  8:          3          1    IO-APIC-edge  rtc
 10:       4335          1   IO-APIC-level  aic7xxx
 11:       8903          0   IO-APIC-level  uhci_hcd
 12:         49          1    IO-APIC-edge  i8042
NMI:          0          0 
LOC:    4848187    4848186 
ERR:          0
MIS:          0

第一类是中断数IRQ number。从上面可以看出,CPU0一直在处理中断,而CPU1就很少,因为在同一个CPU上处理中断容易缓存命中,有助于提高performance。最后两列,第一个是interrupt controller,以及注册这个中断的设备名字。

从/proc/stat读取的中断信息:

intr 5167833 5154006 2 0 2 4907 0 2 68 4 0 4406 9291 50 0 0

第一个值表示所有中断的总和,后面的每一个都代表一个interrupt line(从0开始)。这里统计的是所有曾经产生的中断,即便你的device被关闭了,这里仍然可以看到它之前产生的中断,因此有时候,/proc/stat里的信息要比/proc/interrupts里的更有参考意义。

这两个文件的 另一个区别是:interrupts是与架构无关的,而stat是架构相关的。系统中可用的IRQ line根据架构的不同而不同,比如SPARC上只有16个IRQ line,而在IA-64上则有256个IRQ line。

下面这个是在IA-64上的/proc/interrupts的输出实例:

           CPU0       CPU1       
 27:       1705      34141  IO-SAPIC-level  qla1280
 40:          0          0           SAPIC  perfmon
 43:        913       6960  IO-SAPIC-level  eth0
 47:      26722        146  IO-SAPIC-level  usb-uhci
 64:          3          6   IO-SAPIC-edge  ide0
 80:          4          2   IO-SAPIC-edge  keyboard
 89:          0          0   IO-SAPIC-edge  PS/2 Mouse
239:    5606341    5606052           SAPIC  timer
254:      67575      52815           SAPIC  IPI
NMI:          0          0 
ERR:          0

10.2.2. Autodetecting the IRQ Number

因为driver必须要request irq,但是怎么知道该用哪个irq number呢?答案是auto detect。

某些设备可以按照经验值先指定中断号,然后尝试获取。比如:

if (short_irq < 0) /* not yet specified: force the default on */
    switch(short_base) {
        case 0x378: short_irq = 7; break;
        case 0x278: short_irq = 2; break;
        case 0x3bc: short_irq = 5; break;
    }

有些则是由device本身design的时候就决定使用哪个中断号,然后通过它的I/O port或者PCI configure space,driver可以读到device的中断号,像这类设备,所谓的auto detect就只需要probe device,并不需要做额外的工作来probe interrupt,目前大多数的设别其实都是按照这种方式工作的。比如PCI的标准已经支持这种操作,PCI总线可以知道PCI设备将要使用什么中断。

10.2.2.1 Kernel-assisted probing

对于某些没那么高级的设备,需要driver自己获取device使用的中断好,可以使用kernel提供的一些helper函数。

unsigned long probe_irq_on(void);

这个函数返回当前没有被使用的IRQ number的bitmask。device driver必须保存这个bitmask,并在probe_irq_off的时候传递进去。在调用完这个函数之后,device driver可以安排它的device产生中断了。

int probe_irq_off(unsigned long);

当device已经产生了中断,device driver调用上面的函数,把之前从probe_irq_on拿到的bitmask再传递进去。这个函数返回被probe on的IRQ number。如果没有interrupt被probe on,返回0;如果有多个被probe on(ambiguous detection),返回负值。

device driver需要在调用probe_irq_on之后才enable device的中断,在调用probe_irq_off之前就要disable device的中断。

这里是一个例子:

int count = 0;
do {
    unsigned long mask;

    mask = probe_irq_on(  );
    outb_p(0x10,short_base+2); /* enable reporting */
    outb_p(0x00,short_base);   /* clear the bit */
    outb_p(0xFF,short_base);   /* set the bit: interrupt! */
    outb_p(0x00,short_base+2); /* disable reporting */
    udelay(5);  /* give it some time */
    short_irq = probe_irq_off(mask);

    if (short_irq =  = 0) { /* none of them? */
        printk(KERN_INFO "short: no irq reported by probe\n");
        short_irq = -1;
    }
    /*
     * if more than one line has been activated, the result is
     * negative. We should service the interrupt (no need for lpt port)
     * and loop over again. Loop at most five times, then give up
     */
} while (short_irq < 0 && count++ < 5);
if (short_irq < 0)
    printk("short: probe failed %i times, giving up\n", count);

10.2.2.2 Do-it-yourself probing

极少数情况下,需要device driver完全自己probe IRQ。一般的解决办法都是,先enable所有的IRQ number,然后等着,看哪个IRQ number是有中断产生的,那个IRQ number就是自己device的IRQ number。然而,有些情况下,不需要测试所有的IRQ number,只需要测试个别的几个就可以了。比如这个例子,就是假设3,5,7,9里面的一个会被使用:

int trials[  ] = {3, 5, 7, 9, 0};
int tried[  ]  = {0, 0, 0, 0, 0};
int i, count = 0;

/*
 * install the probing handler for all possible lines. Remember
 * the result (0 for success, or -EBUSY) in order to only free
 * what has been acquired
 */
for (i = 0; trials[i]; i++)
    tried[i] = request_irq(trials[i], short_probing,
            SA_INTERRUPT, "short probe", NULL);

do {
    short_irq = 0; /* none got, yet */
    outb_p(0x10,short_base+2); /* enable */
    outb_p(0x00,short_base);
    outb_p(0xFF,short_base); /* toggle the bit */
    outb_p(0x00,short_base+2); /* disable */
    udelay(5);  /* give it some time */

    /* the value has been set by the handler */
    if (short_irq =  = 0) { /* none of them? */
        printk(KERN_INFO "short: no irq reported by probe\n");
    }
    /*
     * If more than one line has been activated, the result is
     * negative. We should service the interrupt (but the lpt port
     * doesn't need it) and loop over again. Do it at most 5 times
     */
} while (short_irq <=0 && count++ < 5);

/* end of loop, uninstall the handler */
for (i = 0; trials[i]; i++)
    if (tried[i] =  = 0)
        free_irq(trials[i], NULL);

if (short_irq < 0)
    printk("short: probe failed %i times, giving up\n", count);

// handler
irqreturn_t short_probing(int irq, void *dev_id, struct pt_regs *regs)
{
    if (short_irq =  = 0) short_irq = irq;    /* found */
    if (short_irq != irq) short_irq = -irq; /* ambiguous */
    return IRQ_HANDLED;
}

当然,如果你事先不知道到底哪些IRQ number会被使用,你只能从0到NR_IRQS-1,这样一个一个去试一试了。

10.2.3. Fast and Slow Handlers

就是字面的意思,有的handler处理的快,有的处理的慢,快的handler就是fast handler,慢的handler就是slow handler。

fast handler因为很快处理完,所以在处理期间保持当前CPU中断关闭就可以;如果是slow handler,就要注意了,不能一直关闭中断,因为很多别的device也要产生中断并等待CPU处理,slow的handler可能会导致这些device等待时间过长。在现在的kernel里,几乎没有fast和slow的明显界限,简单说,在处理期间需要关闭当前CPU中断的就是fast handler。

一般的device driver不推荐使用SA_INTERRUPT,这个flag主要给timer的interrupt来用。

10.2.3.1 The internals of interrupt handling on the x86

这里大概讲了一下x86系统上kernel内部中断机制的实现,参考文件:arch/i386/kernel/irq.c, arch/i386/kernel/apic.c, arch/i386/kernel/entry.S, arch/i386/kernel/i8259.c, and include/asm-i386/hw_irq.h。

首先是在entry.S里,这里是kernel中处理中断的最底层,用汇编做的实现。这里主要是把IRQ number压栈,然后跳到do_IRQ(在irq.c里)继续执行。

然后do_IRQ要做的第一步就是搞清楚这个IRQ number,这样后面的interrupt controller才知道怎么处理。先获取这个IRQ number的spinlock,防止别的CPU再处理这个IRQ,然后找到这个IRQ number对应的interrupt handler。如果这个IRQ number没有handler,do_IRQ后面就啥也不干,release这个IRQ number的spinlock,然后处理所有的software中断,再直接返回。

如果这个IRQ number有对应的handler,handle_IRQ_event就会调用它的handler。如果发现这个handler是slow的(没有设置SA_INTERRUPT),hardware的中断会被enable,然后handler被调用,之后就是cleanup,然后处理所有的软件中断,然后该干嘛干嘛去了(比如因为中断产生,需要wake up别的process)。

这里提到了之前probe_irq_on/probe_irq_off的工作原理:当调用了probe_irq_on的时候,kernel会把所有目前没有handler的IRQ number设置IRQ_WAITING,然后device driver打开了device的中断以后,do_IRQ会搜索所有的IRQ nubmber的handler,没有handler的就会把IRQ_WAITING这个flag清掉;probe_irq_off这个函数的功能就简单了,只需要搜索所有不带IRQ_WAITING这个标的就行了,不带这个标的IRQ number,是产生了中断,但是没有注册handler,一般就是device driver要找的那个IRQ number了。

10.3. Implementing a Handler


handler在实现的时候,因为是运行在中断时期,有一些限制条件需要注意,这些限制条件和kernel timer的限制条件类似:

1,不能和user space交换数据,因为运行在interrupt context,而不是process context。

2, 不能调用任何可能导致sleep的函数,比如wait_event,或者不是GFP_ATOMIC的kmalloc。

3, 不能通过semaphore加锁,因为semaphore会导致休眠。

4, handler里不能调用schedule函数,因为会放弃CPU。

中断的handler的角色,就是对设备的状态更新作出反应,比如根据中断的类型,读写数据等。很多device driver可能需要根据device的情况,在handler处理完当前的中断之前,把device的中断关闭,比如设一个寄存器等。

在handler的处理过程中,典型的任务就是唤醒某些等待event的线程,比如设备产生了数据,可能需要唤醒读写数据线程等。需要注意的是,handler占用的时间越少越好,如果中间需要做费时的操作,应该把费时的操作放在tasklet或者workqueue来做,由kernel的thread来负责完成这些操作,而handler应该尽快返回。

static inline void short_incr_bp(volatile unsigned long *index, int delta)
{
    unsigned long new = *index + delta;
    barrier(  );  /* Don't optimize these two together */
    *index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;
}

irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    struct timeval tv;
    int written;

    do_gettimeofday(&tv);

        /* Write a 16 byte record. Assume PAGE_SIZE is a multiple of 16 */
    written = sprintf((char *)short_head,"%08u.%06u\n",
            (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
    BUG_ON(written != 16);
    short_incr_bp(&short_head, written);
    wake_up_interruptible(&short_queue); /* awake any reading process */
    return IRQ_HANDLED;
}

以及对应的read/write实现:

ssize_t short_i_read (struct file *filp, char _ _user *buf, size_t count, 
     loff_t *f_pos)
{
    int count0;
    DEFINE_WAIT(wait);

    while (short_head =  = short_tail) {
        prepare_to_wait(&short_queue, &wait, TASK_INTERRUPTIBLE);
        if (short_head =  = short_tail)
            schedule(  );
        finish_wait(&short_queue, &wait);
        if (signal_pending (current))  /* a signal arrived */
            return -ERESTARTSYS; /* tell the fs layer to handle it */
    } 
    /* count0 is the number of readable data bytes */
    count0 = short_head - short_tail;
    if (count0 < 0) /* wrapped */
        count0 = short_buffer + PAGE_SIZE - short_tail;
    if (count0 < count) count = count0;

    if (copy_to_user(buf, (char *)short_tail, count))
        return -EFAULT;
    short_incr_bp (&short_tail, count);
    return count;
}

ssize_t short_i_write (struct file *filp, const char _ _user *buf, size_t count,
        loff_t *f_pos)
{
    int written = 0, odd = *f_pos & 1;
    unsigned long port = short_base; /* output to the parallel data latch */
    void *address = (void *) short_base;

    if (use_mem) {
        while (written < count)
            iowrite8(0xff * ((++written + odd) & 1), address);
    } else {
        while (written < count)
            outb(0xff * ((++written + odd) & 1), port);
    }

    *f_pos += count;
    return written;
}

10.3.1. Handler Arguments and Return Value

irq 的handler有三个参数:irq, dev_id,和 regs。

irq就是注册handler的时候使用的中断号,dev_id就是之前driver注册handler的时候传递的dev_id,这个是void *,一般driver都会传递private data,比如device structure的指针,然后在handler里根据这个data,就能知道是哪个device的interrupt,从而进行处理。比如这样的例子:

static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs 
                             *regs)
{
    struct sample_dev *dev = dev_id;

    /* now `dev' points to the right hardware item */
    /* .... */
}

static void sample_open(struct inode *inode, struct file *filp)
{
    struct sample_dev *dev = hwinfo + MINOR(inode->i_rdev);
    request_irq(dev->irq, sample_interrupt,
                0 /* flags */, "sample", dev /* dev_id */);
    /*....*/
    return 0;
}

regs(struct pt_regs *regs)是CPU调handler之前,当前CPU的process context的snapshot,device driver很少使用。

device driver的interrupt handler需要一个返回值,让kernel直到handler的中断处理是否完成。如果handler可以处理,就返回IRQ_HANDLED,否则返回IRQ_NONE。kernel也提供了宏生成返回值:

IRQ_RETVAL(handled)

10.3.2. Enabling and Disabling Interrupts

有时候driver里需要暂时block住interrupt的delivery,比如为了防止死锁,在拿了spinlock之后,device driver可能需要关闭硬件中断,无论是哪种用法,device driver中关闭硬件中断都是很少用的。

10.3.2.1 Disabling a single interrupt

kernel提供了专门的方式来disable、enable中断,再次强调,device driver中很少需要关闭中断,如果这个IRQ number还是share的,那就更加麻烦。

#include <asm/irq.h>
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

如果调用了上面的函数,kernel会更新PIC中对应IRQ number的mask,从而把所有CPU上的IRQ number中断打开或者关闭。需要注意的是,调用了多少次disable,就要调用多少次enable才能真正把interrupt enable起来。另外,当IRQ number被disable的时候,如果当前有IRQ number对应的handler正在执行,它会等到handler执行完成才会disable IRQ number。

10.3.2.2 Disabling all interrupts

上面的函数只能关闭某一个中断,下面的函数可以把当前CPU的中断全部关掉。

#include <asm/system.h>
//关闭当前CPU的中断,并保存之前的中断状态到flags里。
void local_irq_save(unsigned long flags);
//关闭当前CPU的中断,但是不保存任何状态。
void local_irq_disable(void);

//打开当前CPU的中断,并恢复中断状态位flags。
void local_irq_restore(unsigned long flags);
//无条件打开当前CPU的中断状态。
void local_irq_enable(void);

10.4. Top and Bottom Halves


也就是中断上半部和下半部。

通常driver可能需要在interrupt做很多费时的处理,比如读写数据,但是interrupt handler又不能执行太久,因为hardware的中断在hanlder执行的过程中是关闭的,handler不执行完,中断不会被打开。既要做很多事情,又要很快的做完,本身是冲突的,因此kernel的设计是把整个中断处理过程分为两个部分,也就是上半部和下半部。

上半部就是interrupt handler,也就driver调用request_irq注册给kernel里的部分,是中断发生时由kernel负责调用的部分;而下半部是driver自己的实现,通过上半部的调度来让下半部执行。

上半部和下半部一个很重要的区别是,上半部是关中断的,下半部中断是打开的,这样可以大大缩短中断被关闭的时间,从而提高性能。通常来说,上半部干的事情就是把device产生的数据保存起来,然后把下半部调度起来去处理这些数据,下半部的执行就很自由了,比如唤醒线程,做IO等;这样分开执行的好处是下半部在处理数据的同时,上半部可以继续处理新产生的中断。

下半部的实现方式有两种:tasklet和work queue。tasklet比较快速,但是要求driver的下半部是atomic context,work queue没有这些限制,但是延迟较高。

10.4.1. Tasklets

tasklet是运行在软中断上下文中,并且和其他的thread后者tasklet是并发的关系,所以如果driver有不止一个tasklet,那就要做好竞争条件的处理。

tasklet和调度它的handler是运行在同一个CPU上的,因此只有handler执行完以后,才会调用tasklet。但是考虑到tasklet执行的时候还会有新的中断产生,导致handler运行,tasklet和handler之前还是要做好资源保护。

//kernel定义tasklet原型
DECLARE_TASKLET(name, function, data);

//LDD3 sample code定义tasklet
void short_do_tasklet(unsigned long);
DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);

这里是LDD3 sample的interrupt handler:

irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    do_gettimeofday((struct timeval *) tv_head); /* cast to stop 'volatile' warning */
    short_incr_tv(&tv_head);
    tasklet_schedule(&short_tasklet);
    short_wq_count++; /* record that an interrupt arrived */
    return IRQ_HANDLED;
}

可以看到,interrupt handler里会调度tasklet,tasklet就会执行short_do_tasklet这个函数:

void short_do_tasklet (unsigned long unused)
{
    int savecount = short_wq_count, written;
    short_wq_count = 0; /* we have already been removed from the queue */
    /*
     * The bottom half reads the tv array, filled by the top half,
     * and prints it to the circular text buffer, which is then consumed
     * by reading processes
     */

    /* First write the number of interrupts that occurred before this bh */
    written = sprintf((char *)short_head,"bh after %6i\n",savecount);
    short_incr_bp(&short_head, written);

    /*
     * Then, write the time values. Write exactly 16 bytes at a time,
     * so it aligns with PAGE_SIZE
     */

    do {
        written = sprintf((char *)short_head,"%08u.%06u\n",
                (int)(tv_tail->tv_sec % 100000000),
                (int)(tv_tail->tv_usec));
        short_incr_bp(&short_head, written);
        short_incr_tv(&tv_tail);
    } while (tv_tail != tv_head);

    wake_up_interruptible(&short_queue); /* awake any reading process */
}

10.4.2. Workqueues

workqueue运行在kernel一个特殊的context里, 不是普通的process context,所以workqueue里可以休眠,但是不能访问user space,因为不是process context,就没有对应的user space。

这里是一个使用work queue的例子:

static struct work_struct short_wq;

    /* this line is in short_init(  ) */
    INIT_WORK(&short_wq, (void (*)(void *)) short_do_tasklet, NULL);

注意,这个work queue的sample里,work struct里的function仍然是short_do_tasklet。schedule的实现:

irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    /* Grab the current time information. */
    do_gettimeofday((struct timeval *) tv_head);
    short_incr_tv(&tv_head);

    /* Queue the bh. Don't worry about multiple enqueueing */
    schedule_work(&short_wq);

    short_wq_count++; /* record that an interrupt arrived */
    return IRQ_HANDLED;
}

从code上看,workqueue的实现和tasklet的实现差别很小,主要就是schedule的方式不一样,这里使用了schedule_work,而不是schdule_tasklet。

10.5. Interrupt Sharing


在最开始的时候,IRQ line是不能share的。后来,现代的硬件都支持IRQ line share,这样同一个IRQ line可以支持多个device的中断。大多数情况下,share IRQ line比较容易。

10.5.1. Installing a Shared Handler

shared interrupt的申请也是通过request_irq,只是和之前的方式有一些区别:

1. SA_SHIRQ这个bit在request_irq的flag中一定要设置。

2. dev_id这个pointer一定是unique的,但是一定不能是NULL。

kernel保存了share interrupt的所有handlers,并且拿dev_id作为区分各个handler的签名,因此这个值必须是global 唯一的。当满足以下两个条件时,request_irq会成功:

1. 这个interrupt line目前没有人使用。

2. interrupt line已经被人使用,但是别人也设置了share。

等被share的interrupt line发生了中断时,kernel会loop调用这个interrupt line上所有的handler,并且把他们对应的dev_id传递过去。当handler被调用时,需要检查是否是自己的device发生了中断,如果不是,handler必须返回IRQ_NONE。

如果handler需要从kernel里移除,仍然调用free_irq即可,kernel会根据传递进来的dev_id把对应的handler移除,因此dev_id必须是唯一的。

对于share的IRQ,有一点要注意,因为不止一个device在使用这个IRQ line,所以任何一个device driver都不要轻易的disable某个IRQ,否则对别的device可能产生比较大的影响。

10.5.2. Running the Handler

这里是一个例子:

irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    int value, written;
    struct timeval tv;

    /* If it wasn't short, return immediately */
    value = inb(short_base);
    if (!(value & 0x80))
        return IRQ_NONE;
    
    /* clear the interrupting bit */
    outb(value & 0x7F, short_base);

    /* the rest is unchanged */

    do_gettimeofday(&tv);
    written = sprintf((char *)short_head,"%08u.%06u\n",
            (int)(tv.tv_sec % 100000000), (int)(tv.tv_usec));
    short_incr_bp(&short_head, written);
    wake_up_interruptible(&short_queue); /* awake any reading process */
    return IRQ_HANDLED;
}

所有share IRQ的handler都会被调用,当handler被调用时,要自己判断是不是自己的device产生的中断,上面的例子是通过检查base address读出来的值是否有0x80来实现的。

真正的driver仍然需要把中断处理拆分为上半部和下半部,share IRQ对此没有影响。而且,driver可以使用dev_id来帮助判断是否是自己的device产生的中断。

10.5.3. The /proc Interface and Shared Interrupts

share interrupt对/proc/stat的输出没有影响,但是对/proc/interrupts是有影响的,看一下/proc/interrupts:

           CPU0       
  0:  892335412         XT-PIC  timer
  1:     453971         XT-PIC  i8042
  2:          0         XT-PIC  cascade
  5:          0         XT-PIC  libata, ehci_hcd
  8:          0         XT-PIC  rtc
  9:          0         XT-PIC  acpi
 10:   11365067         XT-PIC  ide2, uhci_hcd, uhci_hcd, SysKonnect SK-98xx, EMU10K1
 11:    4391962         XT-PIC  uhci_hcd, uhci_hcd
 12:        224         XT-PIC  i8042
 14:    2787721         XT-PIC  ide0
 15:     203048         XT-PIC  ide1
NMI:      41234 
LOC:  892193503 
ERR:        102
MIS:          0

可以看到,凡是share的IRQ,在最后一列会列出来哪些handler share了这个interrupt,从上面的输出就能看出来IRQ10,11等都是被share的IRQ。

10.6. Interrupt-Driven I/O


当user mode和device的读写交互可能发生延迟时,应该使用buffer。buffer的好处在于把read、write系统调用解放出来,不用block在数据交互的过程中,这样可以提高performance。

所谓interrupt-driver I/O,意思就是在interrupt发生时,发生的I/O操作。比如input buffer在中断发生时被填满,当user process把数据读走时被清空;output buffer在中断发生时被device读走清空,在user process在写时被填满。

为了实现上述的步骤,硬件应该按照下面的语法产生中断:

1, 对于input data,当device有新的数据时,要能够产生中断通知CPU。实际的行为取决于device使用了IO port,memory mapping,还是DMA。

2, 对于output data,当device准备好从buffer中读数据或者完成了data transfer时产生中断,通常只有memory mapping和DMA会在buffer传输完成之后产生中断。

10.6.1. A Write-Buffering Example

这里是一个例子:

    while (written < count) {
        /* Hang out until some buffer space is available. */
        space = shortp_out_space(  );
        if (space <= 0) {
            if (wait_event_interruptible(shortp_out_queue,
                        (space = shortp_out_space(  )) > 0))
                goto out;
        }

        /* Move data into the buffer. */
        if ((space + written) > count)
            space = count - written;
        if (copy_from_user((char *) shortp_out_head, buf, space)) {
            up(&shortp_out_sem);
            return -EFAULT;
        }
        shortp_incr_out_bp(&shortp_out_head, space);
        buf += space;
        written += space;

        /* If no output is active, make it active. */
        spin_lock_irqsave(&shortp_out_lock, flags);
        if (! shortp_output_active)
            shortp_start_output(  );
        spin_unlock_irqrestore(&shortp_out_lock, flags);
    }

out:
    *f_pos += written;

 

static void shortp_start_output(void)
{
    if (shortp_output_active) /* Should never happen */
        return;

    /* Set up our 'missed interrupt' timer */
    shortp_output_active = 1;
    shortp_timer.expires = jiffies + TIMEOUT;
    add_timer(&shortp_timer);

    /*  And get the process going. */
    queue_work(shortp_workqueue, &shortp_work);
}

 

posted on 2020-04-08 22:59  gapofsky  阅读(236)  评论(0)    收藏  举报

导航