mwaitx 指令不阻塞

mwaitx instruction not blocking

根据AMD的官方文档,mwaitx指令可以和monitorx指令一起使用来监控一个地址范围,看它是否被修改。我的代码似乎立即返回,似乎什么也没做。

有问题的代码:

push rbx
push r9
mov r9, rcx ;save parameter for later
mov eax, 80000001h ;check cpuid flag for monitorx, which is required regardless of whether or not we look at it
cpuid
mov rax, r9 ;the address to monitor
xor rcx, rcx ;no extensions
xor rdx, rdx ;no hints
monitorx rax, rcx, rdx
xor rax, rax ;no hints
mov rcx, 10b ;timeout specified in rbx, no interrupts
xor rbx, rbx ;infinite timeout
mwaitx rax, rcx, rbx ;not waiting; passes immediately
pop r9
pop rbx
ret

C++代码:

int main()
{
    void* data = VirtualAlloc(nullptr, size, MEM_COMMIT, PAGE_READONLY);
    //int x = 5;
    std::cout << data << '\n';
    monitorAddress(data);
    std::cout << data << '\n';

    VirtualFree(data, 0, MEM_RELEASE);
    return 0;
}

AMD 手册 (vol 3, rev 3.33) 中的文档 不是 ECX[0] = 0 会屏蔽中断,即使 [=70 中的 IF=1 =].如果 user-space 能够在没有 IO 特权级别 = 0 的情况下执行此操作(这将允许您 运行 一条 cli 指令),那将是疯狂的,并且措辞并不真正提示一下。

在 user-space 中,应该没有办法让 CPU 卡在内核难以唤醒的状态!如果你想在要求 OS 让这个线程进入睡眠状态之前花更长的时间(例如,使用 Linux futex 来唤醒你备份内存更改),你可以在循环与使用 pause 或其他东西的 spin-wait 循环完全一样。从 OS 的角度来看,情况是一样的:该线程一直占用 CPU。

您的代码很可能确实启用了监视器并进入优化的睡眠状态,但最多几毫秒后在下一个计时器中断时唤醒。 检查 rdtsc 看它休眠了多长时间,因为人类对屏幕输出的感知根本无法区分这与休眠失败。

文档实际上确实说明了 ECX 中支持的扩展标志:

Bit 0: When set, allows interrupts to wake MWAITX, even when eFLAGS.IF = 0. Support for this extension is indicated by a feature flag returned by the CPUID instruction.

因此,作为扩展,您可以覆盖 eFLAGS 中禁用中断的事实,以确保您不会进入持续到 NMI 的睡眠状态。否则,如果 ECX[0] = 0,文档中的所有先前内容均适用,包括:

Events that cause an exit from the monitor event pending state include:

  • A store from another processor matches the address range established by the MONITORX instruction.
  • The timer expires.
  • Any unmasked interrupt, including INTR, NMI, SMI, INIT.
  • RESET.
  • Any far control transfer that occurs between the MONITORX and the MWAITX.

如果您确实确实想要将CPU置于不会被未决中断结束的睡眠状态,您可以使用climonitorx / mwaitx 之前。或者,如果您处于正确的内核模式,则使用传统的 monitor / mwait,而不是在 Linux iopl() 系统调用或其他获取 IOPL 的方式之后使用 user-space =0 with CPL=3 (current privilege level), 所以你不能 运行 一般的特权指令,只有 IO 特权级别允许的特定指令,比如 in/out / cli/sti .

不幸的是:

There is no indication after exiting MWAITX of why the processor exited or if the timer expired. It is up to software to check whether the awaiting store has occurred, and if not, determining how much time has elapsed if it wants to re-establish the MONITORX with a new timer value.

顺便说一句,如果你不希望计时器成为可能的退出条件,你可以只留下 ECX[1] = 0

Bit 1: When set, EBX contains the maximum wait time expressed in Software P0 clocks, the same clocks counted by the TSC. Setting bit 1 but passing in a value of zero on EBX is equivalent to setting bit 1 to a zero. The timer will not be an exit condition.

顺便说一句,EAX=0 不是“没有提示”; EAX[7:4] 始终是所需的 C-state 级别,编码为 C-state - 1。因此 EAX=0 暗示您需要 C1 状态。 (为了暗示你想要 C0 状态,一种较浅的睡眠,可以更快地从中醒来,你可以设置 EAX = 0xf0,因为 F + 1 = 0。)


xor rax,rax代替xor eax,eax也是没有意义的;写入 32 位寄存器会隐式地将整个 64 位寄存器的高位置零,因此不存在错误依赖性。并且没有必要诱使汇编程序浪费 REX 前缀来实际对其进行编码。 MWAITX 隐式输入寄存器无论如何都是 32 位的,所以 xor ecx, ecx 也是合适的。

此外,在 Windows x64 调用约定中,r9 是 call-clobbered(又名 volatile);你可以在没有 saving/restoring 的情况下与 r8..r11.

一起使用

不,您不必每次都 运行 一个 cpuid monitorx / mwaitx! AMD 的文档说您需要为每个程序/库初始化检查一次,但是 CPU 无法真正执行它。它不会跟踪 user-space 进程实际上具有 运行 一个 CPUID 的上下文切换。

;; uint32_t waitx(void *p)
;; returns TSC ticks actually slept for
waitx:
    mov   rax, rcx    ;the address to monitor
    xor   ecx, ecx    ;no extensions
    xor   edx, edx    ;no hints
    monitorx rax, ecx, edx    ; or just monitorx if your assembler complains

    lfence
    rdtsc
    lfence            ; make sure we're done reading the clock before executing later instructions
    mov   r8d, eax    ; low half of start time.   We ignore the high half in EDX, assuming the time is less than 2^32, i.e. less than about 1 second.

    xor   eax, eax    ; hint = EAX[7:4] = 0 = C1 sleep state
                      ; ECX still 0: no TSC timeout in EBX, no overriding IF to 1
    mwaitx  eax, ecx

    rdtscp            ; EAX = low half of end time, EDX = high, ECX = core #
    sub     eax, r8d  ; retval = end - start
    ret

(如果 OS 启用了 Spectre 缓解功能位,LFENCE 将在 AMD CPUs 上序列化执行,为 lfence 提供类似于 Intel CPUs 的保证。否则它是一个 NOP在 AMD、IIRC 上。)