Linux IPC 的实现方案主要分为管道、信号、消息队列、共享内存、信号量、Socket 等几个类型,本文简单描述通过管道实现 IPC 相关的内容。

pipe 匿名管道

在父进程中通过 pipe 内核函数可以创建一个匿名管道。

1
2
3
4
5
6
7
8
#include <unistd.h>
int pipe(int pipefd[2]); // 返回两个 fd,一个读端,一个写端

#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Definition of O_* constants */
#include <unistd.h>

int pipe2(int pipefd[2], int flags); // 还支持一些 flag 选项

pipe2 在 pipe 的基础上,提供了一些选项,如 O_CLOEXEC、O_DIRECT、O_NONBLOCK、O_NOTIFICATION_PIPE。

image.png

通过 fork 创建子进程后,则子进程也获取了此管道的读端和写端。

image.png

父子进程间,一个关闭读端,另一个关闭写端,即可获得一个单向通信的管道。如果要相反方向通信,创建另一个管道做相反处理即可。

示例代码:

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

int
main(int argc, char *argv[])
{

int pipefd[2];
char buf;
pid_t cpid;

if (argc != 2) {
fprintf(stderr, "Usage: %s <string>\n", argv[0]);
exit(EXIT_FAILURE);
}
// 创建匿名管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// fork 出子进程
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}

if (cpid == 0) {
// 子进程关闭写 fd
close(pipefd[1]);
const char msg[] = "message from child \n";
write(STDOUT_FILENO, msg, sizeof(msg));
// 从管道中读到数据,则输出到标准输出
while (read(pipefd[0], &buf, 1) > 0)
write(STDOUT_FILENO, &buf, 1);

write(STDOUT_FILENO, "\n", 1);
// 读到文件结束符后,关闭读 fd 并正常退出
close(pipefd[0]);
_exit(EXIT_SUCCESS);

} else {
// 父进程关闭读 fd
close(pipefd[0]);
// 写入终端输入的所有字符
write(pipefd[1], argv[1], strlen(argv[1]));
// 完毕后关闭写管道
close(pipefd[1]);
// 等待子进程处理完毕
wait(NULL);
exit(EXIT_SUCCESS);
}
}

pipe 的特点

  • 单个匿名管道只能单向通信
  • 只能在父子进程间通信
  • 依赖文件系统的相关 API 实现
  • 生命周期随进程

fifo 具名管道

匿名管道只能做父子进程间的通信,具名管道则解决了这个问题。

具名管道创建时需要提供一个文件路径,将管道以文件形式存储于文件系统中。命名管道是一个设备文件,因此,读写双方进程不存在父子关系,只要可以访问管道的文件路径,就能够通过 fifo 相互通信。值得注意的是, fifo(first input first output) 总是按照先进先出的原则,第一个被写入的数据将首先从管道中读出。

可以使用内核函数 mknod,或者系统库函数 mkfifo / mkfifoat 来创建具名管道。

当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。

FIFO 存在于文件系统中,内容存放在内存中,如果使用 ls -l 观察时,它的文件大小永远是0。

实际上,Linux中的命令的 “ | “ 就是 fifo。

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode); // mode 指定文件的权限

#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>

int mkfifoat(int dirfd, const char *pathname, mode_t mode); // 同 mkfifo,路径逻辑上略有差别

如果 调用 open 打开 fifo 文件时时,没有使用 O_NONBLOCK 参数,将 fd 设置为非阻塞模型,则无论读端还是写端先打开,先打开者都会阻塞,直到阻塞到另一端打开。

当 调用 open 时设置为非阻塞标志 O_NONBLOCK ,则会有下面的影响:

  • 设置为非阻塞时,只读 open 立即返回。但如果没有进程为读而打开一个 FIFO,那么只写 open 将返回 -1,并将 errno 设置成 ENXIO。
  • 若 write 一个尚无进程为读而打开的 FIFO,则产生信号 SIGPIPE。
  • 若某个 FIFO 的最后一个写进程关闭了该 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。
  • 一个给定的 FIFO 可能有多个写进程,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作,可被原子地写到 FIFO 的最大数据量是通过常量 PIPE_BUF 来指定的。

示例代码 - writer

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
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

#define FIFO_FILE_NAME "test_fifo_file"

int main(){
printf("start writer\n");
// 创建 fifo 文件
int ret = mkfifo(FIFO_FILE_NAME, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
if (ret == -1) {
if (errno == EEXIST) {
printf("FIFO file is already exist.\n");
} else {
perror("fifo create");
return -1;
}
}
// 以写入模式打开 fifo 文件,并等待 reader 读取文件
printf("waiting for reader open...\n");
int writeFd = open(FIFO_FILE_NAME, O_WRONLY);
printf("reader has connected, start to write data\n");
// 读取端已打开 fifo 文件,写入消息
char msg[] = "hello world";
for (int i = 0; i < sizeof(msg); i++ ) {
write(writeFd, msg + i, 1);
printf("wirte data %c \n", msg[i]);
}
return 0;
}

示例代码 - reader

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
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define FIFO_FILE_NAME "test_fifo_file"

int main(){
printf("start reader\n");
printf("waiting for writer open...\n");
int readFd = open(FIFO_FILE_NAME, O_RDONLY);
// 写端已打开 fifo,开始读取数据
int ret;
char buf[1];
while (true)
{
ret = read(readFd, buf, sizeof(buf));
if (ret == 0) break;
if (ret == -1) {
printf("read data from fifo error: %d\n", errno);
return -1;
}
printf("read data %s \n", buf);
}
return 0;
}

image.png

image.png

fifo的特点

  • 有名
  • 任一个进程都可以交互
  • 会诞生一个类型p的管道文件
  • 操作具备原子性
  • 全双工
  • 不能lseek来定位

参考文档:

☞ 参与评论