Chinaunix首页 | 论坛 | 博客
  • 博客访问: 224433
  • 博文数量: 26
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 1631
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-09 00:51
个人简介

努力 奋斗 专注于linux内核 微信:tolimit 扣扣:348958453

文章分类
文章存档

2015年(26)

分类: LINUX

2015-06-01 16:04:16

本文为原创,转载请注明:http://blog.chinaunix.net/uid-26772321-id-5053324.html


  在上一篇文章中,我们看到中断实际分为了两个部分,俗称就是一部分是硬中断,一部分是软中断。软中断是专门用于处理中断过程中费时费力的操作,而为什么系统要分硬中断和软中断呢?问得明白点就是为什么需要软中断。我们可以试着想想,如果只有硬中断的情况下,我们需要在中断处理过程中执行一些耗时的操作,比如浮点数运算,复杂算法的运算时,其他的外部中断就不能得到及时的响应,因为在硬中断过程中中断是关闭着的,甚至一些很紧急的中断会得不到响应,系统稳定性和及时性都受到很大影响。所以linux为了解决上述这种情况,将中断处理分为了两个部分,硬中断和软中断。首先一个外部中断得到响应时,会先关中断,并进入到硬中断完成较为紧急的操作,然后开中断,并在软中断执行那些非紧急、可延时执行的操作;在这种情况下,紧急操作可以立即执行,而其他的外部中断也可以获得一个较为快速的响应。这也是软中断存在的必要性。在软中断过程中是不可以被抢占也不能被阻塞的,也不能在一个给定的CPU上交错执行。

