LDD3 第10章 中断处理

时间:2021-09-01 04:02:15

各种硬件和处理器打交道的周期不同,并且总是比处理器慢。必须有一种可以让设备在产生某个事件时通知处理器----中断。

中断仅仅是一个信号,如果硬件需要,就可以发送这个信号。Linux处理中断方式和用户空间的信号是一样的。

注册一个中断,需要处理时,调用函数处理。

中断处理例程和其他代码并发运行,这样处理例程会不可避免的引起并发问题,并竞争数据结构和硬件。

一、安装和处理中断例程

中断信号线是非常珍贵且有限的资源,下列头文件<linux/sched.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);
/* requestid函数返回给请求函数的值为0,表示成功,负值为错误码
 * unsigned int irq:要申请的中断号
 * irqreturn_t (*handler)(int, void *, struct pt_regs *):这是要安装的中断处理函数指针
 * const char *dev_name:传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者
 * void *dev_id:这个指针用于共享的中断信号线
可以在flags中设置的位如下所示:
SA_INTERRUPT:当该位被设置时,表明这是一个“快递”的中断处理例程
SA_SHIRQ:该位表示中断可以在设备之间共享
SA_SAMPLE_RANDOM:该位指出产生中断能对/dev/random设备和/dev/urandom设别使用的熵池有贡献
 */

 在模块初始化时安装中断例程,可能并没有使用到它,但是却占用了宝贵的资源。

所以在模块打开时注册中断,以保证共享这些有限的资源。

request_irq正确位置是设备第一次打开,硬件被告知中断之前。free_irq的位置式最后一次关闭设备,硬件被告知不用再中断之后。

这种技术的缺点是需要维护一个计数器,这样我们才能知道什么时候可以禁用中断。

/proc接口

产生的中断报告显示在文件/proc/interrupts中。

文件/proc/interrupts给出了已经发送到系统上每一个CPU的中断数量。

文件/proc/stat中给出了一些系统活动低层统计信息,包括(但不限于)从系统启动开始收到的中断数量。

自动检测IRQ号

驱动程序初始化时,最迫切的问题之一是如何决定设备将要使用哪条IRQ信号线。中断号的自动检测对于驱动程序可用性来说是一个基本要求。

理论上很简单,实际实现起来就不那么清晰了。我们看看执行该任务的两种方法:调用内核定义的辅助函数,或者实现我们自己的版本。

内核帮助下的探测

内核提供了一个底层设施来探测中断号,它只能在非共享中断的模式下工作,但大多数硬件有能力工作在共享中断模式下,并可提供更好的知道配置中断的方法。

#include <linux/interrupt.h>
unsigned long probe_irq_on(void);
返回一个未分配中断的位掩码
int probe_irq_off(unsigned long);
在请求设备产生中断之后,驱动程序调用这个函数。并将前面on返回的位掩码作参数传递给它
off返回on之后发生的中断编号

 快速和慢速中断处理例程

老版本内核做了很多努力区分“快速和“慢速”中断。快速中断是那些可以很快被处理的中断,慢速中断则会花费更长的时间。

二、实现中断处理例程

中断处理例程和普通C程序没有区别,唯一特殊的地方就是处理器例程实在中断时间内运行的,它的行为会受到一些限制。

中断处理例程的功能就是将有关中断接收的信息反馈给设备,并根据正在服务的中断的不同含义对数据进行相应的读或写。

第一步通常需要清除接口卡上的一个位,大多数硬件设备在他们的"interrupt-pending"位被清除之前不回产生其他的中断。

中断例程典型的任务就是:如果中断通知进程所等待的事件已经发生,比如新的数据到达,就会唤醒在该设备上休眠的进程。

无论是快速还是慢速处理例程,程序员都应该编写执行事件尽可能短的处理例程。如果需要执行一个长时间的计算任务,最好的方法是使用tasklet或者工作队列在更安全的时间内调度计算任务。

short示例代码中,中断例程调用do_gettimeofday,并输出当前时间到大小为一页的循环缓冲区中,然后唤醒任何一个读进程,告诉该进程有新的数据可以换取。

LDD3 第10章 中断处理LDD3 第10章 中断处理
irqreturn_t short_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    struct timeval tv;
    int written;

    do_gettimeofday(&tv);

    /* 写入一个16字节的记录,假定PAGE_SIZE是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);    /*唤醒任何可读进程 */
    return IRQ_HANDLED;   
}
short_interrupt

调用的short_incr_bp函数定义如下:

LDD3 第10章 中断处理LDD3 第10章 中断处理
static inline void short_incr_bp(volatile unsigned long *index, int data)
{
    unsigned long new = *index + delta;
    barrier();        /* 禁止对前后两条语句的优化 */
    *index = (new >= (short_buffer + PAGE_SIZE)) ? short_buffer : new;
}
short_incr_bp

