<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Linux 多核 SMP 系統(tǒng)的引導(dǎo)

          共 3691字,需瀏覽 8分鐘

           ·

          2022-02-12 23:02

          本篇文章基于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/

          瀏覽 99
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伊人麻豆伊人 | 无码电影网 | 国产高清黄色电影 | 这里只有精品视频国产 | 亚洲精品久久久久毛片A级绿茶 |