除了最后一章,到目前为止我们在内核中所做的一切都是响应进程的请求,通过处理一个特殊文件,发送一个ioctl(),或发出一个系统调用。但是内核的工作不仅仅是响应进程请求。另一项同样重要的工作是与连接到计算机的硬件进行通信。
CPU 和计算机的其他硬件之间有两种类型的交互。第一种类型是 CPU 向硬件发出指令,另一种类型是硬件需要告诉 CPU 一些事情。第二种类型,称为中断,更难实现,因为它必须在硬件方便时处理,而不是 CPU 方便时。硬件设备通常只有非常少量的 RAM,如果你没有在信息可用时读取它们,信息就会丢失。
在 Linux 下,硬件中断被称为 IRQ(I中断R请q求)[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 平台上运行,它将无法工作。甚至不要尝试编译这里的代码。
我在为本章编写示例代码时遇到了问题。一方面,为了使示例有用,它必须在每个人的计算机上运行并产生有意义的结果。另一方面,内核已经包含了所有常用设备的设备驱动程序,而这些设备驱动程序将无法与我要编写的内容共存。我找到的解决方案是为键盘中断编写一些东西,并首先禁用常规键盘中断处理程序。因为它在内核源文件中被定义为静态符号(具体来说,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_irq和queue_task受到锁的保护。 |