第二十一课 管道


这一讲将探索一下管道,看看它是什么、有什么用。为使之更加生动有趣,我将用怎样改变 Edit 控件的背景色和文本颜色来说明此技术。

理论:

管道,顾名思义就是有两个端的通道。可以使用管道在进程间、同一进程内进行数据交换,就像手提式无线电话机一样。把管道的一端给另一方,他就可以借助管道和你通讯了。

有两种管道,即有名管道和匿名管道。匿名管道就是没有名字的管道了,也就是说在使用它们时不需要知道其名字。而有名管道正好相反,在使用前必须知道其名字。

也可以根据管道的特性来分类,即是单向的还是双向的。单向管道,数据只能沿一个方向移动,从一端流向另一端,而双向管道数据可以在两端间自由交换。匿名管道通常是单向的而有名管道通常是双向的。有名管道常用于一个服务器联络多个客户端的网络环境。

这一讲将详细讨论一下匿名管道。匿名管道主要目的是作为父进程与子进程、子进程之间通讯的联结通路。在处理控制台问题时,匿名管道是相当有用的。控制台应用程序就是使用控制台作为输入和输出的一种 Win32 应用程序。一个控制台就像一个 DOS 窗口。但控制台应用程序的的确确是32位的应用程序,它可以向其它图形程序一样使用 GUI 函数,只不过它碰巧使用了控制台罢了。

控制台应用程序有三个用于输入输出的标准句柄,它们是标准输入、标准输出和标准错误句柄。标准输入用于从控制台读或取信息而标准输出用于往控制台写或打印信息。标准错误用于汇报输出不能重定向的错误。

控制台应用程序可以通过调用函数 GetStdHandle 来获得这三个句柄。一个图形应用程序没有控制台,如果在其中调用GetStdHandle 就会返回错误;如果的确要使用控制台,可以调用AllocConsole 来分配一个新的控制台以使用,但当处理完成时,别忘了调用 FreeConsole 来释放控制台。

匿名管道用得最多的功能就是 重定向子进程的标准输入和标准输出。父进程可以是一个控制台或者是图形程序,而子进程必须是控制台应用程序。众所周知,控制台应用使用标准输入输出句柄。若要重定向输入输出,就得用指向管道一端的句柄来替换这个标准句柄。控制台应用程序不会知道我们使用了指向管道任一端的句柄,它会把这个句柄作为标准句柄来看待。借用面向对象的术语,这就是多态性的一种。因为子进程不需作任何改动,因此这种方法是非常有用的。

关于控制台应用程序应该掌握的另一点就是它从哪获得标准句柄。当一个控制台应用程序被创建时,父进程有两种选择:为子进程创建一个新的控制台或者是让子进程继承自己的控制台。若使用后者,那父进程本身必须是一个控制台应用程序,或者如果是 GUI 应用程序,它必须首先调用 AllocConsole 分配了一个控制台。

通过调用 CreatePipe 来创建一个匿名管道,它的原型为:

CreatePipe proto pReadHandle:DWORD, \
pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD

如果函数调用成功返回值为非零,否则为零。成功调用之后,就会得到两个句柄,一个指向管道的读出端,另一个指向管道的写入端。现在我将要把重点放到重定向子控制台程序的标准输出到自己进程的所需的步骤上。注意我的这个方法不同于Borland 公司的 API 参考上的例子。Win32 API 参考上假设父进程是控制台应用程序,因此子进程可以继承它的标准句柄。然而大多数情况下我们需要重定向控制台应用程序的输出到 GUI 应用程序。

创建匿名管道使用 CreatePipe ,同时别忘了把 SECURITY_ATTRIBUTES 结构成员bInheritable 设置为TRUE,这样才可以继承句柄。

现在要准备好创建进程的函数即CreateProcess 的参数,只有它才可以装载子控制台应用程序。STARTUPINFO 是一个重要的结构,它决定了子进程出现时主窗口的外观,它对于我们的目标也是至关重要的。通过这个结构就可以隐藏主窗口并且把管道句柄传递给子进程。

以下就是必须要填写的成员:

调用CreateProcess 来创建子进程,但调用成功后子进程仍然不处于激活状态。它被装进了内存但并没有立即运行。

在父进程中关闭管道的写端也是必须的。这是因为父进程并不使用管道的写句柄,而且如果一个管道有两个写入端也就不会工作,因此我们在从管道往外读数据之前必须关闭管道的写端。但是不能在调用CreateProcess 之前关闭,否则管道就坏了。你应当在CreateProcess 刚刚返回并且在读数据之前关闭管道的写端。

现在就可以通过函数ReadFile 在管道的读端读数据了。通过使用ReadFile ,可以使子进程处于运行状态。它将开始执行,并且当它往标准输出( 实际上是管道的写端 )上写数据时,数据就会被送至管道的读端。应当不停调用ReadFile 直至它的返回值为 0 ,也就是说再也没有数据可读了。对从管道读来的数据你可以进行任何处理,在我们的例子中它被显示在 Edit 控件中。

