底半部机制分析:软中断,tasklet,工作队列

一般在有中断的系统中,中断ISR的设计应该尽可能的小,并且在处理中断时,不允许中断ISR再被其他后来的中断打断,也就是避免中断嵌套。现在大多数系统都是不支持中断嵌套的,Linux的实现就是个典型。防止中断嵌套的做法就是处理一个中断时,CPU执行关中断,不接收其他中断。但是这种关中断状态又不能持续太久,关中断时间过长,又会导致后续中断丢失,因此Linux中,将中断处理程序分为两个部分,即上半部和下半部,上半部通常执行时间很短,并且大多与硬件密切相关,所以需要在关中断的环境中运行,而下半部则处理比较费时的一些操作,这部分是在开中断中执行。一般称呼上半部为硬中断,下半部为软中断,Linux的设计中,软中断只能被硬中断打断。

软中断

2.6内核中,软中断的设计始终贯穿一个思想:“谁触发,谁执行(Who marks, who runs)”,所以能有效利用SMP系统的性能,提升了处理效率。2.4以及之前的版本使用的是一种叫Bottom Half的机制来实现下半部,它的致命缺点就是系统中一次能有一个CPU可以执行BH代码,这样在单核CPU中是没有问题的,但在SMP系统中,就严重损失硬件性能了。

2.6.34软中断实现

软中断请求描述include/linux/interrupt.h中:

struct softirq_action
{
    void (*action)(struct softirq_action *);
};
看得出来,2.6.34中,定义了十种类型软中断

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
enum
{
    HI_SOFTIRQ=0, /*用于高优先级的tasklet*/
    TIMER_SOFTIRQ, /*用于定时器的下半部*/
    NET_TX_SOFTIRQ, /*用于网络层发包*/
    NET_RX_SOFTIRQ, /*用于网络层收报*/
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ, /*用于低优先级的tasklet*/
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};

对软中断,linux是在中断处理程序中执行的,具体路径如下:

do_IRQ()->irq_exit()->invoke_softirq()->do_softirq()->__do_softirq()

/*读取本地CPU的软中断掩码并执行与每个设置位相关的可延迟函数,__do_softirq只做固定次数的循环然后就返回。
如果还有其余挂起的软中断,那么内核线程ksofirqd将会在预期的时间内处理他们*/
asmlinkage void __do_softirq(void)
{
    struct softirq_action *h;
    __u32 pending;
    /*把循环计数器的值初始化为10*/
 
    int max_restart = MAX_SOFTIRQ_RESTART;
    int cpu;
 
    /*把本地CPU(被local_softirq_pending选中的)软件中断的位掩码复制到局部变量pending中*/
    pending = local_softirq_pending();
    account_system_vtime(current);
 
    /*增加软中断计数器的值*/
    __local_bh_disable((unsigned long)__builtin_return_address(0));
    lockdep_softirq_enter();
    cpu = smp_processor_id();
 
restart:
    /* Reset the pending bitmask before enabling irqs */
    set_softirq_pending(0); /*清除本地CPU的软中断位图,以便可以激活新的软中断*/
    /*激活本地中断*/
    local_irq_enable();
    h = softirq_vec;//h指向全局的软中断向量表
    do {
         /*根据pending每一位的的设置,执行对应的软中断处理函数*/
        if (pending & 1) {
            int prev_count = preempt_count();
            kstat_incr_softirqs_this_cpu(h - softirq_vec);
            trace_softirq_entry(h, softirq_vec);
            h->action(h); /*执行注册的具体的软中断函数*/
            trace_softirq_exit(h, softirq_vec);
            if (unlikely(prev_count != preempt_count())) {
                printk(KERN_ERR "huh, entered softirq %td %s %p"
                "with preempt_count %08x,"
                " exited with %08x?\n", h - softirq_vec,
                softirq_to_name[h - softirq_vec],
                h->action, prev_count, preempt_count());
                preempt_count() = prev_count;
            }
            rcu_bh_qs(cpu);
         }
        h++;
        pending >>= 1;//按优先级处理各种软中断
    } while (pending);
 
    local_irq_disable();
 
    /*最多重复十次*/
    pending = local_softirq_pending();
    if (pending && --max_restart)
        goto restart;
 
    if (pending) /*如果还有挂起的软中断,唤醒内核线程来处理本地CPU的软中断*/
        wakeup_softirqd();
 
    lockdep_softirq_exit();
 
    account_system_vtime(current);
 
    _local_bh_enable();/*软中断计数器-1,因而重新激活可延迟函数*/
 }

