第 12 章. 中断处理程序

中断处理程序

中断处理程序

除了最后一章,到目前为止我们在内核中所做的一切都是响应进程的请求,通过处理一个特殊文件,发送一个ioctl(),或发出一个系统调用。但是内核的工作不仅仅是响应进程请求。另一项同样重要的工作是与连接到计算机的硬件进行通信。

CPU 和计算机的其他硬件之间有两种类型的交互。第一种类型是 CPU 向硬件发出指令,另一种类型是硬件需要告诉 CPU 一些事情。第二种类型,称为中断,更难实现,因为它必须在硬件方便时处理,而不是 CPU 方便时。硬件设备通常只有非常少量的 RAM,如果你没有在信息可用时读取它们,信息就会丢失。

在 Linux 下,硬件中断被称为 IRQ(I中断Rq求)[1]。IRQ 有两种类型,短中断和长中断。短中断是指预期占用非常短的时间段的中断,在此期间机器的其余部分将被阻塞,并且不会处理其他中断。长中断是指可能占用更长时间的中断,在此期间可能会发生其他中断(但不是来自同一设备的中断)。如果可能,最好将中断处理程序声明为长中断。

当 CPU 接收到中断时,它会停止正在执行的任何操作(除非它正在处理更重要的中断,在这种情况下,它只会在更重要的中断完成后才处理这个中断),将某些参数保存在堆栈上并调用中断处理程序。这意味着在中断处理程序本身中不允许某些事情,因为系统处于未知状态。解决这个问题的方法是让中断处理程序立即完成需要完成的事情,通常是从硬件读取一些东西或向硬件发送一些东西,然后在稍后的时间安排处理新信息(这被称为“下半部”)并返回。然后内核保证尽快调用下半部——当它这样做时,内核模块中允许的所有操作都将被允许。

实现这一点的方法是调用request_irq()以便在收到相关的 IRQ 时调用你的中断处理程序(在 Intel 平台上,有 15 个 IRQ,外加 1 个用于级联中断控制器)。此函数接收 IRQ 号、函数名称、标志、用于/proc/interrupts的名称以及传递给中断处理程序的参数。标志可以包括SA_SHIRQ以指示你愿意与其他中断处理程序共享 IRQ(通常是因为许多硬件设备位于同一 IRQ 上)和SA_INTERRUPT以指示这是一个快速中断。只有当此 IRQ 上还没有处理程序,或者你和另一个处理程序都愿意共享时,此函数才会成功。

然后,在中断处理程序内部,我们与硬件通信,然后使用queue_task_irq()tq_immediate()mark_bh(BH_IMMEDIATE)来调度下半部。我们不能使用标准的queue_task 在 2.0 版本中的原因是,中断可能发生在其他人的queue_task[2] 中间。我们需要mark_bh是因为早期版本的 Linux 只有 32 个下半部数组,现在其中一个(BH_IMMEDIATE)用于驱动程序下半部的链表,这些驱动程序没有分配到下半部条目。

Intel 架构上的键盘

本章的其余部分完全是 Intel 特有的。如果你不是在 Intel 平台上运行,它将无法工作。甚至不要尝试编译这里的代码。

我在为本章编写示例代码时遇到了问题。一方面,为了使示例有用,它必须在每个人的计算机上运行并产生有意义的结果。另一方面,内核已经包含了所有常用设备的设备驱动程序,而这些设备驱动程序将无法与我要编写的内容共存。我找到的解决方案是为键盘中断编写一些东西,并首先禁用常规键盘中断处理程序。因为它在内核源文件中被定义为静态符号(具体来说,drivers/char/keyboard.c),所以无法恢复它。在insmod这个代码之前,在另一个终端上执行sleep 120 ; reboot如果你重视你的文件系统。

此代码将自身绑定到 IRQ 1,这是 Intel 架构下控制的键盘的 IRQ。然后,当它收到键盘中断时,它会读取键盘的状态(这是inb(0x64)的目的)和扫描码,扫描码是键盘返回的值。然后,一旦内核认为可行,它就会运行got_char,它给出所用键的代码(扫描码的前七位)以及它是被按下(如果第 8 位为零)还是释放(如果为 1)。

示例 12-1. intrpt.c

/*  intrpt.c - An interrupt handler.
 *
 *  Copyright (C) 2001 by Peter Jay Salzman
 */

/* The necessary header files */

/* Standard in kernel modules */
#include <linux/kernel.h>               /* We're doing kernel work */
#include <linux/module.h>               /* Specifically, a module */

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        

#include <linux/sched.h>
#include <linux/tqueue.h>

/* We want an interrupt */
#include <linux/interrupt.h>

#include <asm/io.h>

/* In 2.2.3 /usr/include/linux/version.h includes a macro for this, but
 * 2.0.35 doesn't - so I add it here if necessary.
 */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif

/* Bottom Half - this will get called by the kernel as soon as it's safe
 * to do everything normally allowed by kernel modules.
 */
static void got_char(void *scancode)
{
   printk("Scan Code %x %s.\n",
          (int) *((char *) scancode) & 0x7F,
          *((char *) scancode) & 0x80 ? "Released" : "Pressed");
}

/* This function services keyboard interrupts. It reads the relevant
 * information from the keyboard and then scheduales the bottom half
 * to run when the kernel considers it safe.
 */
void irq_handler(int irq, void *dev_id, struct pt_regs *regs)
{
   /* This variables are static because they need to be 
    * accessible (through pointers) to the bottom half routine.
    */
   static unsigned char scancode;
   static struct tq_struct task = {NULL, 0, got_char, &scancode};
   unsigned char status;

   /* Read keyboard status */
   status = inb(0x64);
   scancode = inb(0x60);
  
   /* Scheduale bottom half to run */
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
   queue_task(&task, &tq_immediate);
#else
   queue_task_irq(&task, &tq_immediate);
#endif
   mark_bh(IMMEDIATE_BH);
}

/* Initialize the module - register the IRQ handler */
int init_module()
{
   /* Since the keyboard handler won't co-exist with another handler,
    * such as us, we have to disable it (free its IRQ) before we do
    * anything.  Since we don't know where it is, there's no way to
		* reinstate it later - so the computer will have to be rebooted
		* when we're done.
    */
   free_irq(1, NULL);

   /* Request IRQ 1, the keyboard IRQ, to go to our irq_handler.
	  * SA_SHIRQ means we're willing to have othe handlers on this IRQ.
		* SA_INTERRUPT can be used to make the handler into a fast interrupt. 
    */
   return request_irq(1,   /* The number of the keyboard IRQ on PCs */ 
              irq_handler, /* our handler */
              SA_SHIRQ, 
              "test_keyboard_irq_handler", NULL);
}

/* Cleanup */
void cleanup_module()
{
   /* This is only here for completeness. It's totally irrelevant, since
	  * we don't have a way to restore the normal keyboard interrupt so the
		* computer is completely useless and has to be rebooted.
    */
   free_irq(1, NULL);
}  

注释

[1]

这是 Linux 起源地 Intel 架构上的标准术语。

[2]

queue_task_irq受到全局锁的保护——在 2.2 版本中没有queue_task_irqqueue_task受到锁的保护。