老版本的 Linux 内核尽了很大努力来区分"快速"和"慢速"中断. 快速中断是那些能够很 快处理的, 而处理慢速中断要特别地长一些. 慢速中断可能十分苛求处理器, 并且它值得 在处理的时候重新使能中断. 否则, 需要快速注意的任务可能被延时太长.
在现代内核中, 快速和慢速中断的大部分不同已经消失. 剩下的仅仅是一个: 快速中断 (那些使用 SA_INTERRUPT 被请求的)执行时禁止所有在当前处理器上的其他中断. 注意其 他的处理器仍然能够处理中断, 尽管你从不会看到 2 个处理器同时处理同一个 IRQ.
这样, 你的驱动应当使用哪个类型的中断? 在现代系统上, SA_INTERRUPT 只是打算用在 几个, 特殊的情况例如时钟中断. 除非你有一个充足的理由来运行你的中断处理在禁止其 他中断情况下, 你不应当使用 SA_INTERRUPT.
这个描述应当满足大部分读者, 尽管有人喜好硬件并且对她的计算机有经验可能有兴趣深 入一些. 如果你不关心内部的细节, 你可跳到下一节.
这个描述是从 arch/i386/kernel/irq.c, arch/i386/kernel/ apic.c, arch/i386/kernel/entry.S, arch/i386/kernel/i8259.c, 和 include/asm- i386/hw_irq.h 它们出现于 2.6 内核而推知的; 尽管一般的概念保持一致, 硬件细节在 其他平台上不同.
中断处理的最低级是在 entry.S, 一个汇编语言文件处理很多机器级别的工作. 通过一点 汇编器的技巧和一些宏定义, 一点代码被安排到每个可能的中断. 在每个情况下, 这个代 码将中断号压栈并且跳转到一个通用段, 称为 do_IRQ, 在 irq.c 中定义.
do_IRQ 做的第一件事是确认中断以便中断控制器能够继续其他事情. 它接着获取给定 IRQ 号的一个自旋锁, 因此阻止任何其他 CPU 处理这个 IRQ. 它清除几个状态位(包括称 为 IRQ_WAITING 的一个, 我们很快会看到它)并且接着查看这个特殊 IRQ 的处理者. 如
果没有处理者, 什么不作; 自旋锁释放, 任何挂起的软件中断被处理, 最后 do_IRQ 返回.
常常, 但是, 如果一个设备在中断, 至少也有一个处理者注册给它的 IRQ. 函数 handle_IRQ_event 被调用来实际调用处理者. 如果处理者是慢速的( SA_INTERRUPT 没有 设置 ), 中断在硬件中被重新使能, 并且调用处理者. 接着仅仅是清理, 运行软件中断, 以及回到正常的工作. "常规工作"很可能已经由于中断而改变了(处理者可能唤醒一个进 程, 例如), 因此从中断中返回的最后的事情是一个处理器的可能的重新调度.
探测 IRQ 通过设置 IRQ_WAITING 状态位给每个当前缺乏处理者的 IRQ 来完成. 当中断 发生, do_IRQ 清除这个位并且接着返回, 因为没有注册处理者. probe_irq_off, 当被一 个函数调用, 需要只搜索不再有 IRQ_WAITING 设置的 IRQ.
实现一个处理
至今, 我们已学习了注册一个中断处理, 但是没有编写一个. 实际上, 对于一个处理者, 没什么不寻常的 -- 它是普通的 C 代码.
唯一的特别之处是一个处理者在中断时运行, 因此, 它能做的事情遭受一些限制. 这些限 制与我们在内核定时器上看到的相同. 一个处理者不能传递数据到或者从用户空间, 因为 它不在进程上下文执行. 处理者也不能做任何可能睡眠的事情, 例如调用 wait_event, 使用除 GFP_ATOMIC 之外任何东西来分配内存, 或者加锁一个旗标. 最后, 处理者不能调 用调度.
一个中断处理的角色是给它的设备关于中断接收的回应并且读或写数据, 根据被服务的中 断的含义. 第一步常常包括清除接口板上的一位; 大部分硬件设备不产生别的中断直到它 们的"中断挂起"位被清除. 根据你的硬件如何工作的, 这一步可能需要在最后做而不是开 始; 这里没有通吃的规则. 一些设备不需要这步, 因为它们没有一个"中断挂起"位; 这样 的设备是一少数, 尽管并口是其中之一. 由于这个理由, short 不必清除这样一个位.
一个中断处理的典型任务是唤醒睡眠在设备上的进程, 如果中断指示它们在等待的事件, 例如新数据的到达.
为坚持帧抓取者的例子, 一个进程可能请求一个图像序列通过连续读设备; 读调用阻塞在 读取每个帧之前, 而中断处理唤醒进程一旦每个新帧到达. 这个假定抓取器中断处理器来 指示每个新帧的成功到达.
程序员应当小心编写一个函数在最小量的时间内执行, 不管是一个快速或慢速处理者. 如 果需要进行长时间计算, 最好的方法是使用一个 tasklet 或者 workqueue 来调度计算在 一个更安全的时间(我们将在"上和下半部"一节中见到工作如何被延迟.).
我们在 short 中的例子代码响应中断通过调用 do_gettimeofday 和 打印当前时间到一 个页大小的环形缓存. 它接着唤醒任何读进程, 因为现在有数据可用来读取.
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 ",
(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;
}
这个代码, 尽管简单, 代表了一个中断处理的典型工作. 依次地, 它称为 short_incr_bp, 定义如下:
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;
}
这个函数已经仔细编写来回卷指向环形缓存的指针, 没有暴露一个不正确的值. 这里的 barrier 调用来阻止编译器在这个函数的其他 2 行之间优化. 如果没有 barrier, 编译 器可能决定优化掉 new 变量并且直接赋值给 *index. 这个优化可能暴露一个 index 的 不正确值一段时间, 在它回卷的地方. 通过小心阻止对其他线程可见的不一致的值, 我们 能够安全操作环形缓存指针而不用锁.
用来读取中断时填充的缓存的设备文件是 /dev/shortint. 这个设备特殊文件, 同
/dev/shortprint 一起, 不在第 9 章介绍, 因为它的使用对中断处理是特殊的.
/dev/shortint 内部特别地为中断产生和报告剪裁过. 写到设备会每隔一个字节产生一个 中断; 读取设备给出了每个中断被报告的时间.
如果你连接并口连接器的管脚 9 和 10, 你可产生中断通过拉高并口数据字节的高位. 这 可通过写二进制数据到 /dev/short0 或者通过写任何东西到 /dev/shortint 来完成.
[38]下列代码为 /dev/shortint 实现读和写:
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)
{
}
else
{
}
while (written < count)
iowrite8(0xff * ((++written + odd) & 1), address);
while (written < count)
outb(0xff * ((++written + odd) & 1), port);
*f_pos += count; return written;
}
其他设备特殊文件, /dev/shortprint, 使用并口来驱动一个打印机; 你可用使用它, 如 果你想避免连接一个 D-25 连接器管脚 9 和 10. shortprint 的写实现使用一个环形缓 存来存储要打印的数据, 而写实现是刚刚展示的那个(因此你能够读取你的打印机吃进每 个字符用的时间).
为了支持打印机操作, 中断处理从刚刚展示的那个已经稍微修改, 增加了发送下一个数据 字节到打印机的能力, 如果没有更多数据传送.