软中断

  软中断是在中断框架中专门用于处理非紧急操作的,在SMP系统中,软中断可以并发地运行在多个CPU上,但在一些路径在需要使用自旋锁进行保护。在系统中,很多东西都分优先级,软中断也不例外,有些软中断要求更快速的响应运行,在内核中软中断一共分为10个,同时也代表着10种不同的优先级,系统用一个枚举变量表示:

  1. enum
  2. {
  3.     HI_SOFTIRQ=0, /* 高优先级tasklet */ /* 优先级最高 */
  4.     TIMER_SOFTIRQ, /* 时钟相关的软中断 */
  5.     NET_TX_SOFTIRQ, /* 将数据包传送到网卡 */
  6.     NET_RX_SOFTIRQ, /* 从网卡接收数据包 */
  7.     BLOCK_SOFTIRQ, /* 块设备的软中断 */
  8.     BLOCK_IOPOLL_SOFTIRQ, /* 支持IO轮询的块设备软中断 */
  9.     TASKLET_SOFTIRQ, /* 常规tasklet */
  10.     SCHED_SOFTIRQ, /* 调度程序软中断 */
  11.     HRTIMER_SOFTIRQ, /* 高精度计时器软中断 */
  12.     RCU_SOFTIRQ, /* RCU锁软中断,该软中断总是最后一个软中断 */      /* 优先级最低 */

  13.     NR_SOFTIRQS /* 软中断数,为10 */
  14. };
  注释中的tasklet我们之后会说明,这里先无视它。每一个优先级的软中断都使用一个struct softirq_action结构来表示,在这个结构中,只有一个成员变量,就是action函数指针,因为不同的软中断它的处理方式可能不同,从优先级表中就可以看出来,有块设备的,也有网卡处理的。系统将这10个软中断用softirq_vec[10]的数组进行保存。
  1. /* 用于描述一个软中断 */
  2. struct softirq_action
  3. {
  4.     /* 此软中断的处理函数 */
  5.     void (*action)(struct softirq_action *);
  6. };

  7. /* 10个软中断描述符都保存在此数组 */
  8. static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
  系统一般使用open_softirq()函数进行软中断描述符的初始化,主要就是将action函数指针指向该软中断应该执行的函数。在start_kernel()进行系统初始化中,就调用了softirq_init()函数对HI_SOFTIRQ和TASKLET_SOFTIRQ两个软中断进行了初始化
  1. void __init softirq_init(void)
  2. {
  3.     int cpu;

  4.     for_each_possible_cpu(cpu) {
  5.         per_cpu(tasklet_vec, cpu).tail =
  6.             &per_cpu(tasklet_vec, cpu).head;
  7.         per_cpu(tasklet_hi_vec, cpu).tail =
  8.             &per_cpu(tasklet_hi_vec, cpu).head;
  9.     }

  10.     /* 开启常规tasklet */
  11.     open_softirq(TASKLET_SOFTIRQ, tasklet_action);
  12.     /* 开启高优先级tasklet */
  13.     open_softirq(HI_SOFTIRQ, tasklet_hi_action);
  14. }


  15. /* 开启软中断 */
  16. void open_softirq(int nr, void (*action)(struct softirq_action *))
  17. {
  18.     softirq_vec[nr].action = action;
  19. }
   可以看到,TASKLET_SOFTIRQ的action操作使用了tasklet_action()函数,HI_SOFTIRQ的action操作使用了tasklet_hi_action()函数,这两个函数我们需要结合tasklet进行说明。我们也可以看看其他的软中断使用了什么函数:
  1. open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
  2.     open_softirq(NET_TX_SOFTIRQ, net_tx_action);
  3.     open_softirq(NET_RX_SOFTIRQ, net_rx_action);
  4.     open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);
  5.     open_softirq(BLOCK_IOPOLL_SOFTIRQ, blk_iopoll_softirq);
  6.     open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);
  7.     open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq);
  8.     open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);

   其实很明显可以看出,除了TASKLET_SOFTIRQ和HI_SOFTIRQ,其他的软中断更多地是用于特定的设备和环境,对于我们普通的IO驱动和设备而已,使用的软中断几乎都是TASKLET_SOFTIRQ和HI_SOFTIRQ,而系统为了对这些不同IO设备进行统一的处理,就在TASKLET_SOFTIRQ和HI_SOFTIRQ的action函数中使用到了tasklet。

  对于每个CPU,都有一个irq_cpustat_t的数据结构,里面有一个__softirq_pending变量,这个变量很重要,用于表示该CPU的哪个软中断处于挂起状态,在软中断处理时可以根据此值跳过不需要处理的软中断,直接处理需要处理的软中断。内核使用local_softirq_pending()获取此CPU的__softirq_pending的值。

  当使用open_softirq设置好某个软中断的action指针后,该软中断就会开始可以使用了,其实更明了地说,从中断初始化完成开始,即使所有的软中断都没有使用open_softirq()进行初始化,软中断都已经开始使用了,只是所有软中断的action都为空,系统每次执行到软中断都没有软中断需要执行罢了。

  在每个CPU上一次软中断处理的一个典型流程是:

  1. 硬中断执行完毕,开中断。
  2. 检查该CPU是否处于嵌套中断的情况,如果处于嵌套中,则不执行软中断,也就是在最外层中断才执行软中断。
  3. 执行软中断,设置一个软中断执行最多使用时间和循环次数(10次)。
  4. 进入循环,获取CPU的__softirq_pending的副本。
  5. 执行此__softirq_pending副本中所有需要执行的软中断。
  6. 如果软中断执行完毕,退出中断上下文。
  7. 如果还有软中断需要执行(在软中断期间又发发生了中断,产生了新的软中断,新的软中断记录在CPU的__softirq_pending上,而我们的__softirq_pending只是个副本)。
  8. 检查此次软中断总共使用的时间和循环次数,条件允许继续执行软中断,循环次数减一,并跳转到第4步。

  我们具体看一下代码,首先在irq_exit()中会检查是否需要进行软中断处理:

  1. void irq_exit(void)
  2. {
  3. #ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
  4.     local_irq_disable();
  5. #else
  6.     WARN_ON_ONCE(!irqs_disabled());
  7. #endif

  8.     account_irq_exit_time(current);
  9.     /* 减少preempt_count的硬中断计数器 */
  10.     preempt_count_sub(HARDIRQ_OFFSET);
  11.     
  12.     /* in_interrupt()会检查preempt_count上的软中断计数器和硬中断计数器来判断是否处于中断嵌套中 */
  13.     /* local_softirq_pending()则会检查该CPU的__softirq_pending变量,是否有软中断挂起 */
  14.     if (!in_interrupt() && local_softirq_pending())
  15.         invoke_softirq();

  16.     tick_irq_exit();
  17.     rcu_irq_exit();
  18.     trace_hardirq_exit(); /* must be */
  19. }
  我们再进入到invoke_softirq():
  1. static inline void invoke_softirq(void)
  2. {

  3.     if (!force_irqthreads) {
  4. #ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
  5.         /*
  6.          * We can safely execute softirq on the current stack if
  7.          * it is the irq stack, because it should be near empty
  8.          * at this stage.
  9.          */
  10.         /* 软中断处理函数 */
  11.         __do_softirq();
  12. #else
  13.         /*
  14.          * Otherwise, irq_exit() is called on the task stack that can
  15.          * be potentially deep already. So call softirq in its own stack
  16.          * to prevent from any overrun.
  17.          */
  18.         do_softirq_own_stack();
  19. #endif
  20.     } else {
  21.         /* 如果强制使用软中断线程进行软中断处理,会通知调度器唤醒软中断线程ksoftirqd */
  22.         wakeup_softirqd();
  23.     }
  24. }
  重头戏就在__do_softirq()中,我已经注释好了,方便大家看:
  1. asmlinkage __visible void __do_softirq(void)
  2. {
  3.     /* 为了防止软中断执行时间太长,设置了一个软中断结束时间 */
  4.     unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
  5.     /* 保存当前进程的标志 */
  6.     unsigned long old_flags = current->flags;
  7.     /* 软中断循环执行次数: 10次 */
  8.     int max_restart = MAX_SOFTIRQ_RESTART;
  9.     /* 软中断的action指针 */
  10.     struct softirq_action *h;
  11.     bool in_hardirq;
  12.     __u32 pending;
  13.     int softirq_bit;

  14.     /*
  15.      * Mask out PF_MEMALLOC s current task context is borrowed for the
  16.      * softirq. A softirq handled such as network RX might set PF_MEMALLOC
  17.      * again if the socket is related to swap
  18.      */
  19.     current->flags &= ~PF_MEMALLOC;

  20.     /* 获取此CPU的__softirq_pengding变量值 */
  21.     pending = local_softirq_pending();
  22.     /* 用于统计进程被软中断使用时间 */
  23.     account_irq_enter_time(current);

  24.     /* 增加preempt_count软中断计数器,也表明禁止了调度 */
  25.     __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
  26.     in_hardirq = lockdep_softirq_start();

  27. /* 循环10次的入口,每次循环都会把所有挂起需要执行的软中断执行一遍 */
  28. restart:
  29.     /* 该CPU的__softirq_pending清零,当前的__softirq_pending保存在pending变量中 */
  30.     /* 这样做就保证了新的软中断会在下次循环中执行 */
  31.     set_softirq_pending(0);

  32.     /* 开中断 */
  33.     local_irq_enable();

  34.     /* h指向软中断数组头 */
  35.     h = softirq_vec;

  36.     /* 每次获取最高优先级的已挂起软中断 */
  37.     while ((softirq_bit = ffs(pending))) {
  38.         unsigned int vec_nr;
  39.         int prev_count;
  40.         /* 获取此软中断描述符地址 */
  41.         h += softirq_bit - 1;
  42.         
  43.         /* 减去软中断描述符数组首地址,获得软中断号 */
  44.         vec_nr = h - softirq_vec;
  45.         /* 获取preempt_count的值 */
  46.         prev_count = preempt_count();

  47.         /* 增加统计中该软中断发生次数 */
  48.         kstat_incr_softirqs_this_cpu(vec_nr);

  49.         trace_softirq_entry(vec_nr);
  50.         /* 执行该软中断的action操作 */
  51.         h->action(h);
  52.         trace_softirq_exit(vec_nr);

  53.         /* 之前保存的preempt_count并不等于当前的preempt_count的情况处理,也是简单的把之前的复制到当前的preempt_count上,这样做是防止最后软中断计数不为0导致系统不能够执行调度 */
  54.         if (unlikely(prev_count != preempt_count())) {
  55.             pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
  56.                    vec_nr, softirq_to_name[vec_nr], h->action,
  57.                    prev_count, preempt_count());
  58.             preempt_count_set(prev_count);
  59.         }
  60.         /* h指向下一个软中断,但下个软中断并不一定需要执行,这里只是配合softirq_bit做到一个处理 */
  61.         h++;
  62.         pending >>= softirq_bit;
  63.     }

  64.     rcu_bh_qs();
  65.     /* 关中断 */
  66.     local_irq_disable();

  67.     /* 循环结束后再次获取CPU的__softirq_pending变量,为了检查是否还有软中断未执行 */
  68.     pending = local_softirq_pending();
  69.     /* 还有软中断需要执行 */
  70.     if (pending) {
  71.         /* 在还有软中断需要执行的情况下,如果时间片没有执行完,并且循环次数也没到10次,继续执行软中断 */
  72.         if (time_before(jiffies, end) && !need_resched() &&
  73.             --max_restart)
  74.             goto restart;
  75.         /* 这里是有软中断挂起,但是软中断时间和循环次数已经用完,通知调度器唤醒软中断线程去执行挂起的软中断,软中断线程是ksoftirqd,这里只起到一个通知作用,因为在中断上下文中是禁止调度的 */
  76.         wakeup_softirqd();
  77.     }

  78.     lockdep_softirq_end(in_hardirq);
  79.     /* 用于统计进程被软中断使用时间 */
  80.     account_irq_exit_time(current);
  81.     /* 减少preempt_count中的软中断计数器 */
  82.     __local_bh_enable(SOFTIRQ_OFFSET);
  83.     WARN_ON_ONCE(in_interrupt());
  84.     /* 还原进程标志 */
  85.     tsk_restore_flags(current, old_flags, PF_MEMALLOC);
  86. }

  流程就和上面所说的一致,如果还有不懂,可以去内核代码目录/kernel/softirq.c查看源码。

 

