第17章,进程间通信

         当一个进程启动后,操作系统为其分配了4GB的私有地址空间,位于同一个进程中的多个线程共享一个地址空间,因此线程之间的通信非常简单。然而,由于每个进程锁拥有的4GB地址空间都是私有的,一个进程不能访问另一个进程地址空间中的数据。

         进程通信的四种方式:剪贴板、匿名管道、命令通道和邮槽。

剪贴板

          例如,在word文档中复制一份数据后,可以在其他文件中粘贴,这个过程就是在不同进程之间利用剪贴板实现的一次数据传输。剪贴板实际上是系统维护管理的一块内存区域,当在一个进程中复制数据时,是将这个数据放到该块内存区域中,当在另一个进程中粘贴数据时,是从该块内存区域中取出数据,然后显示在窗口上。

         在把数据放置到剪贴板之前,需要先打开剪贴板,如果某个程序已经打开了剪贴板,其他应用程序将不能修改剪贴板,直到前者调用了CloseClipBoard函数,并且只有调用了EmptyClipBoard函数之后,打开剪贴板的当前窗口才拥有剪贴板。EmptyClipBoard函数将清空剪贴板,并释放剪贴板中数据的句柄,然后剪贴板的所有权分配给当前打开剪贴板的窗口。因为剪贴板是所有进程都可以访问的,所以我们在编写这个ClipBoard进程使用剪贴板之前,可能已经有其他进程把数据放置到了剪贴板上,那么在该进程打开剪贴板之后,需要调用EmptyClipBoard函数,清空剪贴板,释放剪贴板上的数据句柄,并将剪贴板的所有权分配给当前打开剪贴板的窗口,之后,就可以向剪贴板中放置数据了。

         OpenClipBoard();    打开剪贴板,将剪贴板中的窗口句柄设置为本窗口;

         CloseClipBoard();   清空剪贴板中的数据,即清空指向数据的句柄。

         EmptyClipBoard();

         SetClipBoardData();

剪贴板延迟提交技术:

         在数据提供进程创建了剪贴板数据后,一直到有其他进程获取剪贴板数据前,这些数据都要占据内存空间。如在剪贴板放置的数据量过大,就会浪费内存空间,降低对资源的利用率。为避免这种浪费,可以采取延迟提交(Delayed rendering)技术。即由数据提供进程先创建一个指定数据格式为空(NULL)剪贴板数据块,直到有其他进程需要数据或自身进程要终止运行时才提交真正的数据。延迟提交的拥有者进程需要做的工作是对WM_RENDERFORMAT、WM_DESTROYCLIPBOARD和WM_RENDERALFROMAT等剪贴板延迟提交消息的处理。

          当另一个进程调用GetClipBoardData函数时,系统将会向延迟提交数据的剪贴板拥有者进程(EmptyClipBoard会使进程拥有剪贴板,并在此时设置窗口的句柄,发送这些消息的时候,就知道了要发送的目的窗口的窗口句柄)发送WM_RENDERFORMAT消息,剪贴板拥有者进程在此消息的响应函数中应使用响应的格式和实际数据句柄来调用SetClipBoardData函数,而不必再调用OpenClipBoard和EmptyClipBoard去打开和清空剪贴板了。在设置完数据也不必调用CloseClipBoard关闭剪贴板。如果有其他进程打开剪贴板并且调用了EmptyClipBoard函数去清空剪贴板的内容,接管剪贴板的拥有权时,系统将向延迟提交的剪贴板拥有者进程发送WM_DESTROYCLIPBOARD消息,以通知该进程剪贴板拥有权的丧失。而失去剪贴板拥有权的进程在接收到该消息后,则不会再向剪贴板提交数据,另外,在延迟提交进程在提交完要提交的数据后,也会收到该消息,如果延迟提交剪贴板拥有者进程将要终止,系统将会为其发送一条WM_RENDERALLFORMATS,通知打开并清除剪贴板内容,在调用SetClipBoardData设置各数据句柄后关闭剪贴板。

剪切板:系统维护的一个全局公共内存区域.每次只允许一个进程对其进行访问。

1.打开剪切板
Bool OpenClipboard(HWND hWndNewOwner);
  指定关联到打开的剪切板的窗口句柄,传入NULL表示关联到当前任务。每次只允许一个进程打开并访问。

每打开一次就要关闭,否则其他进程无法访问剪切板。


2.清空剪切板
Bool EmptyClipboard(void)

  写入前必须先清空,得到剪切板占有权

