033 UNIX再学习 -- 进程间通信之管道

                                          
                                                                                   
                                                                                
                                           

一、进程间通信概念

首先,需要了解一下什么是进程间通信。
进程之间的相互通信的技术,称为进程间通信(InterProcess Communication,IPC)。

下图列出 4 种实现所支持的不同形式的 IPC。


之前进程间交换信息的方法只能是由 fork 或 exec 传送文件。
进程间通信 (IPC)方式有:
(1)管道
(2)消息队列
(3)信号量
(4)共享存储
(5)套接字
其中消息队列、信号量、共享存储统称为 XSI IPC通信方式

下面我们开始一一详细讲解:

二、管道

管道是 UNIX 系统 IPC 的最古老形式。所有 UNIX 系统都提供此种通信机制。管道有以下两种局限性。
(1)历史上,它们是半双工的(即数据只能在一个方向上流动)。现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们决不应预先假定系统支持全双工管道。
(2)管道只能在具有公共祖先的两个进程之间使用。通常,一个管道由一个进程创建,在进程调用 fork 之后,这个管道就能在父进程和子进程之间使用了。
尽管有这两种局限性,半双工管道仍是最常用的 IPC 形式。

其中管道又分为,有名管道 无名管道。

1、无名管道

无名管道是一个与文件系统无关的内核对象,主要用于父子进程之间的通信,需要用专门的系统调用函数创建。

    
    
  1. #include <unistd.h>
  2. int pipe(int pipefd[2]);
  3. 返回值:若成功,返回 0;若出错,返回 -1.

(1)函数功能

主要用于创建管道文件,利用参数返回两个文件描述符。
其中 pipefd[0] 用于从所创建的无名管道中读取数据,pipefd[1] 用于向该管道写入数据,pipefd[1] 的输出是 pipefd[0] 的输入。

(2)基于无名管道实现进程间通信的编程模型

《1》父进程调用 pipe 函数在系统内核中创建无名管道对象,并通过该函数的输出参数 pipefd,获得分别用于读写该管道的两个文件描述符 pipefd[0] 和 pipefd[1] 

《2》父进程调用 fork 函数,创建子进程。子进程复制父进程的文件描述符表,因此子进程同样持有分别用于读写该管道的两个文件描述符 pipefd[0] 和 pipefd[1]

《3》负责写数据的进程关闭无名管道对象的读端文件描述符 pipefd[0],而负责读数据的进程则关闭管道的写端文件描述符 pipefd[1]

《4》父子进程通过无名管道对象以半双工的方式传输数据。如果需要在父子进程间实现双向通信,较一般化的做法是创建两个管道,一个从父流向子,一个从子流向父

《5》父子进程分别关闭自己所持有的写端或读端文件描述符。在与一个无名管道对象相关联的所有文件描述符都被关闭以后,该无名管道对象即从系统内核中被销毁

