第 7 章 视窗报文 --- 7.2 视窗报文的接收
本节深入剖析 Windows/ReactOS 中"视窗报文"(Window Message)接收机制的完整实现,重点讲解 GetMessage/PeekMessage/DispatchMessage 的内核态处理流程。
概述
视窗报文的"接收"是消息循环(Message Loop)的核心。Windows GUI 应用程序的每一个线程都需要一个消息循环:
c
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
这个看似简单的循环背后,win32k 内核需要处理:
- 投递消息(Posted Messages)------ PostMessage 投递
- 发送消息(Sent Messages)------ SendMessage 跨线程发送
- 硬件消息(Hardware Messages)------ 键盘/鼠标原始输入
- 内部事件(Internal Events)------ 系统内部触发的事件
- WM_PAINT 消息 ------ 绘制消息
- WM_TIMER 消息 ------ 定时器消息
- WM_QUIT 消息 ------ 退出消息
- 各种 hook 处理
视窗报文接收的本质是什么?
报文接收本质上是"消息分类与优先级调度":win32k 按照 MSDN 规定的优先级(SentMessages → PostedMessages → Hardware → SentMessages → Paint → Timer)依次扫描各种消息源,在合适的时机唤醒等待的线程,并通过 DispatchMessage 调用窗口过程。
想象一个邮递系统场景:
- 投递消息(Posted):标准信封,按时间顺序放进收件人邮箱;
- 发送消息(Sent):加急快递,需要收件人当面签收并带回执;
- 硬件消息(Hardware):实时事件流(电话铃响),不能延迟;
- 系统事件(Internal):内部通知(公司公告),不投递但需要立即处理;
- WM_PAINT:重绘请求(清洁工来打扫),按需触发;
- GetMessage:收件人去邮箱取件,按优先级逐封检查;
- DispatchMessage:收件人拆开信封并按内容行事(调用 WndProc)。
本节内容概览
- 7.2.0 框架图:报文接收完整架构;
- 7.2.1 报文分类体系:5 种消息源与优先级;
- 7.2.2 USER_MESSAGE 结构:投递消息内核态表示;
- 7.2.3 GetMessage 完整流程:NtUserGetMessage 实现;
- 7.2.4 co_IntGetPeekMessage 调度核心:5 种消息源依次扫描;
- 7.2.5 co_MsqPeekHardwareMessage:硬件消息处理;
- 7.2.6 co_IntProcessMouseMessage:鼠标消息路由与双击判定;
- 7.2.7 co_IntProcessKeyboardMessage:键盘消息处理;
- 7.2.8 IntDispatchMessage:分发到 WndProc;
- 7.2.9 等待新消息:co_MsqWaitForNewMessages;
- 7.2.10 设计哲学问答:6 个关键设计问题解答。
学习目标
读完本节后,读者应当能够:
- 理解 Windows 5 种消息源(Sent/Posted/Hardware/Internal/Paint)的优先级与处理顺序;
- 掌握 GetMessage/PeekMessage 内核态实现细节;
- 分析 DispatchMessage 调用 WndProc 的完整流程;
- 理解硬件消息的捕获与分发机制;
- 解释双击判定、键盘状态更新等细节;
- 掌握消息循环的"等待-唤醒"机制。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| win32k.sys/ntuser | 消息接收、分发、队列管理 |
| win32k.sys/ntuser | 硬件消息处理、键盘/鼠标路由 |
| user32.dll | 用户态 GetMessage/PeekMessage/DispatchMessage 包装 |
| ntoskrnl/ke | 事件对象、线程等待与唤醒 |
7.2.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ 视窗报文接收完整架构 │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 用户态层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ user32.dll │ │
│ │ ├─► GetMessageW / GetMessageA (阻塞式获取) │ │
│ │ ├─► PeekMessageW / PeekMessageA (非阻塞式) │ │
│ │ ├─► DispatchMessageW / DispatchMessageA (分发给 WndProc) │ │
│ │ └─► TranslateMessage (翻译键盘消息) │ │
│ │ │ │ │
│ │ ▼ (通过 NtUserMessageCall / NtUserGetMessage / NtUserPeekMessage) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (syscall) │
│ 内核态层 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ win32k.sys/ntuser/message.c │ │
│ │ ├─► NtUserGetMessage ──► co_IntGetPeekMessage(bGMSG=TRUE) │ │
│ │ ├─► NtUserPeekMessage ──► co_IntGetPeekMessage(bGMSG=FALSE) │ │
│ │ ├─► NtUserDispatchMessage ──► IntDispatchMessage │ │
│ │ └─► co_IntWaitMessage ──► co_MsqWaitForNewMessages │ │
│ │ │ │
│ │ win32k.sys/ntuser/msgqueue.c │ │
│ │ ├─► MsqPeekMessage (投递消息扫描) │ │
│ │ ├─► co_MsqPeekHardwareMessage (硬件消息扫描) │ │
│ │ ├─► co_MsqDispatchOneSentMessage (发送消息分发) │ │
│ │ ├─► co_MsqWaitForNewMessages (等待新消息 - 阻塞) │ │
│ │ ├─► co_IntProcessMouseMessage (鼠标消息路由) │ │
│ │ ├─► co_IntProcessKeyboardMessage (键盘消息处理) │ │
│ │ └─► co_MsqInsertMouseMessage (插入硬件鼠标消息) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 消息源 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 发送消息链表 pti->SentMessagesListHead (SendMessage 跨线程) │ │
│ │ 2. 投递消息链表 pti->PostedMessagesListHead (PostMessage 异步) │ │
│ │ 3. 硬件消息链表 pti->MessageQueue->HardwareMessagesListHead │ │
│ │ 4. 内部事件链表 (WM_ASYNC_*, 系统通知) │ │
│ │ 5. 绘制消息 (pWnd->cPaintsReady > 0) │ │
│ │ 6. 定时器消息 (pWnd->spwndTimer list) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 优先级顺序(MSDN 规定) │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ ① 发送消息 (Sent) ── 必须立即处理,否则可能死锁 │ │
│ │ ② 投递消息 (Posted) ── PostMessage、SendNotifyMessage │ │
│ │ ③ 硬件消息 (Hardware) ── 键盘、鼠标原始输入 │ │
│ │ ④ 内部事件 (Internal) ── 窗口激活、焦点变化等系统事件 │ │
│ │ ⑤ 发送消息 (Sent) 再次 ── 避免发送消息被长时间阻塞 │ │
│ │ ⑥ 绘制消息 (Paint) ── WM_PAINT、WM_NCPAINT │ │
│ │ ⑦ 定时器消息 (Timer) ── WM_TIMER、WM_SYSTIMER │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
7.2.1 报文分类体系
7.2.1.1 5 种消息源
Windows 报文(消息)按产生方式和处理方式分为 5 类:
| 消息类型 | 产生方式 | 异步性 | 示例 |
|---|---|---|---|
| 发送消息(Sent) | SendMessage(直接调用 WndProc)/ 跨线程发送 | 同步(可阻塞) | WM_SETTEXT、WM_COPYDATA |
| 投递消息(Posted) | PostMessage 投递 | 异步 | WM_USER+1、自定义消息 |
| 硬件消息(Hardware) | 键盘/鼠标原始输入 | 异步 | WM_KEYDOWN、WM_MOUSEMOVE |
| 系统事件(Internal) | win32k 内部事件 | 异步 | WM_ASYNC_SETACTIVEWINDOW |
| 绘制消息(Paint) | 窗口区域失效 | 按需 | WM_PAINT、WM_NCPAINT |
7.2.1.2 投递消息(USER_MESSAGE)
win32ss/user/ntuser/msgqueue.h:8(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h#L8):
c
typedef struct _USER_MESSAGE
{
LIST_ENTRY ListEntry; // 链表节点
MSG Msg; // 用户态可见的 MSG 结构
DWORD QS_Flags; // 该消息对应的 QS_* 标志
LONG_PTR ExtraInfo; // 消息附加信息
DWORD dwQEvent; // 队列事件类型
PTHREADINFO pti; // 接收者线程
} USER_MESSAGE, *PUSER_MESSAGE;
MSG 是用户态可见的标准消息结构:
c
typedef struct tagMSG
{
HWND hwnd; // 目标窗口
UINT message; // 消息 ID
WPARAM wParam; // 参数 1
LPARAM lParam; // 参数 2
DWORD time; // 时间戳
POINT pt; // 鼠标位置
DWORD lPrivate; // 私有数据
} MSG, *PMSG;
7.2.1.3 发送消息(USER_SENT_MESSAGE)
win32ss/user/ntuser/msgqueue.h:20(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h#L20):
c
typedef struct _USER_SENT_MESSAGE
{
LIST_ENTRY ListEntry;
MSG Msg;
DWORD QS_Flags; // 原始 QS 位
PKEVENT pkCompletionEvent; // 同步完成事件
LRESULT lResult; // WndProc 返回值
DWORD flags; // 状态标志
PTHREADINFO ptiSender; // 发送者
PTHREADINFO ptiReceiver; // 接收者
SENDASYNCPROC CompletionCallback; // 异步完成回调
PTHREADINFO ptiCallBackSender;
ULONG_PTR CompletionCallbackContext;
INT HookMessage;
BOOL HasPackedLParam;
KEVENT CompletionEvent; // 内嵌完成事件
} USER_SENT_MESSAGE, *PUSER_SENT_MESSAGE;
SMF_* 标志(msgqueue.h:38(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h#L38)):
c
#define SMF_RECEIVERDIED 0x00000002
#define SMF_SENDERDIED 0x00000004
#define SMF_RECEIVERFREE 0x00000008
#define SMF_RECEIVEDMESSAGE 0x00000010
#define SMF_RECEIVERBUSY 0x00004000
7.2.1.4 QS_* 标志
QS_* 标志(Queue Status)表示线程消息队列中"可能有"的消息类型:
c
#define QS_KEY 0x0001
#define QS_MOUSEMOVE 0x0002
#define QS_MOUSEBUTTON 0x0004
#define QS_POSTMESSAGE 0x0008
#define QS_TIMER 0x0010
#define QS_PAINT 0x0020
#define QS_SENDMESSAGE 0x0040
#define QS_HOTKEY 0x0080
#define QS_ALLPOSTMESSAGE 0x0100
#define QS_RAWINPUT 0x0400
#define QS_TOUCH 0x0800
#define QS_POINTER 0x1000
#define QS_EVENT 0x2000
// 组合宏
#define QS_INPUT (QS_MOUSEMOVE | QS_MOUSEBUTTON | QS_KEY)
#define QS_ALLINPUT (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE)
每个消息类型对应一个 QS 位,GetMessage/PeekMessage 可以根据 QS 位快速过滤。
7.2.2 USER_MESSAGE 内核表示
7.2.2.1 MsqCreateMessage
MsqCreateMessage 位于 win32ss/user/ntuser/msgqueue.c:730(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L730):
c
PUSER_MESSAGE FASTCALL
MsqCreateMessage(LPMSG Msg)
{
PUSER_MESSAGE Message;
/* Allocate from the paged lookaside list */
Message = ExAllocateFromPagedLookasideList(pgMessageLookasideList);
if (!Message)
{
ERR("MsqCreateMessage() - Unable to allocate message\n");
return NULL;
}
/* Initialize the message */
RtlCopyMemory(&Message->Msg, Msg, sizeof(MSG));
Message->pti = NULL;
Message->QS_Flags = 0;
Message->ExtraInfo = 0;
Message->dwQEvent = 0;
return Message;
}
关键点:
- 使用 PagedLookasideList 加速分配(消息分配极频繁);
- 复制 MSG 内容并清零其他字段;
- 接收者 pti 在
MsqPostMessage中设置。
7.2.2.2 MsqPostMessage
MsqPostMessage 位于 win32ss/user/ntuser/msgqueue.c:1336(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1336):
c
VOID FASTCALL
MsqPostMessage(PTHREADINFO pti,
MSG* Msg,
BOOLEAN HardwareMessage,
DWORD QS_Flags,
DWORD QEvent,
LONG_PTR ExtraInfo)
{
PUSER_MESSAGE Message;
Message = MsqCreateMessage(Msg);
if (!Message) return;
/* Set the message fields */
Message->QS_Flags = QS_Flags;
Message->dwQEvent = QEvent;
Message->ExtraInfo = ExtraInfo;
Message->pti = pti;
/* Insert into the appropriate list */
if (HardwareMessage)
{
InsertTailList(&pti->MessageQueue->HardwareMessagesListHead,
&Message->ListEntry);
}
else
{
InsertTailList(&pti->PostedMessagesListHead, &Message->ListEntry);
}
/* Update QS flags and wake the queue */
if (QS_Flags)
{
SetMsgBitsMask(pti, QS_Flags);
}
pti->pcti->fsChangeBits |= QS_Flags;
/* Wake the thread's message queue event */
KeSetEvent(pti->pEventQueueServer, IO_NO_INCREMENT, FALSE);
}
关键点:
- 分配 USER_MESSAGE;
- 根据 HardwareMessage 标志选择插入硬件链表或投递链表;
- 设置 QS_Flags 唤醒队列(更新 pcti->fsChangeBits);
- KeSetEvent 唤醒等待 GetMessage 的线程。
7.2.2.3 消息链表组织
每个线程的消息队列维护 3 个核心链表:
┌─────────────────────────────────────────────────────────────┐
│ PTHREADINFO │
│ ├─► PostedMessagesListHead ─► 投递消息链表 │
│ └─► SentMessagesListHead ─► 发送消息链表 │
│ │
│ USER_MESSAGE_QUEUE.MessageQueue │
│ └─► HardwareMessagesListHead ─► 硬件消息链表 │
└─────────────────────────────────────────────────────────────┘
7.2.2.4 QS_Flags 与 PCTI 镜像
每个线程的 PCTI(Per-Thread Info,即 TEB 的 win32 区域)维护 QS 标志位:
c
typedef struct _CLIENTINFO
{
// ...
DWORD fsWakeBits; // 当前可用的 QS 位(实时状态)
DWORD fsChangeBits; // 新到达的 QS 位(未读取)
DWORD fsWakeMask; // 当前等待的 QS 位
DWORD fsWakeMaskJournaling; // 唤醒掩码日志
} CLIENTINFO, *PCLIENTINFO;
fsWakeBits:线程当前"持有"的消息类型;fsChangeBits:自上次 GetMessage 以来新增的消息类型;fsWakeMask:当前等待的 QS 位(被 GetMessage 阻塞时设置)。
7.2.3 GetMessage 完整流程
7.2.3.1 NtUserGetMessage 入口
NtUserGetMessage 位于 win32ss/user/ntuser/message.c:2342(file:///d:/reactos/win32ss/user/ntuser/message.c#L2342):
c
BOOL APIENTRY
NtUserGetMessage(PMSG pMsg,
HWND hWnd,
UINT MsgFilterMin,
UINT MsgFilterMax)
{
BOOL Res;
UserEnterShared();
Res = co_IntGetPeekMessage(pMsg,
hWnd,
MsgFilterMin,
MsgFilterMax,
PM_REMOVE,
TRUE); // bGMSG = TRUE (GetMessage)
UserLeave();
return Res;
}
GetMessage 与 PeekMessage 的核心区别:
- GetMessage:bGMSG = TRUE,表示阻塞模式,无消息时挂起线程;
- PeekMessage:bGMSG = FALSE,表示非阻塞,无消息时立即返回 FALSE。
7.2.3.2 NtUserPeekMessage 入口
c
BOOL APIENTRY
NtUserPeekMessage(PMSG pMsg,
HWND hWnd,
UINT MsgFilterMin,
UINT MsgFilterMax,
UINT RemoveMsg)
{
BOOL Res;
UserEnterShared();
Res = co_IntGetPeekMessage(pMsg,
hWnd,
MsgFilterMin,
MsgFilterMax,
RemoveMsg,
FALSE); // bGMSG = FALSE (PeekMessage)
UserLeave();
return Res;
}
7.2.3.3 co_IntGetPeekMessage 完整实现
win32ss/user/ntuser/message.c:1225(file:///d:/reactos/win32ss/user/ntuser/message.c#L1225):
c
BOOL APIENTRY
co_IntGetPeekMessage(PMSG pMsg,
HWND hWnd,
UINT MsgFilterMin,
UINT MsgFilterMax,
UINT RemoveMsg,
BOOL bGMSG)
{
PWND Window;
PTHREADINFO pti;
BOOL Present = FALSE;
NTSTATUS Status;
LONG_PTR ExtraInfo = 0;
/* 特殊 hWnd 转换 */
if (hWnd == HWND_TOPMOST || hWnd == HWND_BROADCAST)
hWnd = HWND_BOTTOM;
/* 验证 hWnd */
if (hWnd && hWnd != HWND_BOTTOM)
{
if (!(Window = UserGetWindowObject(hWnd)))
{
if (bGMSG) return -1;
else return FALSE;
}
}
else
{
Window = (PWND)hWnd;
}
/* 范围检查 */
if (MsgFilterMax < MsgFilterMin)
{
MsgFilterMin = 0;
MsgFilterMax = 0;
}
/* GetMessage 时根据过滤器计算 WakeMask */
if (bGMSG)
{
RemoveMsg |= ((GetWakeMask(MsgFilterMin, MsgFilterMax)) << 16);
}
pti = PsGetCurrentThreadWin32Thread();
pti->pClientInfo->cSpins++; // 增加自旋计数
do
{
Present = co_IntPeekMessage(pMsg,
Window,
MsgFilterMin,
MsgFilterMax,
RemoveMsg,
&ExtraInfo,
bGMSG);
if (Present)
{
/* 一些断言检查 */
if (pMsg->message != WM_DEVICECHANGE || (pMsg->wParam & 0x8000))
{
ASSERT(FindMsgMemory(pMsg->message) == NULL);
}
/* DDE 消息特殊处理 */
if (pMsg->message >= WM_DDE_FIRST && pMsg->message <= WM_DDE_LAST)
{
if (!IntDdeGetMessageHook(pMsg, ExtraInfo))
{
TRACE("DDE Get return ERROR\n");
continue;
}
}
/* 更新最后位置 */
if (pMsg->message != WM_PAINT && pMsg->message != WM_QUIT)
{
if (!RtlEqualMemory(&pti->ptLast, &pMsg->pt, sizeof(POINT)))
{
pti->TIF_flags |= TIF_MSGPOSCHANGED;
}
pti->timeLast = pMsg->time;
pti->ptLast = pMsg->pt;
}
/* 调用 WH_GETMESSAGE 钩子 */
co_HOOK_CallHooks(WH_GETMESSAGE, HC_ACTION, RemoveMsg & PM_REMOVE, (LPARAM)pMsg);
if (bGMSG || pMsg->message == WM_PAINT) break;
}
if (bGMSG)
{
/* GetMessage 阻塞模式:等待新消息 */
Status = co_MsqWaitForNewMessages(pti,
Window,
MsgFilterMin,
MsgFilterMax);
if (!NT_SUCCESS(Status) ||
Status == STATUS_USER_APC ||
Status == STATUS_TIMEOUT)
{
Present = -1;
break;
}
}
else
{
/* PeekMessage 非阻塞:让出 CPU */
if (!(RemoveMsg & PM_NOYIELD))
{
IdlePing();
UserLeave();
ZwYieldExecution();
UserEnterExclusive();
IdlePong();
}
break;
}
}
while (bGMSG && !Present);
return Present;
}
关键点:
- HWND_TOPMOST/BROADCAST 转换:两者都视为 HWND_BOTTOM(任意窗口);
- DDE 消息特殊处理:DDE 消息需要额外的 hook;
- TIF_MSGPOSCHANGED:标记消息位置已变(GetCursorPos 缓存失效);
- WH_GETMESSAGE hook:应用可监控所有 GetMessage/PeekMessage 返回的消息;
- GetMessage 阻塞:调用 co_MsqWaitForNewMessages 等待;
- PeekMessage 让出:调用 ZwYieldExecution 让出 CPU。
7.2.4 co_IntPeekMessage 调度核心
7.2.4.1 函数实现
win32ss/user/ntuser/message.c:1013(file:///d:/reactos/win32ss/user/ntuser/message.c#L1013):
c
BOOL APIENTRY
co_IntPeekMessage(PMSG Msg,
PWND Window,
UINT MsgFilterMin,
UINT MsgFilterMax,
UINT RemoveMsg,
LONG_PTR *ExtraInfo,
BOOL bGMSG)
{
PTHREADINFO pti;
BOOL RemoveMessages;
UINT ProcessMask;
BOOL Hit = FALSE;
pti = PsGetCurrentThreadWin32Thread();
RemoveMessages = RemoveMsg & PM_REMOVE;
ProcessMask = HIWORD(RemoveMsg);
/* 默认 ProcessMask */
if (!ProcessMask) ProcessMask = (QS_ALLPOSTMESSAGE | QS_ALLINPUT);
IdlePong();
do
{
/* 更新最后访问时间 */
pti->pcti->timeLastRead = EngGetTickCount32();
/* 在循环中分派鼠标移动 */
if (pti->MessageQueue->QF_flags & QF_MOUSEMOVED)
{
IntCoalesceMouseMove(pti);
}
/* 第一遍:分派发送消息 */
while (co_MsqDispatchOneSentMessage(pti))
{
if (HIWORD(RemoveMsg) && !bGMSG) Hit = TRUE;
}
if (Hit) return FALSE;
/* 清除 change bits */
if (ProcessMask & QS_POSTMESSAGE)
{
pti->pcti->fsChangeBits &= ~(QS_POSTMESSAGE | QS_HOTKEY | QS_TIMER);
if (MsgFilterMin == 0 && MsgFilterMax == 0)
{
pti->pcti->fsChangeBits &= ~QS_ALLPOSTMESSAGE;
}
}
if (ProcessMask & QS_INPUT)
{
pti->pcti->fsChangeBits &= ~QS_INPUT;
}
/* 检查投递消息 */
if ((ProcessMask & QS_POSTMESSAGE || ProcessMask & QS_HOTKEY) &&
MsqPeekMessage(pti,
RemoveMessages,
Window,
MsgFilterMin,
MsgFilterMax,
ProcessMask,
ExtraInfo,
0,
Msg))
{
goto GotMessage;
}
/* WM_QUIT 消息 */
if (ProcessMask & QS_POSTMESSAGE && pti->QuitPosted)
{
Msg->hwnd = NULL;
Msg->message = WM_QUIT;
Msg->wParam = pti->exitCode;
Msg->lParam = 0;
if (RemoveMessages)
{
pti->QuitPosted = FALSE;
ClearMsgBitsMask(pti, QS_POSTMESSAGE);
pti->pcti->fsWakeBits &= ~QS_ALLPOSTMESSAGE;
pti->pcti->fsChangeBits &= ~QS_ALLPOSTMESSAGE;
}
goto GotMessage;
}
/* 硬件消息 */
if ((ProcessMask & QS_INPUT) &&
co_MsqPeekHardwareMessage(pti,
RemoveMessages,
Window,
MsgFilterMin,
MsgFilterMax,
ProcessMask,
Msg))
{
goto GotMessage;
}
/* 内部事件 */
{
LONG_PTR eExtraInfo;
MSG eMsg;
DWORD dwQEvent;
if (MsqPeekMessage(pti,
TRUE,
Window,
0, 0,
QS_EVENT,
&eExtraInfo,
&dwQEvent,
&eMsg))
{
handle_internal_events(pti, Window, dwQEvent, eExtraInfo, &eMsg);
continue;
}
}
/* 第二遍:分派发送消息(避免长时阻塞) */
while (co_MsqDispatchOneSentMessage(pti))
{
if (HIWORD(RemoveMsg) && !bGMSG) Hit = TRUE;
}
if (Hit) return FALSE;
/* WM_PAINT 消息 */
if ((ProcessMask & QS_PAINT) &&
pti->cPaintsReady &&
IntGetPaintMessage(Window, MsgFilterMin, MsgFilterMax, pti, Msg, RemoveMessages))
{
goto GotMessage;
}
/* 定时器消息 */
if ((ProcessMask & QS_TIMER) &&
PostTimerMessages(Window))
{
continue;
}
return FALSE;
}
while (TRUE);
GotMessage:
pti->pcti->timeLastRead = EngGetTickCount32();
return TRUE;
}
7.2.4.2 7 步扫描流程
整个 PeekMessage 实现遵循 MSDN 规定的优先级顺序:
┌──────────────────────────────────────────────────────────────────┐
│ PeekMessage 扫描流程 │
├──────────────────────────────────────────────────────────────────┤
│ 1. 发送消息 (Sent) ── co_MsqDispatchOneSentMessage │
│ 2. 投递消息 (Posted) ── MsqPeekMessage │
│ 3. WM_QUIT ── 特殊处理 │
│ 4. 硬件消息 (Hardware) ── co_MsqPeekHardwareMessage │
│ 5. 内部事件 (Internal) ── handle_internal_events │
│ 6. 发送消息 (Sent) 再次 ── co_MsqDispatchOneSentMessage │
│ 7. 绘制消息 (Paint) ── IntGetPaintMessage │
│ 8. 定时器消息 (Timer) ── PostTimerMessages │
│ ────────────────────────────────────────────────────────────── │
│ 全部无消息时:返回 FALSE │
└──────────────────────────────────────────────────────────────────┘
7.2.4.3 发送消息的两遍扫描
为什么需要两遍扫描发送消息?
- 第一遍(在投递消息之前):处理已经到达的发送消息(避免延迟);
- 第二遍(在硬件消息之后):处理在硬件消息处理过程中新到达的发送消息(避免死锁)。
这种设计是为了防止线程 A 在处理硬件消息时,线程 B 调用 SendMessage 给 A 同步等待,造成死锁。
7.2.5 co_MsqPeekHardwareMessage 硬件消息
7.2.5.1 函数实现
win32ss/user/ntuser/msgqueue.c:1938(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1938):
c
BOOL APIENTRY
co_MsqPeekHardwareMessage(IN PTHREADINFO pti,
IN BOOL Remove,
IN PWND Window,
IN UINT MsgFilterLow,
IN UINT MsgFilterHigh,
IN UINT QSflags,
OUT MSG* pMsg)
{
PUSER_MESSAGE CurrentMessage;
PLIST_ENTRY ListHead;
DWORD QS_Flags;
BOOL Ret = FALSE;
ListHead = pti->MessageQueue->HardwareMessagesListHead.Flink;
if (IsListEmpty(ListHead)) return FALSE;
while (ListHead != &pti->MessageQueue->HardwareMessagesListHead)
{
CurrentMessage = CONTAINING_RECORD(ListHead, USER_MESSAGE, ListEntry);
ListHead = ListHead->Flink;
/* 检查窗口过滤 */
if ((!Window || Window == PWND_BOTTOM || UserHMGetHandle(Window) == CurrentMessage->Msg.hwnd) &&
/* 检查消息范围过滤 */
((MsgFilterLow == 0 && MsgFilterHigh == 0 && (CurrentMessage->QS_Flags & QSflags)) ||
(MsgFilterLow <= CurrentMessage->Msg.message && MsgFilterHigh >= CurrentMessage->Msg.message)))
{
*pMsg = CurrentMessage->Msg;
QS_Flags = CurrentMessage->QS_Flags;
if (Remove)
{
if (CurrentMessage->pti != NULL)
{
MsqDestroyMessage(CurrentMessage);
}
ClearMsgBitsMask(pti, QS_Flags);
}
Ret = TRUE;
break;
}
}
return Ret;
}
7.2.5.2 鼠标消息特殊处理
注意:硬件消息的 QS_Flags 可以是 QS_MOUSEMOVE、QS_MOUSEBUTTON 或 QS_KEY。但当鼠标消息被检查时,会通过 co_IntProcessMouseMessage 进行额外处理(路由到正确窗口、双击判定等)。
7.2.6 co_IntProcessMouseMessage 鼠标消息路由
7.2.6.1 函数概述
win32ss/user/ntuser/msgqueue.c:1472(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1472):
co_IntProcessMouseMessage 是鼠标消息的"路由与转换"中心,负责:
- 找到正确目标窗口(捕获/Hit-Test)
- 区分客户区/非客户区
- 双击判定
- 鼠标单击锁定(ClickLock)
7.2.6.2 窗口路由
c
/* find the window to dispatch this mouse message to */
if (MessageQueue->spwndCapture)
{
hittest = HTCLIENT;
pwndMsg = MessageQueue->spwndCapture;
}
else
{
pwndMsg = co_WinPosWindowFromPoint(NULL, &msg->pt, &hittest, FALSE);
}
优先级:
- 捕获窗口 (
spwndCapture)------SetCapture强制所有鼠标消息路由到指定窗口; - Hit-Test 命中窗口 (
co_WinPosWindowFromPoint)------ 鼠标位置命中的窗口。
7.2.6.3 客户区/非客户区转换
c
if (hittest != HTCLIENT)
{
message += WM_NCMOUSEMOVE - WM_MOUSEMOVE;
msg->wParam = hittest;
}
else
{
if (!(MessageQueue->MenuOwner))
{
pt.x += pwndDesktop->rcClient.left - pwndMsg->rcClient.left;
pt.y += pwndDesktop->rcClient.top - pwndMsg->rcClient.top;
}
}
msg->lParam = MAKELONG(pt.x, pt.y);
- 命中客户区 (HTCLIENT):发送
WM_MOUSEMOVE,坐标为窗口客户区局部坐标; - 命中非客户区 (HTBORDER、HTCAPTION 等):发送
WM_NCMOUSEMOVE,wParam 为 hit-test 代码。
7.2.6.4 双击判定
c
if ((msg->message == WM_LBUTTONDOWN) ||
(msg->message == WM_RBUTTONDOWN) ||
(msg->message == WM_MBUTTONDOWN) ||
(msg->message == WM_XBUTTONDOWN))
{
/* translate double-clicks */
if ((MessageQueue->MenuOwner || MessageQueue->MoveSize) ||
hittest != HTCLIENT ||
(pwndMsg->pcls->style & CS_DBLCLKS))
{
if ((msg->message == clk_msg.message) &&
(msg->hwnd == clk_msg.hwnd) &&
((msg->time - clk_msg.time) < (ULONG)gspv.iDblClickTime) &&
(abs(msg->pt.x - clk_msg.pt.x) < UserGetSystemMetrics(SM_CXDOUBLECLK)/2) &&
(abs(msg->pt.y - clk_msg.pt.y) < UserGetSystemMetrics(SM_CYDOUBLECLK)/2))
{
message += (WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
if (update)
{
MessageQueue->msgDblClk.message = 0;
update = FALSE;
}
}
}
if (update) MessageQueue->msgDblClk = *msg;
}
双击判定条件(必须全部满足):
- 窗口类支持双击(
CS_DBLCLKS); - 与上次按键消息类型相同 (
clk_msg.message); - 距离上次按键时间 <
gspv.iDblClickTime(默认 500ms); - 水平距离 <
SM_CXDOUBLECLK/2(默认 4 像素); - 垂直距离 <
SM_CYDOUBLECLK/2(默认 4 像素); - 命中客户区(hittest == HTCLIENT)。
7.2.6.5 消息队列切换处理
c
if (pwndMsg == NULL || pwndMsg->head.pti->MessageQueue != MessageQueue)
{
// Crossing a boundary, so set cursor. See default message queue cursor.
IntSystemSetCursor(SYSTEMCUR(ARROW));
/* Remove and ignore the message */
*RemoveMessages = TRUE;
return FALSE;
}
if (pwndMsg->head.pti != pti && MessageQueue->cThreads > 1)
{
/* This is not for us and we should leave so the other thread can check for messages */
*NotForUs = TRUE;
*RemoveMessages = FALSE;
return FALSE;
}
跨队列处理:
- 目标窗口属于其他消息队列 (
cThreads > 1共享队列)时,丢弃消息并切换光标; - 目标窗口属于其他线程的独立队列时,标记为 NotForUs,让其他线程处理。
7.2.7 co_IntProcessKeyboardMessage 键盘消息处理
7.2.7.1 函数实现
win32ss/user/ntuser/msgqueue.c:1772(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1772):
c
BOOL co_IntProcessKeyboardMessage(MSG* Msg, BOOL* RemoveMessages)
{
PTHREADINFO pti;
PUSER_MESSAGE_QUEUE MessageQueue;
BYTE KeyState, AsyncKeyState;
UINT message;
PWND pWnd;
BOOL EatMsg = FALSE;
pti = PsGetCurrentThreadWin32Thread();
MessageQueue = pti->MessageQueue;
message = Msg->message;
/* 验证消息范围 */
if (message < WM_KEYFIRST || message > WM_KEYLAST) return FALSE;
/* 更新异步按键状态 */
if (message == WM_KEYDOWN || message == WM_SYSKEYDOWN)
UpdateKeyStateFromMsg(MessageQueue, Msg);
/* 处理 IME 热键 */
if (co_IntImmProcessKey(Msg->hwnd, pti->KeyboardLayout->hkl, ...))
{
*RemoveMessages = TRUE;
return FALSE;
}
/* 路由到焦点窗口或活动窗口 */
pWnd = MessageQueue->spwndFocus;
if (!pWnd || pWnd->head.pti->MessageQueue != MessageQueue)
{
pWnd = MessageQueue->spwndActive;
}
if (pWnd && pWnd->head.pti->MessageQueue == MessageQueue)
{
Msg->hwnd = UserHMGetHandle(pWnd);
return TRUE;
}
else
{
*RemoveMessages = TRUE;
return FALSE;
}
}
7.2.7.2 键盘消息路由
优先级:
- 焦点窗口 (
spwndFocus)------ 通过SetFocus设置的键盘接收者; - 活动窗口 (
spwndActive)------ 当前前台窗口; - 如果都不可用,丢弃消息。
7.2.7.3 异步按键状态(AsyncKeyState)
gafAsyncKeyState 是全局异步按键状态数组,256 个键 × 2 bit:
c
BYTE gafAsyncKeyState[256 * 2 / 8]; // 64 字节,每键 2 bit
static BYTE gafAsyncKeyStateRecentDown[256 / 8]; // 32 字节,每键 1 bit
每键 2 bit 表示:
- bit 0:当前是否按下(1=按下);
- bit 1:是否为切换键(1=CapsLock、NumLock 等)。
GetAsyncKeyState API 直接读取这个数组,不进入消息队列,是真正的"实时"状态。
7.2.8 IntDispatchMessage 分发到 WndProc
7.2.8.1 函数实现
win32ss/user/ntuser/message.c:889(file:///d:/reactos/win32ss/user/ntuser/message.c#L889):
c
LRESULT FASTCALL
IntDispatchMessage(PMSG pMsg)
{
LONG Time;
LRESULT retval = 0;
PTHREADINFO pti;
PWND Window = NULL;
BOOL DoCallBack = TRUE;
if (pMsg->hwnd)
{
Window = UserGetWindowObject(pMsg->hwnd);
if (!Window) return 0;
}
pti = PsGetCurrentThreadWin32Thread();
/* 验证窗口属于当前线程 */
if (Window && Window->head.pti != pti)
{
EngSetLastError(ERROR_MESSAGE_SYNC_ONLY);
return 0;
}
/* WM_TIMER / WM_SYSTIMER 特殊处理 */
if (((pMsg->message == WM_SYSTIMER) || (pMsg->message == WM_TIMER)) && (pMsg->lParam))
{
if (pMsg->message == WM_TIMER)
{
if (ValidateTimerCallback(pti, pMsg->lParam))
{
Time = EngGetTickCount32();
retval = co_IntCallWindowProc((WNDPROC)pMsg->lParam, TRUE,
pMsg->hwnd, WM_TIMER, pMsg->wParam,
(LPARAM)Time, -1);
}
return retval;
}
else
{
PTIMER pTimer = FindSystemTimer(pMsg);
if (pTimer && pTimer->pfn)
{
Time = EngGetTickCount32();
pTimer->pfn(pMsg->hwnd, WM_SYSTIMER, (UINT)pMsg->wParam, Time);
}
return 0;
}
}
if (!Window) return 0;
if (pMsg->message == WM_PAINT) Window->state |= WNDS_PAINTNOTPROCESSED;
/* 服务端窗口过程(FNID_DESKTOP 等) */
if (Window->state & WNDS_SERVERSIDEWINDOWPROC)
{
switch (Window->fnid)
{
case FNID_DESKTOP:
DoCallBack = !DesktopWindowProc(Window, pMsg->message, pMsg->wParam, pMsg->lParam, &retval);
break;
case FNID_MESSAGEWND:
DoCallBack = !UserMessageWindowProc(Window, pMsg->message, pMsg->wParam, pMsg->lParam, &retval);
break;
case FNID_MENU:
DoCallBack = !PopupMenuWndProc(Window, pMsg->message, pMsg->wParam, pMsg->lParam, &retval);
break;
}
}
/* 用户态窗口过程 */
if (DoCallBack)
retval = co_IntCallWindowProc(Window->lpfnWndProc,
!Window->Unicode,
pMsg->hwnd,
pMsg->message,
pMsg->wParam,
pMsg->lParam,
-1);
/* WM_PAINT 后续处理 */
if (pMsg->message == WM_PAINT &&
VerifyWnd(Window) &&
Window->state & WNDS_PAINTNOTPROCESSED)
{
Window->state2 &= ~WNDS2_WMPAINTSENT;
IntPaintWindow(Window);
}
return retval;
}
7.2.8.2 三种 WndProc 路径
DispatchMessage 根据窗口的 fnid 决定走哪条路径:
| FNID | 路径 | 位置 |
|---|---|---|
FNID_DESKTOP |
DesktopWindowProc |
内核态 |
FNID_MENU |
PopupMenuWndProc |
内核态 |
FNID_MESSAGEWND |
UserMessageWindowProc |
内核态 |
FNID_SCROLLBAR |
ScrollBarWndProc |
内核态 |
| 普通窗口 | co_IntCallWindowProc → 用户态 WndProc |
用户态 |
WNDS_SERVERSIDEWINDOWPROC 标志 标识这是 win32k 内部维护的窗口(不需要用户态 WndProc)。
7.2.8.3 co_IntCallWindowProc 关键调用
c
retval = co_IntCallWindowProc(Window->lpfnWndProc,
!Window->Unicode,
pMsg->hwnd,
pMsg->message,
pMsg->wParam,
pMsg->lParam,
-1);
参数说明:
Window->lpfnWndProc:窗口过程函数指针(用户态);!Window->Unicode:是否是 ANSI 窗口过程;-1:lParam 缓冲区大小(-1 表示不需要复制数据)。
co_IntCallWindowProc 内部通过 KeUserModeCallback 切换到用户态,调用实际的 WndProc 函数(详见 7.3 节)。
7.2.9 co_MsqWaitForNewMessages 等待新消息
7.2.9.1 函数实现
win32ss/user/ntuser/msgqueue.c:2118(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L2118):
c
NTSTATUS FASTCALL
co_MsqWaitForNewMessages(PTHREADINFO pti, PWND WndFilter,
UINT MsgFilterMin, UINT MsgFilterMax)
{
NTSTATUS ret = STATUS_SUCCESS;
/* 在等待前分派合并的鼠标移动 */
if (pti->MessageQueue->QF_flags & QF_MOUSEMOVED)
{
IntCoalesceMouseMove(pti);
}
/* 释放用户态锁(允许其他线程进入) */
UserLeaveCo();
/* 让出 CPU 片刻 */
ZwYieldExecution();
/* 阻塞等待消息队列事件 */
ret = KeWaitForSingleObject(pti->pEventQueueServer,
UserRequest,
UserMode,
FALSE,
NULL);
/* 重新获取用户态锁 */
UserEnterCo();
/* 处理用户态 APC */
if (ret == STATUS_USER_APC)
{
TRACE("MWFNW User APC\n");
co_IntDeliverUserAPC();
}
return ret;
}
7.2.9.2 等待-唤醒机制
GetMessage 流程
│
▼
co_MsqWaitForNewMessages
│
├─► UserLeaveCo() 释放锁
│
├─► ZwYieldExecution() 让出 CPU
│
└─► KeWaitForSingleObject(pEventQueueServer)
│
│ (线程挂起)
│
▼
其他线程 PostMessage
│
▼
MsqPostMessage 内部
│
└─► KeSetEvent(pEventQueueServer)
│
│ (线程唤醒)
▼
重新进入 GetMessage 循环
关键点:
pEventQueueServer是内核态事件对象(在线程创建时分配);MsqPostMessage投递消息后会KeSetEvent唤醒等待线程;UserLeaveCo/UserEnterCo在等待前后释放/获取用户态锁(允许其他线程进入 win32k)。
7.2.9.3 死锁避免
co_MsqWaitForNewMessages 在等待前会调用 IntCoalesceMouseMove,确保所有合并的鼠标消息在等待前已处理,避免:
- 线程 A 等待消息;
- 线程 B 在等待中产生鼠标消息;
- 鼠标消息被合并但永远不会被处理(因为 A 已挂起)。
7.2.10 设计哲学问答
Q1:为什么 MSDN 规定 PeekMessage 必须按特定顺序扫描消息?
A :这是为了避免死锁 和保证公平性。
- 发送消息优先级最高:避免 SendMessage 跨线程时死锁;
- 投递消息其次:异步消息应尽快处理;
- 硬件消息再次:UI 响应性优先(键盘/鼠标不能延迟);
- 内部事件:系统通知需要及时处理;
- 绘制消息:按需触发,优先级最低。
如果颠倒这个顺序,可能会出现:用户移动鼠标但窗口因处理长时发送消息而冻结,或 SendMessage 永远等不到 WndProc 返回(因为接收方在处理其他消息)。
Q2:为什么需要两遍扫描发送消息?
A :避免跨线程 SendMessage 死锁。
考虑场景:
- 线程 A 正在处理投递消息(来自线程 B 的 PostMessage);
- 线程 C 调用
SendMessage给 A,阻塞等待; - 如果 A 继续处理投递消息,可能触发对 C 的 SendMessage,形成环路死锁。
通过在投递消息前后两遍扫描发送消息,可以在线程 A 处理投递消息时,先把 C 的发送消息处理完,避免 A 反过来给 C 发消息时出现环路。
Q3:为什么 USER_MESSAGE 用 PagedPool 而 USER_MESSAGE_QUEUE 用 NonPagedPool?
A:访问频率与使用环境的差异:
- USER_MESSAGE:消息对象,频繁分配/释放,PagedPool 提供更高的内存效率;
- USER_MESSAGE_QUEUE:队列对象,持有焦点、捕获、激活窗口等关键指针,必须始终驻留内存(NonPagedPool);
- USER_SENT_MESSAGE:发送消息,持有 KEVENT 同步对象,需要 NonPagedPool。
这种区分是为了在性能和安全之间取得平衡。
Q4:为什么硬件消息使用单独链表(HardwareMessagesListHead)?
A:硬件消息有以下特点需要单独处理:
- 多线程共享:键盘和鼠标输入由 RIT(Raw Input Thread)统一接收,然后分派到对应线程;
- 高频率:鼠标移动每毫秒可达数百次,单独链表避免影响投递消息;
- 特殊处理 :需要经过
co_MsqPeekHardwareMessage路由到正确窗口; - 优先级可独立控制 :可以单独等待硬件消息(
GetMessage(..., WM_MOUSEFIRST, WM_MOUSELAST))。
Q5:为什么 WM_PAINT 消息在所有消息之后处理?
A :绘制是累积式的,频繁重绘会浪费 CPU:
- 窗口失效时只设置
WNDS_PAINTNOTPROCESSED标志; - 当消息队列空闲时才真正合成 WM_PAINT 消息;
- 应用程序必须显式处理 WM_PAINT(
BeginPaint/EndPaint)。
这种设计保证了:
- 多次失效只产生一次 WM_PAINT;
- 处理其他消息时不会被绘制消息打断;
- 应用程序可以在合适的时机(如 CPU 空闲)才进行重绘。
Q6:为什么 GetMessage 在没有消息时必须阻塞线程?
A :避免CPU 100% 占用。
如果 GetMessage 在无消息时立即返回 FALSE,应用程序的消息循环将变成:
c
while (GetMessage(&msg, NULL, 0, 0)) { // 永远不阻塞
DispatchMessage(&msg);
}
// CPU 100% 占用
阻塞等待可以让线程在无消息时挂起,直到 MsqPostMessage 通过 KeSetEvent 唤醒,从而实现事件驱动的节能模式。
总结
视窗报文的接收是 win32k 中最复杂、最高频的操作之一。本节介绍了:
- 5 种消息源:Sent、Posted、Hardware、Internal、Paint、Timer;
- 消息优先级:MSDN 规定的 7 步扫描顺序;
- GetMessage/PeekMessage:阻塞/非阻塞两种模式;
- co_IntPeekMessage:核心调度函数,按优先级扫描所有消息源;
- 鼠标/键盘路由:捕获、焦点、Hit-Test、双击判定;
- DispatchMessage:分发到服务端 WndProc 或用户态 WndProc;
- 等待-唤醒机制:KeSetEvent/KeWaitForSingleObject。
核心要点回顾:
- GetMessage 严格遵循 MSDN 7 步优先级扫描;
- 发送消息需要两遍扫描以避免死锁;
- 硬件消息有独立的链表和路由逻辑;
- WM_PAINT 是累积式绘制,按需合成;
- GetMessage 阻塞通过事件对象实现,避免 CPU 占用。
本章代码索引
| 文件 | 内容 |
|---|---|
| win32ss/user/ntuser/message.c(file:///d:/reactos/win32ss/user/ntuser/message.c) | NtUserGetMessage、co_IntGetPeekMessage、co_IntPeekMessage、IntDispatchMessage |
| win32ss/user/ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) | MsqPeekMessage、co_MsqPeekHardwareMessage、co_MsqDispatchOneSentMessage、co_MsqWaitForNewMessages、co_IntProcessMouseMessage、co_IntProcessKeyboardMessage |
| win32ss/user/ntuser/msgqueue.h(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h) | USER_MESSAGE、USER_SENT_MESSAGE、USER_MESSAGE_QUEUE、QS_、SMF_、QF_* 标志 |
| win32ss/user/ntuser/callback.c(file:///d:/reactos/win32ss/user/ntuser/callback.c) | co_IntCallWindowProc、回调机制(详见 7.3) |
| win32ss/user/ntuser/ntstubs.c(file:///d:/reactos/win32ss/user/ntuser/ntstubs.c) | NtUserRealInternalGetMessage(stub) |
| win32ss/user/user32/windows/message.c(file:///d:/reactos/win32ss/user/user32/windows/message.c) | user32 侧 GetMessage/DispatchMessage 包装 |