tasklet

  软中断有多种,部分种类有自己特殊的处理,如从NET_TX_SOFTIRQ和NET_RT_SOFTIRQ、BLOCK_SOFTIRQ等,而如HI_SOFTIRQ和TASKLET_SOFTIRQ则是专门使用tasklet。它是在I/O驱动程序中实现可延迟函数的首选方法,如上一句所说,它建立在HI_SOFTIRQ和TASKLET_SOFTIRQ这两种软中断之上,多个tasklet可以与同一个软中断相关联,系统会使用一个链表组织他们,而每个tasklet执行自己的函数处理。而HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断并没有什么区别,他们只是优先级上的不同而已,系统会先执行HI_SOFTIRQ的tasklet,再执行TASKLET_SOFTIRQ的tasklet。同一个tasklet不能同时在几个CPU上执行,一个tasklet在一个时间上只能在一个CPU的软中断链上,不能同时在多个CPU的软中断链上,并且当这个tasklet正在执行时,其他CPU不能够执行这个tasklet。也就是说,tasklet不必要编写成可重入的函数。

  系统会为每个CPU维护两个链表,用于保存HI_SOFTIRQ的tasklet和TASKLET_SOFTIRQ的tasklet,这两个链表是tasklet_vec和tasklet_hi_vec,它们都是双向链表,如下:

  1. struct tasklet_head {
  2.     struct tasklet_struct *head;
  3.     struct tasklet_struct **tail;
  4. };

  5. static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
  6. static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

   在softirq_init()函数中,会将每个CPU的tasklet_vec链表和tasklet_hi_vec链表进行初始化,将他们的头尾相连,实现为一个空链表。由于tasklet_vec和tasklet_hi_vec处理方式几乎一样,只是软中断的优先级别不同,我们只需要理解系统如何对tasklet_vec进行处理即可。需要注意的是,tasklet_vec链表都是以顺序方式执行,并不会出现后一个先执行,再到前一个先执行(在软中断期间被中断的情况),之后的代码我们详细说明。

  介绍完tasklet_vec和tasklet_hi_vec链表,我们来看看tasklet,tasklet简单来说,就是一个处理函数的封装,类似于硬中断中的irqaction结构。一般来说,在一个驱动中如果需要使用tasklet进行软中断的处理,只需要一个中断对应初始化一个tasklet,它可以在每次中断产生时重复使用。系统使用tasklet_struct结构进行描述一个tasklet,而且对于同一个tasklet_struct你可以选择放在tasklet_hi_vec链表或者tasklet_vec链表上。我们来看看:

  1. struct tasklet_struct
  2. {
  3.     struct tasklet_struct *next; /* 指向链表下一个tasklet */
  4.     unsigned long state; /* tasklet状态 */
  5.     atomic_t count; /* 禁止计数器,调用tasklet_disable()会增加此数,tasklet_enable()减少此数 */
  6.     void (*func)(unsigned long); /* 处理函数 */
  7.     unsigned long data; /* 处理函数使用的数据 */
  8. };

  tasklet状态主要分为以下两种:

  • TASKLET_STATE_SCHED:这种状态表示此tasklet处于某个tasklet链表之上(可能是tasklet_vec也可能是tasklet_hi_vec)。
  • TASKLET_STATE_RUN:表示此tasklet正在运行中。

  这两个状态主要就是用于防止tasklet同时在几个CPU上运行和在同一个CPU上交错执行。

 

  而func指针就是指向相应的处理函数。在编写驱动时,我们可以使用tasklet_init()函数或者DECLARE_TASKLET宏进行一个task_struct结构的初始化,之后可以使用tasklet_schedule()或者tasklet_hi_schedule()将其放到相应链表上等待CPU运行。我们使用一张图描述一下软中断和tasklet结合运行的情况:


  我们知道,每个软中断都有自己的action函数,在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函数中,就用到了它们对应的TASKLET_HI_VEC链表和TASKLET_VEC链表,并依次顺序执行链表中的每个tasklet结点。

  在SMP系统中,我们会遇到一个问题:两个CPU都需要执行同一个tasklet的情况,虽然一个tasklet只能放在一个CPU的tasklet_vec链表或者tasklet_hi_vec链表上,但是这种情况是有可能发生的,我们设想一下,中断在CPU1上得到了响应,并且它的tasklet放到了CPU1的tasklet_vec上进行执行,而当中断的tasklet上正在执行时,此中断再次发生,并在CPU2上进行了响应,此时CPU2将此中断的tasklet放到CPU2的tasklet_vec上,并执行到此中断的tasklet。

  实际上,为了处理这种情况,在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函数中,会先将对应的tasklet链表取出来,并把对应的tasklet链表的head和tail清空,如果在执行过程中,某个tasklet的state为TASKLET_STATE_RUN状态,则把此tasklet加入到原先已清空的tasklet链表的末尾,然后设置__softirq_pending变量,这样,在下次循环软中断的过程中,会再次运行这个tasklet。

  我们可以看看TASKLET_SOFTIRQ的action处理:

  1. static void tasklet_action(struct softirq_action *a)
  2. {
  3.     struct tasklet_struct *list;

  4.     local_irq_disable();
  5.     /* 将tasklet链表从该CPU中拿出来 */
  6.     list = __this_cpu_read(tasklet_vec.head);
  7.     /* 将该CPU的此软中断的tasklet链表清空 */
  8.     __this_cpu_write(tasklet_vec.head, NULL);
  9.     __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
  10.     local_irq_enable();

  11.     /* 链表已经处于list中,并且该CPU的tasklet_vec链表为空 */
  12.     while (list) {
  13.         struct tasklet_struct *t = list;

  14.         list = list->next;

  15.         /* 检查并设置该tasklet为TASKLET_STATE_RUN状态 */
  16.         if (tasklet_trylock(t)) {
  17.             /* 检查是否被禁止 */
  18.             if (!atomic_read(&t->count)) {
  19.                 /* 清除其TASKLET_STATE_SCHED状态 */
  20.                 if (!test_and_clear_bit(TASKLET_STATE_SCHED,
  21.                             &t->state))
  22.                     BUG();
  23.                 /* 执行该tasklet的func处理函数 */
  24.                 t->func(t->data);
  25.                 /* 清除该tasklet的TASKLET_STATE_RUN状态 */
  26.                 tasklet_unlock(t);
  27.                 continue;
  28.             }
  29.             tasklet_unlock(t);
  30.         }

  31.         /* 以下为tasklet为TASKLET_STATE_RUN状态下的处理 */
  32.         /* 禁止中断 */
  33.         local_irq_disable();
  34.         /* 将此tasklet添加的该CPU的tasklet_vec链表尾部 */
  35.         t->next = NULL;
  36.         *__this_cpu_read(tasklet_vec.tail) = t;
  37.         __this_cpu_write(tasklet_vec.tail, &(t->next));
  38.         /* 设置该CPU的此软中断处于挂起状态,设置irq_cpustat_t的__sofirq_pending变量,这样在软中断的下次执行中会再次执行此tasklet */
  39.         __raise_softirq_irqoff(TASKLET_SOFTIRQ);
  40.         /* 开启中断 */
  41.         local_irq_enable();
  42.     }
  43. }

 