这个函数非常谨慎,它可以将指针限制在循环缓冲区的范围之内,并且不会因为传递一个不正确的值而返回。index可能会被编译器优化,barrier调用和volatile防止出现新的变量。

/dev/shortint的读取和写入

LDD3 第10章 中断处理LDD3 第10章 中断处理
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);    /* 某个信号已到达 */
            return -ERESTARTSYS;      /* 告诉fs层来做进一步处理 */
    }
    /* count0 是可读取数据的字节数 */
    count0 = short_head - short_tail;
    if(count0 < 0)     /* 已交换 */
        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, count char __user *buf, size_t count, loff_t *f_pos)
{
    int written = 0, odd = *f_pos & 1;
    unsigned long port = short_base;    /* 输出到并口的数据锁存器 */
    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;
}
read and write

处理例程的参数及返回值

处理函数有三个参数被传递给了中断处理例程:irq、dev_id和regs。让我们看看每个参数的意义。

中断号(int irq)是可以打印到日志消息,第二个参数void *dev_id是一种客户数据类型(驱动程序的私有数据)。传递给request_irq函数的void *参数会在中断发生时作为参数被传回处理例程。通常dev_id传递一个纸箱自己设备的数据结构指针。这样多个设备的驱动程序在中断处理例程中不需要做任何额外的代码,就可以找出那个设备产生了当前中断事件

中断处理例程中参数的典型用法:

LDD3 第10章 中断处理LDD3 第10章 中断处理
static irqreturn_t sample_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    struct sample_dev *dev = dev_id;
    /* 现在,dev指向正确的硬件项 */
    /* ... */
}
sample_interrupt

与这个处理例程相关联的典型open代码:

LDD3 第10章 中断处理LDD3 第10章 中断处理
static void sample_open(struct inode *inode, stuct file *filp)
{
    struct sample_dev *dev = hwinfo + MINO(indoe->i_rdev);
    request_irq(dev->irq, sample_interrupt,
        0 /* flags */, "sample", dev /* dev_id */);

    /* ... */
    return 0;
}
sample_open

最后一个参数struct pt_reg *regs很少使用,它保存了处理器进入中断代码之前的处理器上下文快照。

中断处理例程应该返回一个值,用来指明是否真正处理了一个中断。需要处理返回IRQ_HANDLED,否则,返回IRQ_NONE。

也可以通过,如果要处理handled非0

IRQ_RETVAL(handled)

启用和禁用中断

驱动程序中应尽量少禁用中断,同时这种技术不用在驱动程序汇总作为互斥机制使用。

禁用单个中断

LDD3 第10章 中断处理LDD3 第10章 中断处理
#include <asm/irq.h>
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
禁用函数

调用这些函数会更新可编程中断控制器(programmable interrupt controller, PIC)中指定中断的掩码,因而就可以在所有的处理器上禁用或者启用IRQ。

这些函数的调用是可以嵌套的,调用disable_irq两次成功,那么在IRQ重启前,需要调用两次enable_irq

禁用所有的中断

LDD3 第10章 中断处理LDD3 第10章 中断处理
#include <asm/system.h>
void local_irq_save(unsigned long flags);
void local_irq_disable(void);

/* 打开 */
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
函数原型

三、顶半部和底半部

中断处理的一个主要问题是怎样在处理例程内完成耗时的任务。响应一次设备中断需要完成一定数量的工作,但是中断处理例程需要尽快结束而不能使中断阻塞的时间过长。

LInux将中断处理例程分成两部分解决这个问题。顶半部实际响应中断的例程,底半部是一个被顶半部调度,并在稍后更安全的时间内执行的例程。

这之间的区别在于,底半部处理例程执行时,所有的中断都是打开的。典型的情况就是顶半部保存设别的数据到一个设备特定的缓冲区并调度它的底半部,然后退出。

底半部有两种处理机制:tasklet和工作队列。

tasklet

tasklet是一个可以在由系统决定的安全时刻在软件中断上下文被调度运行的特殊函数。它可以被多次调度运行,但tasklet不会积累,只运行一次。

所以如果驱动程序有多个tasklet,他们必须使用某种锁机制来避免彼此间的冲突。

tasklet运行时,当然可以有其他的中断发生,因此在tasklet和中断处理例程之间的锁还是需要的。

必须使用宏DECLARE_TASKLET声明tasklet:

LDD3 第10章 中断处理LDD3 第10章 中断处理
DECLARE_TASKLET(name, function, data);
name:是给tasklet起的名字
function:是执行tasklet时调用的函数(它有一个unsigned long型的参数并且返回void)
data:是一个用来传递给tasklet函数的unsigned long类型的值
DECLARE_TASKLET

驱动程序short如下声明它自己的tasklet:

LDD3 第10章 中断处理LDD3 第10章 中断处理
void short_do_tasklet(usnigned long);
DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);
DECLARE_TASKLET

