来源不详!
摘 要 本文分析了Intel x86 SMP体系结构计算机的启动过程中对多CPU的处理,并考察了Linux操作系统(LinuxKernel 2.4.3)启动时对SMP的初始化工作。得知,在SMP加电启动时,只有一个CPU进行引导工作,当Linux操作系统的内核加载完毕后,再激活其它CPU,分别进入空闲进程,等待任务。
关键词 Linux,SMP,启动过程
1 引言
要了解操作系统对SMP系统中多CPU的支持,首先要了解SMP的启动过程。本文就以基于Intel x86SMP体系结构的计算机为考察对象,从系统加电启动开始,分析各CPU在这个启动过程中所起到的作用,然后再考察Linux操作系统内核加载的过程,从中得到操作系统内核对SMP系统的各处理器的不同的初始处理机制。通过这些,可以使我们清楚地了解到系统从加电启动一直到操作系统开始进行任务调度这一期间中所进行的过程,从而使我们能够更好地了解Linux操作系统对多处理器的支持。
本文的分析基于Intel x86SMP体系结构,操作系统内核为LinuxKernel 2.4.3。
我们将本文中出现的一些术语总结如下[1]:
1. AP:非启动CPU(不处理系统加电初始化)。
2. APIC: 可编程中断控制器。
3. BSP: 启动CPU(负责系统加电初始化的CPU)。
4. IPI: 处理器间中断;负责多处理器之间的硬件中断。
5. UP: 单处理器。
6. MP: 多处理器。
2 SMP系统的加电启动
我们看一下系统启动过程的几个步骤 [4] :
1、 系统加电,BIOS启动、自检;
2、 BIOS开始调入执行启动引导区程序,此程序负责具体录入操作系统(Linux)启动部分;
3、 Linux操作系统启动部分运行Setup.S和Head.S,将全部内核解压到内存中;
4、 进入执行内核中的start_kernel函数,对SMP系统初始化,产生空闲进程,再进入init过程。
首先我们来看第1步中BIOS所起到的作用[1]。
UP的BIOS有以下几个基本功能:
1)检测系统组件;
2)建立各种系统配置表格供操作系统使用(如中断向量表);
3)初始化处理器以及系统的其他部分使其处于一个可知状态;
4)BIOS提供面向设备的运行时刻服务。
MP BIOS增加了以下扩展功能:
1)将所有的处理器以及其他相关组件的配置信息传递给操作系统;
2)初始化所有处理器以及相关组件到一个已知状态。
这里由于BIOS代码并不是支持多线程的,所以在SMP中,系统必须让所有AP进入中断屏蔽状态,不与BSP一起执行BIOS代码。为了达到这一目的,可以利用两种手段:1、利用系统硬件本身进行处理;2、系统硬件与BIOS程序一起处理。在后一种方法中,BIOS程序将其它AP置于中断屏蔽状态,使其休眠,只选择BSP执行BIOS代码中的后继部分。BIOS要同时完成对APIC以及其他与MP相关的系统组件初始化过程,并建立相应的系统配置表格,以便操作系统使用。
到了启动的第2步,BIOS开始调入执行启动引导区程序,录入Linux操作系统的启动部分。
因此我们可以看到,在系统加电启动过程中,实际上只有一个CPU(BSP)负责启动工作,而其它的CPU(AP)则处于中断屏蔽状态,等待着操作系统的激活。
3 Linux操作系统内核对SMP的初始处理机制[2][4]
BIOS调入执行启动引导区程序后,这段程序录入Linux操作系统的启动部分,解压缩Linux内核核心映像,然后转入start_kernel函数开始执行。在这以前,系统没有对AP做任何处理。在start_kernel函数中,主要处理例如cache、内存等初始化工作,最后要调用smp_init函数,在这个函数里,具体实现SMP系统各CPU的初始处理机制。
我们来分析smp_init函数
在[linux/init/main.c]中
static void __init smp_init(void)
{
smp_boot_cpus();
smp_threads_ready=1;
smp_commence();
}
在函数smp_boot_cpus中,要建立并初始化各AP,关键代码如下:
在[linux/arch/i386/kernel/smpboot.c]中
void __init smp_boot_cpus(void)
{
……
for (apicid = 0; apicid < NR_CPUS; apicid++) {
if (apicid == boot_cpu_id)
continue; // 是BP,因为上面已经初始化完毕,就不再需要初始化
if (!(phys_cpu_present_map & (1 << apicid)))
continue; // 如果CPU不存在,不需要初始化
if ((max_cpus >= 0) && (max_cpus <=cpucount+1))
continue; //如果超过最大支持范围,不需要初始化
do_boot_cpu(apicid);// 对每个AP调用do_boot_cpu函数
……
}
……
}
下面我们看一下do_boot_cpu中做了什么工作:
在[linux/arch/i386/kernel/smpboot.c]中
static void __init do_boot_cpu (int apicid)
{
struct task_struct *idle; // 空闲进程结构
……
if (fork_by_hand() < 0) //在每个cpu上建立0号进程,这些进程共享内存
……
idle->thread.eip = (unsigned long) start_secondary;
// 将空闲进程结构的eip设置为start_secondary函数的入口处
……
start_eip = setup_trampoline(); // 得到trampoline.S代码的入口地址
stack_start.esp = (void *) (1024 + PAGE_SIZE + (char *)idle);
……
*((volatile unsigned short *) phys_to_virt(0x469)) =start_eip >> 4;
Dprintk("2.\n");
*((volatile unsigned short *) phys_to_virt(0x467)) =start_eip & 0xf;
Dprintk("3.\n");
// 将trampoline.S的入口地址写入热启动的中断向量(warm resetvector)40:67
……
apic_write_around(APIC_ICR2,SET_APIC_DEST_FIELD(apicid));
// 确定发送对象
apic_write_around(APIC_ICR, APIC_INT_LEVELTRIG |APIC_DM_INIT);
// 发送INIT IPI
……
apic_write_around(APIC_ICR2,SET_APIC_DEST_FIELD(apicid));
//确定发送对象
apic_write_around(APIC_ICR, APIC_DM_STARTUP | (start_eip>> 12));
//发送STARTUP IPI
……
}
现在对上面初始设置做如下概括[1]:
BSP将AP在一开始被唤醒后需要执行的代码(trampoline.S)的首地址写入热启动向量(warm resetvector),即从40:67开始的两个字。这样,当BSP对AP发送IPI时,AP响应中断,自动跳入这个trampoline.S代码部分继续执行。为了AP有足够的时间响应中断,BSP在发送中断请求后要延迟一段时间,。
在这以后,事实上AP已经在工作了,我们跟随AP,看它在做什么。AP响应中断直接跳转至trampoline.S的入口处,trampoline.S在载入符号表(gdt)和局部符号表(ldt)之后进入保护模式并跳至head.S的入口处:
在[linux/arch/i386/kernel/trampoline.S]中
……
inc %ax #protected mode (PE) bit
lmsw %ax # 进入保护模式
jmp flush_instr
flush_instr:
ljmpl $__KERNEL_CS, $0x00100000
# 一个长跳转,0x10:0x00100000是内核被解压后的起始地址,即head.S的startup_32[7]
……
AP转入head.S继续执行,但是执行的代码与BSP所执行的并不完全一致:
在[linux/arch/i386/kernel/head.S]中
ENTRY(stext)
ENTRY(_stext)
startup_32:
……
incb ready # 该段代码每执行一次,ready值加1,BSP执行时ready的值从0变为1
……
movb ready, %cl
cmpb $1,%cl
je 1f # 当执行CPU为BSP时,ready的值为1
call SYMBOL_NAME(initialize_secondary) # 执行initialize_secondary函数
jmp L6
1:
call SYMBOL_NAME(start_kernel) # 执行start_kernel()函数
L6:
jmp L6
ready: .byte 0 # ready为字节变量,初始值为0
AP执行head.S的过程是当执行到上述代码的时候,由于ready的值被改变,不再等于1,所以就继续向前执行,调用initialize_secondary函数,而不是像BSP一样执行标号1处的代码(调用start_kernel函数)。
initialize_secondary函数里面的代码很简单:
在[linux/arch/i386/kernel/smpboot.c]中
void __init initialize_secondary(void)
{
asm volatile(
"movl %0,%%esp\n\t"
"jmp *%1"
:
:"r" (current->thread.esp),"r"(current->thread.eip));
}
这是一段内嵌汇编程序,将程序跳转至current->thread.esp(即前面的idle->thread.esp)处 [8]。CPU执行start_secondary函数,进入空闲状态。
在[linux/arch/i386/kernel/smpboot.c]中
int __init start_secondary(void *unused)
{
cpu_init();
smp_callin();
while (!atomic_read(&smp_commenced))
rep_nop();
local_flush_tlb();
return cpu_idle(); // 进入空闲进程
}
至此,一个AP的初始化过程就完成了。
4 总结
下面简要的再把Linux的SMP启动过程做一总结。
在SMP中,首先要对各个处理器进行初始化。然后BSP工作,而其它的CPU(AP)则停留在一个初始化好的中断屏蔽状态休眠。BSP继续进行启动过程,在执行到操作系统的start_kernel之前,BSP所进行的工作与单处理器系统所做的工作是相同的。在start_kernel中,BSP通过smp_init对每个AP进行初始化。初始化的方式是通过APIC发送IPI。当BSP初始化完毕所有的AP之后,就继续执行start_kernel中的其余部分代码。而AP在接收到IPI之后,跳转到事先设置好的地址处执行trampoline.S和head.S。在执行head.S的过程中直接跳入事先创建好的空闲进程,进入空闲状态,等待以后的系统调度。