目录

一、中断简介

二、中断API函数

1.获取中断号相关函数

2.申请中断函数

3.中断释放函数

4.中断处理函数

5.中断使能和禁止函数

三、中断上文与中断下文

1.软中断

2.tasklet

3.工作队列

四、设备树中的中断节点


一、中断简介

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

由于中断的存在极大的提高了 CPU的运行效率, 但是设备的中断会打断内核进程中的正常调度和运行, 系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。

二、中断API函数

1.获取中断号相关函数

每个中断都有一个中断号,通过中断号即可区分不同的中断。在 Linux 内核中使用一个 int 变量表示中断号,用到中断号, 中断信息一般写到了设备树里面, 可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号。

unsigned int irq_of_parse_and_map(struct device_node *dev,int index)

dev: 设备节点

index:索引号, interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号

如果使用 GPIO 的话,可以使用 gpio_to_irq

int gpio_to_irq(unsigned int gpio)

gpio: 要获取的 GPIO 编号
返回值: GPIO 对应的中断号

2.申请中断函数

在 Linux 内核中要想使用某个中断是需要申请的 request_irq 函数用于申请中断, request_irq
函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函
数。 request_irq 函数会激活(使能)中断,所以不需要手动去使能中断

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev)

irq:要申请中断的中断号
handler:中断处理函数,当中断发生会执行此中断处理函数
flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字

dev: 如果将 flags 设置为 IRQF_SHARED, dev 用来区分不同的中断,一般情况下将
dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值: 0 中断申请成功,负值 中断申请失败,如果返回-EBUSY 表示中断已经被申请了

常用的中断标志:

linux preempt 硬中断 linux中断处理机制_软中断

3.中断释放函数

中断使用完成以后就要通过 free_irq 函数释放掉相应的中断。 如果中断不是共享的,free_irq 会删除中断处理函数并且禁止中断。

void free_irq(unsigned int irq,void *dev)

irq: 要释放的中断号
dev:如果中断设置为共享(IRQF_SHARED),此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉

4.中断处理函数

使用 request_irq 函数申请中断的时候需要设置中断处理函数

irqreturn_t (*irq_handler_t) (int, void *)

第一个参数:要中断处理函数要相应的中断号

第二个参数:一个指向 void 的指针,是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构

返回值:irqreturn_t 类型

 irqreturn_t 类型定义如下所示:

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

irqreturn_t 是个枚举类型, 一共有三种返回值。 一般中断服务函数返回值使用如下形式

return IRQ_RETVAL(IRQ_HANDLED)

5.中断使能和禁止函数

 enable_irq 和 disable_irq 用于使能和禁止指定的中断。

void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

 irq:要禁止的中断号

等到当前正在执行的中断处理函数执行完才返回, 因此需要保证不会产生新的中断, 并且确保所有已经开始执行的中断处理程序已经全部退出。

在这种情况下, 可以使用另外一个中断禁止函数

void disable_irq_nosync(unsigned int irq)

disable_irq_nosync 函数调用以后立即返回, 不会等待当前中断处理程序执行完毕。
 

使能/关闭全局中断

local_irq_enable()
local_irq_disable()

local_irq_enable 用于使能当前处理器中断系统, local_irq_disable 用于禁止当前处理器中断系统。一般不能直接简单粗暴的通过这两个函数来打开或者关闭全局中断,这样会使系统崩溃。

在打开或者关闭全局中断时,要考虑到别的任务的感受,要保存中断状态,处理完后要将中断状态恢复到以前的状态

local_irq_save(flags)
local_irq_restore(flags)

local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断,将中断到 flags 状态。

三、中断上文与中断下文

为保证系统实时性, 中断服务程序必须足够简短, 但实际应用中某些时候发生中断时必须处理大量的事物。

如果都在中断服务程序中完成, 则会严重降低中断的实时性,所以, linux 系统提出了一个概念: 把中断服务程序分为两部分: 上半部-下半部

linux preempt 硬中断 linux中断处理机制_linux_02

中断服务程序分为上半部(top half)和下半部(bottom half),上半部负责读中断源,并在清中断后登记中断下半部,而耗时的工作在下半部处理。 

