除了最后一章,到目前为止我们在内核中所做的一切都是为了响应进程的请求,无论是通过处理特殊文件、发送一个ioctl(),或是发出一个系统调用。但是内核的工作不仅仅是响应进程的请求。另一项同样重要的工作是与连接到计算机的硬件进行通信。
CPU 和计算机其他硬件之间有两种类型的交互。第一种类型是 CPU 向硬件发出指令,另一种类型是硬件需要告诉 CPU 一些事情。第二种类型,称为中断,实现起来要困难得多,因为它必须在硬件方便的时候处理,而不是 CPU 方便的时候。硬件设备通常只有非常少量的 RAM,如果你不及时读取它们的信息,信息就会丢失。
在 Linux 下,硬件中断被称为 IRQ(Interrupt Requests,中断请求)[1]。IRQ 分为两种类型:短 IRQ 和长 IRQ。短 IRQ 是指预期占用非常短的时间的中断,在此期间机器的其余部分将被阻止,并且不会处理其他中断。长 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)来调度下半部。我们不能在 2.0 版本中使用标准的queue_task的原因是中断可能发生在其他人的queue_task[2]。我们需要mark_bh,因为早期版本的 Linux 只有一个包含 32 个下半部的数组,而现在其中一个(BH_IMMEDIATE)用于驱动程序的下半部的链表,这些驱动程序没有分配到下半部条目。
本章的其余部分完全是 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 */ /* 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_irq和queue_task受到锁的保护。 |