内外部中断和定时器中断外中断总称

提问回答都赚钱
> 问题详情
以下哪一项不属于操作风险事件?( A.内部欺诈B.外部欺诈C.经营中断和系统瘫
悬赏:0&&答案豆&&&&提问人:匿名网友&&&&提问收益:0.00答案豆&&&&&&
以下哪一项不属于操作风险事件?( A.内部欺诈B.外部欺诈C.经营中断和系统瘫痪D.本币汇率短期内剧烈波动请帮忙给出正确答案和分析,谢谢!
发布时间:&&截止时间:
网友回答&(共0条)
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&2.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&5.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&4.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&4.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&4.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&4.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&3.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&10.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&1.00元收益
回答悬赏问题预计能赚取&4.00元收益
你可能喜欢的
[] [] [] [] [] [] [] [] [] [] [] []
请先输入下方的验证码查看最佳答案所属子分类:
注册时间: 10:00
论坛积分:8
STM32F103,4路串口中断接受没有问题,单测外部中断也没有问题,放在一起的时候,出问题了,外中断能影响串口中断(选用的是下降沿中断,单侧是没有问题的,和串口中断在一起的时候,发现串口丢数据了,而且在按键没有抬起时,串口不能接受了,在主循环里一个流水等也不流了)反复改NVIC里的中断优先级了,4路串口在0MS定时发送500字节左右,都能正确响应(中断函数里就是收到什么在回发出来),同时定时器里控制灯闪和主程序里流水也没问题。好几天了,哪位给指教一下,对NVIC里的优先级什么的搞的云里雾里的,呵呵要求外中断级别是最低的,不能影响串口
俺在线等结果呢,在好几个群里发了问,都无人回应!!
void& EXTI_Configuration(void)
&EXTI_InitTypeDef& EXTI_InitS
/*********************外部中断0配置*************************/
& &GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource1); //把PA.0设置为外部中断0
& &EXTI_DeInit();&&&& //将EXIT寄存器重设置为缺省值
&&&EXTI_InitStructure.EXTI_Line = EXTI_Line1;& //设置外部中断线0
& &EXTI_InitStructure.EXTI_Mode = EXTI_Mode_I//EXTI_Mode_I EXTI_Mode_Event //设置EXTI线路为重点请求
& &EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_F& //设置输入线路为下降沿为中断请求
& &EXTI_InitStructure.EXTI_LineCmd = ENABLE;&& //中断使能
& &EXTI_Init(&EXTI_InitStructure);&
& &//EXTI_GenerateSWInterrupt(EXTI_Line1);
void NVIC_Configuration(void)
&NVIC_InitTypeDef NVIC_InitS
#ifdef& VECT_TAB_RAM&
& &/* Set the Vector Table base location at 0x */
& &NVIC_SetVectorTable(NVIC_VectTab_RAM, 0x0);
#else& /* VECT_TAB_FLASH& */
& &/* Set the Vector Table base location at 0x */
& &NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0);&&
&&NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);&// 设置先占优先级2位,从优先级2位 ;
& &// Enable the TIM2 global Interrupt
& &NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQC& // TIM2 全局中断
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 先占优先级 0 ;
& &NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;& // 从优先级 0 ;
& &NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;&// IRQ通道被使能 ;
& &NVIC_Init(&NVIC_InitStructure);
&&// Enable the TIM3 Interrupt
& &NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQC // TIM3 全局中断
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 先占优先级 1 ;
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 先占优先级 0 ;
& &NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;&// IRQ通道被使能 ;
& &NVIC_Init(&NVIC_InitStructure);
&&NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQC& // USART1 全局中断 ;
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
& &NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
& &NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
& &NVIC_Init(&NVIC_InitStructure);
&&NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQC& // USART2 全局中断 ;
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
& &NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
& &NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
& &NVIC_Init(&NVIC_InitStructure);
& &NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQC& // USART3 全局中断 ;
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 4;
& &//&NVIC_InitStructure.NVIC_IRQChannelSubPriority = 4;
& &NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
& &NVIC_Init(&NVIC_InitStructure);
& &NVIC_InitStructure.NVIC_IRQChannel = UART4_IRQC& // USART4 全局中断 ;
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 5;
& &//NVIC_InitStructure.NVIC_IRQChannelSubPriority = 5;
& &NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
& &NVIC_Init(&NVIC_InitStructure);
& &&/*& */
&&NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQC//EXTI0_IRQC//设置外部中断0
& &NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;//主优先级为4
& &NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//从优先级为4
& &NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//中断使能
& &NVIC_Init(&NVIC_InitStructure);
void USART2_IRQHandler(void)
&u8 mm,i,p,n;
&if(USART_GetITStatus(USART2, USART_FLAG_ORE) != RESET)
&&&&mm=(u8)(USART_ReceiveData(USART2));
&&&&USART_SendData(USART2,mm);
void UART4_IRQHandler(void)
&u8 mm,i,p,n;
&//USART_SendData(USART1,'&');
&if(USART_GetITStatus(UART4, USART_FLAG_RXNE) != RESET)
&&&&mm=(u8)(USART_ReceiveData(UART4));
&&&&USART_SendData(UART4,mm);
&//USART_SendData(USART1,'&');
最开始调试的时候在中断里一直判断这个USART_FLAG_RXNE,后来在把4个串口都打开的时候在判断这个不行了,程序一直在这个中断里不出来,所以就判断USART_FLAG_ORE这个,目前串口还没问题,这个我稍后在查
注册时间: 10:41
回复数: 61084
主题数: 385
酷贴数:28
论坛积分:65039
来自: 湖南
你的分组完全乱了.
好好看看中断分组.
组1,只有1位抢占优先级.所以抢占优先级要嘛是1,要嘛是0.
&你看看你的设置...
我的淘宝小店:
注册时间: 10:00
论坛积分:8
回复【1楼】&正点原子&:
---------------------------------
从0&到4&我都实验过
注册时间: 10:00
论坛积分:8
回复【1楼】&正点原子&:
---------------------------------
您给说个数吧,我按照这个数填上好用了我在理解一下,您给说个数吧,能把串口1中断高于外中断就可以,其他的不先不管,我实验一下
注册时间: 10:41
回复数: 61084
主题数: 385
酷贴数:28
论坛积分:65039
来自: 湖南
组1.
抢占0,sub0&sub1&sub2&sbu3...&sub7,
抢占1,sub0&sub1&sub2&sub3...&sub7
而抢占0&抢占1.
所以(0,x)&(1,x)
我的淘宝小店:
注册时间: 10:00
论坛积分:8
回复【4楼】&正点原子&:
---------------------------------
谢谢原子兄,我实验一下,呵呵
请选择一个版面...
STM32-F0/F1/F2专区
STM32-F3/F4专区
MSP430专区
ARM7/ARM9/ARM11专区
其他Cortex系列
uCOS & uCGUI & Emwin
trochili(飞鸟)操作系统
中国RTOS联盟
FPGA/CPLD/DSP专区
数字,模拟,高频电路
编程语言学习
蓝牙/WIFI/Zigbee等通信技术
开发工具专栏
UOL 单片机面向对象语言
四轴飞行器
DIY大赛专区
DIY项目资料专区
论坛建设区
二手交易专栏
& 开源电子网() |中断 - Linux源代码导读报告
Linux源代码阅读——中断
为什么要有中断
中断的作用
中断的处理原则
Linux 中断机制
中断控制器
中断描述符
中断数据结构
中断的初始化
中断处理过程
CPU 的中断处理流程
保存中断信息
从中断中返回
编写中断处理程序
软中断、tasklet与工作队列
上半部与下半部
1 为什么要有中断
1.1 中断的作用
处理器的运算速度一般要比外部硬件快很多。以读取硬盘为例,如果是简单的顺序执行,CPU 必须等待很长时间,不停地轮询硬盘是否读取完毕,这会浪费很多 CPU 时间。中断提供了这样一种机制,使得读取硬盘这样的操作可以交给硬件来完成,CPU 挂起当前进程,将控制权转交给其他进程,待硬件处理完毕后通知 CPU,操作系统把当前进程设为活动的,从而允许该进程继续执行,处理读取硬盘的结果。
另一方面,有些事件不是程序本身可预见的,需要硬件以某种方式告诉进程。例如时钟中断为定时器提供了基础,如果没有时钟中断,程序只能每执行几条指令就检查一下当前系统时间,这在效率上是不可接受的。
从广义上说,中断是改变 CPU 处理指令顺序的硬件信号。分为两类:
异步的:在程序执行的任何时刻都可能产生,如时钟中断
同步的:在特殊或错误指令执行时由 CPU 控制单元产生,称为异常
1.2 中断的处理原则
中断处理的基本原则就是“快”。如果反应慢了,数据可能丢失或被覆盖。例如键盘按键中断,所按下的键的 keycode 放在 KBDR 寄存器中,如果在中断被处理之前用户又按了一个键,则 KBDR 的值被新按下的键的 keycode 覆盖,早先按下的键对应的数据就丢失了。
当一个中断信号到达时,CPU 必须停止当前所做的事,转而处理中断信号。为了尽快处理中断并为接收下一个中断做好准备,内核应尽快处理完一个中断,将更多的处理向后推迟。
为达到“快”这一目标,内核允许不同类型的中断嵌套发生,即在中断处理的临界区之外可以接受新的中断。这样,更多的 I/O 设备将处于忙状态。
2 Linux 中断机制
2.1 中断控制器
中断控制器是连接设备和 CPU 的桥梁,一个设备产生中断后,需要经过中断控制器的转发,才能最终到达 CPU。时代发展至今,中断控制器经历了 PIC(Programmable Interrupt Controller,可编程中断控制器) 和 APIC (Advanced Programmable Interrupt Controller,高级可编程中断控制器) 两个阶段。前者在 UP(Uni-processor,单处理器) 上威震四方,随着 SMP (Symmetric Multiple Processor,对称多处理器) 的流行,APIC 已广为流行并将最终取代 PIC。
8259A (PIC) 管脚图
上图中的管脚说明:
IR0~IR7 (Interrupt Request0~7,用于连接设备)
INT (连接 CPU,当有中断请求时,拉高该管脚以通知 CPU 中断的到来)
INTA (连接 CPU,CPU 通过该管脚应答中断请求,并通知 PIC 提交中断的 vector 到数据线)
CS (片选,用于将两个 8259A 串联成可连接 15 个设备的 PIC)
8259A 中的寄存器:
ICW: Initialization Command Word,初始化命令寄存器,用于初始化 8259A
OCW: Operation Command Word,操作命令字,用于控制 8259A
IRR: Interrupt Request Register,中断请求寄存器,共 8bit,对应 IR0~IR7 八个中断管脚。当某个管脚的中断请求到来后,若该管脚没有被屏蔽,IRR 中对应的 bit 被置1。表示 PIC 已经收到设备的中断请求,但还未提交给 CPU。
ISR: In Service Register,服务中寄存器,共 8bit,每 bit 意义同上。当 IRR 中的某个中断请求被发送给 CPU 后,ISR 中对应的 bit 被置1。表示中断已发送给 CPU,但 CPU 还未处理完。
IMR: Interrupt Mask Register,中断屏蔽寄存器,共 8bit,每 bit 意义同上。用于屏蔽中断。当某 bit 置1时,对应的中断管脚被屏蔽。
arch/x86/kernel/i8259_32.c 中通过位运算来开启和关闭中断。
63 void disable_8259A_irq(unsigned int irq)
unsigned int mask = 1 &&
spin_lock_irqsave(&i8259A_lock, flags);
// 用 spinlock 锁住
cached_irq_mask |=
// 将 IRQ 的相应位置1,屏蔽中断
if (irq & 8)
outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管脚负责 8259A 的级联(见下图),为0时使用主片,为1时使用从片
outb(cached_master_mask, PIC_MASTER_IMR);
spin_unlock_irqrestore(&i8259A_lock, flags); // 解开自旋锁
77 void enable_8259A_irq(unsigned int irq)
unsigned int mask = ~(1 && irq);
spin_lock_irqsave(&i8259A_lock, flags);
// 用 spinlock 锁住
cached_irq_mask &=
// 将 IRQ 的相应位置0,开启中断
if (irq & 8)
outb(cached_slave_mask, PIC_SLAVE_IMR); // IR2 管脚负责 8259A 的级联(见下图),为0时使用主片,为1时使用从片
outb(cached_master_mask, PIC_MASTER_IMR);
spin_unlock_irqrestore(&i8259A_lock, flags); // 解开自旋锁
PIC 的每个管脚具有优先级,连接号码较小的设备具有较高的中断优先级。
在 PIC 默认的 Full Nested 模式下,通过 PIC 发起中断的流程如下:
一个或多个 IR 管脚上产生电平信号,若对应的中断没有被屏蔽,IRR 中相应的 bit 被置1。
PIC 拉高 INT 管脚通知 CPU 中断发生。
CPU 通过 INTA 管脚应答 PIC,表示中断请求收到。
PIC 收到 INTA 应答后,将 IRR 中具有最高优先级的 bit 清零,并设置 ISR 中对应的 bit。
CPU 通过 INTA 管脚第二次发出脉冲,PIC 收到后计算最高优先级中断的 vector,并将它提交到数据线上。
等待 CPU 写 EOI (End of Interrupt)。收到 EOI 后,ISR 中最高优先级的 bit 被清零。如果 PIC 处于 AEOI 模式,当第二个 INTA 脉冲收到后,ISR 中最高优先级的 bit 自动清零。
PIC 还有优先级轮转模式,即 PIC 在服务完一个管脚之后将其优先级临时降低,并升高未服务管脚的优先级,以实现类似轮询的模式,避免一个管脚持续发出中断导致其他设备“饿死”。
下图是一个典型的 PIC 中断分配,管脚基本上都被古董级设备占据了。
arch/x86/kernel/i8259_32.c 中 8259A 引脚的分配(function init_8259A)
outb_pic(0x11, PIC_MASTER_CMD); /* ICW1: select 8259A-1 init */
outb_pic(0x20 + 0, PIC_MASTER_IMR);
/* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */
outb_pic(1U && PIC_CASCADE_IR, PIC_MASTER_IMR); /* 8259A-1 (the master) has a slave on IR2 */
if (auto_eoi)
/* master does Auto EOI */
outb_pic(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR);
/* master expects normal EOI */
outb_pic(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR);
outb_pic(0x11, PIC_SLAVE_CMD);
/* ICW1: select 8259A-2 init */
outb_pic(0x20 + 8, PIC_SLAVE_IMR);
/* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */
outb_pic(PIC_CASCADE_IR, PIC_SLAVE_IMR);
/* 8259A-2 is a slave on master's IR2 */
outb_pic(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR); /* (slave's support for AEOI in flat mode is to be investigated) */
从上图可见,PIC 能接的设备数量实在太少了,而且不支持多处理器。
为了使用 8259A 级联连接较多的设备,可以采用两种方式:
IRQ 共享:中断处理程序执行多个中断服务程序(ISR),每个 ISR 是一个与共享 IRQ 线相关的函数。
IRQ 共享需要满足两个条件:
每个 ISR 都愿意共享 IRQ,即 request_irq() 时指定了 IRQF_SHARED
所有 ISR 具有相同的触发条件(电平触发或边沿触发、高低电平或上下边沿)
IRQ 动态分配:在可能的最后时刻,才把 IRQ 线分配给一个设备。
当然,APIC 是现代的解决方案。即使是 APIC,也需要使用 IRQ 共享。
I/O APIC 的组成为:一组 24 条 IRQ 线,一张 24 项的中断重定向表,可编程寄存器,通过 APIC 总线发送和接收 APIC 信息的一个信息单元。
与 8259A 不同,中断优先级不与引脚号相关联,中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器和选择处理器的方式。
来自外部硬件设备的中断以两种方式在可用 CPU 之间分发:
2.2 中断描述符
Intel 提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。
Linux 使用与 Intel 稍有不同的分类,把中断描述符分为五类:
中断门(interrupt gate):用户态的进程不能访问Intel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。
set_intr_gate(n,addr)
上述系统调用在 IDT 的第 n 个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址 addr,DPL 字段设置为0。
系统门(system gate):用户态的进程可以访问Intel陷阱门(门的DPL字段为3)。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下,可以发布into、bound及int $0x80三条汇编语言指令。
set_system_gate(n,addr)
系统中断门(system interrupt gate):能够被用户态进程访问的Intel中断门(门的DPL字段为3)。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。
set_system_intr_gate(n,addr)
陷阱门(trap gate):用户态的进程不能访问的一个Intel陷阱门(门的DPL字段为0)。大部分Linux异常处理程序都通过陷阱门来激活。
set_trap_gate(n,addr)
任务门(task gate):不能被用户态进程访问的Intel任务门(门的DPL字段为0)。Linux对“Double fault”异常的处理程序是由任务门激活的。
set_task_gate(n,gdt)
门中的段选择符中存放一个TSS的全局描述符表的指针,该TSS中包含要被激活的函数。
在 IDT 中插入门的函数定义在 include/asm-x86/desc.h 中。
这些函数以不同的参数调用内部函数 _set_gate()。_set_gate 调用两个内部函数
pack_gate: 设置门的数据结构:中断号、门类型、处理函数地址、DPL、ist、目录段寄存器
38 static inline void pack_gate(gate_desc *gate, unsigned type, unsigned long func,
unsigned dpl, unsigned ist, unsigned seg)
gate-&offset_low = PTR_LOW(func);
// 处理函数低内存偏移
gate-&segment = __KERNEL_CS;
// 内核代码段
gate-&ist =
gate-&p = 1;
gate-&dpl =
gate-&zero0 = 0;
gate-&zero1 = 0;
gate-&type =
// 门类型(宏定义)
gate-&offset_middle = PTR_MIDDLE(func);
// 处理函数中内存偏移
gate-&offset_high = PTR_HIGH(func);
// 处理函数高内存偏移
write_idt_entry: 宏定义为 native_write_idt_entry,用 memcpy 将设置好的门写入 IDT。
2.3 中断数据结构
在 Linux 中,中断描述符的核心数据结构是 include/linux/irq.h 中的 irq_desc 结构体。每个 irq_desc 实例描述一条中断线。
153 struct irq_desc {
irq_flow_handler_t
// 中断事件处理函数,下面会介绍
struct irq_chip
// irq_chip 指针,描述了一些硬件信息,下面会介绍
struct msi_desc
*handler_ // chip 中使用的数据
// chip 中使用的数据
struct irqaction
/* IRQ action list */ // irqaction 指针,下面会介绍
unsigned i
/* IRQ status */
// IRQ 线状态标志
/* nested irq disables */
unsigned int
/* nested wake enables */
unsigned int
/* For detecting broken IRQs */ // 中断计数
unsigned int
irqs_ // 无法处理的中断计数
unsigned long
last_ /* Aging timer for unhandled count */
168 #ifdef CONFIG_SMP
// 多处理器中的处理器亲和性
171 #endif
172 #if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
174 #endif
175 #ifdef CONFIG_PROC_FS
struct proc_dir_entry
// 在 /proc 文件系统中的目录
177 #endif
const char
// 中断名称
179 } ____cacheline_internodealigned_in_
irq_desc 在 kernel/irq/handle.c 中被使用,此文件是 IRQ 机制的核心入口,描述了各中断线。
50 struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.status = IRQ_DISABLED,
// 默认屏蔽中断
.chip = &no_irq_chip,
// 没有与 chip 相关联
// 未知(坏的)IRQ 处理程序,输出 IRQ 信息供调试,更新 CPU IRQ 次数计数器,回应 IRQ。
.handle_irq = handle_bad_irq,
.depth = 1,
// 默认是第一层(没有嵌套中断)
.lock = __SPIN_LOCK_UNLOCKED(irq_desc-&lock), // 还没有自旋锁
57 #ifdef CONFIG_SMP
.affinity = CPU_MASK_ALL
// 处理器亲和性未定义
下面介绍 irq_desc 中的主要数据成员。
handle_irq
handle_irq 是函数指针,指向 kernel/irq/chip.c 中的中断事件处理函数。
handle_simple_irq
handle_level_irq
handle_fasteoi_irq
handle_edge_irq
handle_percpu_irq
这个函数指针是由 kernel/irq/chip.c 中的 __set_irq_handler() 设置的。
chip 是 irq_chip 结构体指针,include/linux/irq.h 中的 irq_chip 结构体定义了对每根中断线的底层硬件操作:
99 struct irq_chip {
const char
// 中断线名称
unsigned int
(*startup)(unsigned int irq);
// 初始化中断的函数指针
(*shutdown)(unsigned int irq);
// 停止中断的函数指针
(*enable)(unsigned int irq);
// 启用中断的函数指针
(*disable)(unsigned int irq);
// 关闭中断的函数指针
(*ack)(unsigned int irq);
// 确认中断的函数指针
(*mask)(unsigned int irq);
// 屏蔽中断的函数指针
(*mask_ack)(unsigned int irq);
// 确认并屏蔽中断的函数指针
(*unmask)(unsigned int irq);
// 取消屏蔽中断的函数指针
(*eoi)(unsigned int irq);
// 中断处理结束的函数指针
(*end)(unsigned int irq);
(*set_affinity)(unsigned int irq, cpumask_t dest);
// 设置处理器亲和性
(*retrigger)(unsigned int irq);
// 重新出发中断
// 设置中断触发类型,根据 IRQ_TYPE 宏定义,包括上边沿、下边沿、边沿、高电平、低电平等
(*set_type)(unsigned int irq, unsigned int flow_type);
(*set_wake)(unsigned int irq, unsigned int on);
// 唤醒中断
/* Currently used only by UML, might disappear one day.*/
119 #ifdef CONFIG_IRQ_RELEASE_METHOD
(*release)(unsigned int irq, void *dev_id);
121 #endif
* For compatibility, -&typename is copied into -&name.
* Will disappear.
const char
action 是 irqaction 结构体指针,指向一个 irqaction 链表。irqaction 在 include/linux/interrupt.h 中定义,每个结构体描述一个中断处理程序。
60 struct irqaction {
irq_handler_
// 中断处理程序的函数指针
// 处理器亲和性
const char *
// 中断处理程序名称,显示在 /proc/interrupts 中
void *dev_
// 设备 ID
struct irqaction *
// 指向链表中的下一个 irqaction 结构体
// 中断通道号
struct proc_dir_entry *
// 在 /proc 文件系统中的目录
status 是描述 IRQ 线状态的一组标志。在同一文件中宏定义:
49 #define IRQ_INPROGRESS
/* IRQ handler active - do not enter! */
50 #define IRQ_DISABLED
/* IRQ disabled - do not enter! */
51 #define IRQ_PENDING
/* IRQ pending - replay on enable */
52 #define IRQ_REPLAY
/* IRQ has been replayed but not acked yet */
53 #define IRQ_AUTODETECT
/* IRQ is being autodetected */
54 #define IRQ_WAITING
/* IRQ not yet seen - for autodetection */
55 #define IRQ_LEVEL
/* IRQ level triggered */
56 #define IRQ_MASKED
/* IRQ masked - shouldn't be seen again */
57 #define IRQ_PER_CPU
/* IRQ is per CPU */
综上所述,内核中的中断描述符表是一个 irq_desc 数组,数组的每一项描述一根中断线的信息,包括芯片中断处理程序、底层硬件操作函数、注册的中断处理程序链表等。
中断向量表可以通过 /proc/interrupts 查看:
[boj@~]$ cat /proc/interrupts
IO-APIC-edge
IO-APIC-edge
IO-APIC-edge
IO-APIC-fasteoi
IO-APIC-edge
IO-APIC-edge
IO-APIC-edge
IO-APIC-fasteoi
uhci_hcd:usb5, yenta, i915
IO-APIC-fasteoi
uhci_hcd:usb4
IO-APIC-fasteoi
uhci_hcd:usb3
IO-APIC-fasteoi
IO-APIC-fasteoi
firewire_ohci
IO-APIC-fasteoi
ehci_hcd:usb1, uhci_hcd:usb2, mmc0
PCI-MSI-edge
PCI-MSI-edge
Non-maskable interrupts
Local timer interrupts
Spurious interrupts
Performance monitoring interrupts
IRQ work interrupts
Rescheduling interrupts
Function call interrupts
TLB shootdowns
Thermal event interrupts
Threshold APIC interrupts
Machine check exceptions
Machine check polls
负责打印 /proc/interrupts 的代码位于 arch/x86/kernel/irq_32.c。
242 int show_interrupts(struct seq_file *p, void *v)
2.4 中断的初始化
中断机制的初始化分为三步:
arch/x86/kernel/head_32.S 中 setup IDT,在内核引导分析报告中已经阐述。
init/main.c 的 start_kernel() 中的 trap_init()
init/main.c 的 start_kernel() 中的 init_IRQ()
trap_init()
trap_init() 定义于 arch/x86/kernel/traps_32.c,作用是设置中断向量。
初始化 APIC 映射表
调用 set_trap_gate、set_intr_gate、set_task_gate、set_system_gate 等,初始化中断描述符表。
调用 set_system_gate,初始化系统调用
将已设置的中断向量置保留位
将已设置的系统调用置保留位
初始化 CPU 作为屏障
执行 trap_init 的钩子函数
init_IRQ() 定义于 arch/x86/kernel/paravirt.c,由 paravirt_ops.init_IRQ() 和 native_init_IRQ() 二者组成。
native_init_IRQ() 定义于 arch/x86/kernel/i8259.c。该函数主要将 IDT 未初始化的各项初始化为中断门。
pre_intr_init_hook() 调用 init_ISA_irqs(),初始化 irq_desc 数组、status、action、depth。
在循环中,对于所有在 FIRST_EXTERNAL_VECTOR(0x20) 与 NR_VECTOR(0x100)之间的、不是系统中断的 256 - 32 - 1 = 223 项,调用 set_intr_gate(),初始化为中断门。
现在我们关心的是,这些中断门的中断处理程序是什么?在 x86 体系结构下没找到 interrupt 数组的定义,因此使用 64 位体系结构的做说明:
// arch/x86/kernel/i8259_64.c
80 static void (*__initdata interrupt[NR_VECTORS - FIRST_EXTERNAL_VECTOR])(void) = {
IRQLIST_16(0x2), IRQLIST_16(0x3),
IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7),
IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb),
IRQLIST_16(0xc), IRQLIST_16(0xd), IRQLIST_16(0xe), IRQLIST_16(0xf)
以上是 interrupt 数组的定义。在下面的代码中,## 是将字符串连接起来,这样宏定义的函数 IRQ(0x4,6) 就是 IRQ0x46_interrupt,生成 224 个这样的函数填入数组。
// arch/x86/kernel/i8259_64.c
70 #define IRQ(x,y) \
IRQ##x##y##_interrupt
73 #define IRQLIST_16(x) \
IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
那么这 224 个函数在哪里呢?通过下面的宏可以生成一个汇编函数,它调用了 common_interrupt 函数:
// include/asm/hw_irq_64.h
155 #define IRQ_NAME2(nr) nr##_interrupt(void)
156 #define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)
162 #define BUILD_IRQ(nr)
asmlinkage void IRQ_NAME(nr);
asm("\n.p2align\n"
"IRQ" #nr "_interrupt:\n\t"
"push $~(" #nr ") ; "
"jmp common_interrupt");
common_interrupt 是汇编函数,这个函数最终调用了 do_IRQ,这是我们下章要介绍的核心中断处理函数。
// arch/x86/kernel/entry_32.S
613 common_interrupt:
TRACE_IRQS_OFF
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
619 ENDPROC(common_interrupt)
看来,只需要调用 BUILD_IRQ 就能生成中断处理函数了。Linux Kernel 正是这样做的:
// arch/x86/kernel/i8259_64.c
37 #define BI(x,y) \
BUILD_IRQ(x##y)
40 #define BUILD_16_IRQS(x) \
BI(x,0) BI(x,1) BI(x,2) BI(x,3) \
BI(x,4) BI(x,5) BI(x,6) BI(x,7) \
BI(x,8) BI(x,9) BI(x,a) BI(x,b) \
BI(x,c) BI(x,d) BI(x,e) BI(x,f)
....................................................
BUILD_16_IRQS(0x2) BUILD_16_IRQS(0x3)
62 BUILD_16_IRQS(0x4) BUILD_16_IRQS(0x5) BUILD_16_IRQS(0x6) BUILD_16_IRQS(0x7)
63 BUILD_16_IRQS(0x8) BUILD_16_IRQS(0x9) BUILD_16_IRQS(0xa) BUILD_16_IRQS(0xb)
64 BUILD_16_IRQS(0xc) BUILD_16_IRQS(0xd) BUILD_16_IRQS(0xe) BUILD_16_IRQS(0xf)
不得不发表一下感慨,Linux Kernel 对 C 语言宏的运用真是炉火纯青。如果是我写代码,很可能不得不先写个代码生成器,用它生成 224 个函数的源码。但 Linux Kernel Source 里出现几百个几乎一模一样的函数太不优雅了,于是利用 C 语言的预处理机制,采用二级宏定义,尽可能降低代码量。
2.5 内核接口
中断处理程序不是编译内核时就完全确定的,因此要为开发者留下编程接口。
2.6.17 内核引入了 generic IRQ 机制,支持 i386、x86-64 和 ARM 三个体系结构。generic IRQ 层的引入,是为了剥离 IRQ flow 和 IRQ chip 过于紧密的耦合。为驱动开发者提供通用的 API 来 request/enable/disable/free 中断,而不需要知道任何底层的中断控制器细节。
这些中断 API 是在内核中用 EXPORT_SYMBOL 导出的。
kernel/irq/manage.c 中的 request_irq:
536 int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
irq: 中断通道号,无符号整数
handler:中断处理程序的函数指针 (irq_return_t isr_func(int irq, void *dev_id))
irq_flags:标志位
IRQF_SHARED: 共享中断通道
IRQF_DISABLED: 中断处理程序执行时关中断
IRQF_SAMPLE_RANDOM: 随机发生中断,可用于产生随机数
IRQF_TRIGGER_LOW:2.6.26 中没有,低电平有效
IRQF_TRIGGER_HIGH: 2.6.26 中没有,高电平有效
IRQF_TRIGGER_RISING: 2.6.26 中没有,上升沿有效
IRQF_TRIGGER_FALLING: 2.6.26 中没有,下降沿有效
dev_name:名称,显示在 /proc/interrupts 中
dev_id:设备 ID,区分不同的设备
内部机制:
检查输入数据的合法性
为临时变量 irqaction 分配内存空间,初始化 irqaction 数据结构
如果是调试模式,测试是否运行正常
进入工作函数 setup_irq(unsigned int irq, struct irqaction *new)
如果是 IRQF_SAMPLE_RANDOM 模式,随机初始化 irq
如果希望共享中断通道,所有中断处理程序需要有相同的触发特性标识、PERCPU 特性
把新 irqaction 结构体挂在链表尾部
如果设置了 IRQF_TRIGGER_MASK,初始化触发特性
初始化 irq 状态、嵌套深度
启动(enable)此 IRQ
释放自旋锁
调用 /kernel/irq/proc.c 中的 register_irq_proc() 和 register_handler_proc(),建立 /proc 文件系统中的相关数据结构
返回成功(0)
如果出错,输出内核调试信息,释放自旋锁,返回错误
释放 irqaction 的内存空间,返回 setup_irq 的返回值
kernel/irq/manage.c 中的 free_irq:
435 void free_irq(unsigned int irq, void *dev_id)
irq: 中断通道号,无符号整数
dev_id: 请求中断时指定的设备 ID
内部机制:
不能在中断上下文中调用
循环,沿链表查找要删除的中断处理程序
如果发现是已经释放的,则输出内核调试信息,释放自旋锁
如果 dev_id 不对,沿着 irqaction 链表继续向下寻找
如果找到了,从链表中移除这个 irqaction
关闭此 IRQ,关闭硬件,释放自旋锁,从 /proc 文件系统中删除对应目录
同步 IRQ 以防正在其他 CPU 上运行
如果是调试模式,测试驱动程序是否知道此共享 IRQ 已移除
释放内存空间,返回
kernel/irq/manage.c 中的 enable_irq:
153 static void __enable_irq(struct irq_desc *desc, unsigned int irq)
内部调用了 __enable_irq,首先上自旋锁,找到 irq_desc 结构体指针,判断嵌套深度,刷新 IRQ 状态,释放自旋锁。
desc: 指向 irq_desc 结构体的指针
irq: 中断通道号
kernel/irq/manage.c 中的 disable_irq:
140 void disable_irq(unsigned int irq)
irq: 中断通道号
关闭中断 (无等待)
disable_irq 会保证存在的 IRQ handler 完成操作,而 disable_irq_nosync 立即关中断并返回。事实上,disable_irq 首先调用 disable_irq_nosync,然后调用 synchronize_irq 同步。
111 void disable_irq_nosync(unsigned int irq)
同步中断 (多处理器)
30 void synchronize_irq(unsigned int irq)
设置 IRQ 芯片
kernel/irq/chip.c: set_irq_chip()
93 int set_irq_chip(unsigned int irq, struct irq_chip *chip)
设置 IRQ 类型
kernel/irq/chip.c: set_irq_type()
122 int set_irq_type(unsigned int irq, unsigned int type)
设置 IRQ 数据
kernel/irq/chip.c: set_irq_data()
150 int set_irq_data(unsigned int irq, void *data)
设置 IRQ 芯片数据
kernel/irq/chip.c: set_irq_chip_data()
202 int set_irq_chip_data(unsigned int irq, void *data)
3 中断处理流程
3.1 CPU的中断处理流程
本节摘自参考文献之
每个能够发出中断请求的硬件设备控制器都有一条名为 IRQ 的输出线。所有现有的 IRQ 线都与一个名为可编程中断控制器(PIC)的硬件电路的输入引脚相连,可编程中断控制器执行下列动作:
监视 IRQ 线,检查产生的信号。如果有两条以上的 IRQ 线上产生信号,就选择引脚编号较小的 IRQ 线。
如果一个引发信号出现在 IRQ 线上:
把接收到的引发信号转换成对应的向量号
把这个向量存放在中断控制器的一个 I/O 端口(0x20、0x21),从而允许 CPU 通过数据总线读此向量。
把引发信号发送到处理器的 INTR 引脚,即产生一个中断。
等待,直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它;当这种情况发生时,清 INTR 线。
返回第1步。
当执行了一条指令后,CS和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否已经发生了一个中断或异常。如果发生了一个中断或异常,那么控制单元执行下列操作:
确定与中断或异常关联的向量i (0 ≤ i ≤ 255)。
读由idtr寄存器指向的 IDT表中的第i项(在下面的分析中,我们假定IDT表项中包含的是一个中断门或一个陷阱门)。
从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址。
确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较,如果CPL小于DPL,就产生一个“General protection”异常,因为中断处理程序的特权不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“General protection”异常。这最后一个检查可以避免用户应用程序访问特殊的陷阱门或中断门。
检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈。通过执行以下步骤来做到这点:
读tr寄存器,以访问运行进程的TSS段。
用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到(参见第三章的“任务状态段”一节)
在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
如果故障已发生,用引起异常的指令地址装载CS和eip寄存器,从而使得这条指令能再次被执行。
在栈中保存eflags、CS及eip的内容。
如果异常产生了一个硬件出错码,则将它保存在栈中。
装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或者异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
用保存在栈中的值装载CS、eip或eflags寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。
检查处理程序的CPL是否等于CS中最低两位的值(这意味着被中断的进程与处理程序运行在同一特权级)。如果是,iret终止执行;否则,转入下一步。
从栈中装载ss和esp寄存器,因此,返回到与旧特权级相关的栈。
检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相应的段寄存器。控制单元这么做是为了禁止用户态的程序(CPL=3)利用内核以前所用的段寄存器(DPL=0)。如果不清这些寄存器,怀有恶意的用户态程序就可能利用它们来访问内核地址空间。
3.2 保存中断信息
Linux 内核的中断处理机制自始至终贯穿着 “重要的事马上做,不重要的事推后做” 的思想。
中断处理程序首先要做:
将中断号压入栈中,以便找到对应的中断服务程序
将当前寄存器信息压入栈中,以便中断退出时恢复上下文
显然, 这两步都是不可重入的。因此在进入中断服务程序时,CPU 已经自动禁止了本 CPU 上的中断响应。
上章中断初始化过程的分析中,已经介绍了 interrupt 数组的生成过程,其中索引为 n 的元素中存放着下列指令的地址:
pushl n-256
jmp common_interrupt
执行结果是将中断号 - 256 保存在栈中,这样栈中的中断都是负数,而正数用来表示系统调用。这样,系统调用和中断可以用一个有符号整数统一表示。
现在重述一下 common_interrupt 的定义:
// arch/x86/kernel/entry_32.S
613 common_interrupt:
TRACE_IRQS_OFF
movl %esp,%eax # 将栈顶地址放入 eax,这样 do_IRQ 返回时控制转到 ret_from_intr()
call do_IRQ # 核心中断处理函数
jmp ret_from_intr # 跳转到 ret_from_intr()
其中 SAVE_ALL 宏将被展开成:
# 保存除 eflags、cs、eip、ss、esp (已被 CPU 自动保存) 外的其他寄存器
pushl %eax
pushl %ebp
pushl %edi
pushl %edx
pushl %ecx
pushl %ebx
movl $ _ _USER_DS, %edx
movl %edx, %ds
# 将用户数据段选择符载入 ds、es
movl %edx, %es
3.3 处理中断
前面汇编代码的实质是,以中断发生时寄存器的信息为参数,调用 arch/x86/kernel/irq32.c 中的 do_IRQ 函数。
我们注意到 unlikely 和 unlikely 宏定义,它们的含义是
#define likely(x)
__builtin_expect((x),1)
#define unlikely(x)
__builtin_expect((x),0)
__builtin_expect 是 GCC 的内部机制,意思是告诉编译器哪个分支条件更有可能发生。这使得编译器把更可能发生的分支条件与前面的代码顺序串接起来,更有效地利用 CPU 的指令流水线。
do_IRQ 函数流程:
保存寄存器上下文
调用 irq_enter:
// kernel/softirq.c
281 void irq_enter(void)
283 #ifdef CONFIG_NO_HZ
// 无滴答内核,它将在需要调度新任务时执行计算并在这个时间设置一个时钟中断,允许处理器在更长的时间内(几秒钟)保持在最低功耗状态,从而减少了电能消耗。
int cpu = smp_processor_id();
if (idle_cpu(cpu) && !in_interrupt())
tick_nohz_stop_idle(cpu); // 如果空闲且不在中断中,则停止空闲,开始工作
287 #endif
__irq_enter();
289 #ifdef CONFIG_NO_HZ
if (idle_cpu(cpu))
tick_nohz_update_jiffies(); // 更新 jiffies
292 #endif
// include/linux/hardirq.h
135 #define __irq_enter()
/* 在宏定义函数中,do { ... } while(0) 结构可以把语句块作为一个整体,就像函数调用,避免宏展开后出现问题 */
rcu_irq_enter();
account_system_vtime(current);
add_preempt_count(HARDIRQ_OFFSET);
\ /* 程序嵌套数量计数器递增1 */
trace_hardirq_enter();
} while (0)
如果可用空间不足 1KB,可能会引发栈溢出,输出内核错误信息
如果 thread_union 是 4KB 的,进行一些特殊处理
调用 desc-&handle_irq(irq, desc),调用 __do_IRQ() (kernel/irq/handle.c)
取得中断号,获取对应的 irq_desc
如果是 CPU 内部中断,不需要上锁,简单处理完就返回了
应答中断芯片,这样中断芯片就能开始接受新的中断了。
更新中断状态。
IRQ_REPLAY:如果被禁止的中断管脚上产生了中断,这个中断是不会被处理的。当这个中断号被允许产生中断时,会将这个未被处理的中断转为 IRQ_REPLAY。
IRQ_WAITING:探测用,探测时会将所有没有中断处理函数的中断号设为 IRQ_WAITING,只要这个中断管脚上有中断产生,就把这个状态去掉,从而知道哪些中断管脚上产生过中断。
IRQ_PENDING、IRQ_INPROGRESS 是为了确保同一个中断号的处理程序不能重入,且不能丢失这个中断的下一个处理程序。具体地说,当内核在运行某个中断号对应的处理程序时,状态会设置成 IRQ_INPROGRESS。如果发现已经有另一实例在运行了,就将这下一个中断标注为 IRQ_PENDING 并返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
如果链表上没有中断处理程序,或者中断被禁止,或者已经有另一实例在运行,则进行收尾工作。
释放自旋锁
执行函数链:handle_IRQ_event()。其中主要是一个循环,依次执行中断处理程序链表上的函数,并根据返回值更新中断状态。如果愿意,可以参与随机数采样。中断处理程序执行期间,打开本地中断。
如果当前中断已经处理完,则退出;不然取消中断的 PENDING 标志,继续循环。
取消中断的 INPROGRESS 标志
收尾工作:有的中断在处理过程中被关闭了,-&end() 处理这种情况;释放自旋锁。
执行 irq_exit(),在 kernel/softirq.c 中:
递减中断计数器
检查是否有软中断在等待执行,若有则执行软中断。
如果使用了无滴答内核看是不是该休息了。
恢复寄存器上下文,跳转到 ret_from_intr (跳转点早在 common_interrupt 中就被指定了)
在中断处理过程中,我们反复看到对自旋锁的操作。在单处理器系统上,spinlock 是没有作用的;在多处理器系统上,由于同种类型的中断可能连续产生,同时被几个 CPU 处理(注意,应答中断芯片是紧接着获得自旋锁后,位于整个中断处理流程的前部,因此在中断处理流程的其余部分,中断芯片可以触发新的中断并被另一个 CPU 开始处理),如果没有自旋锁,多个 CPU 可能同时访问 IRQ 描述符,造成混乱。因此在访问 IRQ 描述符的过程中需要有 spinlock 保护。
3.4 从中断中返回
上面的中断处理流程中隐含了一个问题:整个处理过程是持续占有CPU的(除开中断情况下可能被新的中断打断外),这样
连续的低优先的中断可能持续占有 CPU, 而高优先的某些进程则无法获得 CPU
中断处理的这几个阶段中不能调用可能导致睡眠的函数
对于第一个问题,较新的 linux 内核增加了 ksoftirqd 内核线程,如果持续处理的软中断超过一定数量,则结束中断处理过程,唤醒 ksoftirqd,由它来继续处理。
对于第二个问题,linux 内核提供了 workqueue(工作队列)机制,定义一个 work 结构(包含了处理函数),然后在上述的中断处理的几个阶段的某一步中调用 schedule_work 函数,work 便被添加到 workqueue 中,等待处理。
工作队列有着自己的处理线程, 这些 work 被推迟到这些线程中去处理。处理过程只可能发生在这些工作线程中,不会发生在内核中断处理路径中,所以可以睡眠。下章将简要介绍这些中断机制。
3.5 编写中断处理程序
本节编写一个简单的中断处理程序 (catchirq) 作为内核模块,演示捕获网卡中断。
catchirq.c
#include &linux/module.h&
#include &linux/kernel.h&
#include &linux/init.h&
#include &linux/interrupt.h&
#include &linux/timer.h&
#define DEBUG
#ifdef DEBUG
#define MSG(message, args...) printk(KERN_DEBUG "catchirq: " message, ##args)
#define MSG(message, args...)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("boj");
// module_param(name, type, perm)
module_param(irq, int, 0644);
module_param(interface, charp, 0644);
int irq_handle_function(int irq, void *device_id)
static int count = 1;
MSG("[%d] Receive IRQ at %ld\n", count, jiffies);
return IRQ_NONE;
int init_module()
if (request_irq(irq, irq_handle_function, IRQF_SHARED, interface, (void *)&irq))
MSG("[FAILED] IRQ register failure.\n");
return -EIO;
MSG("[OK] Interface=%s IRQ=%d\n", interface, irq);
void cleanup_module()
free_irq(irq, &irq);
MSG("IRQ is freed.\n");
Makefile(编写说明参见 Documentation/kbuild/)
obj-m := catchirq.o
KERNELDIR := /lib/modules/$(shell uname -r)/build
make -C $(KERNELDIR) M=$(shell pwd)
make -C $(KERNELDIR) M=$(shell pwd) clean
命令:make
[boj@~/int]$ ls
built-in.o
catchirq.c
catchirq.ko
catchirq.mod.c
catchirq.mod.o
catchirq.o
modules.order
Module.symvers
查看 /proc/interrupts(前面章节已经贴出来了),获知我们想截获的网卡(eth0)是 21 号中断。通过 insmod 的 interface 和 irq 指定模块加载参数(源文件中的 module_params 指定的)
sudo insmod catchirq.ko interface=eth1 irq=21
成功插入一个内核模块:
[boj@~]$ lsmod | grep catchirq
我们看到,/proc/interrupts 的 21 号中断增加了一个中断处理程序:eth1
[boj@~/int]$ cat /proc/interrupts
IO-APIC-edge
IO-APIC-edge
IO-APIC-edge
IO-APIC-fasteoi
IO-APIC-edge
IO-APIC-edge
IO-APIC-edge
IO-APIC-fasteoi
uhci_hcd:usb5, yenta, i915
IO-APIC-fasteoi
uhci_hcd:usb4
IO-APIC-fasteoi
uhci_hcd:usb3
IO-APIC-fasteoi
eth0, eth1
IO-APIC-fasteoi
firewire_ohci
IO-APIC-fasteoi
ehci_hcd:usb1, uhci_hcd:usb2, mmc0
PCI-MSI-edge
PCI-MSI-edge
Non-maskable interrupts
Local timer interrupts
Spurious interrupts
Performance monitoring interrupts
IRQ work interrupts
Rescheduling interrupts
Function call interrupts
TLB shootdowns
Thermal event interrupts
Threshold APIC interrupts
Machine check exceptions
Machine check polls
dmesg 中可以看到大量如下形式的内核信息。这恰好是我们在源码中的 DEBUG 模式通过 printk 输出的。
// [Time] module_name: [count] Receive IRQ at jiffies
[] catchirq: [499] Receive IRQ at
[] catchirq: [500] Receive IRQ at
[] catchirq: [501] Receive IRQ at
[] catchirq: [502] Receive IRQ at
[] catchirq: [503] Receive IRQ at
[] catchirq: [504] Receive IRQ at
演示完毕,卸载内核模块:
sudo rmmod catchirq
根据 dmesg,catchirq 模块输出了最后一句话,被正常卸载。从 /proc/interrupts 看到,中断处理程序表恢复原状。
[] catchirq: [2245] Receive IRQ at
[] catchirq: [2246] Receive IRQ at
[] catchirq: [2247] Receive IRQ at
[] catchirq: IRQ is freed.
4 软中断、tasklet与工作队列
4.1 上半部与下半部
软中断、tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的“下半部”(bottom half)演变而来。下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,只剩下了前三者。
上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务。举个例子:在网络传输中,网卡接收到数据包这个事件不一定需要马上被处理,适合用下半部去实现;但是用户敲击键盘这样的事件就必须马上被响应,应该用中断实现。
两者的主要区别在于:中断不能被相同类型的中断打断,而下半部依然可以被中断打断;中断对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,有一些参考标准:
如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
如果一个任务和硬件相关,将其放在中断处理程序中执行。
如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
其他所有任务,考虑放在下半部去执行。
4.2 软中断
软中断作为下半部机制的代表,是随着 SMP 的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。特性是:
产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。
可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。
4.3 tasklet
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
多个不同类型的tasklet可以并行在多个CPU上。
软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。
一般而言,在可延迟函数上可以执行四种操作:初始化/激活/执行/屏蔽。屏蔽我们这里不再叙述,前三个则比较重要。下面将软中断和tasklet的三个步骤分别进行对比介绍。
初始化是指在可延迟函数准备就绪之前所做的所有工作。一般包括两个大步骤:首先是向内核声明这个可延迟函数,以备内核在需要的时候调用;然后就是调用相应的初始化函数,用函数指针等初始化相应的描述符。
如果是软中断则在内核初始化时进行,其描述符定义如下:
struct softirq_action
void (*action)(struct softirq_action *);
kernel/softirq.c 中的软中断描述符数组:static struct softirq_action softirq_vec[32]
前 6 个已经被内核注册使用:
tasklet 使用的 HI_SOFTIRQ
tasklet 使用的 TASKLET_SOFTIRQ
网络协议栈使用的 NET_TX_SOFTIRQ
网络协议栈使用的 NET_RX_SOFTIRQ
系统计时器
其余的软中断描述符可以由内核开发者使用。
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
例如网络子系统通过以下两个函数初始化软中断(net_tx_action/net_rx_action是两个函数):
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
当内核中产生 NET_TX_SOFTIRQ 软中断之后,就会调用 net_tx_action 这个函数。
tasklet 则可以在运行时定义,例如加载模块时。定义方式有两种:
DECLARE_TASKET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
其参数分别为描述符,需要调用的函数和此函数的参数。初始化生成的就是一个实际的描述符,假设为 my_tasklet。
激活:标记一个可延迟函数为挂起(pending)状态,表示内核可以调用这个可延迟函数。类似处于 TASK_RUNNING 状态的进程,处在这个状态的进程只是准备好了被 CPU 调度,但并不一定马上就会被调度。
软中断使用 raise_softirq() 函数激活,接收的参数就是上面初始化时用到的数组索引 nr。
tasklet 使用 tasklet_schedule() 激活,该函数接受 tasklet 的描述符作为参数,例如上面生成的 my_tasklet:
tasklet_schedule(&my_tasklet)
执行就是内核运行可延迟函数的过程,但是执行只发生在某些特定的时刻。
每个CPU上都有一个32位的掩码__softirq_pending,表明此CPU上有哪些挂起(已被激活)的软中断。此掩码可以用local_softirq_pending()宏获得。所有的挂起的软中断需要用do_softirq()函数的一个循环来处理。
对于 tasklet,软中断初始化时设置了发生 TASKLET_SOFTIRQ 或 HI_SOFTIRQ 软中断时所执行的函数:
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
tasklet_action 和 tasklet_hi_action 内部实现不同,软中断和 tasklet 因此具有了不同的特性。
4.4 工作队列
上面的可延迟函数运行在中断上下文中(如上章所述,软中断的一个检查点就是 do_IRQ 退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。
因此在 2.6 版的内核中出现了在内核态运行的工作队列(替代了 2.4 内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,以完成不同的工作。
Understanding the Linux Kernel, Third Edition
Copyright & 2012 李博杰 PB
This document is available from

我要回帖

更多关于 吉利自由舰刹车总称 的文章

 

随机推荐