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

一、进程间通信概念

首先,需要了解一下什么是进程间通信。
进程之间的相互通信的技术,称为进程间通信(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=2798
hello 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.out
test.c
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

#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.out
test.c
test.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/2049d	Inode: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 用法时有用到的。
一般形式:[命令1] | [命令2] | [命令3]
实例:
ls 命令查看  
# ls  
sh.sh  text.txt  
  
可以可以指定查找脚本文件  
# ls | grep *sh  
sh.sh  



  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聚优致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值