Reactos 第 7 章 视窗报文 — 7.5 视窗报文的发送

第 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 标志)。

本节内容概览

  1. 7.5.0 框架图:视窗报文发送的整体架构;
  2. 7.5.1 视窗报文发送总览:4 种语义(Send/Post/SendCallback/Notify);
  3. 7.5.2 USER_SENT_MESSAGE 与 SMF_ 状态标志*;
  4. 7.5.3 SendMessage 同线程 fast path
  5. 7.5.4 co_IntSendMessageTimeoutSingle 跨线程发送详解
  6. 7.5.5 跨进程边界与 DDE 检查
  7. 7.5.6 死锁检测与超时机制
  8. 7.5.7 co_MsqSendMessage 消息队列同步等待
  9. 7.5.8 接收者死亡与发送者死亡
  10. 7.5.9 co_MsqSendMessageAsync 异步发送
  11. 7.5.10 广播、Notify 与 SendMessageCallback
  12. 7.5.11 co_MsqReplyMessage 同步响应
  13. 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_SENDMESSAGEco_MsqSendMessage 中由发送方设置在接收方的 fsWakeBits 上。

QS_SMRESULTco_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 messageReplyMessage 通过 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;
   }
}

处理流程

  1. 接收方 WndProc 处理消息;
  2. 接收方调用 ReplyMessage 设置 QS_SMRESULT
  3. 消息被重新插入到回调发送方SentMessagesListHead
  4. 发送方线程的 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 特点

  • 不等待接收方处理;
  • 指针参数(wParamlParam被复制到接收方进程(适合带数据的通知);
  • 不能发送指针消息(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)复杂得多。本节介绍了:

  1. 4 种发送语义:SendMessage(同步)、SendNotifyMessage(异步复制)、SendMessageCallback(异步回调)、PostMessage(异步无返回值);
  2. USER_SENT_MESSAGE 结构:包含消息内容、收发双方线程、事件、回调、状态标志;
  3. SMF_ 状态*:RECEIVERFREE、RECEIVERBUSY、RECEIVERDIED、SENDERDIED、RECEIVEDMESSAGE;
  4. 同线程 fast path:直接调用 WndProc,无线程切换;
  5. 跨线程同步等待 :Block/NonBlock 双模式、KeWaitForMultipleObjects 等待事件;
  6. 跨进程边界检查:拒绝 WM_CREATE、WM_NOTIFY、密码消息;
  7. 死锁检测:MsqIsHung、SMTO_ABORTIFHUNG、SMTO_NOTIMEOUTIFNOTHUNG;
  8. 接收者/发送者死亡:SMF_RECEIVERDIED、SMF_SENDERDIED、APC 投递;
  9. 异步发送:co_MsqSendMessageAsync(无 event、仅回调);
  10. 广播:HWND_BROADCAST / HWND_TOPMOST 遍历所有顶层窗口;
  11. ReplyMessage :通过 pusmCurrent + QS_SMRESULT 同步响应。

核心要点回顾

  1. 同步发送是"线程间 RPC",使用 pkCompletionEvent 同步;
  2. 同线程发送走 fast path,性能高且无死锁;
  3. 跨进程消息有严格的安全检查(密码、WM_CREATE、WM_NOTIFY);
  4. SMF_* 状态机管理消息的完整生命周期;
  5. NonBlock 模式在等待期间处理自己的 sent messages,避免嵌套死锁;
  6. 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 对接)
相关推荐
callJJ6 小时前
Volta + Claude Code 在 Windows 上的路径 Bug 复盘
windows·bug
女神下凡6 小时前
这是 Cursor(Composer) 的五种核心交互模式
服务器·人工智能·windows·vscode·microsoft
techdashen6 小时前
从 Windows 的 ping.exe 入手:动态库、调用约定与 Rust FFI
开发语言·windows·rust
独隅7 小时前
IntelliJ IDEA 在 Windows 上的完整安装与使用指南
java·windows·intellij-idea
逻极7 小时前
Windows 平台 Ollama AMD GPU 一键编译指南:基于 ROCm 7.1 的自动化实战
人工智能·windows·stm32·自动化·gpu·amd·ollama
caimouse8 小时前
Reactos 第 9 章 设备驱动 — 9.13 同步I/O与异步I/O
windows
caimouse8 小时前
Reactos 第 9 章 设备驱动 — 9.10 磁盘的Miniport驱动模块
windows·嵌入式硬件
caimouse9 小时前
Reactos 第 9 章 设备驱动 — 9.11 命名管道与Mailslot
windows
x***r1519 小时前
Krita 5.2.13 安装教程 Windows版:自定义路径+开源绘画软件配置指南
windows