momo zone

调核人的blog

Hardirq ,Softirq,Tasklet和Workqueue

中断是一个繁杂的话题,由中断引发的问题很容易引发争论。除我之前有讲过中断睡眠的问题,还有关于tasklet 和workqueue。 这里有必要重新整理总结一下了。

教课书或者intel手册上是这样划分中断的:

中断可分为同步(synchronous)中断和异步(asynchronous)中断:

1. 同步中断是当指令执行时由 CPU 控制单元产生,之所以称为同步,是因为只有在一条指令执行完毕后 CPU 才会发出中断,而不是发生在代码指令执行期间,比如系统调用。

2. 异步中断是指由其他硬件设备依照 CPU 时钟信号随机产生,即意味着中断能够在指令之间发生,例如键盘中断。

根据 Intel 官方资料,同步中断称为异常(exception),异步中断被称为中断(interrupt)。

中断可分为可屏蔽中断(Maskable interrupt 比如打印机中断)和非屏蔽中断(Nomaskable interrupt)。异常可分为故障(fault)比如缺页异常、陷阱(trap)比如调试异常、终止(abort)三类。

从广义上讲,中断可分为四类:中断故障陷阱终止。这些类别之间的异同点请参看 表 1。

表 1:中断类别及其行为
类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 返回到当前指令
终止 不可恢复的错误 同步 不会返回

X86 体系结构的每个中断都被赋予一个唯一的编号或者向量(8 位无符号整数)。非屏蔽中断和异常向量是固定的,而可屏蔽中断向量可以通过对中断控制器的编程来改变。

ok ,上面都是教课书或各种手册上的陈词滥调,那么在具体某个操作系统实现整个中断处理的时候却完全不像前面说的那样那么简单。

传统的中断机制是这样的:BIOS初始化-〉中断向量初始化-〉内核安装中断描述符表-〉发生中断-〉查找中断描述符表获得中断处理函数-〉关中断-〉处理中断,完毕-〉开中断。

前面所讲的基本都是硬中断(因教课书的局限性以及手册的严谨性),也就是传统中断的处理方式,硬件的支持贯穿于整个中断处理过程。后来发现在关中断-〉处理中断,完毕-〉开中断 之间,由于关中断会造成中断丢失,尤其是中断处理过程花费较长时间的情况下。所以从 linux1.x版本开始,中断处理程序从概念上被分为上半部分(top half)和下半部分(bottom half)。

在中断发生时上半部分的处理 过程立即执行,因为它是完全屏蔽中断的,所以要快,否则其它的中断就得不到及时的处理。但是下半部分(如果有的话)几乎做了中断处理程序所有的事情,可以 推迟执行。内核把上半部分和下半部分作为独立的函数来处理,上半部分的功能就是“登记中断”,决定其相关的下半部分是否需要执行。需要立即执行的部分必须 位于上半部分,而可以推迟的部分可能属于下半部分。下半部分的任务就是执行与中断处理密切相关但上半部分本身不执行的工作,如查看设备以获得产生中断的时 间信息,并根据这些信息(一般通过读设备上的寄存器得来)进行相应的处理。从这里我们可以看出下半部分其实是上半部分引起的,例如当打印机端口产生一个中 断时,其中断处理程序会立即执行相关的上半部分,上半部分就会产生一个软中断(下半部分的一种,后面再介绍)并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠的打印机任务队列中的处理进程。

它们最大的不同是上半部分不可中断,而下半部分可中断。在理想的情况下,最好是中断处 理程序上半部分将所有工作都交给下半部分执行,这样的话在中断处理程序上半部分中完成的工作就很少,也就能尽可能快地返回。但是,中断处理程序上半部分一 定要完成一些工作,例如,通过操作硬件对中断的到达进行确认,还有一些从硬件拷贝数据等对时间比较敏感的工作。剩下的其他工作都可由下半部分执行(一个典型的情景就是网络数据包到达网卡,上半部必须要给该数据包上时间戳,然后其他处理推迟到后半部再处理)。

