Linux IPC 的实现方案主要分为管道、信号、消息队列、共享内存、信号量、Socket 等几个类型,本文简单描述通过管道实现 IPC 相关的内容。
pipe 匿名管道
在父进程中通过 pipe 内核函数可以创建一个匿名管道。
1 2 3 4 5 6 7 8
| #include <unistd.h> int pipe(int pipefd[2]);
#define _GNU_SOURCE #include <fcntl.h> /* Definition of O_* constants */ #include <unistd.h>
int pipe2(int pipefd[2], int flags);
|
pipe2 在 pipe 的基础上,提供了一些选项,如 O_CLOEXEC、O_DIRECT、O_NONBLOCK、O_NOTIFICATION_PIPE。
通过 fork 创建子进程后,则子进程也获取了此管道的读端和写端。
父子进程间,一个关闭读端,另一个关闭写端,即可获得一个单向通信的管道。如果要相反方向通信,创建另一个管道做相反处理即可。
示例代码:
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); } cpid = fork(); if (cpid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (cpid == 0) { 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); close(pipefd[0]); _exit(EXIT_SUCCESS);
} else { 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);
#include <fcntl.h> /* Definition of AT_* constants */ #include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
|
如果 调用 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"); 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; } } printf("waiting for reader open...\n"); int writeFd = open(FIFO_FILE_NAME, O_WRONLY); printf("reader has connected, start to write data\n"); 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); 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; }
|
fifo的特点
- 有名
- 任一个进程都可以交互
- 会诞生一个类型p的管道文件
- 操作具备原子性
- 全双工
- 不能lseek来定位
参考文档:
本文链接:https://www.zoucz.com/blog/2022/07/01/a857c240-a332-11ee-bb9f-1566042b6386/