“引导处理器”完成整个系统的引导和初始化,并创建起多个进程,从而可以由多个处理器同时参与处理时,才启动所有的“应用处理器”,让他们完成自身的初始化以后,投入运行。参考intel手册:
(资料图片)
我们在这里关心的是“引导处理器”怎样为各个“应用处理器”做好准备,然后启动其运行的过程。
在初始化阶段,引导处理器先完成自身的初始化,进入保护模式并开启页式存储管理机制,再完成系统特别是内存的初始化,然后从start_kernel()
–>rest_init()
–>kernel_init()
–>smp_init()
进行SMP系统的初始化。
由于此时APs处于暂停状态,所以BP需要通过smp_init()
–>cpu_up()
–>native_cpu_up()
–>do_boot_cpu()
–>wakeup_secondary_cpu_via_init()
发送IPI中断唤醒APs,这样APs就开始了正常的运行过程,拥有和BP一样的地位。详细过程我们后面分析。先来看总体大纲图:
smp_init的代码在init/main.c:
/*Calledbybootprocessortoactivatetherest.*/staticvoid__initsmp_init(void){unsignedintcpu;/*FIXME:Thisshouldbedoneinuserspace--RR*/for_each_present_cpu(cpu){if(num_online_cpus()>=setup_max_cpus)break;if(!cpu_online(cpu))cpu_up(cpu);//(1)--------//cpu_up到最终调用smp_ops.cpu_up(cpu);//.cpu_up = native_cpu_up是一个回调函数。在arch/x86/kernel/smp.c注册}/*Anycleanupwork*/printk(KERN_INFO"Broughtup%ldCPUs\n",(long)num_online_cpus());}
native_cpu_up的注册:
structsmp_opssmp_ops={…….smp_cpus_done=native_smp_cpus_done,.cpu_up=native_cpu_up,……}
接下来看标号(1)处 native_cpu_up(unsigned int cpu) 。依次启动系统中各个CPU。
int__cpuinitnative_cpu_up(unsignedintcpu){......mtrr_save_state();per_cpu(cpu_state,cpu)=CPU_UP_PREPARE;//设置对应CPU的状态err=do_boot_cpu(apicid,cpu);//唤醒AP------------......while(!cpu_online(cpu)){//在这里不停的一直等。确认前一个AP唤醒后,再唤醒下一个APcpu_relax();......}return0;}
发送IPI中断唤醒APs,并且在IPI中断中,带有AP唤醒后要执行的代码地址(实际上只是一个vector,AP会把这个vector«12作为要执行的代码地址)。
staticint__cpuinitdo_boot_cpu(intapicid,intcpu){unsignedlongboot_error=0;unsignedlongstart_ip;inttimeout;structcreate_idlec_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())/*执行do_fork_idle:将init进程使用copy_process复制,并且调用init_idle函数,设置可以运行*的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已经在start_kernel()-->setup_per_cpu_areas()初始化完成,这里只是保存它的基地址*到early_gdt_descr,等后面唤醒时,AP自己设置到GDTR。见startup_32_smp末尾*/early_gdt_descr.address=(unsignedlong)get_cpu_gdt_table(cpu);//AP初始化完成后,就运行start_secondary函数,见startup_32_smp末尾initial_code=(unsignedlong)start_secondary;//为AP设定好执行start_secondary时将要使用的stack,见startup_32_smp末尾stack_start.sp=(void*)c_idle.idle->thread.sp;//real-modecodethatAPrunsafterBSPkicksit(嘻嘻)/*复制trampoline_data到trampoline_end之间的代码(在arch/i386/kernel/trampoline.S中)到* trampoline_base处。这里复制到trampoline_base的代码是等下AP唤醒后要执行的代码。所以得通过IPI*的方式告诉AP,trampoline_base对应物理页所在位置。*trampoline_base是之前在start_kernel()-->setup_arch()-->smp_alloc_memory():*trampoline_base=(void*)alloc_bootmem_low_pages(PAGE_SIZE)*处申请的页。这里为什么要在低端内存去分配trampoline_base?还记得之前说的 IPI传递给AP只是传递*了一个vector,这个vector只有8位大小,AP自己再<<12,所以AP总共只能寻址1M的物理地址空间。因为* AP在唤醒后是处于实模式的。**所以底下调用virt_to_phys,获取trampoline_base对应物理页的地址start_eip,start_eip是4K对其*的,所以start_eip是形如0xSS000,等下通过IPI发送给AP的是0xSS*/start_ip=setup_trampoline(){memcpy(trampoline_base,trampoline_data,trampoline_end-trampoline_data);returnvirt_to_phys(trampoline_base);}....../**KickthesecondaryCPU.UsethemethodintheAPICdriver*ifit"sdefined-oruseanINITbootAPICmessageotherwise:*/if(apic->wakeup_secondary_cpu)boot_error=apic->wakeup_secondary_cpu(apicid,start_ip);else/*这里是重点拉,发送IPI中断。*在这个函数中通过操作APIC_ICR寄存器,BSP向目标AP发送IPI消息,触发目标AP从start_eip地址处,*实模式开始运行。*/boot_error=wakeup_secondary_cpu_via_init(apicid,start_ip);if(!boot_error){/**allowAPstostartinitializing.*/pr_debug("BeforeCallout%d.\n",cpu);cpumask_set_cpu(cpu,cpu_callout_mask);pr_debug("AfterCallout%d.\n",cpu);/**Wait5stotalforaresponse*/for(timeout=0;timeout<50000;timeout++){/*AP唤醒后会进入start_secondary()-->smp_callin()设置对应的cpu_callin_mask*所以这里只要检测到cpu_callin_mask被设置了,代表AP激活成功*/if(cpumask_test_cpu(cpu,cpu_callin_mask))break;/*Ithasbooted*/udelay(100);/**Allowothertaskstorunwhilewewaitforthe*APtocomeonline.Thisalsogivesachance*fortheMTRRwork(triggeredbytheAPcomingonline)*tobecompletedinthestopmachinecontext.*/schedule();}if(cpumask_test_cpu(cpu,cpu_callin_mask)){/*SignalAPthatitmaycontinuetoboot*/cpumask_set_cpu(cpu,cpu_may_complete_boot_mask);pr_debug("CPU%d:hasbooted.\n",cpu);//提示对应的AP激活成功}else{boot_error=1;......可能出了什么问题}}......returnboot_error;}
发送IPI中断,至于为什么这里apic_icr_write可以发送vector到AP,请参考intel文档。
wakeup_secondary_cpu_via_init(intphys_apicid,unsignedlongstart_eip){....../**STARTUPIPI*//*Targetchip*//*Bootonthestack*//*Kickthesecond*/apic_icr_write(APIC_DM_STARTUP|(start_eip>>12),phys_apicid);......}
3、trampoline.S 这段代码就是前面do_boot_cpu()—>setup_trampoline()拷贝到trampoline_base的代码:
ENTRY(trampoline_data)r_base=.wbinvd//NeededforNUMA-Qshouldbeharmlessforothersmov%cs,%ax//Codeanddatainthesameplacemov%ax,%dscli//Weshouldbesafeanyway/*这个是设置标识,以便BP知道AP运行到这里了。当前处于实模式,DS段寄存器指向前面的r_base处,此处往* r_base处写入0xA5A5A5A5。BP可以*通过虚拟地址trampoline_base寻址到r_base来查看是否设置$0xA5A5A5A5,以此来检测AP激活是否成功*/movl$0xA5A5A5A5,trampoline_data-r_base//writemarkerformasterknowswe"rerunning/*GDTtablesinnondefaultlocationkernelcanbebeyond16MBand*lgdtwillnotbeabletoloadtheaddressasinrealmodedefault*operandsizeis16bit.Uselgdtlinsteadtoforceoperandsize*to32bit.*//*设置临时idt和gdt,方便后面开启保护模式*至于为什么这里要减r_base,因为此时的DS段寄存器已经指向r_base*boot_idt_descr-r_base+DS段寄存器<<4=boot_idt_descr*/lidtlboot_idt_descr-r_base#loadidtwith0,0lgdtlboot_gdt_descr-r_base#loadgdtwithwhateverisappropriatexor%ax,%axinc%ax//protectedmode(PE)bitlmsw%ax//intoprotectedmode将%ax加载到CR0,进入保护模式//flushprefetchandjumptostartup_32_smpinarch/i386/kernel/head.S/*长跳转至startup_32_smp。此时的__BOOT_CS为0x10,对应GDT的描述符base为0,然后没有开启分页,直接*访问startup_32_smp物理地址*/ljmpl$__BOOT_CS,$(startup_32_smp-__PAGE_OFFSET)boot_gdt_descr:.word__BOOT_DS+7//gdtlimit.longboot_gdt-__PAGE_OFFSET//gdtbase/*由于编译时boot_gdt是加上了__PAGE_OFFSET,而当前还没有开启页表,所以boot_gdt-__PAGE_OFFSET*后作为物理地址直接使用。*/boot_idt_descr:.word0//idtlimit=0.long0//idtbase=0L.globltrampoline_endtrampoline_end://-------------------------------------boot_gdt来自于arch/x86/kernel/head_32.SENTRY(boot_gdt).fillGDT_ENTRY_BOOT_CS,8,0/*GDT_ENTRY_BOOT_CS为2,这里有两项*/.quad0x00cf9a000000ffff/*kernel4GBcodeat0x00000000*/.quad0x00cf92000000ffff/*kernel4GBdataat0x00000000*/
ENTRY(startup_32_smp)cld/*前面长跳转已经设置好CS,这里设置其他段寄存器。__BOOT_DS为0x18,使用GDT第4项,base全为0。也就是说*从现在开始,只需要关注EIP*/movl$(__BOOT_DS),%eaxmovl%eax,%dsmovl%eax,%esmovl%eax,%fsmovl%eax,%gs....../**Enablepaging*//*还记得前面fork的idel线程吗?这里使用和init进程同样的页表,以使后面能够正确的找到idel线程的内核栈和*执行函数。*/movl$pa(swapper_pg_dir),%eaxmovl%eax,%cr3/*setthepagetablepointer..*/movl%cr0,%eaxorl$X86_CR0_PG,%eaxmovl%eax,%cr0/*..andsetpaging(PG)bit开启分页*//* CS保持原样,更新EIP,此时的EIP为0xC01000xx线性地址,因为在编译时,符号1:的地址在3g后面*/ljmp$__BOOT_CS,$1f1:/*更新SS和esp,以使用idel进程的内核栈。还记得在do_boot_cpu():stack_start.sp =(void *)*c_idle.idle->thread.sp;后面执行的函数都使用该内核栈*/lssstack_start,%esp/*把eflags全部置零*/pushl$0popflcallsetup_idt/*使用BP已经设置好的GDT。见do_boot_cpu()*early_gdt_descr.address=(unsignedlong)get_cpu_gdt_table(cpu)*/lgdtearly_gdt_descrlidtidt_descr/*由于重新设置了GDT,所以更新CS为__KERNEL_CSGDT第13项*/ljmp$(__KERNEL_CS),$1f1:movl$(__KERNEL_DS),%eax//更新其他所有的段寄存器movl%eax,%ssmovl$(__USER_DS),%eaxmovl%eax,%dsmovl%eax,%esmovl$(__KERNEL_PERCPU),%eaxmovl%eax,%fs//setthiscpu"spercpu,这样AP就能找到自己的cpuid,至于原理//请参考https://frankjkl.github.io/2019/03/09/Linux内核-smp_processor_id/....../*对于BP来讲stack_start为init进程的内核栈,initial_code为i386_start_kernel*//*对于AP来讲stack_start为BP设置的idel进程的内核栈,initial_code为start_secondary*/movl(stack_start),%esp1:/*见do_boot_cpu函数*initial_code=(unsignedlong)start_secondary*/jmp*(initial_code)
此时分页和保护模式都已经开启,且完全进入BP事先为我们fork好的idel线程的上下文。
staticvoid__cpuinitstart_secondary(void*unused){......cpu_init();preempt_disable();/*设定cpu_callin_mask来告诉BP,AP已经启动。BP才能继续运行。*参考do_boot_cpu:if (cpumask_test_cpu(cpu, cpu_callin_mask))*/smp_callin();/*otherwisegccwillmoveupsmp_processor_idbeforethecpu_init*/barrier();......//通知BPAP已经启动(BP会在native_cpu_up的while循环里等待)set_cpu_online(smp_processor_id(),true);......//更新AP的状态per_cpu(cpu_state,smp_processor_id())=CPU_ONLINE;......cpu_idle();}
do_boot_cpu() -> early_gdt_descr.address = (unsigned long)get_cpu_gdt_table(cpu);startup_32_smp() –> lgdt early_gdt_descr;
CS 为 0x**00(**代表IPI中包含的vector),IP为0,CS:IP就可以引用trampoline.S中的代码
觉得文章不错,点击“分享”、“赞”、“在看” 呗!
关键词: