Maple's Story

学习笔记:进程间通信

字数统计: 2.2k阅读时长: 8 min
2020/05/07 Share

进程间通信( Inter Process Communication, IPC ) 指在不同的进程之间传播或交换信息。常用的进程间通信方法有:管道、消息队列、共享内存、信号量、套接字等, 其中前4种主要用于同一台机器上的进程间通信,而套接字主要用于跨机器的网络通信。


管道

管道是一种两个进程间进行单向通信的机制。因为其单向性,管道又被称为半双工通信

无名管道

无名管道具有以下特点:

  • 数据只能从一个进程流向另一个进程(一个读管道、一个写管道);如要进行双工通信,需要建立两个管道。
  • 管道只能用于具有亲缘关系(父子、兄弟进程)的进程间通信。
  • 管道没有名字(无名管道)。
  • 缓冲区大小受限,传输的是无格式的字节流,需管道通信双方事先约定好数据格式。
  • 从本质上说,管道也是一种文件,但只存在于内存中。

无名管道由 pipe()创建, 管道两端分别用描述符fd[0]以及fd[1]表示,前者为读端,后者为写段。一个进程用pipe()创建管道后,fork一个子进程,再通过管道实现两者之间的通信。要实现读写,只需将无关的两个文件描述符关闭即可。

1
2
3
#include <unistd.h>

int pipe(int fd[2]);

有名管道

有名管道( FIFO ) 提供一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中。这样只需访问该路径,即可通过 FIFO相互通信。

有名管道与无名管道的区别

有名管道有以下特点:

  • 它可以使互不相关的两个进程间实现彼此通信
  • 该管道可以通过路径名来指出,并且在文件系统中是可见的
  • 在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便
  • FIFO 严格地遵循先进先出规则,对管道及 FIFO 的读操作总是从开始处返回数据,对它们的写操作则是把数据添加到末尾 。

有名管道由 mkfifo() 函数来创建,一般文件的 I/O 函数均可用于 FIFO , 如 close()read()write()等。

1
2
3
4
5
#include <sys/types .h>
#include <sys/stat .h>

// 第一个参数为路径名,第二个参数与打开普通文件的 open() 函数的 mode 参数相同
int mkfifo (canst char * pathname , mode t mode);

有名管道的安全性

由于有名管道支持一对多的形式,多个进程同时向一个 FIFO 文件中写数据,一个进程从其中读取数据,是可能发现数据块的相互交错的。

为了解决这个问题,Unix 提供了写操作的原子化:在一个以 O_WRONLY (堵塞只写)打开的 FIFO 中,如果写入的数据长度小于 PIPE_BUF (管道写入最大值),那么要么全部写入,要么一个不写。以此来保证数据不会交错。

通过客户端在向服务端发送信息前先建立以自己的进程号 ( pid ) 命名的读入管道,告知服务器将其作为回复数据的写入管道,可以以此实现一对多双向交流。


消息队列

消息队列是在系统内核中保存消息的队列,以消息链表的形式出现。链表中的节点以 msg 声明。

  1. 创建新队列或取得已存在消息队列:

    1
    int msgget(key_t key, int msgflg);

    key 为端口号,可以由函数 ftok 生成
    msgflg 如果等于 IPC_CREAT 若无该队列,则创建并返回新标识符,已存在则返回原标识符
    msgflg 如果等于 IPC_EXCL 若无该队列,则返回 -1, 若存在,则返回 0

  2. 向队列中读 / 写消息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 缓冲区通用结构
    struct msgstru {
    long mtype; // 大于0
    char mtext[512];
    };

    // 从队列中取用消息
    ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

    // 将数据放到消息队列中
    int msgsnd (int msqid, canst void *msgp, size_t msgsz ,int msgflg);

msqid 为消息队列标识符
msgp 为指向消息缓冲区的指针,用于暂时存储发送和接收的消息
msgsz 消息的大小
msgtyp 从消息队列中读取的消息形式,为0 则读取所有消息
msgflg 指明程序在队列没有数据时该采取的行动

  1. 设置消息队列属性;
1
int msgctl (int msgqid, int cmd, struct msqid_ds *buf );

