Linux驱动开发之中断

 

概述

所谓中断是指CPU在执行程序的过程中,出现了某些突发事件急待处理,CPU必须暂停当前程序的执行,转去处理突发事件,处理完毕后又返回原程序被中断的位置继续执行。

根据中断的来源,中断可分为内部中断和外部中断,内部中断的中断源来自CPU内部(软件中断指令、溢出、除法错误等,例如,操作系统从用户态切换到内核态需借助CPU内部的软件中断),外部中断的中断源来自CPU外部,由外设提出请求。

根据中断是否可以屏蔽,中断可分为可屏蔽中断与不可屏蔽中断(NMI),可屏蔽中断可以通过设置中断控制器寄存器等方法被屏蔽,屏蔽后,该中断不再得到响应,而不可屏蔽中断不能被屏蔽。

根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断。采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。也就是说,向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。

一个典型的非向量中断服务程序如下所示,它先判断中断源,然后调用不同中断源的中断服务程序。

irq_handler()
{
    ...
    int int_src = read_int_status();  /* 读硬件的中断相关寄存器 */
    switch (int_src)  {               /* 判断中断源 */
    case DEV_A:
         dev_a_handler();
         break;
    case DEV_B:
         dev_b_handler();
         break;
    ...
    default:
         break;
 }
 ...
}

在ARM多核处理器里最常用的中断控制器是GIC(Generic Interrupt Controller),如图所示,它支持3种类型的中断。

gic

SGI(Software Generated Interrupt):软件产生的中断,可以用于多核的核间通信,一个CPU可以通过写GIC的寄存器给另外一个CPU产生中断。多核调度用的IPI_WAKEUP、IPI_TIMER、IPI_RESCHEDULE、IPI_CALL_FUNC、IPI_CALL_FUNC_SINGLE、IPI_CPU_STOP、IPI_IRQ_WORK、IPI_COMPLETION都是由SGI产生的。

  • PPI(Private Peripheral Interrupt):某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU。

  • SPI(Shared Peripheral Interrupt):共享外设的中断,这类外设的中断可以路由到任何一个CPU。

对于SPI类型的中断,内核可以通过如下API设定中断触发的CPU核:

extern int irq_set_affinity (unsigned int irq, const struct cpumask *m);

在ARM Linux默认情况下,中断都是在CPU0上产生的,比如,我们可以通过如下代码把中断irq设定到CPU i上去:

irq_set_affinity(irq, cpumask_of(i));

Linux中断处理程序架构

设备的中断会打断内核进程中的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。但是,这个良好的愿望往往与现实并不吻合。在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。

下图描述了Linux内核的中断处理机制。为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一个平衡点,Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)

banbu

  1. 顶半部是中断处理程序的第一部分,它会尽可能快地响应中断请求,并进行一些必要的处理,如保存 CPU 寄存器状态、清除中断控制器的中断请求等。顶半部的任务是尽可能快地完成中断处理,以避免阻塞其他进程的执行, 往往被设计成不可中断.

  2. 底半部是中断处理程序的第二部分,它会在顶半部之后执行。底半部的任务是完成一些比较耗时的操作,如磁盘 I/O、网络 I/O 等。底半部的执行可以被延迟,直到系统有空闲资源可用。

将中断处理程序分解为顶半部和底半部的好处在于,它可以提高系统的响应速度和可靠性。顶半部可以尽快响应中断请求,避免阻塞其他进程的执行;而底半部可以在系统有空闲资源时执行,避免竞争和阻塞其他进程的执行。

在Linux中,查看/proc/interrupts文件可以获得系统中中断的统计信息,并能统计出每一个中断号上的中断在每个CPU上发生的次数,具体如图所示。

wtp@wtp-VirtualBox:~$ cat /proc/interrupts 
           CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       
  0:         29          0          0          0          0          0   IO-APIC   2-edge      timer
  1:          9          0          0          0          0          0   IO-APIC   1-edge      i8042
  8:          0          0          0          0          0          0   IO-APIC   8-edge      rtc0
  9:          0          0          0          0          0          0   IO-APIC   9-fasteoi   acpi
 10:          0          0          0          0          0          0   IO-APIC  10-edge      vboxvideo
 12:          0          0          0          0          0        158   IO-APIC  12-edge      i8042
 14:          0          0          0          0          0          0   IO-APIC  14-edge      ata_piix
 15:          0          0          0       4374          0          0   IO-APIC  15-edge      ata_piix
 19:          0          0          0         91          0      30851   IO-APIC  19-fasteoi   enp0s3
 20:          0          0          0          0       1740          0   IO-APIC  20-fasteoi   vboxguest
 21:          0     135717       6698          0          0          0   IO-APIC  21-fasteoi   ahci[0000:00:0d.0], snd_intel8x0
 22:          0          0          0          0         27          0   IO-APIC  22-fasteoi   ohci_hcd:usb1
