1. 中断

中断是一种硬件信号,由具体的硬件设备产生的。不同的硬件设备对应唯一的中断号,处理器是通过中断号(IRQ n)识别不同的硬件设备,并执行对应的中断处理程序或中断服务例程(ISR)。

2. 中断处理机制

中断处理系统在Linux中是非常依赖体系结构的,硬件设备产生中断,通过总线把电信号发送到中断控制器,如果中断线是激活状态(中断线是允许被屏蔽的),中断处理器会将中断信号发往处理器,处理器会立即停止正在做的事,关闭中断系统,然后跳到内存中内核预定义的位置开始执行代码,这个预定义的位置就是中断处理程序的入口,对于每条中断线,处理器都会跳到对应的唯一的位置,内核就可以知道中断的IRQ号,中断处理程序的入口就在栈中保存当前的中断和当前被中断任务的寄存器值。然后内核调用函数do_IRQ(),do_IRQ()函数对所接收的中断进行应答,禁止这条中断线上的中断传递。

unsigned int do_IRQ(struct pt_regs regs);

do_IRQ()函数需要判断这条中断线上是否存在有效的中断处理程序,如果存在,则调用handle_IRQ_event()函数(源v5.4 | kernel/irq/handle.c | do_IRQ)。如果不存在或者执行完handle_IRQ_event()函数,则会回到do_IRQ()做清理工作,再回到入口点,调用ret_from_intr()检查重新调度是否正在挂起(即设置need_resched),如果重新调度正在挂起,内核正在返回用户空间(也就是说, 中断了用户进程),最后调用schedule()。如果内核正在返回内核空间(也就是说, 中断了内核本身),只有在preempt_count为0时,才会调用schedule(),否则,抢占内核是不安全的。在schedule()被调用后,或没有挂起的工作,那么,原来寄存器被恢复,内核恢复到曾经中断的点。

2.1 注册中断处理程序

硬件的驱动程序是可以通过request_irq()函数去注册一个中断处理程序,它被声明在(源v5.4 | linux/interrupt.h | request_irq),并且激活给定的中断线(IRQ n)。

//将中断线绑定中断处理程序
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name,void *dev);
  1. 参数irq:表示要分配的中断号。
  2. 参数handler:指向处理这个中断的实际中断处理程序。
  3. 参数flag:可以为0,也可以是下列一个或多个标志的位掩码。

    • IRQF_DISABLED:内核在处理中断处理程序期间,禁止所有的其他中断。–
    • IRQF_SAMPLE_RANDOM:该设备产生的中断间隔时间作为熵填充到内核熵池,内核熵池负责从各个随机事件导出真正的随机数。
    • IRQF_TIMER:为系统定时器的中断处理而准备
    • IRQF_SHARED:多个中断处理程序之间共享中断线,同一个中断线的所有的中断处理程序都要指定该标志。
  4. 参数name:与中断相关的设备的ASCII文本,如:键盘中断是keyboard,这些名字会被/proc/irq和/proc/interrupts文件使用,以便与用户通信。
  5. 参数dev:主要用于共享中断线,dev将提供唯一的标志信息(cookie),用来区分共享同一个中断处理程序的多个设备,当一个中断处理程序需要释放时,以便从共享中断线的诸多中断处理程序中删除指定的那一个。如果没有共享中断线,该值为NULL即可。

注意:request_irq()函数可能会睡眠的,因此,不能在中断上下文或者其他不允许阻塞的代码中调用该函数。

2.2 释放中断处理程序

卸载驱动处理程序时,需要注销相应的中断处理程序,并释放中断线。

void free_irq(unsigned int irq, void *dev)

如果指定的中断线不是共享的,那么,函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则删除dev所对应的处理程序,共享中断线只有在删除了最后一个中断处理程序时才会被禁用。

2.3 编写中断处理程序

接收到设备的中断信号,中断处理程序进行相应的处理逻辑,尽可能简单,不允许有调用阻塞的函数。如果有时间比较长的逻辑,可以放在下半部处理程序。

//intr_handler自定义的函数名
static irqreturn_t intr_handler(int irq, void *dev);
  1. 参数irq:这个处理程序要响应的中断的中断号。
  2. 参数dev:与传递给request_irq()的dev保持一致,dev将提供唯一的标志信息(cookie),用来区分共享同一个中断处理程序的多个设备。

Linux中断处理程序是不允许嵌套其他中断处理程序,极大地简化了中断处理程序的编写。

在中断上下文中的代码应当迅速、简洁, 中断处理程序没有单独的栈,而是共享所中断进程的内核栈。

2.4 共享的中断处理程序

  • request_irq()的参数flags必须设置IRQF_SHARED标志。
  • 对于每个注册的中断处理程序来说, dev参数必须是唯一,不能给共享的处理程序传递NULL值。
  • 中断处理程序必须能够区分它的设备是否能产生中断,这需要知道硬件是否能产生中断,还需要处理程序中有相关的处理逻辑。

注意:如果同一条中断线上的任何一个设备没有按以上规则进行共享,那么中断线就无法共享。2.6内核后, 共享的处理程序可以混用IRQF_DISABLED。

2.5 中断控制

函数说明
local_irq_disable()禁止本地中断传递
local_irq_enable()激活本地中断传递
local_irq_save(unsigned long flags)保存本地中断传递的当前状态,然后禁止本地中断传递
local_irq_restore(unsigned long flags)恢复本地中断传递到给定的状态
disable_irq(unsigned int irq)禁止给定中断线,并确保该函数返回之前在该中断线上没有处理程序在运行
disable_irq_nosync(unsigned int irq)禁止给定中断线,不会等待当前中断处理程序执行完毕
enable_irq(unsigned int irq)激活给定中断线
synchronize_irq(unsigned int irq)等待一个特定的中断处理程序退出,才会返回
irqs_disabled()如果本地处理器上的中断系统被禁止,则返回非0;否则返回0
in_interrupt()如果在中断上下文中(包括执行中断处理程序和正在执行下半部处理程序), 则返回非0, 如果在进程上下文中,则返回0
in_irq()如果当前正在执行中断处理程序, 则返回非0;否则返回0

3. 下半部和推后执行的工作

中断处理流程中推后执行那一部分。

  • 对时间非常敏感
  • 与硬件相关
  • 保证不被其他中断(特别是相同的中断)打断

以上都放在中断处理程序中执行,其他任务考虑放在下半部分执行。

下半部机制功能状态
BH一个静态创建、由32个bottom halves组成的链表,上半部通过32整数中的一位来标识出哪个bottom half可以执行,虽然分属不同处理器,也不允许任何两个bottom half同时执行。(不够灵活,简单有性能瓶颈)在2.5中去除
任务队列(Task queues)引入任务队列机制来实现工作的推后执行,替代BH机制。驱动程序会将下半部注册到相应的等待队列,等待调用执行。(不够灵活,不能满足性能要求较高的子系统)在2.5中去除
软中断(Softirq)一组静态定义的下半部接口,有32个,可以在所有处理器上同时运行,同类型的接口也可以同时执行。tasklet是需要在编译阶段进行静态注册。(针对性能要求较高的子系统)从2.3开始引入
tasklet一组基于软中断实现的灵活性强、动态创建的下半部实现机制, 不同类型的tasklet可以在不同的处理器上执行, 但类型相同tasklet,不能同时执行。tasklet可以通过代码进行动态注册。(大部分的场景)从2.3开始引入
工作队列(Work queues)工作队列取代了任务队列从2.3开始引入

3.1 软中断

软中断是在编译期间静态分配的,软中断由softirq_action结构(源v5.4 | /include/linux/interrupt.h | softirq_action)表示。软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和SCSI)直接使用软中断。

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

注册的软中断,都在这个数组里面(源v5.4 | /kernel/softirq.c | softirq_vec

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

3.1.1 软中断处理程序

同一个处理器,一个软中断不会抢占另外一个软中断,但中断处理程序可以抢占软中断。其他软中断(包括相同类型的软中断)可以在其他处理器上同时执行。

软中断处理程序原型:

void softirq_handler(struct softirq_action *);

3.1.2 执行软中断

一个注册的软中断必须在被中断处理程序标记后才会执行,这叫触发软中断。

如果有待处理的软中断,do_softirq()函数(源v5.4 | /kernel/softirq.c | do_softirq)会循环遍历每一个,调用他们的处理程序。

asmlinkage __visible void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;
    
    //是否在中断上下文
    if (in_interrupt())
        return;
    
    //保存本地中断传递的当前状态,然后禁止本地中断传递
    local_irq_save(flags);

    //待处理的软中断的32位图,判断是哪个软中断在等待处理
    pending = local_softirq_pending();

    if (pending && !ksoftirqd_running(pending))
        do_softirq_own_stack();
    
    //恢复本地中断传递到给定的状态
    local_irq_restore(flags);
}

do_softirq_own_stack函数(源v5.4 | /include/linux/interrupt.h | do_softirq_own_stack

#ifdef __ARCH_HAS_DO_SOFTIRQ
void do_softirq_own_stack(void);
#else
static inline void do_softirq_own_stack(void)
{
    __do_softirq();
}
#endif

__do_softirq()函数(源v5.4 | /kernel/softirq.c | __do_softirq)

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;
    struct softirq_action *h;
    bool in_hardirq;
    __u32 pending;
    int softirq_bit;

    /*
     * Mask out PF_MEMALLOC as the current task context is borrowed for the
     * softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
     * again if the socket is related to swapping.
     */
    current->flags &= ~PF_MEMALLOC;

    pending = local_softirq_pending();
    account_irq_enter_time(current);

    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    in_hardirq = lockdep_softirq_start();

restart:
    //重设待处理的位图,清零
    set_softirq_pending(0);

    local_irq_enable();

    h = softirq_vec;
    
    //遍历位图
    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;

        h += softirq_bit - 1;

        vec_nr = h - softirq_vec;
        prev_count = preempt_count();

        kstat_incr_softirqs_this_cpu(vec_nr);

        trace_softirq_entry(vec_nr);
        //调用对应软中断处理函数
        h->action(h);
        trace_softirq_exit(vec_nr);
        if (unlikely(prev_count != preempt_count())) {
            pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
                   vec_nr, softirq_to_name[vec_nr], h->action,
                   prev_count, preempt_count());
            preempt_count_set(prev_count);
        }
        h++;
        pending >>= softirq_bit;
    }

    if (__this_cpu_read(ksoftirqd) == current)
        rcu_softirq_qs();
    local_irq_disable();

    pending = local_softirq_pending();
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;

        wakeup_softirqd();
    }
    
    lockdep_softirq_end(in_hardirq);
    account_irq_exit_time(current);
    __local_bh_enable(SOFTIRQ_OFFSET);
    WARN_ON_ONCE(in_interrupt());
    current_restore_flags(old_flags, PF_MEMALLOC);
}

3.1.3 使用软中断

1.分配索引

tasklet类型列表,(源v5.4 | /include/linux/interrupt.h | tasklet类型列表),索引号小的优先级高。

tasklet索引号软中断描述
HI_SOFTIRQ0优先级高的tasklet
TIMER_SOFTIRQ1定时器的下半部
NET_TX_SOFTIRQ2发送网络数据包
NET_RX_SOFTIRQ3接收网络数据包
BLOCK_SOFTIRQ4BLOCK装置
IRQ_POLL_SOFTIRQ5支持IO设备轮询的软中断
TASKLET_SOFTIRQ6正常优先权的tasklets
SCHED_SOFTIRQ7调度程序
HRTIMER_SOFTIRQ8高分辨率定时器
RCU_SOFTIRQ9RUC锁定
NR_SOFTIRQS10softirq种类数量,非软中断

2.注册处理程序

通过open_softirq()进行注册软中断处理程序,以下是网格子系统注册软中断的处理程序。

open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);

3.触发软中断

raise_softirq()函数将软中断设置成挂起状态,下次调用do_softirq()函数时投入运行。

raise_softirq(NET_TX_SOFTIRQ);

