Linux kernel中断下半部softirq、tasklet分析

一、软中断的辅助内核线程:ksoftirqd

在cpu的热插拔阶段,内核为每个cpu创建了一个用于执行软件中断的内核辅助线程ksoftirqd,

同时定义了一个per_cpu变量用于保存每个守护进程的task_struct结构指针:

DEFINE_PER_CPU(struct task_struct *, ksoftirqd);  

大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。

1、为每个cpu创建一个内核thread :ksoftirqd/x

//softirq.c (kernel)

static struct smp_hotplug_thread softirq_threads = {
    .store          = &ksoftirqd,
    .thread_should_run  = ksoftirqd_should_run,
    .thread_fn      = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",
};
 
static __init int spawn_ksoftirqd(void)
{
    register_cpu_notifier(&cpu_nfb);
    BUG_ON(smpboot_register_percpu_thread(&softirq_threads));    // 为每个cpu创建一个thread :ksoftirqd/0 、 ksoftirqd/1、ksoftirqd/2、ksoftirqd/3
    return 0;
}
early_initcall(spawn_ksoftirqd);
 
 
int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread)
{
.... 核心代码如下:
    for_each_online_cpu(cpu) {
        ret = __smpboot_create_thread(plug_thread, cpu);   
             // --> kthread_create_on_cpu(smpboot_thread_fn, td, cpu, ht->thread_comm); //真正的thread处理函数是smpboot_thread_fn
        smpboot_unpark_thread(plug_thread, cpu);
    }
    list_add(&plug_thread->list, &hotplug_threads);
......
}

smpboot_thread_fn函数中去执行softirq_threads中的run_ksoftirqd

二、软中断具体实现

软件中断的数据结构

struct softirq_action 
{ 
    void    (*action)(struct softirq_action *); 
};
enum 
{ 
    HI_SOFTIRQ=0,       //高优先级的tasklets
    TIMER_SOFTIRQ,      //定时器的下半部
    NET_TX_SOFTIRQ,     //发送网络数据包
    NET_RX_SOFTIRQ,     //接收网络数据包
    BLOCK_SOFTIRQ,      //block
    BLOCK_IOPOLL_SOFTIRQ, 
    TASKLET_SOFTIRQ,    //正常优先级的tasklets
   SCHED_SOFTIRQ,       //调度程序
   HRTIMER_SOFTIRQ,     //高精度定时器
   RCU_SOFTIRQ,         // RCU锁定/* Preferable RCU should always be the last softirq */ 
   
   NR_SOFTIRQS 
};

软中断在编译的时候静态分配,使用struct softirq_action结构表示,编译的时候就固定了这个数组的大小:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;  
  1. 注册软中断处理函数

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
        softirq_vec[nr].action = action;
}

如:

open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
  1. 触发软中断

void raise_softirq(unsigned int nr)

void raise_softirq(unsigned int nr)  
{  
    unsigned long flags;  
 
    local_irq_save(flags);  
    raise_softirq_irqoff(nr);  
    local_irq_restore(flags);  
}  

中断被禁止下调用:

void __raise_softirq_irqoff(unsigned int nr)

inline void raise_softirq_irqoff(unsigned int nr)  
{  
    __raise_softirq_irqoff(nr);  
     ......  
    if (!in_interrupt())  
        wakeup_softirqd();  
}  

通过__raise_softirq_irqoff设置cpu的软中断__softirq_pending标志位(irq_stat[cpu].__softirq_pending )

然后通过in_interrupt()判断现在是否在中断上下文中,或者软中断是否被禁止,

如果都不成立,则唤醒软中断的守护进程,在守护进程中执行软中断的回调函数。否则什么也不做,软中断将会在中断的退出阶段被执行

typedef struct {  
    unsigned int __softirq_pending;  
} ____cacheline_aligned irq_cpustat_t;

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;  

__softirq_pending字段中的每一个bit,对应着某一个软中断,某个bit被置位,说明有相应的软中断等待处理。