软中断使用:

open_softirq():中断注册。开启一个指定的软中断向量nr,初始化nr对应的描述符softirq_vec[nr]。

raise_softirq():中断触发。

软中断机制中还有一个内核线程ksoftirqd,这个线程干嘛用,在kernel/softirq.c中的一端注释说的很清楚了。只为平衡系统负载

 
/*
 * we cannot loop indefinitely here to avoid userspace starvation,
 * but we also don't want to introduce a worst case 1/HZ latency
 * to the pending events, so lets the scheduler to balance
 * the softirq load for us.
 */

如果系统一直不断触发软中断请求,那么CPU就会一直去处理软中断,因为至少每次时钟中断(TIMER_SOFTIRQ)都会执行一次do_softirq(),这样一来,系统中其他重要任务就得不到CPU而一直处于饥饿状态,所以加这么个小线程,将过多的软中断请求放到系统何时的时间段执行。

中断触发,到唤醒内核线程ksoftirqd来处理中断的大致流程是这样的:

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

inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);
    if (!in_interrupt())
        wakeup_softirqd();//唤醒内核中断处理线程
}

#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
#define local_softirq_pending() \
        __IRQ_STAT(smp_processor_id(), __softirq_pending)
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)

要操作的数据结构在这里irq_cpustat_t:/*arch/xxx/include/asm/hardirq.h*/

typedef struct {

    unsigned int __softirq_pending;
    unsigned int __nmi_count; /* arch dependent */
    unsigned int irq0_irqs;
#ifdef CONFIG_X86_LOCAL_APIC
    unsigned int apic_timer_irqs; /* arch dependent */
    unsigned int irq_spurious_count;
#endif
    unsigned int x86_platform_ipis; /* arch dependent */
    unsigned int apic_perf_irqs;
    unsigned int apic_pending_irqs;
#ifdef CONFIG_SMP
    unsigned int irq_resched_count;
    unsigned int irq_call_count;
    unsigned int irq_tlb_count;
#endif
#ifdef CONFIG_X86_THERMAL_VECTOR
    unsigned int irq_thermal_count;
#endif
#ifdef CONFIG_X86_MCE_THRESHOLD
    unsigned int irq_threshold_count;
#endif
} ____cacheline_aligned irq_cpustat_t;

上面所做的一切就是在属于本处理器的irq_cpustat_t结构中,操作__softirq_pending标志,raise_softirq()就是给__softirq_pending标志中相应的位置位,然后判断系统是否当前处于一个中断上下文中,如果不是,就立马唤醒内核软中断处理线程ksoftirqd来处理刚才触发的软中断。

ksoftirqd线程

2.6.34中ksoftirqd线程的实现是这样子的:

static int run_ksoftirqd(void * __bind_cpu)
{
    /*设置进程状态为可中断*/
    set_current_state(TASK_INTERRUPTIBLE);
    while (!kthread_should_stop()) {/*不应该马上返回*/
        preempt_disable();
        /*实现软中断中一个关键数据结构是每个CPU都有的32位掩码(描述挂起的软中断),
         他存放在irq_cpustat_t数据结构的__softirq_pending字段中。为了获取或设置位掩码的值,
         内核使用宏local_softirq_pending,他选择cpu的软中断为掩码*/
 
        if (!local_softirq_pending()) {/*位掩码为0,标示没有软中断*/
            preempt_enable_no_resched();
            schedule();
            preempt_disable();
        }
 
        __set_current_state(TASK_RUNNING);
        while (local_softirq_pending()) {
            /* Preempt disable stops cpu going offline.
             If already offline, we'll be on wrong CPU:
             don't process */
 
            if (cpu_is_offline((long)__bind_cpu))
                goto wait_to_die;
 
            do_softirq();/*调用软中断处理函数*/
            preempt_enable_no_resched();
            cond_resched();
            preempt_disable();
            rcu_sched_qs((long)__bind_cpu);
        }
        preempt_enable();
        set_current_state(TASK_INTERRUPTIBLE);
    }
    __set_current_state(TASK_RUNNING);
 
    return 0;
 
wait_to_die:
    preempt_enable();
    /* Wait for kthread_stop */
    set_current_state(TASK_INTERRUPTIBLE);
    while (!kthread_should_stop()) {
        schedule();
        set_current_state(TASK_INTERRUPTIBLE);
    }
    __set_current_state(TASK_RUNNING);
    return 0;
}

tasklet

tasklet的本质也是软中断,软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet机制来实现的。其特点就是:1、不同的tasklet代码在同一时刻可以在多个CPU上并行执行;2、与软中断相比,同一段tasklet代码在同一时刻只能在一个CPU上运行,而软中断中注册的中断服务函数在同一时刻可以在多个CPU上运行。

tasklet描述tasklet_struct:/*include/linux/interrupt.h*/

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

next指针指向下一个tasklet,它用于将多个tasklet连接成一个单向循环链表。

state定义了tasklet的当前状态,这是一个32位无符号整数,不过目前只使用了bit 0和bit 1,bit 0为1表示tasklet已经被调度去执行了,而bit 1是专门为SMP系统设置的,为1时表示tasklet当前正在某个CPU上执行,这是为了防止多个CPU同时执行一个tasklet的情况。

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

count是一个对tasklet引用的原子计数,count为0时,tasklet代码段才能执行,如果非0,则该tasklet是被禁止的,因此在执行tasklet代码之前,都必须先检查count是否为0。

软中断HI_SOFTIRQ和TASKLET_SOFTIRQ的实现差不多,只不过是优先级不一样,下面就挑高优先级的tasklet实现来走走流程:

static void tasklet_hi_action(struct softirq_action *a)
{
    struct tasklet_struct *list;
    /*临界区获取当前CPU的高优先级tasklet任务链表*/
    local_irq_disable();
    list = __get_cpu_var(tasklet_hi_vec).head;
    __get_cpu_var(tasklet_hi_vec).head = NULL;
    __get_cpu_var(tasklet_hi_vec).tail = &__get_cpu_var(tasklet_hi_vec).head;
    local_irq_enable();
    while (list) {
        struct tasklet_struct *t = list;
        list = list->next;
        /*加锁,这个在非SMP系统中,直接通过,在SMP系统中,如果其他CPU在运行这段tasklet代码,则本CPU直接跳过*/
        if (tasklet_trylock(t)) {
            /*只有引用计数为0,表示本tasklet是使能的,才能继续执行*/
            if (!atomic_read(&t->count)) {
                /*如果到这地步了,这个tasklet的状态还是正在被调度,那就出问题了*/
                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;
        *__get_cpu_var(tasklet_hi_vec).tail = t;
        __get_cpu_var(tasklet_hi_vec).tail = &(t->next);
        /*这个很有意思,自己再重新触发HI_SOFTIRQ 软中断,这个会在内核软中断处理线程中处理的*/
        __raise_softirq_irqoff(HI_SOFTIRQ);
        local_irq_enable();
    }
}

其他没什么好说的了。

workqueue(2.6.34)

基本术语:

  1. workqueue: 所有工作项(需要被执行的工作)被排列于该队列.
  2. worker thread: 是一个用于执行 workqueue 中各个工作项的内核线程, 当workqueue中没有工作项时, 该线程将变为 idle 状态.
  3. single threaded(ST): worker thread 的表现形式之一, 在系统范围内, 只有一个worker thread为workqueue 服务.
  4. multi threaded(MT): worker thread 的表现形式之一, 在多CPU系统上每个CPU上都有一个worker thread为 workqueue 服务.

workqueue机制在include/linux/workqueue.h和kernel/workqueue.c中定义和实现。

工作队列由workqueue_struct结构来维护,定义如下:

struct workqueue_struct {
    struct cpu_workqueue_struct *cpu_wq;
    struct list_head list;
    const char *name;
    int singlethread;
    int freezeable; /* Freeze threads during suspend */
    int rt;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

cpu_workqueue_struct结构是针对每个CPU定义的。对于每一个CPU,内核都为它挂接一个工作队列,这样就可以将新的工作动态放入到不同的CPU下的工作队列中去,以此体现对“负载平衡”的支持。
struct cpu_workqueue_struct {
    spinlock_t lock; /*结构锁*/
    struct list_head worklist; /*工作列表*/
    wait_queue_head_t more_work; /*要处理的等待队列*/
    struct work_struct *current_work; /*处理完毕的等待队列*/
    struct workqueue_struct *wq; /*工作队列节点*/
    struct task_struct *thread; /*工作者线程*/
} ____cacheline_aligned;

工作项定义
struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func; /*工作队列函数指针,指向具体需要处理的工作*/
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

工作项的创建

静态

  1. DECLARE_WORK(n, f)
  2. DECLARE_DELAYED_WORK(n, f)

动态

  1. INIT_WORK(struct work_struct work, work_func_t func);
  2. PREPARE_WORK(struct work_struct work, work_func_t func);
  3. INIT_DELAYED_WORK(struct delayed_work work, work_func_t func);
  4. PREPARE_DELAYED_WORK(struct delayed_work work, work_func_t func);

内核默认全局工作队列 keventd_wq,位于kernel/workqueue.c

720 static struct workqueue_struct *keventd_wq __read_mostly;
1177 keventd_wq = create_workqueue("events");

工作项加入keventd_wq的接口:
int schedule_work(struct work_struct *work)
{
    return queue_work(keventd_wq, work);
}
 
int schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)
{
    return queue_delayed_work(keventd_wq, dwork, delay);
}

用户自定义工作队列

创建:

  1. create_singlethread_workqueue(name) // 仅对应一个内核线程
  2. create_workqueue(name) // 对应多个内核线程, 同上文.

提交:

  1. int queue_work(workqueue_t *queue, work_t *work);
  2. int queue_delayed_work(workqueue_t *queue, work_t *work, unsigned long delay);

一个例子wq.c:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/workqueue.h>
static struct workqueue_struct *queue = NULL;
static struct work_struct work;
 
static void work_handler(struct work_struct *data)
{
    printk(KERN_ALERT "work handler for work_item in queue Test_wq \n");
    /*workqueue 中的每个工作完成之后就被移除 workqueue.*/
}
 
static int __init wq_init(void)
{
    /*创建工作队列*/
    queue = create_singlethread_workqueue("Test_wq");
    if (!queue)
    {
        goto err;
    }
 
    /*创建工作项*/
    INIT_WORK(&work, work_handler);
    /*挂载工作项到工作队列中*/
    queue_work(queue, &work);
    return 0;
 
err:
    return -1;
}
 
static void __exit wq_exit(void)
{
    destroy_workqueue(queue);
}
MODULE_LICENSE("GPL");
module_init(wq_init);
module_exit(wq_exit);

Makefile
obj-m := wq.o
KERNELBUILD :=/lib/modules/$(shell uname -r)/build
default:
    make -C $(KERNELBUILD) M=$(shell pwd) modules
clean:
    rm -rf *.o *.ko *.mod.c .*.cmd *.markers *.order *.symvers .tmp_versions

简单总结一下吧:

软中断:

1、软中断是在编译期间静态分配的。

2、最多可以有32个软中断,2.6.34用了10个。

3、软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序(ISR)。

4、可以并发运行在多个CPU上,必须设计为可重入的函数,需要使用自旋锁来保护其数据结构。

6、执行时间有:从硬件中断代码返回时、在ksoftirqd内核线程中和某些显示检查并执行软中断的代码中。

tasklet:

1、tasklet是使用两类软中断实现的:HI_SOFTIRQ和TASKLET_SOFTIRQ。

2、可以动态增加减少,没有数量限制。

3、同一类tasklet不能并发执行。

4、不同类型可以并发执行,同一类型不能并发。

5、大部分情况使用tasklet。

工作队列:

1、处于进程上下文,由内核线程去执行。

2、可以睡眠,阻塞。

通过对下半部机制的三种实现分析(softirq,tasklet,workqueue),在具体需要使用时就不再犯难了,需要睡眠,有阻塞的,只能用工作队列了,其次再选tasklet,直接使用软中断的机会比较低吧,一般都是在需要提高性能的时候才考虑了,使用软中断的重点就在于如何采取有效的措施,才能保证共享数据的安全。因为软中断在多CPU上会并发执行。

你可能感兴趣的:(底半部机制分析:软中断,tasklet,工作队列)