APIC timer 和 TSC

APIC timer

  1. 目录

  2. 垫话

  3. 前言

  4. APIC timer 模式

3.1 periodic 模式

3.2 one-shot 模式

3.3 TSC-Deadline 模式

  1. 使能 APIC timer

  2. 初始化

5.1 校准步骤

5.2 前置条件

5.3 示例汇编代码

5.4 示例 C 代码

  1. 轰轰烈烈的讨论

  2. 更多参考

  3. 垫话
    关于 APIC timer,本号《Intel SDM 之 APIC [2]》一文对 SDM 中相关章节进行了翻译。

我在上一篇文章《粗谈 APIC timer 的校准及选型问题》中,梳理了 http://OSDev.org 上一篇关于 APIC timer 校准问题的讨论,本文继续梳理(其实就是无耻的翻译)上一篇文章提及但未涉及的另一篇 wiki(https://wiki.osdev.org/APIC_timer)。

虽然对 SDM 的翻译一文中对 APIC timer 的编程也略有涉及,但是不够具体,也没有校准方面的讨论。

http://OSDev.org wiki 好的地方在于讲的是真的透彻,而且里面有真刀真枪的代码。如果你正致力于自己写一个 kernel 之类的中二目标,里面大量硬件相关的代码不失为一个好的参考。

  1. 前言
    local APIC timer 的好处是其是硬件直连到 CPU core 上的,不同于 Programmable Interval Timer(笔者:也就是 PIT,古早的 8259 时代的 timer。较新的芯片组设计中,为了兼容性也保留了 PIT,但其是位于 I/O APIC 上的,如果要拿来用,需要走中断广播。http://OSDev.org 也有 PIT 相关的 wiki:https://wiki.osdev.org/PIT),PIT 是一块独立的芯片。正因为如此,APIC timer 无需任何资源管理(原文是 no need for any resource management,笔者没搞懂这里的“资源”指的是什么,可能是指 IO/内存地址空间等),用起来更简单。缺点是其工作频率是 CPU 频率(之一)(原文在 CPU 频率之前有一个括号注释 “one of”,笔者个人理解应该是指 APIC timer 运行在 CPU 频率分频之后的频率上),不同机器之间 CPU 频率各不相同,而 PIT 则不然,PIT 使用的是一个标准频率(1.193182 MHz)(笔者:这也是为啥 PIT 可以拿来对 APIC timer 做校准)。要使用 APIC timer,你得先搞清楚它 1 秒能触发多少个中断。

  2. APIC timer 模式
    timer 有 2 或 3 种模式。头两种模式(periodic 和 one-shot)是所有 local APICs 都会支持的。第三种模式(TSC-Deadline 模式)是一种扩展,只有比较新的 CPUs 才支持。

3.1 periodic 模式
对于 periodic 模式,软件会设置一个 “initial count”,local APIC 会将其用作 “current count”。local APIC 会不停地递减 current count,直到其为 0,随后触发一个 timer IRQ 并将 current count 复位为 initial count,并继续开始递减 current count,如此往复。通过这种方式,local APIC 可以根据 initial count 以一个固定的频率触发 IRQs。current count 递减的频率,为 CPU 外部频率(bus 频率)除以 local APIC “Divide Configuration Register” 中的值。笔者:意思就是 APIC timer 的频率是 bus 频率分频而来的,而且这个分频系数可配。

举例来说,对于一个外部/bus 频率为 800 MHz 的 2.4 GHz 主频的 CPU,如果 Divide Configuration Register 设置为 “除以 4”,且 initial count 被设置为 123456,;则 local APIC timer 递减计数的频率为 200 MHz,并会每 617.28 us 触发一个 timer IRQ,也就 1620.01Hz 的 IRQs 频率。笔者:忽略 CPU 的主频信息吧,这个玩意误导性极大,就记住 APIC timer 频率只跟 bus 频率有关系就行了,下面的 TSC-Deadline 模式除外。更详细的讨论见第 6 节“ 轰轰烈烈的讨论”。

3.2 one-shot 模式
对于 one-shot 模式,local APIC 会以与 periodic 模式相同的频率递减 current count(并在 count 为 0 时触发一个 timer IRQ);不同之处是当 current count 为 0 时,不会将 current count 复位为 initial count,如果想要多次触发 timer IRQ 的话,软件必须每次都重新设置一个新的 count。

该模式的好处在于,软件可以精确控制 timer IRQs 应该于何时出现。举例来说,在切换任务时,OS 可以根据新任务的优先级来设置 count 的值(如此实现,可以让有些任务运行的时间少一点,有些任务运行的时间多一点),而不会产生多余的 IRQs。有些 OS 会通过这种手法,实现一个通用的高精度定时器服务(笔者:典型如 linux 的 hrtimer),local APIC 的 count 值会根据最近要发生的事件做相应设置。举例来说,假设系统当前处于这种状态:当前正在运行的任务会在 1234 纳秒后被抢占,一个正在睡眠的任务会在 333 纳秒后被唤醒,还有一个 alarm 信号会在 44444 纳秒后发射,如此可以将 timer 的 count 设置为 333 纳秒(最早到期的 delay),当 timer IRQ 触发时,OS 会知晓距离当前任务被抢占还有 901 纳秒,距离 alarm 信号的发射还有 441111 纳秒,故而将 count 设置为 901 纳秒后再触发下一次 timer IRQ。

不好的地方在于,使用 one-shot 模式会比较难以追踪 real-time,而且为了避免竞态条件需要做一些特殊处理;特别是当新设置的 count 值比老的 count 值更小时。

3.3 TSC-Deadline 模式
TSC-Deadline 模式与另两种模式大相径庭。此模式并不是以 CPU 的外部/bus 频率递减一个 count,软件会设置一个 “deadline”,且当 CPU 的 time stamp counter 值大于等于 deadline 时,local APIC 触发一个 timer IRQ。

忽略这些不同之处,软件可以像 one-shot 模式那样使用此模式。好处(相较于 one-shot 模式来说)是你可以得到更高的精确性(笔者:关于精确性和准确性,参考《粗谈 APIC timer 的校准及选型问题》),因为 CPU 的 time stamp counter 运行在 CPU 的(标称)内部频率而不是外部/bus 频率,并且更容易避免/处理竞态条件。

  1. 使能 APIC timer
    使能 local APIC timer 之前,你需要先搞定 local APIC 的前置配置。包括:

确定 local APIC 的物理地址(通过 ACPI tables 或 MultiProcessor Specification tables)。
指定一个 spurious 中断并通过软件使能 APIC。
确认配置了 TRR(Task Priority Register),这样才不会阻塞/推迟更低优先级的 IRQs。笔者:关于 TRR,参考《Intel SDM 之 APIC [3]》。
前戏做完之后:

设置 local APIC timer 的 divide configuration register。
配置 local APIC timer 的中断 vector,并 umask 掉 timer IRQ(笔者:就是使能 timer IRQ)。
设置 local APIC timer 的 initial count。
注意:推荐按照上面的顺序操作(特别是,一定要在最后一步设置 local APIC timer 的 initial count)。如果顺序乱了(比如先设置 initial count 再使能 timer),在有些机器(真机或虚拟机)上会搞出问题(比如,一切都看似正常,counter 也在递减,但就是不触发 IRQ)。

  1. 初始化
    5.1 校准步骤
    注意:下面是推荐的确定 APIC timer 频率的手法。

注意:根据 Intel IA-32(x86) 以及 Intel 64(x86_64) 文档,APIC timer 的频率等于 bus 频率,或 core crystal 频率除以所选的分频器。bus 频率以及 core crystal 频率可以分别通过 CPUID 0x15 和 0x16 来获取。通过 CPUID 0x15 还可以获取 TSC 频率。APIC timer 的实际工作频率,取决于系统使用的是 local APIC(笔者:集成在 CPU 里面)还是分离(笔者:独立的芯片)的 APIC(82489DX)。当 local APIC 集成在 core crystal 里面时,APIC timer 使用 core 频率,否则使用 bus 频率。

APIC timer 校准的手法有很多,但都是通过一个不同的、与 CPU bus 频率无关的时钟源来干。比如说:Real Time Clock(https://wiki.osdev.org/RTC),TimeStamp Counter(https://wiki.osdev.org/TSC),PIT,甚至可以 polling CMOS 寄存器(https://wiki.osdev.org/CMOS#Getting_Current_Date_and_Time_from_RTC)。本文我们使用古早的 PIT,因为它出现的最早。下面是校准步骤:

复位 APIC 到一个已知状态。
使能 APIC timer。
复位 APIC timer counter。
通过一个不同的时钟,等待一个特定的时间。
从 APIC timer counter 获取 ticks 数(笔者:所谓的 ticks 数,counter 值的改变量)。
将其对齐到 1 秒。
将其除以你所期望的分频系数(得到结果 X)。
配置 APIC timer 每 X ticks 触发一个中断。
可以设置 APIC timer 的 tick(递减 counter)频率,该频率称为 “divide value(笔者:就理解为分频系数吧)”。也就是说,你可以将 APIC timer counter ticks 乘上该 “divide value” 得到实际的 CPU bus 频率。该 “divide value” 可设置范围是 1(每个 bus cycle 都 tick 一下)到 128(每 128 个 cycle 才 tick 一下)。具体细节参考 Intel 手册 vol3A Chapter 9.5.3。注意,根据我的测试观察,Bochs(笔者:一个 x86 模拟器)似乎并不能正确处理 divide value 为 1 的设置,所以我用了 16。

5.2 前置条件
开始之前先定义一些常量与函数。

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(https://wiki.osdev.org/IDT)(笔者:中断描述符表,就是中断向量表),且定义了一个将指定中断写入中断门(笔者:x86 下的蛋疼玩意,简单理解为一个中断向量)的函数:writegate(intnumber, israddress)。简单起见,我假设你并未修改默认的中断映射关系(几乎每个指导文档里面都会遵循的):

中断 0 - 31:异常。
中断 32:timer,IRQ0。
中断 39:spurious irq,IRQ7。
如果你已经修改了该配置,则请对上面的代码做相应调整。

笔者:这里原文写的是 interrupt,但我觉得不够严谨,应该是 vector。

5.3 示例汇编代码
下面是 fasm 语法的汇编,作用是初始化 APIC timer(笔者:嫌汇编啰嗦的话就直接看 5.4 节“示例 C 代码”吧,不寒碜):

;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

5.4 示例 C 代码
下面的示例代码将 APIC timer 初始为每 10 毫秒触发一次中断的状态。具体手法是:让 APIC timer 跑起来,通过 PIT 等 10 毫秒,从 APIC timer 获取 ticks 数。此处假设你实现了可以读写 APIC 寄存器的函数,以及 “pit_prepare_sleep”/“pit_perform_sleep” 函数,可以尽可能准确的测量 timer 的频率。

void apic_start_timer() {
  // Tell APIC timer to use divider 16
  write(APIC_REGISTER_TIMER_DIV, 0x3);

  // Prepare the PIT to sleep for 10ms (10000µs)
  pit_prepare_sleep(10000);

  // Set APIC init counter to -1
  write(APIC_REGISTER_TIMER_INITCNT, 0xFFFFFFFF);

  // Perform PIT-supported sleep
  pit_perform_sleep();

  // Stop the APIC timer
  write(APIC_REGISTER_LVT_TIMER, APIC_LVT_INT_MASKED);

  // Now we know how often the APIC timer has ticked in 10ms
  uint32_t ticksIn10ms = 0xFFFFFFFF - read(APIC_REGISTER_TIMER_CURRCNT);

  // Start timer as periodic on IRQ 0, divider 16, with the number of ticks we counted
  write(APIC_REGISTER_LVT_TIMER, 32 | APIC_LVT_TIMER_MODE_PERIODIC);
  write(APIC_REGISTER_TIMER_DIV, 0x3);
  write(APIC_REGISTER_TIMER_INITCNT, ticksIn10ms);
}
  1. 轰轰烈烈的讨论
    5.1 节“校准步骤”中提到了一个“core crystal 频率”的概念,笔者觉得这里值得一场轰轰烈烈的讨论。

所谓的“core crystal 频率”到底指的是啥?是不是就是 CPU 主频?

如果 core crystal 频率就是 CPU 主频,那么文章就自相矛盾了:3.1 节“periodic 模式”信誓旦旦地说,APIC timer 的频率只与 bus 频率有关,也就是要么是 bus 频率要么是由 bus 频率分频而来(3.1 节并没有讨论是否与 core 集成在一起的情况),而且《粗谈 APIC timer 的校准及选型问题》一文也信誓旦旦地说 APIC timer 频率与 CPU 频率没啥关系。

我在 SDM 上找了一下 5.1 节“校准步骤”所提及的两个 CPUID:
在这里插入图片描述
注意这两句话:

EBX[31:0]/EAX[31:0] indicates the ratio of the TSC frequency and the core crystal clock frequency.
The core crystal clock may differ from the reference clock, bus clock, or core clock frequencies.
结合 3.3 节“periodic 模式”,我们知道 TSC 频率就是 CPU 主频(虽然《粗谈 APIC timer 的校准及选型问题》一文大佬强调 TSC 频率实际上与 CPU 频率也并不完全是一回事,但相关话术太过暧昧,我们选择采纳本文的结论),那么可以从上面两句话推测出如下结论:

TSC 频率(CPU 主频)与 core crystal 频率之间不是一回事,它俩之间是有比例的,证明 CPU 主频与 core crystal 频率并不是一回事。
SDM 显式说明了,core crystal 频率 与 core clock 频率可能并不相同,再次证明二者不是一回事。
那么笔者进行的合理推测是:

core crystal 频率(包括前言部分所说的 CPU 频率),指的是送进 CPU packet 的 clock 的频率,而 CPU 主频实际上是通过 core crystal 频率倍频而来。
如果 APIC timer 集成在 CPU 里(也就是集成在 core crystal 里),那么其频率只可能从送进 CPU packet 的 core crystal 频率而来(要么直接使用 core crystal 频率,要么倍频或分频)。
如果 APIC timer 是一颗独立的芯片(不在 CPU 里面),那么其频率只可能从 bus 频率而来(要么直接使用 bus 频率,要么倍频或分频)。
只有这样文章才是自洽的。

而且我感觉文章存在把 cpu frequency 和 core crystal frequency 混用的问题,导致有点暧昧不清模棱两可。

TSC

主要就是看sdm
18.17
时间戳计数器
Intel 64 和 IA-32 架构(从奔腾处理器开始)定义了一个时间戳计数器机制,可用于监视和识别处理器事件的相对时间发生。计数器的架构包括以下组件:
•TSC 标志 — 指示时间戳计数器可用性的功能位。如果函数 CPUID.1:EDX.TSC[bit 4] = 1,则计数器可用。
•IA32_TIME_STAMP_COUNTER MSR(在 P6 系列和奔腾处理器中称为 TSC MSR)— 用作计数器的 MSR。

•RDTSC 指令 — 用于读取时间戳计数器的指令。
TSD 标志 — 控制寄存器标志用于启用或禁用时间戳计数器(如果 CR4.TSD[bit 2] = 1,则启用)。
时间戳计数器(在 P6 系列、奔腾、奔腾 ​​M、奔腾 4、英特尔至强、英特尔酷睿 Solo 和英特尔酷睿双核处理器及更高版本的处理器中实现)是一个 64 位计数器,在处理器复位后设置为 0。复位后,即使处理器被 HLT 指令或外部 STPCLK# 引脚停止,计数器也会递增。请注意,外部 DPSLP# 引脚的断言可能会导致时间戳计数器停止。
处理器系列以不同的方式递增时间戳计数器:

对于奔腾 M 处理器(系列 [06H],型号 [09H、0DH]);对于奔腾 4 处理器、英特尔至强处理器(系列 [0FH],型号 [00H、01H 或 02H]);以及对于 P6 系列处理器:时间戳计数器会随着每个内部处理器时钟周期递增。
内部处理器时钟周期由当前核心时钟与总线时钟之比决定。Intel®
SpeedStep® 技术转换也可能影响处理器时钟。

对于 Pentium 4 处理器、Intel Xeon 处理器(系列 [0FH]、型号 [03H 及更高]);对于 Intel Core Solo
和 Intel Core Duo 处理器(系列 [06H]、型号 [0EH]);对于 Intel Xeon 处理器 5100 系列和 Intel
Core 2 Duo 处理器(系列 [06H]、型号 [0FH]);对于 Intel Core 2 和 Intel Xeon 处理器(系列 [06H]、
DisplayModel [17H]);对于 Intel Atom 处理器(系列 [06H]、DisplayModel [1CH]):时间戳计数器以恒定速率递增。该速率可能由处理器的最大核心时钟与总线时钟之比设置,也可能由处理器启动时的最大解析频率设置。最大解析频率可能与处理器基本频率不同,有关更多详细信息,请参阅第 20.7.2 节。在某些处理器上,TSC 频率可能与品牌字符串中的频率不同。
特定的处理器配置决定了行为。恒定的 TSC 行为可确保每个时钟滴答的持续时间是统一的,并且即使处理器核心改变频率,也支持将 TSC 用作挂钟定时器。这是未来的架构行为。
RDTSC 指令读取时间戳计数器,并保证每次执行时都返回单调递增的唯一值,64 位计数器回绕除外。英特尔保证时间戳计数器在重置后 10 年内不会回绕。对于 Pentium 4、Intel Xeon、P6 系列和 Pentium 处理器,计数器回绕的周期更长。
通常,RDTSC 指令可以由在任何特权级别和虚拟 8086 模式下运行的程序和过程执行。 TSD 标志允许将此指令的使用限制在特权级别 0 上运行的程序和过程。安全操作系统将在系统初始化期间设置 TSD 标志以禁用用户对时间戳计数器的访问。禁用用户对时间戳计数器的访问的操作系统应通过用户可访问的编程接口模拟该指令。
RDTSC 指令不与其他指令序列化或排序。它不一定等到所有先前的指令都已执行后才读取计数器。同样,后续指令可以在执行 RDTSC 指令操作之前开始执行。
RDMSR 和 WRMSR 指令读取和写入时间戳计数器,将时间戳计数器视为普通的 MSR(地址 10H)。在 Pentium 4、Intel Xeon 和 P6 系列处理器中,使用 RDMSR 读取时间戳计数器的所有 64 位(与 RDTSC 一样)。当使用 WRMSR 写入系列 [0FH]、型号 [03H、04H] 之前的处理器上的时间戳计数器时:只能写入时间戳计数器的低 32 位(高 32 位清除为 0)。对于系列 [0FH]、型号 [03H、04H、06H];对于系列 [06H]]、
型号 [0EH、0FH];对于系列 [06H]]、DisplayModel [17H、1AH、1CH、1DH]:所有 64 位均可写入。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值