调用关系:


__raise_softirq_irqoff(nr);
    or_softirq_pending(1UL << nr); //#define or_softirq_pending(x) (local_softirq_pending() |= (x))
        local_softirq_pending() |= (1UL << nr) //#define local_softirq_pending() __IRQ_STAT(smp_processor_id(), __softirq_pending)
            __IRQ_STAT(smp_processor_id(), __softirq_pending) |= (1UL << nr) //#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
            irq_stat[cpu].__softirq_pending |= (1UL << nr) //extern irq_cpustat_t irq_stat[];
  1. 执行软中断

基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。

实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。

在irq_exit中执行:

void irq_exit(void)   
{           
......
    sub_preempt_count(IRQ_EXIT_OFFSET);
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();-->do_softirq_own_stack()--> __do_softirq()
    ......
}  

如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,

当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中,内核会保证调用__do_softirq时,本地cpu的中断处于关闭状态,

进入__do_softirq:

asmlinkage void __do_softirq(void)  
{  
        ......  
    pending = local_softirq_pending();  
  
    __local_bh_disable((unsigned long)__builtin_return_address(0),  
                SOFTIRQ_OFFSET);  
restart:  
    /* Reset the pending bitmask before enabling irqs */  
    set_softirq_pending(0);  
  
    local_irq_enable();  
  
    h = softirq_vec;  
  
    while ((softirq_bit = ffs(pending))) {
        ......  
        trace_softirq_entry(vec_nr);  
        h->action(h);  
        trace_softirq_exit(vec_nr);  
        ......   
        h++;  
        pending >>= softirq_bit;
    }
    rcu_bh_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);  
    ......
}  
  • 首先取出pending的状态;

  • 禁止软中断,主要是为了防止和软中断守护进程发生竞争;

  • 清除所有的软中断待决标志;

  • 打开本地cpu中断;

  • 循环执行待决软中断的回调函数;

  • 如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出:

  • 没有新的软中断等待执行;

  • 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次;

  • 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断;

  • 退出前恢复软中断

在ksoftirqd辅助内核线程中执行

从前面分析可以看出,软中断也可能由ksoftirqd辅助内核线程中执行,这要发生在以下两种情况下:

  • 在irq_exit中执行软中断,但是在经过MAX_SOFTIRQ_RESTART次循环后,软中断还未处理完,这种情况虽然极少发生,但毕竟有可能;

  • 内核的其它代码主动调用raise_softirq,而这时正好不是在中断上下文中,ksoftirqd辅助内核线程将被唤醒;

ksoftirqd辅助内核线程最终也会调用__do_softirq执行软中断的回调,具体的代码位于run_ksoftirqd函数中,内核会关闭抢占的情况下执行__do_softirq,具体的过程这里不做讨论。

三、tasklet具体实现

tasklet是建立在软中断上的一种延迟执行机制,它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断类型。

在软中断的初始化函数softirq_init的最后,内核注册了TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断:

void __init softirq_init(void)  
{  
        ......  
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);  
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);  
}

tasklet的数据结构

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

next用于把同一个cpu的tasklet链接成一个链表

state用于表示该tasklet的当前状态,

目前只是用了最低的两个bit,分别用于表示已经准备被调度执行和已经在另一个cpu上执行:

enum  
{  
    TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */  
    TASKLET_STATE_RUN   /* Tasklet is running (SMP only) */  
};

count原子变量用于tasklet对tasklet_disable和tasklet_enable的计数,count为0时表示允许tasklet执行,否则不允许执行,每次tasklet_disable时,该值加1,tasklet_enable时该值减1。

func是tasklet被执行时的回调函数指针,

data则用作回调函数func的参数

  1. 初始化一个tasklet

静态初始化:

// 定义名字为name的tasklet,默认为enable状态,也就是count字段等于0。
DECLARE_TASKLET(name, func, data);

// 定义名字为name的tasklet,默认为enable状态,也就是count字段等于1。      
DECLARE_TASKLET_DISABLED(name, func, data); 

