对称多处理器和处理器间中断的实现

来源:互联网 发布:纸牌屋经典片段 知乎 编辑:程序博客网 时间:2024/05/16 14:23

对称多处理器和处理器间中断是什么我这里就不解释了,网络上这部分内容比较多,随便一百度就有很多中文资料,我们来看一下他们是怎么实现的。

另外本文假定你已经具有了Local APIC的基础知识,至少要知道怎么写APIC的寄存器吧.

中断命令寄存器

要启动"主"处理器之外的任何一个处理器(事实上,处理器没有主次之分,除了启动之外),或者要发送一个处理器间中断,离不开的一个Local Apic寄存器就是中断命令寄存器(Interrupt Command Register,ICR).

这就是ICR的结构图,地址是0x310(高32位),0x300(低32位),比较重要的是最高8位存放的是LocalApicID,INIT,StartUp,NMI,最低8位存放的Vector

(有时候不是这样的,这里指的是本文用到的模式.)

获取处理器的个数和每个处理器的LocalApicID

如何获取一共有几个CPU和每个CPU的LocalApicID?这显然是要启动一个"次"CPU首先要面对的问题.

这个问题我们要用到ACPI(不是APIC,高级配置和电源管理接口)

寻找ACPI可以这样写:

int initACPI(void){   u8 *start = (u8 *)0xe0000;   u8 * const end   = (u8 *)0xfffff; /*BIOS read-only area.*/   while(start < end){      u64 signature = *(u64 *)start;      if(signature == *(const u64 *)"RSD PTR ")      {         if(parseRSDP(start) == 0) {    return 0; }      }      start += 0x10;   }   u8 *ebda = (u8 *)(pointer)((*((u16 *)0x40E))*0x10);   if(ebda != (u8 *)0x9FC00)      return -1; /*Maybe EBDA doesn't exist.*/   u8 * const ebdaEnd = ebda + 0x3FF;   while(ebda < ebdaEnd){      u64 signature = *(u64 *)ebda;      if(signature == *(const u64 *)"RSD PTR ")      {         if(parseRSDP(ebda) == 0) {    return 0; }      }   }   printkInColor(0xff,0x00,0x00,"Can't find ACPI.\n");   return -1;}
找到RSDP就调用parseRSDP分析.

ACPI的RSDP前20个字节的和一定为0,如果不是0就代表出错了(checksum).

static int parseRSDP(u8 *rsdp){   u8 sum = 0;   for(int i = 0;i < 20;++i)   {      sum += rsdp[i];   }   if(sum)   {      return -1;   }   printkInColor(0x00,0xff,0x00,"ACPI is found!!\n");   printk("ACPI Information:");   {      char oem[7];      memcpy((void *)oem,(const void *)rsdp + 9,sizeof(oem)/sizeof(char) - 1);      printk("OEM = %s,",oem);   }   u8 version = rsdp[15];   printk("Version = %d\n",(int)version);   if(version == 0)   {      u32 rsdt = *(u32 *)(rsdp + 16);      parseRSDT((ACPIHeader *)(pointer)rsdt);   }else if(version == 2)   {      u64 xsdt = *(u64 *)(rsdp + 24);      u32 rsdt = *(u32 *)(rsdp + 16);      if(xsdt)         parseXSDT((ACPIHeader *)(pointer)xsdt);      else         parseRSDT((ACPIHeader *)(pointer)rsdt);   }else{      printkInColor(0xff,0x00,0x00,"\nUnknow ACPI's version!!!\n");      return -1;   }   printk("\n");   return 0;}
然后可以从中获取XSDT或者RSDT(取决于版本和xsdt是否为0).
ACPIHeader的定义是这样的:

(用到了GCC的0长度数组扩展,不占用任何内存空间,只表示一个地址)

typedef struct ACPIHeader{   u32 signature;   u32 length;   u8 version;   u8 checksum;   u8 oem[6];   u8 oemTableID[8];   u32 oemVersion;   u32 creatorID;   u32 creatorVersion;   u32 data[0];} __attribute__ ((packed)) ACPIHeader;
然后调用parseXSDT,parseRSDT,这两个函数实际上是完全一样的,我们只贴出parseRSDT
static int parseRSDT(ACPIHeader *rsdt){   u32 *start = rsdt->data;   u32 *end = (u32 *)((u8 *)rsdt + rsdt->length);   while(start < end)   {      u32 dt = *(start++);      parseDT((ACPIHeader *)(pointer)dt);   }   return 0;}
ACPIHeader后面存放各种各样设备的信息的地址,比如APIC、FACP、SDDT、HPET等,对于每个设备调用parseDT
static int parseDT(ACPIHeader *dt){   u32 signature = dt->signature;   char signatureString[5];   memcpy((void *)signatureString,(const void *)&signature,4);   signatureString[4] = '\0';   printk("Found device %s from ACPI.\n",signatureString);   if(signature == *(u32 *)"APIC")      parseApic((ACPIHeaderApic *)dt);   return 0;}
不难理解,打印出设备的名字然后判断是否是APIC,是则调用parseApic,这就是我们最核心的代码了
static int parseApic(ACPIHeaderApic *apic){   char temp[20];   temp[0] = '0';temp[1] = 'x';   itoa(apic->localApicAddress,temp + 2,0x10,8,'0',1);   printk("\nLocal Apic Address: %s\n",temp);   localApicAddress = (u8 *)(pointer)apic->localApicAddress;   u8 *start = apic->data;   u8 *end = ((u8 *)apic) + apic->header.length;   while(start < end)   {      ApicHeader *apicHeader = (ApicHeader *)start;      u8 type = apicHeader->type;      u8 length = apicHeader->length;      switch(type)      {      case APIC_TYPE_LOCAL_APIC:         {    LocalApic *localApic = (LocalApic *)apicHeader;    printk("Found CPU: processor ID => %d, apic ID => %d \n",       (int)localApic->apicProcessorID,(int)localApic->apicID);            cpus[cpuCount++] = localApic->apicID;            break; }      case APIC_TYPE_IO_APIC:         {    IOApic *ioApic = (IOApic *)apicHeader;    itoa(ioApic->ioApicAddress,temp + 2,0x10,8,'0',1);            printk("Found I/O Apic : I/O Apic ID => %d, I/O Apic Address => %s\n",       (int)ioApic->ioApicID,temp);    ioApicAddress = (u8 *)(pointer)ioApic->ioApicAddress;            break; }      case APIC_TYPE_INTERRUPT_OVERRIDE:         break;      default:         printk("Unknow Apic information type:%d,length:%d.\n",(int)type,(int)length); break;      }      start += length;   }   printk("\n");   return 0;}

