Chinaunix首页 | 论坛 | 博客
  • 博客访问: 84869
  • 博文数量: 18
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 321
  • 用 户 组: 普通用户
  • 注册时间: 2013-07-30 21:09
文章分类

全部博文(18)

文章存档

2015年(3)

2014年(9)

2013年(6)

我的朋友

分类: LINUX

2014-10-27 23:16:26

http://blog.chinaunix.net/uid-23769728-id-3079164.html
http://blog.csdn.net/droidphone/article/details/7518428
http://www.cnblogs.com/hustcat/archive/2009/08/31/1557507.html

对于别人写的两片blog做一下读书笔记
在目前的linux中,为了提高中断的性能,将中断分为hard irq和soft irq。hard irq就是我们经常说的硬件中断,该中断发生后必须终止当前的执行,中断处理程序被调用,在处理程序被调用期间,中断响应一般是关闭的。对于中断处理程序来说一定要处理的块,以便让系统能响应其它的中断事件。从这个角度出发,中断处理程序只处理必须立即处理的事情,而将可以放到稍后处理的工作都放到soft irq,即软中断中去执行。一般来说,软中断有两个地方可以被执行,一是任何一个中断处理程序执行完后,会调用软中断处理程序,但需要注意在软中断执行期间,中断响应是打开的。另外一个处理的地方就是ksoftirqd线程,每个cpu一个。ksoftirqd会每隔一定时间会将未处理软中断事件做处理。下面这张图表示了linux中整个中断的处理流程,其中softirq实际是在真个中断处理过程中,当中断处理程序执行完后,其被执行的,这时的中断响应是打开的。




1、上下文
一般来说,CPU在任何时刻都处于以下三种情况之一:
(1)运行于用户空间,执行用户进程;
(2)运行于内核空间,处于进程上下文;
(3)运行于内核空间,处于中断上下文。
应用程序通过系统调用陷入内核,此时处于进程上下文。现代几乎所有的CPU体系结构都支持中断。当外部设备产生中断,向CPU发送一个异步信号,CPU调用相应的中断处理程序来处理该中断,此时CPU处于中断上下文。
在进程上下文中,可以通过current关联相应的任务。进程以进程上下文的形式运行在内核空间,可以发生睡眠,所以在进程上下文中,可以使作信号量(semaphore)。实际上,内核经常在进程上下文中使用信号量来完成任务之间的同步,当然也可以使用锁。
中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程)。由于没有进程背景,在中断上下文中不能发生睡眠,否则又如何对它进行调度。所以在中断上下文中只能使用锁进行同步,正是因为这个原因,中断上下文也叫做原子上下文(atomic context)(关于同步以后再详细讨论)。在中断处理程序中,通常会禁止同一中断,甚至会禁止整个本地中断,所以中断处理程序应该尽可能迅速,所以又把中断处理分成上部和下部(关于中断以后再详细讨论)。

在SMP架构下,由于中断可以嵌套,软中断在执行中可以被硬件中断,因此需要有一套同步机制,这个同步机制就是preempt_count变量,该变量是每个进程一个,存放在thread_info结构中,定义如下:
#define preempt_count() (current_thread_info()->preempt_count)
内核无论出于进程上下文还是中断上下文,都可以通过current来引用到该结构




按照x86处理器在外部中断发生时的硬件逻辑,在do_IRQ被调用时,处理器已经屏蔽了对外部中断的响应。在图中我们看 到中断的处理大体上被分成两部分HARDIRQ和SOFTIRQ,对应到代码层面,do_IRQ()中调用irq_enter函数可以看做hardirq 部分的开始,而irq_exit函数的调用则标志着softirq部分的开始:
 

  1. unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
  2. {
  3.         ...
  4.         irq_enter();

  5.         //handle external interrupt (ISR)
  6.         //在这里执行中断处理函数
  7.         ...
  8.         irq_exit();

  9.         return 1;
  10. }
