1. 进程通讯 WM_COPYDATA
如果你尝试在进程A的线程中把一个 LPARAM (内含一个指针)交给进程B,进程B有可能在使用这一指针时当掉。问题出在这个指针所指的数据乃位于进程A的地址空间中。进程B不可能看到这个地址空间,所以这个指针会被以进程B的地址空间解释之。那当然是牛头不对马嘴了。
为解决这个问题, Windows 定义了一个消息,名为 WM_COPYDATA ,专门用来在线程之间搬移数据--不管两个线程是否同属一个进程。和其他所有的消息一样,你必须指定一个窗口,也就是一个 HWND ,当做消息的目的地。 所以欲接收此消息的线程必须有一个窗口(也就是它必须是个 UI 线程)。
WM_COPYDATA 消息的使用方式如下:
SendMessage(hwndReceiver,
WM_COPYDATA,
(WPARAM)hwndSender,
(LPARAM)&cds);
LPARAM 参数 cds 必须指向一个特定的 Window s 数据结构:
typedef struct tagCOPYDATASTRUCT { // cds
DWORD dwData; // 用户自定义值, 它通常指示 lpData 中的内容的用途
DWORD cbData; // lpData 所指之数据大小(bytes)
PVOID lpData; // 一块数据, 可以被传送到接收端
} COPYDATASTRUCT, *PCOPYDATASTRUCT;
必须使用SendMessage传送WM_COPYDATA, 不能够使用PostMessage或任何其他变种函数如PostThreadMessage之流. 这是因为系统必须管理用以传递数据的缓冲区的生命期。如果你使用PostMessage, 数据缓冲区会在接收端(线程)有机会处理该数据之前, 被系统清除并摧毁。lpData 所指的空间可以在 heap 之中, 也可以在 stack 之中。如果接收端(线程)需要改变数据内容, 或是需要储存一份比较久远的数据副本, 接收端应该自行拷贝一份。
// COPYRECV--WM_COPYDATA
LONG CMainFrame::OnCopyData( UINT wParam, LONG lParam)
{
// HWND hwnd = (HWND)wParam; // handle of sending window
PCOPYDATASTRUCT pcds = (PCOPYDATASTRUCT) lParam; // 取得数据指针
// The view is inside the mainframe,
// and the edit control is in the view
CEditView* pView = (CEditView*)GetActiveView();
ASSERT_VALID(pView);
// Find the edit control
CEdit& ctlEdit = pView->GetEditCtrl();
switch (pcds->dwData) // 自定义数据
{
case ACTION_DISPLAY_TEXT:
{
LPCSTR szNewString = (LPCSTR)(pcds->lpData);
// Setting a CString equal to a LPCSTR makes a copy of the string.
CString strTextToDisplay = szNewString;
// Throw away any \r\n that may already be there
strTextToDisplay.TrimRight();
// Now add our own
strTextToDisplay += "\r\n";
// Set the cursor back at the end of the text
int nEditLen = ctlEdit.GetWindowTextLength();
ctlEdit.SetSel(nEditLen, nEditLen);
// Append the text
ctlEdit.ReplaceSel(strTextToDisplay);
ctlEdit.ShowCaret();
break;
}
case ACTION_CLEAR_WINDOW:
ctlEdit.SetWindowText("");
break;
default:
break;
}
return 1;
}
发送端往往通过窗口标题查找目标窗口,但事实上MFC程序的窗口标题往往会被系统自动补加上 document 名称. 为了消除额外的窗口标题文字,可以做这样的修改:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
cs.style &= ~(FWS_ADDTOTITLE); // Do not put in a document name
return CFrameWnd::PreCreateWindow(cs);
}
COPYSEND--建造并送出 WM_COPYDATA
void CCopySendDlg::OnOk()
{
CEdit *pEdit = (CEdit*)GetDlgItem(IDC_EDIT_SENDTEXT);
ASSERT_VALID(pEdit);
// Get the text from the edit control
CString strDisplayText;
pEdit->GetWindowText(strDisplayText);
COPYDATASTRUCT cds;
memset(&cds, 0, sizeof(cds));
cds.dwData = ACTION_DISPLAY_TEXT;
cds.cbData = strDisplayText.GetLength() + 1; // +1 for the NULL
cds.lpData = (LPVOID)(LPCTSTR) strDisplayText;
SendToServer(cds);
}
void CCopySendDlg::OnClear()
{
COPYDATASTRUCT cds;
memset(&cds, 0, sizeof(cds));
cds.dwData = ACTION_CLEAR_WINDOW;
SendToServer(cds);
}
void CCopySendDlg::SendToServer(const COPYDATASTRUCT& cds)
{
CWnd *pDisplayWnd = CWnd::FindWindow(NULL, szDisplayAppName); // 找到目标窗口
if (pDisplayWnd)
{
pDisplayWnd->SendMessage(WM_COPYDATA,
(WPARAM)GetSafeHwnd(), (LPARAM)&cds);
}
else
AfxMessageBox(IDS_ERR_NOSERVER);
}
void CCopySendDlg::OnExit()
{
EndDialog(IDOK);
}
WM_COPYDATA 的优点:
它是唯一一个可以在 16 位程序和 32 位程序之间搬移数据的方法。
WM_COPYDATA 的缺点:
1. 内部机制的爆发力不够, 效率不高
2. 只能用于SendMessage, 接收端必须有一个消息队列,以及一个相关的窗口, SendMessage 是一个同步函数调用, 强迫传送端和接受端有同步效果
2. 进程通讯 共享内存
Win32 的一个基础观念就是,进程之间要有严密的保护。每一个进程认为它拥有整部机器。从一个进程中要看到另一个进程的地址空间的任何一部分,都是不可能的。这样的分离策略是如此完整,以至于每一个进程似乎生活在完全相同的地址范围内。一个程序所存在的实际内存的地址,对另一个程序而言,是朦胧而不可见的。程序能够看到的只是所谓的逻辑地址。事实上,程序所存在的内存的实际地址可能会不断地改变,因为虚拟内存管理器会自动搬移程序的一部分,进出虚拟储存空间(硬盘)中。
所谓共享内存,是一块在设计时即打算给一个以上的进程在同一时间都看得到的内存区域。
一、设定一块共享内存区域(Shared Memory Area)
设定一块共享内存区域,需要两个步骤:
1. 产生一个所谓的 file-mapping 核心对象, 并指定共享区域的大小;
2. 将共享区域映射到你的进程的地址空间中;
第一个步骤使用 CreateFileMapping 函数。一般而言,这个函数允许你存取一个文件,就好像它是内存中的数据一样,但是我们要使用的是另一个特殊的模式,它会在页面文件(paging file )中产生一块空间,使任何一个进程都可以根据其名称而存取到它。
HANDLE CreateFileMapping(
HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 安全属性
DWORD flProtect, // 文件的保护属性
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName
);
hFile
这个参数正常而言应该是 CreateFile 传回来的一个有关于文件的 handle, 用以告诉系统将它映射到内存中. 然而如果指定此参数为(HANDLE)0xFFFFFFFF,我们就可以使用页面文件(paging file)中的一块空间, 取代一般的文件。
flProtect
文件的保护属性
可以是PAGE_READONLY 或PAGE_READWRITE 或 PAGE_WRITECOPY。针对跨进程的共享内存, 你应该指定此参数为PAGE_READWRITE
dwMaximumSizeHigh
映射之文件大小的高 32 位。如果使用页面文件(paging file ),此参数将总是为 0,因为页面文件没有大到足够容纳 4G B 的共享内存空间。
dwMaximumSizeLow
映射区域的低 32 位。对于共享内存而言, 此值应该就是你要共享的内存大小。
lpName
共享内存区域的名称。任何进程或线程都可以根据这个名称, 引用到这个 file-mapping 对象。如果你要产生共享内存, 此参数不应该是一般情况下所使用的 NULL 。
返回值
如果成功,则CreateFileMapping传回一个 handle否则传回 NULL. GetLastError可以获得失败的合理解释。如果参数中所指定的文件已经存在, CreateFileMapping便会失败, 这时候GetLastError会传回ERROR_ALREADY_EXISTS。
现在我们有了一个核心对象,但是我们还没有获得一个指针指向可用的内存。为了从共享内存中获得一个指针,我们必须使用 MapViewOfFile。
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap
);
hFileMappingObject
file-mapping 核心对象的handle, 这是CreateFileMapping 或 OpenFileMapping的传回物
dwDesiredAccess
对共享内存而言, 此值应该设为FILE_MAP_ALL_ACCESS。其他目的则使用其他设定。
dwFileOffsetHigh
映射文件的高 32 位偏移值。如果使用页面文件(paging file ),该参数应该总是为 0,因为页面文件不可能大到足够容纳 4GB 共享内存区域。
dwFileOffsetLow
映射文件的低 32 位偏移值。对于共享内存而言,该参数应该总是 0 以便能够映射整个共享区域。
dwNumberOfBytesToMap
真正要被映射的字节数量。如果指定为 0, 表示要映射整个空间。所以, 对共享内存而言, 最简单的做法就是将此参数指定为 0。
返回值
如果成功, 则传回一个指针, 指向被映射出来的"视图"(mapped view) 的起头。如果失败, 传回 NULL, 你可以利用 GetLastError 找出原因。
下面是一小段程序代码,用以示范如何产生一个共享内存。我们必须储存线程的 ID 而不是其 handle, 因为 handle 只在其自己的地址空间中有意义。为了让程序代码简洁, 我没有做任何错误检验。
HANDLE hFileMapping; // 核心对象
LPDWORD pCounter; // 指向共享内存
hFileMapping = CreateFileMapping( // 产生共享内存核心对象
(HANDLE)0xFFFFFFFF, // File handle
NULL, // Security attributes
PAGE_READWRITE, // Protection
0, // Size - high 32 bits
sizeof(DWORD), // Size - low 32 bits 一个DWORD大小
"Server Thread ID"); // Name
pCounter = (LPDWORD) MapViewOfFile( // 取得核心对象的内存指针
hFileMapping, // File mapping object
FILE_MAP_ALL_ACCESS, // Read/Write
0, // Offset - high 32 bits
0, // Offset - low 32 bits
0); // Map the whole thing
*pCounter = GetCurrentThreadId(); // 存储一个DWORD线程ID
内存映射文件在内存中产生出一块新的区域,数据可以被放在上面。这有点像GlobalAlloc。它并不会要求系统将一个现有的内存区域变成"可共享"。
二、找出共享内存
当你决定要如何使用这块共享内存时,你必须决定这块内存是要以点对点(peer to peer)的形式呈现,还是希望被一个 server 进程产生,然后被数个 client 进程打开取用。
点对点的形式: 每一个进程都必须有相同的能力,产生共享内存并将它初始化。每一个进程都应该调用 CreateFileMapping,然后调用 GetLastError。如果传回的错误代码是 ERROR_ALREADY_EXISTS, 那么进程就可以假设这一共享内存区域已经被其他进程打开并初始化过了。否则进程就可以合理地认为自己排第一位, 并接下来将共享内存初始化。
client/server : 只有 server 进程才应该产生并初始化共享内存. 所有的 client 进程都应该使用OpenFileMapping。该函数会传回一个 handle, 代表一个 file-mapping 核心对象, 那是稍早被 server 进程以 CreateFileMapping 产生出来的.
HANDLE OpenFileMapping(
DWORD dwDesiredAccess, // 对于共享内存 FILE_MAP_ALL_ACCESS
BOOL bInheritHandle, // 如果是 TRUE, 表示这个 handle 可以被子进程继承
LPCTSTR lpName // 共享内存的名称
);
返回值
如果成功, 则OpenFileMapping 传回一个 handle. 如果失败, 则传回 NULL, 这时你可以利用 GetLastError 获得更多的细节信息。
在调用 OpenFileMapping 之后, 进程应该调用 MapViewOfFile 以获得一个指针, 指向共享内存。第二个进程以及后续各进程调用 OpenFileMapping 之后所获得的地址, 并不保证和第一个进程所获得的地址相同。事实上, 对于不同的进程, 共享内存可以被映射到不同的地址。
三、清理(Cleaning up)
一旦你完成了对共享内存的操作, 你应该调用 Un mapViewOfFile, 交出原本由 MapViewOfFile所获得的指针,然后再调用 CloseH andle, 交出file-mapping核心对象的 handle。
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress // 指向共享内存, 这个值必须符合MapViewOfFile 的传回值
);
如果成功,则 UnmapViewOfFile() 传回 TRUE 。如果失败,则传回FALSE,这时你可以利用 GetLastError() 获得更多的细节信息。
四、同步处理
在创建共享内存以及初始化的过程中,可能会发生线程切换,导致其他进程打开共享内存时,它尚未初始化。最好的方法就是使用Mutex同步处理。
五、共享内存的使用摘要
-> 不要把 C++ collection classes 放到共享内存中。
-> 不要把拥有虚函数之 C++ 类放到共享内存中。
-> 不要把 CObject 派生类之 MFC 对象放到共享内存中。
-> 不要使用"point within the shared memory"的指针。
-> 不要使用"point outside of the shared memory"的指针。
-> 使用"based"指针是安全的,但是要小心使用。