【Linux】进程间通信之管道

        每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Com),也就是说进程通信使不同的进程可以看见同一份系统资源。


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

管道的使用方法与文件类似,都能使用read,write,open等普通IO函数

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

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

★  管道的使用方法

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

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

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


1、管道是一种最基本的IPC机制,由pipe函数来创建

       #incldue<unistd.h>

       int pipe(int filedes[2]);

       调用pipe函数时在内核中开辟一块缓冲区(称为管道),用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(0想象为“口”进行读,1想象为“笔”进行写)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0])或者write(filedes[1])向这个文件读写数据,其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。 开辟了管道之后如何实现两个进程间的通信呢?比如可以按下面的步骤通信mni    

     按以下步骤实现两个进程间的通信:
⑴、父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
⑵、父进程调用fork创建子进程,此时子进程也有两个文件描述符指向同一管道。
⑶、父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用循环队列实现的,数据从写端流入,从读端流出,这样就实现了进程间通信。

父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道,如下图


2、管道特点

1)、管道依赖文件系统,利用文件打开和关闭。
         管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那继承管道文件描述符。
2)、管道是传输资源的一种媒介。
         只允许具有血缘关系的进程进行通信,常用于父子间的通信,即 管道通信是需要进程之间有关系。-----命名管道
3)、管道 只支持单向通信,与半双工不一样,半双工有来往。两个进程通过一个管道只能实现单向通信。
4)、面向字节流的数据传输服务,流式服务
5)、二进制文件跑起来就成了进程,当进程退出,管道资源会自动退出。 管道生命周期随进程
 如果父进程进行读,子进程进行写,则需要父进程关闭写端(1),子进程关闭   读端(0),如下图所示

3、使用管道需要注意几种特殊情况
假设都是阻塞I/o操作,没有设置O_NONBLOCK标志
    1)所有指向管道写端的文件描述符(1)都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都读取后,再次read会返回0,就像文件末尾一样。
    2)有指向管道写端的文件描述符(1)没关闭了,而持有管道写端的进程也没有向管道中写数据,这是有进程从管道的读端读数据,那么管道中剩余的数据都读取后,再次read会堵塞,直到管道中数据可读了才读取数据并返回
    3)所有指向管道读端的文件描述符(0)都关闭了,这时有进程从管道的写端写数据,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
    4)有指向管道读端的文件描述符(0)没关闭了,而持有管道读端的进程也没有从管道中读数据,这是有进程向管道的写端写数据,那么管道被写满时再次write会阻塞直到管道中有空位置了才能写入数据并返回。
以第三种情况进行说明
  8 #include<stdio.h>
  9 #include<unistd.h>
 10 #include<errno.h>
 11 #include<string.h>
 12
 13 int main()
 14 {
 15     int pipe_fd[2];
 16     if(pipe(pipe_fd) < 0){//pipe failure
 17         printf("%d : %s\n",errno,strerror(errno));
 18         return -1;
 19     }
 20     //pipe success
 21     pid_t id = fork();//create process
 22     if(id < 0){
 23         printf("fork failure\n");
 24     }else if(id == 0){//child process write
 25         close(pipe_fd[0]);//close read
 26 
 27         const char* msg = "hello lyf\n";
 28         int count = 0;
 29         while(1){
 30         //while(count < 3){
 31             write(pipe_fd[1],msg,strlen(msg));
 32             count++;
 33             sleep(2);
 34         }
 35         close(pipe_fd[1]);
 36     }else{//father process read                                             
 37         close(pipe_fd[1]);//close write
 38
 39         char buf[1024];
 40         int count = 0;
 41         //while(1){
 42         while(count < 3){
 43             memset(buf,'\0',sizeof(buf)-1);
 44             read(pipe_fd[0],buf,sizeof(buf)-1);
 45             printf("child: %s\n",buf);
 46             count++;
 47         }
 48         close(pipe_fd[0]);
 49         sleep(5);
 50
 51         int status = 0;
 52         int ret = waitpid(id, &status,0);
 53         if(ret == id){
 54             printf("get sig: %d\n",status&0xff);
 55             printf("exit code: %d\n",(status>>8)&0xff);
 56         }
 57     }
 58     return 0;
 59 }  

