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
}
谢谢大家!