3.分配内存
HGLOBAL GlobalAlloc(UINT uFlags, SIZE_T dwBytes);
  在堆上动态分配以字节为单位的内存区域。成功则指向该内存,失败NULL。参数:1.分配内存属性, 2.分配的大小

        Win32内存管理没有提供一个单独的本地堆和全局堆,也就是说,在Win32平台下,已经没有本地堆和全局堆了。和其他内存管理函数相比,全局内存函数的运行速度稍微慢些,而且他们没有提供更多的特性,所以新的应用程序应该使用堆函数,然而,全局函数仍然与动态数据交换,以及剪贴板函数一起使用。本程序是利用剪贴板在进程间进行通信,因此还是需要使用GlobalAlloc这个函数。
4.锁定内存
LPVOID GlobalLock(HGLOBAL hMem);

      此函数的作用是对全局内存对象进行加锁,然后返回该内存块第一个字节的指针。
  锁定由GlobalAlloc分配的内存,并将内存对象的锁定计数器+1,成功返回指向内存对象起始地址的指针。失败NULL

系统为每个全局内存对象维护一个锁定计数器,初始为0,GlobalLock使计数器+1,GlobalUnLock计数器-1.一旦计数器值大于0,

这块内存区域将不允许被移动或删除,只有当为0时,才解除对这块内存的锁定。如果分配时GMEM_FIXED属性,计数器一直为0

5.设置剪切板
HANDLE SetClipboardData(UINT uFormat, HANDLE hMem);

  执行成功,返回数据句柄,否则返回NULL

6.解除锁定
BOOL GlobalUnlock(HGLOBAL hMem);
  将GlobalAlloc分配的属性为GMEM_MOVEABLE的内存对象计数器-1.

 

7.关闭剪切板
Bool CloseClipboard(void);

  必须关闭剪切板其他进程才能使用剪切板,且关闭后当前进程就不能写入数据。

8.判断剪贴板中的数据是否是指定格式的数据

IsClipboardFormatAvaiable

         判断剪贴板中的数据是否是我们想要的格式的数据,是的话,在获取剪贴板中的数据。

9.获取剪切板数据
HANDLE GetClipboardData(UINT uFormat);

  执行成功,返回数据句柄,否则返回NULL数据格式,指定格式的数据的句柄

 

一:UINT uFormate格式说明:标准剪贴簿数据格式

Windows支持不同的预先定义剪贴簿格式, 这些格式在WINUSER.H定义成以CF为前缀的标识符。

■三种能够储存在剪贴簿上的文字数据型态:

CF_TEXT    以NULL结尾的ANSI字符集字符串。它在每行末尾包含一个carriage  return和linefeed字符,这是最简单的剪贴簿数据格式。

CF_OEMTEXT    含有文字数据(与CF_TEXT类似)的内存块。但是它使用的是OEM字符集。

CF_UNICODETEXT    含有Unicode文字的内存块。与CF_TEXT类似,它在每一行的末尾包含一个carriage  return和linefeed字符,以及一个NULL字符(两个0字节)以表示数据结束。CF_UNICODETEXT只支援Windows NT。

■两种附加的剪贴簿格式、但是它们不需要以NULL结尾,因为格式已经定义了数据的结尾。

CF_SYLK    包含Microsoft 「符号连结」数据格式的整体内存块。这种格式用在Microsoft的Multiplan、Chart和Excel程序之间交换数据,它是一种ASCII码格式。

CF_DIF    包含数据交换格式(DIF)之数据的整体内存块。用于把数据送到VisiCalc电子表格程序中。这也是一种ASCII码格式

■下面三种剪贴簿格式与位图有关。所谓位图就是数据位的矩形数组

CF_BITMAP    与设备相关的位图格式。位图是通过位图句柄传送给剪贴簿的。

CF_DIB    定义一个设备无关位图的内存块。

CF_PALETTE    调色盘句柄。

■下面是两个metafile格式、metafile就是一个以二进制格式储存的画图命令集

CF_METAFILEPICT    以旧的metafile格式存放的「图片」 。

CF_ENHMETAFILE    增强型metafile(32位Windows支持的)句柄。

■最后介绍几个混合型的剪贴簿格式:

CF_PENDATA与Windows的笔式输入扩充功能联合使用。

CF_WAVE声音(波形)文件。

CF_RIFF使用资源交换文件格式(Resource Interchange File Format)的多媒体数据。

CF_HDROP与拖放服务相关的文件列表。

 

二:UINT uFlags格式说明:内存属性

GMEM_FIXED

  分配一块固定的内存区域,不允许系统移动,这时返回值是一个指针。

GMEM_MOVEABLE

  分配一块可移动的内存区域,实际上内存块在物理内存中是不可移动的,这里的可移动指的是在应用程序的默认逻辑堆内可以移动。返回值是内存对象的句柄。可以通过调研GlobalLock()函数将一个句柄转化为一个指针,这个标志不能喝GMEM_FIXED同时使用

GMEM_ZEROINT   

  初始化内存对象为全0,如果不用这个标志,内存对象将为不确定的内容

GHND

  GMEM_MOVEABLEGMEM_ZEROINT块标志联合使用,即可移动同时初始化为0