内核中的中断处理机制在不断变化,而变化的要点并不是在上半部,而是下半部。由上面介绍可以看出上半部仍然是遵循传统的中断机制,也就是依赖硬件的中断。所以上半部也可以称为硬中断。下半部由于只是处理上半部推托过来的任务,完全依赖代码实现,所以也可以理解成”软中断” ,只是这里的“软中断”非内核文档中说的软中断softirq,真正的软中断softirq(作为下半部实现的一种)是在2.4中引入的。内核中实现下半部的手段不断演化,目前已经从最原始的BH(bottom half)进化到软中断(softirq在2.3引 入)、tasklet(在2.3引入)、工作队列(work queue在2.5引入)。也就是说在2.6 中传统的BH 机制已经被剔除 ,现在提到BH 其实就是指softirq, tasklet.workequeue 这三种实现。

##############################################################

SOFTIRQ

##############################################################

引入softirq,替换传统BH的原因是:

1.系统中一次只能有一个CPU可以执行BH代码,

2.BH函数不允许嵌套。

后来SMP 普及后,上述缺点就成了致命伤。softirq支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq必须是可重入的。整个softirq机制的设计与实现始终贯穿着 一个思想:“谁触发,谁执行”(Who marks, who runs),也就是说,每个CPU都单独负责它所触发的软中断,互不干扰。这就有效地利用了SMP系统的性能和特点,极大地提高了处理效率。

在include/linux/interrupt.h中定义了一个softirq_action结构来描述一个softirq请求,如下所示:

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

其中,函数指针action指向软中断请求的服务函数。

在kernel/softirq.c中定义了一个全局的softirq软中断向量表softirq_vec[NR_SOFTIRQS]:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

对应NR_SOFTIRQS个 softirq_action结构表示的软中断描述符。内核预定义了一些软中断向量的含义供我们使用:

enum
{
 HI_SOFTIRQ=0,
 TIMER_SOFTIRQ,
 NET_TX_SOFTIRQ,
 NET_RX_SOFTIRQ,
 BLOCK_SOFTIRQ,
 BLOCK_IOPOLL_SOFTIRQ,
 TASKLET_SOFTIRQ,
 SCHED_SOFTIRQ,
 HRTIMER_SOFTIRQ,
 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

 NR_SOFTIRQS
};

没错,这个软中断向量表和硬中断向量表相仿,优先级从上到下依次降低。这里枚举类型用法比较巧妙,每个枚举值按顺序拥有一个索引值,

HI_SOFTIRQ就是0,到NR_SOFTIRQS的索引值就正好是枚举值的个数10。用宏也可以定义:
#define HI_SOFTIRQ 0
#define TIMER_SOFTIRQ 1
.......
#define NR_SOFTIRQS 10

open_softirq向内核注册一个软中断,其实质是设置软中断向量表相应槽位,注册其处理函数:

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

下面介绍一下softirq的处理流程:

处理时机1 :由硬中断直接调用执行软中断

1.上半部(硬中断)处理函数 do_IRQ  in arch/x86/kernel/irq.c:

/*
 * do_IRQ handles all normal device IRQ's (the special
 * SMP cross-CPU interrupts have their own specific
 * handlers).
 */
unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
 struct pt_regs *old_regs = set_irq_regs(regs);

 /* high bit used in ret_from_ code */
 unsigned vector = ~regs->orig_ax;
 unsigned irq;

 exit_idle();
 irq_enter();

 irq = __get_cpu_var(vector_irq)[vector];

 if (!handle_irq(irq, regs)) {
 ack_APIC_irq();

 if (printk_ratelimit())
 pr_emerg("%s: %d.%d No irq handler for vector (irq %d)\n",
 __func__, smp_processor_id(), vector, irq);
 }

 irq_exit();

 set_irq_regs(old_regs);
 return 1;
}

特殊一点的比如apic时钟中断 in arch/x86/kernel/apic/apic.c:

/*
 * Local APIC timer interrupt. This is the most natural way for doing
 * local interrupts, but local timer interrupts can be emulated by
 * broadcast interrupts too. [in case the hw doesn't support APIC timers]
 *
 * [ if a single-CPU system runs an SMP kernel then we call the local
 * interrupt as well. Thus we cannot inline the local irq ... ]
 */
