天天看点

linux软中断与硬中断实现原理概述。

1、软中断通过open_softirq注册一个软中断处理函数,即在软中断向量表softirq_vec数组中添加新的软中断处理action函数。

2、调用raised_softirq软中断触发函数,即把软中断标记为挂起状态。

内核会在一些位置检查是否有挂起状态的软中断,如果有的话会调用do_softirq函数来执行软中断处理函数。

3、do_softirq函数做的一个重要的工作是切换到软中断请求栈。切换到软中断请求栈就代表了此时已经处于了软中断上下文中了。

                 (1)切换到软中断请求栈(2)调用__do_softirq函数

4、__do_softirq函数

(1)开始执行软中断处理函数

(2)如果还有更多挂起的软中断没有处理,则调用wakeup_softirq函数唤醒内核线程来处理本地CPU的软中断。该线程会循环调用do_softirq函数而不是在内核那些检查软中断是否挂起的位置进行检查,节省了时间。

所以软中断的本质就是在内核某些位置检查是否有挂起的软中断(local_software_pending()不为0则表示有挂起的软中断),若有则调用do_softirq函数,在do_softirq函数中切换到软中断请求栈后,调用__do_softirq软中断回调函数。

硬中断的本质是接收到中断信号后,跳转到公共段代码执行do_IRQ,并切换到硬中断请求栈,执行中断回调函数。

        如果在每次中断的时候才检查是否有软中断需要执行的话,那么由于两次软中断的间隔的等待时间过长,这样对于用软中断处理收发网络包是不可取的,所以do_softirq可以连续检查10次是否有需要执行的软中断,这样处理软中断效率会比较高,之所以没有让do_softirq执行更多次的检查是因为由于软中断优先级比较高所以会导致用户程序长时间得不到执行。解决这个平衡的方法是如果有超过10次的软中观需要处理则唤醒ksoftirqd线程,该线程会循环检查是否有挂起的软中断,由于ksoftirqd线程的优先级低于用户进程,所以不会影响用户进程的执行。

《深入理解linux内核》 -P179,P180

以下文章转自http://bbs.chinaunix.net/thread-2333484-1-1.html

作者:独孤九贱

平台:2.6.31.13 + x86 32位

供仅讨论学习之处,不对错误之处负责,转载请注明出处。

1、软中断

软中断的原理就略过了,讲内核的书上都有,此处省略1500字。。。。。。

1.1 注册

还是以我最熟悉的两个老朋友做为开篇:

        open_softirq(NET_TX_SOFTIRQ, net_tx_action);

        open_softirq(NET_RX_SOFTIRQ, net_rx_action);

open_softirq向内核注册一个软中断,其实质是设置软中断向量表相应槽位,注册其处理函数:
  1. void open_softirq(int nr, void (*action)(struct softirq_action *))
  2. {
  3.         softirq_vec[nr].action = action;
  4. }
复制代码 softirq_vec是整个软中断的向量表:
  1. struct softirq_action
  2. {
  3.         void        (*action)(struct softirq_action *);
  4. };
  5. static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
复制代码 NR_SOFTIRQS是最大软中断向量数,内核支持的所有软中断如下:
  1. enum
  2. {
  3.         HI_SOFTIRQ=0,
  4.         TIMER_SOFTIRQ,
  5.         NET_TX_SOFTIRQ,
  6.         NET_RX_SOFTIRQ,
  7.         BLOCK_SOFTIRQ,
  8.         TASKLET_SOFTIRQ,
  9.         SCHED_SOFTIRQ,
  10.         HRTIMER_SOFTIRQ,
  11.         RCU_SOFTIRQ,       
  12.         NR_SOFTIRQS
  13. };

复制代码 好像后为为RPS新增了一个,不过这我的内核版本偏低。

1.2 激活 

当需要调用软中断时,需要调用raise_softirq函数激活软中断,这里使用术语“激活”而非“调用”,

是因为在很多情况下不能直接调用软中断。所以只能快速地将其标志为“可执行”,等待未来某一时刻调用。

为什么“在很多情况下不能直接调用软中断”?试想一下下半部引入的理念,就是为了让上半部更快地执行。

如果在中断程序代码中直接调用软中断函数,那么就失去了上半部与下半部的区别,也就是失去了其存在的意义。

内核使用一个名为__softirq_pending的位图来描述软中断,每一个位对应一个软中断,位图包含在结构irq_stat中:

  1. typedef struct {
  2.         unsigned int __softirq_pending;
  3.         ……
  4. } ____cacheline_aligned irq_cpustat_t;
  5. DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);
复制代码 宏or_softirq_pending用于设置相应的位(位或操作):
  1. #define or_softirq_pending(x)        percpu_or(irq_stat.__softirq_pending, (x))
复制代码 local_softirq_pending用于取得整个位图(而非某一位):
  1. #define local_softirq_pending()        percpu_read(irq_stat.__softirq_pending)
复制代码 宏__raise_softirq_irqoff是or_softirq_pending的包裹:
  1. #define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
复制代码 raise_softirq_irqoff通过调用__raise_softirq_irqoff实现激活软中断,它的参数nr即位软中断对应的位图槽位:
  1. inline void raise_softirq_irqoff(unsigned int nr)
  2. {
  3.         //置位图,即标记为可执行状态
  4.         __raise_softirq_irqoff(nr);
  5.         //设置了位图后,可以判断是否已经没有在中断上下文中了,如果没有,则是一个立即调用软中断的好时机。
  6.         //in_interrupt另一个作用是判断软中断是否被禁用。
  7.         //wakeup_softirqd唤醒软中断的守护进程ksoftirq。
  8.         if (!in_interrupt())
  9.                 wakeup_softirqd();
  10. }
复制代码 现在可以来看"激活"软中断的所有含义了,raise_softirq函数完成这一操作:
  1. void raise_softirq(unsigned int nr)
  2. {
  3.         unsigned long flags;
  4.         //所有操作,应该关闭中断,避免嵌套调用
  5.         local_irq_save(flags);
  6.         raise_softirq_irqoff(nr);
  7.         local_irq_restore(flags);
  8. }

复制代码 可见,激活的操作,主要是两点:

<1>、最重要的,就是置相应的位图,等待将来被处理;

<2>、如果此时已经没有在中断上下文中,则立即调用(其实是内核线程的唤醒操作),现在就是将来;

2、调度时机

是的,除了raise_softirq在,可能会(嗯,重要的是“可能”)通过wakeup_softirqd唤醒ksoftirqd外,还得明白软中断的其它调用时机。

A、当do_IRQ完成了I/O中断时调用irq_exit:

  1. #ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
  2. # define invoke_softirq()        __do_softirq()
  3. #else
  4. # define invoke_softirq()        do_softirq()
  5. #endif
  6. void irq_exit(void)
  7. {
  8.         account_system_vtime(current);
  9.         trace_hardirq_exit();
  10.         sub_preempt_count(IRQ_EXIT_OFFSET);
  11.         if (!in_interrupt() && local_softirq_pending())
  12.                 invoke_softirq();                //调用软中断
复制代码 B、如果系统使用I/O APIC,在处理完本地时钟中断时:
  1. void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)
  2. {
  3.         ……
  4.         irq_exit();
  5.         ……
  6. }

复制代码 C、local_bh_enable

local_bh_enable就是打开下半部,当然重中之中就是软中断了:

  1. void local_bh_enable(void)
  2. {
  3.         _local_bh_enable_ip((unsigned long)__builtin_return_address(0));
  4. }
  5. static inline void _local_bh_enable_ip(unsigned long ip)
  6. {
  7.         ……
  8.         if (unlikely(!in_interrupt() && local_softirq_pending()))
  9.                 do_softirq();
  10.         ……
  11. }

复制代码 D、在SMP中,当CPU处理完被CALL_FUNCTION_VECTOR处理器间中断所触发的函数时:

唔,对多核中CPU的之间的通信不熟,不太清楚这个机制……

linux软中断与硬中断实现原理概述。

3、do_softirq

不论是哪种调用方式,最终都会触发到软中断的核心处理函数do_softirq,它处理当前CPU上的所有软中断。

内核将软中断设计尽量与平台无关,但是在某些情况下,它们还是会有差异,先来看一个x86 32位的do_softirq版本:

  1. asmlinkage void do_softirq(void)
  2. {
  3.         unsigned long flags;
  4.         struct thread_info *curctx;
  5.         union irq_ctx *irqctx;
  6.         u32 *isp;
  7.         //软中断不能在中断上下文内嵌套调用。中断处理程序或下半部采用的是"激活"方式。
  8.         if (in_interrupt())
  9.                 return;
  10.         //禁止中断,保存中断标志
  11.         local_irq_save(flags);
  12.         //内核使用一个CPU位图,确实几个软中断可以同时在不同的CPU上运行,包括相同的软中断。例如,
  13.         //NET_RX_SOFTIRQ可以同时跑在多个处理器上。
  14.         //local_softirq_pending用于确定当前CPU的所有位图是否被设置。即是否有软中断等待处理。
  15.         //回想一下经常发生的网卡接收数据处理:当网卡中断落在哪一个CPU上时,与之相应的软中断函数就会在其上执行。
  16.         //从这里来看,实质就是哪个网卡中断落在相应的CPU上,CPU置其软中断位图,这里做相应的检测(这里local_softirq_pending只
  17.         //是一个总的判断,后面还有按位的判断),检测到有相应的位,执行之
  18.         if (local_softirq_pending()) {
  19.                 //取得线程描述符
  20.                 curctx = current_thread_info();
  21.                 //构造中断上下文结构,softirq_ctx是每个CPU的软中断上下文
  22.                 //static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
  23.                 //这里先取得当前CPU的软中断上下文,然后为其赋初始值——保存当前进程和栈指针
  24.                 irqctx = __get_cpu_var(softirq_ctx);
  25.                 irqctx->tinfo.task = curctx->task;
  26.                 irqctx->tinfo.previous_esp = current_stack_pointer;
  27.                 //构造中断栈帧
  28.                 isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
  29.                 //call_on_stack切换内核栈,并在中断上下文上执行函数__do_softirq
  30.                 call_on_stack(__do_softirq, isp);
  31.                 WARN_ON_ONCE(softirq_count());
  32.         }
  33.         //恢复之
  34.         local_irq_restore(flags);
  35. }

复制代码 当配置了CONFIG_4KSTACKS,每个进程的thread_union只有4K,而非8K。发生中断时,内核栈将不使用进程的内核栈,而使用每个 cpu的中断请求栈。

内核栈将使用每个 cpu的中断请求栈,而非进程的内核栈来执行软中断函数:

  1. static void call_on_stack(void *func, void *stack)
  2. {
  3.         asm volatile("xchgl        %%ebx,%%esp        \n"                                //交换栈指针,中断栈帧的指针stack做为传入参数(%ebx),交换后esp是irq_ctx的栈顶,ebx是进程内核栈的栈
  4.                      "call        *%%edi                \n"                                        //调用软中断函数
  5.                      "movl        %%ebx,%%esp        \n"                                        //恢复之,直接使用movl,而非xchgl是因为函数执行完毕,中断的栈帧指针已经没有用处了
  6.                      : "=b" (stack)
  7.                      : "0" (stack),
  8.                        "D"(func)
  9.                      : "memory", "cc", "edx", "ecx", "eax");
  10. }
复制代码 PS:所有的这些执行,应该都是在定义4K栈的基础上的:
  1. #ifdef CONFIG_4KSTACKS
  2. union irq_ctx {
  3.         struct thread_info      tinfo;
  4.         u32                     stack[THREAD_SIZE/sizeof(u32)];
  5. } __attribute__((aligned(PAGE_SIZE)));
  6. static DEFINE_PER_CPU(union irq_ctx *, hardirq_ctx);
  7. static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
  8. ……
  9. static void call_on_stack(void *func, void *stack)
  10. ……

复制代码 是的,这个版本相对复杂,但是如果看了复杂的,再来看简单的,就容易多了,当平台没有定义do_softirq函数时(__ARCH_HAS_DO_SOFTIRQ),

内核提供了一个通用的:

  1. #ifndef __ARCH_HAS_DO_SOFTIRQ
  2. asmlinkage void do_softirq(void)
  3. {
  4.         __u32 pending;
  5.         unsigned long flags;
  6.         if (in_interrupt())
  7.                 return;
  8.         local_irq_save(flags);
  9.         pending = local_softirq_pending();
  10.         if (pending)
  11.                 __do_softirq();
  12.         local_irq_restore(flags);
  13. }
  14. #endif

