15 张飞SMP结构中的中断机制和进程调度
Download
Report
Transcript 15 张飞SMP结构中的中断机制和进程调度
Linux源代码阅读
SMP结构中的中断机制和进程调度
张飞
概要
SMP结构中的中断机制
分布式中断处理
中断初始化
处理器间中断IPI
SMP结构中的进程调度
分布式中断处理
APIC简介
SMP结构中的中断控制硬件机构
全局APIC
本地APIC
高级可编程中断控制器APIC
为了充分利用smp体系结构的并行性,要求动态分配中断请求,也
就是说可以向任意cpu发出中断请求.
传统的i386处理器都采用8259A中断控制器,其作用是提供多个
外部中断源与单一cpu之间的连接.如果在SMP结构中还是采用
8259A中断控制器,那就只能静态的把所有的外部中断源划分成
若干组,分别把每一组都连接到一个8259A,而8259A则与cpu有
一对一的连接.这样就达不到动态分配中断请求的目的.
为了更好的支持smp结构,从Pentium开始,Intel设计了一种更为
通用的中断控制器,称为高级可编程中断控制器APIC (Advanced
Programmable Interrupt Controller).
SMP结构中的中断控制硬件机构
cpu 0
cpu 1
cpu n
本地 APIC
本地 APIC
本地 APIC
本地中断请求
本地中断请求
本地中断请求
ICC(中断控制器通信)总线
全局APIC
外部中断请求
分布式中断处理硬件机制概述
两种APIC:本地APIC和全局APIC,通过中断控
制器通信(Interrupt Controller
Communication,ICC)总线相连.
本地APIC集成在cpu内部,通过内部APIC可以
向其他cpu发送中断请求.
全局APIC负责把来自外部设备的中断请求提
交和分配给系统中各个cpu的任务.
全局APIC
组成
全局APIC由一组IRQ线路,一个有24个表项的中断
重定向表(Interrupt Redirection Table),一个可编
程寄存器和一个用来发送和接受经过ICC总线的
APIC消息的消息单元组成.
和8259A的IRQ引脚不同,中断优先级和引脚号无
关,重定向表中的每个表项都可以被单独编程来说
明中断向量和优先级,目标处理器以及如何选定处
理器.重定向表中的消息用来把外部IRQ信号转换
成通过ICC总线发往一个或多个本地APIC单元的消
息.
全局APIC
工作模式
固定模式
把IRQ信号发送到相应的重定向表表项所列出的本地
APIC上.
最低优先级模式
把IRQ信号发送到正在执行优先级最低的进程的处理器
的本地APIC上.所有的本地APIC都有一个可编程任务优
先级寄存器(task priority register),它包含了当前正在运
行的进程的优先级.在每次任务切换时这个寄存器的值必
须由内核进行修改.
本地APIC
组成
每个本地APIC都有几个32位的寄存器,一个内部时钟,一个定
时器设备,240个不同的中断向量(从0x20~0xff,0~0x1f用于
cpu本身的陷阱)以及两条为局部中断保留的IRQ线路,这两条
线路用于重启系统.
本地APIC的一个重要功能是实现处理器间中断IPI
当一个cpu想要向其他cpu发送中断时,将中断向量和目标处
理器的本地apic标志符保存到自己本地apic的中断命令寄存
器中,然后通过ICC总线向目标处理器的本地apic发送一条消
息, 目标处理器的本地apic就向自己的cpu发出相应的中断.
SMP结构中的中断控制硬件机构
cpu 0
cpu 1
cpu n
本地 APIC
本地 APIC
本地 APIC
本地中断请求
本地中断请求
本地中断请求
ICC(中断控制器通信)总线
全局APIC
外部中断请求
概要
SMP结构中的中断机制
分布式中断处理
中断初始化
处理器间中断IPI
SMP结构中的进程调度
中断初始化
smp相关的几个主要中断向量
设置中断门
中断响应程序的建立
相关中断处理程序代码
smp_reschedule_interrupt()
smp_call_function_interrupt()
smp相关的几个主要中断向量
smp结构专用的几个IRQ向量定义在include/asmi386/apic.h中
#define
#define
#define
#define
#define
#define
SPURIOUS_APIC_VECTOR
ERROR_APIC_VECTOR
INVALIDATE_TLB_VECTOR
RESCHEDULE_VECTOR
CALL_FUNCTION_VECTOR
LOCAL_TIMER_VECTOR
0xff
0xfe
0xfd
0xfc
0xfb
0xef
其他不常用的向量合并到CALL_FUNCTION_VECTOR
中以节省向量空间,使用比较频繁的是TLB、
reschedule和local APIC中断向量.
设置中断门
void __init init_IRQ(void)
{
…
for (i = 0; i < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
#ifdef CONFIG_SMP
set_intr_gate(FIRST_DEVICE_VECTOR, interrupt[0]);
set_intr_gate(RESCHEDULE_VECTOR, reschedule_interrupt);
set_intr_gate(INVALIDATE_TLB_VECTOR, invalidate_interrupt);
set_intr_gate(CALL_FUNCTION_VECTOR, call_function_interrupt);
#endif
…
}
设置中断门
for循环设置了除SYSCALL_VECTOR外从FIRST_EXTERNAL_VECTOR开始
的NR_IRQS个中断门向量,在smp结构中覆盖了其中的4个,其他的中断向
量基本上没有什么变化,还与采用8259A时大致相同,不同的是现在由全局
APIC取代8259A将外部中断请求送达各个cpu.
为中断向量FIRST_DEVICE_VECTOR设置的中断响应入口程序是
interrupt[0].
中断向量RESCHEDULE_VECTOR的中断响应入口程序是
reschedule_interrupt.
中断向量INVALIDATE_TLB_VECTOR的中断响应入口程序设置为
invalidate_interrupt.
中断向量CALL_FUNCTION_VECTOR对应call_function_interrupt程序.
中断响应程序的建立
上述几个主要中断向量的实际中断处理程序由
下面一组宏语句建立:
#ifdef CONFIG_SMP
BUILD_SMP_INTERRUPT(reschedule_interrupt,RESCHEDULE_VECTOR)
BUILD_SMP_INTERRUPT(invalidate_interrupt,INVALIDATE_TLB_VECTOR)
BUILD_SMP_INTERRUPT(call_function_interrupt,CALL_FUNCTION_VECTOR)
#endif
中断响应程序的建立
其中BUILD_SMP_INTERRUPT宏定义如下:
#define BUILD_SMP_INTERRUPT(x,v) XBUILD_SMP_INTERRUPT(x,v)
#define XBUILD_SMP_INTERRUPT(x,v)\
asmlinkage void x(void); \
asmlinkage void call_##x(void); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(x) ":\n\t" \
"pushl $"#v"\n\t" \
SAVE_ALL \
SYMBOL_NAME_STR(call_##x)":\n\t" \
"call "SYMBOL_NAME_STR(smp_##x)"\n\t" \
"jmp ret_from_intr\n");
中断响应程序的建立
BUILD_SMP_INTERRUPT(reschedule_interrupt,RES
CHEDULE_VECTOR)宏展开如下:
asmlinkage void reschedule_interrupt(void);\
asmlinkage void call_reschedule_interrupt(void);\
__asm__(
reschedule_interrupt:
pushl $RESCHEDULE_VECTOR #中断号存进核心栈
SAVE_ALL
#保存各个寄存器值
call_smp_reschedule_interrupt:
call smp_reschedule_interrupt
jmp ret_from_intr
)
中断响应程序的建立
至此,结合前面说的中断门的初始化,smp专有的主要
中断向量及其响应机制建立起来:
当发生RESCHEDULE_VECTOR中断时,响应程序的入口是
reschedule_interrupt(),实际负责中断处理程序的函数为
smp_reschedule_interrupt().
同理,与INVALIDATE_TLB_VECTOR相对应的入口程序是
invalidate_interrupt(),实际中断处理程序的则是
smp_invalidate_interrupt();
与CALL_FUNCTION_VECTOR相对应的入口程序是
call_function_interrupt(),而实际处理中断请求的是
smp_call_function_interrupt().
具体中断处理程序
smp_reschedule_interrupt()
asmlinkage void smp_reschedule_interrupt(void)
{
ack_APIC_irq();
//发送中断请求确认
}
extern inline void ack_APIC_irq(void)
{
apic_write_around(APIC_EOI, 0); /*向本地APIC的控制
寄存器写入0,表示已经收到了中断请求*/
}
具体中断处理程序
smp_reschedule_interrupt()
该函数应其他cpu的请求进行一次进程调度,但是
从程序代码上看,该函数仅仅发回一个中断确认.
实际上,对中断请求的服务隐藏在内核对中断处
理的公共部分:内核在针对特定中断请求的服务
完成后都要检查(本cpu)是否需要进行进程调度,
这正是smp_reschedule_interrupt()的所要达到
的目的—引发目标CPU一次中断,以便检查是否需
要重新调度.
具体中断处理程序
smp_call_function_interrupt()
该函数响应CALL_FUNCTION_VECTOR,用于请求目标
CPU执行一个指定的函数.发送者先设置好一个全局的
call_data_struct数据结构,然后向目标CPU发出请求,目
标CPU收到中断向量后,调用该函数进行处理
static spinlock_t call_lock = SPIN_LOCK_UNLOCKED;
struct call_data_struct {
void (*func) (void *info);
//指向要求对方执行的函数
void *info;
//函数参数
atomic_t started;
atomic_t finished;
int wait;
};
static struct call_data_struct * call_data;
具体中断处理程序
smp_call_function_interrupt()
asmlinkage void smp_call_function_interrupt(void)
{
void (*func) (void *info) = call_data->func; //取出函数指针
void *info = call_data->info;
int wait = call_data->wait;
}
ack_APIC_irq();
//先发回中断确认
atomic_inc(&call_data->started);
(*func)(info);
//调用指定函数
if (wait)
//指定动作完成后,wait设置为1
atomic_inc(&call_data->finished);
具体中断处理程序
smp_call_function_interrupt()
当然,一般的函数是没有必要请其他CPU来执行的,
因为系统中所有的CPU都可共享同样的代码和数
据.之所以要请求其他CPU执行,是因为某个函数必
须由特定的CPU来执行.例如,pentium处理器有一
条cpuid指令,通过这条指令可以查询本CPU的型号、
版本以及是否支持一些特殊的功能、当前的功能
设置等等信息.但是这条指令只能由具体的CPU本
身执行,而不能由别的CPU代替.这样,如果要知道
系统中某一个CPU的有关情况,就只能请求该CPU
来执行cpuid指令.
概述
SMP结构中的中断机制
分布式中断处理
中断初始化
处理器间中断IPI
SMP结构中的进程调度
处理器间中断IPI
IPI概述
IPI中断向量
IPI中断请求函数
IPI概述
IPI (Interprocessor Interrupt)称为处理机间中断.实
际上,前面介绍的RESCHEDULE_VECTOR、
INVALIDATE_TLB_VECTOR以及
CALL_FUNCTION_VECTOR都属于处理器间中断IPI.
从前一部分可以看出,与单cpu系统的中断处理机制相
比,smp系统仅仅作了少量修改,并且这些修改又大部
分集中在处理器间中断IPI这一部分,也许我们可以从
另一个角度来看待smp系统的中断机制-smp中断机
制和单CPU系统中断机制在某种程度上是等价的.
SMP结构与单CPU系统中断机制的类比
单个“超级”处理
器
CPU 0
CPU 1
CPU n
本地
APIC
本地
APIC
本地
APIC
CPU
8259A
全局APIC
外部中断
外部中断
IPI中断向量
本地APIC可以识别6种消息,这些消息是由接收消息的CPU作为不同
中断向量来解释的.(之所以不同于一般中断向量,可能是因为这些中
断向量并不对应着实际的中断请求引脚).
SPURIOUS_APIC_VECTOR (0xff)
入口程序spurious_interrupt(),实际中断处理程序smp_spurious_interrupt().
ERROR_APIC_VECTOR (0xfe)
出错计数器溢出中断,入口程序error_interrupt(),中断处理程序
smp_error_interrupt().
INVALIDATE_TLB_VECTOR (0xfd)
RESCHEDULE_VECTOR (0xfc)
CALL_FUNCTION_VECTOR (0xfb)
LOCAL_TIMER_VECTOR (0xef)
I/O APIC把定时中断自动发给所有的CPU.相应的入口程序是
apic_timer_interrupt(),实际中断服务程序为smp_apic_timer_interrupt().
中断请求函数
中断请求函数用于向目标CPU发送指定的IPI请求向量
static inline void send_IPI_allbutself(int vector)
向除自己以外的所有CPU发送一个IPI.
static inline void send_IPI_all(int vector)
向所有的CPU(包括自己)发送一个IPI.
void send_IPI_self(int vector)
向自己发送一个IPI.
static inline void send_IPI_mask(int mask, int vector)
向由mask指定的一个或多个CPU发送一个IPI.
这几个函数功能大同小异,实现代码也都比较简单.其中以
send_IPI_mask函数最为灵活,因而也稍为复杂一些.
send_IPI_mask函数
static inline void send_IPI_mask(int mask, int vector)
{
unsigned long cfg;
unsigned long flags;
__save_flags(flags);
__cli();
//中断前状态信息保存在flags中
//关中断
static inline int __prepare_ICR2 (unsigned int mask)
apic_wait_icr_idle();
{return//确认或等待APIC_ICR处于空闲状态
SET_APIC_DEST_FIELD(mask);}
/*ICR2、ICR是本地APIC的2个控制寄存器*/
inline int __prepare_ICR (unsigned
int shortcut, int vector)
cfg static
= __prepare_ICR2(mask);
//根据中断请求的目标CPU准备将要
{return APIC_DM_FIXED | shortcutcfg)
| vector//写入寄存器APIC_ICR2的值
| APIC_DEST_LOGICAL;}
apic_write_around(APIC_ICR2,
cfg = __prepare_ICR(0, vector); //根据要发送的中断向量准备将要
apic_write_around(APIC_ICR, cfg); //写入寄存器APIC_ICR的值
/*对APIC_ICR的写入操作引发并完成将中断向量发送至目标cpu的工作*/
__restore_flags(flags);
//恢复标志
}
其他相关发送函数
再看一个较为简单的函数send_IPI_all
static inline void send_IPI_all(int vector)
{
__send_IPI_shortcut(APIC_DEST_ALLINC, vector);
}
static inline void __send_IPI_shortcut(unsigned int shortcut, int vector)
{
unsigned int cfg;
apic_wait_icr_idle();
//确认或等待ICR空闲
cfg = __prepare_ICR(shortcut, vector);.
//对ICR进行编程
apic_write_around(APIC_ICR, cfg);
}/*与send_IPI_reschedule相比,少了耗时的开/关中断动作,并且只对APIC编程一次*/
除send_IPI_mask()之外的几个发送函数实际上都是简单的调用
__send_IPI_shortcut函数,只不过传给这个函数的参数不同而已.
在这些函数基础上,还定义了一些功能更为专一、明确的函数.比如当一个CPU需要
另一个CPU进行一次进程调度时,可以调用下面的函数:
void smp_send_reschedule(int cpu)
{
send_IPI_mask(1 << cpu, RESCHEDULE_VECTOR);
}
概要
SMP结构中的中断机制
分布式中断处理
中断初始化
处理器间中断IPI
SMP结构中的进程调度
相关数据结构
单CPU系统中,任一时刻只有当前进程是在运行中的,但在SMP系统中同
时有好几个进程在运行,因此在task_struct结构中引入两个字段:一个是
has_cpu,为1时说明进程正在运行,为0则表示不在运行;另一个字段是
processor,当has_cpu为1时指出进程是在哪一个cpu上运行.
一个相关的宏操作can_schedule():
#ifdef CONFIG_SMP
#define idle_task(cpu) (init_tasks[cpu_number_map(cpu)])
#define can_schedule(p,cpu) ((!(p)->has_cpu) && \
((p)->cpus_allowed & (1 << cpu)))
#else
#define idle_task(cpu) (&init_task)
#define can_schedule(p,cpu) (1)
#endif
调度函数Schedule()中相关代码
当一个CPU通过schedule()从系统的就绪队列中挑选了一个进程作为运
行的下一个进程next,即从当前进程prev切换到这个进程时,就将其
task_struct结构中的has_cpu字段设置成1,并将processor设置成该CPU
的逻辑编号.
asmlinkage void schedule(void)
{
…
this_cpu = prev->processor;
//取得当前CPU逻辑号
…
#ifdef CONFIG_SMP
next->has_cpu = 1;
//将要上台进程的has_cpu设置为1
next->processor = this_cpu;
//设置processor字段
#endif
spin_unlock_irq(&runqueue_lock);
if (prev == next)
goto same_process;
…
switch_to(prev, next, prev);
//从prev切换到next进程
__schedule_tail(prev);
//处理下台的prev进程,看能否在其他cpu上重新调度
…
}
static inline void __schedule_tail(struct task_struct *prev)函数
开始
保存先前进程调度策略
并清零prev->policy中
SCHED_YIELD标志位
将prev的has_cpu
字段清零
N
下台前运行态?
policy = prev->policy;
prev->policy = policy & ~SCHED_YIELD;
wmb();
task_lock(prev);
prev->has_cpu = 0;
mb();
if (prev->state == TASK_RUNNING)
Y
“空转”进程
或自动下台?
N
之前是
运行态?
Y
Y
N
尝试重新调度prev进程
if ((prev == idle_task(smp_processor_id())) ||
(policy & SCHED_YIELD))
if (prev->state == TASK_RUNNING)
返回
reschedule_idle(prev);
static void reschedule_idle(struct task_struct * p)函数
static void reschedule_idle(struct task_struct * p)
{
#ifdef CONFIG_SMP
int this_cpu = smp_processor_id();
//取得当前CPU逻辑号
struct task_struct *tsk, *target_tsk;
int cpu, best_cpu, i, max_prio;
cycles_t oldest_idle;
best_cpu = p->processor;
//最好可以在原来的CPU上重新运行
if (can_schedule(p, best_cpu)) {
//可以在原来的CPU上调度么
tsk = idle_task(best_cpu);
//取得该CPU的”空转”进
程
if (cpu_curr(best_cpu) == tsk) {
//判断目的CPU是否空闲
int need_resched;
send_now_idle:
need_resched = tsk->need_resched;
//保存原先的need_resched
tsk->need_resched = 1;
//设置need_resched标志
/*如果need_resched为-1,则没有必要发送IPI,idle进程会自动监视该变量*/
if ((best_cpu != this_cpu) && !need_resched)
//需要发送IPI?
smp_send_reschedule(best_cpu);
//发送RESCHEDULE_VECTOR
return;
}
(接上)
oldest_idle = (cycles_t) -1;
target_tsk = NULL;
max_prio = 1;
/*查找所有可用CPU,看看能否重新调度进程p*/
for (i = 0; i < smp_num_cpus; i++) { //smp_num_cpus:系统中活动CPU个数
cpu = cpu_logical_map(i);
//取得该CPU的逻辑号
if (!can_schedule(p, cpu))
continue;
tsk = cpu_curr(cpu);
if (tsk == idle_task(cpu)) { //存在空闲CPU,选择运行时间最长的进程来剥夺
if (last_schedule(cpu) < oldest_idle) {
oldest_idle = last_schedule(cpu);
target_tsk = tsk;
}
} else {if (oldest_idle == -1ULL) {
//若没有空闲CPU
int prio = preemption_goodness(tsk, p, cpu);
if (prio > max_prio) {
//寻找一个运行资格较p最低的进程来剥夺
max_prio = prio;
target_tsk = tsk;
}
}
}
}
(接上)
tsk = target_tsk;
if (tsk) {
//如果存在可以剥夺的进程
if (oldest_idle != -1ULL) {
//如果存在空闲CPU
best_cpu = tsk->processor;
goto send_now_idle;
}
tsk->need_resched = 1;
//置need_resched标志
if (tsk->processor != this_cpu)
smp_send_reschedule(tsk->processor); //发送IPI
}
return;
#else /*ifundef CONFIG_SMP*/
int this_cpu = smp_processor_id();
struct task_struct *tsk;
tsk = cpu_curr(this_cpu);
//取得当前进程
if (preemption_goodness(tsk, p, this_cpu) > 1) //运行资格高于当前进程
tsk->need_resched = 1;
#endif
}
谢谢大家!