APIC timer

来自osdev
跳到导航 跳到搜索

本地APIC定时器的最大好处是,它与每个CPU核心都是硬连线的,而PIT(Programmable Interval Timer - 可编程间隔定时器)是一个单独的电路。 因此,不需要任何资源管理,这使得事情变得更容易。 缺点是它以CPU的频率之一振荡,该频率因机器而异,而PIT使用标准频率 (1,193,182Hz)。 要利用它,你必须知道它每秒能中断多少次。

APIC定时器模式

定时器有2或3种模式。 前两种模式(周期性periodic和一次性one-shot)由所有本地APIC支持。 第三种模式(TSC-Deadline模式)是仅在最近的CPU上支持的扩展。

周期模式

对于周期模式,软件设置“初始计数”,本地APIC将其用于“当前计数”。 本地APIC递减当前计数直到其达到零,然后生成定时器IRQ并将当前计数重置为初始计数,并再次开始递减当前计数。 这样,本地APIC根据初始计数以固定速率生成IRQs。 当前计数的递减率取决于CPU的外部频率(“总线频率”)除以本地APIC的“除配置寄存器(Divide Configuration Register)”中的值。

例如,对于外部/总线频率为800兆赫兹的2.4GHzCPU,如果将分频配置寄存器设置为“除以4”并且将初始计数设置为123456; 然后,本地APIC定时器将以200 MHz的速率递减计数,并每617.28产生一个定时器IRQ,给出1620.01Hz的IRQs速率。

One-Shot模式

对于One-Shot单触发模式,本地APIC以与周期模式相同的方式递减当前计数(并在计数达到零时生成定时器IRQ); 但是,当当前计数达到零时,它不会将当前计数重置为初始计数。 相反,如果软件想要更多的定时器IRQ,它必须每次设置一个新的计数。

这种模式的优点是,软件可以精确控制定时器IRQ何时发生。 例如,在任务切换期间,操作系统可以将计数设置为取决于新任务优先级的值 (以便某些任务运行少量时间而其他任务运行较长时间), 而且不会有任何不必要的IRQ。 一些操作系统使用这种方法来实现通用的高精度定时器服务,其中本地APIC计数被设置为取决于哪个事件将最快发生的值。 例如,如果当前运行的任务开关应在1234纳秒内被抢先,则休眠任务需要在333纳秒内唤醒,并且必须在44444纳秒内发送警报信号, 然后定时器的计数将设置为333纳秒(所需的最早延迟) 当定时器IRQ发生时,OS知道在当前任务应该被抢占之前还有901纳秒,在需要发送报警信号之前还有441111纳秒 (并将下一个定时器IRQ的计数设置为901纳秒)。

缺点是,使用一次性模式更难实时跟踪,需要特别注意避免竞争条件;尤其是如果在旧计数到期之前设置了新计数。

TSC-Deadline 模式

TSC-Deadline模式与其他2种模式有很大不同。 该模式不是使用CPU的外部/总线频率来减少计数,而是软件设置“Deadline”,当CPU的时间戳计数器的值大于或等于Deadline时本地APIC生成定时器IRQ。

尽管有这些不同之处,但软件将/可以以与使用一次模式相同的方式使用它。 优点 (与 one-shot模式相比) 是你获得更高的精度 (因为CPU的时间戳计数器以CPU的(标称)内部频率而不是CPU的外部/总线频率运行), 而且更容易避免/处理竞争条件。

启用APIC定时器

在启用本地APIC定时器之前,应该设置本地APIC的其余部分。这包括:

  • 确定本地APIC的物理地址(通过ACPI表或多处理器规格表)
  • 指定一个虚拟中断和软件启用APIC
  • 确保TPR(任务优先级寄存器)已设置(因此它不会阻止/延迟低优先级IRQ)

一旦完成此操作:

  • 设置本地APIC定时器的除法配置寄存器
  • 配置本地APIC定时器的中断向量,并取消定时器的IRQ掩码
  • 设置本地APIC定时器的初始计数

注意: 建议遵循上面给出的顺序 (尤其是最后设置本地APIC定时器的初始计数)。 以不同的顺序进行操作(例如设置初始计数,然后启用定时器)可能会导致某些(真实或虚拟)机器出现问题 (例如,一切似乎都正确,计数器正在减少,但IRQ从不发送)。

初始化

“请注意,这是确定APIC定时器频率的推荐方法。”