复制代码 无需更多的解释,它非常的简洁。

不论是哪个版本,都将调用__do_softirq函数:

  1. asmlinkage void __do_softirq(void)
  2. {
  3.         struct softirq_action *h;
  4.         __u32 pending;
  5.         int max_restart = MAX_SOFTIRQ_RESTART;
  6.         int cpu;
  7.         //保存位图
  8.         pending = local_softirq_pending();
  9.         //进程记帐
  10.         account_system_vtime(current);
  11.         //关闭本地CPU下半部。为了保证同一个CPU上的软中断以串行方式执行。
  12.         __local_bh_disable((unsigned long)__builtin_return_address(0));
  13.         lockdep_softirq_enter();
  14.         //获取本地CPU
  15.         cpu = smp_processor_id();
  16. restart:
  17.         //清除位图
  18.         set_softirq_pending(0);
  19.         //锁中断,只是为了保持位图的互斥,位图处理完毕。后面的代码可以直接使用保存的pending,
  20.         //而中断处理程序在激活的时候,也可以放心地使用irq_stat.__softirq_pending。
  21.         //所以,可以开中断了
  22.         local_irq_enable();
  23.         //取得软中断向量
  24.         h = softirq_vec;
  25.         //循环处理所有的软中断
  26.         do {
  27.                 //逐步取位图的每一位,判断该位上是否有软中断被设置。若有,处理之
  28.                 if (pending & 1) {
  29.                         //保存抢占计数器
  30.                         int prev_count = preempt_count();
  31.                         kstat_incr_softirqs_this_cpu(h - softirq_vec);
  32.                         trace_softirq_entry(h, softirq_vec);
  33.                         //调用软中断
  34.                         h->action(h);
  35.                         trace_softirq_exit(h, softirq_vec);
  36.                         //判断软中断是否被抢占,如果是,则输出一段错误信息
  37.                         if (unlikely(prev_count != preempt_count())) {
  38.                                 printk(KERN_ERR "huh, entered softirq %td %s %p"
  39.                                        "with preempt_count %08x,"
  40.                                        " exited with %08x?\n", h - softirq_vec,
  41.                                        softirq_to_name[h - softirq_vec],
  42.                                        h->action, prev_count, preempt_count());
  43.                                 preempt_count() = prev_count;
  44.                         }
  45.                         //??qsctr,这个是啥东东
  46.                         rcu_bh_qsctr_inc(cpu);
  47.                 }
  48.                 //指向下一个软中断槽位
  49.                 h++;
  50.                 //移位,取下一个软中断位
  51.                 pending >>= 1;
  52.         } while (pending);
  53.         //当软中断处理完毕后,因为前面已经开了中断了,所以有可能新的软中断已经又被设置,
  54.         //软中断调度程序会尝试重新软中断,其最大重启次数由max_restart决定。
  55.         //所以,这里必须再次关闭中断,再来一次……
  56.         local_irq_disable();
  57.         //取位图
  58.         pending = local_softirq_pending();
  59.         //有软中断被设置,且没有超过最大重启次数,再来一次先
  60.         if (pending && --max_restart)
  61.                 goto restart;
  62.         //超过最大重启次数,还有软中断待处理,调用wakeup_softirqd。其任处是唤醒软中断守护进程ksoftirqd。
  63.         if (pending)
  64.                 wakeup_softirqd();
  65.         lockdep_softirq_exit();
  66.         account_system_vtime(current);
  67.         //恢复下半部
  68.         _local_bh_enable();
  69. }

复制代码 中断跟踪

如果中断跟踪CONFIG_TRACE_IRQFLAGS被定义,lockdep_softirq_enter/lockdep_softirq_exit用于递增/递减当前进程的软中断上下文计数器softirq_context:

  1. # define lockdep_softirq_enter()        do { current->softirq_context++; } while (0)
  2. # define lockdep_softirq_exit()        do { current->softirq_context--; } while (0)
复制代码 trace_softirq_entry与trace_softirq_exit配合使用,可以用于判断软中断的延迟。

继续阅读