动态初始化:

struct tasklet_struct tasklet_xxx;  
......  
tasklet_init(&tasklet_xxx, func, data);  
  1. tasklet的enable&disable

// 通过给count字段加1来禁止一个tasklet,如果tasklet正在运行中,则等待运行完毕才返回(通过TASKLET_STATE_RUN标志)。
tasklet_disable()      

// tasklet_disable的异步版本,它不会等待tasklet运行完毕。    
tasklet_disable_nosync()  

 // 使能tasklet,只是简单地给count字段减1。
tasklet_enable()         
  1. tasklet的调度

// 如果TASKLET_STATE_SCHED标志为0,则置位TASKLET_STATE_SCHED,然后把tasklet挂到该cpu等待执行的tasklet链表上,
// 接着调用raise_softirq_irqoff(TASKLET_SOFTIRQ)触发软件中断请求。
tasklet_schedule(struct tasklet_struct *t)          
                      

// 效果同上,区别是它发出的是HI_SOFTIRQ软件中断请求。
tasklet_hi_schedule(struct tasklet_struct *t)      
  1. tasklet的销毁

tasklet_kill(struct tasklet_struct *t)  
//如果tasklet处于TASKLET_STATE_SCHED状态,或者tasklet正在执行,则会等待tasklet执行完毕,
//然后清除TASKLET_STATE_SCHED状态。

tasklet的内部执行机制

内核为每个cpu定义了一个tasklet_head结构,用于管理每个cpu上的tasklet的调度和执行:

struct tasklet_head  
{  
    struct tasklet_struct *head;  
    struct tasklet_struct **tail;  
};  
  
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);  
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);  

分析软中断的执行,我们知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现的,两个软中断只是有优先级的差别,所以我们只讨论TASKLET_SOFTIRQ的实现,TASKLET_SOFTIRQ的软中断回调函数是tasklet_action,我们看看它的代码:

static void tasklet_action(struct softirq_action *a)  
{  
    struct tasklet_struct *list;  
  
    local_irq_disable();                  1)禁止中断
    list = __this_cpu_read(tasklet_vec.head);      移出当前cpu的待处理tasklet链表到一个临时链表
    __this_cpu_write(tasklet_vec.head, NULL);      2)清空当前cpu的tasklet链表
    __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);  
    local_irq_enable();                  3)允许响应中断
  
    while (list) {                  4)循环遍历链表,获取每一个待处理的tasklet
        struct tasklet_struct *t = list;  
  
        list = list->next;  
  
        if (tasklet_trylock(t)) {      // == !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行
            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);      // == clear_bit(TASKLET_STATE_RUN, &(t)->state);
        }  
  
        local_irq_disable();  
        t->next = NULL;  
        *__this_cpu_read(tasklet_vec.tail) = t;  
        __this_cpu_write(tasklet_vec.tail, &(t->next));  
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);  
        local_irq_enable();  
    }  
}  

解析如下:

  1. 禁止中断(没有必要首先保存其状态,因为这里的代码总是作为软中断被调用,而且中断总是被激活的), 并为当前处理器检索tasklet_vec或tasklet_hig_vec链表

  1. 将当前处理器上的该链表设置为NULL,达到清空的效果

  1. 允许响应中断,没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所有中断应该是被允许的

  1. 循环遍历获取链表上的每一个待处理的tasklet

  1. 如果是多处理器系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否正在其他处理器上运行,如它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去(回忆一下,同一时间里,相同类型的tasklet只能有一个执行)

  1. 如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了

  1. 检查count值是否为0,确保tasklet没有被禁止,如果tasklet被禁止了,则跳到下一个挂起的tasklet去

  1. 我们已经清楚地知道这个tasklet没有在其他地方执行,并且被我们设置成执行状态,这样它在其他部分就不会被执行并且引用计数为0,现在可以执行tasklet的处理程序了

  1. tasklet运行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志

  1. 重复执行下一个tasklet,直至没有剩余的等待处理的tasklet。

  • 关闭本地中断的前提下,移出当前cpu的待处理tasklet链表到一个临时链表后,清除当前cpu的tasklet链表,之所以这样处理,是为了处理当前tasklet链表的时候,允许新的tasklet被调度进待处理链表中。

  • 遍历临时链表,用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行,而且tasklet没有被禁止:

  • 如果没有运行,也没有禁止,则清除TASKLET_STATE_SCHED状态位,执行tasklet的回调函数。

  • 如果已经在运行,或者被禁止,则把该tasklet重新添加会当前cpu的待处理tasklet链表上,然后触发TASKLET_SOFTIRQ软中断,等待下一次软中断时再次执行。