(3)示例说明


    
    
  1. //示例一
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <stdlib.h>
  5. #include <fcntl.h>
  6. #include <string.h>
  7. #include <sys/stat.h>
  8. int main()
  9. {
  10.     int pipefd[ 2];
  11.     if (pipe (pipefd) == -1)
  12.     {
  13.         perror ( "pipe");
  14.         exit (EXIT_FAILURE);
  15.     }
  16.     pid_t pid;
  17.     if((pid=fork())< 0)
  18.     {
  19.         perror( "fork");
  20.     }
  21.     else if(pid== 0)
  22.     {
  23.         printf( "这是子进程,pid=%d,",getpid());
  24.         printf( "父进程的pid=%d\n",getppid());
  25.         if (close (pipefd[ 1]) == -1)
  26.         {
  27.             perror ( "close");
  28.             exit (EXIT_FAILURE);
  29.         }
  30.        
  31.         char text[ 20];
  32.         ssize_t readed = read (pipefd[ 0], text, 20);
  33.         if (readed == -1)
  34.         {
  35.             perror ( "read");
  36.             exit (EXIT_FAILURE);
  37.         }
  38.         printf( "%s\n", text);
  39.        
  40.         if (close (pipefd[ 0]) == -1)
  41.         {
  42.             perror ( "close");
  43.             exit (EXIT_FAILURE);
  44.         }
  45.     }
  46.     else
  47.     {
  48.   sleep ( 1);
  49.         printf( "这是父进程,pid=%d\n",getpid());
  50.        
  51.         if (close (pipefd[ 0]) == -1)
  52.         {
  53.             perror ( "close");
  54.             exit (EXIT_FAILURE);
  55.         }
  56.        
  57.         ssize_t written = write (pipefd[ 1], "hello world", 12);
  58.         if (written == -1)
  59.         {
  60.             perror ( "write");
  61.             exit (EXIT_FAILURE);
  62.         }
  63.        
  64.         if (close (pipefd[ 1]) == -1)
  65.         {
  66.             perror ( "close");
  67.             exit (EXIT_FAILURE);
  68.         }
  69.     }
  70.     return 0;
  71. }
  72. 输出结果:
  73. 这是子进程,pid= 2799,父进程的pid= 2798
  74. 这是父进程,pid= 2798
  75. hello world

    
    
  1. //示例二
  2. #include <stdio.h> 
  3. #include <unistd.h> 
  4. #include <string.h> 
  5. #include <stdlib.h> 
  6.  
  7. int main(void)
  8.   int result,n; 
  9.   int fd[ 2]; 
  10.   pid_t pid; 
  11.   char line[ 256]; 
  12.   if(pipe(fd) < 0){ 
  13.   perror( "pipe"); 
  14.    return -1
  15.  } 
  16.   if((pid = fork()) < 0){ 
  17.   perror( "fork"); 
  18.    return -1
  19.  } else if(pid > 0){ //parent 
  20.   close(fd[ 0]); 
  21.    if(fd[ 1] != STDOUT_FILENO){ 
  22.    dup2(fd[ 1],STDOUT_FILENO); 
  23.   } 
  24.   execl( "/bin/ls", "ls",( char*) 0); 
  25.  } else{ //child 
  26.   close(fd[ 1]); 
  27.    while((n =read(fd[ 0],line, 256)) > 0){ 
  28.     if(write(STDOUT_FILENO,line,n) != n){ 
  29.     perror( "write"); 
  30.      exit( -1); 
  31.    } 
  32.   } 
  33.   close(fd[ 0]); 
  34.  } 
  35.   return 0
  36. 输出结果:
  37. a.out
  38. test.c
  39. test.c~

(4)示例解析

创建了一个从父进程到子进程的管道,将父进程的读关闭,子进程的写关闭。使得父进程经由该管道想子进程传送数据。 管道方向如下:

当管道的一端被关闭后,下列两条规则其作用:
(1)当读(read)一个写端已经被关闭的管道时,在所有数据都被读取后,read 返回 0,表示文件结束。
(2)如果写(write)一个读端已经被关闭的管道,则产生信号 SIGPIPE。如果忽略该信号或者捕获该信号并从其处理程序返回,则 write 返回 -1,errno 设置为 EPIPE。
在写管道(或FIFO)时,常量 PIPE_BUF 规定了内核中管道缓冲区的大小,如果对管道调用 write,而且要求写的字节数小于等于 PIPE_BUF,则此操作不会与其他进程对同一管道(或FIFO)的 write 操作交叉进行。但是,若有多个进程同时写一个管道(或FIFO),而且我们要求写的字节数超过 PIPE_BUF 字节数时,那么我们所写的数据可能会与其他进程所写的数据相互交叉。用 pathconf 或 fpathconf 函数可以确定 PIPE_BUF 的值

(5)函数 popen 和 pclose


    
    
  1. #include<stdio.h> 
  2. FILE *popen(const char* cmdstring, const char *type)//若成功则返回文件指针,出错则返回NULL。 
  3. int pclose(FILE *fp); //返回cmdstring的终止状态,若出错则返回-1。 
《1》函数解析
常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据,为此,标准 I/O 库提供了两个函数 popen 和 pclose。这两个函数实现的操作是:创建一个管道,fork 一个子进程,关闭未使用的管道端,执行一个 shell 运行命令,然后等待命令终止。
《2》函数使用
函数 popen 先执行 fork,然后调用 exec 执行 cmdstring,并且返回一个标准 I/O 文件指针。如果 type 是“r”,则文件指针连接到 cmdstring 的标准输出。如果 type 是“w”,则文件指针连接到 cmdstring 的标准输入。


