浅析Linux的软中断


1、Linux软中断浅析
Linux中的软中断机制用于 系统中对时间要求最严格以及最重要的中断下半部进行使用。在系统设计过 程中,大家都清楚中断上下文不能处理太多的事情,需要快速的返回,否则很容易导致中断事件的丢失,所以这就产生了一个问题:中断发生之后的事务处理由谁来 完成?在前后台程序中,由于只有中断上下文和一个任务上下文,所以中断上下文触发事件,设置标记位,任务上下文循环扫描标记位,执行相应的动作,也就是中 断发生之后的事情由任务来完成了,只不过任务上下文采用扫描的方式,实时性不能得到保 证。在 Linux系统和Windows系统中,这个不断循环的任务就是本文所要讲述的软中断daemon。在Windows中处理耗时的中断事务称之为中 断延迟处理,在Linux中称之为中断下半部,显然中断上半部处理清中断之类十分清闲的动作,然后在退出中断服务程序时触发中断下半部,完成具体的功能。
在Linux中,中断下半部的实现基于软中断机制。所以理清楚软中断机制的原理,那么中断下半部的实现也就非常简单了。通过上述的描述,大家也应该 清楚为什么要定义软中断机制了,一句话就是为了要处理对时间要求苛刻的任务,恰好中断下半部就有这样的需求,所以其实现采用了软中断机制。 
构成软中断机制的核心元素包括:
1、 软中断状态寄存器soft interrupt state(irq_stat)
2、 软中断向量表(softirq_vec)
3、 软中断守护daemon

软中断的工作工程模拟了实际的中断处理过程,当某一软中断时间发生后,首先需要设置对应的中断标记位,触发中断事务,然后唤醒守护线程去检测中断状 态寄存器,如果通过查询发现某一软中断事务发生之后,那么通过软中断向量表调用软中断服务程序action()。这就是软中断的过程,与硬件中断唯一不同 的地方是从中断标记到中断服务程序的映射过程。在CPU的硬件中断发生之后,CPU需要将硬件中断请求通过向量表映射成具体的服务程序,这个过程是硬件自 动完成的,但是软中断不是,其需要守护线程去实现这一过程,这也就是软件模拟的中断,故称之为软中断。
一个软中断不会去抢占另一个软中断,只有硬件中断才可以抢占软中断,所以软中断能够保证对时间的严格要求。
Linux中软中断实现分析
在Linux中最多可以注册32个软中断,目前系统用了6个软中断,他们为:定时器处理、SCSI处理、网络收发处理以及Tasklet机制,这里的tasklet机制就是用来实现下半部的,
描述软中断的核心数据结构为中断向量表,其定义如下:
struct softirq_action
{
void (*action)(struct softirq_action *); /* 软中断服务程序 */
void *data; /* 服务程序输入参数 */
};
软中断守护daemon是软中断机制的实现核心,其实现过程也比较简单,通过查询软中断状态irq_stat来判断事件是否发生,如果发生,那么映 射到软中断向量表,调用执行注册的action函数就可以了。从这一点分析可以看出,软中断的服务程序的执行上下文为软中断daemon。在Linux中 软中断daemon线程函数为do_softirq()。
触发软中断事务通过raise_softirq()来实现,该函数就是在中断关闭的情况下设置软中断状态位,然后判断如果不在中断上下文,那么直接唤醒守护daemon。
常用的软中断函数列表如下:
1、 Open_softirq,注册一个软中断,将软中断服务程序注册到软中断向量表。2、 Raise_softirq,设置软中断状态bitmap,触发软中断事务。
Tasklet机制实现分析
Tasklet为一个软中断,考虑到优先级问题,分别占用了向量表中的0号和5号软中断。

当tasklet的软中断事件发生之后,执行tasklet-action的软中断服务程序,该服务程序会扫描一个tasklet的任务列表,执行该任务中的具体服务程序。在这里举一个例子加以说明:
当用户读写USB设备之后,发生了硬件中断,硬件中断服务程序会构建一个tasklet_struct,在该结构中指明了完成该中断任务的具体方法 函数(下半部执行函数),然后将tasklet_struct挂入tasklet的tasklet_struct链表中,这一步可以通过 tasklet_schedule函数完成。最后硬件中断服务程序退出并且CPU开始调度软中断daemon,软中断daemon会发现tasklet发 生了事件,其会执行tasklet-action,然后tasklet-action会扫描tasklet_struct链表,执行具体的USB中断服务 程序下半部。这就是应用tasklet完成中断下半部实现的整个过程。

