Linux下中断机制之tasklet执行过程(详细)总结

一、简介

由于中断会打断内核中进程的正常调度运行,所以要求中断服务程序尽可能的短小精悍;但是在实际系统中,当中断到来时,要完成工作往往进行大量的耗时处理。因此期望让中断处理程序运行得快,并想让它完成的工作量多,这两个目标相互制约,诞生——顶/底半部机制,本文主要介绍tasklet的执行过程。
在这里插入图片描述
读者可根据情况了解以下知识:
软中断过程总结
中断机制详细总结

由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。
总结下tasklet的优点:
(1)无类型数量限制;
(2)效率高,无需循环查表;
(3)支持SMP机制;
它的特性如下:
1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
2)多个不同类型的tasklet可以并行在多个CPU上。
3)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。

二、tasklet过程分析

tasklet的主要过程如下:
1、tasklet初始化;
2、tasklet调度过程;
3、tasklet执行过程;

2.1 tasklet初始化
tasklet初始化在start_kernel()->softirq_init()中进行,初始化tasklet_vec和tasklet_hi_vec两个链表,并注册TASKLET_SOFTIRQ和HI_SOFTIRQ两个软中断。

asmlinkage __visible void __init start_kernel(void)
{
...
    softirq_init();
...
}

void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

tasklet描述符:

struct tasklet_struct
{
      struct tasklet_struct *next;//将多个tasklet链接成单向循环链表
      unsigned long state;//TASKLET_STATE_SCHED(Tasklet is scheduled for execution)  TASKLET_STATE_RUN(Tasklet is running (SMP only))
      atomic_t count;//0:激活tasklet 非0:禁用tasklet
      void (*func)(unsigned long); //该tasklet处理程序
      unsigned long data;  //传递给tasklet处理函数的参数
};

tasklet链表:

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);//低优先级
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);//高优先级

两种静态初始化、一种动态初始化方法

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
//定义名字为name的非激活tasklet
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } 
//定义名字为name的激活tasklet
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
//动态初始化tasklet
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);//这里count为0,表示tasklet处于激活状态
    t->func = func;
    t->data = data;
}

2.1 tasklet调度过程

调度的相关函数:

static inline void tasklet_disable(struct tasklet_struct *t)
//函数暂时禁止给定的tasklet被tasklet_schedule调度,直到这个tasklet被再次被enable;若这个tasklet当前在运行, 这个函数忙等待直到这个tasklet退出
static inline void tasklet_enable(struct tasklet_struct *t)
//使能一个之前被disable的tasklet;若这个tasklet已经被调度, 它会很快运行。tasklet_enable和tasklet_disable必须匹配调用, 因为内核跟踪每个tasklet的"禁止次数"
static inline void tasklet_schedule(struct tasklet_struct *t)
//调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己
tasklet_hi_schedule(struct tasklet_struct *t)
//和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期.
tasklet_kill(struct tasklet_struct *t)
//确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync 

调度原理
程序在多个上下文中可以多次调度同一个tasklet执行(也可能来自多个cpu core),不过实际上该tasklet只会一次挂入首次调度到的那个cpu的tasklet链表,也就是说,即便是多次调用tasklet_schedule,实际上tasklet只会挂入一个指定CPU的tasklet队列中(而且只会挂入一次),也就是说只会调度一次执行。这是通过TASKLET_STATE_SCHED这个flag来完成的,我们可以用下面的图片来描述:

在这里插入图片描述tasklet_schedule()被调用的时机大多在中断上半部中,然后将工作交给__tasklet_schedule()处理。
__tasklet_schedule()锁中断情况下插入当前taskelt到tasklet_vec中,并触发TASKLET_SOFTIRQ软中断。
tasklet_scheduler()中设置了当前tasklet的TASKLET_STATE_SCHED标志位,只要该tasklet没有被执行,那么即使驱动程序多次调用tasklet_schedule()也不起作用。
因此一旦该tasklet挂入到某个CPU的tasklet_vec后,就必须在该CPU的软中断上下文中执行,直到执行完毕并清除了TASKLET_STATE_SCHED标志位,才有机会到其他CPU上运行。

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))//置TASKLET_STATE_SCHED位,如果原来未被置位,则调用__tasklet_schedule()
        __tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{
    unsigned long flags;

    local_irq_save(flags);
    t->next = NULL;
    *__get_cpu_var(tasklet_vec).tail = t;
    __get_cpu_var(tasklet_vec).tail = &(t->next);//将t挂入到tasklet_vec链表中
    raise_softirq_irqoff(TASKLET_SOFTIRQ);//触发软中断
    local_irq_restore(flags);
}

