第五章 走得太远,别忘了回家的路(1) ——《箴言》第三章 Windows运行机理之读书笔记之一 【内核分析 运行机理 LE文件的格式 VxD的设计实现】(略) 一、“整个消息的处理就很简单了” “我们首先从16位的Windows来认识消息。在16位时代,Windows的整个内核是32位的、分时的、抢占的。所以从Windows的内核模型得知,有两种VM,一种是SYSTEM VM,一种是DOS的VM。一个系统中可以运行很多的DOS窗口,因为在16位的时代,能运行DOS的程序很重要的,所以在当时,Windows的主要任务之一,就是能同时运行很多DOS窗口。Windows的内核实现上用了很多微内核,而微内核的工作很多是靠消息来完成的。” 梁先生云:“在16位时代,Windows的整个内核是32位的、分时的、抢占的。”我第一次听到这样说,别的就不说了,就看看那时成熟的Windows3.1吧,还是来看看MS自己的说法吧:关于16位还是32位:“Windows 3.1 and Windows 3.11 (called Windows for Workgroups) are the 16-bit version of the Windows operating system family.”(《Windows Architecture Training for Developers》1998 Microsoft Corporation) “Windows 3.1 is a 16-bit operating environment that runs on the MS-DOS operating system.”可见16位时代的Windows决不是“整个内核是32位”的;关于抢占性:“There are two types of multitasking: cooperative (or nonpreemptive) and preemptive. In a cooperative multitasking environment such as Windows 3.1, use of the processor is never taken from a task. Instead, a task must voluntarily yield control of the processor before any other task can run.”,由此可见,16位时代也不是“Windows的整个内核是抢占的”,而是非抢占性的(nonpreemptive)。非抢占性的而想要同时又是“分时的”,孤陋寡闻的我又算是开了一回眼。 至于说到16位时代的Windows要能运行DOS程序,我想,如果16位时代的Windows这种座落在MS-DOS基础上的操作环境封杀了MS-DOS程序,那才是天大的笑话呢。16位时代的“Windows的内核实现上用了很多微内核”?如果不是作者把VM与微内核混为一谈的话,那么就又是我的孤陋寡闻了。 “到Windows 32位时,消息的运行机理就不相同了。从内核中可以看出,有一个Win32的VxD,把DOS的抢占分时都放在这个VM中完成,系统VM就进一步和系统底层融合。然后在这个基础上分出时间片。这样,每个应用程序就自己有自己的消息队列。所有的消息队列看上去是放在USER32的模块内,但每个应用程序自己有一个USER32,因为每个应用程序在内存内都是从4000000B(也就是4MB的位置开始的),这样,每个GetMessage和PeekMessage都在处理事件。实际上,每个GetMessage都会成为一个WaitsingleMessage,当有事件来后,就直接进行处理,也不用做什么调度。因为自己完成自己的消息处理,每个程序都是独立的,所以要用底层内核来实现页面的切换。它某程序切入时,其他程序就会被切出。当切换出去时,整个消息队列也就被切换出去了。所以,整个消息的处理就很简单了。” 从梁先生对Windows 32位的描述(内核中的“Win32的VxD”、DOS VM、“系统VM”)来看,那就以Windows 95为例吧。为了叙述用语与作者保持一致,下面我们姑且把“每个应用程序”当作每个进程来看。 “每个应用程序就自己有自己的消息队列”?每个应用程序?且不说MS-DOS程序了,就说16位和32位的Windows-based应用程序吧。关于Win32线程,MSDN2001(PSDK - Message Routing - Queued Messages)上说,“The system maintains a single system message queue and one thread-specific message queue for each GUI thread. To avoid the overhead of creating a message queue for non-GUI threads, all threads are created initially without a message queue. The system creates a thread-specific message queue only when the thread makes its first call to one of the User or GDI functions.”Win32之非GUI线程就没有线程(登记)消息队列(与Win16不同)。关于系统消息队列,MSDN2001中Windows 95 Resources Kit(第31章)说:“Windows 95 uses an asynchronous input model for all input to the system and applications. As the various input devices generate interrupts, the interrupt handler converts these interrupts to messages and sends the messages to a raw input thread area, which in turn passes each message to the appropriate message queue. Although each Win32-based thread can have its own message queue, all Win16-based applications share a common message queue”([system message queue] NT3.51Arch User介绍中谈及Win16 “a Common Queue”--RIQ)。为了最大限度地提供兼容性,仿真了16位Windows的Windows-based应用程序运行环境,16位的Windows-based应用程序在Windows 95中都共享一个系统消息队列(Windows NT对此处理不同)。 “Windows 32位”下16位的Windows-based应用程序同样也是非抢占性的,正是需要调度的(参见MSDN2001 技术文章:《GetMessage and PeekMessage Internals》Bob Gunderson 1992年)。因此也就不是“每个GetMessage都会成为一个WaitsingleMessage,当有事件来后,就直接进行处理,也不用做什么调度。因为自己完成自己的消息处理,每个程序都是独立的”。至于32位的Windows-based应用程序,也不是“每个GetMessage都会成为一个WaitsingleMessage,当有事件来后,就直接进行处理,也不用做什么调度”的。当有Posted事件来后,此时如果也已有其他应用程序发送的消息,那么就需要调度,按照Windows的处理规则,这样的发送消息将先得到处理,然后才可能是Posted消息。 32位的Windows-based应用程序,不仅GUI程序(进程)这样的“每个应用程序就自己有自己的消息队列”,而且更具体地说每个GUI线程都有自己的消息队列,同一进程中的线程收到其他GUI线程发送的消息时也象其他进程(中的线程)发送的消息那样需要调度的处理。 在列举了MSG结构之后,“从以上结构中可以看到,每个消息都对应着一个窗口。USER模块是管理窗口的,一般每个窗口自己有一个消息队列,当键盘或鼠标有消息时,就会发给激活的窗口,当在程序设计中用SendMessage来发送消息时,就会明确指定窗口句柄,当运行此函数后,就会把消息放到此窗口的消息队列中。” “从以上结构(MSG结构 笔者注)中可以看到,每个消息都对应着一个窗口”,不错,每个消息(结构)都有一个窗口句柄(成员)变量,但却未必总“都对应着一个窗口”。直接用PostMessage(NULL, message, wparam, lparam)来Posted至线程的消息,虽然也要指定这个窗口句柄值,但那是一个NULL值,意味着这个消息不“对应着一个窗口”。又如用Win32的PostThreadMessage() 或Win16的PostAppMessage()Posted的消息,那消息结构中的窗口句柄也不“对应着一个窗口”。而对PostMessage(HWND_TOPMOST/HWND_BROADCAST, message, wparam, lparam)来说,则是广播消息,则此时一个消息就常要对应多个窗口。 梁先生云“一般每个窗口自己有一个消息队列”。“每个窗口自己”?无论是16位的Windows还是32位的Windows(Windows 9x及Windows NT系列)都没有达到这个地步。即使是32位的Windows应用程序(进程)也只是每个GUI线程也只有一个消息队列而已(从应用程序的角度上)。【(Posted)消息队列归属Task(APP, Win16)或Thread(GUI,Win32)】 :“当在程序设计中用SendMessage来发送消息时,就会明确指定窗口句柄,当运行此函数后,就会把消息放到此窗口的消息队列中。”线程内用SendMessage来发送消息时,就像普通的函数调用一样,是直接进行的,没有经过消息队列这一环节。关于线程内发送消息的情形,我们还是看看随后作者自己的话吧:“SendMessage:当用它向一个窗口(也可以是本身窗口)发送消息时,它不会把消息放入消息队列中,而是直接发送给窗口。窗口接到消息后就立即处理,处理完成后,把结果作为返回值传送回来。这样的处理过程就像是操作函数一样。”即使是其他线程 用SendMessage来发送消息时也不一律“就会把消息放到此窗口的消息队列中”,在Win16中并不把消息放在窗口的消息队列中(参见MSDN2001 技术文章:《GetMessage and PeekMessage Internals》Bob Gunderson 1992年);【在不能立即调度执行的情况下,在Windows NT 3.1中,据Jeffrey的看法,才是这样的(这点与MSDN2001有出入);】而按照MSDN及《Programming Applications for Microsoft Windows》的说法,在不能立即调度执行的情况下,Windows NT 系列(NT xx以后?)则消息并不放在登记消息队列中、而是放在发送消息队列中(MSDN只称说“pending messages”)(可以参看《Programming Applications for Microsoft Windows》1999年第4版)。 “其实,明白了消息的处理过程,消息也就很简单了。消息不过是定义一个结构,定义一堆ID,在程序运行中调用switch和case去完成相应的功能。” switch和case这样的处理方式只是其中的一种形式而已,ATL/WTL的if-else结构也是一种形式,MFC的映射表也是一种形式,等等。 线程外用SendMessage发来的消息并不就总是“直接发送给窗口的”,它是需要调度的,如果接受消息的线程尚未将控制权交给系统【SendMessage之Incomming nonqueue message缺口】,那么系统此时是不能将此消息“直接发送给窗口”的,否则此线程的消息处理或将会乱成一锅粥了。至于控制权,对Windows事件驱动系统而言,大抵是由应用程序通过调用GetMessage、PeekMessage等来控制的,这些调用可以说是Windows事件驱动系统之控制权管理的“原语”。【我们来看看MSDN(2001.10)上的说法:SendMessage:“If the specified window was created by a different thread, the system switches to that thread and calls the appropriate window procedure. Messages sent between threads are processed only when the receiving thread executes message retrieval code.”(适用于Windows NT 3.1 and later, Windows 95 and later)】 “PostMessage:当用它向一个窗口(也可以是本身窗口)发送消息时,它把消息放入消息队列中,自己什么也不干就会返回,到底消息什么时候处理,有没有被处理它是不知道的。” 关于PostMessage,有一种情形,就是不一定总是“当用它向一个窗口(也可以是本身窗口)发送消息时,它把消息放入消息队列中”。“A common programming error is to assume that the PostMessage function always posts a message. This is not true when the message queue is full. An application should check the return value of the PostMessage function to determine whether the message has been posted and, if it has not been, repost it.”(Platform SDK: Windows User Interface MSDN2001.10)【与MAC处理方式不同(见《Inside Macintosh》1985年),下面在适当时候再来看看】 “其实,很多程序可以完全不用注册窗口。它只要做一些事件,当有事件来时,就处理相应的事件。例如,以下就是一个Windows程序,其中就没有用到任何消息循环,只是在运行中弹出一个对话框: //--------------------------------------------------------------- // HelloMsg.c – Displays “Hello Windows 98!” in a //message box // ----------------------------------------------------------- #include <windows.h> int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { MessageBox (NULL, TEXT (“Hello, Windows 98!”), TEXT (“HelloMsg”), 0) ; return 0 ; } 可以看到,这个Win32的程序就没用到微软的框架,它完全是自己做自己的事,直到被中止。” 【“很多程序可以完全不用注册窗口”,因为MessageBox所建立的窗口是一种system classes,“#32770”(The class for a dialog box.) 。“A system class is a window class registered by the system. Many system classes are available for all processes to use, while others are used only internally by the system.”[Message Windows 2000/XP: The class for a message-only window.] (MSDN2001.10)】 其实,MessageBox就已内置了消息循环。“An application creates the message box by using the MessageBox or MessageBoxEx function ”,“A message box is a modal dialog box and the system creates it by using the same internal functions that DialogBox uses.”关于DialogBox,“The function displays the dialog box (regardless of whether the template specifies the WS_VISIBLE style), disables the owner window, and starts its own message loop to retrieve and dispatch messages for the dialog box.” (Platform SDK: Windows User Interface MSDN2001.10)如果我们的高手对MFC不是那么不屑一顾的话,还可在MFC库的源代码中看到MFC通过建立Windows无模式对话框来实现MFC模式对话框的运行机理,其中就包含了内部消息循环的建立,这对DialogBox、MessageBox之类的内部消息循环处理的理解不无裨益罢。 至此“消息的运行方式”、“Windows的消息内核原理”、“Windows系统中消息的运作方式”等等,就算是结束了。无怪乎梁先生云“整个消息的处理就很简单了”,“其实,明白了消息的处理过程,消息也就很简单了”。高手举重若轻的本领也象以前一样不得不令人佩服。在领略了好些“你不说我还明白,你越说我越糊涂了”的所谓“简单”之后,不过还是不解心中的诸多疑惑…… 二、“Message Deadlocks”、大师Petzold与Jeffrey的差异