进程间的通讯方式

简介

在 Linux 中有许多种方式允许进程之间进行通讯(Inter-process communication)

由于历史原因,Linux 内核提供System VPOSIX两种实现,两者概念十分相似。尽管如此,仍然推荐使用POSIX实现,因为接口的规范和统一性更好

这些 IPC 方法可以按照如下方式分类(完整列表请看参考中的 Presentation):

  • 通讯 Communication
    • 数据传输 Data transfer
      • 字节流 Byte stream
        • 匿名管道 Pipe
        • 命名管道 FIFO
        • 流式套接字 Steam socket
      • 消息 Message
        • SystemV Message queue
        • POSIX Messag queue
        • 数据报套接字 Datagram socket
    • 共享内存 Shared memory
      • SystemV shmem
      • POSIX shmem
      • 内存映射 Memory mapping
        • 匿名 Anonymous
        • 文件映射 File mapping
  • 同步 Synchronization
    • 信号量 Semaphore
      • SystemV semaphore
      • POSIX semaphore
  • 信号 Singal
    • 实时信号 Realtime
    • 标准信号 Standard

匿名管道 Pipe

平时在 Shell 中经常用到,使用方法就是在命令间使用|连接

它的作用是把前一个命令的输出作为后一个命令的输入

比如在搜索时就会经常用到:

1
2
Cywair@Cywair-mach:/$ cat var/log/syslog.1 |grep "cron"
May 24 00:17:01 Cywair-mach CRON[585699]: (root) CMD ( cd / && run-parts --report /etc/cron.hourly)
1
2
bakaft@BakaFT-PC:~$ ls -al | grep "socket"
drwxrwxr-x 9 bakaft bakaft 4096 May 23 12:05 socket-programming

实现

在 Xv6 的学习过程中实现过 6.828-Hw2-Shell - BakaFT’s blog

简单来说:

  1. 使用pipe()创建管道(p[0]指向 Write 端,p[1]指向 Read 端)
  2. 使用fork()创建两个子进程(子进程继承了父进程的管道描述符)
  3. 第一个子进程运行左程序,并且使用dup2()stdout指向管道的 Write 端
  4. 第二个子进程运行右程序,并且使用dup2()stdin指向管道的 Read 端
  5. 关闭非必要的文件描述符,避免阻塞现象

特点

  • 随进程持续

  • 不存在于文件系统中,直接与内核内存中的缓冲区连通

  • 存在阻塞现象:

    • 读管道时,管道为空,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
2
3
4
5
6
7
8
bakaft@BakaFT-PC:~$ tty
/dev/pts/1
bakaft@BakaFT-PC:~$ mkfifo named-pipe
bakaft@BakaFT-PC:~$ ls -al named-pipe
prw-r--r-- 1 bakaft bakaft 0 May 25 14:53 named-pipe
bakaft@BakaFT-PC:~$ cat named-pipe
Hi there, I've redirected my keyboard input to this pipe.
bakaft@BakaFT-PC:~$

将键盘输入重定向到管道并输入字符串

1
2
3
4
5
6
bakaft@BakaFT-PC:~$ tty
/dev/pts/0
bakaft@BakaFT-PC:~$ cat>named-pipe
Hi there, I've redirected my keyboard input to this pipe.
^C
bakaft@BakaFT-PC:~$

另外,还可以在 C 中调用mkfifo(3)实现

1
2
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

特点

  • 随进程持续
  • 全双工通信
  • 以文件形式存在
  • 可以做到无关程序之间的通讯
  • 遵循 FIFO 原则,从头开始读,从尾开始写

缺点

  • 存在与匿名管道一样的阻塞现象(但是你可以在 C 调用时通过mode参数改为非阻塞模式,具体看 man page)

消息队列 Message queue

特点

  • 随内核持续

System V MQ

消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息

每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。每种类型的消息都被对应的链表所维护:

特别的是,消息类型为 0 的链表按照加入队列的顺序记录了所有消息

POSIX MQ

与 System V MQ 类似,但是

  • 相比前者是用整数,它使用/somename的形式标记

  • 存在一个通知机制

  • 消息队列存在于一个虚拟文件系统

    1
    2
    3
    4
    5
    6
    bakaft@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
2
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);

关于 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() 中的fdoffset是不需要的,但是出于移植性考虑建议赋值为-10

    1
    2
    3
    4
    5
    6
    7
    8
    MAP_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
    4
    bakaft@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
    2
    bakaft@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 的作者

    TLPI front cover

  • Linux man-pages maintainer

    Kernel.org/doc/man-pages 和 man7.org 由他维护

Author

BakaFT

Posted on

2022-05-25

Updated on

2023-12-28

Licensed under

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×