Linux 多核 SMP 系統(tǒng)的引導(dǎo)
本篇文章基于Linux 2.6.32,x86體系結(jié)構(gòu)
系統(tǒng)的引導(dǎo)和初始化階段是個特例,因為在這個階段里系統(tǒng)中只有一個“上下文”,只能由一個處理器來處理。在這個階段里,也就是在系統(tǒng)剛加電或“總清(reset)”之后,系統(tǒng)中暫時只有一個處理器運行,這個處理器稱之為“引導(dǎo)處理器”BP;其余的處理器則處于暫停狀態(tài),稱為“應(yīng)用處理器”AP?!耙龑?dǎo)處理器”完成整個系統(tǒng)的引導(dǎo)和初始化,并創(chuàng)建起多個進程,從而可以由多個處理器同時參與處理時,才啟動所有的“應(yīng)用處理器”,讓他們完成自身的初始化以后,投入運行。參考intel手冊:
The MP initialization protocol defines two classes of processors: the bootstrap processor (BSP) and the application processors (APs). Following a power-up or RESET of an MP system, system hardware dynamically selects one of the processors on the system bus as the BSP. The remaining processors are designated as APs.
我們在這里關(guān)心的是“引導(dǎo)處理器”怎樣為各個“應(yīng)用處理器”做好準(zhǔn)備,然后啟動其運行的過程。
在初始化階段,引導(dǎo)處理器先完成自身的初始化,進入保護模式并開啟頁式存儲管理機制,再完成系統(tǒng)特別是內(nèi)存的初始化,然后從?start_kernel()?–>?rest_init()?–>?kernel_init()?–>?smp_init()?進行SMP系統(tǒng)的初始化。由于此時APs處于暫停狀態(tài),所以BP需要通過?smp_init()?–>?cpu_up()?–>?native_cpu_up()?–>?do_boot_cpu()?–>?wakeup_secondary_cpu_via_init()?發(fā)送IPI中斷喚醒APs,這樣APs就開始了正常的運行過程,擁有和BP一樣的地位。詳細過程我們后面分析。先來看總體大綱圖:

