第9章 进程、线程和DLL操作技巧
本章介绍了进程的相关概念和对进程的各种操作,以及进程间通信的方式。进程通常被定义为程序运行的实例,它一般包括两部分,即进程内核和进程地址空间。进程是不活泼的,若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。线程的操作要注意线程对资源的同步和互斥的情况。最后阐述了动态链接库的创建及调用方式。
9.1 进程的操作技巧
在操作系统的教材中,都学习过程序和进程的概念:程序是一段代码,它是一个静态的概念,进程是程序的一次执行,它是一个动态的概念,进程具有生命周期,在其生命周期期间,具有不同的状态:新建、运行、阻塞、就绪和完成5个状态。在多任务操作系统中,可以同时运行多个进程,每个进程都有自己的虚拟地址空间。进程在自己的地址空间中修改数据不会影响其他的进程。操作系统为了识别不同的进程,为每个进程分配一个进程标识。我们如何去创建一个进程,如何在进程之间进行通信呢?下面将会解决这些疑问。
9.1.1 进程的概念
进程是当前操作系统下一个被加载到内存的、正在运行的应用程序的实例。每一个进程都是由内核对象和地址空间所组成的,内核对象可以让系统在其内存放有关进程的统计信息并使系统能够以此来管理进程,而地址空间则包括了所有程序模块的代码和数据,以及线程堆栈、堆分配空间等动态分配的空间。进程仅仅是一个存在,是不能独自完成任何操作的,必须拥有至少一个在其环境下运行的线程,并由其负责执行在进程地址空间内的代码。在进程启动的同时即同时启动了一个线程,该线程被称作主线程或执行线程,由此线程可以继续创建子线程。如果主线程退出,那么进程也就没有存在的可能了,系统将自动撤销该进程并完成对其地址空间的释放。
加载到进程地址空间的每一个可执行文件或动态链接库文件的映像都会被分配一个与之相关联的全局唯一的实例句柄(Hinstance),该实例句柄实际是一个记录有进程加载位置的基本内存地址。进程的实例句柄在程序入口函数WinMain()中通过第一个参数HINSTANCE hinstExe传递,其实际值即为进程所使用的基本地址空间的地址。对于VC++链接程序所链接产生的程序,其默认的基本地址空间地址为0x00400000,如没有必要不要修改该值。在程序中,可以通过GetModuleHandle()函数得到指定模块所使用的基本地址空间。
9.1.2 创建/终止进程
1.问题阐述
进程的创建通过CreateProcess()函数来实现,CreateProcess()通过创建一个新的进程及在其地址空间内运行的主线程来启动并运行一个新的程序。具体地,在执行CreateProcess()函数时,首先由操作系统负责创建一个进程内核对象,初始化计数为1,并立即为新进程创建一块虚拟地址空间。随后将可执行文件或其他任何必要的动态链接库文件的代码和数据装载到该地址空间中。在创建主线程时,也是首先由系统负责创建一个线程内核对象,并初始化为1。最后启动主线程并执行进程的入口函数WinMain(),完成对进程和执行线程的创建。
2.实现技巧
CreateProcess()函数的原型声明如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName, // 可执行模块名
LPTSTR lpCommandLine, // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全属性
BOOL bInheritHandles, // 句柄继承标志
DWORD dwCreationFlags, // 创建标志
LPVOID lpEnvironment, // 指向新的环境块的指针
LPCTSTR lpCurrentDirectory, // 指向当前目录名的指针
LPSTARTUPINFO lpStartupInfo, // 指向启动信息结构的指针
LPPROCESS_INFORMATION lpProcessInformation // 指向进程信息结构的指针
);
3.实例代码
在程序设计时,某一个具体的功能模块可以通过函数或线程等不同的形式来实现。对于同一进程而言,这些函数、线程都是存在于同一个地址空间下的,而且在执行时,大多只对与其相关的一些数据进行处理。如果算法存在某种错误,将有可能破坏与其同处一个地址空间的其他一些重要内容,这将造成比较严重的后果。为保护地址空间中的内容可以考虑将那些需要对地址空间中的数据进行访问的操作部分放到另外一个进程的地址空间中运行,并且只允许其访问原进程地址空间中的相关数据。具体地,可在进程中通过CreateProcess()函数去创建一个子进程,子进程在全部处理过程中只对父进程地址空间中的相关数据进行访问,从而可以保护父进程地址空间中与当前子进程执行任务无关的全部数据。对于这种情况,子进程所体现出来的作用同函数和线程比较相似,可以看成是父进程在运行期间的一个过程。为此,需要由父进程来掌握子进程的启动、执行和退出。下面这段代码即展示了此过程:
CString sCommandLine;
char cWindowsDirectory[MAX_PATH];
char cCommandLine[MAX_PATH];
DWORD dwExitCode;
PROCESS_INFORMATION pi;
STARTUPINFO si = {sizeof(si)};
// 得到Windows目录
GetWindowsDirectory(cWindowsDirectory, MAX_PATH);
// 启动“记事本”程序的命令行
sCommandLine = CString(cWindowsDirectory) + "//NotePad.exe";
::strcpy(cCommandLine, sCommandLine);
// 启动“记事本”作为子进程
BOOL ret = CreateProcess(NULL, cCommandLine, NULL, NULL, FALSE, 0, NULL, NULL,
&si, &pi);
if (ret) {
// 关闭子进程的主线程句柄
CloseHandle(pi.hThread);
// 等待子进程的退出
WaitForSingleObject(pi.hProcess, INFINITE);
// 获取子进程的退出码
GetExitCodeProcess(pi.hProcess, &dwExitCode);
// 关闭子进程句柄
CloseHandle(pi.hProcess);
}
4.小结
此段代码首先通过CreateProcess()创建Windows自带的“记事本”程序为子进程,子进程启动后父进程通过WaitForSingleObject()函数等待其执行的结束,在子进程没有退出前父进程是一直处于阻塞状态的,这里子进程的作用同单线程中的函数类似。一旦子进程退出,WaitForSingleObject()函数所等待的pi.hProcess对象将得到通知,父进程将得以继续,如有必要可以通过GetExitCodeProcess()来获取子进程的退出代码。
9.1.3 获取系统进程的技巧
1.问题阐述
进程的定义是为执行程序指令的线程而保留的一系列资源的集合。进程是一个可执行的程序,由私有虚拟地址空间、代码、数据和其他操作系统资源(如进程创建的文件、管道、同步对象等)组成。进程是一些所有权的集合,一个进程拥有内存、CPU运行时间等一系列资源,为线程的运行提供一个环境,每个进程都有它自己的地址空间和动态分配的内存、线程、文件和其他一些模块。
2.实现技巧
系统统快照的获取可以通过Win32 API函数CreateToolhelp32Snapshot()来完成,通过该函数不仅可以获取进程的快照,同样可以获取堆、模块和线程的系统快照。函数的声明如下:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags, //指定要创建包含哪一类系统信息的快照函数
DWORD th32ProcessID //指定进程的ID号,当设定为0时表示指定当前进程
);
一旦系统得到系统快照句柄,就可以对当前的标识号进行枚举,进程号通过函数Process32First()和Procee32Next()得到,这两个函数可以用于获取系统快照中第一个和下一个系统的信息,这两个函数的声明如下:
BOOL WINAPI Process32First(
HANDLE hSnapshot, // 系统快照句柄
LPPROCESSENTRY32 lppe // 指向结构体PROCESSENTRY32的指针
);
BOOL WINAPI Process32Next(
HANDLE hSnapshot, // 系统快照句柄
LPPROCESSENTRY32 lppe // 指向结构体PROCESSENTRY32的指针
);
3.实例代码
#include <tlhelp32.h>
void CTestView::OnRButtonDown(UINT nFlags, CPoint point)
{
CString StrInfo="系统当前进程包括:/n";
int nProcess =0;
HANDLE snapshot=CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0) ;
if(snapshot == NULL)return ;
SHFILEINFO shSmall;
PROCESSENTRY32 processinfo ;
processinfo.dwSize=sizeof(processinfo) ;
BOOL status=Process32First(snapshot,&processinfo) ;
while (status)
{
ZeroMemory(&shSmall, sizeof(shSmall));
SHGetFileInfo(processinfo.szExeFile,0,&shSmall,sizeof(shSmall),
SHGFI_ICON|SHGFI_SMALLICON);
StrInfo+=processinfo.szExeFile;
StrInfo+="/n";
status = Process32Next (snapshot, &processinfo) ;
nProcess++;
}
MessageBox(StrInfo,"信息提示",MB_OK);
CView::OnRButtonDown(nFlags, point);
}
4.小结
获取当前系统所有已启动的进程,通常分为两个过程:首先获取系统进程快照,然后根据快照枚举进程。在Windows操作系统下,系统已为所有保存在系统内存中的进程、线程及模块等当前状态的信息制作了一个系统快照,用户可以通过对系统快照的访问完成对进程当前状态的检测。
9.1.4 终止指定进程的技巧
1.问题阐述
终止进程也就是结束进程,让进程从内存中卸载。进程的终止的原因一般有4种。
l 主线程的入口函数返回。
l 进程中的一个线程调用ExitProcess函数。
l 次进程中的所有线程结束。
l 其他进程中又有线程都结束。
2.实现技巧
函数Process32First()和函数Process32Next()能够枚举系统中的所有进程,函数SHGetFileInfo()能够获得进程的信息,一旦得到进程的标识号,就可以对进程进行终止。由于被管理进程在当前进程之外,因此进程首先通过OpenProcess()函数来获取一个已经存在的进程对象的句柄,然后才可以通过该句柄对指定的进程进行管理和控制。OpenProcess()函数的声明如下:
HANDLE WINAPI OpenProcess(
DWORD dwDesiredAccess, //访问标志
BOOL bInheritHandle, //处理进程标志
DWORD dwProcessId //进程标志号
);
3.实例代码
#include <tlhelp32.h>
void CTestView::OnRButtonDown(UINT nFlags, CPoint point)
{
int nProcess =0;
HANDLE snapshot=CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0) ;
if(snapshot == NULL)
return ;
SHFILEINFO shSmall;
PROCESSENTRY32 processinfo ;
processinfo.dwSize=sizeof(processinfo) ;
BOOL status=Process32First(snapshot,&processinfo) ;
while (status)
{
ZeroMemory(&shSmall, sizeof(shSmall));
SHGetFileInfo(processinfo.szExeFile,0,&shSmall,sizeof(shSmall),
SHGFI_ICON|SHGFI_SMALLICON);
CString StrInfo="是否需要终止进程:";
StrInfo+=processinfo.szExeFile;
if(AfxMessageBox(StrInfo,MB_YESNO)==IDYES)
{
DWORD dwProcessID=processinfo.th32ProcessID;
HANDLE hProcess=::OpenProcess(PROCESS_TERMINATE,FALSE,
dwProcessID);
::TerminateProcess(hProcess,0);
CloseHandle(hProcess);
}
status = Process32Next (snapshot, &processinfo) ;
nProcess++;
}
CView::OnRButtonDown(nFlags, point);
}
4.小结
进程结束后,调用GetExitCodeProcess函数可以得到其退出代码,如果在调用这个函数时,目标进程还没有结束,此函数会返回STILL_ACTIVE,表示进程还在运行。
9.1.5 使用文件映射机制实现进程间通信的技巧
1.问题阐述
传统上都是用fread()、fwrite()之类的函数来存取文件,而文件映射把部分文件或全部文件映射在process的内存空间中,因此可以像存取内存一样存取文件。
2.实现技巧
下面我们介绍创建文件映射的方法。在介绍CreateFileMapping()函数之前,必须先创建CreateFile()函数和OpenFile()函数打开映射到内存空间的文件,取得文件的句柄。
CreateFileMapping()的函数声明:
HANDLE CreateFileMapping(
HANDLE hFile, //文件的句柄
LPSECURITY_ATTRIBUTES lpAttributes, //与安全有关的设置
DWORD flProtect, //用来决定与view有关的属性
DWORD dwMaximumSizeHigh, //设置映射文件的尺寸
DWORD dwMaximumSizeLow,
LPCTSTR lpName //文件映射对象的名称
);
MapViewOfFile()函数的声明:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, //文件映射的句柄
DWORD dwDesiredAccess, //此view的属性
DWORD dwFileOffsetHigh, //view文件的起点
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap //映射区的大小
);
3.实例代码
//发送数据
void CTestDlg::OnBnClickedBtnsendinfo()
{
UpdateData(TRUE);
//创建文件映像对象
HANDLE hMapping;
LPSTR StrData;
hMapping=CreateFileMapping((HANDLE)0xFFFFFFFF,NULL,PAGE_READWRITE,0,
0x100,"COMMUNICATION");
if(hMapping==NULL)
{
MessageBox("创建文件映像对象","信息提示",MB_OK);
return;
}
//将文件映射到一个进程的地址空间上
StrData=(LPSTR)MapViewOfFile(hMapping,FILE_MAP_ALL_ACCESS,0,0,0);
if(StrData==NULL)
{
AfxMessageBox("MapViewOfFile() failed.");
MessageBox("文件映射失败","信息提示",MB_OK);
return;
}
//向映射内存写数据
sprintf(StrData,m_StrSendData);
//释放映像内存
UnmapViewOfFile(StrData);
}
//接收数据
void CTestDlg::OnBnClickedBtnreceiveinfo()
{
//创建文件映像对象
HANDLE hMapping;
LPSTR StrData;
hMapping=CreateFileMapping((HANDLE)0xFFFFFFFF,NULL,PAGE_READWRITE,0,
0x100,"COMMUNICATION");
if(hMapping==NULL)
{
MessageBox("创建文件映像对象","信息提示",MB_OK);
return;
}
//将文件映射到一个进程的地址空间上
StrData=(LPSTR)MapViewOfFile(hMapping,FILE_MAP_ALL_ACCESS,0,0,0);
if(StrData==NULL)
{
AfxMessageBox("MapViewOfFile() failed.");
MessageBox("文件映射失败","信息提示",MB_OK);
return;
}
//获取映像内存的数据量
m_StrReceiveData.Format("%s",StrData);
//释放映像内存
UnmapViewOfFile(StrData);
UpdateData(FALSE);
}
4.小结
由于各个process之间是独立的,因此彼此之间无法存取对方的内存空间,这虽然是安全的保护机制,但是如果要两个process之间进行数据交换,那就又有问题了。不过win3又另外提供了一系列的进程通信的机制,帮助process与另外一个process交换数据。
9.1.6 使用消息实现进程间通信
1.问题阐述
消息是Windows提供的一种驱动机制,在前面的章节中,已经多次使用消息解决问题了。使用消息进行进程通信的过程,就是使用消息激活某种操作的过程。对于进程间的通信,一般采用用户自定义的消息来完成进程间的通信,当然如果要实现的是Windows定义的消息功能,则完全可以使用已定义消息。例如完全可以在一个进程中向另一个进程中的EDIT发送WM_COPY消息,那么,如何用消息来完成进程间的通信呢?
2.实现技巧
在进程间进行消息通信,那么进程之间首先应该约定唯一的确定的消息标识,这个消息标识必须是唯一的。定义了消息标识后,消息就可以通过就PostMessage,SendMessage或者PostThreadMessage函数给接收方进程的窗口发送消息。那么进程间通信还存在另外一个问题,就是消息发送给哪一个窗口,消息的发送方必须知道接收方的一个标识,比如窗口的句柄。所以在通信前,两个通信的进程之间要进行协商,确定消息的接收方的窗口标识。消息发送方可以通过FindWindow()/FindWindowEx()函数根据窗口的标题或者接收窗体的类名搜索窗口。所以在进程通信之间,进程双方将约定好窗口类名或者窗口的标题。前者只搜索顶层窗口,不搜索子窗口;而后者可以搜索子窗口,搜索的过程不区分大小写。可以用FindWindow搜索指定的窗口,然后使用FindWindowEx来搜索它的子窗口。MFC封装了FindWindow函数,没有对FindWindowEx函数进行封装。FindWindow和FindwindowEx的原型如下:
HWND FindWindow(
LPCTSTR lpClassName, //窗口类名
LPCTSTR lpWindowName //窗口标题
)
HWND FindwWindowEx(
HWND hwndParent, //父窗口句柄
HWND hwndChildAfter, //开始搜索的子窗口句柄
LPCTSTR lpszClass, //窗口类名
LPCTSTR lpszWindow //窗口标题
)
3.实例代码
本节编写了两个程序:传输数据(Send.exe)和接收数据(Recv.exe)。本例主要由Send.exe发送3个指令,Recv.exe接收这3个指令后,分别对这3个指令进行响应,根据指令改变窗口背景颜色。
发送端程序设计,用MFC的AppWizard(exe)创建新项目Send,设置“Project name”为“Send”,单击【确定】按钮后进入创建应用程序类型,选择“Dialog Based”类型并单击【Finish】按钮。在对话框上增加3个按钮控件,在SendDlg.h中增加3个消息标识,对控件按钮的BN_CLICKED消息进行响应,其主要代码参考如下:
void CSendDlg::OnRedBtn()
{
CString strRecvWndName = "Receiver";
CWnd* pWnd = CWnd::FindWindow(NULL,strRecvWndName);
if(pWnd)
pWnd->PostMessage(WM_RED_CONN,0,0);
}
void CSendDlg::OnGreenBtn()
{
CString strRecvWndName = "Receiver";
CWnd* pWnd = CWnd::FindWindow(NULL,strRecvWndName);
if(pWnd)
pWnd->PostMessage(WM_GREEN_CONN,0,0);
}
void CSendDlg::OnBlueBtn()
{
CString strRecvWndName = "Receiver";
CWnd* pWnd = CWnd::FindWindow(NULL,strRecvWndName);
if(pWnd)
pWnd->PostMessage(WM_BLUE_CONN,0,0);
}
接收端程序设计,用MFC的AppWizard(exe)创建新项目Receiver,设置“Project name”为“Receiver”,单击【确定】按钮后进入创建应用程序类型,选择“Dialog Based”类型并单击【Finish】按钮。在Receiver.h中增加3个消息标识,增加3个自定义消息响应函数,代码参考如下:
/************************************************************************/
/*
/************************************************************************/
// ReceiverDlg.cpp 的实现:
void CReceiverDlg::OnRed(WPARAM wParam,LPARAM lParam)
{
m_pbbrush=CreateSolidBrush(RGB(255,0,0));//设置皮肤颜色
Invalidate();
}
/************************************************************************/
/*
/************************************************************************/
void CReceiverDlg::OnGreen(WPARAM wParam,LPARAM lParam)
{
m_pbbrush=CreateSolidBrush(RGB(0,255,0));//设置皮肤颜色
Invalidate();
}
/************************************************************************/
/*
/************************************************************************/
void CReceiverDlg::OnBlue(WPARAM wParam,LPARAM lParam)
{
m_pbbrush=CreateSolidBrush(RGB(0,0,255));//设置皮肤颜色
Invalidate();
}
/************************************************************************/
/*
/************************************************************************/
HBRUSH CReceiverDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
switch(nCtlColor)
{
case CTLCOLOR_LISTBOX:
case CTLCOLOR_STATIC:
case CTLCOLOR_DLG:
case CTLCOLOR_MSGBOX :
pDC->SetBkMode(TRANSPARENT);
pDC->SetTextColor(RGB(0,0,0));
return m_pbbrush;
break;
default:
return CReceiverDlg::OnCtlColor(pDC, pWnd, nCtlColor);
break;
}
return hbr;
}
上面代码中前3个函数为消息自定义的消息响应函数,它们接收到发送者的消息后,对背景画刷进行初始化。这个背景画刷由消息响应函数OnCtrlColor()返回,以达到更改背景的作用,在每个消息响应函数中调用Invalidate()函数,刷新接收方的背景,以使更改的背景及时显现。
4.小结
通过上面的实例可以看出利用消息进行进程间通信不失为一种便捷的方法,进程间的数据交换量不大却能完成一定的功能,要对上下层程序制定好规范、详尽的协议,便可编制出协调性很好的进程间的通信软件。
9.1.7 使用共享数据段实现进程间的通信的技巧
1.问题阐述
进程间的数据交换和共享是一种非常重要和实用的技术。大、中型软件的开发设计多是由众多程序设计人员合作完成的,通常一个程序设计人员只负责其中一个或几个模块的开发,这些模块可以是动态链接库也可以是应用程序或其他形式的程序组件。这些独立开发出来的程序模块最终需要作为一个整体来运行,即组成一个系统,在系统运行期间这些模块往往需要频繁地进行数据交换和数据共享,对于动态链接库同其主应用程序之间的数据交换是非常容易实现的,但是在两个应用程序之间或动态链接库同其主调应用程序之外的其他应用程序进行数据交换就比较困难了。尤其是在交换数据量过大、交换过于频繁的情况下更是难以实现,本文即对此展开讨论,并提出了一种通过共享内存来实现进程间大数据量快速交换的一种方法。
2.实现技巧
在Windows操作系统下,任何一个进程不允许读取、写入或修改另一个进程的数据(包括变量、对象和内存分配等),但是在某个进程内创建的文件映射对象的视图却能够为多个其他进程所映射,这些进程共享的是物理存储器的同一个页面。因此,当一个进程将数据写入此共享文件映射对象的视图时,其他进程可以立即获取数据变更情况。为了进一步提高数据交换的速度,还可以采用由系统页文件支持的内存映射文件而直接在内存区域使用,显然这种共享内存的方式是完全可以满足在进程间进行大数据量数据快速传输任务要求的。下面给出在两个相互独立的进程间通过文件映射对象来分配和访问同一个共享内存块的应用实例。在本例中,由发送方程序负责向接收方程序发送数据,文件映射对象由发送方创建和关闭,并且指定一个唯一的名字供接收程序使用。接收方程序直接通过这个唯一指定的名字打开此文件映射对象,并完成对数据的接收。
在发送方程序中,首先通过CreateFileMapping()函数创建一个内存映射文件对象,如果创建成功则通过MapViewOfFile()函数将此文件映射对象的视图映射进地址空间,同时得到此映射视图的首地址。可见,共享内存的创建主要是通过这两个函数完成的,这两个函数原型声明如下:
HANDLE CreateFileMapping(HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName);
LPVOID MapViewOfFile(HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap);
CreateFileMapping()函数的参数hFile指定了待映射到进程地址空间的文件句柄,如果为无效句柄则系统会创建一个使用来自页文件而非指定磁盘文件存储器的文件映射对象。很显然,在本例中为了使数据能快速交换,需要人为将此参数设定为INVALID_HANDLE_VALUE;参数flProtect设定了系统对页面采取的保护属性,由于需要进行读写操作,因此可以设置保护属性PAGE_READWRITE;双字型参数dwMaximumSizeHigh和dwMaximumSizeLow指定了所开辟共享内存区的最大字节数;最后的参数lpName用来给此共享内存设定一个名字,接收程序可以通过这个名字将其打开。MapViewOfFile()函数的参数hFileMappingObject为CreateFileMapping()返回的内存文件映像对象句柄;参数dwDesiredAccess再次指定对其数据的访问方式,而且需要同CreateFileMapping()函数所设置的保护属性相匹配。这里对保护属性的重复设置可以确保应用程序能更多地对数据的保护属性进行有效控制。
3.实例代码
下面给出创建共享内存的部分关键代码:
hRecvMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE |
SEC_COMMIT, 0, 1000000, "DataMap");
if (hRecvMap != NULL)
{
lpData = (LPBYTE)MapViewOfFile(hRecvMap, FILE_MAP_WRITE, 0, 0, 0);
if (lpData == NULL)
{
CloseHandle(hRecvMap);
hRecvMap = NULL;
}
}
// 通知接收程序内存文件映射对象的视图已经打开
HWND hRecv = ::FindWindow(NULL, DECODE_PROGRAMM);
if (hRecv != NULL)
::PostMessage(hRecv, WM_MAP_OPEN, 0, 0);
数据的传送实际是将数据从发送方写到共享内存中,然后由接收程序及时从中取走即可。数据从发送方程序写到共享内存比较简单,只需用memcpy()函数将数据复制过去,关键在于能及时通知接收程序数据已写入到共享内存,并让其立即取走。在这里仍采取消息通知的方式,当数据写入共享内存后通过PostMessage()函数向接收方程序发送消息,接收方在消息响应函数中完成对数据的读取:
// 数据复制到共享内存
memcpy(lpData, RecvBuf, sizeof(RecvBuf));
// 通知接收方接收数据
HWND hDeCode = ::FindWindow(NULL, DECODE_PROGRAMM);
if (hDeCode != NULL)
::PostMessage(hDeCode, WM_DATA_READY, (WPARAM)0, (LPARAM)sizeof(RecvBuf));
当数据传输结束,即将退出程序时,需要将映射进来的内存文件映射对象视图卸载和资源释放等。这部分工作主要由UnmapViewOfFile()和CloseHandle()等函数完成:
HWND hDeCode = ::FindWindow(NULL, DECODE_PROGRAMM);
if (hDeCode != NULL)
::PostMessage(hDeCode, WM_MAP_CLOSE, 0, 0);
if (lpData != NULL)
{
UnmapViewOfFile(lpData);
lpData = NULL;
}
if (hRecvMap != NULL)
{
CloseHandle(hRecvMap);
hRecvMap = NULL;
}
在接收程序中,在收到由发送方发出的WM_MAP_OPEN消息后,由OpenFileMapping()函数打开由名字“DataMap”指定的文件映射对象,如果执行成功,继续用MapViewOfFile()函数将此文件映射对象的视图映射到接收应用程序的地址空间并得到其首地址:
m_hReceiveMap = OpenFileMapping(FILE_MAP_READ, FALSE, "DataMap");
if (m_hReceiveMap == NULL)
return;
m_lpbReceiveBuf = (LPBYTE)MapViewOfFile(m_hReceiveMap,FILE_MAP_READ,0,0,0);
if (m_lpbReceiveBuf == NULL)
当发送方程序将数据写入到共享内存后,接收方将收到消息WM_DATA_READY,在响应函数中将数据从共享内存复制到本地缓存中,再进行后续的处理。同发送程序类似,在接收程序接收数据完毕后,也需要用UnmapViewOfFile()、CloseHandle()等函数完成对文件视图等打开过的资源进行释放。
memcpy(RecvBuf, (char*)(m_lpbReceiveBuf), (int)lParam);
// 程序退出前资源的释放
UnmapViewOfFile(m_lpbReceiveBuf);
m_lpbReceiveBuf = NULL;
CloseHandle(m_hReceiveMap);
m_hReceiveMap = NULL;
4.小结
经实际测试,使用共享内存在处理大数据量数据的快速交换时表现出了良好的性能,在数据可靠性等方面要远远高于发送WM_COPYDATA消息的方式。这种大容量、高速的数据共享处理方式在设计高速通信类软件中有着很好的使用效果。
9.1.8 用命名管道实现进程间的通信的技巧
1.问题阐述
命名管道是通过网络来完成进程间的通信的,它屏蔽了底层的网络协议细节。所以在不了解网络协议的情况下,也可以利用命名管道来实现进程间的通信。命名管道充分利用了Windows NT和Windows 2000内建的安全机制。命名管道是围绕Windows文件系统设计的一种机制,采用“命名管道文件系统(Named Pipe File System,NPFS)”接口。将命名管道作为一种网络编程方案时,它实际上建立了一个客户机/服务器通信体系,并在其中可靠地传输数据。创建管道的进程称为管道服务器,连接到一个管道的进程称为管道客户机。管道服务器和一台或多台管道客户机进行单向或双向的通信。一个命名管道的所有实例共享同一个管道名,但是每一个实例均拥有独立的缓存与句柄,并且为客户——服务通信提供一个分离的管道。实例的使用保证了多个管道客户能够在同一时间使用同一个命名管道。
2.实现技巧
命名管道提供了两种基本通信模式:字节模式和消息模式。在字节模式中,数据以一个连续的字节流的形式,在客户机和服务器之间流动。而在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发,每次在管道上发出了一条消息后,它必须作为一条完整的消息读入。
由于命名管道采用“命名管道文件系统(Named Pipe File System,NPFS)”接口,因此,客户机和服务器可利用标准的Win32文件系统函数(例如ReadFile和WriteFile)来进行数据的收发,创建命名管道CreateNamedPipe的原型如下:
HANDLE CreateNamedPipe(
LPCTSTR lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstances,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTimeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
其中,lpName为管道的名字,格式为//./pipe/pipename,名字的最大长度为256,管道名字对字符的大小写不敏感。
dwPipeMode是管道的打开模式,等待客户端连接函数ConnectNamedPipe的原型如下:
BOOL ConnectNamedPipe(
HANDLE hNamedPipe,
LPOVERLAPPED lpOverlapped
);
等待命名管道函数WaitNamedPipe的原型如下:
BOOL WaitNamedPipe(
LPCTSTR lpNamedPipeName,
DWORD nTimeOut
);
读取数据和写入数据的函数原型分别如下。
1)读取数据
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
2)写入数据
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
3.实例代码
下面给出创建共享命名管道的部分关键代码。
1)服务器端
void CNamedPipeSrvView::OnPipeCreate()
{
// TODO: 在此添加代码
hPipe=CreateNamedPipe(".//pipe//MyPipe",
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
0,1,1024,1024,0,NULL);
if(INVALID_HANDLE_VALUE==hPipe)
{
MessageBox("创建命名管道失败!");
hPipe=NULL;
return;
}
HANDLE hEvent;
hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
if(!hEvent)
{
MessageBox("创建事件对象失败!");
CloseHandle(hPipe);
hPipe=NULL;
return;
}
OVERLAPPED ovlap;
ZeroMemory(&ovlap,sizeof(OVERLAPPED));
ovlap.hEvent=hEvent;
if(!ConnectNamedPipe(hPipe,&ovlap))
{
if(ERROR_IO_PENDING!=GetLastError())
{
MessageBox("等待客户端连接失败!");
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe=NULL;
return;
}
}
if(WAIT_FAILED==WaitForSingleObject(hEvent,INFINITE))
{
MessageBox("等待对象失败!");
CloseHandle(hPipe);
CloseHandle(hEvent);
hPipe=NULL;
return;
}
CloseHandle(hEvent);
}
void CNamedPipeSrvView::OnPipeRead()
{
// TODO: 在此添加相关代码
char buf[100];
DWORD dwRead;
if(!ReadFile(hPipe,buf,100,&dwRead,NULL))
{
MessageBox("读取数据失败!");
return;
}
MessageBox(buf);
}
void CNamedPipeSrvView::OnPipeWrite()
{
// TODO: 在此添加相关代码
char buf[]="http://www.163.com";
DWORD dwWrite;
if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("写入数据失败!");
return;
}
}
2)客户端
void CNamedPipeCltView::OnPipeConnect()
{
// TODO: 在此添加相关代码
if(!WaitNamedPipe(".//pipe//MyPipe",NMPWAIT_WAIT_FOREVER))
{
MessageBox("当前没有可利用的命名管道实例!");
return;
}
hPipe=CreateFile(".//pipe//MyPipe",GENERIC_READ | GENERIC_WRITE,
0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(INVALID_HANDLE_VALUE==hPipe)
{
MessageBox("打开命名管道失败!");
hPipe=NULL;
return;
}
}
void CNamedPipeCltView::OnPipeRead()
{
// TODO: 在此添加相关代码
char buf[100];
DWORD dwRead;
if(!ReadFile(hPipe,buf,100,&dwRead,NULL))
{
MessageBox("读取数据失败!");
return;
}
MessageBox(buf);
}
void CNamedPipeCltView::OnPipeWrite()
{
// TODO: 在此添加相关代码
char buf[]="命名管道测试程序";
DWORD dwWrite;
if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("写入数据失败!");
return;
}
}
4.小结
命名管道服务器和客户机的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接收管道客户机的连接请求,而客户机只能同一个现成的命名管道服务器建立连接。命名管道服务器只能在Windows NT或Windows 2000上创建,所以,我们无法在两台Windows 95或Windows 98计算机之间利用管道进行通信。不过,客户机可以是Windows 95或Windows 98计算机,与Windows NT或Windows 2000计算机进行连接通信。
9.1.9 使用邮槽实现进程间通信的技巧
1.问题阐述
邮槽是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。
2.实现技巧
邮槽可以实现进程间通信,同样我们也可利用标准的Win32文件系统函数(例如:ReadFile和WriteFile)来进行数据的收发,创建邮槽的函数原型如下:
HANDLE CreateMailslot(
LPCTSTR lpName,
DWORD nMaxMessageSize,
DWORD lReadTimeout,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
3.实例代码
服务器端的代码参考如下:
void CMailslotSrvView::OnMailslotRecv()
{
// TODO: 在此添加相关代码
HANDLE hMailslot;
hMailslot=CreateMailslot(".//mailslot//MyMailslot",0,
MAILSLOT_WAIT_FOREVER,NULL);
if(INVALID_HANDLE_VALUE==hMailslot)
{
MessageBox("创建邮槽失败!");
return;
}
char buf[100];
DWORD dwRead;
if(!ReadFile(hMailslot,buf,100,&dwRead,NULL))
{
MessageBox("读取数据失败!");
CloseHandle(hMailslot);
return;
}
MessageBox(buf);
CloseHandle(hMailslot);
}
客户端的代码参考如下:
void CMailslotCltView::OnMailslotSend()
{
// TODO: 在此添加相关代码
HANDLE hMailslot;
hMailslot=CreateFile(".//mailslot//MyMailslot",GENERIC_WRITE,
FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(INVALID_HANDLE_VALUE==hMailslot)
{
MessageBox("打开邮槽失败!");
return;
}
char buf[]="http://www.sunxin.org";
DWORD dwWrite;
if(!WriteFile(hMailslot,buf,strlen(buf)+1,&dwWrite,NULL))
{
MessageBox("写入数据失败!");
CloseHandle(hMailslot);
return;
}
CloseHandle(hMailslot);
}
3.小结
为保证邮槽在各种Windows平台下都能够正常工作,我们传输消息的时候,应将消息的长度限制在424字节以下。