pclose 函数关闭标准 I/O 流,等待命令终止,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose 返回的终止状态与 shell 已执行 exit (127) 一样。
cmdstring 由 Bourbe shell 以下列方式执行:
sh -c cmdstring
这表示 shell 将扩展 cmdstring 中的任何特殊字符。例如,可以使用:
fp = popen ("ls *.c", "r");
或者
fp = popen ("cmd 2>$1", "r");
《3》示例说明

    
    
  1. #include<stdio.h> 
  2.  
  3. int main(void)
  4.   char line[ 256]; 
  5.  FILE* fpin; 
  6.   int n; 
  7.   if((fpin = popen( "/bin/ls", "r")) == NULL){ 
  8.   perror( "popen"); 
  9.    return -1
  10.  } 
  11.  
  12.   while(fgets(line, 256, fpin) != NULL){ 
  13.    if( fputs(line, stdout) == EOF){ 
  14.    perror( "fputs"); 
  15.     return -1
  16.   } 
  17.  } 
  18.   if(pclose (fpin) == -1)
  19.  {
  20.   perror ( "pclose");
  21.    return -1;
  22.  }
  23.   return 0
  24. 输出结果:
  25. a.out
  26. test.c
  27. test.c~

2、有名管道 

(1)有名管道简介

有名管道亦称 FIFO,是一种特殊的文件,它的路径名存在于文件系统中。通过 mkfifo 命令可以创建管道文件

    
    
  1. //创建管道文件
  2. # mkfifo myfifo
  3. //在文件系统中,管道文件被显示成这样子
  4. # ls -la myfifo
  5. prw-r--r-- 1 root root 0 Jun  3 13: 49 myfifo
查看 mkfifo --help

     
     
  1. # mkfifo --help
  2. 用法:mkfifo [选项]... 名称...
  3. 以指定的名称创建先进先出文件(FIFO)。
  4. 长选项必须使用的参数对于短选项时也是必需使用的。
  5.   -m, --mode=模式    设置权限模式(类似chmod),而不是rwxrwxrwx 减umask
  6.   -Z, --context=CTX   将每个创建的目录的SELinux 安全环境设置为CTX
  7.       --help  显示此帮助信息并退出
  8.       --version  显示版本信息并退出
可以看到创建管道时是可以添加权限的:

     
     
  1. 创建管道
  2. # mkfifo -m 0666 myfifo
  3. 查看管道权限
  4. # ls -la myfifo
  5. prw-rw-rw- 1 root root 0 Jun  3 14: 52 myfifo
即使是毫无亲缘关系的进程,也可以通过管道文件通信。

    
    
  1. //在一个终端执行:
  2. # echo 'hello,FIFO!' > myfifo
  3. //在另一个终端执行:
  4. # cat myfifo
  5. hello,FIFO!
管道文件在磁盘上只有 i 节点没有数据块,也不保存数据。

(2)基于有名管道实现进程间通信的逻辑模型


(3)函数 mkfifo

有名管道不仅可以用于 shell 命令,也可以在代码中使用。
shell编程之前讲过了,参看:UNIX再学习 -- shell编程
基于有名管道实现进程间的通信的编程模型:

其中除了 mkfifo 函数时专门针对有名管道的,其它函数都与操作普通文件没有任何差别。
有名管道是文件系统的一部分,如不删除,将一直存在。
下面介绍一下函数 mkfifo:

    
    
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. int mkfifo(const char *pathname, mode_t mode);
  4. 返回值:成功返回 0,失败返回 -1.
《1》参数解析
pathname:文件路径名
mode:权限模式
《2》函数功能
创建有名管道文件
《3》示例说明

    
    
  1. #include<stdio.h> 
  2. #include<sys/stat.h> 
  3. #include<fcntl.h> 
  4. #include<stdlib.h> 
  5. #define MYFIFO "myfifo" 
  6.  
  7. int main(void)
  8.   char buffer[ 256]; 
  9.   pid_t pid; 
  10.   int fd; 
  11.  unlink(MYFIFO); 
  12.   if(mkfifo(MYFIFO, 0666) < 0)
  13.  { 
  14.   perror( "mkfifo"); 
  15.    return -1
  16.  } 
  17.  
  18.   if((pid = fork())< 0)
  19.  { 
  20.   perror( "fork"); 
  21.    return -1
  22.  }
  23.   else if(pid > 0)
  24.  { 
  25.    char s[] = "hello world!"
  26.   fd = open(MYFIFO,O_RDWR); 
  27.   write(fd,s, sizeof(s)); 
  28.   close(fd); 
  29.  }
  30.   else
  31.  { 
  32.   fd = open(MYFIFO,O_RDONLY); 
  33.   read(fd,buffer, 256); 
  34.    printf( "%s\n",buffer); 
  35.   close(fd); 
  36.    exit( 0); 
  37.  } 
  38.  waitpid(pid, NULL, 0); 
  39.   return 0
  40. 输出结果:
  41. hello world!