Linux中的tasklet实现比较简单,其又封装了一个重要数据结构tasklet_struct,使用tasklet主要函数列表如下:
1、 tasklet_init,初始化一个tasklet_struct,当然可以采用静态初始化的方法,宏为:DECLARE_TASKLET。
2、 tasklet_schedule,调度一个tasklet,将输入的tasklet_struct添加到tasklet的链表中。
Linux中的软中断机制就是模拟了硬件中断的过程,其设计思想完全可以在其他嵌入式OS中得以应用。


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, /* Preferable RCU should always be the last 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. /*
  2. * This function must run with irqs disabled!
  3. */
  4. inline void raise_softirq_irqoff(unsigned int nr)
  5. {
  6. //置位图,即标记为可执行状态
  7. __raise_softirq_irqoff(nr);

  8. /*
  9. * If we're in an interrupt or softirq, we're done
  10. * (this also catches softirq-disabled code). We will
  11. * actually run the softirq once we return from
  12. * the irq or softirq.
  13. *
  14. * Otherwise we wake up ksoftirqd to make sure we
  15. * schedule the softirq soon.
  16. */
  17. //设置了位图后,可以判断是否已经没有在中断上下文中了,如果没有,则是一个立即调用软中断的好时机。
  18. //in_interrupt另一个作用是判断软中断是否被禁用。
  19. //wakeup_softirqd唤醒软中断的守护进程ksoftirq。
  20. if (!in_interrupt())
  21. wakeup_softirqd();
  22. }

现在可以来看"激活"软中断的所有含义了,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的之间的通信不熟,不太清楚这个机制……

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. /* build the stack frame on the softirq stack */
  28. //构造中断栈帧
  29. isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));

  30. //call_on_stack切换内核栈,并在中断上下文上执行函数__do_softirq
  31. call_on_stack(__do_softirq, isp);
  32. /*
  33. * Shouldnt happen, we returned above if in_interrupt():
  34. */
  35. WARN_ON_ONCE(softirq_count());
  36. }

  37. //恢复之
  38. local_irq_restore(flags);
  39. }

当配置了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. /*
  3. * per-CPU IRQ handling contexts (thread information and stack)
  4. */
  5. union irq_ctx {
  6. struct thread_info tinfo;
  7. u32 stack[THREAD_SIZE/sizeof(u32)];
  8. } __attribute__((aligned(PAGE_SIZE)));

  9. static DEFINE_PER_CPU(union irq_ctx *, hardirq_ctx);
  10. static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
  11. ……

  12. static void call_on_stack(void *func, void *stack)
  13. ……

是的,这个版本相对复杂,但是如果看了复杂的,再来看简单的,就容易多了,当平台没有定义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. /* Reset the pending bitmask before enabling irqs */
  18. //清除位图
  19. set_softirq_pending(0);

  20. //锁中断,只是为了保持位图的互斥,位图处理完毕。后面的代码可以直接使用保存的pending,
  21. //而中断处理程序在激活的时候,也可以放心地使用irq_stat.__softirq_pending。
  22. //所以,可以开中断了
  23. local_irq_enable();

  24. //取得软中断向量
  25. h = softirq_vec;

  26. //循环处理所有的软中断
  27. do {
  28. //逐步取位图的每一位,判断该位上是否有软中断被设置。若有,处理之
  29. if (pending & 1) {
  30. //保存抢占计数器
  31. int prev_count = preempt_count();
  32. kstat_incr_softirqs_this_cpu(h - softirq_vec);

  33. trace_softirq_entry(h, softirq_vec);
  34. //调用软中断
  35. h->action(h);
  36. trace_softirq_exit(h, softirq_vec);
  37. //判断软中断是否被抢占,如果是,则输出一段错误信息
  38. if (unlikely(prev_count != preempt_count())) {
  39. printk(KERN_ERR "huh, entered softirq %td %s %p"
  40. "with preempt_count %08x,"
  41. " exited with %08x?\n", h - softirq_vec,
  42. softirq_to_name[h - softirq_vec],
  43. h->action, prev_count, preempt_count());
  44. preempt_count() = prev_count;
  45. }
  46. //??qsctr,这个是啥东东
  47. rcu_bh_qsctr_inc(cpu);
  48. }
  49. //指向下一个软中断槽位
  50. h++;
  51. //移位,取下一个软中断位
  52. pending >>= 1;
  53. } while (pending);

  54. //当软中断处理完毕后,因为前面已经开了中断了,所以有可能新的软中断已经又被设置,
  55. //软中断调度程序会尝试重新软中断,其最大重启次数由max_restart决定。
  56. //所以,这里必须再次关闭中断,再来一次……
  57. local_irq_disable();

  58. //取位图
  59. pending = local_softirq_pending();
  60. //有软中断被设置,且没有超过最大重启次数,再来一次先
  61. if (pending && --max_restart)
  62. goto restart;

  63. //超过最大重启次数,还有软中断待处理,调用wakeup_softirqd。其任处是唤醒软中断守护进程ksoftirqd。
  64. if (pending)
  65. wakeup_softirqd();

  66. lockdep_softirq_exit();

  67. account_system_vtime(current);
  68. //恢复下半部
  69. _local_bh_enable();
  70. }

中断跟踪
如果中断跟踪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配合使用,可以用于判断软中断的延迟。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值