SMP概述.ppt

Download Report

Transcript SMP概述.ppt

Linux 内核分析之
SMP
王韶娟
概 述
袁禄来 吴长俊
2005年4月
SMP 概 述





SMP基础知识
SMP结构中的互斥问题
高速缓存与内存的一致性
SMP结构中的中断机制
SMP结构中的进程调度
一 SMP基础知识
SMP基础知识
因为在给定的时间内,cup的最高速度是有限的,提高计算速度方法之
一就是使用多个cpu。
并行计算机分类
 根据指令流和数据流的不同,通常把计算机系统分为:
 单指令流单数据流(SISD)
 单指令流多数据流(SIMD)
 多指令流单数据流(MISD)
 多指令流多数据流(MIMD)
 并行计算机系统绝大部分为MIMD系统(5类),包括
 并行向量机(PVP,Parallel Vector Processor,一般为专用);
 对称多处理机(SMP,Symmetric Multiprocessor);
 大规模并行处理机(MPP,Massively Parallel Processor);
 机群(Cluster);
 分布式共享存储多处理机(DSM,Distributed Shared Memory)
SMP基础知识
• 五种并行计算结构模型
VP
VP
VP
总线交叉开关网络
SM
SM
SM
(a) PVP
P/C
P/C
P/C
总线交叉开关网络
SM
SM
MB
P/C
LM
NIC
MB
P/C
LM
NIC
SM
定制网络
(b) SMP
(c) M P P
MB
P/C
LM
DIR
NIC
MB
P/C
LM
DIR
NIC
定制网络
MB
P/C
LM
B
LD IOB
NIC
MB
P/C
LM
B
LD IOB
NIC
商品网络(以太网等)
(d) DSM
(e)COW
SMP 基础知识
 对称性:硬件上,cpu没主次之份(除启动和初始化外);软件上,每个cpu平等动
态地从进程就绪队列中调度进程加于执行,中断请求也是等概论动态地分配给每个
cpu,由其提供中断服务。
 SMP系统一般使用同一种商品化微处理器,具有片上或外置高速缓存(减少访问内
存的冲突)
 经由高速总线(或交叉开关)连向共享存储器。每个处理器可等同地访问共享存储
器、I/O设备和操作系统服务。
 单一操作系统映像,全系统只有一个操作系统驻留在共享存储器中,它根据各个处
理器的负载情况,动态地分配各个进程到各个处理器,并保持负载平衡;
 共享总线带宽,所有处理器共享总线带宽,完成对内存模块和I/O模块的访问。
P/C
P/C
P/C
总线交叉开关网络
SM
SM
SM
SMP 基础知识
 优点:
低通信延迟,各个进程通过读/写操作系统提供的共享数据缓存区来
完成处理器间的通信,其延迟通常小于网络通信延迟;
 缺点:
 问题:欠可靠,总线、存储器、操作系统失效可能导致系统崩溃;
 可扩展性较差,由于所有处理器都共享总线带宽,而总线带宽每
3年才增加2倍,赶不上处理器速度和存储容量的增长步伐,因此
SMP的处理器个数一般少于64个,且只能提供每秒数百亿次的浮
点运算。
 实例:
SGI POWER Challenge XL系列、DEC Alphaserver 84005/440、
HP9000/T600和IBM RS6000/R40。
二 SMP结构中的互斥问题
SMP结构中的互斥问题
 i386系列处理器(特别是Pentium)解决SMP结构中对临界资源操作的原子