一些常量和类型:

typedef struct ACPIHeaderApic{   ACPIHeader header;   u32 localApicAddress;   u32 flags;   u8 data[0];} __attribute__ ((packed)) ACPIHeaderApic;typedef struct ApicHeader{   u8 type;   u8 length;} __attribute__ ((packed)) ApicHeader;typedef struct LocalApic{   ApicHeader header;   u8 apicProcessorID;   u8 apicID;   u32 flags;} __attribute__ ((packed)) LocalApic;typedef struct IOApic{   ApicHeader header;   u8 ioApicID;   u8 reserved;   u32 ioApicAddress;   u32 globalSystemInterruptBase;} __attribute__ ((packed)) IOApic;#define APIC_TYPE_LOCAL_APIC         0x0#define APIC_TYPE_IO_APIC            0x1#define APIC_TYPE_INTERRUPT_OVERRIDE 0x2

大体就是先保存并显示LocalApicAddress,然后遍历ApicHeader后面的data区域,如果发现CPU就记录下来,发现IOApic也记录了下来(这个就和本文没有关系了....)

遍历结束后cpuCount中就是CPU个数,而cpus中保存着每个CPU的LocalApicID,这样我们以后启动CPU就有了充足的资料

启动CPU

还记得刚刚我们讨论的LocalApic的中断命令寄存器吗?现在就要派上用场了.

 

启动CPU首先要对每个CPU执行Init指令,查一下上面的表格,我们知道init是将ICR的Delivery Mode设置为101 (2进制)

 

其他位我们这样设置:

Trigger Mode(触发模式)设置为0(边缘触发,EDGE Trigger,这个我也不是很清楚为什么,这样设置可以用)

Destination Mode(目标模式)设置为0(Physical,物理模式,如果不这样设置就不能直接用LocalApicID)

Destination Shorthand(目标速记)设置为0(No Shorthand,不使用速记,事实上我们可以设置为All Excluding Self,来一次性发往所有其他的CPU,但我们故意采用了相对麻烦的方法)

Level(高电平和低电平,这块我不清楚,设置为1可用)

Vector 不同Delivery Mode有不同作用,执行init时貌似不用,置为0

Destination Field结合上面我们设置的目标模式和目标速记,这个目标字段我们应该设置为目标CPU的LocalApicID<<24

 

知道了这个,下一步就是写入了,这是一个64位的寄存器,我们只能分两次写入,先写低位的话就开始执行了,后写的高位就没用了,所以我们要先写高位

高32位只有一个目标字段,于是:

localApicOut(0x310,0x1 << 24);

0x1就是我们要启动的CPU的LocalApicID

 

再写低32位

localApicOut(0x300,0x00004500);

0x310是IRC的高32位地址,0x300是低32位地址,不难理解吧

现在LocalApicID为1的CPU已经准备好了,现在我们来启动它

启动它就是要向他发送StartUp指令,这样Delivery Mode设置为110(二进制)

Startup的命令几乎和Init的完全一样,不同的就是Vector要设置成被启动CPU开始执行的物理地址/0x1000

localApicOut(0x310,0x1 << 24);localApicOut(0x300,0x00004600 | 0x10);

这样一来,LocalApicID为0x1的CPU就已经启动了,从0x10 * 0x1000 的物理地址开始执行,注意这个CPU是在实模式中的,我们还要重新使这个CPU进入保护模式、长模式

结合我们刚才获得的cpus和cpuCount中的信息,我们就不难写出遍历所有CPU并启动的代码了,要注意判断这个CPU是不是已经启动运行的那个"主"CPU

处理器间中断

处理器间中断的发送和上面很类似,只要将Vector设置为要发送的中断号,将Delivery Mode设置为0,就可以发送处理器间中断了

利用这个特性我们还可以将Delivery Mode设置成NMI来发送不可屏蔽中断,无论目标CPU正在干什么都会执行不可屏蔽中断(2号中断)的处理程序,实现了不可屏蔽中断的可编程化,但要注意,这种情况下Vector也是没用的,不能通过改变Vector来改变不可屏蔽中断的向量号
 

参考资料

(LOCAL)APIC文档:Intel® 64 Architecture x2APIC Specification

   http://www.intel.com/content/dam/doc/specification-update/64-architecture-x2apic-specification.pdf