//中断已经被禁止,调用下面指令
raise_softirq_irqoff(NET_TX_SOFIRQ);

3.2 tasklet

tasklet是一种利用软中断实现的一种下半部机制, 但是,它的接口更简单,锁保护也要求较低。

3.2.1 tasklet的实现

tasklet结构体定义(源v5.4 | /include/linux/interrupt.h | tasklet_struct)

struct tasklet_struct
{
    struct tasklet_struct *next;//链表,指向下一个tasklet
    unsigned long state;//tasklet状态
    atomic_t count;//引用计数
    void (*func)(unsigned long);//tasklet处理函数
    unsigned long data;//给tasklet处理函数的参数
};

tasklet状态是0、TASKLET_STATE_SCHED(已经在被调度,准备执行)、TASKLET_STATE_RUN(tasklet正在运行)。

count成员是tasklet的引用计数器。

  • 它不为0, 则tasklet被禁止,不允许被执行。
  • 它为0, tasklet才会被激活, 并且设置成挂起状态, 该tasklet才会执行。

3.2.2 调度tasklet

已调度,等待执行的tasklet会被存放在两个单处理器数据结构,由tasklet_struct结构构成的链表

  • tasklet_vec:存放普通的tasklet,由tasklet_schedule()进行调度,使用的是TASKLET_SOFTIRQ软中断。
  • tasklet_hi_vec:存放高优先级的tasklet,由tasklet_hi_schedule()进行调度,使用的是HI_SOFTIRQ软中断。

tasklet_schedule函数定义,(源v5.4 | /include/linux/interrupt.h | tasklet_schedule), 检查tasklet struct的状态。

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}

和tasklet_schedule一样, 检查tasklet struct的状态。

static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_hi_schedule(t);
}

__tasklet_schedule函数和__tasklet_hi_schedule函数实际上都是调用tasklet_schedule_common函数。tasklet_hi_schedule和tasklet_schedule,唯一区别:tasklet_hi_schedule使用的是HI_SOFTIRQ软中断,而tasklet_schedule使用的是TASKLET_SOFTIRQ软中断。(源v5.4 | kernel/softirq.c | __tasklet_schedule

void __tasklet_schedule(struct tasklet_struct *t)
{
    __tasklet_schedule_common(t, &tasklet_vec,
                  TASKLET_SOFTIRQ);
}
void __tasklet_hi_schedule(struct tasklet_struct *t)
{
    __tasklet_schedule_common(t, &tasklet_hi_vec,
                  HI_SOFTIRQ);
}

tasklet_schedule_common函数作用将需要调度的tasklet, 加入到每个处理器的tasklet_vec链表上,将软中断TASKLET_SOFTIRQ设置成挂起状态,等待do_softirq()调用。(源v5.4 | kernel/softirq.c | __tasklet_schedule_common

static void __tasklet_schedule_common(struct tasklet_struct *t,
                      struct tasklet_head __percpu *headp,
                      unsigned int softirq_nr)
{
    struct tasklet_head *head;
    unsigned long flags;
    //保存本地中断传递的当前状态,然后禁止本地中断传递
    local_irq_save(flags);
    head = this_cpu_ptr(headp);
    t->next = NULL;
    //头插法,插入表头
    *head->tail = t;
    head->tail = &(t->next);
    //将软中断TASKLET_SOFTIRQ设置成挂起状态,等待do_softirq()调用
    raise_softirq_irqoff(softirq_nr);
    //打开中断,恢复上下文
    local_irq_restore(flags);
}

以上函数执行完后,就执行do_softirq()调用相对应的处理程序,tasklet_action()和tasklet_hi_action()。

3.2.3 使用tasklet

1.声明tasklet

静态创建一个tasklet结构(源v5.4 | include/linux/interrupt.h | tasklet宏定义

#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 }

两者的区别是初始的count的引用计数不一样,引用计数为0,tasklet处于激活状态,引用计数为1,tasklet处于禁止状态。

通过传入一个利用tasklet_init()函数动态创建tasklet结构(源v5.4 | kernel/softirq.c | tasklet_init

void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}
EXPORT_SYMBOL(tasklet_init);