irq_enter()函数的核心调用是__irq_enter(),后者的主要作用是在图2的 preempt_count变量的HARDIRQ部分+1,即标识一个hardirq的上下文,所以可以认为do_IRQ()调用irq_enter函数 意味着中断处理进入hardirq阶段。此时处理器响应外部中断的能力依然是被disable掉的(EFLAG.IF=0),因为ISR基本上属于设备驱 动程序涉足的领域,内核无法保证在设备驱动程序的ISR中会否将EFLAG.IF置1(此处涉及可能的中断嵌套以及中断栈溢出问题,可以参考ARM中断处 理的那个帖子),所以我们会在内核代码中看到内核开发者为了尽可能避免非受控的ISR部分给系统带来的损害所做的努力。按照现在内核的设计理念,在 hardirq部分最好不要打开中断,所以处在hardirq上下文中的设备驱动程序的ISR应该尽可能快地返回(所谓只完成最关键的任务),而将耗时的 中断处理善后工作留到softirq部分,因为在softirq部分,内核会使EFLAG.IF=1,所以也意味着softirq部分可以随时被外部中断 所打断。
下面我们重点讨论一下softirq部分的实现原理,do_IRQ()中调用irq_exit函数标志softirq部分 的开始。 irq_exit()函数会首先在图2的preempt_count变量的HARDIRQ部分-1,目的是清除hardirq的上下文标记。然后它有个关键的调用invoke_softirq,用于实际的softirq处理工作:


  1. void irq_exit(void)
  2. {
  3.         ...
  4.         sub_preempt_count(IRQ_EXIT_OFFSET);
  5.         if (!in_interrupt() && local_softirq_pending())
  6.                 invoke_softirq();

  7.         rcu_irq_exit();
  8.         ...
  9. }
ILDD中对in_interrupt()函数的叙述很明确:"...其主要用意是根据当前preempt_count变 量,来判断当前代码是否在一个中断上下文中执行。根据in_interrupt的定义来看,Linux内核认为HARDIRQ、SOFTIRQ以及NMI 都属于interrupt范畴...",所以softirq部分是否被执行,取决于:1.当前是否在中断上下文,2. 是否有pending的softirq需要处理。

在教科书中经常看到一句话:任何时刻,一个CPU中每个软IRQ只能有一个实例可以执行;然而相同的软IRQ可以在不同的CPU中执行。其实这句话还是很难理解的,从代码来看,若当前CPU已经处于in_interrupt状态,即当前的preempt_count中的softirq部分不为0,则不允许进行软irq的调用执行。比如当前正在进行softirq的执行,也就是正好执行在invoke_softirq中,然后发生中断了,在中断处理完成后执行irq_exit时,发现in_interrupt为ture,表示上次是处于软中断执行阶段,也就是说当前cpu上已经有一个软IRQ实例在执行了,因此就不会再进行invoke_softirq调用。

第一个条件,主要用来防止softirq部分的重入,因为一旦有pending的softirq需要处理,那么invoke_softirq()的调用 (实际的工作发生在__do_softirq函数中)首先会将图2中SOFTIRQ部分+1,这样若在当前正在处理softirq过程中发生了外部中 断,hardirq部分标识了一个pending softirq,那么在irq_exit函数中将直接返回,而不会调用到invoke_softirq。

softirq部分的核心代码段如下:

  1. asmlinkage void __do_softirq(void)
  2. {
  3.         ...
  4.         //获得__softirq_pending变量上保存的pending softirq数据
  5.         //local_softirq_pending()获得的变量是每cpu一个,因为这些信息和任何一个进程都没有关系的
  6.         pending = local_softirq_pending();
  7.         //将图2中SOFTIRQ部分+1,标识softirq上下文,此时in_interrupt()返回true.
  8.         __local_bh_disable((unsigned long)__builtin_return_address(0), SOFTIRQ_OFFSET); 
  9.         ...
  10. restart:
  11.         /* Reset the pending bitmask before enabling irqs */
  12.         set_softirq_pending(0);
  13.         //注意此处,内核调用local_irq_enable打开处理器响应外部中断能力,EFLAG.IF=1,所以接下来的softirq处理时外部中断可以进入处理器
  14.         local_irq_enable();

  15.         h = softirq_vec;
  16.         do {
  17.                 if (pending & 1) {
  18.                         ...
  19.                         //调用每个类型的softirq所对应的处理函数,见下图3
  20.                         h->action(h); 
  21.                         ...
  22.                 }
  23.                 h++;
  24.                 pending >>= 1;
  25.         } while (pending);
  26.         //softirq处理结束前,重新关闭中断。中断的再次打开发生在中断的返回,也就是在图1中处理器在执行iret指令时的硬件逻辑,POP EFLAG, EFLAG.IF=1
  27.         local_irq_disable();
  28.         
  29.         //再次检测是否有新的pending softirq,因为softirq执行时可能有新的外部中断进来,如果有,此处一并处理。
  30.         pending = local_softirq_pending();
  31.         if (pending && --max_restart)
  32.                 goto restart;
  33.         ...
  34.         //将图2中SOFTIRQ部分-1,标识softirq上下文的结束
  35.         __local_bh_enable(SOFTIRQ_OFFSET); 
  36. }