记得用完后关闭管道的读句柄。

代码举例:


.386 
.model flat,stdcall 
option casemap:none 
include \masm32\include\windows.inc 
include \masm32\include\user32.inc 
include \masm32\include\kernel32.inc 
include \masm32\include\gdi32.inc 
includelib \masm32\lib\gdi32.lib 
includelib \masm32\lib\user32.lib 
includelib \masm32\lib\kernel32.lib 
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD 

.const 
IDR_MAINMENU equ 101         ; the ID of the main menu 
IDM_ASSEMBLE equ 40001 

.data 
ClassName            db "PipeWinClass",0 
AppName              db "One-way Pipe Example",0 EditClass db "EDIT",0 
CreatePipeError     db "Error during pipe creation",0 
CreateProcessError     db "Error during process creation",0 
CommandLine     db "ml /c /coff /Cp test.asm",0 

.data? 
hInstance HINSTANCE ? 
hwndEdit dd ? 

.code 
start: 
    invoke GetModuleHandle, NULL 
    mov hInstance,eax 
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT 
    invoke ExitProcess,eax 

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD 
    LOCAL wc:WNDCLASSEX 
    LOCAL msg:MSG 
    LOCAL hwnd:HWND 
    mov wc.cbSize,SIZEOF WNDCLASSEX 
    mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc 
    mov wc.cbClsExtra,NULL 
    mov wc.cbWndExtra,NULL 
    push hInst 
    pop wc.hInstance 
    mov wc.hbrBackground,COLOR_APPWORKSPACE 
    mov wc.lpszMenuName,IDR_MAINMENU 
    mov wc.lpszClassName,OFFSET ClassName 
    invoke LoadIcon,NULL,IDI_APPLICATION 
    mov wc.hIcon,eax 
    mov wc.hIconSm,eax 
    invoke LoadCursor,NULL,IDC_ARROW 
    mov wc.hCursor,eax 
    invoke RegisterClassEx, addr wc 
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL 
    mov hwnd,eax 
    .while TRUE 
        invoke GetMessage, ADDR msg,NULL,0,0 
        .BREAK .IF (!eax) 
        invoke TranslateMessage, ADDR msg 
        invoke DispatchMessage, ADDR msg 
    .endw 
    mov eax,msg.wParam 
    ret 
WinMain endp 

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM 
    LOCAL rect:RECT 
    LOCAL hRead:DWORD 
    LOCAL hWrite:DWORD 
    LOCAL startupinfo:STARTUPINFO 
    LOCAL pinfo:PROCESS_INFORMATION 
    LOCAL buffer[1024]:byte 
    LOCAL bytesRead:DWORD 
    LOCAL hdc:DWORD 
    LOCAL sat:SECURITY_ATTRIBUTES 
    .if uMsg==WM_CREATE 
        invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL 
        mov hwndEdit,eax 
    .elseif uMsg==WM_CTLCOLOREDIT 
        invoke SetTextColor,wParam,Yellow 
        invoke SetBkColor,wParam,Black 
       invoke GetStockObject,BLACK_BRUSH 
        ret 
    .elseif uMsg==WM_SIZE 
        mov edx,lParam 
        mov ecx,edx 
        shr ecx,16 
        and edx,0ffffh 
        invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE 
    .elseif uMsg==WM_COMMAND 
       .if lParam==0 
            mov eax,wParam 
            .if ax==IDM_ASSEMBLE 
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES 
                mov sat.lpSecurityDescriptor,NULL 
                mov sat.bInheritHandle,TRUE 
                invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL 
                .if eax==NULL 
                    invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK 
                .else 
                    mov startupinfo.cb,sizeof STARTUPINFO 
                    invoke GetStartupInfo,addr startupinfo 
                    mov eax, hWrite 
                    mov startupinfo.hStdOutput,eax 
                    mov startupinfo.hStdError,eax 
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES 
                    mov startupinfo.wShowWindow,SW_HIDE 
                    invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo 
                    .if eax==NULL 
                        invoke MessageBox,hWnd,addr CreateProcessError,addr         AppName,MB_ICONERROR+MB_OK 
                    .else 
                        invoke CloseHandle,hWrite 
                        .while TRUE 
                            invoke RtlZeroMemory,addr buffer,1024 
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL 
                            .if eax==NULL 
                                .break 
                            .endif 
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0 
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer 
                        .endw 
                    .endif 
                    invoke CloseHandle,hRead 
                .endif 
            .endif 
        .endif 
    .elseif uMsg==WM_DESTROY 
        invoke PostQuitMessage,NULL 
    .else 
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret 
    .endif 
    xor eax,eax 
    ret 
WndProc endp 
end start 

分析:

