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

来源:互联网 发布:pkpm节能计算软件 编辑:程序博客网 时间:2024/06/06 19:01

一、进程间通信概念

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

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


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

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

二、管道

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

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

1、无名管道

无名管道是一个与文件系统无关的内核对象,主要用于父子进程之间的通信,需要用专门的系统调用函数创建。
#include <unistd.h>int pipe(int pipefd[2]);返回值:若成功,返回 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)示例说明

//示例一#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <sys/stat.h>int main(){    int pipefd[2];    if (pipe (pipefd) == -1)    {        perror ("pipe");        exit (EXIT_FAILURE);    }    pid_t pid;    if((pid=fork())<0)    {        perror("fork");    }    else if(pid==0)    {        printf("这是子进程,pid=%d,",getpid());        printf("父进程的pid=%d\n",getppid());        if (close (pipefd[1]) == -1)        {            perror ("close");            exit (EXIT_FAILURE);        }                char text[20];        ssize_t readed = read (pipefd[0], text, 20);        if (readed == -1)        {            perror ("read");            exit (EXIT_FAILURE);        }        printf("%s\n", text);                if (close (pipefd[0]) == -1)        {            perror ("close");            exit (EXIT_FAILURE);        }    }    else    {sleep (1);        printf("这是父进程,pid=%d\n",getpid());                if (close (pipefd[0]) == -1)        {            perror ("close");            exit (EXIT_FAILURE);        }                ssize_t written = write (pipefd[1], "hello world", 12);        if (written == -1)        {            perror ("write");            exit (EXIT_FAILURE);        }                if (close (pipefd[1]) == -1)        {            perror ("close");            exit (EXIT_FAILURE);        }    }    return 0;}输出结果:这是子进程,pid=2799,父进程的pid=2798这是父进程,pid=2798hello world
//示例二#include <stdio.h>  #include <unistd.h>  #include <string.h>  #include <stdlib.h>    int main(void){  int result,n;  int fd[2];  pid_t pid;  char line[256];  if(pipe(fd) < 0){  perror("pipe");  return -1;  }  if((pid = fork()) < 0){  perror("fork");  return -1;  }else if(pid > 0){ //parent  close(fd[0]);  if(fd[1] != STDOUT_FILENO){  dup2(fd[1],STDOUT_FILENO);  }  execl("/bin/ls","ls",(char*)0);  }else{ //child  close(fd[1]);  while((n =read(fd[0],line,256)) > 0){  if(write(STDOUT_FILENO,line,n) != n){  perror("write");  exit(-1);  }  }  close(fd[0]);  }  return 0;  }  输出结果:a.outtest.ctest.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

#include<stdio.h>  FILE *popen(const char* cmdstring, const char *type);  //若成功则返回文件指针,出错则返回NULL。  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》示例说明

#include<stdio.h>    int main(void){  char line[256];  FILE* fpin;  int n;  if((fpin = popen("/bin/ls","r")) == NULL){  perror("popen");  return -1;  }    while(fgets(line, 256, fpin) != NULL){  if(fputs(line,stdout) == EOF){  perror("fputs");  return -1;  }  }  if(pclose (fpin) == -1){perror ("pclose");return -1;}return 0;  }  输出结果:a.outtest.ctest.c~

2、有名管道 

(1)有名管道简介

有名管道亦称 FIFO,是一种特殊的文件,它的路径名存在于文件系统中。通过 mkfifo 命令可以创建管道文件
//创建管道文件# mkfifo myfifo//在文件系统中,管道文件被显示成这样子# ls -la myfifo prw-r--r-- 1 root root 0 Jun  3 13:49 myfifo
查看 mkfifo --help
# mkfifo --help用法:mkfifo [选项]... 名称...以指定的名称创建先进先出文件(FIFO)。长选项必须使用的参数对于短选项时也是必需使用的。  -m, --mode=模式   设置权限模式(类似chmod),而不是rwxrwxrwx 减umask  -Z, --context=CTX  将每个创建的目录的SELinux 安全环境设置为CTX      --help显示此帮助信息并退出      --version显示版本信息并退出
可以看到创建管道时是可以添加权限的:
创建管道# mkfifo -m 0666 myfifo查看管道权限# ls -la myfifo prw-rw-rw- 1 root root 0 Jun  3 14:52 myfifo
即使是毫无亲缘关系的进程,也可以通过管道文件通信。
//在一个终端执行:# echo 'hello,FIFO!' > myfifo //在另一个终端执行:# cat myfifo hello,FIFO!
管道文件在磁盘上只有 i 节点没有数据块,也不保存数据。

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


(3)函数 mkfifo

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

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

下面介绍一下函数 mkfifo:

#include <sys/types.h>#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);返回值:成功返回 0,失败返回 -1.
《1》参数解析
pathname:文件路径名
mode:权限模式
《2》函数功能
创建有名管道文件
《3》示例说明
#include<stdio.h>  #include<sys/stat.h>  #include<fcntl.h>  #include<stdlib.h>  #define MYFIFO "myfifo"    int main(void){  char buffer[256];  pid_t pid;  int fd;  unlink(MYFIFO);  if(mkfifo(MYFIFO,0666) < 0){  perror("mkfifo");  return -1;  }    if((pid = fork())<0){  perror("fork");  return -1;  }else if(pid > 0){  char s[] = "hello world!";  fd = open(MYFIFO,O_RDWR);  write(fd,s,sizeof(s));  close(fd);  }else{  fd = open(MYFIFO,O_RDONLY);  read(fd,buffer,256);  printf("%s\n",buffer);  close(fd);  exit(0);  }  waitpid(pid,NULL,0);  return 0;  }  输出结果:hello world!

4、FIFO用途

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

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

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

产生的管道文件在磁盘上只有 i 节点没有数据块,不保存数据。
我们来查看一下管道文件类型:
# stat myfifo   文件:"myfifo"  大小:0         块:0          IO 块:4096   先进先出设备:801h/2049dInode:2128483     硬链接:1权限:(0666/prw-rw-rw-)  Uid:(    0/    root)   Gid:(    0/    root)最近访问:2017-06-03 14:52:53.952811041 +0800最近更改:2017-06-03 14:52:53.952811041 +0800最近改动:2017-06-03 14:52:53.952811041 +0800创建时间:-
值得注意的是:
当使用 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 用法时有用到的。
参看:C语言再学习 -- Xargs用法详解
一般形式:[命令1] | [命令2] | [命令3]
实例:
ls 命令查看  # ls  sh.sh  text.txt    可以可以指定查找脚本文件  # ls | grep *sh  sh.sh  



原创粉丝点击