8 分钟
Linux IPC
概述
IPC(Inter-Process Communication,进程间通讯),有多种。
系统学习:https://www.cnblogs.com/philip-tell-truth/p/6284475.html
管道 (pipe)
手册:
pipe(7) 文档
|pipe(2) 系统调用
管道是 Unix 系统 最古老的一种 IPC 机制。该机制的特点是:
- 通讯机制是半双工的,即 Pipe 的两个端点一个只能写入,另一个只能读取。
- 管道只能在具有公共祖先的进程间通讯,通常,一个管道由一个进程创建,在进程调用 fork 后,这个管道在父子进程通讯中使用。
在 Linux 中管道又称匿名管道,而 FIFO 被称为命名管道,本部分介绍的是匿名管道。
Linux 中 pipe 的函数声明为:
#include <unistd.h>
int pipe (int fd[2])
- 参数
fd
是一个长度为 2 的数组,用来存放管道两个端点的文件描述符。fd[0]
为读取端点的文件描述符fd[1]
为写入端点的文件描述符
- 返回值
-1
表示出错
实例:一个进程创建一个管道由,在进程调用 fork 后,父进程发送 hello world\n
字符串,子进程读取。
// gcc src/c/01-namespace/03-ipc/01-pipe.c && sudo ./a.out
#include <unistd.h> // For pipe(2), STDOUT_FILENO
#include <limits.h> // For PIPE_BUF
#include <stdlib.h> // For EXIT_FAILURE, exit
#include <stdio.h> // For perror
#include <sys/wait.h> // For waitpid(2)
#include <string.h> // For strlen(3)
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
void main()
{
int n;
int fd[2];
pid_t pid;
const char *msg = "hello world\n";
const int MAXLINE = 1024;
char line[MAXLINE];
if (pipe(fd) < 0)
errExit("pipe");
if ((pid = fork())< 0)
errExit("fork");
if (pid > 0) // 父进程
{
close(fd[0]); // 父进程不需要使用管道的读取端点,所以关闭它
write(fd[1], msg, strlen(msg));
wait(NULL);
}
else // 子进程
{
close(fd[1]); // 子进程不需要使用管道的写入端点,所以关闭它
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
}
输出为:hello world\n
popen 和 pclose
手册:
popen(3) 库函数
popen(3) 库函数
封装了,父进程创建一个子进程,写入子进程标准输入 或 读取子进程标准输出的能力(读写只能二选一)。
函数声明为:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen
实现原理为:
- 调用
pipe(pfd)
系统调用创建一个管道 - 调用
fork
创建一个子进程- 父进程根据
type
决定关闭pfd[0]
或pfd[1]
,然后调用fdopen()
库函数将另一个pfd[0]
或pfd[1]
封装成*FILE
返回 - 子进程根据
type
决定关闭pfd[0]
或pfd[1]
,然后调用dup2
系统调用重定向标准输入/输出到pfd[0]
或pfd[1]
,然后调用exec
使用sh -c command
执行命令。
- 父进程根据
协同进程
一种比较常见的父子进程通讯方式。即,父进程创建两个管道,分别连接到子进程的标准输入和标准输出上,从而实现通讯。
该方式在只有 ksh 提供支持,sh,bash,csh 并不支持。
实现上需要注意标准 I/O 缓冲机制问题(全缓冲)
shell 管道
在 shell 中 command1 | command2
的语法,是通过 pipe(2) 系统调用
和 dup2(2) 系统调用
实现的。即:
- 当前进程标准输入,对接上级进程的标准输出
- 当前进程标准输出,对接下级进程的标准输入
实现上需要注意标准 I/O 缓冲机制问题(全缓冲)
注意事项
- 管道被设计来用于一对一单向通讯。并发使用存在如下问题:存在消息长度存在
PIPE_BUF
限制(4096)。如果单次写入超过该限制,则会被拆成多次发送。在多写的场景,会发生消息不连续的问题。这种场景让程序正确运行实现上比较困难,因此不建议用在在并发场景中。 - 如果写一个写端已经关闭的管道,读端将将返回
0
表示文件结束(不是-1
,-1
表示错误)。 - 如果写一个读端已经关闭的管道,将触发
SIGPIPE
信号,该信号的默认处理器为终止进程。如果处理了该信号,则写端将返回-1
,并且设置errno
为EPIPE
。 - 如果没有进程打开了管道,则这个管道会被自动销毁,数据将丢弃。
- 管道的缓冲器长度为
PIPE_BUF
(4096),如果缓冲区满了,write
调用将阻塞(未设置非阻塞),直到读端消费掉缓冲区数据。
命名管道 (FIFO)
手册:
fifo(7) 文档
|mkfifo(1) 命令
| mkfifo(3) 库函数
FIFO (first-in first-out special file, named pipe) 又称命名管道,其特点和管道 (pipe) 相比
- 相同点是:通讯机制是半双工的,即 Pipe 的两个端点一个只能写入,另一个只能读取。
- 不点在是:FIFO 会在文件系统中创建一个类型为
fifo
的文件,支持任意进程打开该文件进行通讯,而管道 (pipe) 只能在具有公共祖先的进程间通讯。
Linux 中 FIFO 文件创建函数声明为:
#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);
创建完成后:
- 使用
stat(2) 系统调用
,使用S_IFIFO
判断是否是 FIFO 文件。 - 使用
open(2)
系统调用,即可打开一个 FIFO 文件。O_RDONLY
打开该 FIFO 的读端点O_WRONLY
打开该 FIFO 的写断点O_NONBLOCK
是否阻塞- 如果未设置
O_NONBLOCK
,O_RDONLY
要阻塞到有进程使用O_WRONLY
打开的时候返回。 - 如果设置了
O_NONBLOCK
,O_RDONLY
要会立即返回,如果没有写入进程,则返回 -1,errno 设置为ENXIO
。
- 如果未设置
- 使用
write(2) 系统调用
写入一个 fifo 文件描述符时,类似与管道。如果没有进程打开该 fifo 文件的读断点(O_RDONLY
),将触发SIGPIPE
信号,该信号的默认处理器为终止进程。如果处理了该信号,则写端将返回-1
,并且设置errno
为EPIPE
。 - 使用
read(2) 系统调用
读取一个 fifo 文件描述符时,类似与管道。如果没有进程打开该 fifo 文件的写断点(O_WRONLY
),读端将将返回0
表示文件结束(不是-1
,-1
表示错误)。 - 如果没有进程打开 fifo 文件,这个文件将仍然在文件系统中保留,但 FIFO 中的数据已经被删除了。
- 管道的缓冲器长度为
PIPE_BUF
(4096),如果缓冲区满了,write
调用将阻塞(未设置非阻塞),直到读端消费掉缓冲区数据。
用途 1:在 shell 中数据流转而无需落盘
- 可以通过
mkfifo(1) 系统命令
直接在文件系统中创建一个 fifo 文件。 - 可以通过
| tee fifo文件
,<fifo文件
,>fifo文件
重定向和管道符读写 fifo 文件,来以有向无环图的方式串联多个标准 IO 处理程序,并不产生任何磁盘数据。
如 apue
书上的一个例子
mkfifo fifo1
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2
在这个例子中,任务 DAG 为:
---(fifo1)--> prog3
|
infile ----(pipe)---> prog1 ---(tee)---
|
---(pipe)---> prog2
用途 2:编写客户端/服务器模型的程序
不推荐,有其他更好的方法,如 Unix Domain Socket。
System V (XSI) IPC
手册:sysvipc(7)
这一部分不详细介绍,只简要介绍函数声明吧
System V (XSI) IPC 来源于 System V Unix 系统。System V IPC 饱受批评的地方是:没有使用文件系统作为标识符,而是构造了自己的命名空间。
System V IPC 有三主要功能:
- 消息队列
- 信号量
- 共享内存
apue
书作者认为 System V IPC 基本上没有什么优点:
- 最基本的问题:System V IPC 结构在系统范围内生效,且没有引用计数。以消息队列为例,也就是说,当所有进程都退出了,这个消息队列仍然存在,只有调用了
ipcrm
命令或者msgctl(2)
系统调用才会删除。 - 另一个问题:System V IPC 在文件系统中没有名字,无法使用任何文件系统相关的系统调用或命令(ls、rm、chmod 都不行)来访问消息队列。因此在内核中添加了数十个系统调用以及
ipcmk(1)
ipcs(1)
、ipcrm(1)
、lsipc
等命令。不复用文件描述符机制,无法使用多路复用函数(select、poll)。 - 不认为 System V IPC 作者列出的优点有说服力。
- 性能上并不比其他 IPC 机制优秀。
共性
System V IPC 可以通过 key_t
和 id
来定位一个对象。
key_t
在 Linux 中为int
id
类型为int
#include <sys/ipc.h> // https://man7.org/linux/man-pages/man3/ftok.3.html // 由文件系统路径和项目 id (只会用到低 8 位)生成一个 key_t key_t ftok(const char *pathname, int proj_id);
一般情况下,转换路径为,pathname, proj_id ---(ftok)---> key_t ---(msgget/semget/shmget)---> id
,获取到 id
后,就可以操作 System V IPC 对象了。
消息队列
#include <sys/msg.h>
// https://man7.org/linux/man-pages/man2/msgget.2.html
// 创建或获取一个 System V 消息队列。
// return 成功返回 id,失败返回 -1
int msgget(key_t key, int msgflg);
// https://man7.org/linux/man-pages/man2/msgctl.2.html
// 对消息队列进行控制操作,如删除,修改元数据等
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// https://man7.org/linux/man-pages/man2/msgsnd.2.html
// 发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// https://man7.org/linux/man-pages/man2/msgrcv.2.html
// 接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
信号量
#include <sys/sem.h>
// https://man7.org/linux/man-pages/man2/semget.2.html
// 创建或获取一个 System V 信号量。
// return 成功返回 id,失败返回 -1
int semget(key_t key, int nsems, int semflg);
// https://man7.org/linux/man-pages/man2/semctl.2.html
// 对信号量进行控制操作,如删除,修改元数据等
int semctl(int semid, int semnum, int cmd, ...);
// https://man7.org/linux/man-pages/man2/semop.2.html
// 信号量操作
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout);
共享内存
#include <sys/shm.h>
// https://man7.org/linux/man-pages/man2/shmget.2.html
// 创建或获取一个 System V 共享内存。
int shmget(key_t key, size_t size, int shmflg);
// https://man7.org/linux/man-pages/man2/shmctl.2.html
// 对共享内存进行控制操作,如删除,修改元数据等
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// https://man7.org/linux/man-pages/man2/shmat.2.html
// 将现有的共享内存对象 Attach 到调用进程的地址空间。
void *shmat(int shmid, const void *shmaddr, int shmflg);
// https://man7.org/linux/man-pages/man2/shmdt.2.html
// 从调用进程的地址空间中 Detach 段。
int shmdt(const void *shmaddr);
以上共享内存是支持在任意进程中实施的,但是如果是父进程和子孙进程间进行内存共享,可以通过如下方式实现:
- mmap 设置为
MAP_SHARED
并绑定/dev/zero
。 - mmap 设置为
MAP_SHARED | MAP_ANONYMOUS
,fd 字段设置为-1
POSIX IPC
POSIX IPC 是对 Systemc V IPC 的标准化产物。功能上和 Systemc V IPC 基本对等
消息队列
API 和 系统调用
glibc 库函数 | 系统调用 | 描述 |
---|---|---|
mq_open(3) | mq_open(2) | 创建或打开一个消息队列 |
mq_getattr(3) | mq_getsetattr(2) | 获取属性 |
mq_setattr(3) | mq_getsetattr(2) | 修改属性 |
mq_notify(3) | mq_notify(2) | 注册一个处理函数,当一个空的消息队列放入消息是,处理函数将被调用 |
mq_receive(3) | mq_timedreceive(2) | 从消息队列中接收消息 |
mq_send(3) | mq_timedsend(2) | 向消息队列中发送消息 |
mq_timedreceive(3) | mq_timedreceive(2) | 从消息队列中接收消息(支持超时) |
mq_timedsend(3) | mq_timedsend(2) | 向消息队列中发送消息(支持超时) |
mq_close(3) | close(2) | 关闭消息队列 |
mq_unlink(3) | mq_unlink(2) | 删除消息队列 |
更多参见:mq_overview(7) 手册
信号量
- sem_open(3) 创建或打开一个信号量
- sem_post(3) 操作信号量
- sem_wait(3) 如果一个值信号量当前为零,该操作将阻塞直到值大于零。
- sem_close(3) 关闭信号量
- sem_unlink(3) 删除信号量
- sem_init(3) 匿名信号量初始化
- sem_destroy(3) 匿名信号量销毁(在 free 内存前调用)
在 Linux 上,命名信号量是在虚拟文件系统中创建的,通常安装在 /dev/shm 下,名称为 sem.somename
。 (这就是信号量名称的限于 NAME_MAX-4 个字符,而不是 NAME_MAX 个字符的原因)
更多参见:sem_overview(7) 手册
共享内存
相关 API
- shm_open(3) 创建或打开一个共享内存对象。类似于 open(2)。调用返回一个下面列出的其他接口需要使用的文件描述符。
- ftruncate(2) 设置共享内存对象的大小。(一个新创建的共享内存对象的长度为零。)
- mmap(2) 将共享内存对象映射到虚拟地址调用进程的空间。
- munmap(2) 取消映射到当前进程虚拟内存空间的共享内存。
- shm_unlink(3) 删除共享内存对象名称。
- close(2) 关闭由 shm_open(3) 分配的不再需要文件描述符。
- fstat(2) 获取描述共享内存对象的统计结构。 此调用返回的信息包括对象的大小 (st_size)、权限 (st_mode)、所有者 (st_uid) 和组 (st_gid)。
- fchown(2) 更改共享内存对象的所有权。
- fchmod(2) 更改共享内存对象的权限。
在 Linux 中,共享内存是在 tmpfs(5)
虚拟文件系统中创建的,通常安装在 /dev/shm
下。自从内核 2.6.19,Linux 支持使用访问控制列表 (ACL) 来控制虚拟对象的权限文件系统。
更多参见:shm_overview(7) 手册
Socket
TODO
Unix Domain Socket
手册:unix(7)
TODO