这个例子调用 ml.exe 来汇编一个名为 test.asm 的程序,并且重定向 ml.exe 的输出到客户区的 Edit 控件中。当程序被加载时,象往常一样要注册窗口类和创建主窗口。在主窗口被创建的过程中要做的第一件事就是创建用于显示程序 ml.exe 输出的 Edit 控件。

现在有趣的事来了,我们将改变此 Edit 控件的文本颜色和背景色。当 Edit 控件将要重画客户区时,它会给父窗口发送 WM_CTLCOLOREDIT 消息。参数 wParam 包含了用于画控件自己的客户区设备描述符的句柄 (HDC) 。我们可以利用这种机制来修改 HDC 的特性。


    .elseif uMsg==WM_CTLCOLOREDIT 
        invoke SetTextColor,wParam,Yellow 
        invoke SetTextColor,wParam,Black 
        invoke GetStockObject,BLACK_BRUSH 
        ret

SetTextColor 把文本颜色变为黄色,背景颜色变为黑色。最后我们返回一个通过调用GetStockObject 而得到黑色刷子的句柄。处理WM_CTLCOLOREDIT 必须返回一个刷子的句柄,因为 Windows 将要使用这个刷子来重画 Edit 控件的背景。在这个例子中,我希望背景是黑色,所以返回了一个黑色刷子的句柄。

现在当用户选择 Assemble 子菜单时,就会创建一个匿名管道。

            .if ax==IDM_ASSEMBLE 
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES 
                mov sat.lpSecurityDescriptor,NULL 
                mov sat.bInheritHandle,TRUE  

在调用CreatePipe 之前,必须要填写SECURITY_ATTRIBUTES 结构。如果我们不关心安全性的话,可以在lpSecurityDescriptor 成员中填入 NULL 。bInheritHandle 则必须为 TRUE ,这样管道的句柄才可以被子进程继承。

invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

在此之后,我们调用CreatePipe 来创建管道,如果成功,那么变量hRead 和 hWrite 将分别被填入相应的管道的读出端和写入端的句柄。


                    mov startupinfo.cb,sizeof STARTUPINFO 
                    invoke GetStartupInfo,addr startupinfo 
                    mov eax, hWrite 
                    mov startupinfo.hStdOutput,eax 
                    mov startupinfo.hStdError,eax 
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES 
                    mov startupinfo.wShowWindow,SW_HIDE 

下一步就是填写STARTUPINFO 结构了。调用 GetStartupinfo 用父进程的缺省值来填写STARTUPINFO 结构。如果要使程序同时工作在 Windows9x 和 Windows NT 下,就必须调用GetStartupInfo 来填写STARTUPINFO 结构。调用返回后,就可以修改重要的成员了。因为我们要子进程输出到父进程而不是缺省的标准输出和标准错误,所以我们把hStdOutput 和 hStdError 都赋成管道写端的句柄。为了隐藏子进程的主窗口,必须把成员变量wShowWidow 赋值为SW_HIDE 。最后通过把成员 dwFlags 赋值为STARTF_USESHOWWINDOW 和 STARTF_USESTDHANDLES 来指明成员hStdOutput, hStdError 和 wShowWindow 是有效的。

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

现在调用CreateProcess 来创建子进程。注意为使管道工作,参数bInheritHandles 必须设置为TRUE。 invoke CloseHandle,hWrite 成功创建子进程之后,在父进程中必须关闭管道的写端。我们已经把写端的句柄通过结构STARTUPINFO 传给了子进程。如果不关闭,那么管道就有两个写入端,而这样的管道是不会工作的。所以必须在创建子进程后但在读数据前关闭这个句柄。


                        .while TRUE 
                            invoke RtlZeroMemory,addr buffer,1024 
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL 
                            .if eax==NULL 
                                .break 
                            .endif 
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0 
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer 
                        .endw 

现在已经准备好从子进程的标准输出读数据了。直到再也没有数据了,即 ReadFile 返回为 NULL时才会退出循环,否则一直会等待数据。我们调用ReadFile 之前先调用RtlZeroMemory 来清空内存,并且用管道的读句柄代替文件句柄。注意读数据的最大长度为 1023 ( sizeof(buffer)-1 ),因为我们需要把接受的字符变为一个 Edit 控件可以处理的 ASCII 串。当ReadFile 返回时,就把此数据传给 Edit 控件。然而这有一个小小的问题,如果使用SetWindowText API 往 Edit 控件中写数据,新数据就会覆盖已存在的旧数据,而我们想把新数据添加在已有数据的后面。为达此目的,首先通过发送一个 wParam 为-1的 EM_SETSEL 消息,把 Edit 控件的输入焦点移动到文本的末端;然后发送一个 EM_REPLACESEL 消息把数据添加后面。

invoke CloseHandle,hRead

当ReadFile 返回为NULL时,就跳出循环并关闭管道的读句柄。




翻译:ZhangJun,校对:LuoYunBin's Win32 ASM Page, http://asm.yeah.net