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
}