除了最后一章,到目前为止我们在内核中所做的一切都是响应进程的请求,无论是通过处理特殊文件,发送一个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)来调度下半部。
本章的其余部分完全是 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” ;) |