GPTR

  GMEM_FIXEDGMEM_ZEROINT标志联合使用,即不可移动同时初始化为0

复制代码
 1 void  CMFC_TabCtrlDlg::SetClipBoardData_(CString strText)
 2 {
 3     /*
 4     OpenClipboard打开剪切板:指定关联到打开的剪切板的窗口句柄,传入NULL表示关联到当前任务。每次只允许一
 5     个进程打开并访问。每打开一次就要关闭,否则其他进程无法访问剪切板。
 6     EmptyClipboard清空剪切板:写入前必须先清空,得到占有权
 7     */
 8     if (::OpenClipboard(m_hWnd) &&::EmptyClipboard())
 9     {
10         //根据环境变量获取数据长度
11         size_t cbStr = (strText.GetLength() + 1) * sizeof(TCHAR);
12 
13         //在堆上动态分配以字节为单位的全局内存区域。成功则指向该内存,失败NULL。参数:1.分配内存属性,2.大小
14         HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, cbStr); 
15 
16         if (hMem == NULL) 
17         {
18             //关闭剪切板,释放剪切板所有权,关闭后就不能写入数据
19             CloseClipboard();
20             return; 
21         }
22 
23         //锁定由GlobalAlloc分配的内存,并将内存对象的锁定计数器+1;成功返回指向内存对象起始地址的指针。失败NULL
24         LPTSTR lpDest = (LPTSTR)GlobalLock(hMem);
25         /*
26         系统为每个全局内存对象维护一个锁定计数器,初始为0,GlobalLock使计数器+1,
27         */
28 
29         //拷贝数据到剪贴板内存。
30         memcpy_s(lpDest, cbStr, strText.LockBuffer(), cbStr);
31         strText.UnlockBuffer();
32 
33         //解除内存锁定,将属性为GMEM_MOVEABLE的内存对象计数器-1.
34         GlobalUnlock(hMem); 
35         /*
36         GlobalUnLock计数器-1.一旦计数器值大于0,这块内存区域将不允许被移动或删除,只
37         有当为0时,才解除对这块内存的锁定。如果分配时GMEM_FIXED属性,计数器一直为0
38 
39         */
40 
41         //根据环境变量设置数据格式
42         UINT uiFormat = (sizeof(TCHAR) == sizeof(WCHAR))?CF_UNICODETEXT:CF_TEXT;
43 
44         //设置数据到剪贴板。执行成功,返回数据句柄,否则返回NULL
45         if(SetClipboardData(uiFormat, hMem) == NULL); 
46         {
47             CloseClipboard();
48             return;
49         }
50 
51         CloseClipboard();
52     }
53 }
复制代码

 

2.从剪切板内存获取数据

复制代码
 1 void CMFC_TabCtrlDlg::GetClipBoardData_(void)
 2 {
 3     //if (IsClipboardFormatAvailable(CF_UNICODETEXT)) //判断某种格式的数据是否可用
 4     if(::OpenClipboard(m_hWnd))
 5     {
 6         UINT uiFormat = (sizeof(TCHAR) == sizeof(WCHAR))?CF_UNICODETEXT:CF_TEXT;
 7 
 8         ////执行成功,返回数据句柄,否则返回NULL。参数:1.数据格式,2.指定格式的数据的句柄
 9         HGLOBAL hMem = GetClipboardData(uiFormat); 
10 
11         if (hMem != NULL) 
12         { 
13             //获取UNICODE的字符串。
14             LPCTSTR lpStr = (LPCTSTR)GlobalLock(hMem); 
15             if (lpStr != NULL) 
16             { 
17                 SetDlgItemText(IDC_EDIT1, lpStr);
18             } 
19             GlobalUnlock(hMem);
20         } 
21     }
22     CloseClipboard();
23 }
复制代码


匿名管道:
             
匿名管道是一个未命名的、单向管道,通常(只能)用来在一个父进程和一个子进程之间传输数据。匿名管道只能实现本地机器上两个进程间的通信,而不能实现跨网络的通信。
              管道(Pipe)实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。匿名管道(Anonymous Pipes)是在父进程和子进程间单向传输数据的一种没有名字的管道,只能在本地计算机中使用,而不可用于网络间的通信。
             局限性:

第一:匿名管道只能实现本地进程之间的通信,不能实现跨网络之间的进程间的通信。

第二:匿名管道只能实现父进程和子进程之间的通信,而不能实现任意两个本地进程之间的通信。

            匿名管道的另外一种功能,其通过匿名管道可以实现子进程输出的重定向,

何为输出重定向呢?还请听下面详解:

比如我现在建立一个 Win32 的 Console 程序,然后在其中使用如下代码来输出一些信息:

#include <iostream>
using namespace std;
 
int main(int argc, char * argv)
{
    cout<<"Zachary  XiaoZhen "<<endl<<endl;
    cout<<"Happy  New   Year"<<endl<<endl;
    
    system("pause");
}

那么在默认下,编译运行上面的代码时,Windows 会弹出一个黑框框,并且在这个黑框框中显示一些信息,

image

为什么一定要将输出的信息显示在这个黑框框中呢?有没有办法让其显示在我们自己定义的文本框中呢?

而后我们再看一幅截图:

QQ截图未命名

上面画了很多红线的这个区域中的信息来自那里呢?为什么会在这个文本框中输出呢?

其实这就可以通过匿名管道来实现,

在卸载 QQ 游戏这幅截图中呢,其实运行了两个进程,

一个就是我们看到的这个输出了图形界面的进程,我们称之为卸载表象进程(父进程),

而另外一个用来执行真正意义上的卸载的进程我们称之为卸载实质进程(子进程)。

其实该卸载表象进程在其执行过程中创建了卸载实质进程来执行真正的卸载操作,

而后,卸载实质进程会输出上面用红色矩形标记的区域中的信息,

如果我们使用默认的输出的话,卸载实质进程会将上面红色区域标记中的信息输出到默认的黑框框中(控制台中),

但是我们可以使用匿名管道来更改卸载实质进程的输出,

让其将输出数据输入到匿名管道中,而后卸载表象进程从匿名管道中读取到这些输出数据,

然后再将这些数据显示到卸载表象进程的文本框中就可以了。

而上面的这种用来更改卸载实质进程的输出的技术就称之为输出重定向。

当然与之相对的还有输入重定向的。

我们可以让一个进程的输入来自于匿名管道,而不是我们在黑框框中输入数据。

话说到这份上呢,也可以点出一点东东了,

上面的这个重定向不就是利用匿名管道实现的父进程和子进程之间的通信嘛。

I/O转向的概念
   在设计程序时,必须指定数据的输入来源(如键盘),以及数据处理完毕后的输出目的地(如文件,打印机),所以必须在程序中指定输入/输入设备后才能运行。可是指定了某个输入/输入设备后,如果再想换由其他设备输入/输出,则必须要修改源程序,重新进行编译、链接。为了避免这个缺点,C语言提供的I/O的重定向能力。
   C语言将键盘与屏幕叫做标准I/O(Standard Input/Output, STDIO),凡是以标准I/O作为输入/输出的程序,均可重定向改由其他文件或设备做输入/输出。所以我们在编写程序时,可先用标准I/O作为输入/输出对象,等到真正运行时,再重定向到真正需要输入/输出的文件。这样就可避免在编写程序时设置输入/输出的文件名,当需要更改时又返回到程序进行修改的烦恼了。
   大多数操作系统如Linux,Unix及MS D OS都具有I/O重定向的能力,在这些系统上运行程序(不仅限于C程序),都可以使用I/O重定向。许多操作系统把设备视为一个文件,所以I/O可以重定向到文件,也可以转向到一些接口设备。

匿名管道的使用

          匿名管道主要用于本地父进程和子进程之间的通信,在父进程中的话,首先是要创建一个匿名管道,在创建匿名管道成功后,可以获取到对这个匿名管道的读写句柄,然后父进程就可以向这个匿名管道中写入数据和读取数据了,但是如果要实现的是父子进程通信的话,那么还必须在父进程中创建一个子进程,同时,这个子进程必须能够继承使用父进程的一些公开的句柄,为什么呢?因为在子进程中必须要使用父进程创建的匿名管道的读写句柄,通过这个匿名管道才能实现父子进程的通信,所以必须继承父进程的公开句柄。同时在创建子进程的时候,必须将子进程的标准输入句柄设置为父进程中创建匿名管道时得到的读管道句柄,将子进程的标准输出句柄设置为父进程中创建匿名管道时得到的写管道句柄。然后在子进程就可以读写匿名管道了。

命名管道:

          命名管道是通道网络来完成进程间的通信,它屏蔽了底层的网络协议细节。我们在不了解网络协议的情况下,也可以利用命名管道来实现网络间的通信。将命名管道作为一种网络编程方案时,它实际上建立了一个客户机/服务器通信体系,并在其中可靠地传输数据。命名管道是围绕Windows文件系统设计的一种机制,采用 命名管道文件系统(Named Pipe File System,NPFS)接口,因此客户机和服务器可利用标准的Win32文件函数(如:ReadFile和WriteFile)来进行数据的收发。
          命令管道服务器和客户机的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接受管道客户机的连接请求,而客户机只能同一个现成的命名管道服务器建立连接。
          命名管道提供了两种基本通信模式:字节模式和消息模式。在字节 模式中,数据以一个连续的字节流的形式,在客户机和服务器之间流动。而在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发,每次在管道上发出一条消息后,它必须作为一条完整的消息读入。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值