性问题
– 单纯的读或写本身就是原子的
– 对于一些既要读又要写的需要两个以上微操作才能完成的指令,如“读
-改-写”(在单cpu系统上是原子的对于这种情况,i386 CPU提供了
指令执行期间对总线加锁的手段
• CPU芯片上有一条引线LOCK,汇编程序中在指令前加上前缀
“LOCK”,汇编后机器代码就使CPU在执行该指令时把引线LOCK
的电位拉低(锁总线),这样别的CPU就暂时不能通过总线访问内
存了
SMP结构中的互斥问题
 i386系列处理器(特别是Pentium)解决SMP结构中对临界资源操作
的原子性问题
– 这方面有个特例,执行指令xchg时CPU会自动锁总线,而不需在
程序中加前缀“LOCK”
– xchg指令将一个内存单元中的内容与一个寄存器的内容对换,因
此常用于对内核信号量(Semephore)的操作
– 显然,这是一个既有读又有写的操作指令
– 下面的函数spin_trylock()就是一个使用xchg的实例(取自
include/asm-i386/spinlock.h)
SMP结构中的互斥问题
static inline int spin_trylock(spinlock_t *lock)
{
char oldval;
__asm__ __volatile__(
"xchgb %b0,%1“
:"=q" (oldval), "=m" (lock->lock)
:"0" (0) : "memory");
return oldval > 0;
}
将操作数%0即oldval先设置成0,然后与操作数%1即内存单元lock->lock的内
容交换。如果lock->lock内容原来是0,说明lock在此之前已经被锁上了,所以
spin_trylock返回0表示加锁失败;反之如果lock->lock内容原来非0,则现在变
0,加锁成功,所以spin_trylock返回1。
由于执行xchg指令时CPU会自动锁总线,所以不需前缀
“LOCK”
三 高速缓存与内存的一致性
高速缓存与内存的一致性
(1)数据一致性:
 问题:
高速缓存中的副本数据与内存中的不一样了
 方法:
Intel Pentium中为已经装入高速缓存的数据提供了一种自动与内
存保持一致的机制——Snooping (窥探)
 实现:
每个CPU内部都有一部分专门的硬件,一旦启用了高速缓存后就
时刻监视着系统总线上对内存的操作(对内存的操作一定要经过
系统总线)。如果发现有来自其他CPU的写操作,而本CPU的高
速缓存中又缓冲存储着该次写操作的目标,就会自动把相应的缓
冲线废弃,使得在需要用到这些数据时重新将其装入高速缓存。
高速缓存与内存的一致性
 TLB (地址转换/查找缓冲区)的一致性(重点)
 问题:一个CPU改变内存中某个页面映射目录或者页面映射表的 内
容,从而可能引起其他CPU的TLB与此不一致
 方法:通过IPI(处理器间中断)来解决,向系统中正在使用这个映
射表的CPU发出一个中断请求,请它们废弃各自TLB中的内容
 实现:
(a)某cpu更改了某TLB,则设立中断向量
INVALIDATE_TLB_VECTOR,以其为参数,调用函数
send_IPI_mask()
(b) 其他cpu收到中断向量后,调用中断处理函数
smp_invalidate_interrupt()
(附加另一实现:内核还在send_IPI_mask的基础上提供了一个
函数flush_tlb_others()。当一个CPU要求其他CPU废弃各自TLB
中的内容时就可以调用flush_tlb_others()达到“冲刷(废弃)其
他(进程的)TLB”的目的。)
高速缓存与内存的一致性
 通过send_IPI_mask()向有关的CPU发送一个
INVALIDATE_TLB_VECTOR中断请求
 发送以后要等待位图flush_cpumask变成全0,表示所有
的目标CPU都已经响应了这次中断请求。
分析:smp_invalidate_interrupt(void)
取CPU逻辑号
冲刷整个TLB?
是
在
flush_cpumask()
位图中
否
否
退出
是
冲刷某个具体
TLB页面
否
指向该CPU的
当前进程
是
响应APIC中断请求
该CPU状态为
TLBSTATE_OK
是
冲刷整个TLB
否
清除位图中CPU相应位
清除flush_cpumask
位图中CPU对应位
高速缓存与内存的一致性
 并不是只要页面目录或页面表内容发生了变化就一定要刷新TLB的内
容。如果有把握知道可能发生的变化绝不会影响CPU的运行,就可以
不用刷新。
 事实上内核中可能改变页面映射的只有以下几种情况:
 与vmalloc()有关。
 与HIGHMEM的映射有关。
 与外设总线(如PCI总线)有关的映射。
因此只要一个内核线程与这些操作无关,那么也就可以不必去刷新
TLB的内容,所以,在一些特殊情况下,CPU虽然还在使用属于某个
虚拟空间的页面目录或页面表,但是即使它们发生了变化也没有必要
刷新TLB的内容。
 懒得更新(比如执行系统调用exit()时,即使当前进程的LTB发生
变化,也不需要更新)设置当前TLB为TLBSTATE_LAZY,调用函数
leave_mm()
高速缓存与内存的一致性
 对某个TLB的冲刷(flush TLB)
 更新一部分,调用宏操作__flush_tlb_one()
 全部更新,调用宏操作local_flush_tlb()
四 SMP结构中的中断机制
 SMP中断机制简介
 中断初始化
 处理器间中断IPI
 SMP结构与单CPU系统中断机制的类比
§4.1 SMP中断机制简介
 单CPU系统
8259A中断控制器
 SMP结构
APIC——高级可编程中断控制器
• 本地APIC
• 全局APIC
CPU
CPU
本地 APIC
本地 APIC
本地中断请求
CPU
……
本地 APIC
本地中断请求
本地中断请求
ICC总线
全局 APIC
外部中断请求
图4.1
SMP结构中的中断控制机构
全局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线路,
这两条线路用于重启系统.
 一个重要功能——实现处理器间中断IPI
当一个CPU想要向其他CPU发送中断时,将中断向量和目
标处理器的本地APIC标志符保存到自己本地APIC的中断
命令寄存器中,然后通过ICC总线向目标处理器的本地
APIC发送一条消息, 目标处理器的本地APIC就向自己的
CPU发出相应的中断.
 SMP中断机制简介
 中断初始化
 处理器间中断IPI
 SMP结构与单CPU系统中断机制的类比
§4.2 中断初始化
 SMP相关的几个主要中断向量
 设置中断门
 中断响应程序的建立
 相关中断处理程序代码
SMP相关的几个主要中断向量
#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
}
…
中断响应程序的建立
上述几个主要中断向量的实际中断处理程序由
下面一组宏语句建立:
#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
中断响应程序的建立
 至此,结合前面说的中断门的初始化,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()
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()
 该函数响应CALL_FUNCTION_VECTOR,用于