这个处理例程保存数据并如下调度tasklet:

LDD3 第10章 中断处理LDD3 第10章 中断处理
irqreturn_t short_tl_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    do_gettimeofday((struct timeval *)tv_head);    /* 强制转换以免出现“易失性”警告 */
    short_incr_tv(&tv_head);
    tasklet_schedule(&short_tasklet);
    short_wq_count++;    /* 记录中断的产生 */
    return IRQ_HANDLED;
}
short_tl_interrupt

这个例程执行中断处理的大多数任务,如下所示:

LDD3 第10章 中断处理LDD3 第10章 中断处理
void short_do_tasklet(unsigned long unused)
{
    int savecount = short_wq_count, written;
    short_wq_count = 0; /* 已经从队列中移除 */
    /*
     * 底半部读取由顶半部填充的tv数组,
     * 并向循环文本缓冲区中打印信息,而缓冲区的数据则由
     * 读取进程获得
     */

    /* 首先将调用此bh之前发生的中断数量写入 */
    written = sprintf((char *)short_head, "bh after %6i\n", savecount);
    short_incr_bp(&short_head, written);
    
    /*
     * 然后写入时间值,每次写入16字节
     * 所以它与PAGE_SIZE是对齐的
     */
    do {
        written = sprinf((char *)short_head, "%08u.%06u\n",
                (int)(tv_tail->tv_sec % 100000000),
                (int)(tv_tail->tv_usec));
        short_incr_bp(&hsort_head, written);
        short_incr_tv(&tv_tail);
    } while(tv_tail != tv_head);
    wake_up_interruptible(&short_queue);    /* 唤醒任何读取进程 */
}
short_do_tasklet

工作队列

工作队列会在将来的某个事件、在某个特殊的工作者进程上下文中调用一个函数。

工作队列函数运行在进程上下文中,必要时可以休眠。但是我们不能从工作队列向用户空间复制数据,除非使用高级技术。

work_struct结构,该结构如下声明并初始化:

static struct work_struct short_wq;
/* 下面这行出现在short_init()中 */
INIT_WORK(&short_wq, (void (*)(void *))short_do_tasklet, NULL);

在使用工作队列时,short构造了另一个中断处理例程,如下:

LDD3 第10章 中断处理LDD3 第10章 中断处理
irqreturn_t short_wq_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    /* 获取当前的时间信息 */
    do_gettimeofday((struct timeval *)tv_head);
    short_incr_tv(&tv_head);

    /* 排序bh,不必关心多次调度的情况 */
    schedule_work(&short_wq);

    short_wq_count++;        /* 记录中断的到达 */
    return IRQ_HANDLED;
}
short_wq_interrupt

四、中断共享

现在现代硬件已经能允许中断的共享了。

安装共享的处理例程

  • 请求中断时,必须指定falgs参数中的SA_SHIRQ位
  • dev_id参数必须是唯一的。任何指向模块地址空间的指针都可以使用,但dev_id不能设置成NULL

当请求一个共享中断时,如果满足下面条件,request_irq就会成功。

  • 中断信号线空闲
  • 任何已经注册了该中断信号线的处理例程也标识了IRQ是共享的

当两个或者更多的驱动程序共享同一个根中断信号线,而硬件又通过这根信号线中断处理器时,内核会调用每一个为这个中断注册的处理例程,并将它们自己的dev_id传回去。

运行处理例程

LDD3 第10章 中断处理LDD3 第10章 中断处理
irqreturn_t short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    int value, written;
    struct timeval tv;

    /* 如果不是short产生的,则立即返回 */
    value = inb(short_base);
    if(!(value & 0x80))
        return IRQ_NONE;

    /* 清除中断位 */
    outb(value & 0x7F, short_base);

    /* 其余部分没有什么变化 */
    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);    /* 唤醒任何的读取进程 */
    return IRQ_HANDLED;
}
short_sh_interrupt

/proc接口和共享的中断

系统上安装的中断处理例程不会对/proc/stat造成影响,所有为同一个中断号安装的处理例程会出现在/proc/interrupts文件的同一行上。

中断驱动的I/O

如果与驱动程序管理的硬件之间的数据传输因为某种原因延迟的话,驱动程序作者就应该实现缓冲。数据缓冲区有助于将数据传送和接收与系统调用write和read分离开来,从而提高系统的整体性能。

一个好的缓冲机制需要采用中断驱动的I/O,一个输入缓冲区在中断时间内被填充,并由读取该设备的进程取走缓冲区内的数据。一个输出缓冲区由写入设备的进程填充,并在中断时间内取走数据。