smp_init
smp_init的代碼在init/main.c:
/*?Called?by?boot?processor?to?activate?the?rest.?*/
static?void?__init?smp_init(void)
{
?unsigned?int?cpu;
?/*?FIXME:?This?should?be?done?in?userspace?--RR?*/
?for_each_present_cpu(cpu)?{
??if?(num_online_cpus()?>=?setup_max_cpus)
???break;
??if?(!cpu_online(cpu))
???cpu_up(cpu);//(1)--------
?????????//cpu_up到最終調(diào)用smp_ops.cpu_up(cpu);
?????????//.cpu_up = native_cpu_up是一個回調(diào)函數(shù)。在arch/x86/kernel/smp.c注冊?
?}
?/*?Any?cleanup?work?*/
?printk(KERN_INFO?"Brought?up?%ld?CPUs\n",?(long)num_online_cpus());
}
native_cpu_up的注冊:
struct?smp_ops?smp_ops?=?{?
???……?
??.smp_cpus_done??=?native_smp_cpus_done,
??.cpu_up?=?native_cpu_up,?
???……?
}?
native_cpu_up
接下來看標(biāo)號(1)處 native_cpu_up(unsigned int cpu) 。依次啟動系統(tǒng)中各個CPU。
int?__cpuinit?native_cpu_up(unsigned?int?cpu)
{
????......
????mtrr_save_state();
????per_cpu(cpu_state,?cpu)?=?CPU_UP_PREPARE;//設(shè)置對應(yīng)CPU的狀態(tài)
????????
????err?=?do_boot_cpu(apicid,?cpu);?//喚醒AP------------
????......
?while?(!cpu_online(cpu))?{//在這里不停的一直等。確認(rèn)前一個AP喚醒后,再喚醒下一個AP
??cpu_relax();
??......
?}
?return?0;
}
1、do_boot_cpu
發(fā)送IPI中斷喚醒APs,并且在IPI中斷中,帶有AP喚醒后要執(zhí)行的代碼地址(實際上只是一個vector,AP會把這個vector?12作為要執(zhí)行的代碼地址)。
static?int?__cpuinit?do_boot_cpu(int?apicid,?int?cpu)
{
?unsigned?long?boot_error?=?0;
?unsigned?long?start_ip;
?int?timeout;
?struct?create_idle?c_idle?=?{
??.cpu?=?cpu,
??.done?=?COMPLETION_INITIALIZER_ONSTACK(c_idle.done),
?};
?/*??
??*?完成c_idle.work.func?=?do_fork_idle
??*/
?INIT_WORK(&c_idle.work,?do_fork_idle);
?......
?if?(!keventd_up()?||?current_is_keventd())
????????/*?執(zhí)行do_fork_idle:將init進程使用copy_process復(fù)制,并且調(diào)用init_idle函數(shù),設(shè)置可以運行???
?????????*?的CPU。fork出一個idel線程,地址空間還是沿用init進程地址空間。
?????????*/
??c_idle.work.func(&c_idle.work);
?else?{
??......
?}
?set_idle_for_cpu(cpu,?c_idle.idle);
do_rest:
?per_cpu(current_task,?cpu)?=?c_idle.idle;
?......
?/*?AP的GDT已經(jīng)在start_kernel()-->setup_per_cpu_areas()初始化完成,這里只是保存它的基地址
?????*?到early_gdt_descr,等后面喚醒時,AP自己設(shè)置到GDTR。見startup_32_smp末尾
?????*/
????early_gdt_descr.address?=?(unsigned?long)get_cpu_gdt_table(cpu);
????
????//AP初始化完成后,就運行start_secondary函數(shù),見startup_32_smp末尾
?initial_code?=?(unsigned?long)start_secondary;
????
????//為AP設(shè)定好執(zhí)行start_secondary時將要使用的stack,見startup_32_smp末尾
?stack_start.sp?=?(void?*)?c_idle.idle->thread.sp;
?
????//real-mode?code?that?AP?runs?after?BSP?kicks?it(嘻嘻)
????/*?復(fù)制trampoline_data到trampoline_end之間的代碼(在arch/i386/kernel/trampoline.S中)到
?????* trampoline_base處。這里復(fù)制到trampoline_base的代碼是等下AP喚醒后要執(zhí)行的代碼。所以得通過IPI
?????*?的方式告訴AP,trampoline_base對應(yīng)物理頁所在位置。
?????*?trampoline_base是之前在start_kernel()-->setup_arch()-->smp_alloc_memory():
?????*????????trampoline_base?=?(void?*)?alloc_bootmem_low_pages(PAGE_SIZE)
?????*?處申請的頁。這里為什么要在低端內(nèi)存去分配trampoline_base?還記得之前說的 IPI傳遞給AP只是傳遞
?????*?了一個vector,這個vector只有8位大小,AP自己再<<12,所以AP總共只能尋址1M的物理地址空間。因為
?????* AP在喚醒后是處于實模式的。
?????*?
?????*?所以底下調(diào)用virt_to_phys,獲取trampoline_base對應(yīng)物理頁的地址start_eip,start_eip是4K對其
?????*?的,所以start_eip是形如0xSS000,等下通過IPI發(fā)送給AP的是0xSS
?????*/
????start_ip?=?setup_trampoline(){
????????memcpy(trampoline_base,?trampoline_data,
????????????????????????trampoline_end?-?trampoline_data);
????????return?virt_to_phys(trampoline_base);
????}
?......
????
?/*
??*?Kick?the?secondary?CPU.?Use?the?method?in?the?APIC?driver
??*?if?it's?defined?-?or?use?an?INIT?boot?APIC?message?otherwise:
??*/
?if?(apic->wakeup_secondary_cpu)
??boot_error?=?apic->wakeup_secondary_cpu(apicid,?start_ip);
?else
????????/*?這里是重點拉,發(fā)送IPI中斷。
?????????*?在這個函數(shù)中通過操作APIC_ICR寄存器,BSP向目標(biāo)AP發(fā)送IPI消息,觸發(fā)目標(biāo)AP從start_eip地址處,
?????????*?實模式開始運行。
?????????*/
??boot_error?=?wakeup_secondary_cpu_via_init(apicid,?start_ip);?
?if?(!boot_error)?{
??/*
???*?allow?APs?to?start?initializing.
???*/
??pr_debug("Before?Callout?%d.\n",?cpu);
????????
??cpumask_set_cpu(cpu,?cpu_callout_mask);
??pr_debug("After?Callout?%d.\n",?cpu);
??/*
???*?Wait?5s?total?for?a?response
???*/
??for?(timeout?=?0;?timeout?50000;?timeout++)?{
????????????/*?AP喚醒后會進入start_secondary()-->smp_callin()?設(shè)置對應(yīng)的cpu_callin_mask
?????????????*?所以這里只要檢測到cpu_callin_mask被設(shè)置了,代表AP激活成功
????*/
???if?(cpumask_test_cpu(cpu,?cpu_callin_mask))
????break;?/*?It?has?booted?*/
???udelay(100);
???/*
????*?Allow?other?tasks?to?run?while?we?wait?for?the
????*?AP?to?come?online.?This?also?gives?a?chance
????*?for?the?MTRR?work(triggered?by?the?AP?coming?online)
????*?to?be?completed?in?the?stop?machine?context.
????*/
???schedule();
??}
??if?(cpumask_test_cpu(cpu,?cpu_callin_mask))?{
???/*?Signal?AP?that?it?may?continue?to?boot?*/
???cpumask_set_cpu(cpu,?cpu_may_complete_boot_mask);
???pr_debug("CPU%d:?has?booted.\n",?cpu);//提示對應(yīng)的AP激活成功
??}?else?{
???boot_error?=?1;
???......可能出了什么問題
??}
?}
?......
?return?boot_error;
}
2、wakeup_secondary_cpu_via_init發(fā)送IPI
發(fā)送IPI中斷,至于為什么這里apic_icr_write可以發(fā)送vector到AP,請參考intel文檔。
wakeup_secondary_cpu_via_init(int?phys_apicid,?unsigned?long?start_eip)
{
????......
????/*?
????*?STARTUP?IPI?
????*/??
????/*?Target?chip?*/??
????/*?Boot?on?the?stack?*/??
????/*?Kick?the?second?*/??
????apic_icr_write(APIC_DM_STARTUP?|?(start_eip?>>?12),??
????phys_apicid);?
????......
}?
AP接收到IPI,就開始激活執(zhí)行了。
3、trampoline.S 這段代碼就是前面do_boot_cpu()—>setup_trampoline()拷貝到trampoline_base的代碼:
ENTRY(trampoline_data)
r_base?=?.
?wbinvd?????????//?Needed?for?NUMA-Q?should?be?harmless?for?others
?mov?%cs,?%ax???//?Code?and?data?in?the?same?place
?mov?%ax,?%ds
?cli???//?We?should?be?safe?anyway
?
?/*?這個是設(shè)置標(biāo)識,以便BP知道AP運行到這里了。當(dāng)前處于實模式,DS段寄存器指向前面的r_base處,此處往
??* r_base處寫入0xA5A5A5A5。BP可以
??*?通過虛擬地址trampoline_base尋址到r_base來查看是否設(shè)置$0xA5A5A5A5,以此來檢測AP激活是否成功
??*/
?movl?$0xA5A5A5A5,?trampoline_data?-?r_base??//?write?marker?for?master?knows?we're?running
?/*?GDT?tables?in?non?default?location?kernel?can?be?beyond?16MB?and
??*?lgdt?will?not?be?able?to?load?the?address?as?in?real?mode?default
??*?operand?size?is?16bit.?Use?lgdtl?instead?to?force?operand?size
??*?to?32?bit.
??*/
?
?/*?設(shè)置臨時idt和gdt,方便后面開啟保護模式
??*?至于為什么這里要減r_base,因為此時的DS段寄存器已經(jīng)指向r_base
??*?boot_idt_descr?-?r_base?+?DS段寄存器<<4?=?boot_idt_descr
??*/
?lidtl?boot_idt_descr?-?r_base?#?load?idt?with?0,?0
?lgdtl?boot_gdt_descr?-?r_base?#?load?gdt?with?whatever?is?appropriate
?xor?%ax,?%ax
?inc?%ax????//?protected?mode?(PE)?bit
?lmsw?%ax??//?into?protected?mode?將%ax加載到CR0,進入保護模式
?
?//?flush?prefetch?and?jump?to?startup_32_smp?in?arch/i386/kernel/head.S
?/*?長跳轉(zhuǎn)至startup_32_smp。此時的__BOOT_CS為0x10,對應(yīng)GDT的描述符base為0,然后沒有開啟分頁,直接
??*?訪問startup_32_smp物理地址
??*/
?ljmpl?$__BOOT_CS,?$(startup_32_smp-__PAGE_OFFSET)
boot_gdt_descr:
?.word?__BOOT_DS?+?7???????????//?gdt?limit
?.long?boot_gdt?-?__PAGE_OFFSET?//?gdt?base?
?/*?由于編譯時boot_gdt是加上了__PAGE_OFFSET,而當(dāng)前還沒有開啟頁表,所以boot_gdt?-?__PAGE_OFFSET
??*?后作為物理地址直接使用。
??*/
?
boot_idt_descr:
?.word?0????//?idt?limit?=?0
?.long?0????//?idt?base?=?0L
.globl?trampoline_end
trampoline_end:
//?-------------------------------------boot_gdt來自于arch/x86/kernel/head_32.S
ENTRY(boot_gdt)
?.fill?GDT_ENTRY_BOOT_CS,8,0?/*?GDT_ENTRY_BOOT_CS為2,這里有兩項?*/
?.quad?0x00cf9a000000ffff?/*?kernel?4GB?code?at?0x00000000?*/
?.quad?0x00cf92000000ffff?/*?kernel?4GB?data?at?0x00000000?*/
在這段代碼中,設(shè)置標(biāo)識,以便BSP知道該AP已經(jīng)運行到這段代碼,加載GDT和LDT表基址。然后啟動保護模式,更新CS段寄存器,跳轉(zhuǎn)到startup_32_smp 處。
4、startup_32_smp
ENTRY(startup_32_smp)
?cld
?/*?前面長跳轉(zhuǎn)已經(jīng)設(shè)置好CS,這里設(shè)置其他段寄存器。__BOOT_DS為0x18,使用GDT第4項,base全為0。也就是說
??*?從現(xiàn)在開始,只需要關(guān)注EIP?
??*/
?movl?$(__BOOT_DS),%eax?
?movl?%eax,%ds
?movl?%eax,%es
?movl?%eax,%fs
?movl?%eax,%gs
?
?......
/*
?*?Enable?paging
?*/
??/*?還記得前面fork的idel線程嗎?這里使用和init進程同樣的頁表,以使后面能夠正確的找到idel線程的內(nèi)核棧和
??*?執(zhí)行函數(shù)。
??*/
?movl?$pa(swapper_pg_dir),%eax?
?movl?%eax,%cr3??/*?set?the?page?table?pointer..?*/
?movl?%cr0,%eax
?orl??$X86_CR0_PG,%eax
?movl?%eax,%cr0??/*?..and?set?paging?(PG)?bit?開啟分頁?*/
?
?/* CS保持原樣,更新EIP,此時的EIP為0xC01000xx線性地址,因為在編譯時,符號1:的地址在3g后面*/
?ljmp?$__BOOT_CS,$1f?
1:
?/*?更新SS和esp,以使用idel進程的內(nèi)核棧。還記得在do_boot_cpu():stack_start.sp =?(void *)?
??*?c_idle.idle->thread.sp;?后面執(zhí)行的函數(shù)都使用該內(nèi)核棧??
??*/
?lss?stack_start,%esp
?
?/*?把eflags全部置零?*/
?pushl?$0
?popfl
?
?call?setup_idt
?
?/*?使用BP已經(jīng)設(shè)置好的GDT。見do_boot_cpu()
??*?early_gdt_descr.address?=?(unsigned?long)get_cpu_gdt_table(cpu)?
??*/
?lgdt?early_gdt_descr?
?
?lidt?idt_descr
?
?/*?由于重新設(shè)置了GDT,所以更新CS為__KERNEL_CS?GDT第13項?*/
?ljmp?$(__KERNEL_CS),$1f?
1:?movl?$(__KERNEL_DS),%eax?//?更新其他所有的段寄存器
?movl?%eax,%ss
?
?movl?$(__USER_DS),%eax
?movl?%eax,%ds
?movl?%eax,%es
?
?movl?$(__KERNEL_PERCPU),?%eax
?movl?%eax,%fs?//?set?this?cpu's?percpu,這樣AP就能找到自己的cpuid,至于原理
?????//?請參考?https://frankjkl.github.io/2019/03/09/Linux內(nèi)核-smp_processor_id/
?
?......
?/*?對于BP來講stack_start為init進程的內(nèi)核棧,initial_code為i386_start_kernel?*/
?/*?對于AP來講stack_start為BP設(shè)置的idel進程的內(nèi)核棧,initial_code為start_secondary?*/
?movl?(stack_start),?%esp
1:
?/*?見do_boot_cpu函數(shù)?
??*?initial_code?=?(unsigned?long)start_secondary
??*/
?jmp?*(initial_code)
這個函數(shù)的主要作用在于開啟分頁,更新EIP,ESP。重新設(shè)置GDT,更新所有的段寄存器,最后跳轉(zhuǎn)到start_secondary執(zhí)行。
5、start_secondary
此時分頁和保護模式都已經(jīng)開啟,且完全進入BP事先為我們fork好的idel線程的上下文。
static?void?__cpuinit?start_secondary(void?*unused)
{
?......
?cpu_init();
?preempt_disable();
????
????/*?設(shè)定cpu_callin_mask來告訴BP,AP已經(jīng)啟動。BP才能繼續(xù)運行。?
?????*?參考do_boot_cpu:if (cpumask_test_cpu(cpu, cpu_callin_mask))?
??*/?
?smp_callin();
?
????/*?otherwise?gcc?will?move?up?smp_processor_id?before?the?cpu_init?*/
?barrier();
?
????......
?
????//通知BP?AP已經(jīng)啟動(BP會在native_cpu_up的while循環(huán)里等待)
?set_cpu_online(smp_processor_id(),?true);
?......
????//更新AP的狀態(tài)
?per_cpu(cpu_state,?smp_processor_id())?=?CPU_ONLINE;
?......
?cpu_idle();
}
本函數(shù)主要是通知BP本AP啟動完成,然后cpu_idle,參與到任務(wù)調(diào)度。
總結(jié)
整理一下AP啟動的整個過程:
wakeup_secondary_cpu_via_init:BP發(fā)送IPI中斷給AP trampoline.S AP引導(dǎo)代碼,為16進制代碼,啟用保護模式 head.s 為AP創(chuàng)建分頁管理 start_secondary 通知BP啟動成功。AP參與任務(wù)調(diào)度。
F&Q:
1、每個AP自己的GDTR在哪里設(shè)置的?(每個AP的GDT都已經(jīng)由BP處理器初始化完成,就等待設(shè)置到CPU上)
do_boot_cpu() -> early_gdt_descr.address = (unsigned long)get_cpu_gdt_table(cpu);
startup_32_smp() –> lgdt early_gdt_descr;
2、發(fā)送IPI到AP后,CS:IP如何設(shè)置的?
CS 為 0x**00(**代表IPI中包含的vector),IP為0,CS:IP就可以引用trampoline.S中的代碼
參考:
https://www.bbsmax.com/A/xl56ELa7Jr/ 《Linux內(nèi)核源代碼情景分析》 https://www.tldp.org/HOWTO/Linux-i386-Boot-Code-HOWTO/smpboot.html
原文:
https://frankjkl.github.io/2019/03/10/Linux%E5%86%85%E6%A0%B8-SMP%E7%B3%BB%E7%BB%9F%E7%9A%84%E5%BC%95%E5%AF%BC/