void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)
{
 struct pt_regs *old_regs = set_irq_regs(regs);

 /*
 * NOTE! We'd better ACK the irq immediately,
 * because timer handling can be slow.
 */
 ack_APIC_irq();
 /*
 * update_process_times() expects us to have done irq_enter().
 * Besides, if we don't timer interrupts ignore the global
 * interrupt lock, which is the WrongThing (tm) to do.
 */
 exit_idle();
 irq_enter();
 local_apic_timer_interrupt();
 irq_exit();

 set_irq_regs(old_regs);
}

2.上半部(硬中断)退出处理函数 irq_exit()   in kernel/softirq.c  :

void irq_exit(void)
{
 account_system_vtime(current);
 trace_hardirq_exit();
 sub_preempt_count(IRQ_EXIT_OFFSET);
 if (!in_interrupt() && local_softirq_pending())
 invoke_softirq();

 rcu_irq_exit();
#ifdef CONFIG_NO_HZ
 /* Make sure that timer wheel updates are propagated */
 if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
 tick_nohz_stop_sched_tick(0);
#endif
 preempt_enable_no_resched();
}

3.软中断调用函数 invoke_softirq() in kernel/softirq.c :

/*macro if defined, means that the IRQs are guaranteed to be disabled when irq_exit() function is called.
 In such a case, the kernel may skip some instructions (disabling IRQs etc).... and thus call __do_IRQ() instead of do_IRQ.*/

#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED 
static inline void invoke_softirq(void)
{
 if (!force_irqthreads)
 __do_softirq();
 else
 wakeup_softirqd();
}
#else
static inline void invoke_softirq(void)
{
 if (!force_irqthreads)
 do_softirq();
 else
 wakeup_softirqd();
}
#endif

X86 实际调用的是do_softirq , ARM架构的会调用__do_softirq 区别在于前者保证硬中断已关闭。

不过这里do_softirq 并不是在kernel/softirq.c 中定义的那个,因为宏__ARCH_HAS_DO_SOFTIRQ 在x86下被定义了,所以真正的do_softirq在arch/x86/kernel/irq_32.c :

asmlinkage void do_softirq(void)
{
 unsigned long flags;
 struct thread_info *curctx;
 union irq_ctx *irqctx;
 u32 *isp;

 if (in_interrupt())
 return;

 local_irq_save(flags);

 if (local_softirq_pending()) {
 curctx = current_thread_info();
 irqctx = __this_cpu_read(softirq_ctx);
 irqctx->tinfo.task = curctx->task;
 irqctx->tinfo.previous_esp = current_stack_pointer;

 /* build the stack frame on the softirq stack */
 isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));

 call_on_stack(__do_softirq, isp);
 /*
 * Shouldn't happen, we returned above if in_interrupt():
 */
 WARN_ON_ONCE(softirq_count());
 }

 local_irq_restore(flags);
}

这里有个令人疑惑的问题,local_irq_save(flags) 和local_irq_restore(flags) 之间是关中断的,那么__do_softirq 也就是在关中断情况下执行。 这样不就和下半部在开中断下执行的设计初衷相违背了吗? 答案就在__do_softirq 中

最终还是要执行kernel/softirq.c 中的 __do_softirq:

/*
 * We restart softirq processing MAX_SOFTIRQ_RESTART times,
 * and we fall back to softirqd after that.
 *
 * This number has been established via experimentation.
 * The two things to balance is latency against fairness -
 * we want to handle softirqs as soon as possible, but they
 * should not be able to lock up the box.
 */
#define MAX_SOFTIRQ_RESTART 10

