这是在任何来源上都没有真正明确解释的事情,有趣的是第二条评论 http://www.osronline.com/article.cfm?article=529也问同样的问题。
首先,DPC软件中断与常规SSDT软件中断不同,后者不会被推迟,并且以被动IRQL运行,并且可以随时中断。 DPC软件中断不使用int
or syscall
或类似的事情,被推迟并在调度级别运行。
研究了 ReactOS 内核和 WRK 之后,我现在确切地知道发生了什么
驱动程序,当它接收到IRP_MN_START_DEVICE
从 PnP 管理器,使用初始化一个中断对象IoConnectInterrupt https://github.com/Zer0Mem0ry/ntoskrnl/blob/master/Ke/amd64/intobj.c使用中的数据CM_RESOURCE_LIST
它在 IRP 中接收。特别令人感兴趣的是 PnP 管理器分配给设备的向量和亲和力(如果设备在其 PCIe 配置空间中公开 MSI 功能,则这很容易做到,因为它不必担心底层 IRQ 路由)。它将向量、指向 ISR 的指针、ISR 的上下文、IRQL 传递给IoConnectInterrupt
哪个调用KeInitializeInterrupt
使用参数初始化中断对象,然后调用KeConnectInterrupt
它将当前线程的关联性切换到目标处理器,锁定调度程序数据库并检查 IDT 条目是否指向 BugCheck 包装器KxUnexpectedInterrupt0[IdtIndex]
。如果是,则它将 IRQL 提高到 31,因此以下是原子操作,并使用 HAL API 启用由 LAPIC 上的 PnP 管理器映射的向量,并为其分配与 IRQL 相对应的 TPR 优先级。然后,它将向量映射到该向量的 IDT 条目中的处理程序地址。为此,它传递地址&Interrupt->DispatchCode[0]
进入 IDT 映射例程KeSetIdtHandlerAddress
。看来这是一个template https://reactos.org/archives/public/ros-diffs/2006-August/013809.html对于所有中断对象来说都是相同的,根据 WRK 是KiInterruptTemplate
。果然,检查ReactOS内核,我们看到在KeInitializeInterrupt
-- 这被称为IoConnectInterrupt
- 代码:
RtlCopyMemory(Interrupt->DispatchCode,
KiInterruptDispatchTemplate,
sizeof(Interrupt->DispatchCode));
KiInterruptDispatchTemplate
目前似乎是空白,因为 ReactOS 的 amd64 端口正处于早期开发阶段。然而,在 Windows 上它将被实现并且作为KiInterruptTemplate
.
然后它将 IRQL 降低回旧的 IRQL。如果 IDT 条目没有指向 BugCheck ISR,那么它会初始化一个链式中断——因为 IDT 条目已经有一个地址。它用CONTAINING_RECORD
通过其成员获取中断对象,处理程序的地址(DispatchCode[0]
)并将新的中断对象连接到已经存在的中断对象,初始化已经引用的中断对象LIST_ENTRY
作为列表的头部并通过设置将其标记为链式中断DispatchAddress
成员的地址KiChainedDispatch
。然后它会删除调度程序数据库自旋锁并将关联性切换回来并返回中断对象。
然后驱动程序设置一个 DPC——DeferredRoutine
作为设备对象的成员——使用IoInitializeDpcRequest
.
FORCEINLINE VOID IoInitializeDpcRequest ( _In_ PDEVICE_OBJECT DeviceObject, _In_ PIO_DPC_ROUTINE DpcRoutine )
KeInitializeDpc(&DeviceObject->Dpc,
(PKDEFERRED_ROUTINE) DpcRoutine,
DeviceObject);
KeInitializeDpc
calls KiInitializeDpc
这是硬编码的 https://doxygen.reactos.org/d1/d92/dpc_8c.html#ae288b59fa56efcc73de60cc1b6127923将优先级设置为中,这意味着KeInsertQueueDpc
会将其放置在 DPC 队列的中间。KeSetImportanceDpc
and KeSetTargetProcessorDpc
可以在调用后使用来分别设置生成的返回 DPC 的优先级和目标处理器。它将 DPC 对象复制到设备对象的成员,如果已经存在 DPC 对象,则将其排队到已存在的 DPC 中。
当中断发生时,KiInterruptTemplate
中断对象的模板是被调用的 IDT 中的地址,然后将哪一个是DispatchAddress
成员将是KiInterruptDispatch
对于正常中断或KiChainedDispatch
对于链式中断。它将中断对象传递给KiInterruptDispatch
(它能做到这一点是因为,正如我们之前看到的,RtlCopyMemory
copied KiInterruptTemplate
进入中断对象,这意味着它可以使用具有相对 RIP 的 asm 块来获取它所属的中断对象的地址(它也可以尝试对CONTAINING_RECORD
函数)但是intsup.asm
包含以下代码来执行此操作:lea rbp, KiInterruptTemplate - InDispatchCode ; get interrupt object address jmp qword ptr InDispatchAddress[rbp]; finish in common code
). KiInterruptDispatch
然后将获取中断的自旋锁,可能使用KeAcquireInterruptSpinLock
。情监侦(ServiceContext
) calls IoRequestDpc
为设备和 ISR 创建的设备对象地址作为参数,以及中断特定上下文和可选的 IRP(我猜它是从头部获得的)DeviceObject->Irp
如果例程旨在处理 IRP)。我希望它是一个单行包装器KeInsertQueue
但传递设备对象的 Dpc 成员,这正是它的本质:KeInsertQueueDpc(&DeviceObject->Dpc, Irp, Context);
。首先KeInsertQueue
将 ISR 的设备 IRQL 的 IRQL 提高到 31,从而防止所有抢占。WRK https://github.com/markjandrews/wrk-v1.2/blob/master/base/ntos/ke/dpcobj.c第 263 行包含以下内容dpcobj.c
:
#if !defined(NT_UP)
if (Dpc->Number >= MAXIMUM_PROCESSORS) {
Number = Dpc->Number - MAXIMUM_PROCESSORS;
TargetPrcb = KiProcessorBlock[Number];
} else {
Number = CurrentPrcb->Number;
TargetPrcb = CurrentPrcb;
}
这表明DPC->Number
成员必须由以下设置KeSetTargetProcessorDpc
作为目标核心数 + 最大处理器数。这很奇怪,果然我去查看了 ReactOS 的KeSetTargetProcessorDpc
确实如此!KiProcessorBlock
似乎是一个用于快速访问每个内核的 KPRCB 结构的内核结构。
然后它使用以下命令获取核心的正常 DPC 队列自旋锁DpcData = KiSelectDpcData(TargetPrcb, Dpc)
返回&Prcb->DpcData[DPC_NORMAL]
因为它传递给它的 DPC 类型是普通的,而不是线程的。然后它获取队列的自旋锁,这在 ReactOS 上似乎是一个空函数体,我认为这是因为:
/* 在 UP 版本中,自旋锁在 IRQL >= DISPATCH 处不存在 */
这是有道理的,因为 ReactOS 仅支持 1 个核心,这意味着另一个核心上没有可以访问 DPC 队列的线程(一个核心可能有一个用于该核心队列的目标 DPC)。只有一个DPC队列。如果它是多核系统,则必须获取自旋锁,因此这些看起来是实现多核功能时的占位符。如果它未能获取 DPC 队列的自旋锁,那么它将要么在 IRQL 31 处自旋等待,要么下降到中断本身的 IRQL 并自旋等待,从而允许内核发生其他中断,但不允许其他线程在内核上运行。
请注意,Windows 将使用KeAcquireSpinLockAtDpcLevel
要获取这个自旋锁,ReactOS 没有。KeAcquireSpinLockAtDpcLevel
不触及IRQL https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-keacquirespinlockatdpclevel。虽然,在WRK中它直接使用KiAcquireSpinLock
可以在第275行看到dpcobj.c
它只获取自旋锁,对 IRQL 不执行任何操作(KiAcquireSpinLock(&DpcData->DpcLock);
).
获取自旋锁后,它首先确保 DPC 对象尚未在队列中(DpcData
当执行以下操作时,成员将为空cmpxchg
初始化它与DpcData
从返回KiSelectDpcData(TargetPrcb, Dpc)
) 如果是,则丢弃自旋锁并返回;否则,它会设置 DPC 成员以指向所传递的中断特定上下文,然后将其插入到队列的头部(InsertHeadList(&DpcData->DpcListHead, &Dpc->DpcListEntry);
)或尾部(InsertTailList(&DpcData->DpcListHead, &Dpc->DpcListEntry);
)基于其优先级(if (Dpc->Importance == HighImportance)
)。然后它会确保 DPC 尚未执行if (!(Prcb->DpcRoutineActive) && !(Prcb->DpcInterruptRequested))
。然后它检查是否KiSelectDpcData
返回第二个KDPC_DATA
结构,即 DPC 为螺纹类型(if (DpcData == &TargetPrcb->DpcData[DPC_THREADED])
)如果是并且if ((TargetPrcb->DpcThreadActive == FALSE) && (TargetPrcb->DpcThreadRequested == FALSE))
然后它会锁定xchg
to set TargetPrcb->DpcSetEventRequest
分别为 true 然后设置TargetPrcb->DpcThreadRequested
and TargetPrcb->QuantumEnd
为 true 并设置RequestInterrupt
如果目标 PRCB 是当前 PRCB,则设置为 true,否则仅当目标核心不空闲时才将其设置为 true。
现在到了原来问题的关键。 WRK 现在包含以下代码:
#if !defined(NT_UP)
if (CurrentPrcb != TargetPrcb) {
if (((Dpc->Importance == HighImportance) ||
(DpcData->DpcQueueDepth >= TargetPrcb->MaximumDpcQueueDepth))) {
if (((KiIdleSummary & AFFINITY_MASK(Number)) == 0) ||
(KeIsIdleHaltSet(TargetPrcb, Number) != FALSE)) {
TargetPrcb->DpcInterruptRequested = TRUE;
RequestInterrupt = TRUE;
}
}
} else {
if ((Dpc->Importance != LowImportance) ||
(DpcData->DpcQueueDepth >= TargetPrcb->MaximumDpcQueueDepth) ||
(TargetPrcb->DpcRequestRate < TargetPrcb->MinimumDpcRate)) {
TargetPrcb->DpcInterruptRequested = TRUE;
RequestInterrupt = TRUE;
}
}
#endif
本质上,在多处理器系统上,如果从 DPC 对象获取的目标核心不是线程的当前核心,则: 如果 DPC 非常重要或者超过最大队列深度且逻辑and
目标关联性和空闲核心的比率为 0(即目标核心不空闲)并且(好吧,KeIsIdleHaltSet
看起来是完全相同的事情(它检查目标 PRCB 中的休眠标志))它设置了一个DpcInterruptRequested
目标核心的 PRCB 中的标志。如果 DPC 的目标是当前核心,则如果 DPC 的重要性不低(注意:这将允许中等重要性!),或者如果 DPC 队列深度超过最大队列深度,并且核心上的 DPC 请求率没有增加t 超过最小值 it在当前核心的 PRCB 中设置一个标志以表明存在 DPC。
现在它释放 DPC 队列自旋锁:KiReleaseSpinLock(&DpcData->DpcLock);
(#if !defined(NT_UP)
当然)(这不会改变 IRQL)。然后它检查程序是否请求中断(if (RequestInterrupt == TRUE)
),那么如果是单处理器系统(#if defined(NT_UP)
)它只是简单地调用KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
但如果是多核系统,则需要检查目标 PRCB 以查看是否需要发送 IPI。
if (TargetPrcb != CurrentPrcb) {
KiSendSoftwareInterrupt(AFFINITY_MASK(Number), DISPATCH_LEVEL);
} else {
KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
}
它本身就说明了它的作用;如果当前 PRCB 不是 DPC 的目标 PRCB,则它发送 IPIDISPATCH_LEVEL
使用处理器编号的优先级KiSendSoftwareInterrupt
;否则,它使用KiRequestSoftwareInterrupt
。根本没有文档,但我猜测这是一个 Self IPI,它将包装一个 HAL 函数,该函数对 ICR 进行编程,以调度级别优先级向自身发送 IPI(我的推理是 ReactOS 在这个阶段调用HalRequestSoftwareInterrupt https://doxygen.reactos.org/d2/d8f/hal_2halarm_2generic_2pic_8c.html#ac06227bbf507880cbff06875d4bdcc97这显示了未实现的 PIC 写入)。所以这不是软件中断INT
有意义,但实际上,简单地说,是一个硬件中断。然后,它将 IRQL 从 31 降低回之前的 IRQL(即 ISR IRQL)。然后它返回到ISR,然后它会返回到KiInterruptDispatch
; KiInterruptDispatch
然后将使用释放 ISR 自旋锁KeReleaseInterruptSpinLock
这会将 IRQL 减少到中断之前的状态,然后弹出陷阱帧,但我本以为它会首先弹出陷阱帧,然后对 LAPIC TPR 进行编程,因此寄存器恢复过程是原子的,但我认为它不会这真的不重要。
ReactOS 有以下内容(WRK 没有KeReleaseSpinlock
或记录的 IRQL 降低程序,因此这是我们拥有的最好的程序):
VOID NTAPI KeReleaseSpinLock ( KIRQL NewIrql )
{
/* Release the lock and lower IRQL back */
KxReleaseSpinLock(SpinLock);
KeLowerIrql(OldIrql);
}
VOID FASTCALL KfReleaseSpinLock ( PKSPIN_LOCK SpinLock, KIRQL OldIrql )
{
/* Simply lower IRQL back */
KeLowerIrql(OldIrql);
}
KeLowerIrql 是 HAL 函数 KfLowerIrql 的包装器,该函数包含KfLowerIrql(OldIrql);
就是这样。
VOID FASTCALL KfLowerIrql ( KIRQL NewIrql )
{
DPRINT("KfLowerIrql(NewIrql %d)\n", NewIrql);
if (NewIrql > KeGetPcr()->Irql)
{
DbgPrint ("(%s:%d) NewIrql %x CurrentIrql %x\n",
__FILE__, __LINE__, NewIrql, KeGetPcr()->Irql);
KeBugCheck(IRQL_NOT_LESS_OR_EQUAL);
for(;;);
}
HalpLowerIrql(NewIrql);
}
该函数基本上可以防止新的 IRQL 高于当前 IRQL,这是有道理的,因为该函数应该降低 IRQL。如果一切正常,该函数将调用HalpLowerIrql(NewIrql);
这是多处理器 AMD64 实现的框架——它实际上并没有实现 APIC 寄存器写入(或 x2APIC 的 MSR),它们是 ReactOS 的多处理器 AMD64 实现上的空函数,因为它正在开发中;但在 Windows 上,他们不会,他们实际上会对 LAPIC TPR 进行编程,以便现在可以发生排队的软件中断。
HalpLowerIrql(KIRQL NewIrql, BOOLEAN FromHalEndSystemInterrupt)
{
ULONG Flags;
UCHAR DpcRequested;
if (NewIrql >= DISPATCH_LEVEL)
{
KeSetCurrentIrql (NewIrql);
APICWrite(APIC_TPR, IRQL2TPR (NewIrql) & APIC_TPR_PRI);
return;
}
Flags = __readeflags();
if (KeGetCurrentIrql() > APC_LEVEL)
{
KeSetCurrentIrql (DISPATCH_LEVEL);
APICWrite(APIC_TPR, IRQL2TPR (DISPATCH_LEVEL) & APIC_TPR_PRI);
DpcRequested = __readfsbyte(FIELD_OFFSET(KIPCR, HalReserved[HAL_DPC_REQUEST]));
if (FromHalEndSystemInterrupt || DpcRequested)
{
__writefsbyte(FIELD_OFFSET(KIPCR, HalReserved[HAL_DPC_REQUEST]), 0);
_enable();
KiDispatchInterrupt();
if (!(Flags & EFLAGS_INTERRUPT_MASK))
{
_disable();
}
}
KeSetCurrentIrql (APC_LEVEL);
}
if (NewIrql == APC_LEVEL)
{
return;
}
if (KeGetCurrentThread () != NULL &&
KeGetCurrentThread ()->ApcState.KernelApcPending)
{
_enable();
KiDeliverApc(KernelMode, NULL, NULL);
if (!(Flags & EFLAGS_INTERRUPT_MASK))
{
_disable();
}
}
KeSetCurrentIrql (PASSIVE_LEVEL);
}
首先,它检查新的 IRQL 是否高于调度级别,如果是,则将其设置为正常并写入 LAPIC TPR 寄存器并返回。如果不是,它会检查当前 IRQL 是否为调度级别(>APC_LEVEL
)。这意味着根据定义,新的 IRQL 将是less高于调度级别。我们可以看到,在这种情况下,它等于DISPATCH_LEVEL
而不是让它低于并将其写入 LAPIC TPR 寄存器。然后它检查是HalReserved[HAL_DPC_REQUEST]
这似乎是 ReactOS 使用的而不是DpcInterruptRequested
我们之前看到过,所以只需用它替换即可。然后将其设置为 0(请注意,PCR 从内核模式下 FS 段所指向的段描述符的开头开始)。然后它启用中断和调用KiDispatchInterrupt
之后如果eflags
寄存器在期间改变了IF标志KiDispatchInterrupt
它禁用中断。然后,在最终将 IRQL 设置为被动级别之前,它还会检查内核 APC 是否处于挂起状态(这超出了本说明的范围)
VOID NTAPI KiDispatchInterrupt ( VOID )
{
PKIPCR Pcr = (PKIPCR)KeGetPcr();
PKPRCB Prcb = &Pcr->Prcb;
PKTHREAD NewThread, OldThread;
/* Disable interrupts */
_disable();
/* Check for pending timers, pending DPCs, or pending ready threads */
if ((Prcb->DpcData[0].DpcQueueDepth) ||
(Prcb->TimerRequest) ||
(Prcb->DeferredReadyListHead.Next))
{
/* Retire DPCs while under the DPC stack */
//KiRetireDpcListInDpcStack(Prcb, Prcb->DpcStack);
// FIXME!!! //
KiRetireDpcList(Prcb);
}
/* Re-enable interrupts */
_enable();
/* Check for quantum end */
if (Prcb->QuantumEnd)
{
/* Handle quantum end */
Prcb->QuantumEnd = FALSE;
KiQuantumEnd();
}
else if (Prcb->NextThread)
{
/* Capture current thread data */
OldThread = Prcb->CurrentThread;
NewThread = Prcb->NextThread;
/* Set new thread data */
Prcb->NextThread = NULL;
Prcb->CurrentThread = NewThread;
/* The thread is now running */
NewThread->State = Running;
OldThread->WaitReason = WrDispatchInt;
/* Make the old thread ready */
KxQueueReadyThread(OldThread, Prcb);
/* Swap to the new thread */
KiSwapContext(APC_LEVEL, OldThread);
}
}
首先,它禁用中断 _disable 只是一个 asm 块的包装器,它清除 IF 标志,并在 clobber 列表中拥有内存和 cc(以防止编译器重新排序)。但这看起来像arm语法。
{
__asm__ __volatile__
(
"cpsid i @ __cli" : : : "memory", "cc"
);
}
这确保了它可以以不间断的过程耗尽 DPC 队列;与禁用中断一样,它不能被时钟中断打断并重新调度。这可以防止 2 个调度程序同时运行的情况,例如,如果一个线程产生了Sleep()
它最终调用KeRaiseIrqlToSynchLevel
这类似于禁用中断。这将防止计时器中断中断它并在当前执行的线程切换过程之上调度另一个线程切换 - 它确保调度是原子的。
它检查当前核心的普通队列上是否有DPC,或者是否有定时器到期或延迟就绪线程,然后调用KiRetireDpcList https://doxygen.reactos.org/d9/d85/ntoskrnl_2include_2internal_2ke_8h.html#afe4303889868fcf9b40b0361859d87df它基本上包含一个 while 队列深度 != 0 循环,它首先检查它是否是一个计时器到期请求(我现在不会讨论),如果不是,则获取 DPC 队列自旋锁,从队列中取出一个 DPC 并将成员解析为参数(中断仍然禁用),减少队列深度,删除自旋锁,启用中断并调用DeferredRoutine
。当。。。的时候DeferredRoutine
返回,它再次禁用中断,如果队列中有更多中断,它会重新获取自旋锁(禁用自旋锁和中断可确保从队列中删除 DPC 是原子的,以便另一个中断和 DPC 队列排出无法在同一个 DPC 上工作 -它将已经从队列中删除)。由于 DPC 队列自旋锁尚未在 ReactOS 上实现,我们可以假设 Windows 上可能会发生什么:如果它无法获取自旋锁,那么假设它是一个自旋锁,并且我们仍然处于DISPATCH_LEVEL
并且中断被禁用,它会旋转,直到其他核心上的线程调用KeReleaseSpinLockFromDpcLevel(&DpcData->DpcLock);
这并没有那么大的阻碍,因为每个线程都有大约 100 uops 的自旋锁,所以我们可以在以下位置禁用中断:DISPATCH_LEVEL
.
请注意,排出过程仅排出当前核心的队列。当 DPC 队列为空时,它会重新启用中断并检查是否有任何延迟就绪线程并使它们全部就绪。然后它沿着调用链返回到KiInterruptTemplate
然后ISR正式结束。
因此,作为概述,在KeInsertQueuedpc
,如果要排队的 DPC 是另一个核心,并且它具有高优先级,或者队列深度超过 PRCB 中定义的最大值,则它会在核心的 PRCB 中设置 DpcRequested 标志,并向最有可能运行的核心发送 IPIKiDispatchInterrupt
以某种方式(ISR 可能只是 IRQL 较低的过程,它确实调用KiDispatchinterrupt
) 这将耗尽该核心上的 DPC 队列;实际调用的包装器KiDispatchinterrupt
可能会或可能不会禁用 PRCB 中的 DpcRequested 标志,例如HalpLowerIrql
是,但我不知道,可能确实是HalpLowerIrql
正如我所建议的。后KeInsertQueuedpc
,当它降低 IRQL 时,不会发生任何事情,因为 DpcRequested 标志位于另一个内核中,而不是当前内核中。如果要排队的 DPC 以当前核心为目标,则如果它具有高或中优先级,或者队列深度已超过最大队列深度,并且 DPC 速率小于 PRCB 中定义的最小速率,则它会设置 DpcRequested 标志在 PRCB 中并请求一个自我 IPI,它将调用调度程序使用的相同通用包装器,因此可能类似于HalpLowerIrql
. After KeInsertQueuedpc
它降低了 IRQLHalpLowerIrql
并看到DpcRequested
因此在降低 IRQL 之前先清空当前核心的队列。
你看到这有问题吗? WRK 显示正在请求“软件”中断(其 ISR 可能调用KiDispatchInterrupt
因为它是一个多用途函数,并且只有一个函数被使用过:KiRequestSoftwareInterrupt(DISPATCH_LEVEL) in all scenarios
)但随后 ReactOS 显示KiDispatchInterrupt
当 IRQL 下降时被调用as well。你会期望当KiInterruptDispatch
删除 ISR 自旋锁,执行此操作的函数将仅检查延迟就绪线程或计时器到期请求,然后删除 IRQL,因为一旦 LAPIC TPR 被编程,就会发生用于排空队列的软件中断,但 ReactOS 实际上会检查队列上的项目(使用 PRCB 上的标志)并在过程中启动队列排空以降低 IRQL。没有用于自旋锁释放的 WRK 源代码,但我们假设它只是不执行 ReactOS 上发生的事情,而是让“软件”中断处理它——也许它会将整个 DPC 队列检查出其等价物HalpLowerIrql
。但等一下,这是什么Prcb->DpcInterruptRequested
那么如果它不像 ReactOS 那样用于启动队列耗尽呢?也许它只是用作控制变量,以便它不会对 2 个软件中断进行排队。我们还注意到 ReactOS 还在此阶段请求“软件”中断 https://doxygen.reactos.org/d1/d92/dpc_8c.html#a0af285a1d39243f12185b860548b260c(对于arm的向量中断控制器)这非常奇怪。所以也许不是那时。这明显表明它被调用了两次。看起来它耗尽了队列,然后当 IRQL 丢弃后立即出现“软件”中断(很可能也调用KiRetireDpcList
在某个阶段)在 ReactOS 和 WRK 上都做同样的事情。我想知道有人对此有何看法。我的意思是为什么既要Self IPI,又要排空队列?其中一项操作是多余的。
至于惰性IRQL。我在 WRK 或 ReactOS 上没有看到任何证据,但它的实施地点是KiInterruptDispatch
。可以使用以下命令获取当前的 IRQLKeGetCurrentIrql
然后将其与中断对象的IRQL进行比较and then对 TPR 进行编程以对应于当前的 IRQL。它要么静默中断并使用自 IPI 为该向量排队另一个中断,要么只是简单地切换陷阱帧。