Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1686003
  • 博文数量: 511
  • 博客积分: 967
  • 博客等级: 准尉
  • 技术积分: 2560
  • 用 户 组: 普通用户
  • 注册时间: 2012-07-06 14:19
文章分类

全部博文(511)

文章存档

2016年(11)

2015年(61)

2014年(257)

2013年(63)

2012年(119)

分类: LINUX

2014-05-12 01:09:55

转载请注明出处:wloveg.blog.chinaunix.net

6.软中断

前面小节介绍了中断处理流程,确切地说是硬中断处理流程。内核出于性能等方面的考虑,将中断分为两部分:上半部处理中断中需要及时响应且处理时间较短的部分,下半部用于处理对响应时间要求不高且处理时间可能较长的部分。本节将主要介绍中断下半部的三种实现方式之一:软中断。

首先介绍一下软中断及其相关的数据结构。软中断是在编译期间静态分配的,由数据结构softirq_action表示:

  1. <include/linux/interrupt.h>
  2. struct softirq_action
  3. {
  4.     void    (*action)(struct softirq_action *);
  5. };

softirq_action结构体非常简单,仅包含一个指向软中断处理函数的指针。为了管理软中断,内核维护着一个softirq_action结构体数组softirq_vec[NR_SOFTIRQS],称之为软中断向量表:


  1. <kernel/softirq.c>
  2. static struct softirq_action softirq_vec[NR_SOFTIRQS];

其中NR_SOFTIRQS是一个枚举类型内核支持的最大软中断数。


  1. < include/linux/interrupt.h >
  2. enum
  3. {
  4.     HI_SOFTIRQ=0,
  5.     TIMER_SOFTIRQ,
  6.     NET_TX_SOFTIRQ,
  7.     NET_RX_SOFTIRQ,
  8.     BLOCK_SOFTIRQ,
  9.     BLOCK_IOPOLL_SOFTIRQ,
  10.     TASKLET_SOFTIRQ,
  11.     SCHED_SOFTIRQ,
  12.     HRTIMER_SOFTIRQ,
  13.     RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
  14.     NR_SOFTIRQS
  15. };

通过上面的枚举类型可知当前内核版本支持10种软中断,其中HI_SOFTIRQTASKLET_SOFTIRQ用于实现taskletTIMER_SOFTIRQHRTIMER_SOFTIRQ用于实现定时器,NET_TX_SOFTIRQNET_RX_SOFTIRQ用于实现网络设备的发送和接受操作,BLOCK_SOFTIRQBLOCK_IOPOLL_SOFTIRQ用于实现块设备操作,SCHED_SOFTIRQ用于调度器。

内核中通过调用open_softirq函数注册软中断处理程序,该函数接收两个参数:软中断索引号(枚举类型)和处理程序。根据索引号寻址到软中断向量表softirq_vec的指定元素,使其action成员指向处理程序。


  1. <kernel/softirq.c>
  2. void open_softirq(int nr, void (*action)(struct softirq_action *))
  3. {
  4.     softirq_vec[nr].action = action;
  5. }

例如,网络系统中注册接收和发送数据包的软中断:

  1. <net/core/dev.c>
  2. open_softirq(NET_TX_SOFTIRQ, net_tx_action);
  3. open_softirq(NET_RX_SOFTIRQ, net_rx_action);

一个已经注册的软中断必须“触发”后才能在“适当的时机”被执行,raise_softirq函数和__raise_softirq_irqoff宏用于触发软中断,将一个软中断设置为挂起状态(pending)。raise_softirq最终也是通过调用__raise_softirq_irqoff来实现的:


  1. <kernel/softirq.c>
  2. inline void raise_softirq_irqoff(unsigned int nr)
  3. {
  4.     __raise_softirq_irqoff(nr); //触发nr号软中断

  5.     /*
  6.      * 如果当前不处在中断上下文中,则唤醒内核线程ksoftirqd来处理软中断。
  7.      * 否则什么都不做,函数正常返回,等待“适当的时机”来处理软中断。
  8.      */
  9.     if (!in_interrupt())
  10.         wakeup_softirqd();
  11. }
  12. void raise_softirq(unsigned int nr)
  13. {
  14.     unsigned long flags;

  15.     local_irq_save(flags); //禁止中断
  16.     raise_softirq_irqoff(nr); //触发软中断之前先要禁止中断
  17.     local_irq_restore(flags); //开启中断
  18. }

__raise_softirq_irqoff被定义为宏,以上述的枚举类型作为参数:

  1. typedef struct {
  2.     unsigned int __softirq_pending; //软中断挂起位图,每bit代表一种软中断
  3.     unsigned int local_timer_irqs;
  4. } ____cacheline_aligned irq_cpustat_t;

  5. irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned; //每个cpu拥有一个软中断状态结构体,所以即使是相同类型的软中断也可以在其他cpu上同时执行。

  6. #define __IRQ_STAT(cpu, member)    (irq_stat[cpu].member)

  7. #define local_softirq_pending() \
  8.     __IRQ_STAT(smp_processor_id(), __softirq_pending)

  9. #define or_softirq_pending(x) (local_softirq_pending() |= (x))//将本地cpu的软中断挂起位图与x做或运算

  10. #define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)//将指定的nr号软中断设置为挂起状态

