linux内核5-中断

概念

中断是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内核访问陷阱门

扫描二维码关注公众号,回复: 12419050 查看本文章

在这里插入图片描述

中断处理机制

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 中断处理过程

  1. GDT获取段基址+偏移量 得到处理程序。
  2. 特权级变化:比如用户态的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数据结构

  1. 记录某IRQ号对应的流控处理函数,中断控制器, IRQ的自身属性等
  2. 中断线共享数据结构:irqaction hander: 中断服务例程, flags:中断线与设备之间的关系, dev_id: 主次设备号,next: 下个列表
  3. 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, &regs, 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 从异常、中断,系统调用返回
  1. 内核调度(shedule)与中断/异常/系统(统称中断)调用的关系如何?
    中断/异常(包括系统调用)返回时,是进行调度(schedule)的重要时机点,其中,中断(时钟中断)返回时调度依赖的最主要的时机点,时钟中断处理函数中不会直接进行调度,只是根据相应的调度算法,决定是否需要调度,以及调度的next task,如果需要调度,则设置NEED_RESCHED标记。调度(schedule)的实际执行是在中断返回的时候,检查NEED_RESCHED标记,如果设置则进行调度
  2. 信号处理与中断的关系如何?
    信号处理是在当前进程从内核态返回用户态时进行的,在发生中断、异常(包括系统调用)、或fork时,都有可能从内核态返回用户态,都是处理信号的时机。注意:只有current进程的信号才能在此时得到处理。其它非正在运行的进程的信号无法处理
  3. 内核抢占与中断关系如何?
    中断/异常发生在内核态时,也就是说中断/异常返回时,需要返回内核态,走resume_kernel流程,此时,如果内核支持内核抢占,则此时是个关键的调度时机点,如果内核不支持抢占,则不会发生调度。也就是说:如果当前进程上下文处于内核态,当不支持内核抢占时,则无论进程的优先级和时间片如何,都是不能发生调度的,只能在返回用户态时,才能发生调度。从这点可以看出,当不支持内核抢占时,Linux的实时性很差(开启内核抢占后稍好),当在内核态(中断、软中断、其它内核流程)执行时间或流程太长时,可能导致进程调度饥饿,极端情况下,当在内核态发生死锁时,会直接导致整个系统因无法调度而死锁,当然针对这种情况(softlockup),内核提供了专门的watchdog机制来检测
  4. 内核线程的调度有何特别之处?中断返回时,内核线程会发生调度么?
    关于内核线程的调度,跟普通线程相比,从原理和机制上看,没有特别之处。但关键的不同在于:内核线程始终运行在内核态,当没有开启内核抢占时,设想当一个内核线程被中断/异常打断,此时从中断/异常返回时会发生调度吗?答案是不会,因为当前进程的上下文处于内核态,在没有开启内核抢占的情况下,是不会发生调度行为的,除非该内核线程主动调用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深入理解的第四章

猜你喜欢

转载自blog.csdn.net/CPriLuke/article/details/112758647
今日推荐