IPC: 管道、命名管道(FIFO)

管道

1、概念

     管道是单向的、先进先出、无结构的字节流,它把一个进程的输出和另一个进程的输入连接在一起。

      写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。

      管道提供了简单的流控制机制。进程试图读一个空管道时,在数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中读走数据之前,写进程将一直阻塞。

2、管道的特点

      (1)单向数据通信,具有固定的读端和写端

      (2)只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)

      (3)管道所传输的是无格式的字节流,要求管道输入方与输出方事先约定好数据格式

      (4)管道的生命周期随进程,进程退出,文件会被操作系统回收

      (5)LINUX把管道看作是一种文件,采用文件管理的方法对管道进行管理,对于它的读写也可以使 

             用普通的read()和write()等函数。但是它不是普通的文件,并不属于其他任何文件系统,只

             存在于内核的内存空间中。

3、管道创建与关闭

      >管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fds[0]和fds[1]

      >fds[0]固定用于读管道,fds[1]固定用于写管道

      >管道关闭时只需将这两个文件描述符关闭即可,可使用普通的close()函数逐个关闭各个文件描述符

    

4、管道创建函数

              所需头文件 #include<unistd.h>
      函数原型 
int pipe(int fd[2])

     函数传入值

fd[2]用于保存管道的两个文件描述符,之后就可以直接操作这两个文件描述符

    

    函数返回值

成功:0
出错:-1

5、管道读写说明

    > 使用管道进行父子进程间通信的步骤:

     >创建管道:父进程调用pipe()函数创建一个管道

     >此时,管道的读端和写端都在一个进程之中,这种管道是没有多大用的。

     >父进程通过fork()函数创建一子进程

     >子进程会继承父进程所创建的管道,这时,父子进程中管道的文件描述符对应关系如图所示。

     >确定管道的传输方向:在父、子进程中根据需要的传输方向关闭无关的读端或写端文件描述符

     >通信:在写进程中调用write()函数,在读进程中调用read()函数

     >关闭管道:调用close()关闭管道相关的文件描述符。

   

        

       >关闭了父进程的写端fd[1]和子进程的读端fd[0],这样就可以建立一条“子进程写入父进程读取”

         的通道。

      > 也可以关闭父进程的读端fd[0]和子进程的写端fd[1],这样就可以建立一条“父进程写入子进程读

         取”的通道。

      >父进程还可以创建多个子进程,各个子进程都继承了相应的fd[0]和fd[1],这时,只需要关闭相应

         端 口就可以建立起各子进程(兄弟进程)之间的通道。

6、使用管道需要注意的4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志)

   (1)如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回 0,就像读到文件末尾一样。

   (2) 如果所有指向管道写端的文件描述符都没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

   (3)如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。

   (4)如果所有指向管道读端的文件描述符都没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

7、匿名管道的局限性

     >只支持单向数据流

     >只能用于具有亲缘关系的进程之间通信,没有名字

     >缓冲区有限,管道只存在于主存中,大小为一个页面

     >所传送的是无格式字节流

8、管道实例

      首先创建管道,之后父进程用fork函数创建出子进程,然后通过关闭父进程的读描述符和子进程的写描述符,建立起它们之间的管道通信。

#include <unistd.h> 

#include <sys/types.h> 
#include <errno.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 

#define MAX_DATA_LEN   256 

#define DELAY_TIME 1 
int main() 
{    
 pid_t pid;     
 int pipe_fd[2];   
 char buf[MAX_DATA_LEN];   
 const char data[] = "Hello bit";   
 int real_read, real_write;   
 memset((void*)buf, 0, sizeof(buf));    
 /* 创建管道 */   
 if (pipe(pipe_fd) < 0)    
 {        
  printf("pipe create error\n");      
  exit(1);   
 }     
 if ((pid = fork()) == 0)    
 {
  /* 子进程关闭写描述符,并通过使子进程暂停3s等待父进程已关闭相应的读描述符 */
  close(pipe_fd[1]);       
  sleep(3);                
  /* 子进程读取管道内容 */        
  if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)   
  {         
   printf("%d bytes read from the pipe is '%s'\n", real_read, buf);   
  }
  /* 关闭子进程读描述符 */
  close(pipe_fd[0]);         
  exit(0);     
 }
 else if (pid > 0)    
 {        
  /* 父进程关闭读描述符,并通过使父进程暂停1s等待子进程已关闭相应的写描述符 */             close(pipe_fd[0]);      
  sleep(1);       
  if((real_write = write(pipe_fd[1], data, strlen(data))) !=  -1)    
  {         
   printf("Parent wrote %d bytes : '%s'\n", real_write, data); 
  }              
  close(pipe_fd[1]);   
  /*关闭父进程写描述符*/     
  waitpid(pid, NULL, 0); /*收集子进程退出信息*/    
  exit(0);     
 }        
 return 0;  
}

运行结果:

[zr@localhost pipe]$ gcc -o pipe pipe.c

[zr@localhost pipe]$ ./pipe
Parent wrote 9 bytes : 'Hello bit'
9 bytes read from the pipe is 'Hello bit'
[zr@localhost pipe]$

命名管道

1、概念

       FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信,值得注意的是,FIFO总是按照先进先出的的原则工作,第一个被写入的数据将首先从管道中读出。

2、 命名管道的创建和使用

创建:

       Linux有两种方式创建命名管道:

        (1)在shell下交互地建立一个命名管道

            可使用mknod或mkfifo命令

        (2)在程序中使用系统函数建立命名管道

           创建命名管道的系统函数有两个:mknod和mkfifo, 函数原型如下:

      

        这两个函数都能创建一个真实存在于文件系统中的文件,filename指定了文件名,mode指定了文件的读取权限。尽量使用mkfifo(简单、规范)

       mkfifo作用是在文件系统中创建一个文件,该文件提供FIFO功能,即命名管道。对文件系统来说,匿名管道不可见,它的作用仅限于在父进程和子进程两个进程间通信,而命名管道是一个可见文件,因此它可用于任何两个进程间通信,不管两个进程间是不是父子进程,也不管两个进程间有没有关系。

使用方法:

      命名管道的使用方法与管道的使用方法基本相同,只是使用命名管道时,必须先调用open()将其打开。因为命名管道是存在于硬盘上的文件,而管道是存在于内存中的特殊文件。

     注意:调用open打开命名管道的进程可能会被阻塞。但如果同时用读写方式(O_RDWR)打开,则一定不会阻塞,如果以只读方式(O_RDONLY)打开,则调用open函数的进程将会被阻塞直到有写方打开管道;同时以写方式(O_WRONLY)打开也会阻塞直到有读方打开管道。