文章目录
概念
中断是CPU对系统发生某个时间做出的一种反应(what
), 中断的引入是为了支持CPU和设备之间的并行操作(why
),CPU在收到中断信号以后,并不立即执行响应,而是在执行每条指令周期的最后一个时钟周期,一旦检测到中断信号有效,并且中断允许位置1的时候,CPU才在当前指令执行完以后转入中断响应周期(when
)。中断是一种C/S结构,当外设产生中断后并不是立刻传给CPU而是用中断控制器进行收集,然后分发给某个CPU进行处理并且应答(how
)。
一些关键观念:
CPU正在执行内核态的代码时被中断则堆栈不发生切换
小任务能睡眠,并可使用信号量
中断请求队列的建立是为了解决中断线的共享问题。
注册中断处理程序是为了初始化IDT
操作系统启动以后RTC时钟不再被使用, 使用的时钟是OS时钟
在中断上下文中,用自旋锁加锁
中断的下半部处理方式soft_irq,tasklet,workqueue
高精度定时器提供纳秒级的精度
内核初始化时,中断描述符表的地址存放在IDTR寄存器
1.1 中断机制与策略分离
CPU的设计独立于中断控制器设计
,操作系统只提供接口
,通过该接口调用针对具体设备的中断服务程序
。中断和中断处理就被解耦; 当你禁止,修改中断,操作系统或CPU架构都无需改变,全部由中断控制器进行处理。
1.2 中断子系统
硬件无关代码
:无论哪种CPU或中断控制器,中断处理的过程都有相同内容
CPU体系相关的中断处理
中断控制器驱动代码
普通外设驱动
使用linux内核通用的中断处理模块的API来实现自己的驱动逻辑
1.3 中断向量和描述符表
中断向量:每个中断源都被分配一个8位无符号证书中卫类型码,即中断向量;I=f(irq), x86: I = 32 + irq
中断描述符表IDT:每个中断占据一个表项,每个表项页叫做门描述符(gate descriptor), "门”的含义是当中断发生时必须先通过这些门,然后才能进入响应的处理程序,另外,中断描述符表也有中断入口地址寻址功能。 有三种类型的门:
中断门 interrupt gate
: 请求特权级DLP=0, 用户态程序访问中断门,所有处理程序都由中断门激活(关中断);
陷阱门 trap gate
: 与中断门不同是其控制权进行中断门时,维持中断标志位不变,即它不关中断;
系统门 system gate
: 给linux内核访问陷阱门
中断处理机制
2.1 中断描述表初始化
存放位置:idtr
内核代码:idt_descr, header_32.s
struct desc_ptr idt_descr = { 256 * 16, (unsigned long) idt_table };
第一个变量是中断描述表的数量,另外一个中断描述表的位置 ,
初始化流程:可编程控制器将中断描述表的起始地址装入到IDTR寄存器中,然后开始初始化每一个页表项。在启动阶段,实模式由BIOS使用,启动内核后会被移到另外区域,进行保护模式预初始化。
相关代码
trap_init():
set_trap_gate()
set_system_gate():
set_intr_gate()
上述关于中断门的设置: 必须跳过用于系统调用的向量0x80, 中断处理程序的入口是一个数组interrupt[], 数组中的每个元素都是指向ISR的指针,该处理列程都是内核中的代码段(段地址是存放在全局描述符表GDT中)
for (i = 0; i < NR_IRQ; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
2.2 中断处理过程
- GDT获取段基址+偏移量 得到处理程序。
- 特权级变化:比如用户态的SEGV中断会切换到内核态执行
2.3 中断请求队列
原因:由于多个中断设备公用一条中断线,linux为每条线设置一个中断请求队列。相关的函数调用如下图:
IRQn_interrupt()
: 共享同一条中断线的所有请求有一个总的中断处理程序
do_IRQ(n)
: 每个中断请求都有自己单独的中断服务例程, interrupt server Routine1,
概要描述:中断向量I -> 从IDT找到相对应中断门 --> 中断处理程序入口地址 --> 判断是否要进行堆栈切换(IRQn_interrupt()) – > 调用do_IRQ对所接收的中断进行应答并禁止这条中断线上中断发生 --> 调用hander_IRQ_event来运行对应中断服务例程 – > 中断返回(ret_from_intr)(内核空间调用RESTORE_ALL;
2.4 IRQ数据结构
- 记录某IRQ号对应的流控处理函数,中断控制器, IRQ的自身属性等
- 中断线共享数据结构:irqaction
hander: 中断服务例程, flags:中断线与设备之间的关系, dev_id: 主次设备号,next: 下个列表
- request_irq()/free_irq(): 注册或注销中断服务例程
\arch\i386\kernel\entry.S
获取入口地址
common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr
arch\i386\kernel\irq.c do_IRQ
执行与一个中断相关的中断服务例程
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
irq_enter(); //中断嵌套计数
// 应该在前面还包含一些用户态-内核态的切换
for (;;) {
irqreturn_t action_ret;
spin_unlock(&desc->lock);
action_ret = handle_IRQ_event(irq, ®s, action);
spin_lock(&desc->lock);
if (!noirqdebug)
note_interrupt(irq, desc, action_ret);
if (likely(!(desc->status & IRQ_PENDING)))
break;
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;
}
handle_IRQ_event
执行中断例程
/*
* This should really return information about whether
* we should do bottom half handling etc. Right now we
* end up _always_ checking the bottom half, which is a
* waste of time and is not what some drivers would
* prefer.
*/
int handle_IRQ_event(unsigned int irq,
struct pt_regs *regs, struct irqaction *action)
{
int status = 1; /* Force the "do bottom halves" bit */
int retval = 0;
// 激活中断
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
调用每个action
do {
status |= action->flags;
retval |= action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
// 禁止中断
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
local_irq_disable();
return retval;
}
out:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
desc->handler->end(irq);
spin_unlock(&desc->lock);
irq_exit();
ret_from_intr
从异常、中断,系统调用返回
- 内核调度(shedule)与中断/异常/系统(统称中断)调用的关系如何?
中断/异常(包括系统调用)返回时,是进行调度(schedule)的重要时机点,其中,中断(时钟中断)返回时调度依赖的最主要的时机点,时钟中断处理函数中不会直接进行调度,只是根据相应的调度算法,决定是否需要调度,以及调度的next task,如果需要调度,则设置NEED_RESCHED标记。调度(schedule)的实际执行是在中断返回的时候,检查NEED_RESCHED标记,如果设置则进行调度
- 信号处理与中断的关系如何?
信号处理是在当前进程从内核态返回用户态时进行的,在发生中断、异常(包括系统调用)、或fork时,都有可能从内核态返回用户态,都是处理信号的时机。注意:只有current进程的信号才能在此时得到处理。其它非正在运行的进程的信号无法处理
- 内核抢占与中断关系如何?
中断/异常发生在内核态时,也就是说中断/异常返回时,需要返回内核态,走resume_kernel流程,此时,如果内核支持内核抢占,则此时是个关键的调度时机点,如果内核不支持抢占,则不会发生调度。也就是说:如果当前进程上下文处于内核态,当不支持内核抢占时,则无论进程的优先级和时间片如何,都是不能发生调度的,只能在返回用户态时,才能发生调度。从这点可以看出,当不支持内核抢占时,Linux的实时性很差(开启内核抢占后稍好),当在内核态(中断、软中断、其它内核流程)执行时间或流程太长时,可能导致进程调度饥饿,极端情况下,当在内核态发生死锁时,会直接导致整个系统因无法调度而死锁,当然针对这种情况(softlockup),内核提供了专门的watchdog机制来检测
- 内核线程的调度有何特别之处?中断返回时,内核线程会发生调度么?
关于内核线程的调度,跟普通线程相比,从原理和机制上看,没有特别之处。但关键的不同在于:内核线程始终运行在内核态,当没有开启内核抢占时,设想当一个内核线程被中断/异常打断,此时从中断/异常返回时会发生调度吗?答案是不会,因为当前进程的上下文处于内核态,在没有开启内核抢占的情况下,是不会发生调度行为的,除非该内核线程主动调用schedule()释放CPU控制权。也就是说,内核线程触发主动调用schedule,否则会一直占用CPU。所以在编写内核线程时,需要在相关任务处理结束后,主动调用schedule,这点需要注意
中断返回和异常返回的流程基本一致,差别主要在于异常返回时,需要先关一次中断。因为Linux实现中,异常使用的是陷阱门,通过时不会自动关中断;而中断使用的是中断门,通过时会自动关中断
2.6 代码分析中断返回
中断、异常(包括系统调用)、fork返回时,会分别跳转到entry_32.S汇编代码中的ret_from_intr、ret_from_exception、ret_from_fork标号处执行。相应代码分析如下:
1、中断返回(ret_from_intr)
1)主流程
/*从中断返回*/
ret_from_intr:
/*将当前进程的thread_info结构体的指针存入%ebp帧寄存器中*/
GET_THREAD_INFO(%ebp)
#ifdef CONFIG_VM86
/* 取中断之前寄存器EFLAGS的高16位和段寄存器CS的内容构成的32位长整数放入eax中,其目的是检验:
* 1.中断之前CPU是否运行于VM86模式
* (EFLAGS的高16位中的第二位用来标识CPU运行在VM86模式下)
* 2.中断之前CPU运行于用户空间还是系统空间
*(CS的低两位代表着中断发生时CPU的运行级别CPL。若是CS的低两位为1,表示中断发生于用户空间,)
*/
movl PT_EFLAGS(%esp), %eax # mix EFLAGS and CS
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax
#else
/*
* We can be coming here from child spawned by kernel_thread().
*/
movl PT_CS(%esp), %eax
andl $SEGMENT_RPL_MASK, %eax
#endif
// 判断是否返回用户态或者v8086模式,如果不是,则转入resume_kernel,否则进入resume_userspace
cmpl $USER_RPL, %eax
jb resume_kernel # not returning to v8086 or userspace
2)返回用户态
// 如果是返回用户态
ENTRY(resume_userspace)
LOCKDEP_SYS_EXIT
/*
* 前面已经关了中断了,这次再关的原因是,还有其它流程会自己跳转
* 到这里,比如system_call
*/
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don
3)调度和信号处理:
work_pending:
# 返回用户态时,只需要判断need_resched是否置位,不需要判断preempt_count
# 如果need_resched置位,则发生调度,否则跳转到work_notifysig
testb $_TIF_NEED_RESCHED, %cl
# 进行信号处理
jz work_notifysig
work_resched:
# 需要调度,调用schedule函数
call schedule
# 调度返回,注意:到这里已经是新的进程上下文了,后面有机会处理信号
LOCKDEP_SYS_EXIT
# 关中断
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
# 关闭trace irq功能
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
# 再次确认是否还有其它事情处理
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
# 如果没有,则恢复上下文
jz restore_all
# 如果有,再次检查是否需要调度,如果需要,则再次跳转到work_resched进行重新调度
testb $_TIF_NEED_RESCHED, %cl
jnz work_resched # 如果不需要调度,则继续到work_notifysig,进行信号处理了,也就是说如果这里发生调度,也是有机会处理信号的。
work_notifysig: # deal with pending signals and
# notify-resume requests
#ifdef CONFIG_VM86
testl $X86_EFLAGS_VM, PT_EFLAGS(%esp)
movl %esp, %eax
jne work_notifysig_v86 # returning to kernel-space or
# vm86-space
1:
#else
movl %esp, %eax
#endif
# 开trace
TRACE_IRQS_ON
# 开中断(前面关了),意味着信号处理是开中断执行的,还是中断优先级高
ENABLE_INTERRUPTS(CLBR_NONE)
# 再次判断CS低两位,当为1时,表示中断/异常之前处于用户态,否则为内核态,据此再次确认是否需要返回内核态
movb PT_CS(%esp), %bl
andb $SEGMENT_RPL_MASK, %bl
cmpb $USER_RPL, %bl
# 返回内核态
jb resume_kernel
# edx清零
xorl %edx, %edx
# 调用C函数,其中进行通知链即信号的处理
call do_notify_resume
# 信号处理完后,重新跳转到resume_userspace,此时如果没有新的信号产生,则会在前面就通过restore_all恢复了,不会再到这里了
jmp resume_userspace
#ifdef CONFIG_VM86
ALIGN
work_notifysig_v86:
pushl_cfi %ecx # save ti_flags for do_notify_resume
call save_v86_state # %eax contains pt_regs pointer
popl_cfi %ecx
movl %eax, %esp
jmp 1b
#endif
END(work_pending)
4)返回内核态
/*如果配置了内核抢占*/
#ifdef CONFIG_PREEMPT
ENTRY(resume_kernel)
/*
* 前面已经关了中断了,这次再关的原因是,还有其它流程会自己跳转
* 到这里,比如system_call
*/
DISABLE_INTERRUPTS(CLBR_ANY)
/*判断是否可以抢占*/
cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?
/*抢占计数非0,不能抢占,则不产生调度,直接恢复上下文*/
jnz restore_all
/*可以抢占,则需要调度*/
need_resched:
/*判断need_resched是否被设置*/
movl TI_flags(%ebp), %ecx # need_resched set ?
testb $_TIF_NEED_RESCHED, %cl
/*没设置need_resched,则不需要调度,直接恢复上下文*/
jz restore_all
/*
*判断发生中断时(因为PT_EFLAGS(%esp)中保存的是进入中断时的EFLAGS值,这是由CPU硬件自动压栈的,中断走中断门,会自动关中断,异常走陷阱门,不自动关中断)是否关中断了,
*如果关了,表示是异常上下文(Fixme:应该是中断吧),则直接恢复上下文。
*/
testl $X86_EFLAGS_IF,PT_EFLAGS(%esp) # interrupts off (exception path) ?
jz restore_all
/*如果没关中断,表示为中断上下文?则调用preempt_schedule_irq,进行调度*/
call preempt_schedule_irq
jmp need_resched
END(resume_kernel)
2、异常返回(ret_from_exception)
异常返回跟中断返回流程基本一致,差别主要在于异常返回时,需要先关一次中断。因为Linux实现中,异常使用的是陷阱门,通过时不会自动关中断;而中断使用的是中断门,通过时会自动关中断。
/*从异常返回*/
ret_from_exception:
/*
* 这里为什么要关中断?而从中断返回不需要? 因为异常走的是陷阱门,
* 默认是不关中断执行的,而中断走的是中断门,默认是关中断执行的?
*
*/
/*关中断*/
preempt_stop(CLBR_ANY)
/*从中断返回*/
ret_from_intr:
...
3、fork返回( ret_from_fork)
fork返回的后半部分处理跟异常/中断返回一致,前面一部分有单独的处理:包括调用schedule_tail和跳转syscall_exit进行相关处理
点击(此处)折叠或打开
#fork返回,单独处理
ENTRY(ret_from_fork)
CFI_STARTPROC
pushl_cfi %eax
#进行调度收尾处理,包括回收DEAD(X)状态的进程
call schedule_tail
#获取thread_info放入ebp中
GET_THREAD_INFO(%ebp)
popl_cfi %eax
#重设kernel eflags
pushl_cfi $0x0202 # Reset kernel eflags
popfl_cfi
#跳转到syscall_exit进行系统调用退出相关的处理。
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)
中断下半部分
由于系统越来越复杂,需要处理的中断也越来越多,因此, 系统会尽量将中断处理推后,这些推后的处理就是中断的下半部(可中断);
linux提供几种机制:软件中断机制,
3.1 软件中断机制
- soft_irq: 要求比较紧急的场合,在中断上下文中执行,用softirq_action;
- tasklet:在普通的驱动程序中用的相对较多,tasklet是在中断上下文执行, 如下图,小任务是在软中断的基础上实现的。主要使用tasklet进行
- work queue:区别tasklet, 是在进程上下文中执行,因此,可以执行睡眠操作;本质是线程; 相关接口:work_struct, cpuwrod_struct;
do_IRQ
调用结束后执行irq_exit
后可以执行软中断,从下面也可以看出,软中断的处理如果当前cpu并没有内核抢占,就会直接直接执行软件中断,然后进行中断返回,当前进程继续执行,如果存在内核抢占,就会通过任务调度的方式切换出去执行更高的优先级
#define irq_exit() \
do { \
// 当抢占计数器不允许发生内核抢占(count != 0)
preempt_count() -= IRQ_EXIT_OFFSET; \
// 存在
if (!in_interrupt() && softirq_pending(smp_processor_id())) \
do_softirq(); \
preempt_enable_no_resched(); \
} while (0)
do_softirq
处理软件中断,
asmlinkage void do_softirq(void)
{
int max_restart = MAX_SOFTIRQ_RESTART;
__u32 pending;
unsigned long flags;
// 是否产生中断
if (in_interrupt())
return;
//禁用本地cpu中断
local_irq_save(flags);
// 检测是否激活软中断
pending = local_softirq_pending();
if (pending) {
struct softirq_action *h;
// 增加软中断计数器的值
local_bh_disable();
restart:
/* Reset the pending bitmask before enabling irqs */
local_softirq_pending() = 0;
// 激活本地中断, 即在执行软中断是可以睡眠或可以被调度
local_irq_enable();
h = softirq_vec; //数组类型softirq_action 数组
do {
if (pending & 1)
h->action(h); // 执行软中断
h++;
pending >>= 1;
} while (pending);
// 禁用本地中断
local_irq_disable();
// 如果还有更多的软中断,唤起内核线程版本执行软中断
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
__local_bh_enable();
}
local_irq_restore(flags);
}```
* 内核定时器版本还可以通过定时器,调用opensoftirq, raisesoftirq来执行软中断,对应的调用关系
```c
// opensoftirq()在timer定时器初始化时被调用
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
run_local_timers
raise_softirq(TIMER_SOFTIRQ); //调用时钟软降
raise_softirq_irqoff(nr);
wakeup_softirqd(); // do exit也调用词函数, ksoftirqd也在这里调度
wake_up_process(tsk);
try_to_wake_up(p, TASK_STOPPED | TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE, 0);
resched_task(rq->curr); //第二次调用时,一定是通过任务调度方式进行
3.2 tasklet
tasklet是在soft_irq上实现的(HI_SOFTIRQ,TASKLET_SOFTIRQ),是I/O驱动程序实现可延迟函数的首选方法。
假设我们在写一个设备驱动程序,且想使用tasklet, 那具体步骤
* 分配一个tasklet_struct的数据结果,并调用tasklet_init
初始化, 其中找真正执行tasklet的函数为tasklet_action
, 本质就是遍历列表执行的过程;
void __init softirq_init(void)
{
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
tasklet_cpu_notify(&tasklet_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id());
register_cpu_notifier(&tasklet_nb);
}
* 然后调用`tasklet_shedule`进行调度
其中关于tasklet_shedule的处理如下:
void tasklet_hi_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
// 将t添加到链表头
t->next = __get_cpu_var(tasklet_hi_vec).list;
//tasklet_hi_vec 表示高优先级数组,元素为tasklet_head {tasklet_struct *list;}
__get_cpu_var(tasklet_hi_vec).list = t;
raise_softirq_irqoff(HI_SOFTIRQ); //执行以下软中断
local_irq_restore(flags);
}
参考资料
- linux内核之旅-中断趣味谈
- linux深入理解的第四章