级别: 高级 Arun Prasad Velu (arun4linux@users.sourceforge.net), 技术经理, Aspire Communications
2005 年 11 月 28 日 这个系列的文章一共有两篇,介绍的是如何实现中断和硬件的模拟,目的是简化驱动程序的开发。本文是第 1 部分,介绍各种策略和实现细节,您可以对中断模拟应用它们,包括前提条件、硬件、软件设置和用来测试中断服务例程(ISR)的测试用例。
为什么会有人想要在开发设备驱动程序时模拟硬件呢?本文将逐渐解释这个问题,并给出一种方法来解决这个问题。本系列的第 2 部分将更加广泛地介绍这个问题及其解决方案的细节。
您可以对很多操作系统和硬件体系结构应用这些方法和策略。这些策略可以适用于 Linux®、VxWorks 和 Windows® NT/2000 操作系统,可以在 IBM PowerPC® 405GP、Intel x86®、MIPS 和 Motorola PPC 体系架构上使用。本系列文章重点介绍的是 x86 平台上的 Linux。
本文将介绍如何以一种系统化的方式来调试中断和中断服务例程(ISR),并详细介绍一些解释和算法,您可以使用它们来遍历 ISR 中所有可能的路径/流程中的源代码。这些技术在所有可能的情况中都非常有用,包括中断和 ISR 的组合,例如慢中断、快中断、tasklet、bottom-half 等等。最后,本文将讨论在实现这些对象和运行测试用例时所需要的硬件和软件环境。
模拟情景
本文通过模拟各种中断,尽可能地帮助设备驱动程序开发人员测试中断服务例程。采用这种模拟技术的成功实现,您还可以执行功能验证测试(FVT),这可能涉及设备驱动程序、应用程序编程接口和应用程序。
考虑这样一个假想的设备驱动程序。假设我们必须从头开始编写这个设备驱动程序,在开发这个设备驱动程序时还没有实际的硬件。而且这个驱动程序非常复杂:它可能会被多线程的应用程序使用。例如,这个驱动程序可能会通过利用 mmap() 来访问硬件寄存器并进行高级编程。
这个设备会生成不同类型的中断(同时可能会生成多个嵌套的中断),这使得中断服务例程(ISR)的设计和实现变得非常复杂。驱动程序将根据中断的优先级次序来执行数据操作。这个设备驱动程序是为一个嵌入式系统开发的,在这种系统上并没有很好的调试环境。这个设备驱动程序需要对设备本身执行一些诊断操作。最后,驱动程序与 API 和应用程序是紧耦合的,需要调试设备驱动程序在中断丢失和乱序发生时的情况。
这比普通的移植工作复杂很多。
前提条件
本文中介绍的策略需要使用两个不同的系统。
第一个系统的配置:开发/主机机器
第一个系统的配置可以使用任何 Linux 发行版本,需要有为其开发驱动程序的设备。还需要对设备驱动程序应用一个补丁(附加例程)。这些附加例程只用于中断模拟。
您还需要编写一个内核线程,它负责生成各种中断。作为一种实现,我们可以将这个线程分隔成几个支持例程来实现,稍后我们将详细介绍。
第二个系统的配置:测试机器
第二个系统的配置需要使用一个启用了内核调试器 的内核。
要启用内核调试器,需要对内核打上内核调试器的补丁。目前有两个很出名的内核调试器可以使用。我们选择的是 kgdb,而不是 kdb,因为使用 kgdb 可以查看 C 源代码。
ISR 源代码级的调试是本文的主要目标。需要有一个正在为其开发驱动程序的设备,以及一根串口线来进行远程调试。需要对内核打 kgdb 的补丁,编译一个启用内核调试支持的内核映像,并在测试机器上运行这个内核。
您可用从开发/主机机器上通过一条串口线来控制这台测试机器。处于调试模式之后,目标机器的内核就停止了。此时连 jiffies(用来对中断进行计时的内核时钟滴答数)都不会改变,这样就可以对 ISR 进行调试了。
更多配置警告
提示: 对于那些具有 TTL(生存时间)的驱动程序要格外注意,例如面向连接的网络设备驱动程序。
在本文介绍的例子中,我们可以完全控制所触发的中断(模拟的)。
当需要调试某个特定的中断时(在某个时刻只能是一个中断),就需要使用一个启用调试器的配置。当运行严格测试时(此时会产生一系列中断),就需要第一种配置,它不需要内核调试器的支持。这两种配置的组合能够得出最好的结果。
在继续之前,我们首先来讨论一下在这两种方法中都会使用的 ioctl 接口。
ioctl 接口
ioctl 命令应该添加到设备驱动程序中,这样就可以从测试应用程序中控制对中断的模拟了。这个 ioctl 可以在 FVT 测试应用程序代码中使用。这个 ioctl 接口对于我们假想的驱动程序来说意义重大。实际的实现依赖于设备和驱动程序。
在我们的例子中,中断处理的一部分是在应用程序层中运行的,另外一部分是在驱动程序中运行的。要实现这种功能,我们需要几个应用程序线程和内核线程。内核线程和应用程序线程需要相互进行握手。
这个具有特殊用途的 ioctl 接口可以控制中断生成的顺序,以及通过测试应用程序所生成的中断的数量。
我们将讨论两种不同类型的中断:普通中断(normal interrupt) 和错误中断(error interrupt)。
有一种很好的方法可以更好地控制中断的触发和 ISR 的测试,它采用一个两层的架构,具有一个特殊的 ioctl 函数,它让用户的应用程序可以灵活地控制特定的中断,并能在指定的时间控制中断的顺序,以及内核中 ioctl 的实现。采用这种方法,可以对中断的生成更好地进行控制。需要设置适当的域,然后将其传递给这个特殊的 ioctl,它会触发中断,或者向内核线程发送信号来触发中断。
ioctl 结构
清单 1 给出了 ioctl 的结构。
清单 1. ioctl 结构
struct simulation_struct
{
struct interrupt_type EventsArray [MAX_INTR_TYPE] ;
unsigned iteration_count;
unsigned num_events;
};
|
这段代码的解释如下:
-
EventsArray [MAX_INTR_TYPE] 是一个 struct interrupt_type 类型的数组,这是根据设备和不同类型的中断定义的。
-
iteration_count 是控制中断模拟循环次数的计数器。
-
如果该值是 0,就逐一触发所有的
MAX_INTR_TYPE 中断(按照中断模拟模块中预先设定好的顺序执行)。
-
如果该值是 1,普通中断就会依次执行一次。
-
如果该值大于 1,但是小于
MAX_COUNT,那么普通中断就会依次执行 iteration_count 次。
-
如果该值是
MAGIC_NUMBER,就从所传递的结构中获取数据/中断寄存器的值,并按照这个结构的值来产生中断。在这种情况中,num_events 给出了要生成的中断的次数。
MAX_INTR_TYPE、MAX_COUNT 和 MAGIC_NUMBER 都应该根据您的需要和实际的硬件进行定义。按照经验,MAX_COUNT 应该总是小于 MAGIC_NUMBER。
-
num_events 是 EventsArray 中有效项的数量。最小值为 1,最大值为 MAX_INTR_TYPE。num_events 中断就会按照传递给 EventsArray 的每个输入的次数来触发。
ioctl 命令
清单 2. ioctl 命令
ioctl name: INTR_SIMULATE
Input: Pointer to struct simulation_struct
Function Type: New feature
|
ioctl 命令的工作原理如下:
-
如果设置了中断模拟状态标志,它就会返回
EBUSY。当中断模拟早已运行时,就会出现这种情况。
-
检查驱动程序的初始状态。如果状态不是 good,就返回错误代码。
-
将
arg(指向 struct simulation_struct)拷贝到全局结构中。确保这个拷贝是在一个具有自旋锁的临界区中发生的。注: 内核线程会读取这个全局结构,并会根据这个全局结构中的元素来生成中断。此处的确需要自旋锁,因为内核线程是独立运行的,而且要访问这个全局结构。
-
设置中断模拟状态标志,通知中断模拟正在运行。
-
一直等待,直到生成中断为止。
-
中断生成之后,就重新设置中断模拟状态标志,并返回成功。这可以对应用程序进行反馈控制。
ioctl 命令返回下面的代码:
- 成功执行时,返回 0。
- 否则,就返回适当的错误状态标志。
运行 keventd,创建一个新的内核线程。
策略
我们对中断和硬件模拟使用了三种主要的策略:
每种策略都需要运行一个内核线程。
策略 1:软件生成 IRQ
这种方法的主要目的是模拟中断,并测试 ISR 是否处理了所有可能的中断。可以自动执行这些操作并模拟这种情况,这样 ISR 就可以作为一个实际的运行环境来调用了。
我们实现的内核线程可以通过利用 INT 汇编指令为我们的设备驱动程序(而不是为卡)来触发中断(软件生成 IRQ)。
在触发中断之前,该中断的所有其他前提条件(设置地址、数据等)都应该在内核线程中进行处理。一旦中断被触发,就会调用驱动程序的 ISR。在 ISR 中,不需要读取实际设备的寄存器;相反,需要从本地变量中读取这些值,而这些值都是由内核线程(模拟模块)所赋值的。实际上应该复制这些设备的寄存器的值。在内核线程中触发中断之前,还需要设置这些本地寄存器中的位/掩码的值。
根据设备驱动程序的不同,如果设备具有缓冲区,可能需要使用一个缓冲区的拷贝。这取决于驱动程序的实现和设备的设计。由于已经正确设置了(模拟)寄存器的值,ISR 会正常进行处理,就仿佛它是一个真正的中断。您可能会注意到,这不仅是一个硬件模拟,也不仅仅是一个中断模拟。
这种方法需要在 ISR 中进行一些修改。访问实际硬件(卡)寄存器的代码现在应该进行一些修改,访问模拟设备寄存器的本地变量。
要实现这种映射,在访问这些寄存器中的地方可以使用条件编译 #ifdef 语句。为了限制 ISR 中 #ifdef 的数量,应该使用 #define 来定义所有的寄存器,并将所有的 #define 语句都放到一个单独的头文件中。在这些寄存器的 #define 宏前面,还应该定义另外一个宏,它用来说明 ISR 是在模拟模式中运行还是在原来的中断上下文中运行的。例如:
清单 3. 条件编译的例子
#ifdef INTR_SIMULATION // Only for interrupt simulation
#define PCIINTRSTATUS local_pciintrstatus // Access the local variable
#else // Actual Interrupt
#define PCIINTRSTATUS Dev-> DataStruct.ulIpcIntrStatus
#endif
|
在这个驱动程序的实现中,不管在哪里访问设备的寄存器,都需要使用这些 #define 定义的宏,而不是直接使用结构变量。这还可以让程序代码更加清晰,因为这样就避免了使用多个间接联合或结构变量的情况。
在 API 和应用程序中也可以使用这种 #ifdef 技术,这样就可以将这些模拟中断的驱动程序链接到这些模块上,从而执行 FVT 测试。对于这种模拟中断的驱动程序的单元测试(ISR 的单元测试)来说,我们可以使用自己的测试程序,它们可以模拟这些 API 和应用程序(这是整个系统中的一部分)的一些功能。
策略 1 所需要的代码修改
要使用软件生成的 IRQ,需要对代码进行如下修改:
-
所有正在访问的寄存器都需要进行定义(使用
#define 语句)。
-
我们应该使用一个单独的
ioctl 命令来对中断模拟进行控制(请参阅 ioctl 接口一节的内容)。
-
我们还需要编写一个单独的内核线程来触发中断。这个内核线程应该在成功注册 ISR(
request_irq)时在设备驱动程序的 open 函数执行时进行注册。
在下面的伪代码中,使用了内核 API kernel_thread 来注册这个内核线程:
清单 4. 启动内核线程
#ifdef INTR_SIMULATION
//
// Start the Kernel Thread
//
start_kthread( raise_intr_thread, &raise_intr );
#endif // end of INTR_SIMULATION
|
函数 start_kthread 通过调用内核 API kernel_thread 来启动这个线程。
在下面的伪代码中,这个内核线程应该在设备驱动程序的 close 函数中进行销毁:
清单 5. 停止内核线程
#ifdef INTR_SIMULATION
//
// Stop the Kernel Thread
//
Stop_kthread( raise_intr_thread);
#Endif // end of INTR_SIMULATION
|
这部分代码(内核线程注册和注销)也应该放在 #define INTR_SIMULATION 条件编译代码块中。
-
我们应该编写一个测试应用程序来处理这些中断。这个测试应用程序应该模拟部分 API 和应用程序的功能来处理所触发的中断。在我们的例子中,这个测试应用程序应该派生一些线程,并等待(被阻塞)中断来触发这个线程。这种阻塞功能是通过使用
sleep_on_interruptible 函数实现的,它使用了驱动程序的 ioctl 函数中的互斥锁(mutually exclusive lock)。当中断触发时,就会有一个线程被唤醒(wake_up_interruptible)并基于中断恢复执行。
-
需要调用这个特殊的
ioctl 函数 INTR_SIMULATE 来模拟中断。
策略 2:使用内核调试器
这种方法的主要目的是能够单步跟踪 tasklet 和 bottom half 中用来处理中断的源代码。由于在这种方法中我们是对内核进行单步跟踪,因此无法模拟精确的时间序列。正如前面介绍的一样,需要格外注意那些具有诸如 TTL 之类的特性的设备驱动程序(这种情况会使用面向连接的网络设备驱动程序)。
这种策略让我们可以按照每个中断来跟踪设备驱动程序的完整代码。这种方法可以与第一种策略一起使用,也可以用来测试实际设备的驱动程序。
这种策略需要一个内核线程来触发中断,这样就可以调用该设备的 ISR 了。需要在 basklet 或 bottom half 中设置一个断点(break point)。当 ISR 对这个 tasklet/bootom half 进行调度时,内核就会在这个断点处停止。碰到这个断点之后,我们就可以对代码进行单步跟踪了,还可以对变量进行查看或修改。
在这种策略中,我们将采用与策略 1 中相同的方法来访问设备的寄存器,也就是使用本地寄存器变量。如果设备和目标体系结构允许,还可以通过调试器来访问这个设备的寄存器。
通过有效地利用内核调试器,我们可以减少前面介绍的内核线程的工作。采用这种方法,可以模拟各种条件、事件发生次序和变量。虽然我们处于一个 basklet 中,但是仍然可以在调试时修改(本地)寄存器的值,并且可以单步跟踪所有的路径和源代码。
策略 2 所需要的代码修改
策略 1 所需要的所有代码修改同样适用于策略 2。然而,对于策略 2 来说,并不需要在内核线程中进行一些初始化和准备前提条件的代码了,因为可以在调试会话中实现这些初始化的工作。
我们可以决定是在源代码中实现所有的功能,还是在运行时使用调试器来修改这些参数。并不需要很多线程,因为这种方法是按照每个中断逐一运行的。
策略 3:使用轮询线程
这种方法用来对 tasklet/bottom-half 代码进行严格测试。在这种方法中,并不触发中断。而是可以使用轮询(polling)技术 来测试所有的中断序列(乱序)。这种方法还可以与内核调试器一起使用(策略 2)。
要实现这种策略,我们需要两个内核线程。第一个内核线程与在上一个策略中介绍的内核线程非常类似,区别在于它并不会触发中断。然而,我们需要对本地寄存器变量进行修改,一旦完成对某个特定中断的初始化/前提条件的工作,就可以对第二个内核线程(轮询线程)声明这些情况了。
轮询线程会一直等待第一个线程给它发送一个信号。我们可以让这个轮询线程一直等待这个信号的到达,否则就让其睡眠。在收到这个信号之后,它就调度 tasklet/bottom half(软件中断)。tasklet/bottom half 会使用与发生中断时相同的上下文来执行。
有一点值得指出的是,这些 tasklet/bottom half 会关闭这个中断上下文(软件中断)。然而,轮询线程是在一个普通进程上下文中运行的。
策略 3 所需要的代码修改
要使用轮询,需要对代码进行如下修改:
-
所有要访问的寄存器都需要进行定义(使用
#define)。
-
在这种方法中需要两个内核线程。
-
第一个线程与为策略 1 所定义的线程类似,但是它不需要触发中断。中断的其他初始化工作都需要在这个线程中进行。
-
需要另外一个单独的轮询线程,在调度 tasklet 时,第一个线程会通知它。
-
在这两个线程之间需要使用一种进程间通信(IPC)机制。
-
这些内核线程应该在设备驱动程序的
close 函数中进行销毁。这部分代码(内核线程的注册和销毁)也要放到 #define INTR_SIMULATION 条件编译代码段中。
注: 如果我们没有启用这个条件编译标志,那么就可以得到一个可以在目标环境中使用的驱动程序对象文件。
-
测试应用程序并不需要太多变化。它模拟 API 和应用程序的部分功能来处理所触发的中断。这个测试应用程序会派生几个线程,并让其等待(被阻塞)。这种阻塞功能是利用驱动程序的
ioctl 函数中的 sleep_on_interruptible 实现的。不管何时中断被触发,这些线程都会被唤醒(wake_up_interruptible)并基于中断恢复执行。
注: 当我们调度 tasklet 时,阻塞的线程会被唤醒,并继续进行处理。尤其要注意那些会无限阻塞内核的情况。
设计内核线程和测试应用程序
内核线程可以在驱动程序的 open 函数中进行初始化,例如当中断服务例程成功注册时,就提供成功的 request_irq。
这些线程是在 close 中销毁的。用来对这些线程进行初始化和销毁的代码都在 #ifdef INTR_SIMULATION 之下,这样在普通模式下编译的代码不会影响驱动程序对象的发行版本。
在本节中,我们将介绍:
-
两个线程(中断线程和轮询线程)
-
一个测试程序,以及
-
测试用例。
中断线程
这个线程要负责生成所有可能的中断,其基于的算法如下:
清单 6. 中断线程算法
1. 读取全局中断模拟结构。
如果 iteration_count 为 0
1.1. 对于每个循环,
1.1.1. 设置适当的中断状态位。
1.2.1. 如果需要,执行其他准备工作。
1.3.1. 如果编译器选项是轮询模式(#ifdef POLLING)
将中断状态寄存器的变化通知调度线程。
1.4.1. 否则
调用 INT 语句,触发该卡的中断。
1.5.1. 延迟该线程。
1.2. 继续执行,直到循环 MAX_INTR_TYPE 次为止(一次执行完所有可能的 MAX_INTR_TYPE
个中断)
提示:可以使用 cpu_raise_irq 或 cpu_raise_softirq,而不使用 INT 语句,这样在
各种平台之间的可移植性就更好;但是要确保正在适当的位置按照正确的顺序来启用和禁用中断。
2. 如果 iteration_count 为 1,就按照它们的次序触发普通中断(非错误中断)。
2.1. 对于每个循环,
2.1.1. 按照普通的中断次序设置适当的中断状态位。
2.2.1. 如果需要,执行其他准备工作。
2.3.1. 如果编译器选项是轮询模式(#ifdef POLLING)
将中断状态寄存器的变化通知调度线程。
2.4.1. 否则
调用 INT 语句,触发该设备的中断。
2.5.1. 延迟该线程。
2.2. 继续执行,直到循环 MAX_NORMAL_INTR_TYPE 次为止(一次执行完所有可能的
MAX_NORMAL_INTR_TYPE 个中断)
3. 如果 iteration_count 大于 1 并且小于 MAX_COUNT,就按照次序触发普通中断
iteration_count 次。
3.1. 对于每个循环,
3.1.1. 按照普通的中断次序设置适当的中断状态位。
3.1.2. 如果需要,执行其他准备工作。
3.1.3. 如果编译器选项是轮询模式(#ifdef POLLING)
将 MAX_NORMAL_INTR_TYPE 个普通次序中断(MAX_NORMAL_INTR_TYPE
次循环,每个中断 1 次)的中断状态寄存器的变化通知调度线程。
3.1.4. 否则
对于 MAX_NORMAL_INTR_TYPE 个普通次序中断(MAX_NORMAL_INTR_TYPE
次循环,每个中断 1 次),调用 INT 语句,触发该设备的中断。
3.1.5. 延迟该线程。
3.2. 继续执行,直到循环次数等于 iteration_count 为止。
4. 如果 iteration_count 是 MAGIC_NUMBER,就从所传递的结构获取中断寄存器的值,
并按照每个结构的值生成中断。在这种情况中,num_events 给出了要生成的中断的个数。
4.1. 对于每个循环,
4.1.1. 按照普通的中断次序设置适当的中断状态位。
4.1.2. 如果需要,执行其他准备工作。
4.1.3. 如果编译器选项是轮询模式(#ifdef POLLING)
按照每个传递的输入将中断状态寄存器的变化通知调度线程。
4.1.4. 否则
按照每个传递的输入调用 INT 语句,触发该设备的中断。
4.1.5. 延迟该线程。
4.2. 继续执行,直到循环次数等于 num_events 为止。
|
注: 开始时,我们可以延时 1 秒。然后可以对循环进行调节,这样就可以生成与原始系统一样多的中断了。
轮询线程
有关轮询线程,需要记住以下内容:
-
这个线程要判断在一个循环中局部中断状态寄存器中是否发生了变化。
-
如果状态寄存器发生了变化,就意味着已经触发了一个中断。
-
如果有一个中断,就使用
schedule_tasklet 调度 tasklet。
-
继续执行前面介绍的任务。
测试程序
这个测试程序将从利用这个驱动器的 API 和应用程序中继承一部分代码。下面是启用这个测试程序的 7 个步骤:
-
在主程序中,派生所需个数的线程。
-
执行驱动程序/内核中阻塞的
ioctl(sleep_on_interruptible)。
-
为
ioctl INTR_SIMULATE 填充输入结构。
-
执行
ioctl INTR_SIMULATE。
-
不管中断何时唤醒这个线程,就使用与实际 API 和应用程序相同的方法来处理这个中断。
-
向主程序注册序号、中断属性和线程属性。
-
主程序跟踪在步骤 6 中提供的信息,并监视是否有乱序执行或中断丢失的情况发生。
这是使用硬件模拟技术执行的重要测试之一。
启用测试用例
下面的步骤展示了如何启用这些测试用例。
清单 7. 启用测试用例
1. 触发所有可能的(MAX_INTR_TYPE)中断,并检查这些中断是否在驱动程序中正确处理了。
1.1. 使用 printk 语句检查是否在执行正确的中断处理步骤。
1.2. 在 /proc 中注册设备驱动程序。
1.3. 使用内核调试器 kgdb,并检查是否在执行正确的中断处理步骤。
2. 触发所有的普通中断(MAX_NORMAL_INTR_TYPE 中断),并检查它们是否在驱动程序中正确进行
处理了。有些设备驱动程序需要在集中执行一些任务之前执行一系列中断。
3. 检查是否有中断丢失了。
3.1. 在测试应用程序中,当线程被唤醒时,检查它的 Interrupt ID(序号)。
3.2. 检查我们已经实现的中断是否在测试应用程序中捕获了。这是一个用来唤醒线程的测试。
|
参考资料 学习
获得产品和技术
-
KGDB 是对 Linux 内核进行源代码级调试的调试器,可以与 gdb 一起用来调试内核。
-
索取免费的 SEK for Linux,这有两张 DVD,包括最新的 IBM for Linux 的试用版软件,包括 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。
-
在您的下一个开发项目中采用 IBM 试用版软件,这可以从 developerWorks 上直接下载。
讨论
关于作者  | |  | Arun Prasad Velu 拥有 Madras University 的计算机应用硕士学位。他在各种操作系统上开发设备驱动程序已经有 6 年多的时间了,包括 Linux、FreeBSD、OS/2、Windows NT/2000 和 VxWorks。他的主要兴趣是 Linux 和开放源码系统。目前,他是 Aspire Communications 公司的技术经理,这家公司的重点是嵌入式硬件的设计、产品工程化、设备驱动程序的开发、OS 移植以及应用软件的开发。 |
对本文的评价
|