软中断并不是一旦触发就被执行,而是在之后的某个“适当的时机”执行,在Linux内核中共有三个“时机”:

1)从一个硬中断返回时,即在irq_exit中有机会执行软中断(详见前一节中的分析)。

2)在ksoftirqd内核线程中被执行,稍后分析。

3)在那些显式检查或执行待处理软中断的代码中。如网络系统中的netif_rx_ni函数,local_bh_enable以及local_bh_enable_ip等函数。

最终软中断都是通过do_softirq__do_softirq函数被执行的,其中do_softirq也是通过调用__do_softirq来实现的。在内核需要执行软中断时,先通过local_softirq_pending函数检查是否有软中断挂起,如果有则调用do_softirq来执行软中断。


  1. <kernel/softirq.c>
  2. asmlinkage void do_softirq(void)
  3. {
  4.     __u32 pending;
  5.     unsigned long flags;
  6. /*如果in_interrupt()返回值为非0,
  7. *则表示在中断上下文中调用了do_softirq,或者是在禁止了软中断后调用了do_softirq。
  8. *这两种情况都不允许执行软中断,所以直接返回。
  9. */
  10.     if (in_interrupt())
  11.         return;

  12.     local_irq_save(flags); //禁止本地CPU中断,并保存中断状态到flags临时变量中。

  13.     pending = local_softirq_pending(); //将本地软中断挂起状态位图存入局部变量pending

  14.     if (pending) //如果有软中断挂起等待处理
  15.         __do_softirq(); //执行软中断

  16.     local_irq_restore(flags); //将前面保存的中断状态恢复,执行后中断是否禁止取决于第5节中断处理流程中所说的注册中断时是否设置了IRQF_DISABLED 标志。当然,在2.6.36版本以后,此处执行后中断仍是禁止的。
  17. }
  18. asmlinkage void __do_softirq(void)
  19. {
  20.     struct softirq_action *h;
  21.     __u32 pending;
  22. /*为了尽快地响应软中断,__do_softirq会一直处理完所有挂起的软中断,但由于软中
  23. *断执行过程中是开启硬件中断的,如果某些中断处理程序上半部再次触发软中断(例
  24. *如网络收发包中断处理程序),可能会造成大量软中断不停的被触发,导致
  25. *__do_softirq会执行很长时间,从而大大延时了用户态进程的执行。为了避免这种情
  26. *况,内核强制__do_softirq只做MAX_SOFTIRQ_RESTART次循环后就返回。
  27. */
  28.     int max_restart = MAX_SOFTIRQ_RESTART;
  29.     int cpu;

  30.     pending = local_softirq_pending();//将本地软中断挂起状态位图存入局部变量pending
  31.     account_system_vtime(current);

  32. /*在真正执行软中断处理函数之前,先调用__local_bh_disable禁止下半部。因为执行软
  33. *中断时,硬中断是开启的,如果此时产生新中断,当从新中断返回进入irq_exit函数
  34. *时,可能有另外一个__do_softirq函数的实例开始执行。这种情况应该避免,因为软
  35. *中断不能抢占另一个软中断。因此调用__local_bh_disable禁止下半部后,irq_exit函
  36. *数中的!in_interrupt() && local_softirq_pending()条件就不会成立,也就不会调用
  37. *invoke_softirq函数执行新的软中断。
  38. */
  39.     __local_bh_disable((unsigned long)__builtin_return_address(0));
  40.     lockdep_softirq_enter();

  41.     cpu = smp_processor_id();
  42. restart:
  43.     /*由于前面已近将软中断挂起状态位图保存到局部变量中,此处将其清0,以便激活新
  44. *的软中断
  45. */
  46.     set_softirq_pending(0);

  47.     local_irq_enable(); //开启本地CPU中断

  48.     h = softirq_vec; //指向软中断向量表

  49.     do {
  50.         /*根据pending每一位配置,执行对应的软中断处理函数。这些中断处理函数由
  51. *open_softirq注册的.
  52. */
  53. if (pending & 1) {
  54.             int prev_count = preempt_count();
  55.             kstat_incr_softirqs_this_cpu(h - softirq_vec);

  56.             trace_softirq_entry(h, softirq_vec);
  57.             h->action(h); //执行软中断处理函数
  58.             trace_softirq_exit(h, softirq_vec);
  59.            
  60. /*检查软中断处理函数前后的preempt计数器的值是否发生变化,如果改变了
  61. *则打印错误提示信息并将preempt的恢复原值。对这部分代码没有完全理解,
  62. *个人觉得各类型软中断应该彼此独立互不影响,不应该修改其他类型软中断
  63. *的运行环境。至于修改了会引起什么副作用,暂时还不了解,以后再深入分
  64. *析吧。
  65. */
  66.             if (unlikely(prev_count != preempt_count())) {
  67.                 printk(KERN_ERR "huh, entered softirq %td %s %p"
  68.                  "with preempt_count %08x,"
  69.                  " exited with %08x?\n", h - softirq_vec,
  70.                  softirq_to_name[h - softirq_vec],
  71.                  h->action, prev_count, preempt_count());
  72.                 preempt_count() = prev_count;
  73.             }

  74.             rcu_bh_qs(cpu);
  75.         }
  76.         h++; //指向软中断向量表的下一项
  77.         pending >>= 1; //软中断挂起状态位图右移1位
  78.     } while (pending); //遍历所有类型的软中断

  79.     local_irq_disable(); //禁止本地CPU中断

  80.     pending = local_softirq_pending();//再次获取软中断挂起状态位图存入pending
  81.     
  82. /*如果pending不为0,表示有软中断等待处理,并且循环计数器max_restart不为0时,
  83. *回到restart 标号处运行,遍历并处理挂起的软中断.
  84. */
  85. if (pending && --max_restart)
  86.         goto restart;
  87. /*如果完成了max_restart次循环处理软中断后,仍有待处理的软中断(pending为非0),
  88. *那么调用wakeup_softirqd唤醒软中断处理内核线程处理本地CPU上的软中断,稍后
  89. *分析。
  90. */
  91.     if (pending)
  92.         wakeup_softirqd();

  93.     lockdep_softirq_exit();

  94.     account_system_vtime(current);
  95.     _local_bh_enable(); //最后开启中断下半部
  96. }