HI_SOFTIRQ类型的tasklet和上面基本对称,只是tasklet_vec换成了tasklet_hi_vec,TASKLET_SOFTIRQ换成了HI_SOFTIRQ。

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

void __tasklet_hi_schedule(struct tasklet_struct *t)
{
    unsigned long flags;

    local_irq_save(flags);
    t->next = NULL;
    *__this_cpu_read(tasklet_hi_vec.tail) = t;
    __this_cpu_write(tasklet_hi_vec.tail,  &(t->next));
    raise_softirq_irqoff(HI_SOFTIRQ);
    local_irq_restore(flags);
}

2.2 tasklet执行过程

软中断执行时会按照软中断状态__softirq_pending来依次执行pending状态的软中断,当执行到TASKLET_SOFTIRQ软中断时,调用tasklet_action(),HI_SOFTIRQ为tasklet_hi_action。

static void tasklet_action(struct softirq_action *a)
{
    struct tasklet_struct *list;
    local_irq_disable();
    list = __this_cpu_read(tasklet_vec.head);//在关中断情况下读取tasklet_vec立案表头作为临时链表list
    __this_cpu_write(tasklet_vec.head, NULL);//重新初始化tasklet_vec
    __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
    local_irq_enable();
    while (list) {//开中断情况下遍历tasklet_vec链表,所以tasklet是开中断的
        struct tasklet_struct *t = list;
        list = list->next;
        if (tasklet_trylock(t)) {//如果返回false,表示当前tasklet已经在其他CPU上运行,这一轮将会跳过此tasklet。确保同一个tasklet只能在一个CPU上运行。
            if (!atomic_read(&t->count)) {//表示当前tasklet处于激活状态
                if (!test_and_clear_bit(TASKLET_STATE_SCHED,
                            &t->state))//清TASKLET_STATE_SCHED位;如果原来没有被置位,则返回0,触发BUG()。
                    BUG();
                t->func(t->data);//执行当前tasklet处理函数
                tasklet_unlock(t);
                continue;//跳到while继续遍历余下的tasklet
            }
            tasklet_unlock(t);
        }
如果t->count的值不等于0,说明这个tasklet在调度之后,被disable掉了,所以会将tasklet结构体重新放回到tasklet_vec链表,并重新调度TASKLET_SOFTIRQ软中断,在之后enable这个tasklet之后重新再执行它
        local_irq_disable();
        t->next = NULL;
        *__this_cpu_read(tasklet_vec.tail) = t;//把当前tasklet挂入到当前CPU的tasklet_vec中,等待下一次触发时再执行。
        __this_cpu_write(tasklet_vec.tail, &(t->next));
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);//再次置TASKLET_SOFTIRQ位
        local_irq_enable();
    }
}

HI_SOFTIRQ类型对应的tasklet_hi_action()函数:

static void tasklet_hi_action(struct softirq_action *a)
{
    struct tasklet_struct *list;

    local_irq_disable();
    list = __this_cpu_read(tasklet_hi_vec.head);
    __this_cpu_write(tasklet_hi_vec.head, NULL);
    __this_cpu_write(tasklet_hi_vec.tail, this_cpu_ptr(&tasklet_hi_vec.head));
    local_irq_enable();

    while (list) {
        struct tasklet_struct *t = list;

        list = list->next;

        if (tasklet_trylock(t)) {
            if (!atomic_read(&t->count)) {
                if (!test_and_clear_bit(TASKLET_STATE_SCHED,
                            &t->state))
                    BUG();
                t->func(t->data);
                tasklet_unlock(t);
                continue;
            }
            tasklet_unlock(t);
        }

        local_irq_disable();
        t->next = NULL;
        *__this_cpu_read(tasklet_hi_vec.tail) = t;
        __this_cpu_write(tasklet_hi_vec.tail, &(t->next));
        __raise_softirq_irqoff(HI_SOFTIRQ);
        local_irq_enable();
    }
}

二、小结

tasklet基于softirq,但是tasklet和softirq又存在一些区别。

softirqtasklet
分配softirq是静态定义的tasklet既可以静态定义,也可以通过tasklet_init()动态创建
并发性softirq是可重入的,同一类型的软中断可以在多个CPU上并发执行。tasklet是不可重入的,tasklet必须串行执行,同一个tasklet不可能同时在两个CPU上运行。tasklet通过TASKLET_STATE_SCHED和TASKLET_STATE_RUN保证串行
运行softirq运行在开中断环境下。软中断回调函数不能睡眠,因为软中断可能处于中断上下文中,睡眠导致Linux无法调度。软中断的执行时机可能在中断返回时,即退出中断上下文时。或者local_bh_enable()中。taskelt执行时机在softirq中

三、其他知识链接:

软中断过程总结

中断机制详细总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值