关于Linux中禁用中断和锁定的关系问题
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Linux中禁用中断和锁定的关系问题相关的知识,希望对你有一定的参考价值。
据说在linux下,某些操作(如原子操作的内部实现)通过禁用CPU硬中断实现锁定。不知道为什么禁用中断就能实现锁定?
其实我不明白的是,什么叫CPU硬中断。我知道CPU都带有中断控制器,是说把这个中断控制器的功能都禁止了么?这样外设什么的确实不能来打扰CPU了,可是这又如何?不同线程还是可以同时访问临界区的代码嘛。难道说禁用了CPU硬中断后,CPU不能被新线程抢占了?不会吧... 我想。
总之还请明白人多多帮助。囧
一个硬中断的完整处理过程(2.4.24版本) ★ CPU做的工作:
CPU收到中断/异常信号;
CPU判断当前CPL级别如果等于3,则导致堆栈切换3->0,堆栈切换过程:
a. CPU从当前TR指向的TSS中读取SS0和ESP0;
b. CPU将当前的【SS:ESP】寄存器内容临时保存起来,假设为SSt和ESPt;
c. CPU将SS0和ESP0恢复到【SS:ESP】寄存器中;
d. CPU将在b中临时保存的SSt和ESPt压入当前的堆栈【SS:ESP】中(其实就是SS0和ESP0);
CPU判断当前CPL级别如果等于0,则不会有2中的步骤;
CPU将EFLAGS、CS、EIP依次压入当前的堆栈【SS:ESP】中;
如果当前是异常,则CPU将异常码error code压入当前的堆栈【SS:ESP】中,否则会省略该步骤;
对于中断,CPU清零当前EFLAGS->IF位,即关闭CPU中断使能,对于异常,CPU则不会清零该位;
执行对中断/异常处理程序的调用;
注:对中断/异常处理程序的要求,为了正常的从中断/异常处理程序中返回,中断/异常处理程序必须使用IRET指令返回,该指令会依次出栈EIP、CS和EFLAGS,比RET多一个EFLAGS,当EFLAGS恢复后,由于原来保存时IF位为1,因此这里相当于CPU中断使能又打开了;
★ Linux内核做的工作:
1. 中断向量表-->common_interrupt:
common_interrupt:
SAVE_ALL
pushl $ret_from_intr
SYMBOL_NAME_STR(call_do_IRQ):
jmp SYMBOL_NAME_STR(do_IRQ);
SAVE_ALL保存所有CPU没有保存的寄存器,由于do_IRQ是函数,这里直接调用jmp,(一般用call来调用函数,call会导致pushEIP,但jmp不会)这样当do_IRQ返回调用ret(ret相当于popEIP)时,会弹出栈中最后一个元素到EIP,很显然这里就是ret_from_intr,也就是说,从do_IRQ中返回后,会跳转到ret_from_intr处继续执行;
2. 来到do_IRQ:
a. 首先给硬中断计数加1,irq_enter(cpu, irq)也就是:++local_irq_count(cpu);每进入一个硬中断处理函数前,local_irq_count(cpu)计数便被加1,处理完毕后减1;
b. 如果当前设备中断处理函数可以在中断打开的情况下运行,则调用sti将EFLAGS.IF置位,打开硬中断使能;
c. 调用request_irq注册的设备硬中断处理函数;
d. 无论EFLAGS.IF是否为0,都调用cli将EFLAGS.IF清零,将硬中断使能关闭;
e. 给硬中断计数减1,irq_enter(cpu, irq);该函数其实就是:--local_irq_count(cpu);
f. 如果此时有软中断需要运行(如在前面的硬中断处理函数中调用__cpu_raise_softirq),则进入do_softirq中处理软中断,关于do_softirq中的处理步骤见3;
e. do_IRQ执行ret,返回到ret_from_intr。
3. do_softirq:
a. 首先判断当前是否还有没有处理完毕的硬中断处理程序或软中断处理程序,如果有,则直接退出该函数。判断方法见附注【文(6)】。
b.将软中断处理计数加1,local_bh_disable()也就是local_bh_count(cpu)++;每进入do_softirq准备进行处理软中断前,local_bh_count(cpu)计数便被加1,软中断处理函数处理完毕后,在do_softirq返回前,将其值减1;
c. 无论EFLAGS.IF是否为0,都调用cli将EFLAGS.IF清零,将硬中断使能关闭,处理些软中断标志位的问题,随后调用sti将EFLAGS.IF置位,打开硬中断使能;
d. 执行软中断处理函数;
e. 调用cli将EFLAGS.IF清零,将硬中断使能关闭,处理些软中断标志位的问题;
f. 将软中断处理计数减1,local_bh_enable()也就是local_bh_count(cpu)--;
g. 返回到2.e中;
4. ret_from_intr:
ENTRY(ret_from_intr)
GET_CURRENT(%ebx)
movl EFLAGS(%esp),%eax
movb CS(%esp),%al
testl $(VM_MASK | 3),%eax
jne ret_with_reschedule
jmp restore_all
在这段代码中,通过"testl $(VM_MASK |3),%eax"检测中断前夕寄存器EFLAGS的高6位和代码段寄存器CS的内容,来判断中断前夕CPU是否运行于VM86模式、用户空间还是系统空间,对VM86这里不讲了,但是我们知道CS最低两位代表着中断发生时CPU的运行级别CPL,我们知道Linux只采用两种运行级别,系统为0,用户为3,所以,如果CS的最低两位为非0,那就说明中断发生于用户空间。如果中断发生于系统空间,控制就直接转移到restore_all,而如果发生于用户空间,则转移到ret_with_reschedule。在restore_all中恢复1中保存的寄存器,随后调用iret恢复EIP、CS、EFLAGS返回到中断发生时的状态。
5. ret_with_reschedule:
a. 如果发现当前进程的need_resched==1,则会调用schedule;
b. 如果发现还有待需要处理的软中断,则会调用do_softirq;
说明:能够走到ret_with_reschedule处,从4中可知,该中断前一定位于用户层,而且不可能有可中断的硬中断或软中断没有执行,也就是说到达这里in_interrupt必然为0。为什么这里要说不可能有可中断的硬中断或软中断没有执行呢?可中断的硬中断或软中断必然是被中断或者异常所打断的,当后者处理完毕后,从4中可知,由于后者发生前位于内核态(也就是可中断的硬中断或软中断处理过程中的那个点),故控制就直接转移到restore_all,即返回到了可中断的硬中断或软中断被打断时的那个点,继续处理完毕,可见,到达这里,必然不存在可中断的硬中断或软中断未被执行。
附注:关于in_interrupt宏,也就是(local_irq_count(__cpu) +local_bh_count(__cpu) !=0)。什么情况下会导致进入do_softriq时,in_interrupt不为0呢?第一种,如果当前正在处理可中断的硬中断处理函数,假设此时来了另一个通道的硬中断,将导致当前硬中断处理函数被中断,进入do_IRQ,随后处理新来的硬中断处理函数,当处理完毕后,到达do_softirq,由2中可知,此时local_irq_count(__cpu)被原先的硬中断加1,由于其还没有处理完毕,故--local_irq_count(cpu)还没来得及执行,因此local_irq_count(__cpu)>0,也就是in_interrupt!=0;第二种,如果当前正在do_softirq中处理软中断处理函数,现在来了个硬中断,将导致当前硬中断处理函数被中断,进入do_IRQ,随后处理新来的硬中断处理函数,当处理完毕后,又来带到了do_softirq,由3中可知,此时local_bh_count(cpu)被前一个do_softirq加1了,但是由于其是中途被中断的,故local_bh_count(cpu)--还没来得及执行,因此local_bh_count(__cpu)>0,也就是in_interrupt!=0;第三种就是综合第一种和第二种两种情况。 参考技术A 首先原子操作是通过local_irq_enable()这个宏来实现,这个宏实现了硬件中断和软件中断的屏蔽。这样CPU就不能被软件中断,如线程抢占。而当硬件中断产生时,会将其结果保存到中断控制器的SRCPND(resource-pend)寄存器中,然后将SRCPND的内容送去做中断屏蔽检测,这一工作由设置MASK寄存器相应的位完成,如果全部屏蔽,硬件中断就不会产生了。当然了,每种处理器都有自己的中断处理方法,通常的流程是这样。本回答被提问者采纳 参考技术B 禁用了中断后就不会发生抢占了,抢占是通过中断来实现的。
Linux设备树(四 中断)
四 中断
中断一般包括中断产生设备和中断处理设备。中断控制器负责处理中断,每一个中断都有对应的中断号及触发条件。中断产生设备可能有多个中断源,有时多个中断源对应中断控制器中的一个中断,这种情况中断产生设备的中断源称之为中断控制器中对应中断的子中断。一般情况中断产生设备数量要多于中断控制器,多个中断产生设备的中断都由一个中断控制器处理,这种多对一的关系也很像一个树形结构,所以在设备树中,中断也被描述成树,叫中断树。以下表述的时候为了明确是在说中断树,在父节点和子节点前边我们都加上“中断”二字,是为了防止和设备树的父节点、子节点混淆(虽然大部分情况设备树的父子关系就是中断树的父子关系,但是因为存在特例,所以我们还是强调是中断父子关系)。
中断产生设备用interrupts属性描述中断源(interrupt specifier),因为不同的硬件描述中断源需要的数据量不同,所以interrupts属性的类型也是<prop-encoded-array>。为了明确表示一个中断由几个u32表示,又引入了#interrupt-cells属性,#interrupt-cells属性的类型是u32,假如一个中断源需要2个u32表示(一个表示中断号,另一个表示中断类型),那么#interrupt-cells就设置成2。有些情况下,设备树的父节点不是中断的父节点(主要是中断控制器一般不是父节点),为此引入了interrupt-parent属性,该属性的类型是<phandle>,用来引用中断父节点(我们前边说过,一般用父节点的标签,这个地方说中断父节点而不是中断控制器是有原因的)。如果设备树的父节点就是中断父节点,那么可以不用设置interrupt-parent属性。interrupts属性和interrupt-parent属性都是中断产生设备节点的属性,但是#interrupt-cells属性不是,#interrupt-cells属性是中断控制器节点以及interrupt nexus节点的属性,这两类节点都可能是中断父节点。
中断控制器节点用interrupt-controller属性表示自己是中断控制器,这个属性的类型是空,不用设置值,只要存在这个节点就表示该节点是中断控制器。除了这个属性外,中断控制器节点还有#interrupt-cells属性,用来表示该中断控制器直接管理下的interrupt domain(后边我们会讲中断控制器的中断子节点interrupt nexus节点有单独的interrupt domain)用几个u32表示一个中断源(interrupt specifier)。中断控制器节点就包括interrupt-controller和#interrupt-cells两个关于中断的属性。中断控制器的#address-cells属性和中断映射有关系,但是该属性不是为中断设计的,中断映射只是用到了这个属性而已。
前边说中断控制器中的一个中断可能对应中断产生设备中的多个中断源,那这种对应关系用什么描述呢?我们还说过#interrupt-celll属性不仅是中断控制器节点的属性,还是interrupt nexus节点的属性,这个interrupt nexus节点就是描述中断映射关系的,该节点通过interrupt-map,interrupt-map-mask属性描述中断映射关系。interrupt-map属性是<prop-encoded-array>类型的,每个元素表示一个中断映射关系(注意是一个"中断映射关系",不是"一个中断"映射关系),从前向后包括:中断子设备地址,中断子设备中断源(interrupt specifier),中断父设备,中断父设备地址,中断父设备中断源(interrupt specifier)五部分。中断子设备地址具体由几个u32组成是由中断子设备所在总线(不是中断父设备)的#address-cells属性决定的,这个地方为什么用中断设备地址而不用中断设备的phandle,是有原因的,因为中断设备会用interrupt-parrent属性指向中断父节点,所以中断子设备是可以确定的,不需要说明。还因为中断子设备地址可以做与运算,通过interrupt-map-mask属性就可以实现多对一的映射。中断子设备中断源(interrupt specifier)由几个u32组成是由该interrupt nexus节点下的#interrupt-cell决定的。中断父设备是一个指向中断父设备的<phandle>属性,一般情况下是中断控制器,但是按照中断树的逻辑,也可能是更高一级的interrupt nexus节点。中断父设备地址具体由几个u32组成是由中断父设备节点下的#address-cells属性决定的(注意,不是中断父设备所在总线的#address-cells属性)。中断父设备中断源(interrupt specifier)由几个u32组成是由中断父设备的#interrupt-cells属性决定的。
还记得前边说过中断设备的中断源和中断控制器的中断源可能是多对一的关系,如果每个子中断都用interrupt-map中的一行表示,那么interrupt-map属性将非常大。为了让多个子中断共享映射关系,引入了interrupt-map-mask属性,该属性的类型也是<prop-enacoded-array>,包含中断子设备地址和中断子设备中断源的bit mask,给定一个子中断源,那么首先和interrupt-map-mask做与运算,运算结果再通过interrupt-map属性查找对应的中断父设备中断源。这就是我们前边为什么说interrupt-map属性的一行是一个“中断映射关系”,而不是“一个中断”映射关系的原因。
我们再来复习一下,整个中断树的最底层是中断产生设备(也可能是从interrupt nexus节点),中断产生设备用interrupts属性描述他能产生的中断。因为他的中断父设备可能和设备树的父设备不同,那么用interrupt-parent属性指向他的中断父设备。他的中断父设备可能是中断控制器(如果中断产生设备的中断和中断控制器的中断是一一对应的,或者最底层是interrupt nexus节点),也可能是interrupt nexus节点(如果最底层是中断产生设备,且需要映射)。interrupt nexus节点及他的所有直接子节点构成了一个interrupt domain,在该interrupt domain下中断源怎样表示由#interrupt-cells属性决定,如何由中断子设备中断源找到中断父设备中断源由interrupt-map和interrupt-map-mask属性决定。interrupt nexus的父节点可能还是一个interrupt nexus父节点,也可能是一个中断控制器,当向上找到最后一个中断控制器,并且该中断控制器再也没有中断父设备时,整个中断树就遍历完成了。中断控制器用interrupt-controller属性表示自己是中断控制器,并且用#interrupt-cells属性表示他所直接管理的interrupt domain用几个u32表示一个中断源。根据中断树的特性,一个设备树中是有可能有多个中断树的。
以上是中断在设备树中如何描述的规则,听起来是挺复杂的,但只要理解了就很简单,为了帮助理解我们举一个实际的例子。为了突出中断部分,我们做了简化。
/ { model = "Marvell Armada 375 family SoC"; compatible = "marvell,armada375"; soc { #address-cells = <2>; #size-cells = <1>; interrupt-parent = <&gic>; internal-regs { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; [email protected] { compatible = "arm,cortex-a9-twd-timer"; reg = <0xc600 0x20>; interrupts = <GIC_PPI 13 (IRQ_TYPE_EDGE_RISING | GIC_CPU_MASK_SIMPLE(2))>; clocks = <&coreclk 2>; }; gic: interrupt-[email protected] { compatible = "arm,cortex-a9-gic"; #interrupt-cells = <3>; #address-cells = <0>; interrupt-controller; reg = <0xd000 0x1000>, <0xc100 0x100>; }; } pcie-controller { compatible = "marvell,armada-370-pcie"; #address-cells = <3>; #size-cells = <2>; [email protected]1,0 { #address-cells = <3>; #size-cells = <2>; #interrupt-cells = <1>; interrupt-map-mask = <0 0 0 0>; interrupt-map = <0 0 0 0 &gic GIC_SPI 29 IRQ_TYPE_LEVEL_HIGH>; }; }; }
首先我们看到[email protected]这个设备节点下定义了interrupts属性,这说明该设备可以产生中断,但是这个属性下描述了几个中断我们是看不出来的(如果有经验了,我们能猜出只是一个中断,现在我们按照规则确认)。因为该节点没有interrupt-parent属性,那么认为设备树的父节点internal-regs就是中断父节点,在internal-regs父节点下还是没有interrupt-parent属性,那么还是继续找设备树父节点,找到了soc,在该节点下边有interrupt-parent属性。该属性引用的标签为gic,搜索整个设备树,[email protected]的标签为gic。gic节点下有interrupt-controller属性,说明他是一个中断控制器。gic节点还有属性#interrupt-cells = <3>,说明在该控制器的interrupt domain下,中断源(interrupt specifier)用3个u32表示,我们再看[email protected]下的interrupts属性也确实由3个u32组成(可以参考GIC的规范,第一个u32表示中断类型,第二个是中断号,第三个是中断触发条件)。这个例子说明如果中断产生设备的中断源和中断控制器的中断源是一一对应的,那么可以不需要interrupt nexus节点及相关的属性来表示中断映射。
再看[email protected],0这个节点,有#interrupt-cells属性,但是没有interrupt-controller属性,这说明他是一个interrupt nexus节点。该节点的#interrupt-cells属性为1,说明该interrupt nexus节点管辖下的中断源用1个u32表示就可以了。在[email protected],0节点下边没有子节点,且也没有节点的interrupt-parent属性指向[email protected],0节点,所以从设备树上看不到该interrupt domain下的中断产生设备,可能的原因是这些中断产生设备软件可以动态识别所以不需要设备树描述。因为interrupt-map-mask属性是由中断产生设备的地址和中断源(interrupt specifier)组成,且中断源用1个u32表示,那么可以推测中断产生设备地址由3个u32组成。这里需要注意的是[email protected],0节点的#address-cells属性为3,是说该总线下边的设备地址用3个u32表示,但并不代表中断产生设备的设备地址也一定3个u32表示,此处不能说是巧合,但是我们要清楚中断产生设备的地址由几个u32组成是由该设备所在总线决定的,对于pcie总线也确实是3,但是其他总线可能存在其他种的情况。现在我们来分析interrupt-map属性,前三个数字是中断设备地址,第四个数字是中断设备的中断源。因为interrupt-map-mask是全0,这样不管与什么数字做与运算结果都是0,interrupt-map属性的前4个数字也都是0,这说明在[email protected],0下边所有的中断映射到中断父节点的中断都是一个中断。接着是指向gic的<phandle>,因为gic节点下#address-cells属性为0,所以后边不需要描述中断父设备的地址了,后边3个数字都是表示中断父设备中断源的。一句话描述就是[email protected],0下的所有中断都映射到gic,GIC_SPI类型的第29号中断,触发类型为高电平触发。这个例子说明在中断树的最下边可以是interrupt nexus节点。
以上例子中断树的根是gic,gic下边有两个孩子,一个是中断设备[email protected],一个是interrupt nexus节点[email protected],0。gic直接管辖的interrupt domain用3个u32表示中断源,[email protected]在这个interrupt domain下。[email protected],0下定义了一个新的interrupt domain,在该interrupt domain下,中断源用1个u32表示,[email protected],0用interrupt-map和interrupt-map-mask属性将下边所有设备的中断映射到一个gic下边的中断上。
以上是关于关于Linux中禁用中断和锁定的关系问题的主要内容,如果未能解决你的问题,请参考以下文章