(由于之前的blog已经关闭了,所以将此文章迁移至这里,并非转载)
本文内容主要翻译自osdev.org,因为当年再此学习了很多东西但是发现在国内很少有中文资料,于是下定决心为国家富强奋斗终身。要是感兴趣也可以看原文“http://wiki.osdev.org/APIC”。要是有一些疑问也可以e-mail,[email protected]。
APIC全称是”Advanced Programmable Interrupt Controller”(高级可编程中断控制器),是intel对过去PIC(可编程中断控制器)的一个升级版本。它用于多处理系统启动并且是先带Intel(或兼容)处理器中不可分割的一部分,APIC用于将中断重定向,并且用于发送处理器间中断消息(IPI消息)。这些事情在过去的PIC中标准都无法完成。
查看CPUID.01h:EDX[bit 9]标志位,就可以知道本地的CPU是否有内置的Local APIC了。
在一个基于APIC的系统中,每一个核心都有一个Local APIC(这个问题由于当年年资料有限,Intel手册里面也没看太懂,争吵了好久。意思是说,有一个4个CPU且每个CPU都开启HT技术的情况下,有8个LocalAPIC,每个核心一个,并且完全独立。)。Local APIC负责处理CPU中特定的中断配置。还有其他事情,它包含了“Local Vector Table(LVT)”负责配置事件中断(例如定时器什么的,定时器就是传说中的APIC Timer)。
具体有关APIC的更多信息,可以参考Intel开发手册。
此外,还有一个CPU外面的IO APIC(例如Intel82093AA)的芯片组,并且提供基于多处理器的中断管理,在多个处理器之间,实现静态或动态的中断触发路由(可以理解为他是一个中断的调度器,他可以决定一个外部中断给不给上面,并且是给哪个CPU)。IO APIC中可以配置外部的每一个中断发送给的CPU以及用什么类型发送过去。
每一个中断针都能指定为边缘触发或电平触发。每个中断也都能制定特定的中断向量以及中断转向信息。通过将IOAPIC的IO空间映射到内存上来访问并操作IOAPIC。为了提高系统的灵活性,一般在分配映射内存时,内存分配的地址是可以变的,但是一般默认都分配到了0xFEC00000上。
Intel有关APIC的手册可以从Intel的网站上下载到。分类名是”Multiprocessor Specification”。或者是直接点这个链接下载(我不保证若干年之内还有效)。
处理器间中断(IPI)
Inter-Processor Interrupts(IPIs)是一种由Local APIC触发的中断,一般可以用于多CPU间调度之类的使用(简单的说就是一个CPU触发另外的或本身的或所有的CPU进入一个中断),CPU的启动引导序列也要用到这个(就是传说中的INIT-SIPI-SIPI)。还有其他功能,etc。
Local APIC在启动的时候开启(他是什么说,但是我发现我引导CPU后,APIC默认是不时能的,一定要自己开开一下,否则APICTImer不好用的。)并且如果要是想给禁用的话,配置IA32_APIC_BASE的MSR(Model Specific Register)。然后CPU接受中断,直接采用8259的兼容PIC模式。Intel手册中说明,但凡你给关了,你就不能给开开了,只能靠重置CPU。
想要开启Local APIC接收中断,则需要设置Spurious Interrupt Vector Register的第8位即可。
IO APIC可以被设置成兼容模式(就像是模拟8259一样)。
Local APIC的寄存器被映射到物理内存上,一般是0xFEE00XXX这种。其实这个地址可以被修改,在MSR中指定了实际的APIC基址。我们可以通过这一机制去移动他的基址。(网上说你不能移动到任何地方,例如大于4GB的地方。)
下面的代码用于访问APIC。
#define IA32_APIC_BASE_MSR 0x1B
#define IA32_APIC_BASE_MSR_ENABLE 0x800
/** returns a 'true' value if the CPU supports APIC
* and if the local APIC hasn't been disabled in MSRs
* note that this requires CPUID to be supported.
*/
bool cpuHasAPIC()
{
uint32_t eax, edx;
cpuid(1, &eax, &edx);
return edx & CPUID_FLAG_APIC;
}
/* Set the physical address for local APIC registers */
void cpuSetAPICBase(uintptr_t apic)
{
uint32_t edx = 0;
uint32_t eax = (apic & 0xfffff000) | IA32_APIC_BASE_MSR_ENABLE;
#ifdef __PHYSICAL_MEMORY_EXTENSION__
edx = (apic >> 32) & 0x0f;
#endif
cpuSetMSR(IA32_APIC_BASE_MSR, eax, edx);
}
/**
* Get the physical address of the APIC registers page
* make sure you map it to virtual memory ;)
*/
uintptr_t cpuGetAPICBase()
{
uint32_t eax, edx;
cpuGetMSR(IA32_APIC_BASE_MSR, &eax, &edx);
#ifdef __PHYSICAL_MEMORY_EXTENSION__
return (eax & 0xfffff000) | ((edx & 0x0f) << 32);
#else
return (eax & 0xfffff000);
#endif
}
void enableAPIC()
{
/* Hardware enable the Local APIC if it wasn't enabled */
cpuSetAPICBase(cpuGetAPICBase());
/* Set the Spourious Interrupt Vector Register bit 8 to start receiving interrupts */
WriteRegister(0xF0, ReadRegister(0xF0) | 0x100);
}
IO APIC使用两个寄存器完成他的所有操作。一个叫做地址寄存器地址在IOAPICBASE+0,一个叫做数据寄存器地址在IOAPICBASE+0x10(如果愿意的话也可以叫做窗口寄存器和数据寄存器)。注意,一个操作必须使用双字进行操作。地址寄存器中,使用低8位来设置选择要写哪个地方。下面是访问的一个代码示例,大家可以参考下原理并且直接复制用就行了。
uint32_t cpuReadIoApic(void *ioapicaddr, uint32_t reg)
{
uint32_t volatile *ioapic = (uint32_t volatile *)ioapicaddr;
ioapic[0] = (reg & 0xff);
return ioapic[4];
}
void cpuWriteIoApic(void *ioapicaddr, uint32_t reg, uint32_t value)
{
uint32_t volatile *ioapic = (uint32_t volatile *)ioapicaddr;
ioapic[0] = (reg & 0xff);
ioapic[4] = value;
}