第 7 章 视窗报文 --- 7.5 视窗报文的发送
本节深入剖析 Windows/ReactOS 中"视窗报文发送"(Send/Post/Notify)的内核态实现,包括同步发送、异步发送、超时控制、跨进程边界检查、死锁检测、广播、ReplyMessage 等核心机制。
概述
发送消息是 Windows GUI 程序间通信的核心机制之一。与"投递消息"(Post)不同,发送 (Send)意味着"调用者等待接收方处理完消息并返回结果"------这是一种同步 、阻塞 、可获得返回值的通信方式。ReactOS 的视窗报文发送系统需要解决以下核心问题:
- 同线程 fast path:当接收方是当前线程的窗口时,直接调用 WndProc,避免线程间同步;
- 跨线程同步等待 :当接收方是其他线程时,需要等待事件 + 线程上下文切换;
- 超时与死锁检测:避免发送方无限期等待死锁的接收方;
- 跨进程边界检查:防止敏感消息(密码、WM_CREATE)跨进程泄露;
- 接收方/发送方死亡处理:在对方线程意外终止时清理资源;
- 广播:特殊消息(HWND_BROADCAST)遍历所有顶层窗口。
发送消息的本质是什么?
发送消息是一种"同步 RPC"(远程过程调用):发送方线程在 Win32k 内核态挂起,直到接收方线程的 WndProc 处理完消息并通过
ReplyMessage通知结果。Windows 通过USER_SENT_MESSAGE结构 + 事件对象(pkCompletionEvent)+ 等待队列(KeWaitForMultipleObjects)实现这一机制。
想象一个"内部传话"场景:
- SendMessage :员工 A 写一封信给员工 B,站在 B 办公桌前等他回信(同步阻塞);
- PostMessage :员工 A 把信投入 B 的信箱,继续做自己的工作(异步非阻塞);
- SendMessageCallback:员工 A 把信投入 B 的信箱,附上一张"请回信到 5 号邮箱"的便条,回信到达 5 号邮箱时由 A 的助理代为处理;
- SendNotifyMessage :与 SendMessage 类似,但指针参数是复制过去的(适合带数据的消息);
- HWND_BROADCAST :员工 A 把信投入公司所有员工的信箱(遍历所有顶层窗口);
- ReplyMessage :员工 B 把回信写好并交给信使(设置
QS_SMRESULT标志)。
本节内容概览
- 7.5.0 框架图:视窗报文发送的整体架构;
- 7.5.1 视窗报文发送总览:4 种语义(Send/Post/SendCallback/Notify);
- 7.5.2 USER_SENT_MESSAGE 与 SMF_ 状态标志*;
- 7.5.3 SendMessage 同线程 fast path;
- 7.5.4 co_IntSendMessageTimeoutSingle 跨线程发送详解;
- 7.5.5 跨进程边界与 DDE 检查;
- 7.5.6 死锁检测与超时机制;
- 7.5.7 co_MsqSendMessage 消息队列同步等待;
- 7.5.8 接收者死亡与发送者死亡;
- 7.5.9 co_MsqSendMessageAsync 异步发送;
- 7.5.10 广播、Notify 与 SendMessageCallback;
- 7.5.11 co_MsqReplyMessage 同步响应;
- 7.5.12 设计哲学问答:5 个关键设计问题解答。
学习目标
读完本节后,读者应当能够:
- 理解视窗报文发送的 4 种语义(Send/Post/SendCallback/Notify);
- 掌握 USER_SENT_MESSAGE 结构与 SMF_* 状态标志;
- 分析 SendMessage 同线程 fast path 与跨线程同步路径;
- 理解跨进程边界检查(DDE 密码、WM_CREATE)的必要性;
- 掌握死锁检测(MsqIsHung)与超时机制;
- 解释接收者/发送者死亡时的资源清理流程;
- 掌握 co_MsqReplyMessage 的同步响应机制。
涉及的内核子系统
| 子系统 | 职责 |
|---|---|
| win32k.sys/ntuser/message.c | co_IntSendMessage 家族、UserSendNotifyMessage |
| win32k.sys/ntuser/msgqueue.c | co_MsqSendMessage、co_MsqSendMessageAsync、co_MsqReplyMessage |
| win32k.sys/ntuser/msgqueue.h | USER_SENT_MESSAGE 结构、SMF_* 标志、QS_SMRESULT |
| ntoskrnl/ke | KeWaitForMultipleObjects、KeSetEvent 同步原语 |
| user32.dll | SendMessage 用户态入口(user32/windows/message.c) |
7.5.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ 视窗报文发送(Send/Post/Notify)整体架构 │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 用户态 API │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ user32.dll SendMessage / SendMessageTimeout / SendMessageCallback │ │
│ │ SendNotifyMessage / PostMessage / PostThreadMessage │ │
│ │ ReplyMessage │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (syscall) │
│ 内核态 win32k │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ message.c │ │
│ │ ├─► co_IntSendMessage ─┐ │ │
│ │ ├─► co_IntSendMessageTimeoutSingle │ 4 种 Send 变体 │ │
│ │ ├─► co_IntSendMessageNoWait ─┤ │ │
│ │ ├─► co_IntSendMessageWithCallBack ─┘ │ │
│ │ └─► UserSendNotifyMessage │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ msgqueue.c │ │
│ │ ├─► co_MsqSendMessage (同步等待内核态核心) │ │
│ │ ├─► co_MsqSendMessageAsync (异步发送) │ │
│ │ ├─► co_MsqDispatchOneSentMessage (接收方分派) │ │
│ │ └─► co_MsqReplyMessage (接收方响应) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ntoskrnl │ │
│ │ └─► KeWaitForMultipleObjects (跨线程同步等待) │ │
│ │ KeSetEvent / KeUserModeCallback │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 4 种发送语义对比 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ 类型 阻塞 跨线程 返回值 lParam 指针 完成通知 │ │
│ │ ───────────────────────────────────────────────────────────────── │ │
│ │ SendMessage Yes Yes Yes 不允许跨进程 pkCompletionEvent │ │
│ │ SendNotify No Yes No 复制 不需要 │ │
│ │ SendMessageCb No Yes No 不允许跨进程 回调函数 │ │
│ │ PostMessage No Yes No 不允许跨进程 不需要 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 同步发送核心数据结构 │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ USER_SENT_MESSAGE │ │
│ │ ├─► Msg (hwnd, message, wParam, lParam) 消息内容 │ │
│ │ ├─► ptiReceiver / ptiSender 收发双方线程 │ │
│ │ ├─► ptiCallBackSender 回调发送者 │ │
│ │ ├─► pkCompletionEvent 同步事件(信号量) │ │
│ │ ├─► CompletionCallback 异步回调函数 │ │
│ │ ├─► HookMessage 钩子消息标记 │ │
│ │ ├─► HasPackedLParam lParam 是否为打包的指针 │ │
│ │ ├─► QS_Flags = QS_SENDMESSAGE | QS_SMRESULT │ │
│ │ └─► flags = SMF_RECEIVERFREE | SMF_RECEIVERBUSY | SMF_RECEIVERDIED │ │
│ │ | SMF_SENDERDIED │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
7.5.1 视窗报文发送总览:4 种语义
7.5.1.1 4 种发送 API 对比
| API | 阻塞 | 跨线程 | 返回值 | lParam 指针 | 完成机制 | 内核函数 |
|---|---|---|---|---|---|---|
| SendMessage | 同步阻塞 | Yes | Yes | 不可跨进程(自动复制) | pkCompletionEvent | co_IntSendMessage |
| SendMessageTimeout | 同步超时 | Yes | Yes | 不可跨进程 | pkCompletionEvent + Timeout | co_IntSendMessageTimeout |
| SendMessageCallback | 异步非阻塞 | Yes | 间接(回调) | 不可跨进程 | 回调函数 | co_IntSendMessageWithCallBack |
| SendNotifyMessage | 异步非阻塞 | Yes | No | 自动复制 | 不需要 | co_IntSendMessageNoWait |
| PostMessage | 异步非阻塞 | Yes | No | 不可跨进程 | 不需要 | MsqPostMessage |
7.5.1.2 同步 vs 异步语义
同步发送(SendMessage):
线程 A Win32k 内核 线程 B
│ │ │
├──NtUserSendMessage───────►│ │
│ (等待 pkEvent) ├─插入 SentMessagesList►│
│ │ ├──GetMessage 取消息
│ │ ├──WndProc 处理
│ BLOCKED │ ├──ReplyMessage
│ │◄─────────结果──────────┤
│◄─── KeWaitForSingleObject─┤ (Event 触发) │
│ │ │
│ 返回处理结果 │ │
异步发送(SendNotifyMessage):
线程 A Win32k 内核 线程 B
│ │ │
├──NtUserSendNotifyMessage─►│ │
│ (复制 lParam 数据) ├─插入 SentMessagesList►│
│ │ ├──GetMessage 取消息
│◄─── 立即返回 ─────────────┤ ├──WndProc 处理
│ │ │ (无返回值)
7.5.1.3 同步 vs 异步的本质差异
| 维度 | 同步发送 | 异步发送 |
|---|---|---|
| 发送方状态 | 在内核态阻塞(KeWaitForMultipleObjects) |
立即返回用户态 |
| 线程调度 | 发送方不消耗 CPU,但占线程栈 | 发送方继续运行 |
| 结果传递 | 通过共享 USER_SENT_MESSAGE.lResult |
通过回调函数(callback)或无 |
| 死锁风险 | 高(必须检测) | 无(不会等待) |
| 用途 | 状态查询、配置、强制同步 | 通知、状态变化通知 |
7.5.2 USER_SENT_MESSAGE 与 SMF_* 状态标志
7.5.2.1 USER_SENT_MESSAGE 结构
win32ss/user/ntuser/msgqueue.h(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h) 定义:
c
typedef struct _USER_SENT_MESSAGE
{
LIST_ENTRY ListEntry; // 链表节点(SentMessagesListHead)
MSG Msg; // 消息内容
PTHREADINFO ptiReceiver; // 接收方线程
PTHREADINFO ptiSender; // 发送方线程(同步等待时使用)
PTHREADINFO ptiCallBackSender; // 回调发送方(异步回调时使用)
PKEVENT pkCompletionEvent; // 完成事件(同步发送用)
SENDASYNCPROC CompletionCallback; // 完成回调(异步回调用)
ULONG_PTR CompletionCallbackContext; // 回调上下文
LRESULT lResult; // 消息处理结果
ULONG_PTR QS_Flags; // QS_SENDMESSAGE | QS_SMRESULT
DWORD flags; // SMF_* 状态位
INT HookMessage; // MSQ_NORMAL / MSQ_ISHOOK / MSQ_INJECTMODULE
BOOLEAN HasPackedLParam; // lParam 是否为内核态打包的指针
} USER_SENT_MESSAGE, *PUSER_SENT_MESSAGE;
7.5.2.2 SMF_* 状态标志
c
#define SMF_RECEIVERFREE 0x00000001 // 接收方负责释放(默认 sender 释放)
#define SMF_RECEIVERBUSY 0x00000002 // 消息正在被处理
#define SMF_RECEIVERDIED 0x00000004 // 接收方线程死亡
#define SMF_SENDERDIED 0x00000008 // 发送方线程死亡
#define SMF_RECEIVEDMESSAGE 0x00000010 // 消息已被接收
状态转换:
初始: flags = SMF_RECEIVERFREE
分派: flags |= SMF_RECEIVERBUSY | SMF_RECEIVEDMESSAGE
处理完成: flags &= ~SMF_RECEIVERBUSY
接收方死亡: flags |= SMF_RECEIVERDIED
发送方死亡: flags |= SMF_SENDERDIED
7.5.2.3 QS_* 状态位
c
#define QS_SENDMESSAGE 0x00000040 // 有 sent message 待处理
#define QS_SMRESULT 0x00000080 // SendMessage 已完成(结果可用)
QS_SENDMESSAGE 在 co_MsqSendMessage 中由发送方设置在接收方的 fsWakeBits 上。
QS_SMRESULT 在 co_MsqReplyMessage 中由接收方设置在 USER_SENT_MESSAGE.QS_Flags 上。
7.5.3 SendMessage 同线程 fast path
7.5.3.1 何时走 fast path?
当 IntSendTo(Window, pti, Msg) 返回 NULL(即接收方就是当前线程 )时,win32k 走同线程 fast path------直接调用 co_IntCallWindowProc ,不经过 co_MsqSendMessage 同步等待。
7.5.3.2 关键代码
win32ss/user/ntuser/message.c:1550-1641(file:///d:/reactos/win32ss/user/ntuser/message.c#L1550-L1641) co_IntSendMessageTimeoutSingle:
c
if ( !ptiSendTo )
{
// ... 处理 TIF_INCLEANUP ...
if (Msg & 0x80000000)
{
// 内部消息(WM_USER 以上)由 win32k 直接处理
Result = (ULONG_PTR)handle_internal_message( Window, Msg, wParam, lParam );
// ...
}
// 1. 触发 WH_CALLWNDPROC 钩子
IntCallWndProc( Window, hWnd, Msg, wParam, lParam);
// 2. Server-side window proc (FNID_DESKTOP / FNID_MESSAGEWND / FNID_MENU)
if ( Window->state & WNDS_SERVERSIDEWINDOWPROC )
{
switch(Window->fnid)
{
case FNID_DESKTOP:
DoCallBack = !DesktopWindowProc(...);
break;
case FNID_MESSAGEWND:
DoCallBack = !UserMessageWindowProc(...);
break;
case FNID_MENU:
DoCallBack = !PopupMenuWndProc(...);
break;
}
}
// 3. 计算 lParam 缓冲区大小
MsgMemoryEntry = FindMsgMemory(Msg);
if (NULL == MsgMemoryEntry)
lParamBufferSize = -1;
else
lParamBufferSize = MsgMemorySize(MsgMemoryEntry, wParam, lParam);
// 4. 打包 lParam 指针
if (! NT_SUCCESS(PackParam(&lParamPacked, Msg, wParam, lParam, FALSE)))
goto Cleanup;
// 5. 调用 WndProc(同线程直接调用,无需 KeUserModeCallback 跨边界!)
Result = (ULONG_PTR)co_IntCallWindowProc( Window->lpfnWndProc,
!Window->Unicode,
hWnd, Msg, wParam, lParamPacked,
lParamBufferSize );
// 6. 解包 lParam 指针
if (! NT_SUCCESS(UnpackParam(lParamPacked, Msg, wParam, lParam, FALSE)))
goto Cleanup;
// 7. 触发 WH_CALLWNDPROCRET 钩子
IntCallWndProcRet( Window, hWnd, Msg, wParam, lParam, (LRESULT *)uResult);
Ret = TRUE;
goto Cleanup;
}
7.5.3.3 fast path 的关键优势
| 优势 | 说明 |
|---|---|
| 无线程切换 | 同一线程,无需 KeWaitForMultipleObjects |
| 无死锁风险 | 不可能发生"自己等自己" |
| 无内存复制 | lParam 指针在同线程中天然可访问 |
| 无超时问题 | 直接调用,立即返回 |
| 性能更高 | 减少约 1000-10000 个 CPU 周期(无 syscall 进入退出) |
7.5.3.4 PackParam / UnpackParam
lParam 打包机制(file:///d:/reactos/win32ss/user/ntuser/message.c#L284-L300):当消息包含指针 (如 WM_SETTEXT 的字符串指针、LB_ADDSTRING 的字符串指针)时:
PackParam:将用户态指针复制到内核态缓冲区(避免跨进程访问);UnpackParam:处理完毕后写回用户态(用户可读回数据)。- 跨线程发送时,必须打包 ;同线程发送时通常不打包(
FALSE)。
c
NTSTATUS FASTCALL
PackParam(LPARAM *lParamPacked, UINT Msg, WPARAM wParam, LPARAM lParam, BOOL Pack)
{
if (!Pack) {
*lParamPacked = lParam; // 同线程:直接传指针
return STATUS_SUCCESS;
}
// 跨线程:从用户态进程地址空间复制到内核态
// ...
}
7.5.4 co_IntSendMessageTimeoutSingle 跨线程发送详解
7.5.4.1 函数签名
win32ss/user/ntuser/message.c:1509-1704(file:///d:/reactos/win32ss/user/ntuser/message.c#L1509-L1704):
c
static LRESULT FASTCALL
co_IntSendMessageTimeoutSingle( HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam,
UINT uFlags, // SMTO_* 标志
UINT uTimeout, // 超时(毫秒)
ULONG_PTR *uResult );
7.5.4.2 完整执行流程
co_IntSendMessageTimeoutSingle(hWnd, Msg, wParam, lParam, uFlags, uTimeout, uResult)
│
├── 1. 验证窗口句柄 ──► UserGetWindowObject(hWnd)
│ 失败 ──► return FALSE
│
├── 2. 引用窗口对象 ──► UserRefObjectCo(Window, &Ref)
│
├── 3. 确定接收方线程 ──► ptiSendTo = IntSendTo(Window, Win32Thread, Msg)
│ │
│ ├─► NULL(当前线程)──► 走 fast path(7.5.3)
│ └─► 非 NULL(其他线程)──► 继续下方
│
├── 4. DDE 消息检查
│ if (Msg in WM_DDE_FIRST..WM_DDE_LAST)
│ if (!IntDdeSendMessageHook(...))
│ return FALSE
│
├── 5. 窗口销毁检查
│ if (Window->state & WNDS_DESTROYED)
│ return FALSE
│
├── 6. Hung 检测(仅当 SMTO_ABORTIFHUNG)
│ if ((uFlags & SMTO_ABORTIFHUNG) && MsqIsHung(ptiSendTo, 4*MSQ_HUNG))
│ return FALSE
│
├── 7. 跨线程发送(核心)
│ do {
│ Status = co_MsqSendMessage(ptiSendTo, hWnd, Msg, wParam, lParam,
│ uTimeout, uFlags & SMTO_BLOCK,
│ MSQ_NORMAL, uResult);
│ } while ((Status == STATUS_TIMEOUT) &&
│ (uFlags & SMTO_NOTIMEOUTIFNOTHUNG) &&
│ !MsqIsHung(ptiSendTo, MSQ_HUNG));
│
├── 8. 处理超时
│ if (Status == STATUS_TIMEOUT)
│ EngSetLastError(ERROR_TIMEOUT)
│ return FALSE
│
├── 9. 处理错误
│ else if (!NT_SUCCESS(Status))
│ SetLastNtError(Status)
│ return FALSE
│
└── 10. 清理
UserDerefObjectCo(Window)
return TRUE
7.5.4.3 SMTO_* 标志
c
#define SMTO_NORMAL 0x0000 // 默认:阻塞至处理完
#define SMTO_BLOCK 0x0001 // 阻塞接收方线程(用于 SendMessageCallback)
#define SMTO_ABORTIFHUNG 0x0002 // 接收方挂起时立即返回
#define SMTO_NOTIMEOUTIFNOTHUNG 0x0008 // 未挂起时不超时(重试)
7.5.4.4 DDE 特殊处理
c
if ( Msg >= WM_DDE_FIRST && Msg <= WM_DDE_LAST )
{
if (!IntDdeSendMessageHook(Window, Msg, wParam, lParam))
{
ERR("Sending Exit DDE 0x%x\n",Msg);
goto Cleanup; // Return FALSE
}
}
DDE(动态数据交换)消息在发送前需通过 IntDdeSendMessageHook 检查所有权(DDE 通道是否还存在)。如果 DDE 通道已关闭,发送将被拒绝。
7.5.5 跨进程边界与 DDE 检查
7.5.5.1 安全消息边界
win32ss/user/ntuser/msgqueue.c:1107-1138(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1107-L1138) co_MsqSendMessage 中的"边境检查"(Border Patrol):
c
if ( HookMessage == MSQ_NORMAL )
{
pWnd = ValidateHwndNoErr(Wnd);
// These can not cross International Border lines!
if ( pti->ppi != ptirec->ppi && pWnd ) // 跨进程
{
switch(Msg)
{
case EM_GETLINE: // 编辑框获取行(可能含密码)
case EM_SETPASSWORDCHAR: // 设置密码字符
case WM_GETTEXT: // 获取文本
if ( gpsi->atomSysClass[ICLS_EDIT] == pWnd->pcls->atomClassName && // 是编辑框
pWnd->style & ES_PASSWORD ) // 是密码框
{
if (uResult) *uResult = -1;
ERR("Running across the border without a passport!\n");
EngSetLastError(ERROR_ACCESS_DENIED);
return STATUS_UNSUCCESSFUL;
}
break;
case WM_NOTIFY: // 通知消息(含指针)
if (uResult) *uResult = -1;
ERR("Running across the border without a passport!\n");
return STATUS_UNSUCCESSFUL;
}
}
// These can not cross State lines!
if ( Msg == WM_CREATE || Msg == WM_NCCREATE )
{
if (uResult) *uResult = -1;
ERR("Can not tell the other State we have Create!\n");
return STATUS_UNSUCCESSFUL;
}
}
7.5.5.2 三类安全消息
| 消息类型 | 检查原因 | 失败处理 |
|---|---|---|
密码/编辑框文本 (EM_GETLINE / EM_SETPASSWORDCHAR / WM_GETTEXT) |
防止跨进程读取密码 | 返回 ERROR_ACCESS_DENIED |
WM_NOTIFY |
NMHDR 指针无法跨进程 | 返回 STATUS_UNSUCCESSFUL |
WM_CREATE / WM_NCCREATE |
CREATESTRUCT 含本进程指针 | 返回 STATUS_UNSUCCESSFUL |
7.5.5.3 设计意图
Windows 的设计哲学是:"不同进程不应共享内存指针"。当消息携带指针(字符串、结构)时,跨进程发送可能:
- 暴露本进程内存布局;
- 让其他进程读写本进程数据;
- 导致目标进程崩溃(访问无效地址)。
通过拒绝上述消息的跨进程发送,系统强制应用程序使用安全的 IPC 机制(如共享内存、命名管道、LPC)。
7.5.6 死锁检测与超时机制
7.5.6.1 死锁的产生
考虑以下场景:
线程 A: SendMessage(hWndB, WM_USER) // 等待 B 处理
线程 B: SendMessage(hWndA, WM_USER) // 等待 A 处理
双方互相等待 → 死锁!
7.5.6.2 MsqIsHung 挂起检测
c
BOOL FASTCALL
MsqIsHung(PTHREADINFO pti, DWORD dwTimeout)
{
// 检查线程最后活动时间距离现在是否超过 dwTimeout
// 如果超过,认为线程"挂起"(hung)
DWORD LastActivity = pti->pcti->tickLast;
DWORD Now = EngGetTickCount();
return ((Now - LastActivity) > dwTimeout);
}
MSQ_HUNG 常量:win32ss/user/ntuser/msgqueue.h(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h) 中定义(通常 5 秒)。
7.5.6.3 SMTO_ABORTIFHUNG 处理
c
if ((uFlags & SMTO_ABORTIFHUNG) && MsqIsHung(ptiSendTo, 4 * MSQ_HUNG))
{
// FIXME: Set window hung and add to a list.
ERR("Window %p (%p) (pti %p) is hung!\n", hWnd, Window, ptiSendTo);
goto Cleanup; // Return FALSE
}
当 SMTO_ABORTIFHUNG 设置且接收方被检测为挂起(4 倍 MSQ_HUNG 时间)时,立即返回而不等待。
7.5.6.4 SMTO_NOTIMEOUTIFNOTHUNG 重试
c
do
{
Status = co_MsqSendMessage(ptiSendTo, hWnd, Msg, wParam, lParam,
uTimeout, (uFlags & SMTO_BLOCK),
MSQ_NORMAL, uResult);
}
while ((Status == STATUS_TIMEOUT) &&
(uFlags & SMTO_NOTIMEOUTIFNOTHUNG) &&
!MsqIsHung(ptiSendTo, MSQ_HUNG));
当 SMTO_NOTIMEOUTIFNOTHUNG 设置时:
- 如果超时但接收方未挂起 (仍在响应),无限重试;
- 如果接收方已挂起,则停止重试并返回超时。
典型应用 :消息循环在 PeekMessage 中重发消息时使用此标志。
7.5.6.5 幽灵窗口
c
if (0 && MsqIsHung(ptiSendTo, MSQ_HUNG))
{
TRACE("Let's go Ghost!\n");
IntMakeHungWindowGhosted(hWnd);
}
当接收方挂起时,Windows 会将窗口标记为"幽灵窗口"(ghost window)------ 渲染时显示为灰色、不可交互。此代码段在 ReactOS 中被禁用(if (0 && ...))。
7.5.7 co_MsqSendMessage 消息队列同步等待
7.5.7.1 函数签名
win32ss/user/ntuser/msgqueue.c:1055-1334(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1055-L1334):
c
NTSTATUS FASTCALL
co_MsqSendMessage( PTHREADINFO ptirec, // 接收方线程
HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam,
UINT uTimeout, BOOL Block, INT HookMessage,
ULONG_PTR *uResult);
7.5.7.2 完整执行流程
co_MsqSendMessage(ptirec, Wnd, Msg, wParam, lParam, uTimeout, Block, HookMessage, uResult)
│
├── 1. 死亡检查
│ if (pti->TIF_flags & TIF_INCLEANUP || ptirec->TIF_flags & TIF_INCLEANUP)
│ {
│ if (pti->TIF_flags & TIF_INCLEANUP && !(ptirec->TIF_flags & TIF_INCLEANUP))
│ co_MsqSendMessageAsync(...) // 异步发送(fire-and-forget)
│ return STATUS_UNSUCCESSFUL
│ }
│
├── 2. 挂起检查
│ if (IsThreadSuspended(ptirec)) return STATUS_UNSUCCESSFUL
│
├── 3. 跨进程边界检查(见 7.5.5)
│
├── 4. 分配 USER_SENT_MESSAGE
│ Message = AllocateUserMessage(TRUE) // TRUE = 分配 pkCompletionEvent
│
├── 5. 初始化消息字段
│ Message->Msg = { Wnd, Msg, wParam, lParam }
│ Message->ptiReceiver = ptirec
│ Message->ptiSender = pti
│ Message->HookMessage = HookMessage
│ Message->QS_Flags = QS_SENDMESSAGE
│
├── 6. 保存当前消息
│ SaveMsg = pti->pusmSent
│ pti->pusmSent = Message
│
├── 7. 插入接收方队列
│ InsertTailList(&ptirec->SentMessagesListHead, &Message->ListEntry)
│
├── 8. 唤醒接收方
│ MsqWakeQueue(ptirec, QS_SENDMESSAGE, TRUE)
│
├── 9. 关闭栈交换(首次进入)
│ if (pti->cEnterCount == 0)
│ SwapStateEnabled = KeSetKernelStackSwapEnable(FALSE)
│ pti->cEnterCount++
│
├── 10. 同步等待(核心)
│ if (Block)
│ {
│ // 阻塞模式:只等待 Event + Thread 死亡
│ WaitObjects[0] = Message->pkCompletionEvent
│ WaitObjects[1] = ptirec->pEThread
│ WaitStatus = KeWaitForMultipleObjects(2, ..., WaitAny, ...)
│ }
│ else
│ {
│ // 非阻塞模式:处理自己的消息循环
│ WaitObjects[0] = Message->pkCompletionEvent
│ WaitObjects[1] = pti->pEventQueueServer // 自己的消息队列
│ WaitObjects[2] = ptirec->pEThread
│ do
│ {
│ WaitStatus = KeWaitForMultipleObjects(3, ..., WaitAny, ...)
│ while (co_MsqDispatchOneSentMessage(pti)) // 处理自己的 sent messages
│ ;
│ } while (WaitStatus == STATUS_WAIT_1)
│ }
│
├── 11. 恢复栈交换(最后退出)
│ if (--pti->cEnterCount == 0)
│ KeSetKernelStackSwapEnable(SwapStateEnabled)
│
├── 12. 处理 User APC(发送方死亡)
│ if (WaitStatus == STATUS_USER_APC)
│ {
│ Message->flags |= SMF_SENDERDIED
│ co_IntDeliverUserAPC()
│ }
│
├── 13. 唤醒自己(让消息循环继续)
│ KeSetEvent(pti->pEventQueueServer, IO_NO_INCREMENT, FALSE)
│
├── 14. 提取结果
│ Result = Message->lResult
│
└── 15. 释放消息
if (Message->flags & SMF_RECEIVERFREE)
FreeUserMessage(Message)
7.5.7.3 Block vs NonBlock 同步等待对比
| 维度 | Block 模式 | NonBlock 模式 |
|---|---|---|
| 调用者 | co_IntSendMessageTimeout(SMTO_BLOCK) |
co_IntSendMessageWithCallBack(SendMessageCallback) |
| 等待对象 | Event + 接收方 Thread | Event + 自己的消息队列 + 接收方 Thread |
| 消息循环 | 不处理 | 处理自己的 sent messages(避免嵌套死锁) |
| 用途 | 简单同步 | 嵌套发送、回调处理 |
7.5.7.4 关键状态保存
c
SaveMsg = pti->pusmSent; // 保存当前 pusmSent
pti->pusmSent = Message; // 设置新的 pusmSent
// ... 等待、处理 ...
pti->pusmSent = SaveMsg; // 恢复原 pusmSent
pusmSent 是当前正在等待回复的 sent message 。ReplyMessage 通过 pusmCurrent 设置 lResult。
7.5.7.5 cEnterCount 与栈交换
c
if (pti->cEnterCount == 0)
{
SwapStateEnabled = KeSetKernelStackSwapEnable(FALSE);
}
pti->cEnterCount++;
cEnterCount记录线程进入 win32k 的嵌套深度;- 首次进入时禁用栈交换 (
KeSetKernelStackSwapEnable(FALSE))以保证等待期间的栈稳定; - 退出时恢复原设置(可能是启用)。
7.5.8 接收者死亡与发送者死亡
7.5.8.1 接收者死亡(SMF_RECEIVERDIED)
win32ss/user/ntuser/msgqueue.c:1216-1220(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1216-L1220):
c
// Receiving thread passed on and left us hanging with issues still pending.
else if (WaitStatus == STATUS_WAIT_1)
{
ERR("Bk Receiving Thread woken up dead!\n");
Message->flags |= SMF_RECEIVERDIED;
}
触发条件 :KeWaitForMultipleObjects 返回 STATUS_WAIT_1(即 ptirec->pEThread 对象被 signal,意为线程死亡)。
处理:
- 设置
SMF_RECEIVERDIED标志; - 不释放消息内存(因为接收方可能还在处理中);
- 由后续消息队列清理机制释放。
7.5.8.2 发送者死亡(SMF_SENDERDIED)
win32ss/user/ntuser/msgqueue.c:1291-1301(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1291-L1301):
c
// Handle User APC
if (WaitStatus == STATUS_USER_APC)
{
TRACE("User APC\n");
// The Message will be on the Trouble list until Thread cleanup.
Message->flags |= SMF_SENDERDIED;
co_IntDeliverUserAPC();
ERR("User APC Returned\n"); // Should not see this message.
}
触发条件 :等待期间,发送方线程收到 User APC(如 ExitThread、异步过程调用)。
处理:
- 设置
SMF_SENDERDIED标志; - 调用
co_IntDeliverUserAPC()投递 APC; - 消息将留在 usmList 麻烦列表,由接收方或线程清理机制处理。
7.5.8.3 消息释放策略
c
// Determine whether this message is being processed or not.
if ((Message->flags & (SMF_RECEIVERBUSY|SMF_RECEIVEDMESSAGE)) != SMF_RECEIVEDMESSAGE)
{
Message->flags |= SMF_RECEIVERFREE;
}
if (!(Message->flags & SMF_RECEIVERFREE))
{
TRACE("Sender Freeing Message %p ptirec %p bit %d list empty %d\n",...);
FreeUserMessage(Message);
}
释放规则:
- 如果接收方已经开始处理 (
SMF_RECEIVEDMESSAGE已设置),接收方负责释放(清除SMF_RECEIVERFREE); - 如果接收方未处理(如挂起时死亡),发送方负责释放。
7.5.8.4 死亡消息清理
当线程清理(PspExitThread)时,会调用 IntUserThreadCleanup → 清理 SentMessagesListHead 中所有未处理的消息,避免内存泄漏。
7.5.9 co_MsqSendMessageAsync 异步发送
7.5.9.1 函数签名
win32ss/user/ntuser/msgqueue.c:1013-1053(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L1013-L1053):
c
BOOL FASTCALL
co_MsqSendMessageAsync( PTHREADINFO ptiReceiver,
HWND hwnd, UINT Msg, WPARAM wParam, LPARAM lParam,
SENDASYNCPROC CompletionCallback,
ULONG_PTR CompletionCallbackContext,
BOOL HasPackedLParam,
INT HookMessage);
7.5.9.2 实现
c
BOOL FASTCALL
co_MsqSendMessageAsync(PTHREADINFO ptiReceiver, HWND hwnd, UINT Msg,
WPARAM wParam, LPARAM lParam,
SENDASYNCPROC CompletionCallback,
ULONG_PTR CompletionCallbackContext,
BOOL HasPackedLParam, INT HookMessage)
{
PTHREADINFO ptiSender;
PUSER_SENT_MESSAGE Message;
if(!(Message = AllocateUserMessage(FALSE))) // FALSE = 不分配 pkCompletionEvent
{
ERR("MsqSendMessageAsync(): Not enough memory to allocate a message\n");
return FALSE;
}
ptiSender = PsGetCurrentThreadWin32Thread();
Message->Msg.hwnd = hwnd;
Message->Msg.message = Msg;
Message->Msg.wParam = wParam;
Message->Msg.lParam = lParam;
Message->pkCompletionEvent = NULL; // 不需要 event
Message->ptiReceiver = ptiReceiver;
Message->ptiCallBackSender = ptiSender;
Message->CompletionCallback = CompletionCallback;
Message->CompletionCallbackContext = CompletionCallbackContext;
Message->HookMessage = HookMessage;
Message->HasPackedLParam = HasPackedLParam;
Message->QS_Flags = QS_SENDMESSAGE;
Message->flags = SMF_RECEIVERFREE;
InsertTailList(&ptiReceiver->SentMessagesListHead, &Message->ListEntry);
MsqWakeQueue(ptiReceiver, QS_SENDMESSAGE, TRUE);
return TRUE;
}
7.5.9.3 与 co_MsqSendMessage 的区别
| 维度 | co_MsqSendMessage | co_MsqSendMessageAsync |
|---|---|---|
| 同步等待 | Yes(KeWaitForMultipleObjects) |
No(立即返回) |
| pkCompletionEvent | Yes | No |
| ptiSender | Yes | No(仅 ptiCallBackSender) |
| 用途 | 同步发送 | 异步回调发送、线程死亡时的"火与忘" |
7.5.9.4 异步发送的回调处理
win32ss/user/ntuser/msgqueue.c:923-947(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L923-L947) 在 co_MsqDispatchOneSentMessage 中:
c
else if ((Message->CompletionCallback) &&
(Message->ptiCallBackSender == pti))
{
if (Message->QS_Flags & QS_SMRESULT)
{
co_IntCallSentMessageCallback(Message->CompletionCallback,
Message->Msg.hwnd,
Message->Msg.message,
Message->CompletionCallbackContext,
Message->lResult);
Message->CompletionCallback = NULL;
}
else
{
// 消息未处理,重新排队
RemoveEntryList(&Message->ListEntry);
InsertTailList(&Message->ptiCallBackSender->SentMessagesListHead, &Message->ListEntry);
MsqWakeQueue(Message->ptiCallBackSender, QS_SENDMESSAGE, TRUE);
Ret = FALSE;
goto Exit;
}
}
处理流程:
- 接收方 WndProc 处理消息;
- 接收方调用
ReplyMessage设置QS_SMRESULT; - 消息被重新插入到回调发送方 的
SentMessagesListHead; - 发送方线程的
co_MsqDispatchOneSentMessage检测到QS_SMRESULT,调用回调函数。
7.5.10 广播、Notify 与 SendMessageCallback
7.5.10.1 广播(HWND_BROADCAST / HWND_TOPMOST)
win32ss/user/ntuser/message.c:1719-1759(file:///d:/reactos/win32ss/user/ntuser/message.c#L1719-L1759) co_IntSendMessageTimeout:
c
LRESULT FASTCALL
co_IntSendMessageTimeout( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam,
UINT uFlags, UINT uTimeout, ULONG_PTR *uResult )
{
if (hWnd != HWND_BROADCAST && hWnd != HWND_TOPMOST)
return co_IntSendMessageTimeoutSingle(...);
if (!is_message_broadcastable(Msg)) return TRUE;
DesktopWindow = UserGetDesktopWindow();
if (hWnd != HWND_TOPMOST)
{
// 桌面窗口自身也接收
co_IntSendMessageTimeoutSingle(UserHMGetHandle(DesktopWindow), Msg, wParam, lParam, uFlags, uTimeout, uResult);
}
// 遍历所有顶层窗口
Children = IntWinListChildren(DesktopWindow);
for (Child = Children; *Child; Child++)
{
PWND pwnd = UserGetWindowObject(*Child);
if (!pwnd) continue;
// 排除菜单和切换窗口
if (pwnd->fnid == FNID_MENU || pwnd->pcls->atomClassName == gpsi->atomSysClass[ICLS_SWITCH])
continue;
co_IntSendMessageTimeoutSingle(*Child, Msg, wParam, lParam, uFlags, uTimeout, uResult);
}
ExFreePoolWithTag(Children, USERTAG_WINDOWLIST);
return TRUE;
}
is_message_broadcastable :检查消息是否允许广播。系统保留消息(如 WM_CREATE)不允许广播。
7.5.10.2 HWND_TOPMOST 与 HWND_BROADCAST 的区别
| 句柄 | 含义 | 接收方 |
|---|---|---|
| HWND_BROADCAST (-1) | 所有顶层窗口 | 桌面 + 所有顶层 |
| HWND_TOPMOST (0) | 所有顶层窗口 | 仅所有顶层(不含桌面) |
7.5.10.3 UserSendNotifyMessage 通知消息
win32ss/user/ntuser/message.c:2089-2135(file:///d:/reactos/win32ss/user/ntuser/message.c#L2089-L2135):
c
BOOL FASTCALL
UserSendNotifyMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam )
{
BOOL Ret = TRUE;
if (is_pointer_message(Msg, wParam))
{
EngSetLastError(ERROR_MESSAGE_SYNC_ONLY);
return FALSE;
}
if (hWnd == HWND_BROADCAST)
{
HWND *List;
PWND DesktopWindow;
DesktopWindow = UserGetDesktopWindow();
List = IntWinListChildren(DesktopWindow);
if (List != NULL)
{
UserSendNotifyMessage(UserHMGetHandle(DesktopWindow), Msg, wParam, lParam);
for (i = 0; List[i]; i++)
{
PWND pwnd = UserGetWindowObject(List[i]);
if (!pwnd) continue;
if (pwnd->fnid == FNID_MENU || pwnd->pcls->atomClassName == gpsi->atomSysClass[ICLS_SWITCH])
continue;
Ret = UserSendNotifyMessage(List[i], Msg, wParam, lParam);
}
ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
}
}
else
{
Ret = co_IntSendMessageNoWait( hWnd, Msg, wParam, lParam);
}
return Ret;
}
SendNotifyMessage 特点:
- 不等待接收方处理;
- 指针参数(
wParam、lParam)被复制到接收方进程(适合带数据的通知); - 不能发送指针消息(
WM_POINTER*),返回ERROR_MESSAGE_SYNC_ONLY。
7.5.10.4 SendMessageCallback 回调发送
win32ss/user/ntuser/message.c:1784-1955(file:///d:/reactos/win32ss/user/ntuser/message.c#L1784-L1955) co_IntSendMessageWithCallBack:
c
LRESULT FASTCALL
co_IntSendMessageWithCallBack( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam,
SENDASYNCPROC CompletionCallback,
ULONG_PTR CompletionCallbackContext,
ULONG_PTR *uResult)
{
// 1. 引用窗口对象
// 2. 同线程 fast path(如果有)
// 3. 分配 USER_SENT_MESSAGE
// 4. 设置 CompletionCallback
// 5. 插入接收方队列
// 6. 立即返回
// 7. 接收方处理后,调用回调函数
// co_IntCallSentMessageCallback(CompletionCallback, hWnd, Msg,
// CompletionCallbackContext, Result);
}
SendMessageCallback 特点:
- 不阻塞发送方;
- 通过
CompletionCallback通知发送方结果; - 适合"需要结果但不阻塞"的场景(如 UI 线程调用后台线程)。
7.5.10.5 4 种语义的时序图对比
SendMessage: SendNotifyMessage: SendMessageCallback:
A B Win32k A B Win32k A B Win32k
├─Send──────────────────────►├─Send────────────────────►├─Send─────────────────►
│ │ │ │ │ │
│ │ ├─插入队列 │ ├─插入队列 + 回调 │
│ │ │ │ │ │
│ ┌─GetMessage──►│ │ ├─GetMessage─────────►│ │ ├─GetMessage──────►│
│ ├─WndProc───────►│ │ ├─WndProc─────────────►│ │ ├─WndProc─────────►│
│ ├─ReplyMessage──►│ │ │ │ │ ├─ReplyMessage───►│
│ └───────────────│ │ │ │ │ │ │
│◄─Result─────────────────┤ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ ◄┤ │ │ │ │ │
│◄─立即返回─────────────────┤ │ │ │ │ │
│ │ │ │ ◄┤─Callback───────────┤
7.5.11 co_MsqReplyMessage 同步响应
7.5.11.1 函数签名
win32ss/user/ntuser/msgqueue.c:2483-2504(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c#L2483-L2504):
c
BOOL FASTCALL
co_MsqReplyMessage( LRESULT lResult );
7.5.11.2 实现
c
BOOL FASTCALL
co_MsqReplyMessage( LRESULT lResult )
{
PUSER_SENT_MESSAGE Message;
PTHREADINFO pti;
pti = PsGetCurrentThreadWin32Thread();
Message = pti->pusmCurrent;
if (!Message) return FALSE;
if (Message->QS_Flags & QS_SMRESULT) return FALSE;
// SendMessageXxx || Callback msg and not a notify msg
if (Message->ptiSender || Message->CompletionCallback)
{
Message->lResult = lResult;
Message->QS_Flags |= QS_SMRESULT;
// See co_MsqDispatchOneSentMessage, change bits already accounted for and
// cleared and this msg is going away..
}
return TRUE;
}
7.5.11.3 工作流程
1. 接收方 GetMessage 取到 sent message
└─► pti->pusmCurrent = Message
2. WndProc 处理消息
3. 接收方调用 ReplyMessage(result)
└─► co_MsqReplyMessage(result)
├─► Message->lResult = result
└─► Message->QS_Flags |= QS_SMRESULT
4. co_MsqDispatchOneSentMessage 检测 QS_SMRESULT
├─► 若是 SendMessage:KeSetEvent(pkCompletionEvent)
└─► 若是 SendMessageCallback:调用 CompletionCallback
5. 发送方从 KeWaitForMultipleObjects 唤醒
└─► 提取 Message->lResult
7.5.11.4 pusmCurrent 与 pusmSent
| 字段 | 含义 | 设置者 |
|---|---|---|
| pusmCurrent | 当前接收方正在处理的消息 | co_MsqDispatchOneSentMessage |
| pusmSent | 当前发送方正在等待回复的消息 | co_MsqSendMessage |
ReplyMessage 只设置 pusmCurrent.lResult------因为只有当前正在被 WndProc 处理的消息才能被 ReplyMessage 响应。
7.5.11.5 ReplyMessage 失败情况
| 情况 | 返回 | 原因 |
|---|---|---|
pusmCurrent == NULL |
FALSE | 当前没有 sent message 在处理 |
QS_Flags & QS_SMRESULT |
FALSE | 已调用过 ReplyMessage(不能重复) |
| 没有 ptiSender 也没有 CompletionCallback | TRUE(但不设 QS_SMRESULT) | 同步发送但无需响应(如内部消息) |
7.5.12 设计哲学问答
Q1:为什么 SendMessage 同步等待时使用 pkCompletionEvent 而不是直接的 state variable?
A :效率 + 线程阻塞。
KeWaitForMultipleObjects 等待内核事件比 while (!done) Sleep(1) 忙等待效率高得多:
- 无忙等待:发送方线程在内核态挂起,不消耗 CPU;
- 即时唤醒 :
KeSetEvent直接将线程放回就绪队列,延迟 < 1 µs; - 支持超时 :
KeWaitForMultipleObjects接受Timeout参数,硬件定时器到期后自动唤醒; - 支持 APC :等待期间可以投递 User APC(如
ExitThread),线程能优雅退出。
如果使用 volatile int done + 忙等待,会消耗 100% CPU,且无法优雅处理 APC 投递。
Q2:为什么内部消息(Msg & 0x80000000)不同步等待?
A :内部消息是 win32k 系统内部使用,不需要跨线程。
c
if (Msg & 0x80000000)
{
TRACE("SMTS: Internal Message!\n");
Result = (ULONG_PTR)handle_internal_message( Window, Msg, wParam, lParam );
// ...
}
内部消息(WM_USER 以上,且 & 0x80000000)是 win32k 与 user32 之间的私有消息:
- 接收方必定是当前线程(因为是 win32k 内部使用);
- 无需线程同步,fast path 直接处理;
- 避免无意义的线程切换开销。
Q3:跨进程消息为什么要拒绝 WM_CREATE / WM_NOTIFY / 密码消息?
A :防止跨进程内存访问冲突。
WM_CREATE:CREATESTRUCT 包含窗口创建参数(位置、样式、父窗口句柄),这些参数只在创建进程内有意义;WM_NOTIFY:NMHDR 指针指向发送进程的内存,接收进程无法访问;WM_GETTEXT/EM_GETLINE/EM_SETPASSWORDCHAR:在密码编辑框中,这些消息会泄露密码内容。
Windows 的设计原则:"跨进程消息的 wParam/lParam 必须是值类型,不能是指针"。如果应用程序需要传递数据,应该使用共享内存、文件映射、命名管道、共享内存等安全的 IPC 机制。
Q4:为什么 ReplyMessage 只能响应一次?
A :一个消息只能有一个结果。
c
if (Message->QS_Flags & QS_SMRESULT) return FALSE;
- 第一次 ReplyMessage 设置
QS_SMRESULT; - 第二次调用时检测到该标志,直接返回 FALSE;
- 强制语义一致性:消息要么未响应,要么已响应,不存在"多个结果"。
这避免了以下场景:
c
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == WM_USER)
{
ReplyMessage(100); // 第一次
// ... 一些处理 ...
ReplyMessage(200); // 第二次会被忽略
}
return 0;
}
Q5:为什么 NonBlock 模式要在等待时处理自己的 sent messages?
A :避免嵌套死锁。
考虑以下场景:
线程 A: SendMessageCallback(B, WM_USER)
└─► 等待结果(NonBlock)
└─► 期间线程 A 的消息队列中有 sent message from C
└─► 如果不处理,线程 C 也会等 A
└─► A 等 B,B 等 C,C 等 A ──► 死锁!
NonBlock 模式在 KeWaitForMultipleObjects 等待期间主动处理 自己的 sent messages(co_MsqDispatchOneSentMessage),打破嵌套死锁链。
线程 A: SendMessageCallback(B, WM_USER)
├─► 等待 Event / 自己消息队列 / B 线程
│ ├─► Event 触发 ──► 立即返回
│ ├─► 自己消息队列 ──► 处理 from C ──► C 不再等 A
│ └─► B 线程死亡 ──► SMF_RECEIVERDIED
└─► 恢复
总结
视窗报文发送机制是 Windows GUI 应用程序间通信的核心,比投递消息(Post)复杂得多。本节介绍了:
- 4 种发送语义:SendMessage(同步)、SendNotifyMessage(异步复制)、SendMessageCallback(异步回调)、PostMessage(异步无返回值);
- USER_SENT_MESSAGE 结构:包含消息内容、收发双方线程、事件、回调、状态标志;
- SMF_ 状态*:RECEIVERFREE、RECEIVERBUSY、RECEIVERDIED、SENDERDIED、RECEIVEDMESSAGE;
- 同线程 fast path:直接调用 WndProc,无线程切换;
- 跨线程同步等待 :Block/NonBlock 双模式、
KeWaitForMultipleObjects等待事件; - 跨进程边界检查:拒绝 WM_CREATE、WM_NOTIFY、密码消息;
- 死锁检测:MsqIsHung、SMTO_ABORTIFHUNG、SMTO_NOTIMEOUTIFNOTHUNG;
- 接收者/发送者死亡:SMF_RECEIVERDIED、SMF_SENDERDIED、APC 投递;
- 异步发送:co_MsqSendMessageAsync(无 event、仅回调);
- 广播:HWND_BROADCAST / HWND_TOPMOST 遍历所有顶层窗口;
- ReplyMessage :通过
pusmCurrent+QS_SMRESULT同步响应。
核心要点回顾:
- 同步发送是"线程间 RPC",使用
pkCompletionEvent同步; - 同线程发送走 fast path,性能高且无死锁;
- 跨进程消息有严格的安全检查(密码、WM_CREATE、WM_NOTIFY);
- SMF_* 状态机管理消息的完整生命周期;
- NonBlock 模式在等待期间处理自己的 sent messages,避免嵌套死锁;
- ReplyMessage 只能响应一次,强制结果唯一性。
本章代码索引
| 文件 | 内容 |
|---|---|
| win32ss/user/ntuser/message.c(file:///d:/reactos/win32ss/user/ntuser/message.c) | co_IntSendMessage、co_IntSendMessageTimeoutSingle、co_IntSendMessageTimeout、co_IntSendMessageNoWait、co_IntSendMessageWithCallBack、UserSendNotifyMessage、co_IntDoSendMessage、PackParam、UnpackParam |
| win32ss/user/ntuser/msgqueue.c(file:///d:/reactos/win32ss/user/ntuser/msgqueue.c) | co_MsqSendMessage、co_MsqSendMessageAsync、co_MsqDispatchOneSentMessage、co_MsqReplyMessage、MsqIsHung、AllocateUserMessage、FreeUserMessage |
| win32ss/user/ntuser/msgqueue.h(file:///d:/reactos/win32ss/user/ntuser/msgqueue.h) | USER_SENT_MESSAGE 结构、SMF_* 标志、QS_SENDMESSAGE、QS_SMRESULT、MSQ_HUNG |
| win32ss/user/ntuser/ntstubs.c(file:///d:/reactos/win32ss/user/ntuser/ntstubs.c) | NtUserSendMessage、NtUserSendMessageTimeout、NtUserSendNotifyMessage 等系统调用 stub |
| win32ss/user/ntuser/main.c(file:///d:/reactos/win32ss/user/ntuser/main.c) | win32k 初始化 |
| win32ss/user/ntuser/dde.c(file:///d:/reactos/win32ss/user/ntuser/dde.c) | IntDdeSendMessageHook(DDE 消息发送检查) |
| win32ss/user/user32/windows/message.c(file:///d:/reactos/win32ss/user/user32/windows/message.c) | user32 用户态 SendMessage 入口(与 win32k 对接) |