Linux下的通信方式之 System V 系列 IPC

前言

IPC 中文名称之为:进程间通行方式。同时在Linux中具有两种不同标准的 IPC :

  1. System IPC
  2. POSIX IPC

我们首先对于 System IPC 进行学习。

IPC主要包含了下面这中通信方式:

  1. 消息队列用来在进程之间传递消息,允许进程之间交换消息。
  2. 信号量允许多个进程同步它们的动作,用以同步进程对于共享资源的访问。
  3. 共享内存使得多个进程能够共享内存(即同被映射到多个进程的虚拟内存的页帧)的同一块区域(称为一个段),用以允许多个进程共享内存的同一个页的共享内存。

概述

下面这个表格是对于 System IPC 对象编程接口的总结

接口 消息队列 信 号 量 共享内存
头文件 <sys/msg.h> <sys/sem.h> <sys/shm.h>
关联数据结构 msqid_ds semid_ds shmid_ds
创建/打开对象 msgget() semget() shmget()+ shmat()
关闭对象 (无) (无) shmdt()
控制操作 msgctl() semctl() shmctl()
执行 IPC msgsnd()——写入消息msgrcv()——接收消息 semop()——测试/调整信号量 访问共享区域中的内存

IPC Key

IPC 中的创建函数所使用的唯一标识符和进程不同,IPC 需要使用给定的 Key 来创建一个新的 IPC 对象,并且返回一个唯一的标识符来表示这个对象。

也就是说,Key 是一个用来创建 IPC 对象所需要的钥匙,通过上面提到的 get 类函数,将这个 Key 通过算法转换为一个标识符。

所有需访问同一个 IPC对象的进程在执行 get调用时会指定同样的 key以获取该对象的同一个标识符。

这个标识符和文件标识符具有类似的地方,但是仍然具有很大的不同点:

  1. 文件描述符是一个进程特性,而 IPC 标识符是对象本身的属性,且对于系统全局可见。

下面例子展示了如何创建一个 System V 的消息队列:

1
2
3
d = msgget(key, IPC_CREAT| S_IRUSR |S_IWUSR);
if (id==-1)
errExit("msgget");

对于这个 mesget() 函数来说,第一个参数就是需要的 Key,而后面第二个参数其实就是关于权限的参数。返回的结果就是这个消息队列的唯一的标识符。

生成 Key 的两种方式

  1. 使用 IPC\_PRIVATE 产生一个唯一的 key
  2. 使用 ftok() 产生一个唯一的 key

当你需要创建一个仅在当前进程中使用的临时资源时,可以使用 IPC_PRIVATE。而当你需要创建一个资源并希望其他进程也能访问时,则应该使用 ftok() 来生成一个键值。通常在实际应用中,如果资源需要被多个进程共享,则推荐使用 ftok()。

  • IPC_PRIVATE 通常用于创建临时或测试资源,这些资源不会被其他进程共享,并且在使用完毕后会立即销毁。
  • ftok() 用于创建需要被多个进程共享的持久资源,这些资源可能会存在于整个程序的生命周期中。

我们首先讲第一种方式:

这种方式使用 IPC_PRIVATATE 来完成任务。

1
id = mesget(IPC_PRIVATE, S_IRUSR | S_IWUSR)

这项技术对于父进程在执行 fork()之前创建 IPC对象从而导致子进程继承 IPC对象标识符的多进程应用程序是特别有用的。在客户端-服务器应用程序中(即那些包含非相关进程的应用程序)也可以使用这项技术,但客户端必须要通过某种机制获取由服务器创建的 IPC对象的标识符(反之亦然)。如在创建完一个 IPC对象之后,服务器可以将这个标识符写入一个将会被客户端读取的文件中。

第二种方式

这个方式通过函数 ftok 产生一个唯一的 Key

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

key_t ftok(char *pathname, int proj);

//Return interger key on success, or -1 on error

ftok()使用 i-node号来生成 key值,而并没有使用文件名来生成 key值。(由于ftok()算法依赖于 i-node号,因此在应用程序的生命周期中不应该将文件删除和重新创建,因为重新创建文件时很有可能会分配到一个不同的 i-node号。)proj的目的仅仅是允许从同一个文件中生成多个 key,这对于需创建同种类型的多个 IPC对象的应用程序来讲是有用的。以前, proj参数的类型为 char,并且在调用 ftok()通常传入的也是 char值。

glibc ftok()的算法与其他 UNIX实现所采用的算法类似,它们都存在一个类似的限制:两个不同的文件可能会产生同样的 key值(可能性非常小)。之所以会发生这种情况是因为不同文件系统上的两个文件的 i-node号的最低有效位可能会相同,并且两个不同的磁盘设备(位于具备多个磁盘控制器的系统上)可能会拥有同样的次要设备号。但在实践中,不同的应用程序产生同样的 key值的可能性非常非常小以至于使用 ftok()产生 key已经是一项可靠的技术了。

