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

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

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

中断命令寄存器

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

对称多处理器和处理器间中断的实现_第1张图片

这就是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

你可能感兴趣的:(对称多处理器和处理器间中断的实现)