Linux的中断宏观分为两种:软中断和硬中断。声明一下,这里的软和硬的意思是指和软件相关以及和硬件相关,而不是软件实现的中断或硬件实现的中断。
软中断就是"信号机制"。软中不是软件中断。Linux通过信号来产生对进程的各种中断操作, 在Linux中最多可以注册32个软中断,目前系统用了6个软中断,他们为:定时器处理、SCSI处理、网络收发处理以及Tasklet机制,一般来说,软中断是由内核机制的触发事件引起的(例如进程运行超时),但是不可忽视有大量的软中断也是由于和硬件有关的中断引起的,软中断的工作工程模拟了实际的中断处理过程,当某一软中断时间发生后,首先需要设置对应的中断标记位,触发中断事务,然后唤醒守护线程去检测中断状态寄存器,如果通过查询发现某一软中断事务发生之后,那么通过软中断向量表调用软中断服务程序action()。这就是软中断的过程,与硬件中断唯一不同的地方是从中断标记到中断服务程序的映射过程。在CPU的硬件中断发生之后,CPU需要将硬件中断请求通过向量表映射成具体的服务程序,这个过程是硬件自动完成的,但是软中断不是,其需要守护线程去实现这一过程。例如当打印机端口产生一个硬件中断时,会通知和硬件相关的硬中断,硬中断就会产生一个软中断并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠在打印机任务队列中的处理进程。
下列27种软中断信号的注释部分即为它的功能说明(另外5种忽略,一种32种)
#define SIGHUP 1 /* hangup */
#define SIGINT 2 /* interrupt */
#define SIGQUIT 3 /* quit */
#define SIGILL 4 /* illegal instruction (not reset when caught ) */
#define SIGTRAP 5 /* trace trap (not reset when caught ) */
#define SIGIOT 6 /* IOT instruction */
#define SIGEMT 7 /* EMT instr uction */
#define SIGFPE 8 /* floating point exception */
#define SIGKILL 9 /* kill (cannot be caught or ignored) */
#define SIGBUS 10 /* bus error */
#define SIGSEGV 11 /* segmentation violation */
#define SIGSYS 12 /* bad argument to system call */
#define SIGPIPE 13 /* write on a pipe with no one to read it */
#define SIGALAM 14 /* alarm clock */
#define SIGTERM 15 /* software termination signal from kill */
#define SIGSTOP 17 /* sendoble stop signal not from tty */
#define SIGTSTP 18 /* stop signal from tty */
#define SIGCONT 19 /* continue a stopped process */
#define SIGCHLD 20 /* to parent on child stop or exit */
#define SIGTTIN 21 /* to readers pgrp upon background tty read */
#define SIGTTOOU 22 /* like TTIN for output it (tp→t-local &LTOSTOP) */
#define SIGTINT 23 /* to pgrp on every input character if LINTRUP */
#define SIGXCPU 24 /* exceeded CPU time limit */
#define SIGXFSZ 25 /* exceeded file size limit */
#define SIGSTAT 26 /* status update requsted */
#define SIGMOUS 27 /* mouse interrupt */
#define NSIG 32
硬中断就是通常意义上的"中断处理程序",它是直接处理由硬件发过来的中断信号的。当硬中断收到它应当处理的中断信号以后,就回去自己驱动的设备上去看看设备的状态寄存器以了解发生了什么事情,并进行相应的操作。对于软中断,我们不做讨论,那是进程调度里要考虑的事情。由于我们讨论的是设备驱动程序的中断问题,所以焦点集中在硬中断里。我们这里讨论的是硬中断,即和硬件相关的中断。
构成软中断机制的核心元素包括:
1、 软中断状态寄存器soft interrupt state(irq_stat)
2、 软中断向量表(softirq_vec)
3、 软中断守护daemon
2.4版本的内核定义了四个软中断:HI_SOFTIRQ、NET_TX_SOFTIRQ、 NET_RX_SOFTIRQ、TASKLET_SOFTIRQ;2.6版本的内核中定义了六个软中断:HI_SOFTIRQ、 TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、 TASKLET_SOFTIRQ。
向内核注册一个软中断,其实质是设置软中断向量表相应槽位,注册其处理函数:
- void open_softirq(int nr, void (*action)(struct softirq_action *))
- {
- softirq_vec[nr].action = action;
- }
softirq_vec是整个软中断的向量表:
- struct softirq_action
- {
- void (*action)(struct softirq_action *);
- };
-
- static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
NR_SOFTIRQS是最大软中断向量数,内核支持的所有软中断如下:
- enum
- {
- HI_SOFTIRQ=0,
- TIMER_SOFTIRQ,
- NET_TX_SOFTIRQ,
- NET_RX_SOFTIRQ,
- BLOCK_SOFTIRQ,
- TASKLET_SOFTIRQ,
- SCHED_SOFTIRQ,
- HRTIMER_SOFTIRQ,
- RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
-
- NR_SOFTIRQS
- };
当需要调用软中断时,需要调用raise_softirq函数激活软中断,这里使用术语“激活”而非“调用”,
是因为在很多情况下不能直接调用软中断。所以只能快速地将其标志为“可执行”,等待未来某一时刻调用。
为什么“在很多情况下不能直接调用软中断”?试想一下下半部引入的理念,就是为了让上半部更快地执行。
如果在中断程序代码中直接调用软中断函数,那么就失去了上半部与下半部的区别,也就是失去了其存在的意义。
内核使用一个名为__softirq_pending的位图来描述软中断,每一个位对应一个软中断,位图包含在结构irq_stat中:
- typedef struct {
- unsigned int __softirq_pending;
- ……
- } ____cacheline_aligned irq_cpustat_t;
-
- DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);
宏or_softirq_pending用于设置相应的位(位或操作):
- #define or_softirq_pending(x) percpu_or(irq_stat.__softirq_pending, (x))
local_softirq_pending用于取得整个位图(而非某一位):
- #define local_softirq_pending() percpu_read(irq_stat.__softirq_pending)
宏__raise_softirq_irqoff是or_softirq_pending的包裹:
- #define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
raise_softirq_irqoff通过调用__raise_softirq_irqoff实现激活软中断,它的参数nr即位软中断对应的位图槽位:
- /*
- * This function must run with irqs disabled!
- */
- inline void raise_softirq_irqoff(unsigned int nr)
- {
- //置位图,即标记为可执行状态
- __raise_softirq_irqoff(nr);
-
- /*
- * If we're in an interrupt or softirq, we're done
- * (this also catches softirq-disabled code). We will
- * actually run the softirq once we return from
- * the irq or softirq.
- *
- * Otherwise we wake up ksoftirqd to make sure we
- * schedule the softirq soon.
- */
- //设置了位图后,可以判断是否已经没有在中断上下文中了,如果没有,则是一个立即调用软中断的好时机。
- //in_interrupt另一个作用是判断软中断是否被禁用。
- //wakeup_softirqd唤醒软中断的守护进程ksoftirq。
- if (!in_interrupt())
- wakeup_softirqd();
- }
现在可以来看"激活"软中断的所有含义了,raise_softirq函数完成这一操作:
- void raise_softirq(unsigned int nr)
- {
- unsigned long flags;
-
- //所有操作,应该关闭中断,避免嵌套调用
- local_irq_save(flags);
- raise_softirq_irqoff(nr);
- local_irq_restore(flags);
- }
可见,激活的操作,主要是两点:
<1>、最重要的,就是置相应的位图,等待将来被处理;
<2>、如果此时已经没有在中断上下文中,则立即调用(其实是内核线程的唤醒操作),现在就是将来;
是的,除了raise_softirq在,可能会(嗯,重要的是“可能”)通过wakeup_softirqd唤醒ksoftirqd外,还得明白软中断的其它调用时机。
A、当do_IRQ完成了I/O中断时调用irq_exit:
- #ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
- # define invoke_softirq() __do_softirq()
- #else
- # define invoke_softirq() do_softirq()
- #endif
-
- void irq_exit(void)
- {
- account_system_vtime(current);
- trace_hardirq_exit();
- sub_preempt_count(IRQ_EXIT_OFFSET);
- if (!in_interrupt() && local_softirq_pending())
- invoke_softirq(); //调用软中断
B、如果系统使用I/O APIC,在处理完本地时钟中断时:
- void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)
- {
- ……
- irq_exit();
- ……
- }
C、local_bh_enable
local_bh_enable就是打开下半部,当然重中之中就是软中断了:
- void local_bh_enable(void)
- {
- _local_bh_enable_ip((unsigned long)__builtin_return_address(0));
- }
-
- static inline void _local_bh_enable_ip(unsigned long ip)
- {
- ……
-
- if (unlikely(!in_interrupt() && local_softirq_pending()))
- do_softirq();
-
- ……
- }
不论是哪种调用方式,最终都会触发到软中断的核心处理函数do_softirq,它处理当前CPU上的所有软中断。
内核将软中断设计尽量与平台无关,但是在某些情况下,它们还是会有差异,先来看一个x86 32位的do_softirq版本:
- asmlinkage void do_softirq(void)
- {
- unsigned long flags;
- struct thread_info *curctx;
- union irq_ctx *irqctx;
- u32 *isp;
-
- //软中断不能在中断上下文内嵌套调用。中断处理程序或下半部采用的是"激活"方式。
- if (in_interrupt())
- return;
-
- //禁止中断,保存中断标志
- local_irq_save(flags);
- //内核使用一个CPU位图,确实几个软中断可以同时在不同的CPU上运行,包括相同的软中断。例如,
- //NET_RX_SOFTIRQ可以同时跑在多个处理器上。
- //local_softirq_pending用于确定当前CPU的所有位图是否被设置。即是否有软中断等待处理。
- //回想一下经常发生的网卡接收数据处理:当网卡中断落在哪一个CPU上时,与之相应的软中断函数就会在其上执行。
- //从这里来看,实质就是哪个网卡中断落在相应的CPU上,CPU置其软中断位图,这里做相应的检测(这里local_softirq_pending只
- //是一个总的判断,后面还有按位的判断),检测到有相应的位,执行之
- if (local_softirq_pending()) {
- //取得线程描述符
- curctx = current_thread_info();
- //构造中断上下文结构,softirq_ctx是每个CPU的软中断上下文
- //static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
- //这里先取得当前CPU的软中断上下文,然后为其赋初始值——保存当前进程和栈指针
- irqctx = __get_cpu_var(softirq_ctx);
- irqctx->tinfo.task = curctx->task;
- irqctx->tinfo.previous_esp = current_stack_pointer;
-
- /* build the stack frame on the softirq stack */
- //构造中断栈帧
- isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
-
- //call_on_stack切换内核栈,并在中断上下文上执行函数__do_softirq
- call_on_stack(__do_softirq, isp);
- /*
- * Shouldnt happen, we returned above if in_interrupt():
- */
- WARN_ON_ONCE(softirq_count());
- }
-
- //恢复之
- local_irq_restore(flags);
- }
当配置了CONFIG_4KSTACKS,每个进程的thread_union只有4K,而非8K。发生中断时,内核栈将不使用进程的内核栈,而使用每个 cpu的中断请求栈。
内核栈将使用每个 cpu的中断请求栈,而非进程的内核栈来执行软中断函数:
- static void call_on_stack(void *func, void *stack)
- {
- asm volatile("xchgl %%ebx,%%esp \n" //交换栈指针,中断栈帧的指针stack做为传入参数(%ebx),交换后esp是irq_ctx的栈顶,ebx是进程内核栈的栈
- "call *%%edi \n" //调用软中断函数
- "movl %%ebx,%%esp \n" //恢复之,直接使用movl,而非xchgl是因为函数执行完毕,中断的栈帧指针已经没有用处了
- : "=b" (stack)
- : "0" (stack),
- "D"(func)
- : "memory", "cc", "edx", "ecx", "eax");
- }
所有的这些执行,应该都是在定义4K栈的基础上的:
- #ifdef CONFIG_4KSTACKS
- /*
- * per-CPU IRQ handling contexts (thread information and stack)
- */
- union irq_ctx {
- struct thread_info tinfo;
- u32 stack[THREAD_SIZE/sizeof(u32)];
- } __attribute__((aligned(PAGE_SIZE)));
-
- static DEFINE_PER_CPU(union irq_ctx *, hardirq_ctx);
- static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
- ……
-
- static void call_on_stack(void *func, void *stack)
- ……
是的,这个版本相对复杂,但是如果看了复杂的,再来看简单的,就容易多了,当平台没有定义do_softirq函数时(__ARCH_HAS_DO_SOFTIRQ),
内核提供了一个通用的:
- #ifndef __ARCH_HAS_DO_SOFTIRQ
-
- asmlinkage void do_softirq(void)
- {
- __u32 pending;
- unsigned long flags;
-
- if (in_interrupt())
- return;
-
- local_irq_save(flags);
-
- pending = local_softirq_pending();
-
- if (pending)
- __do_softirq();
-
- local_irq_restore(flags);
- }
-
- #endif
不论是哪个版本,都将调用__do_softirq函数:
- asmlinkage void __do_softirq(void)
- {
- struct softirq_action *h;
- __u32 pending;
- int max_restart = MAX_SOFTIRQ_RESTART;
- int cpu;
-
- //保存位图
- pending = local_softirq_pending();
- //进程记帐
- account_system_vtime(current);
-
- //关闭本地CPU下半部。为了保证同一个CPU上的软中断以串行方式执行。
- __local_bh_disable((unsigned long)__builtin_return_address(0));
- lockdep_softirq_enter();
-
- //获取本地CPU
- cpu = smp_processor_id();
- restart:
- /* Reset the pending bitmask before enabling irqs */
- //清除位图
- set_softirq_pending(0);
-
- //锁中断,只是为了保持位图的互斥,位图处理完毕。后面的代码可以直接使用保存的pending,
- //而中断处理程序在激活的时候,也可以放心地使用irq_stat.__softirq_pending。
- //所以,可以开中断了
- local_irq_enable();
-
- //取得软中断向量
- h = softirq_vec;
-
- //循环处理所有的软中断
- do {
- //逐步取位图的每一位,判断该位上是否有软中断被设置。若有,处理之
- if (pending & 1) {
- //保存抢占计数器
- int prev_count = preempt_count();
- kstat_incr_softirqs_this_cpu(h - softirq_vec);
-
- trace_softirq_entry(h, softirq_vec);
- //调用软中断
- h->action(h);
- trace_softirq_exit(h, softirq_vec);
- //判断软中断是否被抢占,如果是,则输出一段错误信息
- if (unlikely(prev_count != preempt_count())) {
- printk(KERN_ERR "huh, entered softirq %td %s %p"
- "with preempt_count %08x,"
- " exited with %08x?\n", h - softirq_vec,
- softirq_to_name[h - softirq_vec],
- h->action, prev_count, preempt_count());
- preempt_count() = prev_count;
- }
- //??qsctr,这个是啥东东
- rcu_bh_qsctr_inc(cpu);
- }
- //指向下一个软中断槽位
- h++;
- //移位,取下一个软中断位
- pending >>= 1;
- } while (pending);
-
- //当软中断处理完毕后,因为前面已经开了中断了,所以有可能新的软中断已经又被设置,
- //软中断调度程序会尝试重新软中断,其最大重启次数由max_restart决定。
- //所以,这里必须再次关闭中断,再来一次……
- local_irq_disable();
-
- //取位图
- pending = local_softirq_pending();
- //有软中断被设置,且没有超过最大重启次数,再来一次先
- if (pending && --max_restart)
- goto restart;
-
- //超过最大重启次数,还有软中断待处理,调用wakeup_softirqd。其任处是唤醒软中断守护进程ksoftirqd。
- if (pending)
- wakeup_softirqd();
-
- lockdep_softirq_exit();
-
- account_system_vtime(current);
- //恢复下半部
- _local_bh_enable();
- }