软中断处理线程

  当有过多软中断需要处理时,为了保证进程能够得到一个满意的响应时间,设计时给定软中断一个时间片和循环次数,当时间片和循环次数到达但软中断又没有处理完时,就会把剩下的软中断交给软中断处理线程进行处理,这个线程是一个内核线程,其作为一个普通进程,优先级是120。其核心处理函数是run_ksoftirqd(),其实此线程的处理也很简单,就是调用了上面的__do_softirq()函数,我们可以具体看看:

  1. /* 在smpboot_thread_fun的一个死循环中被调用 */
  2. static void run_ksoftirqd(unsigned int cpu)
  3. {
  4.     /* 禁止中断,在__do_softirq()中会开启 */
  5.     local_irq_disable();
  6.     /* 检查该CPU的__softirq_pending是否有软中断被挂起 */
  7.     if (local_softirq_pending()) {
  8.         /*
  9.          * We can safely run softirq on inline stack, as we are not deep
  10.          * in the task stack here.
  11.          */
  12.         /* 执行软中断 */
  13.         __do_softirq();
  14.         rcu_note_context_switch(cpu);
  15.         /* 开中断 */
  16.         local_irq_enable();
  17.         /* 检查是否需要调度 */
  18.         cond_resched();
  19.         return;
  20.     }
  21.     /* 开中断 */
  22.     local_irq_enable();
  23. }



阅读(2035) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~