内核 Korner - 设备驱动程序的动态中断请求分配

作者:B. Thangaraju 博士

计算机要满足其需求,必须与其外部设备进行通信。中断是设备和处理器之间的通信网关。为设备分配中断请求线以及如何处理中断在设备驱动程序开发中起着至关重要的作用。由于系统中中断请求线的数量有限,因此设备之间共享中断是访问更多设备的必然选择。然而,任何分配已在使用中的中断的尝试最终都会导致系统崩溃。本文解释了中断的基础知识和中断处理的基本原理,并包含了一个字符设备的中断请求 (IRQ) 分配的实现。

任何设备的目的是完成一些有用的工作,为此,它应该与微处理器通信。当处理器想要与设备通信时,它会向设备控制器发送指令。设备控制器控制设备的操作。同样,如果设备想要回复处理器,表明新数据已准备好检索,设备会生成中断以引起处理器的注意。中断是一种硬件机制,使设备能够与处理器通信。

直到 2.6 版本,Linux 都是非抢占式的,这意味着当进程在内核模式下运行时,如果任何更高优先级的进程到达就绪运行队列,则较低优先级的进程在返回用户模式之前不能被抢占。但是,即使 CPU 正在内核模式下执行进程,也允许中断转移 CPU 的注意力。这有助于提高系统的吞吐量。当发生中断时,CPU 暂停当前任务并执行一些其他代码,这些代码响应导致中断的任何事件。

计算机中的每个设备都有一个设备控制器,它有一个硬件引脚,用于在设备需要 CPU 服务时发出断言。此引脚连接到 CPU 中相应的中断引脚,从而促进通信。处理器中连接到控制器的引脚称为中断请求线。一个 CPU 有多个这样的引脚,以便处理器可以为许多设备提供服务。在现代操作系统中,可编程中断控制器 (PIC) 用于管理处理器和各种设备控制器之间的 IRQ 线。系统中空闲 IRQ 的数量受到限制,但 Linux 具有允许许多硬件共享相同中断的机制。

中断服务可以比作程序员的工作。程序员打开一个邮箱,并进行他的日常编程工作。当新邮件到达时,他会被屏幕角落的蜂鸣声或其他通知中断。他立即保存程序并切换到邮箱。然后他阅读邮件,发送确认,并恢复他之前的工作。稍后会发送一份详细的回复,列出他采取的步骤。

类似地,当 CPU 执行进程时,设备可以向 CPU 发送关于某些任务的中断,例如,数据已准备好传输。当发生中断时,CPU 立即将程序计数器的当前值保存在内核模式堆栈中,并执行相应的中断服务例程 (ISR)。ISR 是位于内核中的一个函数,它确定中断的性质并执行所需的任何操作,例如将数据块从硬盘驱动器移动到主内存。执行 ISR 后,CPU 恢复之前的进程并执行。

设备驱动程序是内核中的一个软件模块,它等待来自应用程序的请求。每当应用程序想要从设备读取数据时,都会立即调用相应的设备驱动程序,并且相应的设备将打开以进行读取。如果系统正在等待缓慢的硬件,它就无法完成任何有用的工作。内核开发人员的主要目标之一是有效地利用系统资源。为了避免等待来自硬件的数据,内核将这项工作交给设备控制器并恢复停止的进程。当读取完成时,设备通过中断通知 CPU。然后处理器执行相应的 ISR。

中断分类

中断分为两个大类:同步中断和异步中断。同步中断由 CPU 控制单元在执行指令时生成。控制单元在终止指令后发出中断,因此称为同步中断。异步中断由硬件设备在相对于 CPU 时钟的随机时间创建。在 Intel 上下文中,第一个称为异常,第二个称为中断。中断由一个称为向量的无符号单字节整数标识。向量范围在 0 到 255 之间。前 32 个(0–31)向量是异常和不可屏蔽中断,这在我 2003 年 3 月的 LJ 文章“应用程序员的 Linux 信号”中进行了解释。范围从 32–47 分配给可屏蔽中断,由 IRQ(0–15 IRQ 线号)生成。最后一个范围,从 48–255,用于标识软件中断;例如中断 128(int 0X80 汇编指令),用于实现系统调用。

IRQ 分配

系统中已在使用中的中断的快照存储在 /proc 目录中。$cat /proc/interrupt命令显示与中断相关的数据。以下输出显示在我的机器上

CPU0
  0:   82821789          XT-PIC  timer
  1:        122          XT-PIC  i8042
  2:          0          XT-PIC  cascade
  8:          1          XT-PIC  rtc
 10:     154190          XT-PIC  eth0
 12:        100          XT-PIC  i8042
 14:      21578          XT-PIC  ide0
 15:         18          XT-PIC  ide1
NMI:          0
ERR:          0

第一列是 IRQ 线(向量范围从 32–47),下一列是系统启动后中断在 CPU 中传递的次数。第三列与 PIC 相关,最后一列是已为相应中断注册处理程序的设备名称列表。

动态加载设备驱动程序的最简单方法是首先找到系统中未使用的 IRQ 线。request_irq 函数用于为设备分配指定的 IRQ 线号。request_irq 的语法如下,并在 linux/sched.h 中声明

int
request_irq (unsigned int irq,
             void (*handler) (int, void *,
                              struct pt_regs *),
             unsigned long flags,
             const char *device, void *dev_id);

此函数中参数的详细信息是

  • unsigned int irq:中断号,我们希望从系统请求的中断号。

  • void (*handler) (int, void *, struct pt_regs *):每当生成中断时,我们都必须编写 ISR 来处理中断;否则,处理器只是简单地确认它,而对该中断不做任何其他事情。此参数是指向处理程序函数的指针。处理程序函数的语法是

    void
    handler (int irq, void *dev_id,
             struct pt_regs *regs);
    

    第一个参数是 IRQ 号,我们已经在 request_irq 函数中提到了它。第二个参数是设备标识符,使用主设备号和次设备号来标识哪个设备负责当前中断事件。第三个参数用于在处理器开始执行中断处理程序函数之前,将进程的上下文保存在内核堆栈中。当系统恢复之前进程的执行时,将使用此结构。通常,设备驱动程序编写者不必担心此参数。

  • unsigned long flags:flags 变量用于中断管理。为快速中断处理程序设置 SA_INTERRUPT 标志,它禁用所有可屏蔽中断。当我们想要与多个设备共享 irq 时,设置 SA_SHIRQ;如果我们有兴趣使用 IRQ 线探测硬件设备,则设置 SA_PROBE;SA_RANDOM 用于播种内核随机数生成器。有关此标志的更多详细信息,请参阅 /usr/src/linux/drivers/char/random.c。

  • constant char *device:保存 IRQ 的设备名称。

  • void *dev_id:设备标识符——它是指向设备结构的指针。当共享中断时,此字段指向特定的设备。

request_irq 函数在成功时返回 0,在分配失败时返回 -EBUSY。EBUSY 是错误号 16,它在 /usr/src/linux/include/asm/errno.h 文件中描述。free_irq 函数从设备释放 IRQ 号。此函数的语法是

free_irq (unsigned int irq, void *dev_id);

参数的解释与上面相同。

每当发生中断时,都会调用 ISR。ISR 中描述了要对中断原因执行的操作。内核在内存中维护一个表,其中包含中断例程的地址(中断向量)。当发生中断时,处理器检查中断向量表中的 ISR 地址,然后执行。ISR 的任务是根据中断的性质(例如,读取或写入数据)对设备做出反应。通常,如果中断信号表明设备上正在等待的事件,则 ISR 会唤醒设备上休眠的进程。

处理器响应中断所需的时间称为中断延迟。中断延迟由硬件传播时间、寄存器保存时间和软件传播时间组成。中断延迟应尽可能小,以提高系统的性能;因此,ISR 应该简短,并且仅在很短的时间内禁用中断。其他中断可能在禁用中断时发生,但处理器在准备好进行中断服务之前不允许它们。如果阻止了多个中断,则当处理器准备好进行中断服务时,它会按优先级顺序允许它们。

设备驱动程序开发人员应仅在必要时才在驱动程序代码中禁用中断,因为在禁用中断期间,系统不会更新系统计时器、将网络数据包传输到缓冲区或从缓冲区传输网络数据包等等。驱动程序开发人员应编写 ISR 以释放处理器以执行其他任务。然而,在实际场景中,ISR 处理耗时的任务。在这种情况下,ISR 只能执行与硬件进行时间关键通信以禁用中断的操作,并使用 tasklet 来执行大部分实际数据传输处理。tasklet 是最新 Linux 内核中的高级功能,它在安全时间内执行与中断相关的某些操作。tasklet 是软件中断,它可以被其他中断中断。Bovet 和 Cesati(请参阅在线资源)详细解释了中断的内部原理,而 Rubini 和 Corbet(请参阅资源)介绍了从设备驱动程序角度来看的中断实现。

简单实现

任何内核模块都包含一个设备驱动程序,即使系统正在运行时也可以将其加载到现有内核中。我在清单 1 中所示的简单模块中解释了基本的动态 IRQ 分配过程。以下简单的字符设备驱动程序代码描述了名为 OurDevice 的设备的 IRQ 线的动态分配。当您插入模块时,将执行 init_module 函数。如果分配成功,则打印未使用的主设备号和给定 IRQ 号的设备注册以及相应的 printk 消息。从这里,我们可以在 /proc 目录中检查 IRQ 分配。给定的 IRQ 在模块移除时释放。注册 IRQ 号的最佳位置是驱动程序代码的 open 入口点,随后在 release 函数中释放 IRQ。

清单 1. my_module.c

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/interrupt.h>

static struct file_operations fops;
static int Major, irq = 7;

static void OurISR (int irq, void *device,
                    struct pt_regs *regs)
{
  /* important and immediate time critical tasks */
}

static int __init my_init_module(void)
{
    int status;
    Major = register_chrdev(0, "OurDevice", &fops);

    if (Major == -1) {
        printk (" Dynamic Major number "
                "allocation failed\n");
        return Major;
    }

    status = request_irq(irq,
                         (void *)OurISR,
                         SA_INTERRUPT,
                         "OurDevice", &fops);
    if (status == -EBUSY) {
        printk ("IRQ number allocation failed\n");
        unregister_chrdev(Major, "OurDevice");
        return status;
    }

    printk ("The module is successfully loaded\n");
    printk ("Major number for OurDevice:   %d\n",
            Major);
    printk ("IRQ number for OurDevice:     %d\n",
            irq);
    return 0;
}

static void __exit my_cleanup_module (void)
{
    printk("Major number %d  IRQ number %d "
            "are released\n", Major, irq);
    free_irq(irq, &fops);
    unregister_chrdev(Major, "OurDevice");
    printk("The Module is successfully unloaded\n");
}

module_init (my_init_module);
module_exit (my_cleanup_module);

MODULE_LICENSE("GPL");


my_module.c 文件使用 2.6.0-0.test2.1.29 内核编译。kernel-2.6.0-0.test2.1.30.i586.rpm 与所有依赖的 RPM 一起下载并安装。RPM 从 people.redhat.com/arjanv/2.5/RPMS.kernel 下载,设备驱动程序程序编译如下

gcc -Wall -O3 -finline-functions \
-Wstrict-prototypes -falign-functions=4 \
-I/lib/modules/2.6.0-0.test2.1.29/build/include \
-I/lib/modules/2.6.0-0.test2.1.29/build/include/
↪asm/mach-default
-I./include -D__KERNEL__ -DMODULE -DEXPORT_SYMTAB \
-DKBUILD_MODNAME=my_module -c my_module.c -o \
my_module.o

插入 my_module.o 后,如果设备的主设备号和 IRQ 分配成功,则可以看到相应的 printk 语句输出。如果 IRQ 号已被另一个设备使用,则内核将取消注册该设备并释放主设备号。$cat /proc/interrupt命令显示以下输出

           CPU0
  0:   82887219          XT-PIC  timer
  1:        122          XT-PIC  i8042
  2:          0          XT-PIC  cascade
  7:          0          XT-PIC  OurDevice
  8:          1          XT-PIC  rtc
 10:     154769          XT-PIC  eth0
 12:        100          XT-PIC  i8042
 14:      21636          XT-PIC  ide0
 15:         18          XT-PIC  ide1
NMI:          0
ERR:          0

可以在输出中看到 OurDevice 以及 IRQ 线的条目。当我们移除模块时,内核会释放 IRQ 号,取消注册设备并释放主设备号。

结论

希望本文阐明了中断的基本概念和中断处理例程。当我们设备驱动程序中使用这些概念时,对 request_irq 和 free_irq 函数的讨论非常有用。动态 IRQ 分配过程已通过简单的字符设备驱动程序代码进行了解释。

致谢

感谢 C. Surest 在本文准备过程中提供的帮助。

本文的资源: /article/8064

B. Thangaraju 博士获得了物理学博士学位,并在印度科学研究所担任了五年研究助理。他目前在印度 Wipro Technologies 的 Talent Transformation 嵌入式系统焦点小组担任技术主管。他的工作领域包括 Linux 内部原理、Linux 内核、Linux 设备驱动程序以及嵌入式和实时 Linux。可以通过 bt_raju@vsnl.net 与他联系。

加载 Disqus 评论