进程间的通讯方式
简介
在 Linux 中有许多种方式允许进程之间进行通讯(Inter-process communication)
由于历史原因,Linux 内核提供System V
和POSIX
两种实现,两者概念十分相似。尽管如此,仍然推荐使用POSIX
实现,因为接口的规范和统一性更好
这些 IPC 方法可以按照如下方式分类(完整列表请看参考中的 Presentation):
- 通讯 Communication
- 数据传输 Data transfer
- 字节流 Byte stream
- 匿名管道 Pipe
- 命名管道 FIFO
- 流式套接字 Steam socket
- 消息 Message
- SystemV Message queue
- POSIX Messag queue
- 数据报套接字 Datagram socket
- 字节流 Byte stream
- 共享内存 Shared memory
- SystemV shmem
- POSIX shmem
- 内存映射 Memory mapping
- 匿名 Anonymous
- 文件映射 File mapping
- 数据传输 Data transfer
- 同步 Synchronization
- 信号量 Semaphore
- SystemV semaphore
- POSIX semaphore
- 信号量 Semaphore
- 信号 Singal
- 实时信号 Realtime
- 标准信号 Standard
匿名管道 Pipe
平时在 Shell 中经常用到,使用方法就是在命令间使用|
连接
它的作用是把前一个命令的输出作为后一个命令的输入
比如在搜索时就会经常用到:
1 | Cywair@Cywair-mach:/$ cat var/log/syslog.1 |grep "cron" |
1 | bakaft@BakaFT-PC:~$ ls -al | grep "socket" |
实现
在 Xv6 的学习过程中实现过 6.828-Hw2-Shell - BakaFT’s blog
简单来说:
- 使用
pipe()
创建管道(p[0]指向 Write 端,p[1]指向 Read 端) - 使用
fork()
创建两个子进程(子进程继承了父进程的管道描述符) - 第一个子进程运行左程序,并且使用
dup2()
将stdout
指向管道的 Write 端 - 第二个子进程运行右程序,并且使用
dup2()
将stdin
指向管道的 Read 端 - 关闭非必要的文件描述符,避免阻塞现象
特点
随进程持续
不存在于文件系统中,直接与内核内存中的缓冲区连通
存在阻塞现象:
- 读管道时,管道为空,Write 端存在引用,则会阻塞
- 写管道时,管道已满,Read 端存在引用,则会阻塞
缺点
半双工通信,只能从 Write 端到 Read 端
只能做到亲缘关系进程之间的通讯
容量小,满了再写就要阻塞
Linux 2.6.11 前,容量为 4096 字节
Linux 2.6.11 后,容量为 65535 字节
Linux 2.6.35 后,使用
fcntl(fd, F_SETPIPE_SZ, size)
可以自行调节,但不具有移植性这里补充一下,最高值由
/proc/sys/fs/pipe−max−size
限定
命名管道 Named pipe (FIFO)
顾名思义,这个管道是有名字的
打开两个终端,进行如下操作,即可看到效果
创建管道并查看内容,这个管道会以文件形式存在
1 | bakaft@BakaFT-PC:~$ tty |
将键盘输入重定向到管道并输入字符串
1 | bakaft@BakaFT-PC:~$ tty |
另外,还可以在 C 中调用mkfifo(3)实现
1 |
|
特点
- 随进程持续
- 全双工通信
- 以文件形式存在
- 可以做到无关程序之间的通讯
- 遵循 FIFO 原则,从头开始读,从尾开始写
缺点
- 存在与匿名管道一样的阻塞现象(但是你可以在 C 调用时通过
mode
参数改为非阻塞模式,具体看 man page)
消息队列 Message queue
特点
- 随内核持续
System V MQ
消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息
每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。每种类型的消息都被对应的链表所维护:
特别的是,消息类型为 0 的链表按照加入队列的顺序记录了所有消息
POSIX MQ
与 System V MQ 类似,但是
相比前者是用整数,它使用
/somename
的形式标记存在一个通知机制
消息队列存在于一个虚拟文件系统
1
2
3
4
5
6bakaft@BakaFT-PC:/$ mkdir /dev/mqueue
bakaft@BakaFT-PC:/$ mount -t mqueue none /dev/mqueue
bakaft@BakaFT-PC:/dev/mqueue$ ls
test-queue
bakaft@BakaFT-PC:/dev/mqueue$ cat test-queue
QSIZE:0 NOTIFY:0 SIGNO:0 NOTIFY_PID:0
通知机制
应用程序可以注册到某个队列来收取通知
- 当消息到达之前为空队列时,会被通知
- 可以选择通知方式
- 发送一个 Signal
- 启动一个新的线程
需要注意
- 任意时刻只能有一个进程能够向某个消息队列 注册接收通知
- 在注册进程收到了一条通知后,注册信息失效
- 如果其他程序正在接收通知,注册进程就不会收到通知
- 通过指定
notification
参数为NULL
来取消注册
特点
- 面向消息通讯
- 消息具有优先级
- 具有消息通知机制
- 存在引用计数,只有当所有使用队列的进程全部关闭才会标记删除
缺点
- 严格按照优先级,但丢失了像 System V MQ 消息分类的灵活性
共享内存 Shared memory
进程之间共享同样的物理内存页面,他们的“通讯”就是在内存中交换数据
特点
随内核持续
效率高
直接在用户内存空间共享,而非 用户内存 A=>内核内存=>用户内存 B 这样的交换
缺点
- 需要注意同步
mmap 介绍
接下来的三种内存共享方式均需要用到 mmap(2) - Linux manual page (man7.org)
1 | void *mmap(void *addr, size_t length, int prot, int flags, |
关于 mmap 的部分 flag:
MAP_SHARED
表示对映射区域的修改会写回文件,并且在进程之间共享尽管会写回,但是需要尽量调用
msync()
保证写回一定会执行MAP_PRIVATE
表示既不写回,也不在进程之间共享,这是一种实现malloc
的思路,但是下面不会用到
匿名共享映射 Shared anonymous mappings
1 | int* addr = mmap(NULL,0x64,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0); |
特点
适用于亲缘进程 不需要文件系统支持
mmap() 中的
fd
和offset
是不需要的,但是出于移植性考虑建议赋值为-1
和0
1
2
3
4
5
6
7
8MAP_ANONYMOUS
The mapping is not backed by any file; its contents are
initialized to zero. The fd argument is ignored; however,
some implementations require fd to be -1 if MAP_ANONYMOUS
(or MAP_ANON) is specified, and portable applications
should ensure this. The offset argument should be zero.
The use of MAP_ANONYMOUS in conjunction with MAP_SHARED is
supported on Linux only since kernel 2.4.父子进程共享
addr:length
之间的内存
文件共享映射 Shared file mappings
特点
- 适用于非亲缘进程,需要文件系统支持
- 内存内容从文件初始化
- 所有共享同一个文件的进程共享同一块内存
POSIX 共享内存 POSIX shared memory
特点
适用于非亲缘进程,不需要传统文件系统支持
避免 I/O 开销
存在于一个虚拟文件系统(tmpfs 类型)
1
2
3
4bakaft@BakaFT-PC:/dev/shm$ ls
test-shm
bakaft@BakaFT-PC:/dev/shm$ cat test-shm
This is a string shared between using POSIX shared memory
信号量 Semaphore
POSIX Semaphore
Semaphore 是由内核维护的整数,代表了一种共享资源的剩余数量,当值为 0 时再申请,会发生阻塞并将值下降到负数
两个基本操作
sem_post(): 增加 1
sem_wait(): 减少 1
可能会由于资源缺少而阻塞
常用于:布尔值的表示
- 单一资源(如共享内存是否正在被使用)
匿名信号量 unnamed semaphore
随进程持续
使用
sem_init()
初始化通常把它放到共享内存中实现跨进程状态保存
命名信号量 named semaphnore
随内核持续
使用
sem_open()
初始化,具有/somename
类型的名字这个
somename
的长度被限定为NAME_MAX-4
,这和它在文件系统的文件名格式有关命名信号量存在于一个虚拟文件系统(tmpfs 类型),以
sem.somename
的形式存在(为什么要和共享内存在一个文件夹呢?)
1
2bakaft@BakaFT-PC:/dev/shm$ ls
sem.reader sem.writer
套接字 Socket
一个典型的套接字
fd = socket(domain,type,protocol)
特点
- 随进程持续
- 双向通信
- 基于更高的层次实现
域 Domain
- Unix domain(AF_UNIX)
- 用于主机内部通讯
- 地址为文件路径
- IPv4 domain(AF_INET)
- 在 IPv4 网络间通讯
- 地址是 32 位 IPv4 地址+端口
- IPv6 domain(AF_INET6)
- 在 IPv6 网络间通讯
- 地址是 128 位 IPv6 地址+端口
类型 Type
在所有的 Domain 中,有两种主要的可用 Type:
- 流式传输 Stream(SOCK_STREAM)
- 数据报传输 Datagram(SOCK_DGRAM)
在 Unix domain 中,额外还有:
顺序报文传输(SOCK_SEQPACKET)
通常配合 IPPROTO_SCTP 使用
流式套接字 Steam socket
类型使用
SOCK_STREAM
字节流传输
面向连接
可靠,数据按顺序且不重复地到达或者全部不到达
在 Internet 域上使用 TCP 协议
数据报套接字 Datagram socket
- 类型使用
SOCK_DGRAM
- 面向消息
- 无连接
- 不可靠:
- 可能包会重复
- 可能乱序
- 可能根本收不到
- 在 Internet 域使用 UDP 协议
顺序报文套接字 Sequential packet socket
- 类型使用
SOCK_SEQPACKET
- 处于流式套接字和数据报套接字之间
- 面向消息
- 面向连接
- 可靠
- 在 Unix 域,只能使用 SCTP 协议
关于套接字的一些小知识
双向通讯
UNIX 域的
数据报套接字
是可靠
的在本机,很难出现问题,是吧?
UXIX 域的套接字可以传递文件描述符
?有点读不懂,待验证
对于(狭义)网络通信来说,Internet 域套接字是唯一的方法
UDP 套接字允许广播或多播数据报
socketpair()
- 在 UNIX 域使用
- 双向
可以用来做到父子进程的双向通讯
附录:不同 IPC 的 ID 和句柄类型
附录:不同 IPC 的访问权限
附录:不同 IPC 的生命周期
随进程持续,Process
直到最后一个打开它的进程结束
随内核持续,Kernel
直到重启
随文件持续,Filesystem
参考
本文主要参考了 Michael Kerrisk 在 linux.conf.au 2013 的 Presentation: An introduction to Linux IPC
Michael Kerrisk
The Linux Programming Interface 的作者
Linux man-pages maintainer
Kernel.org/doc/man-pages 和 man7.org 由他维护