进程间通信(IPC)之管道家族

进程间通信(IPC)是操作系统中协调不同进程间数据交换的关键机制。管道作为IPC的一种,分为普通管道、流管道和命名管道,常用于父子进程间的通信。普通管道是单工和血缘限制的,流管道实现双向传输,命名管道打破了血缘关系,允许无关联进程间通信。管道使用文件描述符进行操作,通过read和write系统调用实现数据传输。在Linux中,管道的实现借助文件系统,容量通常为4KB,并具有一次性读取、数据丢失等特点。
摘要由CSDN通过智能技术生成

进程间通信:

             所谓进程间通信,就是让不同的进程之间可以传播或交换信息,但是不同进程的内存空间是相互独立的(进程的

独占性),般而言是不能相互访问的,所以多个不同进程间要通信,就必须存在一种访问介质。而这种访问介质,

就是内核下的缓冲区,缓冲区是系统下的“公共场所”,各进程都可以访问,所以当一个进程从用户空间向内核缓冲

区中写入数据,另一个进程则去内核缓冲区中读取数据,这样就实现了不同进程间的通信。内核提供的这种机制称为

进程间通信(IPC InterProcess Communication)。

       当然,缓冲区并不是唯一的介质,比如双方都能访问的外设,磁盘上的普通文件,通过“注册表”或其他数据库

的某些表项和记录这些同样能实现数据的交换。广义上说这些都是进程间通信的手段,但是却不把这些称为“进程间

通信”。

:最为简单的两个进程间完成一次进程间通信的流程。



一、进程间通信(IPCInterprocess communication):

进程间通信是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、

交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一

个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优

点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。

IPC方法包括管道(PIPE)、消息排队、旗语、共用内存以及套接字(Socket)。进程间通信主要包括了管道、

系统IPC(包了消息队列、信号以及共享存储)、套接字(SOCKET)。




★这里只讨论管道的相关概念:

管道是一种最基本的 IPC机制,由pipe函数创建:头文件: #include <unistd.h>
  函数名:int pipe(int file

des[2]);  调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端然后通 过filedes