对每个类型的softirq的处理发生在上面的do...while...循环中,原理其实非常简单,为节省文字,用下面一个草图来做说明(图3):



所以do...while...循环实际上是从bit 0遍历__softirq_pending变量,目前该变量的0~9分别对应10个不同类型的softirq,每个softirq对应不同的处理函数,比如HI_SOFTIRQ对应的action为tasklet_hi_action,它与TASKLET_SOFTIRQ的action的原理完全一样,也就是我们平常所说的tasklet。__softirq_pending是个per-CPU型的变量,因为SMP系统中每个处理器都可以独立处理各自到来 的外部中断,也都对应有各自的hardirq栈和softirq栈,详见一贴。另外,在前面的帖子中我们一直说hardirq部分标识一个softirq,何谓标识一个softirq?其实就是调用tasklet_schedule()来把TASKLET_SOFTIRQ位置1.

如何触发软件中断:
要触发一个软中断,只要调用api:raise_softirq即可,它的实现很简单,先是关闭本地cpu中断,然后调用:raise_softirq_irqoff
raise_softirq->raise_softirq_irqoff->__raise_softirq_irqoff

  1. void raise_softirq(unsigned int nr)  
  2. {  
  3.     unsigned long flags;  
  4.     // 关闭中断
  5.     local_irq_save(flags);  
  6.     raise_softirq_irqoff(nr);  
  7.     // 打开中断
  8.     local_irq_restore(flags);  
  9. }  

  1. inline void raise_softirq_irqoff(unsigned int nr)  
  2. {  
  3.     __raise_softirq_irqoff(nr);  
  4.   
  5.         ......  
  6.     if (!in_interrupt())  
  7.         wakeup_softirqd();  
  8. }  
先是通过__raise_softirq_irqoff设置cpu的软中断pending标志位(irq_stat[NR_CPUS] ),然后通过in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,如果都不成立,则唤醒软中断的守护进程,在守护进程中执行软中断的回调函数。否则什么也不做,软中断将会在中断的退出阶段被执行。
在这里,wakeup_softirqd就是唤醒ksoftirqd线程

关于tasklet,又有一个非常难以理解的限制:相同的微任务不能在不同的cpu上调度,对于任何微任务而言,都只能有一个实例在执行中;
  1. static void tasklet_action(struct softirq_action *a)  
  2. {  
  3.     struct tasklet_struct *list;  
  4.   
  5.     local_irq_disable();  
  6.     list = __this_cpu_read(tasklet_vec.head);  
  7.     __this_cpu_write(tasklet_vec.head, NULL);  
  8.     __this_cpu_write(tasklet_vec.tail, &__get_cpu_var(tasklet_vec).head);  
  9.     local_irq_enable();  
  10.   
  11.     while (list) {  
  12.         struct tasklet_struct *t = list;  
  13.   
  14.         list = list->next;  
  15.   
  16.         if (tasklet_trylock(t)) {  
  17.             if (!atomic_read(&t->count)) {  
  18.                 if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))  
  19.                     BUG();  
  20.                 t->func(t->data);  
  21.                 tasklet_unlock(t);  
  22.                 continue;  
  23.             }  
  24.             tasklet_unlock(t);  
  25.         }  
  26.   
  27.         local_irq_disable();  
  28.         t->next = NULL;  
  29.         *__this_cpu_read(tasklet_vec.tail) = t;  
  30.         __this_cpu_write(tasklet_vec.tail, &(t->next));  
  31.         __raise_softirq_irqoff(TASKLET_SOFTIRQ);  
  32.         local_irq_enable();  
  33.     }  
  34. }  
从上面的代码可以看出,对于一个微任务,其是否能否执行取决于两个条件:
1. tasklet_trylock用于判断该tasklet是否已经在其它cpu上执行,代码如下
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}

static inline void tasklet_unlock(struct tasklet_struct *t)
{
smp_mb__before_clear_bit(); 
clear_bit(TASKLET_STATE_RUN, &(t)->state);
}

主要就是对state变量的第1位做test和set
2. 对于tasklet的count值做判断,count=0,代表该tasklet是启动的,否则不是

从上面的分析我们可以看到,一个驱动程序若要采用tasklet机制,其首先可以定义一个全局的task_struct变量,然后每次中断触发后,只要把同一个task_struct变量task_schedule到中断处理程序被执行的cpu上面就可以了。后面cpu,即内核会保证若同一个task_struct变量被挂到了多个cpu上,同时只有一个cpu上的会被执行。因此在设计tasklet的程序的时候,是不需要考虑cpu之间锁问题的。





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