注意: 根据英特尔IA-32 (x86) 和英特尔64 (x86_64) 的文档,APIC定时器的频率等于总线的频率 核心晶振的频率除以所选的分频器。 总线和核心晶体的频率可以在CPUID函数中找到分别为0x150x16。 通过CPUID.0x15还可以确定TSC频率。 APIC定时器的频率取决于系统是使用本地APIC还是 “discrete” APIC (82489DX)。 当本地APIC内置在核心晶振中时,APIC定时器使用核心的频率。 否则,它使用的是总线频率。


有几种方法可以做到这一点,但它们都使用不同的、独立于CPU总线频率的时钟源来做到这一点。 例如:实时时钟时间戳计数器(TimeStamp Counter),PIT或甚至轮询CMOS寄存器获取。 在本教程中,我们将使用又老又好的PIT,因为它是最简单的。 需要完成的步骤:

  • 将APIC重置为必要状态
  • 启用APIC定时器
  • 重置APIC定时器计数器
  • 等待不同时钟测量的特定时间
  • 从APIC定时器计数器获取滴答数
  • 调整到一秒钟
  • 除以你选择的数量(结果X)
  • 使APIC定时器在每X个节拍时触发一个中断

APIC定时器可以设置为在给定频率下进行tick (减小计数器),称为 “分值”。 这意味着你必须将APIC定时器计数器tick数乘以这个除法值,才能得到真正的CPU总线频率。 你可以使用值1(每个总线周期上的tick)到128(每128个周期上的tick)。 有关详细信息,请参阅Intel手册vol3A第9.5.4章。 请注意,根据我的测试,Bochs似乎无法正确处理1的除除数,因此我将使用16。

先决条件

在开始之前,让我们定义一些常量和函数。

APIC=映射APIC寄存器的线性地址

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
		; 在IDT中设置特定中断门(interrupt gate)的函数
		;al=interrupt
		;ebx=isr entry point
writegate:	...
		ret

我还将假设你有一个工作的 IDT,并且你有一个函数可以为特定的中断编写一个门: writegate(intnumber,israddress)。 此外,为了简单起见,我假设你没有更改几乎每个教程中的默认中断映射:

  • 中断0-31:异常
  • 中断32: 定时器,IRQ0
  • 中断39:虚拟irq,IRQ7

如果你已经对此进行了更改,请进行相应的修改。


ASM中的示例代码

以下是在fasm语法程序集中初始化APIC定时器的一种可能方法:

		;你应该读取MSR,获得APIC基址,并映射到“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

		; 将LAPIC初始化为合适的状态
		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
		;软件启用,将虚拟中断映射到虚拟isr
		mov			dword [apic+APIC_SPURIOUS], 39+APIC_SW_ENABLE
		; 将APIC定时器映射到中断,并通过该中断在one-shot模式中启用它
		mov			dword [apic+APIC_LVT_TMR], 32
		; 将除数设置为16
		mov			dword [apic+APIC_TMRDIV], 03h

		;ebx=0xFFFFFFFF;
		xor			ebx, ebx
		dec			ebx

		; 在one-shot模式下初始化PIT Ch 2
		; 等待1秒可能会显著减慢引导时间,
		; 所以我们将等待1/100秒,并乘以计数的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

C语言中的示例代码

此代码是如何初始化APIC定时器以使其每10毫秒计时一次的示例。 这是通过让APIC定时器运行,使用PIT等待10毫秒,然后从APIC定时器获取滴答次数来完成的。 它假设你拥有“write/read”APIC寄存器和“pit_prepare_sleep”/“pit_perform_sleep”函数,以尽可能准确地测量定时器的频率。

void apic_start_timer() {
        // 告诉APIC定时器使用divider16
        write(APIC_REGISTER_TIMER_DIV, 0x3);
 
        // 准备PIT休眠10ms (10000 µ s)
        pit_prepare_sleep(10000);
 
        // 将APIC init计数器设置为 -1
        write(APIC_REGISTER_TIMER_INITCNT, 0xFFFFFFFF);
 
        // 执行PIT支持的休眠
        pit_perform_sleep();
 
        // 停止APIC定时器
        write(APIC_REGISTER_LVT_TIMER, APIC_LVT_INT_MASKED);
 
        // 现在我们知道APIC计时器在10毫秒内ticked的频率
        uint32_t ticksIn10ms = 0xFFFFFFFF - read(APIC_REGISTER_TIMER_CURRCNT);
 
        // 在IRQ 0的divider16上启动计时器为周期定时器,计时时间为我们计算的时钟周期数
        write(APIC_REGISTER_LVT_TIMER, 32 | APIC_LVT_TIMER_MODE_PERIODIC);
        write(APIC_REGISTER_TIMER_DIV, 0x3);
        write(APIC_REGISTER_TIMER_INITCNT, ticksIn10ms);
}

另见

文章

论坛主题

外部链接