4、FIFO用途

FIFO有以下两种用途:
(1)shell 命令使用 FIFO 将数据从一条管道传送到另一条时吗,无需创建中间临时文件。
(2)客户进程-服务器进程应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程二者之间传递数据。

3、有名管道和无名管道区别

讲了这么多,我们来看看两者的区别。
根据基于无名/有名管道实现进程间通信的逻辑模型我们可以得出:
若管道对象在使用时内核产生,不使用时就不产生时,那么这一定是无名管道;若在使用时内核中产生了一个管道文件,且不使用时还于内核中存在,那么往往是有名管道。
(1)无名管道特点
《1》只能用于具有亲缘关系的进程之间通信(父子进程或者兄弟进程)。
《2》是一个单工(半双工)的通信模式,具有固定的读写端。
《3》每次使用都需要创建管道对象。
(2)有名管道特点
《1》可以在互不相关的进程之间实现通信。
《2》该管道是通过路径名来指出,在文件系统中是可以看到的,在建立管道后可以当做普通文件来使用读写操作。
《3》严格遵循先进先出的规则,对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。且不支持如 lseek()等文件定位操作。

产生的管道文件在磁盘上只有 i 节点没有数据块,不保存数据。
我们来查看一下管道文件类型:

    
    
  1. # stat myfifo
  2.   文件: "myfifo"
  3.   大小: 0          块: 0          IO 块: 4096   先进先出
  4. 设备: 801h/ 2049d Inode: 2128483     硬链接: 1
  5. 权限:( 0666/prw-rw-rw-)  Uid:(    0/    root)   Gid:(    0/    root)
  6. 最近访问: 2017 -06 -03 14: 52: 53.952811041 + 0800
  7. 最近更改: 2017 -06 -03 14: 52: 53.952811041 + 0800
  8. 最近改动: 2017 -06 -03 14: 52: 53.952811041 + 0800
  9. 创建时间:-
值得注意的是:
当使用 open() 来打开 FIFO 文件时,O_NONBLOCK 旗标会有影响
1、当使用O_NONBLOCK 旗标时,打开 FIFO 文件来读取的操作会立刻返回,但是若还没有其他进程打开 FIFO 文件来读取,则写入的操作会返回 ENXIO 错误代码。 
2、没有使用 O_NONBLOCK 旗标时,打开 FIFO 来读取的操作会等到其他进程打开 FIFO 文件来写入才正常返回。同样地,打开 FIFO 文件来写入的操作会等到其他进程打开 FIFO 文件来读取后才正常返回。
类似于管道,若用 write 写一个尚无进程为读而打开的 FIFO,则产生信号 SIGPIPE。若某个 FIFO 的最后一个写进程关闭了 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。

4、linux下shell编程之管道

在 Linux 下我们可以采用管道操作符 “|”来连接多个命令或进程,在连接的管道线两边,每个命令执行时都是一个独立的进程。前一个命令的输出正是下一个命令的输入。这些进程可以同时进行,而且随着数据流在它们之间的传递可以自动地进行协调,从而能够完成较为复杂的任务。管道我们也并不陌生,之前讲 xargs 用法时有用到的。
一般形式:[命令1] | [命令2] | [命令3]
实例:

    
    
  1. ls 命令查看 
  2. # ls 
  3. sh.sh  text.txt 
  4.  
  5. 可以可以指定查找脚本文件 
  6. # ls | grep *sh 
  7. sh.sh 



                                   
                                   
               
                   
  •                                                
  •                                                     点赞                         2                        
  •                        
  •                                                     收藏
  •                        
  •                                                     分享
  •                                                                                                                        
  •                                                        
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值