1. SMP硬件體系結構:
對於SMP最簡單可以理解為系統存在多個完全相同的CPU,所有CPU共享總線,擁有自己的寄存器。對於內存和外部設備訪問,由於共享總線,所以是共享的。Linux操作系統多個CPU共享在系統空間上映射相同,是完全對等的。
由於系統中存在多個CPU,這是就引入一個問題,當外部設備產生中斷的時候,具體有哪一個CPU進行處理?
為此,intel公司提出了IO APCI和LOCAL APCI的體系結構。
IO APIC連接各個外部設備,並可以設置分發類型,根據設定的分發類型,中斷信號發送的對應CPU的LOCAL APIC上。
LOCAL APIC負責本地CPU的中斷處理,LOCAL APIC不僅可以接受IO APIC的中斷,也需要處理本地CPU產生的異常。同時LOCAL APIC還提供了一個定時器。
如何確定那個CPU是引導CPU?
根據intel公司中的資料,系統上電後,會根據MP Initialization Protocol隨機選擇一個CPU作為BSP,只有BSP會運行BIOS程序,其他AP都進入等待狀態,BSP發送IPI中斷觸發後才可以運行。具體的MP Initialization Protocol細節,可以參考Intel? 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1 第8章。
引導CPU如何控制其他CPU開始運行?
BSP可以通過IPI消息控制AP從指定的起始地址運行。CPU中集成的LOCAL APIC提供了這個功能。可以通過寫LOCAL APIC中提供的相關寄存器,發送IPI消息到指定的CPU上。
如何獲取系統硬件CPU信息的?
在系統初始化後,硬件會在內存的規定位置提供關於CPU,總線, IO APIC等的信息,即SMP MP table。在linux初始化的過程,會讀取該位置,獲取系統相關的硬件信息。
2. linux SMP啟動過程流程簡介
setup_arch()
setup_memory();
reserve_bootmem(PAGE_SIZE, PAGE_SIZE);
find_smp_config(); //查找smp mp table的位置
smp_alloc_memory();
trampoline_base = (void *) alloc_bootmem_low_pages(PAGE_SIZE); //分配trampoline,用於啟動AP的引導代碼。
get_smp_config(); //根據smp mp table,獲取具體的硬件信息
trap_init()
init_apic_mappings();
mem_init()
zap_low_mappings(); 如果沒有定義SMP的話,清楚用戶空間的地址映射。
rest_init();
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
init();
set_cpus_allowed(current, CPU_MASK_ALL);
smp_prepare_cpus(max_cpus);
smp_boot_cpus(max_cpus);
connect_bsp_APIC();
setup_local_APIC(); //初始化 BSP的 LOCAL APCI。
map_cpu_to_logical_apicid();
針對每個CPU調用do_boot_cpu(apicid, cpu)
smp_init(); //每個CPU開始進行調度
trampoline.S AP引導代碼,為16進制代碼,啟用保護模式
head.s 為AP創建分頁管理
initialize_secondary 根據之前fork創建設置的信息,跳轉到start_secondary處
start_secondary 判斷BSP是否啟動,如果啟動AP進行任務調度。
3. 代碼學習總結
find_smp_config();,查找MP table在內存中的位置。具體協議可以參考MP協議的第4章。
這個表的作用在於描述系統CPU,總線,IO APIC等的硬件信息。
相關的兩個全局變量:smp_found_config是否找到SMP MP table,mpf_found SMP MP table的線性地址。
smp_alloc_memory() 為啟動AP的啟動程序分配內存空間。相關全局變量trampoline_base,分配的啟動地址的線性地址。
get_smp_config() 根據MP table中提供的內容,獲取硬件的信息。
init_apic_mappings();獲取IO APIC和LOCAL APIC的映射地址。
zap_low_mappings();如果沒有定義SMP的話,清楚用戶空間的地址映射。將swapper_pg_dir中表項清零。
setup_local_APIC(); 初始化 BSP的 LOCAL APCI。
do_boot_cpu(apicid, cpu)
idle = alloc_idle_task(cpu);
task = copy_process(CLONE_VM, 0, idle_regs(®s), 0, NULL, NULL, 0);
init_idle(task, cpu);
將init進程使用copy_process復制,並且調用init_idle函數,設置可以運行的CPU。
idle->thread.eip = (unsigned long) start_secondary;
修改task_struct中的thread.eip,使得AP初始化完成後,就運行start_secondary函數。
start_eip = setup_trampoline();
調用setup_trampoline()函數,復制trampoline_data到trampoline_end之間的代碼到trampoline_base處,trampoline_base就是之前在setup_arch處申請的內存。start_eip返回值是trampoline_base對應的物理地址。
smpboot_setup_warm_reset_vector(start_eip);設置內存40:67h處為start_eip為啟動地址。
wakeup_secondary_cpu(apicid, start_eip);在這個函數中通過操作APIC_ICR寄存器,BSP向目標AP發送IPI消息,觸發目標AP從start_eip地址處,從實模式開始運行。
trampoline.S
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
movl $0xA5A5A5A5, trampoline_data - r_base
這個是設置標識,以便BSP知道AP運行到這裡了。
lidtl boot_idt - r_base # load idt with 0, 0
lgdtl boot_gdt - r_base # load gdt with whatever is appropriate
加載ldt和gdt
xor %ax, %ax
inc %ax # protected mode (PE) bit
lmsw %ax # into protected mode
# flush prefetch and jump to startup_32_smp in arch/i386/kernel/head.S
ljmpl $__BOOT_CS, $(startup_32_smp-__PAGE_OFFSET)
啟動保護模式,跳轉到startup_32_smp 處
# These need to be in the same 64K segment as the above;
# hence we don't use the boot_gdt_descr defined in head.S
boot_gdt:
.word __BOOT_DS + 7 # gdt limit
.long boot_gdt_table-__PAGE_OFFSET # gdt base
boot_idt:
.word 0 # idt limit = 0
.long 0 # idt base = 0L
.globl trampoline_end
trampoline_end:
在這段代碼中,設置標識,以便BSP知道該AP已經運行到這段代碼,加載GDT和LDT表基址。
然後啟動保護模式,跳轉到startup_32_smp 處。