12.1. 中断处理程序

12.1.1. 中断处理程序

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

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

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

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

实现此目的的方法是调用request_irq()以便在收到相关的 IRQ 时调用你的中断处理程序。[2]此函数接收 IRQ 编号、函数名称、标志、用于/proc/interrupts以及传递给中断处理程序的参数。通常有一定数量的可用 IRQ。IRQ 的数量取决于硬件。标志可以包括SA_SHIRQ表示你愿意与其他中断处理程序共享 IRQ(通常是因为多个硬件设备位于同一 IRQ 上)和SA_INTERRUPT表示这是一个快速中断。只有当此 IRQ 上还没有处理程序,或者你们都愿意共享时,此函数才会成功。

然后,在中断处理程序中,我们与硬件通信,然后使用queue_work() mark_bh(BH_IMMEDIATE)来调度下半部。

12.1.2. Intel 架构上的键盘

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

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

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

示例 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 */
#include <linux/sched.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>	/* We want an interrupt */
#include <asm/io.h>

#define MY_WORK_QUEUE_NAME "WQsched.c"

static struct workqueue_struct *my_workqueue;

/* 
 * 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(KERN_INFO "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 puts the non time critical
 * part into the work queue. This will be run when the kernel considers it safe.
 */
irqreturn_t 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 int initialised = 0;
	static unsigned char scancode;
	static struct work_struct task;
	unsigned char status;

	/* 
	 * Read keyboard status
	 */
	status = inb(0x64);
	scancode = inb(0x60);

	if (initialised == 0) {
		INIT_WORK(&task, got_char, &scancode);
		initialised = 1;
	} else {
		PREPARE_WORK(&task, got_char, &scancode);
	}

	queue_work(my_workqueue, &task);

	return IRQ_HANDLED;
}

/* 
 * Initialize the module - register the IRQ handler 
 */
int init_module()
{
	my_workqueue = create_workqueue(MY_WORK_QUEUE_NAME);

	/* 
	 * 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",
			   (void *)(irq_handler));
}

/* 
 * 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);
}

/* 
 * some work_queue related functions are just available to GPL licensed Modules
 */
MODULE_LICENSE("GPL");
                

注释

[1]

这是 Linux 起源的 Intel 架构上的标准命名法。

[2]

实际上,IRQ 处理可能更复杂。硬件通常以链接两个中断控制器的方式设计,以便来自中断控制器 B 的所有 IRQ 都级联到来自中断控制器 A 的某个 IRQ。当然,这需要内核事后找出它实际上是哪个 IRQ,这会增加开销。其他架构提供一些特殊的、非常低开销的,所谓的“快速 IRQ”或 FIQ。要利用它们,需要用汇编程序编写处理程序,因此它们实际上并不适合内核。可以使它们的工作方式与其他 IRQ 类似,但在该过程之后,它们不再比“普通”IRQ 更快。在具有多个处理器的系统上运行的启用 SMP 的内核需要解决另一个卡车装载的问题。仅知道是否发生了某个 IRQ 是不够的,重要的是它适用于哪个 CPU。仍然对更多细节感兴趣的人,可能现在想在网上搜索“APIC” ;)