现在的位置: 首页 > 综合 > 正文

WINDOWS下进程间的通讯方式

2013年04月21日 ⁄ 综合 ⁄ 共 9759字 ⁄ 字号 评论关闭

在Windows下各个任务是以不同的进程来完成的,当一个进程启动后,操作系统为其分配了4GB的私有地址空间,由于位于同一个进程中的线程共享同一个 地址空间,所以线程间的通信很简单,就像两个人如果在同一个房间里说话的话就比较容易,只要动动嘴皮子就OK了, 但是如果在两个国家里就比较麻烦,必须借助于一些其他的手段,比如打电话等. 以下介绍四种进程通信方式,虽然是在windows下的环境但是在其他的操作系统里也遵循着同样的原理,不信的话可以把大学里的操作系统教材拿出来看看, 它们分别是剪贴板、 匿名管道、命名管道和邮槽。

1. 剪贴板(clipboard)
其实这个东西我们每天操作电脑的时候都在接触,我们经常实用ctrl+c和ctrl+v就是基于了剪贴板的方式来实现了两个进程间的通信, 就拿我现在来说吧,我在写这篇文章的时候是在notepad下写的, 一会我要把这篇文章里的所有文字都粘贴到csdn的网页上, 这里就是两个进程,一个是notepad进程和一个IE进程进行通信, 它们要传输的数据格式是TEXT,当然你也可以把这些内容拷贝到word、Excel、PowerPoint甚至是另一个notepad上面(你要清楚再 启动一个notepad,这个跟前一个notepad是两个进程,虽然它们长得很像),这就说明剪贴板是所有程序都可以访问的,如果你对多线程编程比较了
解的话, 你就会明白一个数据一旦要被很多线程访问,如果这些线程中有一些需要求改这个数据,就要对这个数据加锁来保证数据的正确性了,剪贴板也是一样的,当我把这 段文字ctrl+c时,它就要先对系统中的剪贴板加锁,然后把内容放进去,再释放锁,如果你明白了以上的一些道理,那么请你继续往下看,如果还没太明白那 也请你继续往下看, 也许你对文字的理解能力已经落后于对代码的理解了.
BOOL OpenClipboard()
windows提供的一个API函数,作用是打开剪贴板,如果程序打开了剪贴板,则其他程序经不能修改剪贴板(道理上面讲了),直到 CloseClipboard(), 在windows中所有带有Open这个单词的函数都会有一个与之对应的带有Close这个单词的函数, 而且你在open之后一定不要忘记close,你可以自己试试看,只调用OpenClipboard()而不去执行CloseClipboard()会有 什么效果,至今我还没有发现例外的情况,如果你发现了请你告诉我.
HANDLE SetClipboardData(UINT uFormat, HANDLE hMem)
它的作用是将hMem所“代表”的内存中的内容以uFormat的格式放到剪贴板上,详细的参数说明去查MSDN吧,这里你可能有一些疑问,hMem是个 句柄而内存是用指针来访问的,你说的没错,所以我用了“代表”这个词而没有用“指向”,在windows里很多资源都会有一个HANDLE以它来标识各个 资源一遍于操作系统的管理,内存也一样,我们一般动态开辟(用new, malloc)的heap都不会被操作系统任意移动,因为它是一个进程的私有空间,而如果你开辟全局Heap数据的话,操作系统很可能会移动它,如果这个 时候你已然使用指针的话,那么操作系统一旦移动了一块全局Heap数据就要修改到所有指向这块内存的指针,这显然不现实,而这个时候如果你已然使用你的指
针来管理那块内存的话,那就出了大麻烦,因为那块内存已经被移走了,而如果使用句柄来标识这块内存的话则会解决这个问题,因为它只是一个标签,并没有实际 的物理意义,就像如果你使用一个人的家庭住址来标识这个人的话就会有麻烦,因为一旦他搬走了,你就找错人了, 但是以身份证号就OK了, 详细的情况可以参考GlobalAlloc这个函数。
BOOL IsClipboardFormatAvailable(UINT uFormat)
这个函数的作用就是要检查一下剪贴板中的数据是否是uFormat形式的,比如我现打开了mspaint(画图板)程序画了几笔,然后Ctrl+C,再打 开notepad程序Ctrl+V,你当然知道这不会成功,它就是使用了这个API函数在粘贴前判断了一下剪贴板中的数据类型是否是我所需要的.

好了我们下面来写两个进程来实现它们的通信, 事先说明我写的只是关键代码并不能直接运行
发送方:

  1. void Send(char* pSnd) 
  2. if (OpenClipboard()) 
  3. HANDLE hClip; 
  4. char *pBuf = NULL; // 对一个指针变量以NULL来初始化是个很好的习惯 
  5. EmptyClipboard(); // 清空剪贴板上的内容 
  6. hClip = GlobalAlloc(GMEM_MOVEABLE, strlen(pSnd) + 1); 
  7. pBuf = (char *)GlobalLock(hClip); 
  8. // 得到句柄标识的内存的实际物理地址,lock后系统就不能把它乱移动了 
  9. strcpy(pBuf, pSnd); 
  10. GlobalUnloak(hClip); // 跟open和close的关系是一样的,有lock的也不要忘记unlock 
  11. SetClipboardData(CF_TEXT, hClip); 
  12. CloseClipboard(); // 有open就不要忘记close 

在你的程序中加入以上这段话,它就把pSnd中的内容发到了剪贴板上,相当于你作了Ctrl+C, 不信你可以执行这段程序后,打开一个notepad然后手动Ctrl+v看看是不是很惊奇.

  1. void Receive() 
  2. if (OpenClipboard()) 
  3. if (IsClipboardFormatAvailable(CF_TEXT))//判断剪贴板中的数据是否是文本 
  4. HANDLE hClip; 
  5. char *pBuf = NULL; 
  6. hClip = GetClipboardData(CF_TEXT); // 根据编程中的对称原则,这个我就不介绍了 
  7. pBuf = (char *)GlobalLock(hClip); 
  8. GlobalUnlock(hClip); 
  9. MessageBox(pBuf); //显示出来 
  10. }
  11.  
  12. CloseClipboard(); 

上面这段程序就相当于你执行了Ctrl+V操作,它把剪贴板中的数据取了出来;

剪贴板是系统提供的,所有进程都可以访问它,它就是一段全局内存区,操作系统中的每个进程就都会像线程访问共享变量一样的使用它,很简单,但是问题很多, 正是因为所有的进程都可以访问它,所以如果你的两个进程间的通信如果使用这种方式的话,第一,通信效率不高;第二,会影响到其他进程的执行, 如果我现在Ctrl+C了一段文字,再执行Ctrl+V的时候却出现了一些乱七八糟的东西的话那就会很麻烦, 所以可以基于剪贴板来做一个简单的病毒程序,如果你有兴趣的话;

2. 匿名管道(Pipe)
现在大多数都是基于管道通信的,因为每两个进程都可以共享一个管道来进行单独的对话,就象打电话单独占用一条线路一样,而不必担心像剪贴板一样会有串音, 匿名管道是一种只能在本地机器上实现两个进程间通信的管道,它只能用来实现一个父进程和一个子进程之间实现数据传输.其实它是非常有用的,我做过一个实际 的项目就是利用匿名管道,项目就是让我写一个Ping程序来监测网络的通信状况,并且要把统计结果和执行过程显示在我们的软件里, windows有一个自带的ping程序,而且有执行过程和统计,所以我没必要再发明一个(重复发明就等于犯罪----程序员要牢记阿),
只是windows的那个Ping程序的执行结果都显示在了CMD的界面上了,我需要把它提取出来显示在我们的软件界面上,于是我就利用了匿名管道实现了 这个程序, 当我们的软件要启动Ping任务时,我就先CreatePipe创建匿名管道,再CreateProcess启动了windows下面的Ping程序(它 作为我们软件的子进程),当然要把管道的读写句柄一起传给子进程,这样我就可以轻松的把Ping的执行结果了写入到我的Buffer里了,是不是很 easy。
BOOL CreatePipe(PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize)
这个API函数是有用来创建匿名管道的,它返回管道的读写句柄(hReadPipe,hWritePipe), 记住lpPipeAttributes不能为NULL,因为这意味着函数的返回句柄不能被子进程所继承,你要知道匿名管道可是实现父子进程通信的阿,只有 当一个子进程从其父进程中继承了匿名管道句柄后,这两个进程才可以通信,lpPipeAttributes不为NULL还远不 够,LPSECURITY_ATTRIBUTES这个结构体的内容去查MSDN吧,我只告诉你其中的BOOL bInheritHandle这个成员变量要赋值为TRUE,
这样才真正实现了子进程可以从父进程中继承匿名管道.
BOOL CreateProcess(...)
这个系统API函数是用来在你的进程中启动一个子进程用的,它的参数实在太多了,你还是去查MSDN吧,别怪我太懒惰,我只说几个关键的地方,不想说的太详细.
下面我就在写一个程序利用匿名管道来通信
父进程的实现:

  1. Class CParent 
  2. .... 
  3. private
  4. HANDLE m_hWrite; 
  5. HANDLE m_hRead; 
  6. void CParent::onCreatePipe() 
  7. SECURITY_ATTRIBUTES sa; // 父进程传递给子进程的一些信息 
  8. sa.bInheritHandle = TRUE; // 还记得我上面的提醒吧,这个来允许子进程继承父进程的管道句柄 
  9. sa.lpSecurityDescriptor = NULL; 
  10. sa.nLength = sizeof(SECURITY_ATTRIBUTES); 
  11. if (!CreatePipe(&m_hRead, &m_hWrite, &sa, 0) 
  12. return
  13.  
  14. STARTUPINFO sui; 
  15. PROCESS_INFOMATION pi; // 保存了所创建子进程的信息 
  16. ZeroMemory(&sui, sizeof(STARTUPINFO));
  17. // 对一个内存区清零,最好用ZeroMemory, 它的速度要快于memset 
  18. sui.cb = sizeof(STARTUPINFO); 
  19. sui.dwFlags = STARTF_USESTDHANDLES; 
  20.  
  21. sui.hStdInput = m_hRead; 
  22. sui.hstdOutput = m_hWrite; 
  23. /*以上两行也许大家要有些疑问,为什么把管道读句柄(m_hRead)赋值给了hStdInput, 因为管道是双向的
  24. ,对于父进程写的一端正好是子进程读的一端,而m_hRead就是父进程中对管道读的一端, 自然要把这个句柄
  25. 给子进程让它来写数据了(sui是父进程传给子进程的数据结构,里面包含了一些父进程要告诉子进程的一些信
  26. 息),反之一样*/ 
  27. sui.hStdError = GetStdHandle(STD_ERROR_HANDLE); 
  28.  
  29. if (!CreateProcess("Child.exe", NULL, NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi)) 
  30. CloseHandle(m_hRead); 
  31. CLoseHandle(m_hWrite); 
  32. return
  33. else 
  34. CloseHandle(pi.hProcess); // 子进程的进程句柄 
  35. Closehandle(pi.hThread); 
  36. // 子进程的线程句柄,windows中进程就是一个线程的容器,每个进程至少有一个线程在执行 
  37.  
  38. void CPraent::OnPiepRead() 
  39. char buf[100]; 
  40. DWORD dwRead; 
  41. if (!ReadFile(hRead, buf, 100, &dwRead, NULL))// 从管道中读取数据 
  42. /* 这种读取管道的方式非常不好,最好在实际项目中不要使用,因为它是阻塞式的,如果这个时候管道中
  43. 没有数据他就会一直阻塞在那里, 程序就会被挂起,而对管道来说一端正在读的时候,另一端是无法写的
  44. ,也就是说父进程阻塞在这里后,子进程是无法把数据写入到管道中的, 在调用ReadFile之前最好调用
  45. PeekNamePipe来检查管道中是否有数据,它会立即返回, 或者使用重叠式读取方式,那么ReadFile的最
  46. 后一个参数不能为NULL*/ 
  47. return
  48. Messagebox(buf) 
  49.  
  50. void CParent::onPipeWrite(char *pBuf) 
  51. ASSERT(pBuf != NULL); // 这个很重要 
  52. DWORD dwWrite; 
  53. if (!WriteFile(hWrite, pBuf, strlen(pBuf) + 1, &dwWrite, NULL))// 向管道中写数据 
  54. return

子进程的实现:

  1. Class Child 
  2. ...... 
  3. private
  4. HANDLE m_hRead; 
  5. HANDLE m_hWrite; 
  6. void CChild :: CChild() 
  7. m_hRead = GetStdHandle(STD_INPUT_HANDLE); 
  8. m_hWrite = GetStdhandle(STD_OUTPUT_HANDLE); 
  9. /* GetStdhandle获得标准输入输出句柄,如果你希望你的程序也能跟其他父进程通信的话最好也这么作
  10. ,并不是所有的程序被创建了后都能跟父进程通信的, 我用过很多老外写的小程序,它们都提供了标准的
  11. 对外通信接口,这样很便于你的使用特别对程序员*/ 
  12. void CChild::OnReadPipe() 
  13. void CChild::OnWritePipe() /* 这两个函数与CParent中的相同 */ 

匿名管道由于是匿名的方式所以它不能实现两个同级的进程进行通信,因为一个进程创建了一个管道后,另一个线程并不知道如何找到这个管道,所以它只能通过父 进程直接把管道读写柄直接传递给子进程的方式进行进程通信,至于为什么有了命名管道还要保留匿名管道的问题, 我想主要是因为父子进程通信的方式已然被广泛的采用,而这种方式无疑要比命名管道消耗的资源更少,效率更高,就像自己自己写的进程调用了自己写的一个函数 一样。

3. 命名管道(Pipe)
命名管道不仅可以在本机上实现两个进程间的通信,还可以跨网络实现两个进程间的通信,就像我现在正使用MSN跟我远方的同学聊天一样!其实如果你用过 Socket编写网络程序的话,你就会明白所谓的命名管道之间的通信就相当于把计算机低层网络网络通信部分给封装了起来,使用户使用起来不必了解那么多网 络通信的知识,总之一句话就是用起来简单,其实我们在为别人提供函数库的时候都应该遵循这个规律,把低层烦琐,复杂,抽象的都封装起来,对高层提供统一的 接口.
在Windows2000/NT以后,都可以在创建管道时指定据有访问权限的用户使用管道,进一步保证了安全性,而如果你要是自己使用Socket实现这 个功能的话就太麻烦了,当然很多程序员已然会自己实现它,他们的理由很可能是因为windows都不安全.命名管道实现进程间的通信也跟网络通信一样是 C/S结构的,服务器进程负责创建命名管道及接受客户机的连接请求,就象socket中Server部分要实现bind、linstening和
accept一样, 而客户端只负责连接,对应于socket中的connect一样.
命名管道提供了两种基本通信模式:字节模式和消息模式,在字节模式下,数据以一个连续的字节流的形式在server于client之间流动,而消息模式 下,客户机和服务器则通过一系列不连续的数据单位进行数据收发,每次管道上发出了一条消息后,它必须作为一条完整的消息读入,是不是很像TCP和UDP.

HANDLE CreateNamePipe(....)
创建命名管道的API, 我依然不想解释它的具体参数含义,我只解释它的第一个参数LPCTSTR lpName,它的字符串格式是"\\\\.\\pipe\\pipename"
为什么这么多\, 其实一共就4个,可你看到有8个是因为C/C++中字符串中如果包含一个'\'就必须"\\"才能表达它的意思,你还记得吗?它的实际格式是"\\. \pipe\pipename",它的'.'表示的是本机地址,如果是要与远程服务器连接,就在这个'.'处指定服务器的名称,接下来的pipe是固定的 不要改,pipename就是你要命名的管道名字.

BOOL ConnectNamedPipe(HANDLE hNamePipe, LPOVERLAPPED lpOverlapped)
初看这个函数的名字你一定认为这个是客户端用来连接服务器管道的,事物的表面总是欺骗我们,恰恰相反它是服务器用来等待远程连接的,类似于socket中的listen.
BOOL WaitNamedPipe(LPCTSTR lpNamedPipeName, DWORD nTimeOut)
有了上面那个函数的教训,如果我问题这个函数是作什么的你一定不会立即回答,是的,它是在客户端来判断是否有可以利用的命名管道的,每个客户端最开始都应该使用它判断一些,就像socket中的connect要判断一下server是否已经启动了.

下面是服务器代码:

  1. class CNamePipeServer 
  2. ... 
  3. private
  4. HANDLE m_hPipe; 
  5. /* 创建命名管道等待客户端连接 */ 
  6. void CNamePipeServer::NamePipeCreated() 
  7. m_hPipe = CreateNamedPipe("\\\\.\\pipe\\MyPipe"
  8.  
  9. PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 
  10. 0, 1, 1024, 1024, 0, NULL); 
  11. if (INVALID_HANDLE_VALUE == m_hPipe) 
  12. return
  13. HANDLE hEvent; 
  14. hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 创建一个事件 
  15. if (INVALID_HANDLE_VALUE == hEvent) 
  16. return
  17.  
  18. OVERLAPPED ovlap; 
  19. ZeroMemory(&ovlap, sizeof(OVERLAPPED)); 
  20. ovlap。hEvent = hEvent; 
  21. /* 等待客户连接 , 采用了重叠方式, 该函数会立即返回不会阻塞*/ 
  22. if (!ConnectNamePipe(hPipe, &ovlap)) 
  23. /* 由于函数会立即返回,所以在没有连接的时候不会阻塞会返回,这个时候要判断错误失败的原因*/ 
  24. if (ERROR_IO_PENDING != GetLastError()) 
  25. .... 
  26. return
  27. /* 一个连接到来的时候,Event会立即变为有信号状态 */ 
  28. if (WAIT_FAILED == WaitForSingleObject(hEvent, INFINTE)) 
  29. ... 
  30. return
  31. CloseHandle(hEvent); 
  32. void CNamePipeServer::OnReadPipe() 
  33. void CNamePipeServer::OnWritePipe() 

命名管道读写的方式与匿名管道的相同, 不再冗述。
客户端实现:

  1. clase CNamePipeClient 
  2. ... 
  3. private
  4. HANDLE m_hPipe; 
  5. void CNamePipeClient::OnPipeConnect() 
  6. if (!WaitNamedPipe("\\\\.\\pipe\\MyPipe", NMPWAIT_WAIT_FOREVER)) 
  7. return
  8. /* 打开命名管道,与服务器进行通信 , CreateFile这个函数是不是很熟悉,是的我们写文件的时候都用
  9. 这个API,其实不仅是创建文件,只要是句柄标识的资源似乎都可以用它来来创建,如 与硬件(COM口)之间
  10. 的通信等,这就是对下层具体实现封装,对上提供统一接口的好处,不然不知道我们又要多记多少个API函数*/ 
  11. m_hPipe = CreateFile("\\\\.\\pipe\\MyPipe", GENERIC_READ | GENERIC_WRITE, 0, NULL
  12. , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 
  13. if (INVALID_HANDLE_VALUE == m_hPipe) 
  14. reutrn; 
  15. void CNamePipeClient::OnReadPipe() 
  16. void CNamePipeClient::OnWritePipe() 

同上.

命名管道我没有在实际中使用过,所以对它的一些特点理解的并不是很透彻,不能为大家提供更多的建议了.

4. 邮槽(Mailslot)
邮槽是基于广播通信设计出来的,采用不可靠的数据传输,它是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户端进程写入数据,据说邮槽广泛的应用于网络会议系统.
服务器进程

  1. void MailslotRecv() 
  2. HANDLE hMailslot; 
  3. /* 创建邮槽 */ 
  4. hMailslot = Createmailslot("\\\\.\\mailsolt\\MyMailslot", 0, MAILSLOT_WAIT_FOREVER, NULL); 
  5. if (INVALID_HANDLE_VALUE 
  6.  
  7. == hMailslot) 
  8. return
  9.  
  10. char buf[100] 
  11. DWORD dwRead; 
  12. /* ReadFile在读取邮槽中的数据的时候,如果暂时没有数据它会阻塞在那里,但是一旦有了数据后就立刻返回,
  13. 它在本端的读操作不影响另一端的写操作, 这一点不同于Pipe*/ 
  14. if (!ReadFile(hMailslot, buf, 100, &dwRead, NULL)) 
  15. ... 
  16. return
  17. MessageBox(buf); 
  18. CloseHandle(hMailslot); 

客户端进程:

  1. void MailslotSnd(char *pBuf) 
  2. ASERRT(pBuf != NULL); 
  3. HANDLE hMailslot; 
  4. /* 又是CreateFile,啥也不说了,太帅了*/ 
  5. hMailslot = CreateFile("\\\\.\\mailslot\\MyMailslot", ENERIC_READ | GENERIC_WRITE
  6. , 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 
  7. if (INVALID_HANDLE_VALUE == hMailslot) 
  8. return
  9.  
  10. DWORD dwWrite; 
  11. if (!WriteFile(hMailslot, pBuf, strlen(pBuf) + 1, &dwWrite, NULL)) 
  12. .... 
  13. return
  14. CloseHandle(hMailslot); 

邮槽的使用是不是更简单, 我同样也没有在实际的项目中使用过它,依然不作过多的评价.

抱歉!评论已关闭.