下面是 ftok() 函数的典型用法:

1
2
3
4
5
6
7
8
9
10
key_t key;
int id;

key = ftok("/dir/test",'x');
if(key ==-1)
exit(-1);

id = mesget(key,IPC_CREAT | S_IRUSR | S_IWUSR);
if(id == -1)
exit(-1);

权限参数

需要注意,进程的 umask 对于新创建的 IPC 对象上的权限是不适用的。

如果没有与给定的 key对应的 IPC对象存在并且在 flags参数中指定了 IPC_CREAT (与open()的 O_CREAT标记类似),那么 get调用会创建一个新的 IPC对象。如果不存在相应的 IPC对象并且没有指定 IPC_CREAT(并且没有像 45.2节中描述的那样将 key指定为 IPC_PRIVATE),那么 get调用会失败并返回 ENOENT错误。

一个进程可以通过指定 IPC_EXCL 标记(类似于 open()的 O_EXCL标记)来确保它是创建 IPC对象的进程。如果指定了 IPC_EXCL并且与给定 key对应的 IPC对象已经存在,那么get调用会失败并返 EEXIST错误。

IPC对象的删除

System IPC 对象具有持久性,之后对象没有被删除或者是被系统关闭,它就会一直存在。

由于 System IPC 具有的这个特性,可以使得 System IPC 对象可以由一个进程创建,然后另一个进程使用。但是其实这个特性也有缺点:

● 系统对每种类型的 IPC对象的数量是有限制的。如果没有删除不用的对象,那么应用程序最终可能会因达到这个限制而发生错误。

● 在删除一个消息队列或信号量对象时,多进程应用程序可能难以确定哪个进程是最后一个需要访问对象的进程,从而导致难以确定何时可以安全地删除对象。这里的问题是这些对象是无连接的——内核不会记录哪个进程打开了对象。(共享内存段不存在这个缺点,因为它们的删除操作的语义不同。)

各种 System V IPC机制的 ctl系统调用(msgctl()、semctl()、shmctl())在对象上执行一组控制操作,其中很多操作是特定于某种 IPC机制的,但有一些是适用于所有的 IPC机制的,其中一个就是 IPC_RMID控制操作,它可以用来删除一个对象。如使用下面的调用可以删除一个共享内存对象。

1
2
if (shmctl(id, IPC_RMID, NULL) == -1)
errExit("shmctl");

对于不同的 IPC 而言是删除操作是不同的:

  1. 消息队列、信号量来说,IPC 对象的删除是立即生效的,对象中包含的所有消息对会被销毁,不管是否还有其他进程在使用该对象
  2. 对于共享内存来说,就不是这样子的了。在调用了上述函数之后,只有当所有的使用者都和这片内存分离之后,函数才会将这个共享内存删除。

相关的数据结构和对象权限

内核为 System V IPC对象的每个实例都维护着一个关联数据结构。这个数据结构的形式因 IPC机制(消息队列、信号量、或共享内存)的不同而不同,它是在各个 IPC机制对应的头文件中进行定义的。 但是 System IPC 中还有共同的数据结构—— ipc_perm ,它保存了用于确定对象之上的权限的信息。

1
2
3
4
5
6
7
8
9
struct ipc_perm {
key_t __key;
uid_t uid;
gid_t gid;
uid_t cuid;
git_t cgid;
unsigned short mode;
unsigned short __seq;
};

上面的 ipc_perm 中的 ipc 需要根据不同的 IPC 类型更改。如 shm_perm

SUSv3要求 ipc_perm结构中除__key和__seq字段之外的所有其他字段都要具备。大多数UNIX实现都提供了相应的字段。

uid和 gid字段指定了 IPC对象的所有权。cuid和 cgid字段保存着创建该对象的进程的用户 ID和组 ID。一开始,相应的用户和创建者 ID字段的值是一样的,它们都源自调用进程的有效 ID。创建者 ID是不可变的,而所有者 ID则可以通过 IPC_SET操作进行修改。下面的代码演示了如何修改共享内存段的 uid字段(关联数据结构的类型是 shmid_ds)。

1
2
3
4
5
6
7
struct shmid_ds shmds;

if (shmctl(id, IPC_STAT, &shmds))
exit(-1);
shmds.shm_perm.uid = newuid;//更改所有者id
if (shmctl(id, IPC_SET, &shmds))
exit(-1);

获取系统中所有的Systme IPC

一共有两种方法:

  1. 通过虚拟文件系统:/proc/sysvipc
  2. Linux命令 ipcsipcrm 。前者列出所有的 IPC ,后者用以删除。

https://ysc2.github.io/ysc2.github.io/2024/08/17/Linux下的进程通信方式之System_V_IPC/
作者
Ysc
发布于
2024年8月17日
许可协议