要正确进行中断驱动的数据传输,要求硬件能按照下面的语义来产生中断:

  • 对于输入来说,新数据到达并且准备好接收时,设备就中断CPU。实际执行的动作取决于设备使用的是I/O端口、内存映射、还是DMA。
  • 对于输出来说,当设备准备好接收新数据或对陈功的数据传输进行应答时,就是发出中断。内存映射和具有DMA能力的设备,通常通过产生中断来通知系统他们对缓冲区的处理已经结束。

写缓冲区示例

LDD3 第10章 中断处理LDD3 第10章 中断处理
while(written < count) {
    /* 挂起直到有可用缓冲区空间为止 */
    space = shortp_out_space();
    if(space <= 0) {
        if(wait_event_interruptible(shortp_out_queue,
                (sapce = shortp_out_space()) > 0)
            goto out;
    }
    /* 将数据移动到缓冲区 */
    if((space + written) > count)
        space = count - written;
    if(copy_from_user((char *)shortp_out_head, buf, space)) {
        up(&shortp_out_sem);
        return -EFAULT;
    }
    shorp_incr_out_bp(&shortp_out_head, space);
    buf += space;
    written += space;
    /* 如果没有激活的输出,则激活 */
    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;
写缓冲

启动输出进程的函数如下所示:

LDD3 第10章 中断处理LDD3 第10章 中断处理
static void shortp_start_output(void)
{
    if(shortp_output_active)        /* 不应发生 */
        return;

    /* 设置“丢失中断”定时器 */
    shorp_output_active = 1;
    shorp_timer.expires = jiffies + TIMEOUT;
    add_timer(&shortp_timer);

    /* 然后让进程继续 */
    queue_work(shortp_workqueue, &shortp_work);
}
shortp_start_output

有时候会丢失来自设备的中断,我们不希望永久停止直到系统重启。利用内核定时器意识到中断丢失并继续处理则会好得多。其核心功能如下:

LDD3 第10章 中断处理LDD3 第10章 中断处理
spin_lock_irqsave(&shortp_out_lock, flags);

/* 是否有数据写入 */
if(shortp_out_head == shortp_out_tail) {     /* 空的 */
    shortp_output_active = 0;
    wake_up_interruptible(&shortp_empty_queue);
    del_timer(&shortp_timer);
}
/* 否则写入其他字节 */
else 
    shortp_do_write();

/* 如果有人等待,则唤醒之。 */
if(((PAGE_SIZE + shortp_out_tail - shortp_out_head) % PAGE_SIZE) > SP_MIN_SPACE) {
    wake_up_interruptible(&shortp_out_queue);
}
spin_unlock_irqrestore(&shortp_out_lock, flags);
核心代码

数据真正写入端口的代码:

LDD3 第10章 中断处理LDD3 第10章 中断处理
static void shortp_do_write(void)
{
    unsigned char cr = inb(shortp_base + SP_CONTROL);

    /* 有情况发生,重置定时器 */
    mod_timer(&shortp_timer, jiffies + TIMEOUT);
    /* 想设备输出一个字节 */
    outb_p(*shortp_out_tail, shortp_base + SP_DATA);
    shortp_incr_out_bp(&shortp_out_tail, 1);
    if(shortp_delay)
        udelay(shortp_delay);
    outb_p(cr | SP_CR_STROBE, shortp_base + SP_CONTROL);
    if(shortp_delay)
        udelay(shortp_delay);
    outb_p(cr & ~SP_CR_STROBE, shortp_base + SP_CONTROL);
}
shortp_do_write

然后会中CPU,shortprint使用的汇总单处理例程很短,也很简单:

LDD3 第10章 中断处理LDD3 第10章 中断处理
static irqreturn_t shortp_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    if(!shortp_output_active)
        return IRQ_NONE;
    /* 记录时间,其他信息在工作队列函数中获得 */
    do_gettimeofday(&shortp_tv);
    queue_work(shortp_workqueue, &shortp_work);
    return IRQ_HANDLED;
}
shortp_interrupt

如果中断始终不产生会怎样呢?驱动程序代码会导致停顿。为了避免这种情况发生

LDD3 第10章 中断处理LDD3 第10章 中断处理
static void shortp_timeout(unsigned long unused)
{
    unsigned long flags;
    unsigned char status;

    if(!shortp_output_active)
        return;
    spin_lock_irqsave(&shortp_out_lock, flags);
    status = inb(shortp_base + SP_STATUS);

    /* 如果打印机任然忙,则只是重置定时器 */
    if((status & SP_SR_BUSY) == 0 || (status & SP_SR_ACK)) {
        shortp_timer.expires = jiffies + TIMEOUT;
        add_timer(&shortp_timer);
        spin_unlock_irqqrestore(&shortp_out_lock, flags);
        return;
    }
    /* 否则必须调解中断处理例程 */
    spin_unlock_irqrestore(&shortp_out_lock, flags);
    shortp_interrupt(shortp_irq, NULL, NULL);
}
shortp_timeout