(由于之前的blog已经关闭了,所以将此文章迁移至这里,并非转载)
已经写到第三章了,这次讲的是如何引导CPU1启动。在将之前,可能先介绍一下如何在Windows下开发驱动。因为用户态的程序是Ring3的,而我们需要的级别是Ring0级别的代码。否则是无法操作APIC的。
在硬件上,CPU没主次之分(除启动和初始化外),物理上采用同一种CPU,所有的CPU通过一条总线共享同一个内存以及所有的外设设备,为了减少冲突,多处理器结构中各个CPU都有各自的高速缓存。
多处理器结构中的CPU都是平等的,这点在系统启动的时候来说其实存在一个特例。因为在这个阶段,系统中只有一个CPU。这个CPU叫做“引导处理器”(BP),其余的处于暂停状态,叫做“应用处理器”(AP)。
在上一节中,我们已经让Windows让出来一个CPU,这个CPU就可以理解是一个暂停状态的AP。我们通过已经引导启动的BP或者AP来启动这个AP。
目前我们用的是IA架构的处理器,于是第一步当然是需要查阅资料等。这里说到资料,我认为有必要人手一本Intel编程手册,厚了点,没问题,当字典吧。午睡的时候可以当枕头。
这里需要使用的资料是Intel的《MultiProcessor Specification》v1.4(May 1997)。说真的我已经忘记了名字到底应该怎么写了。这里先暂时提供一个链接供大家下载。来自Intel官方的,我就喜欢看官方给出来的,不喜欢从乱七八糟的地方下东西。
MultiProcessor Specification
这份文档中详细的讲了如何引导AP等。不多说了,直接看B-3部分,这部分就是讲AP引导的。
简单的来说,需要掌握如下几点知识:
没错,只需要如上几点知识即可。可能有人问,手册里面没写需要Windows驱动开发的知识啊,其实是这样的,开头已经说了,我们想操作APIC(一会儿再说APIC的事),必须得是Ring0级别,还有就是我们到后期是需要直接操作物理内存(确切的说是线性内存),所以必要的学会一点简单的即可。另外一点,APIC的知识我应该去哪里学,这个我之前翻译过osdev.org的文章。《对于APIC的一些资料》旁边的这个是链接,可以大体了解下。最后一点是基本的C和基本的汇编等。
Windows驱动开发这部分我就不怎么说了,就是能让我们在内核态运行我们的启动引导代码就行。
其实这里介绍的应该不是CPU启动方法,充其量能说的是,如何来引导AP启动(AP就是Application processor,这个是相对与启动CPU说的,一个多核电脑在启动的时候,先启动一个cpu,然后再通过这个CPU来启动其他的CPU)。
根据MultiProcessor Specification,我们了解到如果要启动AP,则需要通过一个BP(或其他AP)向目标AP发送INIT-SIPI-SIP序列。可能有人问什么是INIT-SIPI-SIPI序列,其实这个本质是IPI,英文忘了怎么写了,中文意思是CPU间中断。就是一个CPU向另外一个CPU发送消息。就像聊天一样。可以私聊,也可以群聊。
发送IPI消息的方式就涉及到了APIC了,因为IPI是通过APIC发送的。具体的操作可以参考Intel开发手册。里面在APIC那部分介绍了。我这里就简单的说一下就好了,IPI消息的类型分为好多种,其中有两种相对较为特殊的,一种是INIT IPI,另外一种是Start IPI。也就是我们说的INIT和SIPI。发送IPI的方法由两次写内存操作来完成,第一次是写APIC的一个地址在内存的映射用于配置消息,另外一次写内存用于将消息发送出去。
下面这部分是GiraffeOS中的一个代码片段,实现的是发送INIT IPI。终于贴代码了,第一次贴。
acpi_ICR2_temp=0x01000000; //select cpu 1
acpi_ICR1_temp=0x00004500; //send init ipi
*(unsigned long *)((unsigned char *)pAPIC + 0x310)=acpi_ICR2_temp;
*(unsigned long *)((unsigned char *)pAPIC + 0x300)=acpi_ICR1_temp;
具体的寄存器设置还是看Intel开发手册吧。有空我将这部分再给贴过来。暂时手上没书,改天,改天。
而Start IPI的发送就是按照下面这部分代码。
acpi_ICR2_temp=0x01000000; //select cpu 1
acpi_ICR1_temp=0x00004680; //send start ipi and start from 0x00080000
*(unsigned long *)((unsigned char *)pAPIC + 0x310)=acpi_ICR2_temp;
*(unsigned long *)((unsigned char *)pAPIC + 0x300)=acpi_ICR1_temp;
现在回来看INIT-SIPI-SIPI序列。根据MultiProcessor Specification中的“Example B-1. Universal Start-up Algorithm”,我把伪代码截取到下面。
BSP sends AP an INIT IPI
BSP DELAYs (10mSec)
If (APIC_VERSION is not an 82489DX) {
BSP sends AP a STARTUP IPI
BSP DELAYs (200μSEC)
BSP sends AP a STARTUP IPI
BSP DELAYs (200μSEC)
}
BSP verifies synchronization with executing AP
根据上述代码,大体完成的代码段应该如下:
memset(&PhysicalAddress,0,sizeof(PHYSICAL_ADDRESS));
PhysicalAddress.LowPart = (unsigned long)0x00080000;
p_mem= (unsigned char *)MmMapIoSpace(PhysicalAddress, sizeof(ap_boot_code), MmNonCached);
memcpy(p_mem,&ap_boot_code,sizeof(ap_boot_code));
MmUnmapIoSpace(p_mem, sizeof(ap_boot_code));
CMOS_WRITE(0x0f,0x0a); //System Shutdown Code
acpi_ICR2_temp=0x01000000; //select cpu 1
acpi_ICR1_temp=0x00004500; //send init ipi
*(unsigned long *)((unsigned char *)pAPIC + 0x310)=acpi_ICR2_temp;
*(unsigned long *)((unsigned char *)pAPIC + 0x300)=acpi_ICR1_temp;
KeQuerySystemTime(&startTime);
while(1){
KeQuerySystemTime(&currentTime);
if((currentTime.QuadPart-startTime.QuadPart) >= 10*10000) break;
}
acpi_ICR2_temp=0x01000000; //select cpu 1
acpi_ICR1_temp=0x00004680; //send start ipi and start from 0x00080000
*(unsigned long *)((unsigned char *)pAPIC + 0x310)=acpi_ICR2_temp;
*(unsigned long *)((unsigned char *)pAPIC + 0x300)=acpi_ICR1_temp;
KeQuerySystemTime(&startTime);
while(1){
KeQuerySystemTime(&currentTime);
if((currentTime.QuadPart-startTime.QuadPart) >= 2) break;
}
acpi_ICR2_temp=0x01000000; //select cpu 1
acpi_ICR1_temp=0x00004680; //send start ipi and start from 0x00080000
*(unsigned long *)((unsigned char *)pAPIC + 0x310)=acpi_ICR2_temp;
*(unsigned long *)((unsigned char *)pAPIC + 0x300)=acpi_ICR1_temp;
KeQuerySystemTime(&startTime);
while(1){
KeQuerySystemTime(&currentTime);
if((currentTime.QuadPart-startTime.QuadPart) >= 10*10000) break;
}
大家可能发现,在发送INIT-SIPI-SIPI序列前,做了一些其他工作。没错,是有一些其他工作,这些工作分为如下三点功能:
第一点的理解,可以理解为,通过INIT-SIPI-SIPI将AP启动后,告诉AP去执行什么代码,否则就跑飞了。
第二点是由AP的引导代码自举执行真正要执行的程序。
第三点是为了防止发送INIT后直接重启电脑了。这点大家可以自己去GOOGLE一下。我这里就不说了。因为很好查的。
还是对于第一点,为什么要将机器码拷贝到0x00080000上呢,其实这里是我找到的一块Windows不用的低1M范围内的内存。因为AP在启动初期,是实模式的情况下,只能寻址到1M范围内内存,通过在SIPI中指定需要发送的段偏移(0x80,具体的看Intel开发手册,我改天给摘抄过来),让AP在启动后直接去执行起始于0x00080000的机器码。
对于这里的机器码应该如何生成,在下一节里面说明。下一节可能需要的知识是汇编。Intel语法的。
现在,如果0x00080000部分的机器码如果写的对,那个被Windows空闲出来的CPU应该已经启动了。其实Windows和Linux在多核操作系统上的启动CPU的方式就是如上的方式,知识SIPI的发送方式是广播的方式发送,而不是向我们用点对点的发送。因为Windows和Linux毕竟是要使用所有的CPU(默认情况下),而我们只是要启动一个,其实我们也可以启动其他的,具体的情况具体操作。
未完,待续……