参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的 写端(很好记,就像0是标准输入

1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或write(filedes[1]);向这

文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。


★管道的分类:

①普通管道(PIPE):通常有两种限制,一是单工,即只能单向传输;二是血缘,即常用于父子进程间(或有血缘关

系的进程间)。

②流管道(s_pipe):去除了上述的第一种限制,实现了双向传输。

③命名管道(name_pipe):去除了上述的第二种限制,实现了无血缘关系的不同进程间通信。




▲文件描述符:内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文

件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。在UNIX、

Linux的系统调用中,大量的系统调用都是依赖于文件描述符。




★管道与文件描述符以及文件指针间的关系:

其实管道的使用方法与文件类似,都能使用read,write,open等普通IO函数管道描述符类似于文件描述符. 事实

上, 管道使用的描述符文件指针和文件描述符最终都会转化成系统中SOCKET描述符 都受到系统内核中SOCKET

描述符的限制本质上LINUX内核源码中管道是通过空文件来实现



★管道的使用方法:

①pipe:创建一个管道,返回两个管道描述符,通常用于父子进程间的通讯。

②popen,pclose:只返回一个管道描述符,这种方式常用于通信的另一方是stdin或stdout。

③mkpipe:命名管道,在多进程间实现交互。




※开辟管道后可按以下步骤实现两个进程间的通信:

⑴父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。

⑵父进程调用fork创建子进程,此时子进程也有两个文件描述符指向同一管道。

⑶父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用循环队列

实现的,数据从写端流入,从读端流出,这样就实现了进程间通信。



相关代码

            


此处的进程间通信是单向通信,即便父进程不关闭写端,子进程不关闭读端,双方都有读端和写端,也无法实

现双向通信?

原因是:管道的读写通过打开的文件描述符来传递,因此要通信的两个进程必须从他们的父进程那里继承管道文件描

述符。上述代码是父进程将文件描述符传给子进程后,父子进程间的通信。当然也可以父进程fork两次,将文件描述

符传给fork出的两个子进程,然后让两个子进程间通信。总之,继承的文件描述符必须能够访问同一管道,这样才能

通信。也就是说,管道通信是需要进程间有关系的。




★使用管道时,须注意以下四种情况:

①如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读取数据,那么管道中剩余的数据被取

完后,在此read后会返回0,因为再无数据可读。

②如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写入数据,此时有进程从管道读

端读取数据,那么管道中剩余的数据都被读取后,再次read会发生阻塞,直到管道中有数据可读时才读取数据后返

回。

③如果所有指向管道读端的文件描述符都关闭了,而仍然有进程向管道的写端写入数据,那么该进程会收到SIGPIPE

信号,导致进的异常终止。

④如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读取数据,此时有进程向管道写

端写入数据,那么管道被写满时再次write会发生阻塞,直到管道中存放数据的空位时才写入数据并返回。




★命名管道:

         管道的一个不足之处在于管道没有一个确切的名字,所以普通管道只能用于具有血缘关系间的进程间进行通信,

而对于无血缘关系的多个进程间要实现进程间通信,普通管道已无法满足,所以促使了命名管道(name_pipe或

FIFO)的提出,而普通管道(pipe)也就被称为匿名管道。命名管道的不同之处在于其提供了一个路径名与之关

联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,即使多个进程不存在血缘关系,也可以访问该

路径,通过FIFO实现进程间通信。FIFO与调度算法中的FIFO同名,都是first input first output。所以按照先进先

出的原则,第一个被写入的数据首先从管道中读出。




▲命名管道的创建:

linux下命名管道的创建方式有两种:

①在Shell下交互地建立一个命名管道。Shell方式下可使用mknod或mkfifo命令。

②在程序中使用系统函数建立命名管道。相关系统函数有mknod和mkfifo。



1.mknod系统函数:






2.mkfifo系统函数:




注:可以看到在mknod和mkfifo系统函数的声明中,函数参数中的path为创建命名管道的全路径名,mode为创建管

道的模式,指其存取权限;dev为设备值,该值取决于创建文件的种类,只在创建设备文件时才会用到。两个函数

调用成功都返回0,失败时都返回-1。



★      mknod和mkfifo这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限。mknod是比较老的函数,而使用mkfifo函数更加简单

和规范,所以建议在可能的情况下,尽量使用mkfifo而不是mknod。 mkfifo函数的作用是在文件系统中创建一个文

件,该文件用于提供FIFO功能,即命名管道。 前边讲的那些管道都没有名字,因此它们被称为匿名管道,或简称管

道。对文件系统来说, 匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。而命名管道是

一个可见的文件,因此,它可以用于任何两个进程之间的通信,不管这两个进程是不 是父子进程,也不管这两个进程

之间有没有关系。





fifo  read端:

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <unistd.h> 
#include <fcntl.h> 
#include <string.h> 
#define _PATH_ "/tmp/file.tmp" 
#define _SIZE_ 100 
int main() 
{ 	
    int fd = open(_PATH_, O_RDONLY);          	        
    if(fd < 0){          		
        printf("open file error!\n");                   		    
        return 1;                   	
     }          	
     char buf[_SIZE_];          	
     memset(buf, '\0', sizeof(buf));          	 
     while(1){          		
         int ret = read(fd, buf, sizeof(buf));                   		
         if (ret <= 0)//error or end of file                   		  
        {                   			
             printf("read end or error!\n");                            			  
             break;                            		
        }                   		
         printf("%s\n", buf);                   		 
         if( strncmp(buf, "quit", 4) == 0 )
         {                   			
             break; 
         }
     }
     close(fd);
     return 0;
}
   



fifo write端:

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <unistd.h> 
#include <string.h> 
#include <fcntl.h> 
#define _PATH_ "/tmp/file.tmp" 
#define _SIZE_ 100 
int main() 
{ 	
    int ret = mkfifo(_PATH_,0666|S_IFIFO);          	
    if(ret == -1){          		
        printf("mkfifo error\n");                   		    
        return 1;                   	
     }          	
    int fd = open(_PATH_, O_WRONLY);          	 
    if(fd < 0){          		
         printf("open error\n");                   	 
    }          	
    char buf[_SIZE_];          	
    memset(buf, '\0', sizeof(buf));          	  
    while(1){          		
         scanf("%s", buf);                   		
         int ret = write(fd, buf, strlen(buf)+1);                   		
         if(ret < 0){                   			 
               printf("write error\n");                            			
               break;                            		  
          }                   		
          if( strncmp(buf, "quit", 4) == 0 )
          {                   			
            break;                            		 
           }                   	
       }              
      close(fd); 	
      return 0;          
}


注:  fifo write端中的“S_IFIFO|0666”指明创建一个命名管道且存取权限为0666,即为创建者、与创建者同组的用

户、其他用户对该命名管道的访问权限都是读和写(这里要注意umask对生成的管道文件权限的影响)。 



命名管道创建后就可以使用了,其使用方法和管道基本是相同的。只是使用命名管道时,必须先调用open()函数

将其打开。因为命名管道是设备文件,存储于硬盘上的文件,而管道则是存储于内存中的特殊文件。需要注意的是,

调用open()打开命名管道的进程可能会被阻塞,但如果同时用读写方式 (O_RDWR)打开,则一定不会导致阻塞;

如果以只读方式(O_RDONLY)打开,则调 用open()函数的进程将会被阻塞直到有写入数据的一方打开管道;同样

以写方式(O_WRONLY)打开 也会阻塞直到有读取数据的一方打开管道。




流管道:

          与linux的文件操作中有基于标准I/O操作一样,管道操作也支持基于文件流的操作,这种基于文件流的管道主要

用来创建一个连接到另一个进程的管道,这里“另一个进程”是可以执行一定操作的可执行文件。例如用户执行“ls 

-l”或者./pipe,由于这类操作很常见,所以将一系列创建过程合并到一个函数popen()中完成,而popen()函数会完

成以下的步骤:①创建一个管道;②fork()一个子进程;③在父子进程中关闭不需要的文件描述符;④执行exec()函

数族调用;⑤执行函数中指定的命令。

       由此可见,这个函数可以大大减少代码的编写量,但使用不太灵活,不能自己创建管道那么灵活,并且popen()

必须使用标准的I/o函数进行操作,也不能使用read(),wirte()这种不带缓冲的I/O函数,必须使用pclose()来关闭管

道流,该函数关闭标准I/O流,并等待命令执行结束。







注:可以看到,popen()函数的参数中command指向的是一个以null结束符结尾的字符串,这个字符串包含一个shell命令。参数

type中有两部分,即“w”和“r”。“w”表示文件指针连接到command的标准输入,即该命令的结果产生输入;“r”表示文件

指针连接到command的标准输出,即该命令的结果产生输出。成功时返回文件流指针,失败时返回null。pclose()函数的参数只有

一个stream,即要关闭的文件流。成功时返回popen()函数执行的退出码,失败时返回-1。




linux下管道的实现机制及容量:

           管道频繁地被用于进程间的通信当中,从本质上来看管道其实就是一种特殊文件。其特殊在管道可以克服使用文

件进行通信时存在的两个问题:①限制管道的大小。实际上,管道是一个固定大小的缓冲区。Linux中该缓冲区的大

小为1页,即4K字节,使得它的大小不像文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管

道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够

的空间供write()调用写。②读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种

情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问

题。


▲注:从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。





★ 在Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个file结构指向同一个临时的VFS索引节点,而这个VFS 索引节点又指向一个物理页面而实现的。

                          管道的架构示意图

 关于linux管道结构的具体实现代码可以点击此处链接进行学习:   ★★★★★http://blog.chinaunix.net/uid-20498361-id-1940274.html★★★★★




★关于管道的容量:

         

注:由结果输出可知管道的容量大小为64K,即1024*64=65536。




总的来说管道的特点有以下几点特性:

①单向数据通信

②管道常用于父子进程间(有血缘关系的进程)---------匿名管道,无血缘关系的进程间通信为命名管道;

③提供一种流式服务(发送和接受不受字节数的限制,可取任意大小,与数据块和数据报区别)

④管道的生命周期是随进程的,进程退出,管道消失

⑤提供同步与互斥功能,保证数据传输的正确性





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值