请求目标CPU执行一个指定的函数.发送者先设
置好一个全局的call_data_struct数据结构,然后
向目标CPU发出请求,目标CPU收到中断向量后,
调用该函数进行处理
具体中断处理程序
 当然,一般的函数是没有必要请其他CPU来执行的,
因为系统中所有的CPU都可共享同样的代码和数据.
之所以要请求其他CPU执行,是因为某些函数必须
由特定的CPU来执行.
例如:pentium处理器有一条cpuid指令,通过这条指令可以
查询本CPU的型号、版本以及是否支持一些特殊的功能、
当前的功能设置等等信息.但是这条指令只能由具体的CPU
本身执行,而不能由别的CPU代替.这样,如果要知道系统中某
一个CPU的有关情况,就只能请求该CPU来执行cpuid指令.
 SMP中断机制简介
 中断初始化
 处理器间中断IPI
 SMP结构与单CPU系统中断机制的类比
§4.3 处理器间中断IPI
 本地APIC的控制寄存器
• APIC_ICR
——用来存储中断向量
• APIC_ICR2
——用来说明发送中断请求的目标
 IPI中断向量
本地APIC可以识别以下6种消息,也就是有下面六种
不同的IPI中断向量:
 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().
主要的发送函数
 static inline void send_IPI_mask(int mask, int vector)
向由mask指定的一个或多个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.
 除send_IPI_mask()之外的几个发送函数实际上
都是简单的调用__send_IPI_shortcut函数,只不
过 传给这个函数的参数不同而已.
 在这些函数基础上,还定义了一些功能更为专一、
明确的函数.比如当一个CPU需要另一个CPU进行
一次进程调度时,可以调用下面的函数:
void smp_send_reschedule(int cpu)
{
send_IPI_mask(1 << cpu,
RESCHEDULE_VECTOR);
}
 send_IPI_mask
static inline void send_IPI_mask(int mask, int vector)
{
unsigned long cfg;
unsigned long flags;
__save_flags(flags);
//中断前状态信息保存在flags中
__cli();
//关中断
apic_wait_icr_idle();
//确认或等待APIC_ICR处于空闲状态
/*ICR2、ICR是本地APIC的2个控制寄存器*/
cfg = __prepare_ICR2(mask); //根据中断请求的目标CPU准备将要
apic_write_around(APIC_ICR2, cfg)
//写入寄存器APIC_ICR2的值
cfg = __prepare_ICR(0, vector); //根据要发送的中断向量准备将要
apic_write_around(APIC_ICR, cfg); //写入寄存器APIC_ICR的值
//对APIC_ICR的写入操作引发并完成将中断向量发送至目标cpu的工作
__restore_flags(flags);
//恢复标志
}
 _send_IPI_shortcut
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_mask相比,少了耗时的开/关中断动
作,并且只对APIC编程一次*/
 SMP中断机制简介
 中断初始化
 处理器间中断IPI
 SMP结构与单CPU系统中断机制的类比
§4.4 SMP结构与单CPU系统
中断机制的类比
从前一部分可以看出,与单cpu系统的中断处
理机制相比,smp系统仅仅作了少量修改,并
且这些修改又大部分集中在处理器间中断IPI
这一部分,也许我们可以从另一个角度来看待
smp系统的中断机制-smp中断机制和单CPU
系统中断机制在某种程度上是等价的.
单个“超级”处理
器
CPU 0
CPU 1
CPU n
本地
APIC
本地
APIC
本地
APIC
CPU
8259A
全局APIC
外部中断
外部中断
五 SMP结构中的进程调度
§5.1 相关数据结构
 task_struct结构中新引入两个字段:
– has_cpu
为1时说明进程正在运行,为0则表示不在运行
– Processor
当has_cpu为1时指出进程是在哪一个cpu上运行
§5.2 相关代码
 一个相关的宏操作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()中相关代码:
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上重新调度
…
}
 __schedule_tail 函数
开始
保存先前进程调度策略
并清零prev->policy中
SCHED_YIELD标志位
policy = prev->policy;
prev->policy = policy & ~SCHED_YIELD;
task_lock(prev);
prev->has_cpu = 0;
将prev的has_cpu
字段清零
N
下台前运行态?
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);
 reschedule_idle 函数
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
}