最后,我们再分析一下处理软中断的内核线程(ksoftirqd)。这实际上是内核为了解决大量软中断重复触发“霸占”CPU导致用户进程处于饥饿状态的一种折中方案。借助内核线程,可以保证在软中断负担很重的时候用户进程不会因为得不到处理时间而处于饥饿状态;同时也保证过量的软中断终究会得到处理。即使在空闲系统上,这种方案也表现良好,软中断处理得非常迅速,因为仅存的内核线程肯定会马上调度。

内核中有两个地方调用wakeup_softirqd唤醒ksoftirqd内核线程:

1)在__do_softirq函数中。

2)在raise_softirq_irqoff函数中。

  1. <kernel/softirq.c>
  2. static int ksoftirqd(void * __bind_cpu)
  3. {
  4.     set_current_state(TASK_INTERRUPTIBLE);//将当前进程状态设置为TASK_INTERRUPTIBLE

  5.     while (!kthread_should_stop()) {
  6.         preempt_disable(); //禁止内核抢占
  7.         if (!local_softirq_pending()) { //检查是否有待处理的软中断
  8.             preempt_enable_no_resched(); //开启内核抢占但不调度
  9.             schedule(); //挂起内核线程,调度其他进程执行
  10.             preempt_disable();
  11.         }

  12.         __set_current_state(TASK_RUNNING); //将内核线程状态设置为运行状态

  13.         while (local_softirq_pending()) {//如果有待处理的软中断就进入循环
  14.             /* Preempt disable stops cpu going offline.
  15.              If already offline, we'll be on wrong CPU:
  16.              don't process */
  17.             if (cpu_is_offline((long)__bind_cpu))
  18.                 goto wait_to_die;
  19.             do_softirq(); //执行软中断
  20.             preempt_enable_no_resched();//开启内核抢占
  21.             cond_resched(); //如果当前进程设置了TIF_NEED_RESCHED标志则调用调度器
  22.             preempt_disable();//再次禁止内核抢占,进入下一次循环处理软中断。
  23.             rcu_sched_qs((long)__bind_cpu);
  24.         }//如果没有待处理的软中断,退出循环
  25.         preempt_enable(); //开启内核抢占
  26.         set_current_state(TASK_INTERRUPTIBLE);//将当前进程状态设置为TASK_INTERRUPTIBLE
  27.     }
  28.     __set_current_state(TASK_RUNNING);
  29.     return 0;

  30. wait_to_die://在多处理器系统中,如果发生处理器切换执行的情况,则跳到此处继续执行。
  31.     preempt_enable();
  32.     /* Wait for kthread_stop */
  33.     set_current_state(TASK_INTERRUPTIBLE);
  34.     while (!kthread_should_stop()) {
  35.         schedule();
  36.         set_current_state(TASK_INTERRUPTIBLE);
  37.     }
  38.     __set_current_state(TASK_RUNNING);
  39.     return 0;
  40. }


参考资料:

[1] Andrew N.Sloss, Dominic Symes, Chris Wright. ARM嵌入式系统开发——软件设计与优化沈建华.

[2] Daniel P.Bovet, Marco Cesati. 深入理解Linux内核(第三版)陈莉君张琼声张宏伟.

[3] Pobert Love. Linux内核设计与实现陈莉君康华张波.

[4] Wolfgang Mauerer. 深入Linux内核架构郭旭.

[5] 陈学松深入Linux设备驱动程序内核机制.

[6] ARM Architecture Reference Manual

[7] The ARM-THUMB Procedure Call Standard




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