Linux进程间通信之管道

来源:互联网 发布:淘宝刷佣金单有风险吗 编辑:程序博客网 时间:2024/06/02 01:08

IPC是进程间通信(interprocess communication)的简称。我们在这里对有关管道的知识进行总结。
我们将介绍三种` 管道:

  • 管道(pipe)。管道是第一个广泛使用的IPC形式,既可在程序中使用,也可以从shell中使用。管道只能在具有共同祖先(指父子进程关系)的进程间使用。
  • FIFO。FIFO是管道概念的一个变体,FIFO(First In First Out,先进先出),也叫有名管道。可以用于任意进程间通信。
  • socketpair。socket编程接口提供的一个创建全双工管道的系统调用。也被称为流管道。

管道:

管道只能用于具有相关关系的进程之间通信,所谓的相关关系,即拥有血缘关系的进程,具有共同祖先。
管道是一个字节流,意味着在使用管道时是不存在消息或消息边界的概念的。从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。此外,通过管道传递的数据是顺序的—-从管道中读取出来的字节的顺序与它们被写入管道的顺序是完全一样的。
在管道中数据的传递方向是单向的。管道的一端用于写入,另一端则用于读取。
管道的容量是有限的。一旦管道被填满之后,后续向该管道的写入操作就会被阻塞直到读者从管道中移除了一些数据为止。

函数原型:

#include<unistd.h>int pipe(int filedes[2]);


说明:

成功的pipe()调用会在数组filedes中返回两个打开的文件描述符:一个表示管道的读取端(filedes[0]),另一个表示管道的写入端(filedes[1])。
与所有文件描述符一样,可以使用read()和write()系统调用在管道上执行I/O。
管道是父进程和子进程间通信的常用手段。管道能在父,子进程间传递数据,利用的是fork调用之后两个管道文件描述符两个都保持打开。

使用pipe()创建完管道之后的情况,如图:
这里写图片描述

调用fork()之后,如图:
这里写图片描述

关闭未使用的文件描述符后:
这里写图片描述

虽然父进程和子进程都可以从管道中读取和写入数据,但这种做法并不常见。因此,在fork()调用之后,其中一个进程应该立即关闭管道的写入端的描述符,另一个则应该关闭读取端的描述符。比如,如上图所示,如果父进程需要向子进程传输数据,那么它就会关闭管道的读取端的描述符,而子进程就会关闭管道的写入端的描述符。

显然,如果要实现父,子进程之间的双向数据传输,就必须使用两个管道:创建两个管道,在两个进程之间发送数据的两个方向上各使用一个。

程序示例:

使用管道将数据从父进程传输到子进程。

int filedes[2];if(pipe(filedes)==-1)   //创建管道    errExit("pipe");switch(fork()){    //创建子进程case -1:    errExit("fork");case 0:   //child    if(close(filedes[1])==-1)   //关闭子进程管道写端        errExit("close");    break;default:    //parent    if(close(filedes[0])==-1)  //关闭父进程管道读端        errExit("close");    break;}


补充:

需要注意的是,当使用两个管道,实现父子进程双向通信时,要注意会发生死锁的情况。因为创建的管道描述符默认是阻塞的。如果两个进程都试图从空管道中读取数据或尝试向已满的管道中写入数据就可能会发生死锁。

关闭未使用的管道文件描述符不仅仅是为了确保进程不会耗尽其文件描述符的限制——-这对于正确使用管道是非常重要的。
当一个管道的所有的写入端都被关闭时,管道的读端调用recv时,recv会返回0,表示文件结束。
如果读取进程没有关闭管道的写入端,那么在其他进程关闭了写入描述符之后,读者也不会看到文件结束,即使它读完了管道中的所有数据,read()也会阻塞以等待数据,这是因为内核知道至少还存在一个管道的写入描述符打开着,即读取进程自己打开了这个描述符。
同理,如果写入进程没有关闭管道的读取端,那么即使在其他进程已经关闭了管道的读取端之后写入进程仍然能够向管道写入数据,最后写入进程会将数据充满整个管道,后续的写入请求会被永远阻塞。



FIFO:

管道没有名字,因此它们的最大劣势是只能用于有一个共同祖先进程的各个进程之间。
FIFO与管道类似,它们两者之间最大的差别在于FIFO在文件系统中拥有一个名称,并且其打开方式与打开一个普通文件是一样的。这样就能够将FIFO用于非相关进程之间的通信。它是一个单向(半双工)数据流。FIFO由mkfifo函数创建。

#include<sys/types.h>#include<sys/stat.h>int mkfifo(const char *pathname,mode_t mode);

成功则返回0,出错则返回-1。
其中pathname是路径名,它是该FIFO的名字。
mode参数指定文件权限位,类似于open的第二个参数。
mkfifo函数已隐含指定O_CREAT | O_EXCL。也就是说,它要么创建一个新的FIFO,要么返回一个EEXIST错误(如果所指定名字的FIFO已经存在)。如果不希望创建一个新的FIFO,那就改为调用open而不是mkfifo。
在创建出一个FIFO后,它必须或者打开来读,或者打开来写,所用的可以是open函数,也可以是某个标准I/O打开函数,例如fopen。FIFO不能打开来既读又写,因为它是半双工的。

FIFO与管道的例子变动如下:

  • 创建并打开一个管道只需要调用pipe。创建并打开一个FIFO则需在调用mkfifo之后再调用open。
  • 管道在所有进程最终都关闭它之后自动消失。FIFO的名字则只有通过调用unlink才从文件系统删除。


代码示例:

同pipe一样,FIFO是单向的。如果想要实现两个进程之间双向通信,则需要创建两个FIFO。其中的一个进程打开第一个FIFO来写,打开第二个FIFO来读。另一个进程打开第一个FIFO来读,打开第二个FIFO来写。下面的代码是父进程与子进程之间通过FIFO实现双向通信的代码示例,无血缘关系的进程之间同样可以使用FIFO进行通信,方法步骤都是一样的。

#define FIFO1   "/tmp/fifo.1"#define FIFO2   "/tmp/fifo.2"//允许用户读,用户写,组成员读和其他用户读#define FILE_MODE  (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)int main(){    int readfd,writefd;    pid_t childpid;    if((mkfifo(FIFO1,FILE_MODE)<0)&&(errno!=EEXIST))        err_sys("can't create %s ",FIFO1);    if((mkfifo(FIFO2,FILE_MODE)<0)&&(errno!=EEXIST))    {        unlink(FIFO1);        err_sys("can't create %s",FIFO2);    }    if((childpid = fork())==0)    {        readfd=open(FIFO1,O_RDONLY,0);        writefd=open(FIFO2,O_WRONLY,0);        //......        exit(0);    }    writefd=open(FIFO1,O_WRONLY,0);    readfd=open(FIFO2,O_RDONLY,0);    //.....    waitpid(childpid,NULL,0);    close(readfd);    close(writefd);    unlink(FIFO1);    unlink(FIFO2);}


创建互联socket对:socketpair()

socket编程接口提供了一个创建全双工的管道的系统调用:socketpair。

#include<sys/socket.h>int socketpair(int domain,int type,int protocol,int sockfd[2]);

socketpair()系统调用只能用在UNIX domain中,即domain参数必须被指定为AF_UNIX。protocol参数必须指定为0。
将type参数指定为SOCK_STREAM,创建一个双向管道(也被成为流管道)。
sockfd数组返回了引用这两个相互连接的socket的文件描述符。
每个socket都可以用来读取和写入,并且这两个socket之间每个方向上的数据信道是分开的。这对套接字可以进行双工通信,每一个描述符既可以读也可以写。这个在同一个进程中也可以进行通信,向sockfd[0]中写入,就可以从sockfd[1]中读取(只能从sockfd[1]中读取),也可以在sockfd[1]中写入,然后从sockfd[0]中读取;但是,若没有在0端写入,而从1端读取,则1端的读取操作会阻塞。
在Linux中,完全可以把这一对socket当成pipe返回的文件描述符一样使用,唯一的区别就是这一对文件描述符中的任何一个都可读和可写。


代码示例:

其实socketpair的使用和pipe的使用完全类似,只不过socketpair创建的是全双工的管道,是双向的。比如父子进程之间通信,调用socketpair创建一个全双工管道,然后调用fork()函数。子进程会拷贝这一对文件描述符。此时,我们可以规定让父进程使用sockfd[0]这个描述符,在父进程中关闭sockfd[1]这个描述符。在子进程中关闭sockfd[0]这个描述符。这样,不管读还是写,父进程只需要通过对sockf[0]这个文件描述符进行I/O操作,就可以完成。

/*************************************************************************> File Name: 1.c> Author:fengxin > Mail:903087053@qq.com > Created Time: 2017年07月29日 星期六 10时59分08秒************************************************************************//*  *进程双向通信  */  #include<stdio.h>  #include<string.h>  #include<sys/types.h>  #include<stdlib.h>  #include<unistd.h>  #include<sys/socket.h>  int main()     {      int sv[2];    //一对无名的套接字描述符      if(socketpair(PF_LOCAL,SOCK_STREAM,0,sv) < 0)   //成功返回零 失败返回-1      {          perror("socketpair");          return 0;      }      pid_t id = fork();        //fork出子进程      if(id == 0)               //子进程      {          close(sv[0]); //在子进程中关闭第一个描述符,使用第二个描述符          const char* msg = "我是孩子\n";          char buf[1024];          while(1)          {              write(sv[1],msg,strlen(msg));              sleep(1);              ssize_t _s = read(sv[1],buf,sizeof(buf)-1);              if(_s > 0)              {                  buf[_s] = '\0';                  printf(" %s\n",buf);              }          }      }      else   //父亲      {          close(sv[1]);//父进程关闭第二个描述符 ,使用第一个描述符        const char* msg = "我是父亲\n";          char buf[1024];          while(1)          {              ssize_t _s = read(sv[0],buf,sizeof(buf)-1);              if(_s > 0)              {                  buf[_s] = '\0';                  printf(" %s\n",buf);                  sleep(1);              }              write(sv[0],msg,strlen(msg));          }      }      return 0;  }  
原创粉丝点击