NMI:          0          0          0          0          0          0   Non-maskable interrupts
LOC:      78383      89130      90029      90696     101494     165078   Local timer interrupts
SPU:          0          0          0          0          0          0   Spurious interrupts
PMI:          0          0          0          0          0          0   Performance monitoring interrupts
IWI:          0          0          0          0          0          0   IRQ work interrupts
RTR:          0          0          0          0          0          0   APIC ICR read retries
RES:      52639      38305     112587      43873      46005      45872   Rescheduling interrupts
CAL:       5618       5585       5547       6520       4997       5922   Function call interrupts
TLB:       8236       8165      10554       9549       8262       8126   TLB shootdowns
TRM:          0          0          0          0          0          0   Thermal event interrupts
THR:          0          0          0          0          0          0   Threshold APIC interrupts
DFR:          0          0          0          0          0          0   Deferred Error APIC interrupts
MCE:          0          0          0          0          0          0   Machine check exceptions
MCP:         14         14         14         14         14         14   Machine check polls
ERR:          0
MIS:        103
PIN:          0          0          0          0          0          0   Posted-interrupt notification event
NPI:          0          0          0          0          0          0   Nested posted-interrupt event
PIW:          0          0          0          0          0          0   Posted-interrupt wakeup event

Linux中断编程

申请和释放中断

申请和释放中断需要使用到一些函数和数据结构。下面是一些常用的函数和数据结构:

#include <linux/interrupt.h>

#define IRQF_TRIGGER_NONE     0x00000000
#define IRQF_TRIGGER_RISING   0x00000001
#define IRQF_TRIGGER_FALLING  0x00000002
#define IRQF_TRIGGER_HIGH     0x00000004
#define IRQF_TRIGGER_LOW      0x00000008
#define IRQF_TRIGGER_MASK     (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE    0x00000010

enum irqreturn {
     IRQ_NONE = (0 << 0),
     IRQ_HANDLED = (1 << 0),
     IRQ_WAKE_THREAD = (1 << 1),
};

typedef enum irqreturn irqreturn_t;

typedef irqreturn_t (*irq_handler_t)(int, void *);
  1. request_irq() 函数:用于申请中断。它的原型为 int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev),其中 irq 是中断号,handler 是中断处理函数,flags 是中断处理标志,name 是中断处理程序的名称,dev 是设备指针, 传递给中断服务程序的私有数据。

  2. free_irq() 函数:用于释放中断。它的原型为 void free_irq(unsigned int irq, void *dev),其中 irq 是中断号,dev 是设备指针。

  3. devm_request_irq() 函数:用于申请中断,并会自动释放中断。它的原型为 static int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id),参数和request_irq() 函数一样。

使能和屏蔽中断

下列3个函数用于屏蔽一个中断源:

void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

disable_irq_nosync()disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,这种情况下,只能调用disable_irq_nosync(n)

下列两个函数(或宏,具体实现依赖于CPU的体系结构)将屏蔽本CPU内的所有中断:

#define local_irq_save(flags) ...
void local_irq_disable(void);

前者会将目前的中断状态保留在flags中(注意flags为unsigned long类型,被直接传递,而不是通过指针),后者直接禁止中断而不保存状态。

与上述两个禁止中断对应的恢复中断的函数(或宏)是:

#define local_irq_restore(flags) ...
void local_irq_enable(void);

以上各以local_开头的方法的作用范围是本CPU内。

底半部机制

在 Linux 内核中,底半部是一种延迟执行的机制,用于处理一些需要长时间运行的任务,例如网络数据包处理、磁盘 I/O 等。底半部通常由中断处理程序启动,但是底半部的执行是在中断上下文之外的,因此可以避免中断处理程序过长的执行时间。

在 Linux 内核中,底半部的实现主要机制有:Tasklets软中断(SoftIRQ)工作队列(Workqueue)

Tasklets

Tasklets 是一种轻量级的底半部机制,类似于软中断,但是只能在进程上下文中执行. Tasklets 的执行顺序是固定的,优先级从高到低依次为:高速网络数据包处理、任务队列、打印输出、块设备 I/O 等。

相关API:

#include <linux/interrupt.h>

struct tasklet_struct
{
     struct tasklet_struct *next;
     unsigned long state;
     atomic_t count;
     void (*func)(unsigned long);
     unsigned long data;
};

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }


void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

void tasklet_schedule(struct tasklet_struct *t);

void tasklet_disable_nosync(struct tasklet_struct *t);

void tasklet_enable(struct tasklet_struct *t);

void tasklet_kill(struct tasklet_struct *t);

伪代码实例:

/* 定义tasklet和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);

/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
   ...
}

/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet);
...
}

/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt,
    0, "xxx", NULL);
...
return IRQ_HANDLED;
}

/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}

工作队列(Workqueue)

工作队列是一种基于内核线程的机制,它会创建一个内核线程来执行底半部任务。当有底半部任务需要执行时,工作队列会将任务加入队列中,并唤醒内核线程来执行任务。工作队列适用于需要长时间运行的底半部任务,例如磁盘 I/O 等。

相关API:

#include <linux/workqueue.h>

struct work_struct {
     atomic_long_t data;
     struct list_head entry;
     work_func_t func;
#ifdef CONFIG_LOCKDEP
     struct lockdep_map lockdep_map;
#endif
};

DECLARE_WORK(name, void (*func)(struct work_struct *));
// 用于声明一个工作队列。参数 `name` 是工作队列的名称,`func` 是指向工作函数的指针。

void INIT_WORK(struct work_struct *work, void (*func)(struct work_struct *));
// 用于初始化一个工作队列。参数 `work` 是指向工作队列结构体的指针,`func` 是指向工作函数的指针。

int queue_work(struct workqueue_struct *wq, struct work_struct *work);
// 用于将一个工作队列加入到一个工作队列中。参数 `wq` 是指向工作队列的指针,`work` 是指向工作队列结构体的指针。

bool cancel_work_sync(struct work_struct *work);
// 用于取消一个工作队列。这个函数会等待工作队列执行完毕后才返回。

void flush_workqueue(struct workqueue_struct *wq);
// 用于将一个工作队列中的所有工作队列都执行完毕后才返回。

伪代码实例:

/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);

/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
...
}

/*中断处理顶半部*/
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
schedule_work(&xxx_wq);
...
return IRQ_HANDLED;
}

/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt,
     0, "xxx", NULL);
...
/* 初始化工作队列 */
INIT_WORK(&xxx_wq, xxx_do_work);
...
}

/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}

软中断(SoftIRQ)

软中断是一种基于定时器的机制,它会定期检查是否有需要执行的底半部任务。当有底半部任务需要执行时,软中断会在下一个上下文中执行。软中断的执行顺序是固定的,优先级从高到低依次为:高速网络数据包处理、任务队列、调度器、打印输出、块设备 I/O 等。

相关API:

#include <linux/interrupt.h>

/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
   frequency threaded job scheduling. For almost all the purposes
   tasklets are more than enough. F.e. all serial device BHs et
   al. should be converted to tasklets, not to softirqs.
 */
 /* 请避免分配新的软中断,除非您确实需要非常高频的线程作业调度。
    对于几乎所有的目的,tasklet已经足够了。
    例如,所有串行设备BH等都应该转换为tasklet,而不是softirq。
 */
enum
{
     HI_SOFTIRQ=0,
     TIMER_SOFTIRQ,
     NET_TX_SOFTIRQ,
     NET_RX_SOFTIRQ,
     BLOCK_SOFTIRQ,
     BLOCK_IOPOLL_SOFTIRQ,
     TASKLET_SOFTIRQ,
     SCHED_SOFTIRQ,
     HRTIMER_SOFTIRQ,
     RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

     NR_SOFTIRQS
};

struct softirq_action
{
     void (*action)(struct softirq_action *);
};
//: 用于定义一个软中断处理函数。每个软中断处理函数都有一个唯一的编号,编号范围是 0 到 NR_SOFTIRQS-1,其中 NR_SOFTIRQS 是软中断的数量。

void open_softirq(int nr, void (*action)(struct softirq_action *));
// 用于注册一个软中断处理函数。参数 `nr` 是软中断的编号,`action` 是指向软中断处理函数的指针。

void raise_softirq(unsigned int nr);
// 用于触发一个软中断。当软中断被触发后,它的处理函数将在下一个可执行的上下文中被调用。

void softirq_init(void);
// 用于初始化软中断机制。
// 软中断机制是在内核初始化过程中自动初始化的,因此不需要在模块中调用 softirq_init() 函数来手动初始化软中断机制。在内核启动时,init/main.c 中的 rest_init() 函数会调用 softirq_init() 函数来初始化软中断机制。

中断共享

多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux支持这种中断共享。下面是中断共享的使用方法。

  1. 共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都以IRQF_SHARED标志申请该中断。

  2. 尽管内核模块可访问的全局地址都可以作为request_irq(…,void*dev_id)的最后一个参数dev_id,但是设备结构体指针显然是可传入的最佳参数。

  3. 在中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某一个函数返回IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速地判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE,如下图所示。

liuc

模板(中断共享部分):

 /* 中断处理顶半部 */
 irqreturn_t xxx_interrupt(int irq, void *dev_id)
 {
    ...
 int status = read_int_status();      /* 获知中断源 */
 if(!is_myint(dev_id,status))         /* 判断是否为本设备中断 */
          return IRQ_NONE;            /* 不是本设备中断,立即返回 */

 /* 是本设备中断,进行处理 */
   ...
   return IRQ_HANDLED;                /* 返回IRQ_HANDLED表明中断已被处理 */
 }

 /* 设备驱动模块加载函数 */
 int xxx_init(void)
 {
    ...
    /* 申请共享中断 */
    result = request_irq(sh_irq, xxx_interrupt,
        IRQF_SHARED, "xxx", xxx_dev);
 ...
}

 /* 设备驱动模块卸载函数 */
 void xxx_exit(void)
 {
    ...
    /* 释放中断 */
    free_irq(xxx_irq, xxx_interrupt);
    ...
 }