4、管道分类

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

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

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

5、命名管道

       管道的一个不足之处是没有名字,因此,只能用于具有亲缘关系的进程间通信。在命名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。

       值得注意的是,FIFO(first input first output)总是按照先进先出的原则工作,第一个被写入的数据将首先从管道中读出。

命名管道的创建和读写

    两种方式创建命名管道:一是Shell下交互一个命名管道;二是在程序中使用系统函数建立命名管道。

    Shell方式下创建命名管道的系统函数有两个:mknod和mkffo。两个函数均定义在头文件#include<sys/stat.h>中。

函数原型为:

       #incldue<sys/types.h>                                                                                     #incldue<sys/types.h>

       #include<sys/stat.h>                                                                                        #include<sys/stat.h>

       #include<fcntl.h>                                                                                                         

       int mknod(const char* path,mode_t mod,dev_t dev);                                      int mkfifo(const char* path,mode_t mode);

       这函数调用成功都返回0,失败都返回-1.

       mknod和mkfifo这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,path指定了文件名,而mode为创建的命名管道的模式,则指定了文件的读写权限。dev为设备值,该值取决于文件创建的种类,它只在创建设备文件时才会用到。mknod是比较老的函数,而使用mkfifo函数更加简单和规范,所以建议在可能的情况下,尽量使用mkfifo而不是mknod。 

       mkfifo函数的作用是在文件系统中创建一个文件,该文件用于提供FIFO功能,即命名管道。 前边讲的那些管道都没有名字,因此它们被称为匿名管道,或简称管道。对文件系统来说, 匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。而命名管道是一个可见的文件,因此,它可以用于任何两个进程之间的通信,不管这两个进程是不 是父子进程,也不管这两个进程之间有没有关系。

下面实现两个进程通过命名管道进行通信:

下图中,左边为fifo read,右边为fifo write。


运行结果如下图所示:


      fifo read端中的“S_IFIFO|0666”指明创建一个命名管道且存取权限为0666,即为创建者、与创建者同组的用户、其他用户对该命名管道的访问权限都是读和写(这里要注意umask对生成的管道文件权限的影响)。

      命名管道创建后就可以使用了,其使用方法和管道基本是相同的。只是使用命名管道时,必须先调用open()函数将其打开。因为命名管道是设备文件,存储于硬盘上的文件,而管道则是存储于内存中的特殊文件。需要注意的是,调用open()打开命名管道的进程可能会被阻塞,但如果同时用读写方式 (O_RDWR)打开,则一定不会导致阻塞;如果以只读方式(O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写入数据的一方打开管道;同样以写方式(O_WRONLY)打开 也会阻塞直到有读取数据的一方打开管道。

6、流管道

        与linux的文件操作中有基于标准I/O操作一样,管道操作也支持基于文件流的操作,这种基于文件流的管道主要用来创建一个连接到另一个进程的管道,这里”另一个进程”是可以执行一定操作的可执行文件。例如用户执行“ls -l”或者./pipe,由于这类操作很常见,所以将一系列创建过程合并到一个函数popen()中完成。

        popen()函数会完成以下的步骤:创建一个管道;fork()一个子进程;在父子进程中关闭不需要的文件描述符;执行exec()函数族调用;执行函数中指定的命令。由此可见,这个函数可以大大减少代码的编写量,但使用不太灵活,不能自己创建管道那么灵活,并且popen()必须使用标准的I/o函数进行操作,也不能使read(),wirte()这种不带缓冲的I/O函数,必须使用pclose()来关闭管道流,该函数关闭标准I/O流,并等待命令执行结束。

7、管道的实现机制和管道的容量

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

       1)限制管道的大小。实际上,管道是一个固定大小的缓冲区。Linux中该缓冲区的大小为1页,即4K字节,使得它的大小不像文件那样不加检验地增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。

       2)读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。

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

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

如下图,查看自己平台下的管道的容量


由上图结果可知,管道的容量大小为65536=1024*64,即64k。

关于fcntl的相关知识,可查看:http://blog.csdn.net/bailyzheng/article/details/7463775

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值