2016-11-02html
中断这个特性相比你们都不会陌生,稍微懂点操做系统知识的人均可以说到一二。可是要真正把中断描述清楚,以及LInux中和windows中的实现方式,这可能仍是有点难度的。今天笔者就想彻头彻尾的把中断给详细分析下。linux
说到中断还不得不从现代操做系统的特性提及,不管是桌面PC操做系统仍是嵌入式都是多任务的操做系统,而很遗憾,处理器每每是单个的,即便在硬件成本逐渐降低,从而硬件配置直线上升的今天,PC机的核心可能已经达到4核心,8核心,而手机移动设备更难以想象的达到16核心,32核心,处理器的数量依然不可能作到每一个任务一个CPU,因此CPU必须做为一种全局的资源让全部任务共享。说到共享,如何共享呢?何时给任务A用,何时给任务B用......这就是进程调度,具体的安排就由调度算法决定了。进程如何去调度?现代操做系统通常都是采用基于时间片的优先级调度算法,把CPU的时间划分为很细粒度的时间片,一个任务每次只能时间这么多的时间,时间到了就必须交出使用权,即换其余的任务使用。这种要看操做系统的定时器机制了。那么时间片到以后,系统作了什么呢?这就要用到咱们的中断了,时间片到了由定时器触发一个软中断,而后进入相应的处理历程。固然这一点不足以代表中断的重要,计算机操做系统天然离不开外部设备:鼠标、键盘、网卡、磁盘等等。就拿网卡来说,我计算机并不知道时候数据包会来到,我能保证的就是数据来了我能正常接收就好了。可是我又不可能一直等着接收数据包,要是这样其余任务就死完了。因此合理的办法是,你数据包来到以后,通知我,而后我再对你处理,怎么通知呢??答:中断!键盘、鼠标亦是如此!算法
好了闲话说了这么多,进入正题吧!编程
如上面所述,中断信号由外部设备发起,准确来讲是由外部设备的控制器发起,由于外部设备自己并不能发起信号。必须网卡设备,的那个网络数据包到达网卡,网卡的控制器就向IO APIC发送中断信号,IO APIC把信号发送给本地APIC,本地APIC把信号传送给CPU,若是根据当时状况,要处理这个中断,就保存当时的运行上下文,切换到中断上下文中,根据IDT查找对应的处理函数进行处理。处理完成后,须要恢复中断以前的状态。windows
大体过程就如上面所述,可是具体这个过程是怎么执行的,关键几点以下:数组
一、设备控制器如何发送中断信号安全
二、APIC如何接受中断信号,以及作了什么处理网络
三、处理器收到中断信号又作了什么操做架构
在此以前,咱们须要介绍下中断控制器8259A和APIC并发
8259A中断控制器由两片8259A芯片级联组成,每一个芯片有8个中断输入引脚,其中IRQ2被用来链接从芯片,因此一共能够支持15个中断号,这也就是早期采用8259A中断控制器只能使用15个外部中断的缘由,使用8259A中断控制器的工做架构以下:
每一个外部设备链接一条中断线,当设备须要中断CPU时,经过这些中断线,发送中断请求。中断控制器感知到这些中断请求,会设置中断控制器中的中断请求寄存器的相应位为1,鉴于多个中断可能并发到达,中断控制器具有中断判优功能,当其选定一个中断做为当前响应中断时,会清除中断请求寄存器中的对应位,而后设置中断服务寄存器的某些位为1,代表CPU正在服务于某个中断请求。
另外8259A还有一个8位的中断屏蔽寄存器,每一位对应于一个中断线,当对应的位被设置后,代表要屏蔽这些中断。为了处理不一样优先级的中断,中断控制器还有同一个优先权判决器,当一个中断到达时,判断到达的中断优先级和ISR中正在服务的中断优先级的大小,若高于正在服务的中断的优先级,须要打断当前中断的处理,转而处理新到达的中断请求,不然,不予理会。
中断触发方式:
中断请求输入端IR0~IR7可采用的中断触发方式有电平触发和边沿触发两种,由初始化命令字ICW1中的LTIM位来设定。
当外部设备请求服务时,设置本身对应寄存器的位为1,即成了高电平,那么中断控制器端就能够接受到中断信号,进入中断的处理。
因为8259A中断控制器只能应用与单处理器,且其中断源的限制,后来Intel开发了高级可编程中断控制器APIC
APIC由两部分:本地APIC和IO APIC。本地APIC和逻辑CPU绑定,它控制传递给逻辑处理器中断信号和产生IPI中断(这是处理器间中断,只用于多处理器状况)、
本地APIC能够接受一下中断源:
以上中断源称为本地中断源,当本地APIC接收到一个中断信号,会经过某个发送协议把信号发送给处理器核心,具体能够经过一组被称之为local vector table 的APIC寄存器设置某个中断源的中断号。
而当接收外部中断时,则须要经过IO APIC,那么local vector table是个什么东西呢?
local vector table
LVT容许用户经过编程指定特定中断的处理动做,每一个中断对应其中的一个表项,具体由一下几个32位寄存器组成:
LVT CMCI Register(FEE0 02F0h)
LVT Timer Register(FEE0 0320h)
LVT Thermal Monitor Register(FEE0 0330h)
LVT Performance Counter Register(FEE0 0340h)
LVT LINT0 Register(FEE0 3350h)
LVT LINT1 Register(FEE0 0360h)
LVT Error Register(FEE0 0370h)
本地APIC和IO APIC 关系以下:
由上图能够看到,IO APIC实际上是做为一个PCI设备挂载在PCI总线上,和传统的PIC相比,IO APIC最大的做用在于中断的分发,外部设备不直接链接在本地APIC,而是链接在IO APIC,由IO APIC处理中断消息后发送给本地APCI。IO APIC通常由24个中断管脚,每一个管脚对应一个RTE,而且其各个管脚没有优先级之分,具体中断的优先级由其对应的向量决定,即前面所说的local vector table。每当IO APIC接收到一个中断消息,就根据其内部的PRT表格式化出一条中断消息,发送给本地APIC。PRT表格式以下:
关于硬件先暂且介绍到这里吧,描述硬件实在感受力不从心,感兴趣的可参考具体的硬件手册。
处理器收到中断信号又作了什么操做
在此以前咱们须要明白几个概念:硬件中断、软件中断、异常
虽然前面描述的不够详细,可是相信仍是能够看出,中断源能够分为两部分:本地中断源和外部中断源。本地中断源有些场合又称为软件中断,由于没有具体的硬件与之对应。而那些由具体硬件触发的中断则称为硬件中断。而异常则是程序指令流执行过程当中的同步过程,好比程序执行过程当中遇到除零错,很显然此时程序没法继续运行,只能处理完了这个异常,才能够继续运行。异常的同步特性和中断的异步又是一个明显的区别。另外在linux中为了让内核延期执行某个任务,也提出了一个软中断(software interrupt)的概念,这点在windows中与之对应的机制为DPC,即延迟过程调用。这两点我们后面在说。
暂且不说中断异常的区别,系统使用一套机制来处理中断和异常,即在内核中维护了一张IDT(Interrupt Descriptor Table)中断描述符表,寄存器IDTR保存有表的基址。每一个表项为8个字节。记录对应中断的处理函数的地址以及其余的一些控制位。因此每一个中断对应一个表项。0-31号中断号位系统为预约义的中断和异常保留的,用户不得使用,因此硬件中断号从32开始分发。
每当CPU接收到一个中断或者异常信号,CPU首先要作的决定是否响应这个中断(具体由中断控制器根据中断优先级决定是否给CPU发送中断信号),若是决定响应,就终止当前运行进程的运行,根据IDTR寄存器获取中断描述符表基地址,而后根据中断号定位具体的中断描述符。这里中断描述符可分为两种状况:
一、 当中断描述符对应的是中断门或者陷阱门时,处理历程运行在当前进程的上下文中,即不须要发生进程上下文的切换,只是若是处理历程和当前进程的运行级别不一样,则须要发生栈的切换,具体以下:
若是当前进程运行在level 3即用户态,则当中断发生时:
若是中断发生时当前进程运行在内核态,则就不须要发生栈的切换,仅仅须要执行上述的后两步。
具体动做参考下图:
二、当中断描述符对应一个任务门时,意味着这次中断的处理由一个单独的程序执行,和当前进程无关。使用新的任务处理中断的优缺点也很明显:
固然缺点也很明显,每次中断都会进行任务的切换,进程上下文的切换所带来的开销要比上面两种方式大的多,而且每次中断都要进行两次进程切换:中断进入和中断返回。形成中断响应延迟过大
因为x86架构下的任务是非重入的,即一个中断处理程序执行期间会关中断,那么此时其余的进程就得不到调度,假如说这个处理程序很繁琐,那么会出现CPU处理时间分配不均的状况,且其余的中断得不到响应,这是不能容许的。因此操做系统在以前的基础上把中断处理历程分红两部分:上半部和下半部。上半部主要处理哪些中断来了必需要处理的事情,这个过程会关闭中断,因此此过程尽量的短,在上半部处理结束,就开启中断。下半部主要处理不那么急迫的事情,这个过程开启中断,这样就增长了中断响应的效率。Linux和windows都采用了这种机制。LInux中使用软中断,而windows总则使用DPC延迟过程调用。
下面咱们主要分析Linux下的softirq机制:
软中断可使内核延期执行某个任务,他们的运做方式和具体的硬件相似,甚至能够说这里就是模拟的硬件中断,因此称之为软件中断也不为过。既然提到软中断,那么天然就设计到几个点:
在3.11.1的内核版本中定义了10个软中断,而且系统不建议用户本身添加软中断,因此对于软中断基本用于已定义好的功用,而若是用户须要,可使用其中的一个类型即TASKLET_SOFTIRQ
具体的软中断类型以下:
1 enum 2 { 3 HI_SOFTIRQ=0, 4 TIMER_SOFTIRQ, 5 NET_TX_SOFTIRQ, 6 NET_RX_SOFTIRQ, 7 BLOCK_SOFTIRQ, 8 BLOCK_IOPOLL_SOFTIRQ, 9 TASKLET_SOFTIRQ, 10 SCHED_SOFTIRQ, 11 HRTIMER_SOFTIRQ, 12 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ 13 14 NR_SOFTIRQS 15 };
每一个CPU维护一个软中断位图__softirq_pending,实际上是一个32位的字段,每一位对应一个软中断。处理软中断时会获取当前CPU的软中断位图,根据各个位的设置,进行处理。
#define local_softirq_pending() __get_cpu_var(irq_stat).__softirq_pending
一、软中断的注册
软中断的核心机制是一张表,相似于IDT,包含32个softirq_vec结构,该结构很简单:就是一个函数地址,每一个软中断对应其中的一个,因此如今也仅仅使用前10项。
1 struct softirq_action 2 { 3 void (*action)(struct softirq_action *); 4 };
系统经过open_softirq函数注册一个软中断,具体就是在softirq_vec数组中根据中断号设置其对应的处理例程。
1 void open_softirq(int nr, void (*action)(struct softirq_action *)) 2 { 3 softirq_vec[nr].action = action; 4 }
nr是上面的一个枚举值,action即是对应软中断的处理函数。
二、软中断的触发
Linux系统经过raise_softirq函数引起一个软中断,每一个CPU有个软中断位图,有32位,最多可对应32个软中断,当置位图对应位为1时,代表触发了对应的软中断。在下次系统检查是否有软中断时就会被检测获得,从而进行处理。
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 inline void raise_softirq_irqoff(unsigned int nr) 2 { 3 __raise_softirq_irqoff(nr); 4 5 /* 6 * If we're in an interrupt or softirq, we're done 7 * (this also catches softirq-disabled code). We will 8 * actually run the softirq once we return from 9 * the irq or softirq. 10 * 11 * Otherwise we wake up ksoftirqd to make sure we 12 * schedule the softirq soon. 13 */ 14 /*若是咱们没有在中断上下文中(硬中断或者软中断),就唤醒软中断守护进程,不然之能等到从中断返回的过程当中*/ 15 if (!in_interrupt()) 16 wakeup_softirqd(); 17 }
1 void __raise_softirq_irqoff(unsigned int nr) 2 { 3 trace_softirq_raise(nr); 4 or_softirq_pending(1UL << nr); 5 }
1 #define or_softirq_pending(x) this_cpu_or(irq_stat.__softirq_pending, (x))
在raise_softirq_irqoff函数中看下,在设置了对应的位以后调用了in_interrupt函数判断是否处于硬中断上下文或者软中断上下文,若是不在就调用wakeup_softirqd唤醒守护进程处理软中断。不然的话等到中断退出的时候处理。
三、软中断的处理
处理时机:
软中断大概在三个地方会被检测是否存在,若是存在会进行处理:
中断上下文:CPU处于处理中断上半部或者下半部,内核用in_interrupt来判断是否处于中断上下文。这是一个宏:
#define in_interrupt() (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))
能够看到这里中断上下文包括硬件中断、软件中断、NMI中断。说到这里,出现了一个preempt_count(),LInux为每一个进程的thread_info结构中维护了一个preempt_count字段,该字段是int型,所以有32位,用于支持内核抢占。当该字段为0的时候,表示当前容许内核抢占,不然不能够。具体请参考另外一篇博文:Linux中的进程调度
处理过程:
软中断的处理核心都在do_softirq函数。
1 asmlinkage void do_softirq(void) 2 { 3 __u32 pending; 4 unsigned long flags; 5 6 if (in_interrupt()) 7 return; 8 /*关闭全部中断 会保存eflags寄存器的内容*/ 9 local_irq_save(flags); 10 11 pending = local_softirq_pending(); 12 13 if (pending) 14 __do_softirq(); 15 /*开启全部中断,恢复eflagS寄存器的内容*/ 16 local_irq_restore(flags); 17 }
首先就会判断当前是否处于中断上下文,若是处于就直接返回,一个软中断既不能打断硬件中断也不能打断软件中断。若是不在中断上下文,就调用local_softirq_pending函数判断是否存在被触发的软中断,若是存在就进入if,调用__do_softirq函数, 不然开启中断,不作处理。
1 asmlinkage void __do_softirq(void) 2 { 3 struct softirq_action *h; 4 __u32 pending; 5 unsigned long end = jiffies + MAX_SOFTIRQ_TIME; 6 int cpu; 7 unsigned long old_flags = current->flags; 8 int max_restart = MAX_SOFTIRQ_RESTART; 9 10 /* 11 * Mask out PF_MEMALLOC s current task context is borrowed for the 12 * softirq. A softirq handled such as network RX might set PF_MEMALLOC 13 * again if the socket is related to swap 14 */ 15 current->flags &= ~PF_MEMALLOC; 16 17 pending = local_softirq_pending(); 18 account_irq_enter_time(current); 19 20 __local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET); 21 lockdep_softirq_enter(); 22 23 cpu = smp_processor_id(); 24 restart: 25 /* Reset the pending bitmask before enabling irqs */ 26 set_softirq_pending(0); 27 28 local_irq_enable(); 29 30 h = softirq_vec; 31 32 do { 33 if (pending & 1) { 34 unsigned int vec_nr = h - softirq_vec; 35 int prev_count = preempt_count(); 36 37 kstat_incr_softirqs_this_cpu(vec_nr); 38 39 trace_softirq_entry(vec_nr); 40 h->action(h); 41 trace_softirq_exit(vec_nr); 42 if (unlikely(prev_count != preempt_count())) { 43 printk(KERN_ERR "huh, entered softirq %u %s %p" 44 "with preempt_count %08x," 45 " exited with %08x?\n", vec_nr, 46 softirq_to_name[vec_nr], h->action, 47 prev_count, preempt_count()); 48 preempt_count() = prev_count; 49 } 50 51 rcu_bh_qs(cpu); 52 } 53 h++; 54 pending >>= 1; 55 } while (pending); 56 57 local_irq_disable(); 58 59 pending = local_softirq_pending(); 60 if (pending) { 61 if (time_before(jiffies, end) && !need_resched() && 62 --max_restart) 63 goto restart; 64 65 wakeup_softirqd(); 66 } 67 68 lockdep_softirq_exit(); 69 70 account_irq_exit_time(current); 71 __local_bh_enable(SOFTIRQ_OFFSET); 72 tsk_restore_flags(current, old_flags, PF_MEMALLOC); 73 }
有了上面的铺垫,这里并不难理解。首先调用local_softirq_pending函数获取当前CPU软中断位图,而后调用__local_bh_disable函数禁止本地软中断,接着调用lockdep_softirq_enter函数标记进入softirq context。下面的restart段就开始处理位图中的软中断了。
进入该节的首要操做对位图清零,由于随时可能有同种类型的软中断被触发,接着就调用local_irq_enable函数开启中断。下面h = softirq_vec;是获取软中断描述符表的起始地址,进入do循环,从pending的第一位开始处理,每次pending右移1位,同时h++,因此h定位具体的软中断类型,pending判断是否被触发。若是被触发,那么进入if内部,内部就是调用了h->action(h)函数处理软中断;
在循环结束后,就再次关中断,而后从新读取pending,若是又有新的软中断被触发&&本次处理软中断未超时&&当前进进程的调度位TIF_NEED_RESCHED没有被设置&&重启次数没到最大限制,就再次执行restart节进行处理。不然只能唤醒守护进程下次再处理软中断。
以后就标记退出softirq context,开启软中断。
每一个CPU都会有一个软中断守护进程ksoftirqd,同时也有一个软中断位图,咱们触发的时候会指定CPU的id,各个CPU处理的软中断就不会影响,即便两个CPU处理同一类型的软中断。这样也避免了不少须要同步的操做,固然两个CPU都在处理同一类型的软中断,那么仍是须要必定的同步来保障临界区的安全。若是在do_softirq的末尾有未处理的软中断,就不得不唤醒守护进程进行处理;一样在raise_softirq_irqoff中在触发指定软中断后,判断是否在中断上下文,若是不在中断上下文就唤醒守护进程,不然下次检查调度的时候处理这些软中断。
基本的处理过程就如上所述,可是仍是存在很多问题,前面代码片断中出现了不少开关中断的操做,为什么须要有这些操做以及这些操做的原理如何?下面咱们分析一下。
开关中断涉及到的函数主要有下面几个:
其中1和3是针对hard irq,而2是针对soft irq。并且以上函数都是成对出现的。
local_irq_save和local_irq_restore是保存和恢复EFLAGS寄存器的状态,首先执行local_irq_save会保存EFLAGS寄存器的状态到一个变量,而后禁止本地中断(可屏蔽的外部中断),local_irq_restore会恢复EFLAGS寄存器到以前保存的状态。
local_irq_disable会直接禁止本地中断(可屏蔽的外部中断),而local_irq_enable会打开本地中断。这些都是针对可屏蔽外部中断,对于NMI和异常没有做用。
local_bh_disable会设置当前进程的抢占计数器,即增长对应的位,这样,当前进程就标识为不可抢占,也就关闭了软件中断。为何说这里关闭了软件中断呢?由于前面咱们设置了抢占计数器,而在每次检查准备调度时候,都会判断当前是否处于中断上下文,若是处于就不发生调度,从而不抢占当前进程。
总结:说实话,内核真是复杂的很,在写本篇博客的时候天然会参考一些书籍以及大牛们的博客,发现本身的确要学的东西太多,别人写书或者写博客,都能很天然的结合其余的模块,旁征博引,而本身虽然已经尽最大可能描述清楚,但仍是以为不够丰满,只能之后慢慢学习了,同时其中可能难免有错误的地方,还请老师们指点!!
参考资料:
一、LInux 3.11.1内核源码
二、http://www.wowotech.net/linux_kenrel/soft-irq.html
三、linux 内核源代码情景分析