进程间通信-----管道
管道
首先了解一下进程通信是什么?
- Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
- 在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
① 管道 (使用最简单)
② 信号 (开销最小)
③ 共享映射区 (无血缘关系)
④ 本地套接字 (最稳定)
管道的概念:
- 管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道。有如下特质:
- 其本质是一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端。
- 规定数据从管道的写端流入管道,从读端流出。
- 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
- 管道的局限性:
① 数据不能进程自己写,自己读。
② 管道中数据不可反复读取。一旦读走,管道中不再存在。
③ 采用半双工通信方式,数据只能在单方向上流动。
④ 只能在有公共祖先的进程间使用管道。 - 常见的通信方式有,单工通信、半双工通信、全双工通信。
管道可以分为匿名管道和命名管道
匿名管道
#include <unistd.h>
功能:创建无名管道
原型 int pipe(int fd[2]);
参数 fd:⽂文件描述符数组,其中 fd[0] 表⽰示读端, fd[1] 表⽰示写端
返回值:成功返回0,失败返回错误代码
- 来看一个从键盘中输入,写入管道,读取管道,再从管道中读取到屏幕上
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 int main( void )
6 {
7 int fds[2];
8 char buf[100];
9 int len;
10 if ( pipe(fds) == -1 )
11 perror("make pipe"),exit(1);
12 //read from stdin
13 while ( fgets(buf, 100, stdin) ) {
14 len = strlen(buf);
15 // write into pipe
16 if ( write(fds[1], buf, len) != len ) {
17 perror("write to pipe");
18 break;
19 }
20 memset(buf, 0x00, sizeof(buf));
21 // read from pipe
22 if ( (len=read(fds[0], buf, 100)) == -1 ) {
23 perror("read from pipe");
24 break;
25 }
26 // write to stdout
27 if ( write(1, buf, len) != len ) {
28 perror("write to stdout");
29 break;
30 }
31 }
32 }
- 我们在站在文件描述符角度,添加进程看看管道。
- 函数调用成功返回 r/w 两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
- 管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
- 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
- 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
- 我们来看代码,程序创建一个从父进程到子进程的管道,并且父进程经由该管道向子进程传送数据。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 int main()
5 {
6 int fd[2];
7 int pid;
8
9 int ret = pipe(fd);
10 if(ret < 0)
11 {
12 perror("pipe error");
13 return -1;
14 }
15 pid = fork();
16 if(pid < 0)
17 {
18 perror("fork error");
19 return -1;
20 }else if(pid > 0)
21 {
22 close(fd[0]);
23 char* buf = "hello world!!\n";
24 write(fd[1], buf, strlen(buf));
25 }else
26 {
27 close(fd[1]);
28 int line[1024] = {0};
29 int n = read(fd[0],line,1024);
30 write(STDOUT_FILENO,line,n);
31 }
32
33 return 0;
34 }
- 在上面的例子中,直接对管道描述符调用了read和write。更有趣的是将管道描述符复制到了标准输入或标准输出上。通常会在此执行另一个程序,该程序或者标准输入(已经创建的管道)读数据,或者将数据写至其标准输出(该管道)。
管道读写规则
- 当没有数据可读时
- O_NONBLOCK disable:read调⽤阻塞,即进程暂停执⾏,⼀一直等到有数据来到为⽌。
- O_NONBLOCK enable:read调用返回 -1,errno 值为EAGAIN。
- 当管道满的时候
- O_NONBLOCK disable: write调⽤阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
- 如果所有管道写端对应的⽂件描述符被关闭,则read返回 0。
- 如果所有管道读端对应的文件描述符被关闭,则 write 操作会产⽣信号SIGPIPE,进⽽可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写⼊的原子性。(原子性:一步操作)
管道特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务。(数据的流式服务:体现的是数据的发送和接收的灵活性,但是数据没有边界,容易沾包(多条数据连接到一起了,接收方没有办法分辨数据界限))。
- 一般⽽言,进程退出,管道释放,所以管道的生命周期随进程的结束而结束。
- 一般⽽言,内核会对管道操作进行同步与互斥
- 同步:保证一个操作访问的时序性(我操作完了,你再操作)
- 互斥:保证操作的同一时间唯一访问(我操作的时候,你不能操作)
- 管道是半双工的,数据只能向⼀个⽅向流动;需要双⽅通信时,需要建立起两个管道。
我在演示一下 ps -ef | grep ssh 这条命令中的管道
1 #include<stdio.h>
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5
6 int main()
7 {
8 int pid = -1;
9 int pipefd[2] = {0};
10
11 if(pipe(pipefd) < 0)
12 {
13 perror("pipe error");
14 return -1;
15 }
16 pid = fork();
17 if(pid < 0)
18 {
19 perror("fork error");
20 return -1;
21 }
22 else if(pid == 0)
23 {
24 dup2(pipefd[0],0);
25 close(pipefd[1]);
26 execl("/bin/grep","grep","ssh",NULL);
27 }
28 else
29 {
30 dup2(pipefd[1],1);
31 close(pipefd[0]);
32 execl("/bin/ps","ps","-ef",NULL);
33 }
34 return 0;
35 }
命名管道
- 管道应⽤的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO⽂件来做这项工作,它经常被称为命名管道。命名管道是⼀种特殊类型的⽂件。
- 命名管道可以从命令行上创建,命令行方法是使用下面这个命令
- :
mkfifo filename
- 大家可以看到创建出来的是管道文件。
上图是我们将“hello world”写入到管道中,在打开一个shell,我们就可以将管道中的数据拿到。
- 系统也提供了创建命名管道的函数
int mkfifo(const char *filename,mode_t mode);
int main(int argc, char *argv[])
{
mkfifo("test", 0644);
return 0;
}
命名管道和匿名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由 mkfifo 函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯⼀一的区别在它们创建与打开的⽅方式不同,一但这些⼯作完成之后,它们具有相同的语义。
命名管道打开规则
- 如果当前打开操作是为读而打开 FIFO 时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该 FIFO
- O_NONBLOCK enable:立刻返回成功
- 如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
- 输入管道数据的一方
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<errno.h>
4 #include<fcntl.h>
5 #include<string.h>
6
7 int main()
8 {
9 char* file = "./test.fifo";
10 umask(0);
11 if( mkfifo(file,0664) < 0)
12 {
13 if(errno == EEXIST)
14 {
15 printf("fifo exit!!\n");
16 }
17 else
18 {
19 perror("mkfifo");
20 return -1;
21 }
22 }
23
24 int fd = open(file, O_WRONLY);
25 if(fd < 0)
26 {
27 perror("open error");
28 return -1;
29 }
30 printf("open fifo success!!\n");
31 while(1)
32 {
33 char buf[1024] = {0};
34 printf("please input:");
35 fflush(stdout);
36 scanf("%s",buf);
37 write(fd,buf,strlen(buf));
38 }
39 return 0;
40 }
- 读取管道数据的一方
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<errno.h>
4 #include<fcntl.h>
5 #include<string.h>
6
7 int main()
8 {
9 char* file = "./test.fifo";
10 umask(0);
11 if( mkfifo(file,0664) < 0)
12 {
13 if(errno == EEXIST)
14 {
15 printf("fifo exit!!\n");
16 }
17 else
18 {
19 perror("mkfifo");
20 return -1;
21 }
22 }
23
24 int fd = open(file, O_RDONLY);
25 if(fd < 0)
26 {
27 perror("open error");
28 return -1;
29 }
30 printf("open fifo success!!\n");
31
32 while(1)
33 {
34 char buff[1024];
35 memset(buff,0x00,1024);
36 int ret = read(fd,buff,1024);
37 if(ret > 0)
38 {
39 printf("peer say:%s\n",buff);
40 }
41 }
42 close(fd);
43
44 return 0;
45 }
上图是往管道中输入数据的一方,我们在打开一个shell,下图就是从管道中读取数据的一方。