(由于之前的blog已经关闭了,所以将此文章迁移至这里,并非转载)
之前已经大体的写过APIC的一些内容,这次是写一些APIC定时器的内容,当然,也是翻译了一些来自OSDev的资料(不要问我为什么不翻译Intel手册,其实都一样,Intel手册里面写的太长了,有时候不一定要把所有东西看完才能使用里面的内容)。如果喜欢看原文,可以点APIC timer – OSDev Wiki.链接。有任何疑问,可以e-mail联系,[email protected]。(其实msn也行)
使用APIC Timer的最大好处是每个cpu内核都有一个定时器(避免了定时器资源的抢占问题,你要是抢走了一个cpu核心,你就肯定抢走了一个定时器),这里的cpu内核是指和核心数,把超线程技术的那个内核也算进去了。相反的我们之前使用的PIT(Programmable Interval Timer)就不这样,PIT是共用的一个。因为这样,我们不需要考虑任何资源管理问题,我们能够很轻松的使用它(老外说话有时候很..)。但是APIC有一个比较麻烦的是,他的精度和CPU的频率有关(这里的频率指的是CPU的外频,现在主流的就是200MHz,但是有点飘,这个我在最后解释)。而PIT使用的是一个标准的频率(1193182Hz)。如果要使用APICTimer,那么你必须自己知道每秒钟能触发多少个中断。
APIC定时器一般包含2-3种定时器模式,前两种(周期触发periodic和一次性触发one-shot)一般被现在所有的Local APIC所支持。但是第三种(TSC-Deadline模式)目前只在最新的CPU里面支持(话说我手上拿的CPU貌似都支持)。
周期触发模式中,程序设置一个”初始计数“寄存器(Initial Count),同时Local APIC会将这个数复制到”当前计数“寄存器(Current Count)。Local APIC会将这个数(当前计数)递减,直到减到0为止,这时候将触发一个IRQ(可以理解为触发一次中断),与此同时将当前计数恢复到初始计数寄存器的值,然后周而复始的执行上述逻辑。可以使用这种方法通过Local APIC实现定时按照一定时间间隔触发中断的功能。”当前计数“寄存器中的值的递减速度依赖于当前CPU的主频除以一个叫做”分频寄存器“(“Divide Configuration Register”)的值(换个思路来说就是,多少个tick减少1)。
举例来说,对于一个2.4GHz的CPU使用的CPU总线频率是800MHz(大家说的外频800MHz),”分频寄存器“(“Divide Configuration Register”)设置的是“除四”(”divide by 4“),并且初始计数(initial count)设置到123456。那么结果就是当前计数(current count)每200M分之1秒减1,大约617.28us触发一个irq(一次中断),也就是1620.01Hz。
对于一次性触发模式,Local APIC中的“当前计数”寄存器的减少方式和周期触发模式一样,也是当“当前计数“寄存器的值到0的时候触发一次定时器IRQ(中断)。但是它不会自动恢复到初始计数。这样,软件必须每次都要写“初始计数”寄存器一个值来让其再一次的计时并触发IRQ。这种模式的有点事,软件可以精确地控制定时器的IRQ的发生时间。例如,系统可以根据进程的优先级来设置任务切换(一些任务使用较短的CPU时间,一些任务使用较长的CPU时间),并且不会产生任何不必要的IRQs(这句话我也不太清楚什么意思,不过大约就是可以精确地控制切换进程的时间到时IRQ产生,因为进程切换也耽误时间)。一些系统通过为计数器付值的方法去实现通用的高精度计时器服务。换个例子来说就是,有3个任务需要分别在1234ns、333ns、4444ns的时候去做,这样的话,就设定定时器显示333ns,到时中断发生时执行任务二,同时设定定时器901ns,到时中断发生时执行任务一,同时在设定定时器441111ns,定时后执行任务三(原文的英语的例子我是不理解为什么要写那么折腾了,我就简单的按上面举例了)。
缺点是,一次性触发可能会影响实时性(因为在设置的时候会耽误一些,导致的不确定性),并且还需要为了避免竞争,维护新的计数值和老的计数值。
TSC-Deadline模式同上述两种模式有很大的区别。触发方式发生了区别,上述两种方式是基于当前计数减少到0触发的,而TSC-Deadline模式是软件设置了一个”deadline“,然后当CPU的时间戳大于或等于”deadline“的时候触发一次IRQ。
尽管存在如上的差异,软件可能会将它用于替代一次性触发模式,相比于一次性触发模式,这样可以得到更高的精度(因为时间戳的生成时CPU的时钟,而不是CPU总线频率),并且它更容易处理资源竞争(最后这句话我真不太理解)。
首先,应该通过写MSR寄存器使能Local APIC硬件。
其次,配置一个用于定时器的中断,并且软件开启APIC。
最后,配置APIC Timer的中断向量以及操作模式。
具体操作方法,参考Inter开发手册Vol3A Chapter 9。
这里使用的方式是使用CPU总线频率作为基准源, 有很多种方法能够完成这部分所讲的内容,并且每种方法都不一样。例如:Real Time Clock,TimeStamp Counter,PIT or even polling CMOS registers。这里要讲的内容仍然要用到PIT,因为他很简单。按照如下步骤执行:
APIC Timer可以被设置为经过若干个tick以后触发一次中断,这里设置的地方叫做”分频数“(divide value)。这意味着可以通过将这一数值乘以APIC Timer的计数来获得当前CPU总线的频率。这里的分频数可以设置最小值是1最大值是128,具体的请参考Intel编程手册Vol3A Chapter9.5.4。(注,网上有人说在Bochs上设置分频数为1不好用,于是他设置了16)。
在开始前,我们先来定义一系列常量和函数。
apic = the linear address where you have mapped the APIC registers
APIC_APICID = 20h
APIC_APICVER = 30h
APIC_TASKPRIOR = 80h
APIC_EOI = 0B0h
APIC_LDR = 0D0h
APIC_DFR = 0E0h
APIC_SPURIOUS = 0F0h
APIC_ESR = 280h
APIC_ICRL = 300h
APIC_ICRH = 310h
APIC_LVT_TMR = 320h
APIC_LVT_PERF = 340h
APIC_LVT_LINT0 = 350h
APIC_LVT_LINT1 = 360h
APIC_LVT_ERR = 370h
APIC_TMRINITCNT = 380h
APIC_TMRCURRCNT = 390h
APIC_TMRDIV = 3E0h
APIC_LAST = 38Fh
APIC_DISABLE = 10000h
APIC_SW_ENABLE = 100h
APIC_CPUFOCUS = 200h
APIC_NMI = (4<<8)
TMR_PERIODIC = 20000h
TMR_BASEDIV = (1<<20)
;Interrupt Service Routines
isr_dummytmr: mov dword [apic+APIC_EOI], 0
iret
isr_spurious: iret
;function to set a specific interrupt gate in IDT
;al=interrupt
;ebx=isr entry point
writegate: ...
ret
同样的也需要配置一些IDT项,并且需要设置一个用于处理中断的中断门和处理函数。这里是:
interrupt 32:timer, IRQ0
interrupt 39 : spurious irq, IRQ7
如果有需要,那就直接改代码就好了。
提供一种asm的代码示例,大家看看就好了,汇编都差不多也就两种语法,intel还有at&a我是都被祸害习惯了。
;you should read MSR, get APIC base and map to "apic"
;you should have used lidt properly
;set up isrs
mov al, 32
mov ebx, isr_dummytmr
call writegate
mov al, 39
mov ebx, isr_spurious
call writegate
;initialize LAPIC to a well known state
mov dword [apic+APIC_DFR], 0FFFFFFFFh
mov eax, dword [apic+APIC_LDR]
and eax, 00FFFFFFh
or al, 1
mov dword [apic+APIC_LDR], eax
mov dword [apic+APIC_LVT_TMR], APIC_DISABLE
mov dword [apic+APIC_LVT_PERF], APIC_NMI
mov dword [apic+APIC_LVT_LINT0], APIC_DISABLE
mov dword [apic+APIC_LVT_LINT1], APIC_DISABLE
mov dword [apic+APIC_TASKPRIOR], 0
;okay, now we can enable APIC
;global enable
mov ecx, 1bh
rdmsr
bts eax, 11
wrmsr
;software enable, map spurious interrupt to dummy isr
mov dword [apic+APIC_SPURIOUS], 39+APIC_SW_ENABLE
;map APIC timer to an interrupt, and by that enable it in one-shot mode
mov dword [apic+APIC_LVT_TMR], 32
;set up divide value to 16
mov dword [apic+APIC_TMRDIV], 03h
;ebx=0xFFFFFFFF;
xor ebx, ebx
dec ebx
;initialize PIT Ch 2 in one-shot mode
;waiting 1 sec could slow down boot time considerably,
;so we'll wait 1/100 sec, and multiply the counted ticks
mov dx, 61h
in al, dx
and al, 0fdh
or al, 1
out dx, al
mov al, 10110010b
out 43h, al
;1193180/100 Hz = 11931 = 2e9bh
mov al, 9bh ;LSB
out 42h, al
in al, 60h ;short delay
mov al, 2eh ;MSB
out 42h, al
;reset PIT one-shot counter (start counting)
in al, dx
and al, 0feh
out dx, al ;gate low
or al, 1
out dx, al ;gate high
;reset APIC timer (set counter to -1)
mov dword [apic+APIC_TMRINITCNT], ebx
;now wait until PIT counter reaches zero
@@: in al, dx
and al, 20h
jz @b
;stop APIC timer
mov dword [apic+APIC_LVT_TMR], APIC_DISABLE
;now do the math...
xor eax, eax
xor ebx, ebx
dec eax
;get current counter value
mov ebx, dword [apic+APIC_TMRCURRCNT]
;it is counted down from -1, make it positive
sub eax, ebx
inc eax
;we used divide value different than 1, so now we have to multiply the result by 16
shl eax, 4 ;*16
xor edx, edx
;moreover, PIT did not wait a whole sec, only a fraction, so multiply by that too
mov ebx, 100 ;*PITHz
mul ebx
;-----edx:eax now holds the CPU bus frequency-----
;now calculate timer counter value of your choice
;this means that tasks will be preempted 1000 times in a second. 100 is popular too.
mov ebx, 1000
xor edx, edx
div ebx
;again, we did not use divide value of 1
shr eax, 4 ;/16
;sanity check, min 16
cmp eax, 010h
jae @f
mov eax, 010h
;now eax holds appropriate number of ticks, use it as APIC timer counter initializer
@@: mov dword [apic+APIC_TMRINITCNT], eax
;finally re-enable timer in periodic mode
mov dword [apic+APIC_LVT_TMR], 32 or TMR_PERIODIC
;setting divide value register again not needed by the manuals
;although I have found buggy hardware that required it
mov dword [apic+APIC_TMRDIV], 03h
void apic_timer_init(uint32 quantum){
uint32 tmp, cpubusfreq;
//set up isrs
writegate(32,isr_dummytmr);
writegate(39,isr_spurious);
//initialize LAPIC to a well known state
(uint32*)(apic+APIC_DFR)=0xFFFFFFFF;
(uint32*)(apic+APIC_LDR)=((uint32*)(apic+APIC_LDR)&0x00FFFFFF)|1);
(uint32*)(apic+APIC_LVT_TMR)=APIC_DISABLE;
(uint32*)(apic+APIC_LVT_PERF)=APIC_NMI;
(uint32*)(apic+APIC_LVT_LINT0)=APIC_DISABLE;
(uint32*)(apic+APIC_LVT_LINT1)=APIC_DISABLE;
(uint32*)(apic+APIC_TASKPRIOR)=0;
//okay, now we can enable APIC
//global enable
cpuSetAPICBase(cpuGetAPICBase());
//software enable, map spurious interrupt to dummy isr
(uint32*)(apic+APIC_SPURIOUS)=39|APIV_SW_ENABLE;
//map APIC timer to an interrupt, and by that enable it in one-shot mode
(uint32*)(apic+APIC_LVT_TMR)=32;
//set up divide value to 16
(uint32*)(apic+APIC_TMRDIV)=0x03;
//initialize PIT Ch 2 in one-shot mode
//waiting 1 sec could slow down boot time considerably,
//so we'll wait 1/100 sec, and multiply the counted ticks
outb(0x61,inb(0x61)&0xFD)|1);
outb(0x43,0xB2);
//1193180/100 Hz = 11931 = 2e9bh
outb(0x42,0x9B); //LSB
in(0x60); //short delay
outb(0x42,0x2E); //MSB
//reset PIT one-shot counter (start counting)
(uint8)tmp=inb(0x61)&0xFE;
outb(0x61,(uint8)tmp); //gate low
outb(0x61,(uint8)tmp|1); //gate high
//reset APIC timer (set counter to -1)
(uint32*)(apic+APIC_TMRINITCNT)=0xFFFFFFFF;
//now wait until PIT counter reaches zero
while(!(inb(0x61)&0x20));
//stop APIC timer
(uint32*)(apic+APIC_LVT_TMR)=APIC_DISABLE;
//now do the math...
cpubusfreq=((0xFFFFFFFF-(uint32*)(apic+APIC_TMRINITCNT))+1)*16*100;
tmp=cpubusfreq/quantum/16;
//sanity check, now tmp holds appropriate number of ticks, use it as APIC timer counter initializer
(uint32*)(apic+APIC_TMRINITCNT)=(tmp<16?16:tmp);
//finally re-enable timer in periodic mode
(uint32*)(apic+APIC_LVT_TMR)=32|TMR_PERIODIC;
//setting divide value register again not needed by the manuals
//although I have found buggy hardware that required it
(uint32*)(apic+APIC_TMRDIV)=0x03;
}
在最开始的时候我说过CPU总线频率不固定的这件事情,主要是在实测过程中会发生一些偏差,应该不算是漂移。大约在199.90~200.10MHz之间飘。这点同硬件的哥们研究过,后来得出的结果是一个叫做”自动跳频”技术造成的,好像可以关了这个就好了,具体我也没试验。只是希望别给大家带来不必要的麻烦。