asmlinkage void __do_softirq(void)
{
 struct softirq_action *h;
 __u32 pending;
 int max_restart = MAX_SOFTIRQ_RESTART;
 int cpu;

 pending = local_softirq_pending();
 account_system_vtime(current);

 __local_bh_disable((unsigned long)__builtin_return_address(0),
 SOFTIRQ_OFFSET);
 lockdep_softirq_enter();

 cpu = smp_processor_id();
restart:
 /* Reset the pending bitmask before enabling irqs */
 set_softirq_pending(0);

 local_irq_enable();

 h = softirq_vec;

 do {
 if (pending & 1) {
 unsigned int vec_nr = h - softirq_vec;
 int 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())) {
 printk(KERN_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() = 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)
 wakeup_softirqd();

 lockdep_softirq_exit();

 account_system_vtime(current);
 __local_bh_enable(SOFTIRQ_OFFSET);
}

哈,软中断处理函数的执行h->action(h) ,夹在local_irq_enable() 和local_irq_disable() 之间。前面的那个疑问解决了。

看到这里softirq 的流程就这些吗?  当然不是,除了在硬中断执行完后进入irq_exit 直接触发softirq 执行,还可以通过先预约再择机(推迟)执行的方式。这里就体现出软中断推迟执行的特点了。

处理时机2 :ksoftirq 内核线程执行软中断

1.预约

预约具体是通过raise_softirq 函数实现的:

in kernel/softirq.c

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 we're in an interrupt or softirq, we're done 
* (this also catches softirq-disabled code). We will 
* actually run the softirq once we return from 
* the irq or softirq. 
* * Otherwise we wake up ksoftirqd to make sure we 
* schedule the softirq soon. */ 
if (!in_interrupt()) 
 wakeup_softirqd(); 
}
in include/linux/interrupt.h
static inline void __raise_softirq_irqoff(unsigned int nr) 
{
 trace_softirq_raise(nr); 
 or_softirq_pending(1UL << nr); 
}

破了几层窗户最后其实就是在softirq 位图对对应的软中断号上标记。

2. ksoftirq 内核线程

预约完毕后将在raise_softirq_irqoff 中唤醒ksoftirq :

in kernel/softirq.c