上半部(中断上文) : 完成尽可能少的比较急的功能, 它往往只是简单的读取寄存器的中断状态, 并清除中断标志后就进行中断标记,把底半部处理程序挂到设备的底半部执行队列中的工作。 顶半部的特点就是响应速度快。


下半部(中断下文) : 处理中断的剩余大部分任务, 可以被新的中断打断

Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出

上半部完成有严格时限的工作 , 例如回复硬件等, 这些工作都是在禁止其他中断情况下进行的。 能够延后执行的都放在下半部进行。 上半部只能通过中断处理程序实现, 下半部的实现目前有 3 种实现方式, 分别为: 软中断、 tasklet 、工作队列(work queues)

linux preempt 硬中断 linux中断处理机制_linux preempt 硬中断_03

1.软中断

Linux 内核使用结构体 softirq_action 表示软中断, softirq_action结构体定义在文件 include/linux/interrupt.h 中

struct softirq_action
{
    void (*action)(struct softirq_action *);
};

在 kernel/softirq.c 文件中一共定义了 10 个软中断

static struct softirq_action softirq_vec[NR_SOFTIRQS];

 NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中

enum
{
    HI_SOFTIRQ=0, /* 高优先级软中断 */
    TIMER_SOFTIRQ, /* 定时器软中断 */
    NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
    NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ, /* tasklet 软中断 */
    SCHED_SOFTIRQ, /* 调度软中断 */
    HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
    RCU_SOFTIRQ, /* RCU 软中断 */
    NR_SOFTIRQS
};

一共有 10 个软中断,数组 softirq_vec 有 10 个元素。 softirq_action 结构体中的 action 成员变量就是软中断的服务函数。

数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。

要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数

void open_softirq(int nr, void (*action)(struct softirq_action *))

nr:要开启的软中断,也就是上面的10个软中断

action:软中断对应的处理函数

 注册好软中断以后需要通过 raise_softirq 函数触发

void raise_softirq(unsigned int nr)

nr:要触发的软中断 

一般在驱动编程不建议使用,选择后面两种实现方式。 

2.tasklet

tasklet是通过软中断实现的, 软中断用轮询的方式处理, 假如是最后一种中断, 则必须循环完所有的中断类型, 才能最终执行对应的处理函数。

为了提高中断处理数量,改进处理效率, 产生了 tasklet 机制。 tasklet 采用无差别的队列机制, 有中断时才执行, 免去了循环查表之苦。

tasklet 机制的优点: 无类型数量限制 效率高 无需循环查表支持 SMP 机制。一种特定类型的 tasklet 只能运行在一个 CPU 上, 不能并行, 只能串行执行。 多个不同类型的 tasklet 可以并行在多个CPU 上。 软中断是静态分配的, 在内核编译好之后, 就不能改变。 但 tasklet 就灵活许多, 可以在运行时改变,比如添加模块时 。

调用 tasklet 以后, tasklet 绑定的函数并不会立马执行, 而是有中断以后, 经过一个很短的不确定时间在来执行

linux preempt 硬中断 linux中断处理机制_服务器_04

 Linux 内核使用 tasklet_struct 结构体来表示 tasklet

struct tasklet_struct
{ 
    struct tasklet_struct *next; /* 下一个 tasklet */
    unsigned long state; /* tasklet 状态 */
    atomic_t count; /* 计数器, 记录对 tasklet 的引用数 */
    void (*func)(unsigned long); /* tasklet 执行的函数 */
    unsigned long data; /* 函数 func 的参数 */
};

next: 链表中的下一个 tasklet, 方便管理和设置 tasklet

state: tasklet 的状态
count: 表示 tasklet 是否出在激活状态, 如果是 0, 就处在激活状态, 如果非 0, 就处在非激活状态
void (*func)(unsigned long): 结构体中的 func 成员是 tasklet 的绑定函数, data 是它唯一的参数。func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数

date: 函数执行的传递给func的参数

如果要使用 tasklet, 必须先定义一个 tasklet, 然后使用 tasklet_init 函数初始化 tasklet

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

t:要初始化的 tasklet
func: tasklet 的处理函数。
data: 要传递给 func 函数的参数

使用宏 DECLARE_TASKLET 一次性完成 tasklet 的定义和初始化, DECLARE_TASKLET 定义在
include/linux/interrupt.h 文件中

DECLARE_TASKLET(name, func, data)
name:要定义的 tasklet 名字, 就是一个 tasklet_struct 类型的变量

func:tasklet 的处理函数

data:传递给 func 函数的参数

在上半部中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行

void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,DECLARE_TASKLET 宏里面的 name, tasklet_struct 类型的变量

tasklet_kill

tasklet_kill(struct tasklet_struct *t)

t:要删除的 tasklet

这个函数会等待 tasklet 执行完毕, 然后再将它移除。 该函数可能会引起休眠, 所以要禁止在中断上下文中使用。

tasklet使用模块

/* 定义 taselet */
struct tasklet_struct testtasklet;

/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
    /* tasklet 具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
    ......
    /* 调度 tasklet */
    tasklet_schedule(&testtasklet);
    ......
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
    ......
    /* 初始化 tasklet */
    tasklet_init(&testtasklet, testtasklet_func, data);
    /* 注册中断处理函数 */
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    ......
}

①: 定义一个 tasklet 结构体
②: 动态初始化 tasklet
③: 编写 tasklet 的执行函数
④: 在中断上文调用 tasklet
⑤: 卸载模块的时候删除 tasklet

3.工作队列

工作队列(workqueue) 是实现中断下文的机制之一, 是一种将工作推后执行的形式。工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度
 

工作队列tasklet 机制有什么不同呢? tasklet 也是实现中断下文的机制, 最主要的区别是 tasklet
不能休眠, 而工作队列是可以休眠的
。 所以, tasklet 可以用来处理比较耗时间的事情, 而工作队列可以处理非常复杂并且更耗时间的事情。

因此如果要推后的工作可以睡眠就可以选择工作队列,否则的话就只能选择软中断或tasklet。

Linux 内核使用 work_struct 结构体表示一个工作

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func; /* 工作队列处理函数 */
};

在这个结构体里面只需要关注 func 这个成员就可以了, 他是一个函数指针, 要将需要完成的
工作写在这个函数里面。

这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示

struct workqueue_struct {
    struct list_head pwqs;
    struct list_head list;
    struct mutex mutex;
    int work_color;
    int flush_color;
    atomic_t nr_pwqs_to_flush;
    struct wq_flusher *first_flusher;
    struct list_head flusher_queue;
    struct list_head flusher_overflow;
    struct list_head maydays;
    struct worker *rescuer;
    int nr_drainers;
    int saved_max_active;
    struct workqueue_attrs *unbound_attrs;
    struct pool_workqueue *dfl_pwq;
    char name[WQ_NAME_LEN];
    struct rcu_head rcu;
    unsigned int flags ____cacheline_aligned;
    struct pool_workqueue __percpu *cpu_pwqs;
    struct pool_workqueue __rcu *numa_pwq_tbl[];
};

Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux 内核使用worker 结构体表示工作者线程

struct worker {
    union {
        struct list_head entry;
        struct hlist_node hentry;
    };
    struct work_struct *current_work;
    work_func_t current_func;
    struct pool_workqueue *current_pwq;
    bool desc_valid;
    struct list_head scheduled;
    struct task_struct *task;
    struct worker_pool *pool;
    struct list_head node;
    unsigned long last_active;
    unsigned int flags;
    int id;
    char desc[WORKER_DESC_LEN];
    struct workqueue_struct *rescue_wq;
};

每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在驱动开发中,只需要定义工作(work_struct)即可,关于工作队列和工作者线程基本不用去管。

初始化工作:INIT_WORK
#define INIT_WORK(_work, _func)
_work :要初始化的工作(work_struct)

_func :工作对应的处理函数


工作的创建和初始化:DECLARE_WORK

#define DECLARE_WORK(n, f)
n :定义的工作(work_struct)

f: 工作对应的处理函数
 

工作的调度函数: schedule_work
bool schedule_work(struct work_struct *work)
work: 要调度的工作。
返回值: 0 成功,其他值 失败

工作队列使用模板:

/* 定义工作(work) */
struct work_struct testwork;

/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
    /* work 具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
    ......
    /* 调度 work */
    schedule_work(&testwork);
    ......
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
    ......
    /* 初始化 work */
    INIT_WORK(&testwork, testwork_func_t);

    /* 注册中断处理函数 */
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    ......
}

四、设备树中的中断节点

如果一个设备需要用到中断功能, 开发人员就需要在设备树中配置好中断属性信息, 因为设备树是用来描述硬件信息的, 然后 Linux 内核通过设备树配置的中断属性来配置中断功能。 对于中断控制器而言, 设备树绑定信息参考文档 Documentation/devicetree/bindings/arm/gic.txt。

i.MX6ULL 的中断控制器节点

intc:interrupt-controller @00a01000
{
    compatible = "arm,cortex-a7-gic";
    #interrupt - cells = < 3>;
    interrupt - controller;
    reg = <0x00a01000 0x1000>,
    <0x00a02000 0x100>;
};

gpio5 : gpio @020ac000{
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x020ac000 0x4000>;
    interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>, 
                 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

①#interrupt-cells:此中断控制器下设备的 cells 大小,一般会使用 interrupts 属性描述中断信息, #interrupt-cells 描述了 interrupts 属性的 cells 大小, 一条信息有几个cells。 每个 cells 都是 32 位整型值, 对于 ARM 处理的 GIC 来说, 一共有 3 个 cells。


第一个 cells: 中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断
第二个 cells: 中断号, SPI中断号的范围为 0~987,  PPI中断号的范围为 0~15

第三个 cells: 标志, bit[3:0]表示中断触发类型, 为1表示上升沿触发, 为2表示下降沿触发, 为4表示高电平触发, 为8表示低电平触发。 bit[15:8]为 PPI 中断的 CPU 掩码


②interrupt-controller 节点为空, 表示当前节点是中断控制器。
 

③interrupts :描述中断源信息, 对于 gpio5 来说一共有两条信息: 中断类型是 SPI, 触发电
平是 IRQ_TYPE_LEVEL_HIGH, 中断源 一个是74, 一个是 75

中断实际上是非常复杂的, 但是作为开发人员, 只需要关心怎么在设备树中指定中断, 怎么在代码中获得中断就可以。像设备树中的中断控制器不需要去修改,需要关注的点是怎么在设备树里面描述一个外设的中断节点。

要接一个外设按键, 通过中断来获取中断的键值,首先在设备树中写节点

key
{
    #address - cells = < 1>;
    #size - cells = < 1>;
    compatible = "key"; //和驱动进行匹配的
    pinctrl - names = "default";
    pinctrl - 0 = <&pinctrl_key>; //把引脚的复用关系设置为 GPIO
    key - gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
    interrupt - parent = <&gpio1>;
    interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* 双边沿触发 */
    status = "okay";
}

首先使用 pinctrl 和 gpio 子系统把这个引脚设置为了 gpio 功能, 因为在使用中断的时候需要把引脚设置成输入

然后使用 interrupt-parent 和 interrupts 属性来描述中断。 interrupt-parent 的属性值是 gpio1, 要使用 gpio1 这个中断控制器。

interrupts 属性设置的是中断源,里面包含两个cells,这是因为在 gpio1中断控制器里面#interrupt-cells 的值为 2。

第一个 cells:指定中断号,18 表示 GPIO1 组的 18 号 IO。

第一个 cells:中断触发方式, IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同时有效,  它定义在文件include/linux/irq.h

所以在设备树里面配置中断的时候只需要俩个步骤即可

①把管脚设置为 gpio 功能
②使用 interrupt-parent 和 interrupts 属性来描述中断

interrupts指定中断号和触发方式

与中断有关的设备树属性信息:

①#interrupt-cells,指定中断源的信息 cells 个数
②interrupt-controller,表示当前节点为中断控制器
③interrupts,指定中断号,触发方式
④interrupt-parent,指定父中断,也就是中断控制器