msgctl 系统调用对 msgqid 标识的消息队列执行 cmd 操作,系统定义了三种 cmd 操作:
IPC_STAT 用来获取对应的 msqid_ds 数据结构,并保存到 buf 指定的内存空间
IPC_SET 用来设置属性,要设置的属性存储在 buf 中
IPC_RMID 用来从内核中删除 msqid 标识的消息队列

相比有名管道,消息队列的优势在于:

  • 消息队列独立于发送和接收进程而存在
  • 可以同时发送消息,避免了命名管道的同步和阻塞问题
  • 接收程序可以根据消息类型有选择地接收数据

消息管道是一种正逐渐被淘汰的通信方式。


共享内存

共享内存是允许两个不相关的内存访问同一块物理内存。进程可以将同一段共享内存连接到它们自己的地址空间里,就像是用函数 malloc 分配的内存一样。 但共享内存为提供同步机制,需要其他机制来同步对共享内存的访问。

1
2
3
#include <sys/shm.h>

int shmget(key_ t key, int size , int flag) ;

key 为共享内存命名, shmget 函数运行成功返回一个与 key 相关的共享内存标识符,用于后续的共享函数,失败返回 -1。
size 以字节为单位指定共享的内存容量。
shmflg 为与 open 函数的 mode 参数一样的权限标志, 可与 IPC_CREAT 做或操作实现不存在时创建共享内存。

共享内存创建后,使用 shmat将其连接到自己的地址空间

1
void *shmat(int shmid, void *addr, int flag);

shmidshmget 函数返回的标识符

shmdt 函数用于将共享内存从当前进程分离,不是删除。

1
int shmdt(const void *shmaddr);

shmaddrshmat 函数的返回指针,调用成功返回0,失败返回 -1.

使用共享内存的优缺点如下所述:

  • 优点:使用共享内存进行进程间的通信非常方便,而且函数的接口也简单,数据的
    共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不
    像无名管道那样要求通信的进程有一定的父子关系。
  • 缺点:共享内存没有提供同步的机制,这使得在使用共享内存进行进程间通信时,
    往往要借助其他的手段来进行进程间的同步工作。

信号量

多线程同步使用的信号量是 POSIX 信号量,而多进程同步使用的是 SYSTEM V 信号量。

常用函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

int semop(int semid, struct sembuf *sops, unsigned nsops);

struct sembuf {
short sem_num; // 选定要操作的信号量序号
short sem_op; // 信号量在一次操作中需要改变的数据: -1:P 操作, 1:V 操作
short sem_flg; // 通常为 SEM_UNDO,使操作系统跟踪信号
// 在进程没有释放信号量而终止时,操作系统释放信号量。
};

int semctl(int semid, int semnum, int cmd, ...);

union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}

semget 函数中,参数 key 是通过调用 ftok 函数得到的键值, nsems 代表创建信号量个数,如果只是访问而不创建则指定为0,一旦创建了该信号量就不能改变其个数。 semflg 指定信号量读写权限,不可加 IPC_CREAT。
semop 函数用于改变信号量的值,sem_id 为 semget 返回的信号量标识符,sembuf 结构如上。
semctl 函数用于控制信号量信息,cmd 通常是 SETVAL 或 IPC_RMID,前者初始化信号量为一个已知的值,由 union semun 中的 val 成员设置。 IPC_RMID 用于删除一个信号量标识符。

共享内存是进程间通信的最快的方式,但是共享内存的同步问题自身无法解决(即进程该何时去共享内存取得数据,而何时不能取),但用信号量即可轻易解决这个问题。


学习资料

《后台开发 核心技术与应用实践 第11章 进程间通信》

原文作者:枫逸

原文链接:https://www.pengchen.top/posts/11653/

发表日期:May 7th 2020, 9:19:58 pm

更新日期:September 5th 2020, 1:44:05 am

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 管道
    1. 1.1. 无名管道
    2. 1.2. 有名管道
    3. 1.3. 有名管道的安全性
  2. 2. 消息队列
  3. 3. 共享内存
  4. 4. 信号量
  5. 5. 学习资料