0%

进程通信-管道

阅读更多

1 前言

管道分为两种:匿名管道与命名管道。匿名管道仅用于具有亲缘关系的父子进程之间,而命名管道用于任意两个进程之间

2 匿名管道

匿名管道是一种最基本的IPC机制,由pipe函数创建:

1
2
#include<unistd.h>
int pipe(int pipefd[2]);

调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过pipefd参数传出给用户程序两个文件描述符,pipefd[0]指向管道的读端,fpipefd1]指向管道的写端

所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1

2.1 管道通信的原理

开辟了管道之后如何实现两个进程间的通信呢?

  1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端
  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道
  3. 父进程关闭管道读端,子进程关闭管道写端(当然也可以反过来,反正一个读一个写)。父进程可以往管道里写,子进程可以从管道里读。管道是用环形队列实现的,数据从写端流入,从读端流出,这样就实现了进程间通信

因为管道是单向通信的,即单工,所以父子进程必须关闭它们各自不需要的端。其次,匿名管道是通过子进程继承父进程的文件描述符表才得以实现父子进程共同看到一份资源,所以匿名管道也就只能在有亲缘关系的进程间实现通信。

2.2 匿名管道的特点

  1. 单向通信
  2. 具有亲缘关系的进程间通信
  3. 管道生命周期随进程(管道文件描述符在进程结束后被关闭)
  4. 面向字节流的服务
  5. 底层实现的同步机制,无需用户在考虑(为空不允许读,为满不允许写(阻塞))

2.3 细节

  1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样
  2. 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回
  3. 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止
  4. 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回

2.4 C源码

pipe.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>  
#include <unistd.h>
#include <string.h>

int main()
{
int fd[2];
if (pipe(fd))
{
perror("pipe");
return 1;
}

//实现父进程写,子进程读
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 2;
}
else if (id == 0) //child
{
close(fd[1]);

char buf[128];
int cnt = 0;
while (cnt++ < 5)
{
ssize_t _s = read(fd[0], buf, sizeof(buf));
if (_s > 0)
{
buf[_s] = '\0';;
printf("father say to child: %s\n", buf);
}
else if (_s == 0)
{
printf("father close write");
break;
}
else
{
perror("read");
break;
}
}

close(fd[0]);
}
else //father
{
close(fd[0]);

char * msg = "hello world";
int cnt = 0;
while (cnt++ < 5)
{
write(fd[1], msg, strlen(msg));
sleep(1);
}

close(fd[1]);
}

return 0;
}

3 命名管道

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

3.1 命名管道的使用

创建命名管道的方式无非也就是那两种:命令和函数。而且命令和函数对应的名字是一样的,mkfifo(mknod)命令/函数

命名管道创建后就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命名管道时,必须先调用open()将其打开因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。需要注意的是,调用open()打开命名管道的进程可能会被阻塞。但如果同时用读写方式(O_RDWR)打开,则一定不会导致阻塞;如果以只读方式(O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;同样以写方式(O_WRONLY)打开也会阻塞直到有读方式打开管道

命名管道与匿名管道不同的地方在于即使没有亲缘关系,也可以通过FIFO来通信,且管道的生命周期不再是随进程,因为即使命名管道文件描述符被关闭,FIFO依然存在于磁盘上,是一个文件

3.2 C源码

server.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>  
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
//创建管道时需要在mode参数位置传S_IFIFO,表明创建的是命名管道
int ret = mkfifo("./.fifo", S_IFIFO | 0644);
if (ret < 0)
{
perror("mkfifo");
return 1;
}

int fd = open("./.fifo", O_WRONLY);
if (fd < 0)
{
perror("open");
return 2;
}

int cnt = 0;
char *msg = "hello world";
while (cnt++ < 5)
{
write(fd, msg, strlen(msg));
sleep(1);
}

close(fd);
return 0;
}

client.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>  
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
int fd = open("./.fifo", O_RDONLY);
if (fd < 0)
{
perror("open");
return 2;
}

int cnt = 0;
char buf[128];
while (cnt++ < 5)
{
ssize_t _s = read(fd, buf, sizeof(buf) - 1);
if (_s > 0)
{
buf[_s] = '\0';;
printf("server say to client: %s\n", buf);
}
else if (_s == 0)
{
printf("server close write\n");
break;
}
else
{
perror("read");
}
sleep(1);
}

close(fd);
return 0;
}

4 参考