Linux 进程间通信——匿名管道和有名管道

本文最后更新于:2021年4月4日 下午

概览:Linux IPC,匿名管道pipe,有名管道FIFO。

进程间通信IPC

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。

但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。

目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

匿名管道

管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。

  • 我们常用的命令 ps aux | grep root,中间的竖线就是管道的意思。ps的结果存入管道,然后grep读取管道,输出结果。

管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。

管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。

一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。

通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的

在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的

从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。

匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

管道的数据结构是队列,循环队列!这样方便操作循环使用。

通信原理

fork之后,父子进程仍然共享同一个文件描述符表。两方文件描述符表的指向都是相同的。

所以父进程可以通过读写管道来和子进程通信。

所以管道要在fork之前创建。

pipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>
int pipe(int pipefd[2]);
/*
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1

管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞

注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
*/

案例:父子进程间通信

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
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//创建管道,父子进程间通信

int main(){

//匿名管道只能用于有亲缘关系的进程之间
//管道应在fork之前创建
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1){
perror("pipe");
exit(0);
}

pid_t pid = fork();

if(pid == 0){

printf("child process, pid : %d \n",getpid());

//子进程关闭读端
close(pipefd[0]);

while(1){
char * str = "床前明月光,疑是地上霜";
write(pipefd[1],str,strlen(str));
sleep(1);
}
}else if(pid > 0){
printf("parent process, pid : %d \n",getpid());

//父进程关闭写端
close(pipefd[1]);

//从读端读取数据
char buf[1024] = {0};
while(1){

int len = read(pipefd[0],buf,sizeof(buf));

printf("parent recv : %s \n",buf);

memset(buf,0,sizeof(buf)); //清空缓冲区
sleep(1);
}
}
}
  • 一般来说管道都是单向通信的,所以一个关闭读端,一个关闭写端。
  • 两方既写又发会造成混乱!

获取管道大小

1
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);

以及命令,就可以看到pipe size。

1
ulimit -a

设置管道非阻塞fctnl

通过使用函数fctnl来设置追加文件权限。

1
2
3
int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag

实例:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main(){

int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1){
perror("pipe");
exit(0);
}

pid_t pid = fork();

if(pid == 0){

close(pipefd[0]);

printf("child , pid: %d, ppid : %d\n",getpid(),getppid());

char * str = "你当寻找你之所爱并守望";

while(1){
write(pipefd[1],str,strlen(str));
sleep(1);
}
}else if(pid > 0){
printf("parent ,pid : %d\n",getpid());

close(pipefd[1]);

//读取数据
char buf[1024] = {0};

//设置非阻塞
int flags = fcntl(pipefd[0],F_GETFL);
flags = flags | O_NONBLOCK;
fcntl(pipefd[0],F_SETFL,flags);

while(1){
int len = read(pipefd[0],buf,sizeof(buf));
printf("len : %d\n",len);
printf("parent recv : %s \n",buf);
memset(buf,0,sizeof(buf));
sleep(1);
}
}

return 0;
}
  • 设置非阻塞之后,每次读取的长度都不太相同。也可能读不到数据。

案例ps aux | grep root

思路:子进程需要execve ps aux,将其获得的数据发送到管道的写端。这里需要用到dup2,重定向,将标准输出重定向到管道的写端。

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
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>

int main(){

int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1){
perror("pipe");
exit(0);
}

pid_t pid = fork();

if(pid == 0){
//子进程
close(pipefd[0]);

//重定向 stdout_fileno -> pipefd[1]
dup2(pipefd[1],STDOUT_FILENO);

execlp("ps","ps","aux",NULL);

//若执行失败
perror("execlp");
exit(0);
}
else if(pid > 0){
//父进程
close(pipefd[1]);

char buf[1024] = {0};
int len = -1;

while((len = read(pipefd[0],buf,sizeof(buf))) > 0){
printf("%s",buf);
memset(buf,0,sizeof(buf));
}

//读完回收子进程
wait(NULL);
}

return 0;
}
  • 当然,父进程收到数据之后,应当过滤操作。

匿名管道特点

使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)

1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。

3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。

4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。

读管道:
    管道中有数据,read返回实际读到的字节数。
    管道中无数据:
        写端被全部关闭,read返回0(相当于读到文件的末尾)
        写端没有完全关闭,read阻塞等待
写管道:
    管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
    管道读端没有全部关闭:
        管道已满,write阻塞
        管道没有满,write将数据写入,并返回实际写入的字节数

有名管道

匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。

有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。

一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一 个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。

FIFO与pipe管道区别

  1. FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。

  2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。

  3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。

创建有名管道

命令行:

1
2
3
4
mkfifo fifo3

ll查看结果:
prw-rw-r-- 1 colourso colourso 0 328 13:33 fifos|

p代表管道,名字最后有一个竖线!

函数创建

1
2
3
4
5
6
7
8
9
10
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
/*
参数:
- pathname: 管道名称的路径
- mode: 文件的权限 和 open 的 mode 是一样的
是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号
*/
  • 一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/O 函数都可用于 fifo。如:close、read、write、unlink 等。
  • FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。

有名管道注意事项

有名管道的注意事项:

1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道

2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道

读管道:

1
2
3
4
管道中有数据,read返回实际读到的字节数
管道中无数据:
管道写端被全部关闭,read返回0,(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待

写管道:

1
2
3
4
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
管道已经满了,write会阻塞
管道没有满,write将数据写入,并返回实际写入的字节数。

案例:两个进程一个写数据,一个读数据。

1