分析到这了我有个疑问,看了上面的代码,如果一个tasklet被tasklet_schedule后,在没有被执行前被tasklet_disable了,岂不是会无穷无尽地引发TASKLET_SOFTIRQ软中断?

通过以上的分析,我们需要注意的是,tasklet有以下几个特征:

  • 同一个tasklet只能同时在一个cpu上执行,但不同的tasklet可以同时在不同的cpu上执行;

  • 一旦tasklet_schedule被调用,内核会保证tasklet一定会在某个cpu上执行一次;

  • 如果tasklet_schedule被调用时,tasklet不是出于正在执行状态,则它只会执行一次;

  • 如果tasklet_schedule被调用时,tasklet已经正在执行,则它会在稍后被调度再次被执行;

  • 两个tasklet之间如果有资源冲突,应该要用自旋锁进行同步保护

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Table of Contents 1. Introduction................................................................................................................5 2. The Players .................................................................................................................6 2.1. User Context .....................................................................................................6 2.2. Hardware Interrupts (Hard IRQs) .....................................................................7 2.3. Software Interrupt Context: Bottom Halves, Tasklets, softirqs ........................7 3. Some Basic Rules........................................................................................................9 4. ioctls: Not writing a new system call ......................................................................10 5. Recipes for Deadlock ...............................................................................................12 6. Common Routines....................................................................................................13 6.1. printk() include/linux/kernel.h.......................................................13 6.2. copy_[to/from]_user() / get_user() / put_user() include/asm/uaccess.h..........................................................................13 6.3. kmalloc()/kfree() include/linux/slab.h.........................................14 6.4. current include/asm/current.h...........................................................15 6.5. local_irq_save()/local_irq_restore() include/asm/system.h15 6.6. local_bh_disable()/local_bh_enable() include/asm/softirq.h 16 6.7. smp_processor_id()/cpu_[number/logical]_map() include/asm/smp.h...................................................................................16 6.8. __init/__exit/__initdata include/linux/init.h.......................................16 6.9. __initcall()/module_init() include/linux/init.h .....................17 6.10. module_exit() include/linux/init.h ..............................................17 6.11. MOD_INC_USE_COUNT/MOD_DEC_USE_COUNT include/linux/module.h 18 7. Wait Queues include/linux/wait.h .................................................................20 7.1. Declaring.........................................................................................................20 7.2. Queuing...........................................................................................................20 7.3. Waking Up Queued Tasks...............................................................................20 8. Atomic Operations...................................................................................................22 9. Symbols .....................................................................................................................23 3 9.1. EXPORT_SYMBOL() include/linux/module.h........................................23 9.2. EXPORT_SYMTAB ............................................................................................23 10. Routines and Conventions.....................................................................................24 10.1. Double-linked lists include/linux/list.h............................................24 10.2. Return Conventions.......................................................................................24 10.3. Breaking Compilation...................................................................................24 10.4. Initializing structure members ......................................................................24 10.5. GNU Extensions ...........................................................................................25 10.6. C++ ...............................................................................................................26 10.7. #if ..................................................................................................................26 11. Putting Your Stuff in the Kernel...........................................................................27 12. Kernel Cantrips......................................................................................................29 13. Thanks.....................................................................................................................31

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liuzl_2010

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值