网上第一本以TXT格式的VC++深入详解孙鑫的书.全文全以TXT格式,并每一章节都分了目录,清晰易读
源代码在线查看: 17.2.3 父进程的实现.txt
17.2.3 父进程的实现
下面,我们就利用匿名管道实现进程间的通信。首先实现父进程,新建一个单文档类型的MFC应用程
序,工程取名为: Parent。首先为该工程增加一个子菜单,名称为"匿名管道"。接着,为该子菜单
添加三个菜单项,井分别为它们添加相应的命令响应函数,本例选择CParentView类接收这些命令响
应函数。各菜单项的E、名称,以及响应函数如表
17.4所示。
表17.4添加的菜单项及相应的晌应函数
ID 菜单名称 响应函数
IDM_PIPE_CREATE 创建管道 OnPipeCreate
IDM_PIPE_READ 读取数据 OnPipeRead
IDM_PIPE_WRITE 写入数据 OnPipeWrite
接下来,为CParentView类增加以下两个私有成员变量,即两个句柄,它们将分别作 为匿名管道的
读写句柄来使用。
private:
HANDLE hWrite,
HANDLE hRead,
并在CParentView类的构造函数中将它们都初始化为NULL:
CParentView::CParentView()
// TODO: add construction code here
hRead=NULL,
hWrite=NULL,
然后在CParentView类的析构函数中,如果判断出这两个变量有值,则调用CloseHandle函数关闭这
两个变量。
CParentView::-CParentView()
if(hRead)
CloseHandle(hRead) ;
if(hWrite)
CloseHandle(hWrite) ,
1.创建匿名管道
现在就可以在【创建管道】菜单项命令响应函数OnPipeCreate中调用CreatePipe创建匿名管道,返
回管道的读写句柄。代码如例 17-3所示。
例17-3
void CParentView: :OnPipeCreate()
1. SECURITY_ATTRIBUTES sa;
2. sa.bInheritHandle=TRUE;
3. sa.lpSecurityDescriptor=NULL;
4. sa.nLength=sizeof(SECURITY_ATTRIBUTES) ;
5. if(!CreatePipe(&hRead, &hWrite , &sa , 0))
6. {
7. MessageBox ( "创建匿名管道失败!");
8. return;
9. }
10. STARTUPINFO sui;
11. PROCESS_INFORMATION pi;
12. ZeroMemory(&sui, sizeof(STARTUPINFO) ) ;
13. sui.cb=sizeof(STARTUPINFO);
14. sui.dwFlags=STARTF_USESTDHANDLES;
15. sui.hStdInput=hRead;
16. sui.hStdOutput=hWrite;
17. sui.hStdError=GetStdHandle(STD_ERROR_HANDLE);
18.
19. if(!CreateProcess("..\\Child\\Debug\\Child.exe" , NULL , NULL , NULL ,
20. TRUE, 0, NULL , NULL , &sui , &pi))
21. {
22. CloseHandle(hRead);
23. CloseHandle(hWrite);
24 . hRead=NULL ;
25. hWrite=NULL;
26. MessageBox("创建子进程失败!");
27. return;
28. }
29. else
30. {
31. CloseHandle(pi.hProcess);
32. CloseHandle(pi.hThread);
33. }
在如例 17-3所示代码中,首先定义了一个安全结构体 (SECURITY_ATTRIBUTES) 类型的变量: sa,
并对其成员分别赋值。这里需要将 bInheritHandle成员设置为 TRUE,让子进程可以继承父进程创
建的匿名管道的读写句柄:将、安全描述符成员 ( lpSecurityDescriptor)设置为 NULL,让系统为
创建的匿名管道赋予默认的安全描述符:长度成员(nLeng也)可以利用 sizeof函数得到 SECURITY_
ATIRmUIES结构体的长度。
然后,就调用 CreatePipe函数创建匿名管道(第 5行代码),前两个参数就是返回的管道的读取和写
入句柄,第三个参数就是刚刚定义的安全结构的地址,最后一个参数设置为 0,让系统使用默认的
缓冲区大小。 CreatePipe函数如果调用失败,将返回一个 0值,这时提示用户:"创建匿名管道失
败!",并立即返回。
如果创建匿名管道成功,就启动子进程,并将匿名管道的读写句柄传递给子进程。为了启动一个进
程,可以调用 CreateProcess函数,根据前面的介绍,我们知道该函数的第九
个参数需要一个 STARTUPIOFO结构体类型的值,因此上述代码中定义了一个这种结构体类型的变量:
sui (第 10行代码 ),因为该结构体中有多个成员,而我们只用到了其中的一小部分,那么其他的
成员,如果没有将它们置为 0之前,它们都只是一些随机的值,如果将这些随机的值传递给 Create
Process函数,可能会影响该函数执行的结果,所以首先应该调用ZeroMemory函数将 sui变量中所有
成员都设置为 0(第 12行代码)。接下来,为 sui变量的几个必要成员赋值。正如前面所说,通常在
使用结构体变量时,都会为该结构体中表示结构体本身大小的成员赋值,因此,这里首先用
STARTUPIOFO结构体长度为 sui变量的 cb成员赋值:接着,设定标志成员 : dwFlags的值为 STARTF_
USESTDHANDLES , 当采用此标志时,就表示当前 STARTUPIOF。这个结构体变量中的标准输入、标准
输出和标准错误句柄这三个成员是有用的,本例将子进程的标准输入句柄、输出句柄分别设置为管
道的读、写句柄。当调用 CreateProcess函数启动一个子进程时,它将继承父进程中所有可继承的
己打开的句柄,但在子进程中无法知道它所继承的句柄中哪一个是管道的读句柄,哪一个是管道的
写句柄,因为管道的读写句柄并不是通过参数,或者其他方式传递进来的,它们只是子进程从其父
进程中继承的众多句柄中的两个。为了让子进程从众多继承的句柄中区分出管道的读、写句柄,就
必须将子进程的特殊句柄设置为管道的读写句柄,因此这里就将子进程的标准输入和输出句柄分别
设置为管道的读、写句柄。这样,在子进程中,只要得到了标准输入和标准输出句柄,就相当于得
到了这个管道的读、写句柄。接下来,还要设置子进程的标准错误设备句柄,这可以通过
GetStdHandle函数得到,该函数可以获得标准输入、标准输出,或者一个标准错误输出句柄。该函
数的原型声明如下:
HANDLE GetStdHandle(DWORD nStdHandlel;
GetStdHandle函数有一个 DWORD类型的参数,通过为该参数指定不同的值来得到不同的句柄。该参
数的取值如表 17.5所示。
表 17.5 nStdHandle参数取值
STD_INPUT_HANDLE 标准输入设备句柄
STD_OUTPUT_HANDLE 标准输出设备句柄
STD_ERROR_HANDLE 标准错误设备句柄
由表 17.5可知,如果将 GetStdHandle函数的参数指定为 STD_INPUT_HANDLE,那么该函数返回的就
是一个标准的错误设备句柄。应注意,这里得到的是父进程的标准错误句柄,也就是说,将子进程
的标准错误句柄设置为父进程的标准错误句柄。虽然这个标准错误设备句柄在本程序中没有被使用,
但读者应该清楚这里得到的是父进程的错误句柄。
接下来,如例 17-3所示的 OnPipeCreate函数就调用 CreateProcess函数创建子进程 (第 19行代
码)。将该函数的第一个参数设置为子进程的应用程序的文件名,这里先指定为"..\\Child\\Debug
\\Child.exe",下面我们再编写 Child这个子进程程序,并把这个子进程程序的目录与本程序所在
目录保持平级:第二个参数指定命令行参数,本例不需要指定,所以设置为 NULL;第三个参数和第四
个参数分别是进程安全属性和线程安全属性,均设置为 NULL,使用默认的安全属性;第五个参数设
置为 TRUE,让父进程的每个可继承的打
开句柄都能被子进程继承:第六个参数是创建标志,本例并不需要指定,所以设置为 0;第七个参数
设置为NULL,让新进程使用调用进程的环境:第八个参数把当前路径设置为 NULL,让子进程与父进
程拥有同样的驱动器和路径:第九个参数就是刚刚定义的 STARUPINFO结构体变量:sui的地址:第
十个参数是PROCESS_INFORMATION结构体变量指针,同样,先定义一个该结构体类型的变量: pi (第
11行代码),然后将该变量的地址传递给这个参数。
程序需要对CreateProcess函数的返回值进行判断,如果该函数调用失败,将返回0值,这时就调用
两次CloseHandle函数关闭管道的读、写句柄,并将这两个句柄设置为NULL(这是为了避免在
CParentView类的析构函数再次调用CloseHandle函数关闭这些句柄,将它们设置为 NULL),然后提
示用户:"创建子进程失败!"并返回:如果 CreateProcess函数调用成功,则调用 CloseHandle函数关
闭所返回的子进程的句柄和子进程中主线程的句柄。为什么要这么做呢?前面已经提到,在创建一个
新进程时,系统会为该进程建立一个进程内核对象和一个线程内核对象,而内核对象都有一个使用
计数,系统会为这两个对象赋予初始的使用计数: 1,在 CreateProcess函数返回之前,它将打开创
建的进程对象和线程对象,并将每个对象与进程和线程相关的句柄放在其最后一个参数: PROCESS_
INFORMATION结构体变量的相应成员中。当 CreateProcess函数在其内部打开这些对象时,每个对象
的使用计数就变为 2,如果在父进程中不需要使用子进程的这两个句柄,则可以调用 CloseHandle
函数关闭它们,系统会将于进程的进程内核对象和线程内核对象的
』
计数减1,当子进程终止运行时,系统会再将这些使用计数减1,这时子进程的进程内核
对象和线程内核对象的计数都变为0了,这两个内核对象就能够被释放了。所以在编程时,
当不需要这些内核对象时,总是应该调用CloseHandle函数关闭它们。
以上就是在父进程中创建匿名管道的实现代码。当子进程启动之后,父子进程间就可以通过创建的
匿名管道来读取数据和写入数据。对于管道的读取和写入实际上是通过调用 ReadFile和WriteFile
这两个函数完成的。前面已经提到过,这两个函数不仅能够完成对文件的读写,还可以完成对像控
制台和管道这类对象的读写操作。
2.读取数据
下面在OnPipeRead函数中实现匿名管道的读取操作,结果如例17-4所示。
例17-4
void CParentView::OnPipeRead(}
char buf [100] ;
DWORD dwRead;
if(!ReadFile(hRead,buf, 100 , &dwRead, NULL))
MessageBox("读取数据失败!"};
return;
MessageBox(buf}i
在实现读取操作时,首先定义了一个 char类型的字符数组: buf,用来存放将要读取到的数据。接
着,定义了一个 DWORD类型的变量: dwRead,用来保存实际读取的字节数。接下来就是调用 ReadFile
函数利用匿名管道的读句柄从管道中读取数据。该函数如果调用失败,将返回 0值,这时提示用户:"
读取数据失败!";如果 ReadFile函数调用成功,则调用 MessageBox函数显示读取到的数据。
3.写入数据
下面在 OnPipeWrite函数中实现医名管道的写入操作,结果如例 17-5所示。
例 17-5
void CParentView::OnPipeWrite()
char buf []= " http://www. sunxin .org" ;
DWORD dwWrite;
if(!WriteFile(hWrite,buf , strlen(buf)+1 , &dwWrite, NULL))
{
MessageBox("写入数据失败!··);
return;
同样地,在实现写入操作时,首先定义了一个 char类型的字符数组: buf,其内容是一个网址:http://www.sunxin.org",就是我们将要写入的数据。接着,定义了一个 DWORD类型的变量: dwWrite,
用来保存实际写入的字节数。接下来就是调用 WriteFile函数利用匿名管道的写入句柄向管道写入
数据。该函数如果调用失败,将返回 O值,这时提示用户:"写入数据失败!",然后调用 retum语句
返回。
最后,需要利用 Build命令生成 Parent程序。
以上就是利用匿名管道实现父子进程间通信的父进程程序的实现,在父进程中创建匿名管道,返回
该管道的读、写句柄,然后调用 CreateProcess函数启动子进程,并且将子进程的标准输入和标准
输出句柄设置为匿名管道的读、写句柄,相当于将该管道的读写句柄作上了一个标记,传递给子进
程,然后在子进程中得到自己的标准输入和标准输出句柄时,相当于得到了匿名管道的读、写句柄。