EvtSubscribe 回调中调用 Sleep 会导致事件遗漏吗?
结论
在 EvtSubscribe 的推送回调中调用 Sleep(5000),会阻塞后续事件的交付。Windows 文档明确提醒,回调执行期间,其他事件无法送达,因此回调应尽量简短。不要在回调中进行耗时分析、等待或慢速 I/O。应先快速复制事件数据,再交给工作线程处理。需要可靠补读时,可结合 bookmark 和查询恢复。
为什么会少收到事件
你的订阅使用了 EvtSubscribeToFutureEvents(代码中的标志值 1),只接收注册后到达且符合条件的事件。查询条件是 Event/System[EventID=1102],所以只有匹配该条件的事件才会触发回调。
每次回调都休眠 5 秒,期间后续事件无法继续交付给该回调。如果事件到达速度超过处理速度,延迟就会累积;日志记录也可能因清理或覆盖而变得不可用。仅凭这段测试输出,无法确认 149 次命令是否各自生成了可供订阅读取的匹配记录。可以检查 Security 日志中的实际记录,并确认订阅查询条件是否匹配。
代码中还有一个问题:printf("%d: Event triggered\\n"); 的格式字符串要求提供一个整数参数,但调用时没有传入,行为未定义。如果只想打印固定消息,可以改用 puts("Event triggered");;如果需要计数,则应传入有效的计数值,并妥善处理并发访问。
把回调改成快速入队
回调只需判断通知类型、提取必要数据并放入线程安全队列,然后立即返回。工作线程再从队列取出数据,执行耗时分析。
DWORD WINAPI SubscriptionCallback(
EVT_SUBSCRIBE_NOTIFY_ACTION action,
PVOID pContext,
EVT_HANDLE hEvent)
{
UNREFERENCED_PARAMETER(pContext);
if (action == EvtSubscribeActionError)
{
DWORD error = (DWORD)(ULONG_PTR)hEvent;
// 将订阅错误记录到线程安全的错误队列或日志中。
return ERROR_SUCCESS;
}
if (action == EvtSubscribeActionDeliver)
{
// 必须在回调返回前复制或渲染事件数据。
// RenderEventXml 和 EnqueueOwnedXml 代表应用自己的实现:
// 前者生成独立字符串,后者将字符串所有权交给工作队列。
wchar_t *xml = RenderEventXml(hEvent);
if (xml != NULL)
{
EnqueueOwnedXml(xml);
}
}
return ERROR_SUCCESS;
}
RenderEventXml 和 EnqueueOwnedXml 是示意函数,需要由应用实现。回调返回后,事件日志服务会关闭 hEvent,因此不能把这个句柄放进队列,让工作线程稍后再读取。回调返回前应完成渲染或复制。队列还需要设置容量并制定错误处理策略,避免消费者长期跟不上时内存持续增长。
需要补读时使用 bookmark
如果程序可能离线,或需要从已处理的位置继续读取,可以保存事件 bookmark,并在重启后从该位置恢复订阅,或使用 EvtQuery 查询后续记录。每分钟从头读取一次不够稳妥:这可能造成重复处理,也可能因日志记录已清理或覆盖而无法补回。应用需要确定 bookmark 的持久化时机,并让处理逻辑能够应对重复记录。
也可以使用 EvtSubscribeStrict 检查订阅期间是否出现记录缺失。收到 EvtSubscribeActionError,且错误为 ERROR_EVT_QUERY_RESULT_STALE 时,应启动补读或报告缺口。这个标志用于发现缺失,不会自动恢复记录。能否恢复仍取决于日志中是否还保留相关记录。
额外检查
- 在回调中区分
EvtSubscribeActionDeliver和EvtSubscribeActionError,不要把错误通知当作事件句柄处理。 - 如果需要接收订阅创建前已有的匹配记录,应选择合适的订阅起始标志或使用 bookmark。当前的
1只接收未来事件。 - 如果只需要事件对象通知,再由应用自行批量取事件,可以考虑使用
EvtSubscribe的 pull 模式,并通过EvtNext枚举结果。仍需及时消费并处理积压。
Microsoft 文档介绍了推送与拉取两种订阅方式、订阅起始标志,以及回调会阻塞其他事件交付的行为:Subscribing to Events、EvtSubscribe、EVT_SUBSCRIBE_CALLBACK。
备注:内容仅供参考。