void wakeup_softirqd(void) { 
/* Interrupts are disabled: no need to stop preemption 
*/ 
struct task_struct *tsk = __get_cpu_var(ksoftirqd); 
if (tsk && tsk->state != TASK_RUNNING) 
 wake_up_process(tsk); 
}
 static int run_ksoftirqd(void * __bind_cpu)
 {
         set_current_state(TASK_INTERRUPTIBLE);

         while (!kthread_should_stop()) {
                 preempt_disable();
                 if (!local_softirq_pending()) {
                         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;
                         local_irq_disable();
                         if (local_softirq_pending())
                                 __do_softirq();
                         local_irq_enable();
                         preempt_enable_no_resched();
                         cond_resched();
                         preempt_disable();
                         rcu_note_context_switch((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;
 }

处理时机3 :调用local_bh_enable显式执行软中断
void local_bh_enable(void)
 {
         _local_bh_enable_ip((unsigned long)__builtin_return_address(0));
 }
void local_bh_enable_ip(unsigned long ip)
 {
         _local_bh_enable_ip(ip);
 }

static inline void _local_bh_enable_ip(unsigned long ip)
 {
         WARN_ON_ONCE(in_irq() || irqs_disabled());
 #ifdef CONFIG_TRACE_IRQFLAGS
         local_irq_disable();
 #endif
         /*
          * Are softirqs going to be turned on now:
          */
         if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
                 trace_softirqs_on(ip);
         /*
          * Keep preemption disabled until we are done with
          * softirq processing:
          */
         sub_preempt_count(SOFTIRQ_DISABLE_OFFSET - 1);

         if (unlikely(!in_interrupt() && local_softirq_pending()))
                 do_softirq();

         dec_preempt_count();
 #ifdef CONFIG_TRACE_IRQFLAGS
         local_irq_enable();
 #endif
         preempt_check_resched();
 }

这种方式在协议栈代码中用的很多,因为协议栈往往造成大量中断的产生,催促软中断的处理似乎是一个好的选择。而且软中断向量NET_TX_SOFTIRQ, NET_RX_SOFTIRQ 优先级除TIMER高于其他,能够保证它及时处理。

##############################################################

TASKLET

##############################################################

上文讲软中断向量的时候看到了 TASKLET_SOFTIRQ,没错它就是用来为tasklet 机制服务的软中断。 说白了,tasklet机制就是基于softirq的:

另外 HI_SOFTIRQ 也是用来服务tasklet 的,只不过他是所有软中断中优先级最高的。

tasklet和基础softirq 的不同主要是:

1.softirq 能够让同一个中断处理函数在不同的CPU上同时执行(注意,这里的同时是真的同时,因为SMP环境拥有两颗或两颗以上的cpu核心),要求该函数必须是可重入的。所以内核或驱动开发者要在中断处理函数中注意互斥问题,也就是加锁,当然这里不能加睡眠锁,只能上自旋锁。

2.tasklet 机制实现了同一tasklet 只能在一个cpu上执行,但不同的tasklet却可以在不同的cpu上执行。这样开发者就可以把互斥问题抛至脑后了。另外,tasklet在执行的时候是非积累的,比如一个时间内某个tasklet被触发3次,那么待轮到tasklet handle 被执行时,实际只执行1次(不可重入)。而且每个tasklet总是在第一次执行的那个 cpu 上执行 ,这样有利于cpu 缓存。

tasklet 结构:

in kernel/softirq.c:

struct tasklet_struct

{

       struct tasklet_struct *next;

       unsigned long state;

       atomic_t count;

       void (*func)(unsigned long);

       unsigned long data;

};


其中,各个成员的含义如下:
(1)next指针指向下一个tasklet,它用于将多个tasklet连接成一个单向循环链表。
为此,内核还专门在softirq.c中定义了一个tasklet_head结构用来表示tasklet队列:
struct tasklet_head { struct tasklet_struct *list; }; 
(2)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) */
};

(3)count是一个原子计数(其实它只有0或1 两种值),对tasklet的引用进行计数。目的是在tasklet已经挂上的情况下enable 或disable这个tasklet 。需要注意的是,只有当count的值为0的时候,tasklet代码段才能执 行,即这个时候该tasklet才是enable的;如果count值非0,则该tasklet是被禁止的(disable)。因此,在执行 tasklet代码段之前,必须先检查其原子值count是否为0。
(4)func是一个函数指针,指向一个可执行的tasklet代码段,data是func函数的参数。


 

tasklet 调度函数:

tasklet 可以看作是软中断的step 2 ,所以他的softirq handler 就是tasklet 的执行/调度 函数:
in kernel/softirq.c:
static void tasklet_action(struct softirq_action *a)
{
 struct tasklet_struct *list;

 local_irq_disable();
 list = __get_cpu_var(tasklet_vec).head;
 __get_cpu_var(tasklet_vec).head = NULL;
 __get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_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;
 *__get_cpu_var(tasklet_vec).tail = t;
 __get_cpu_var(tasklet_vec).tail = &(t->next);
 __raise_softirq_irqoff(TASKLET_SOFTIRQ);
 local_irq_enable();
 }
}

用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如果加锁成功 (当前没有任何其他CPU正在执行这个tasklet),则用原子读函数atomic_read()进一步判断count成员的值。如果count为0, 说明这个tasklet是允许执行的。如果tasklet_trylock()宏加锁不成功,或者因为当前tasklet的count值非0而不允许执行 时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再 执行。为此进行这样几步操作:(1)先关 CPU中断,以保证下面操作的原子性。(2)把这个tasklet重新放回到当前CPU的tasklet队列的 首部;(3)调用__cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;(4)开中断。

##################################################################

Workqueue

#################################################################

工 作队列是Linux 2.6 内核中新增加的一种下半部机制。它与其它几种下半部分机制最大的区别就是它可以把工作推后,交由一个内核线程–工作者线程 (内核线程)去执行。内核线程只在内核空间运行,没有自己的用户空间,它和普通进程一样可以被调度,也可以被抢占。该工作队列总是会在进程上下文执行。缺 省的工作者线程叫做events/n,n是处理器的编号。如果要在工作者线程中执行大量的处理操作时,可以创建属于自己的工作者线程。这样,通过工作队列 执行的代码能占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。

由于softirq和 tasklet在同一个CPU上的串行执行,不利于多媒体实时任务和其它要求严格的任务的处理。在有些系统中采用了新的工作队列机制取代软中断机制来完成 网络接收中断后的推后处理工作。通过由具有最高实时优先级的工作者线程来处理实时多媒体任务或其它要求较高的任务,而由优先级次高的工作者线程来处理其他 的非实时数据业务。Linux 2.6 内核的调度系统采用了内核抢占和O(1)调度,能够满足软实时的要求,因此几乎总能保证处理实时多媒体任务或要求 较高任务的工作者线程优先执行。这样,就保证了多媒体实时任务或要求较高任务得到优先的处理。

工作队列靠内核线程来运行,可能会引起上下文切换(当任务睡眠、阻塞需要重新调度时),这样它造成的开销也比较大。 

由于内核线程已经脱离了内核中断范围所以这里不再讲实现细节了,后续总结内核线程的时候再说吧。

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s

%d 博主赞过: