题记:
博客中的文章希望整理的具有逻辑性,方便后续自己回顾学习以及与大家交流。不知道你是否有同感?有时候忙活了一整天,搜寻了各种资料,时而欣喜若狂,时而低落沮丧;时而钻进了源码的牛角尖,时而老虎吃天无从下手。但是每当经历了如此过程后,在你即将放弃的时刻却恍然大悟,难题orBug迎刃而解。如此反复,日复一日……,倘若事后不进行整理记录,你得到的仅仅是最后时刻那一丁点的兴奋与喜悦,却难有进步和成长。因此决定将“日积月累”作为平日里钻研学习的阵地,开辟此系列用于记录日常学习的点点滴滴。2015,Running
ZSSURE……
背景:
最近一段时间集中接触了些许关于IPC的相关技术,即进程间通信。网上搜索学习了《Unix网络编程卷2:进程间通信》、ZeroMQ Guide文档、ActiveMQ等资料,对IPC有了大致的了解,本篇文章中记录一个尝试匿名管道时遇到的奇葩问题,仅供学习和交流。
进程间通信:
进程间通信就是在不同进程之间传播或交换信息,要想实现信息交换必然需要不同进程都可访问的公共介质。然而进程的用户空间是互相独立的,一般而言不能互相访问,唯一例外的是共享内存区。另外,系统空间都是“公共场所”,所以内核很显然是双方共享介质;除此以外就是双方都可以访问的外设了。另外从广义上来说,磁盘上的普通文件、注册表、数据库等都可用来交互信息,也可以算作是进程间通信的手段。【摘自百度百科,1】
上文既然提到了系统内核,那么不得不提当前主流的三大系统Windows、Unix、Linux。其三者的基本原理是相同的,但是具体到具体的实现方式时可能略有不同。
Windows进程间通信:
Microsoft Win32 API提供了多种进程间通信的方法【2】,主要包括:
1)文件映射(Memory-Mapped Files),能使进程把文件内容当做进程地址区间中的一块内存来对待,因此进程不必使用文件I/O操作,简单的指针操作即可读取和修改文件内容。应用程序有三种方式使得多个进程共享一个文件映射对象,例如继承(父进程创建文件映射对象,子进程继承该对象句柄)、命名文件映射(一个进程建立有名称的文件映射对象,另外的进程可通过该名称打开文件映射对象。第一个进程可通过命名管道、邮件槽等IPC机制将文件映射对象名传递给其他进程)和句柄复制(第一个进程建立文件映射对象,然后通过其它IPC机制(有名管道、邮件槽等)把对象句柄传递给第二个进程。第二个进程复制该句柄就取得对该文件映射对象的访问权限)。【注】:文件映射是在多个进程间共享数据的非常有效方法,有较好的安全性。但文件映射只能用于本地机器的进程之间,不能用于网络中,而开发者还必须控制进程间的同步。
2)共享内存(Shared Memory),实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
3)匿名管道(Pipe),管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向(一端是只读的,另一端点是只写的);也可以是双向的(管道的两端点既可读也可写)。匿名管道(Anonymous Pipe)是 在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写 端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。【注】:匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
4)命名管道(Named Pipe),与匿名管道类似,不同的是进程在创建管道时对其进行命名,其他进程可通过名称来打开管道。【注】:命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。
5)邮件槽(Mailslots),提供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消 息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。【注】:邮件槽与命名管道相似,不过它传输数据是通过不可靠的数据报(如TCP/IP协议中的UDP包)完成的,一旦网络发生错误则无法保证消息正确地接收,而命名管道传输数据则是建立在可靠连接基础上的。不过邮件槽有简化的编程接口和给指定网络区域内的所有计算机广播消息的能力,所以邮件槽不失为应用程序发送和接收消息的另一种选择。
此外还有诸如剪贴板(Clipped Board)、动态数据交换(DDE)、对象连接于嵌入(OLE)、动态链接库(DLL中的全局数据区可以被调用DLL的所有进程共享)、远程过程调用(RPC)、NetBios函数、Sockets(网络编程中最常见,PACS系统中也经常使用)、WM_COPYDATA消息等等。
Unix进程间通信:
与Windows系统类似,Unix系统的进程间通信方式主要有【3】:
1)管道(Pipe)和命名管道(Named Pipe),管道可用于具有亲缘关系进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
2)信号(Signal),信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致得。
3)消息队列(Message Queue),消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。消息缓冲通信技术是由Hansen首先提出的,其基本思想是:根据“生产者-消费者”原理,利用内存中公用消息缓冲区实现进程之间的信息交换。内存中开辟了若干消息缓冲区,用以存放消息.每当一个进程向另一个进程发送消息时,便申请一个消息缓冲区,并把已准备好的消息送到缓冲区,然后把该消息缓冲区插入到接收进程的消息队列中,最后通知接收进程.接收进程收到发送里程发来的通知后,从本进程的消息队列中摘下一消息缓冲区,取出所需的信息,然后把消息缓冲区不定期给系统。系统负责管理公用消息缓冲区以及消息的传递。一个进程可以给若干个进程发送消息,反之,一个进程可以接收不同进程发来的消息,显然进程中关于消息队列的操作是临界区。当发送进程正往接收进程的消息队列中添加一条消息时,接收进程不能同时从该消息队列中到出消息,反之也一样。
4)共享内存(Shared Memory),与上一节类似。
5)信号量(Semaphore),主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
6)套接字(Socket),同上一节
Linux进程间通信:
Linux与Unix方式类似【4】,如下图所示:
其中,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括:Posix消息队列、Posix信号灯、Posix共享内存区。有两点需要简单说明一下:1)由于Unix版本的多样性,电子电气工程协会(IEEE)开发了一个独立的Unix标准,这个新的ANSI
Unix标准被称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就遵循POSIX标准;2)BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)。事实上,很多Unix版本的单机IPC留有BSD的痕迹,如4.4BSD支持的匿名内存映射、4.3+BSD对可靠信号语义的实现等等。
匿名管道遇到的问题:
今天对匿名管道进行了本地测试,使用的编译环境是VS2012,使用语言是C++。
其中服务端代码如下:
// TestServerforPipe.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <windows.h> #define CREATE_PIPE_FAILER 1 #define GET_STDOUTPUT_FAILED 2 #define SET_STDOUTPUT_TOPIPE_FAILED 3 #define CREATE_PROCRESS_FAILED 4 int _tmain(int argc, _TCHAR* argv[]) { // TODO: 在此添加控件通知处理程序代码 int errCode; //创建匿名管道 HANDLE hRead,hWrite; SECURITY_ATTRIBUTES sa; ZeroMemory(&sa,sizeof(sa)); sa.nLength=sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor=NULL; sa.bInheritHandle=TRUE; if(!CreatePipe(&hRead,&hWrite,&sa,0)) { //MessageBox("创建匿名管道失败!"); return 0; } //创建子进程 STARTUPINFO si; PROCESS_INFORMATION pi; si.cb=sizeof(STARTUPINFO); HANDLE hTemp=GetStdHandle(STD_OUTPUT_HANDLE); if(!SetStdHandle(STD_OUTPUT_HANDLE,hWrite)) { printf("设置管道为标准输出失败!\n"); } GetStartupInfo(&si); si.hStdError=hWrite; si.hStdOutput=hWrite; si.wShowWindow=SW_HIDE; si.dwFlags=STARTF_USESTDHANDLES; int bRet=0; if(!(bRet=CreateProcess(NULL,"TestClientforPipe.exe",NULL,NULL,TRUE,NULL,NULL,NULL,&si,&pi))) { errCode=GetLastError(); //MessageBox("创建控制台进程失败!"); return 0; } SetStdHandle(STD_OUTPUT_HANDLE,hTemp); char szReadBuf[100]; DWORD nRead; while(ReadFile(hRead,szReadBuf,99,&nRead,NULL)) { szReadBuf[nRead]='\0'; printf("%s",szReadBuf); } return 0; }
客户端代码如下:
// TestClientforPipe.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <windows.h> #include <iostream> using namespace std; int _tmain(int argc, _TCHAR* argv[]) { while(1) { Sleep(1000); printf("This is a win32 console application for testing the un named pipe\n"); } return 0; }
在测试时刻一直未等到输出,服务端程序阻塞在管道读取函数ReadFile处。利用ProcExplorer可以看到服务端和客户端程序都已顺利启动。
问题解决:
搜索了相关资料,通过与博文【5】中的代码对比,发现博文中客户端代码使用的是cout来向匿名管道中写入数据,而我本地的客户端代码使用的是printf,难道是printf出现了问题?
遂尝试将printf更换成cout,竟然奇迹般的成功了,服务端顺利读取到了客户端写入的数据,如下图所示:
问题分析:
都是向标准输出STDOUT中写入数据,难道cout和printf的差别这么大?具体原因是什么呢?虽然我们平时很常用cout和printf,但是对于其背后的原理和机制了解的并不多,通过博文【6】我们可以知道cout与printf可谓同根同源,cout是封装后的、类型安全的printf;之所以起初使用printf函数失败是因为“缓冲区”的缘故,并且cout中的endl是一种操作符,该操作符可起到刷新缓冲区的作用。printf在向屏幕输出时缓冲区的影响可能不是很大,但是当标准输出STDOUT被重定向为文件或本文中的匿名影响比较大,导致实时性不是很好而使得误认为程序编写错误,因此可以猜测如果等待足够长的时间,原本使用printf的客户端程序在将缓冲区写满后也会写入到匿名管道中,同样也会出发服务端断点。经过实际测试可以发现,当等待大约1分钟,后服务端的确顺利输出了客户端发送过来的数据,如下图所示:
另外如果在调用printf函数后手动强制刷新缓冲区,同样可以实现cout与endl时的效果,手动刷新缓冲区使用fflush(stdout)。效果如下:
看似小小的printf函数,却蕴含着如此内容,想了解printf详细机制的可参考博文【7】,博主我脑容量有限,暂时就不深究了,以后有机会再细细研究。
(本文完)
参考资料:
【1】http://baike.baidu.com/link?url=rv33pHtgI2esWoGXmeWaBoGeIOBdhHnDOST9WB0r2-8Ae7OPZdD_5KWHyu8O8z7viQGwa5VQWhpOgrGLnawv9_,百度百科“进程间通信”
【2】http://blog.csdn.net/weiwangchao_/article/details/7104940,Windows进程间通信
【3】http://cqgw2.blog.163.com/blog/static/2352470201032210542930/,Unix进程间通信
【4】http://www.ibm.com/developerworks/cn/linux/l-ipc/,Linux进程间通信
【5】http://blog.chinaunix.net/uid-28300131-id-3391466.html,Windows中Pipe的使用
【6】http://hi.baidu.com/gbmzzupggpflpvr/item/446e03eab000e82e6cabb85c,cout与printf的区别
【7】http://blog.csdn.net/dog250/article/details/23000909,printf介绍
作者:[email protected]时间:2015-01-15