IPC服务器应用程序
在客户端/服务器应用程序中,服务器通常会创建System V IPC对象,而客户端仅仅需要访问它们。即服务器在执行get调用时需要指定IPC_CREAT标记,而客户端在get调用中会省略这个标记
假设一个客户端参与了一个服务器的一个一个延伸会话,其中每个进程会执行多个IPC操作(如交换多条消息、一组信号量操作、或者多次更新共享内存)。如果服务器进程崩溃或者故意停止然后重启会发生什么情况呢?这时,盲目的重用由前一个服务器进程创建的IPC对象是毫无意义的,因为新服务器进程不清楚与IPC对象的当前状态相关的历史信息(如消息队列中可能存在客户端因响应老的服务器进程之前发送的一条消息而发出的第二个请求)。
在这种情况下,服务器唯一可做的事情就是丢弃既有的客户端、删除由上一个服务器进程创建的IPC对象、创建IPC对象的新实例。新启动的服务器首先会通过在get调用中同时指定IPC_CREAT和IPC_EXCL标记创建一个IPC对象来处理服务器的上一个实例非正常终止的情况。如果 get 调用因具备指定 key 的对象已存在而失败,那么服务器就认为老的服务器进程之前创建了该对象,因此它会使用 IPC_RMID ctl 操作删除这个对象,然后再次执行一个 get 调用来创建对象。
下面给出了一个消息队列可能需要执行的步骤。
#define _GNU_SOURCE
#include <time.h>
#include <utmpx.h>
#include <paths.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <lastlog.h>
#include <paths.h> /* Definition of _PATH_LASTLOG */
#include <fcntl.h>
#include <zconf.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#define KEY_FILE "/some-path/some-file" /* Should be an existing file or one
that this program creates */
int main(int argc, char *argv[])
{
int msqid;
key_t key;
const int MQ_PERMS = S_IRUSR | S_IWUSR | S_IWGRP;
key = ftok(KEY_FILE, 1);
if (key == -1){
perror("ftok");
exit(EXIT_FAILURE);
}
/* 当 msgget() 失败时,尝试以独占方式创建队列 */
while ((msqid = msgget(key, IPC_CREAT | IPC_EXCL | MQ_PERMS)) ==-1) {
if (errno == EEXIST) { /* MQ with the same key already
exists - remove it and try again */
msqid = msgget(key, 0);
if (msqid == -1){
perror("msgget() 无法检索旧队列 ID" );
exit(EXIT_FAILURE);
}
if (msgctl(msqid, IPC_RMID, NULL) == -1){
perror("msgget() 删除旧队列失败 " );
exit(EXIT_FAILURE);
}
printf("Removed old message queue (id=%d)\n", msqid);
}else{ /* Some other error --> give up */
perror("msgget() failed " );
exit(EXIT_FAILURE);
}
}
/*在循环退出时,我们已经成功创建了消息队列,
然后我们可以继续做其他工作...... */
printf("Queue created with id %d\n", msqid);
exit(EXIT_SUCCESS);
}
尽管重新启动的服务器会重新创建 IPC 对象,但如果在创建新 IPC 对象时将同样的 key传递给 get 调用,那么总是会生成同样的标识符。如果服务器重新创建的 IPC 对象使用了同样的标识符,那么客户端就无法知道服务器已经重启并且 IPC 对象已经不包含预期的历史信息了。
为了解决这个问题,内核采用了一个算法(下一节描述),通常能够确保在创建新IPC对象时,对象会得到一个不同的标识符,即使传入的key是一样的。从而所有与老服务器链接的客户端在使用旧的标识符时会从相关IPC系统调用中收到一个错误。
上面程序并没有完全解决在使用System V共享内存时识别出服务器重启的问题,因为共享内存对象只有在所有进程都与其虚拟地址空间分离之后才会被删除。但共享内存对象通常与System V信号量组合使用,而它们则会在IPC_RMID操作中立即会被删除,这意味着客户端在试图访问被删除的信号量对象时能够知道服务器重启这件事情
System V IPC get 调用使用的算法
上图给出了内核内部使用的一些表示System V IPC对象(上面是信号量,但是其他IPC机制类似)相关信息的结构,包括用于计算IPC key的字段。对于每种IPC机制,内核都会维护一个管理的ipc_ids结构,它记录着该IPC机制的所有实例的各种全局信息,包括一个大小会动态变化的指针数组entries,数组中的每个元素执行一个对象实例的关联数据结构(在信号量中是 semid_ds 结构)。entries 数组的当前大小记录在 size 字段中,max_id 字段记录着当前使用中的元素的最大下标
在执行一个 IPC get 调用时,Linux 所采用的算法近似如下(其他系统使用了类似的算法)
- 在关联数据结构列表(entries数组中的元素指向的结构)中搜索key字段与get调用中指定的参数匹配的结构
- 如果没有找到匹配的结构并且没有指定 IPC_CREAT,那么返回 ENOENT 错误。
- 如果找到了一个匹配的结构,但同时指定了 IPC_CREAT 和 IPC_EXCL,那么返回EEXIST 错误。
- 找到一个匹配的结构,直接返回
- 如果没有找到匹配的结构并指定了IPC_CREAT,那么会分配一个新的与所采用的机制对应的关联数据结构(上图是semid_ds)并对其进行初始化:更新pid_ids中的各个字段段,并且可能还会重新设定 entries 数组的大小。指向新结构的指针会被放在entries中第一个未被占用的位置处。在这个初始化过程中包含两个子步骤:
- 传递给get调用的key值被复制到新分配的结构的xxx_perm.__key 字段中
- ipc_ids结构中seq字段的当前值被复制到管理数据结构的xxx_perm.__seq 字段中,将 seq 字段的值加 1
- 使用下面的公式计算 IPC 对象的标识符。其中:
- index表示对象实例在entries数组中的下表
- SEQ_MULTIPLIER 是一个值为 32768 的常数(内核源文件 include/linux/ipc.h 中的 IPCMNI)。
identifier=idex+xxx_perm.__seq *SEQ_MULTIPLIER
对于get调用所采用的算法需要注意如下几点:
- 即使使用同样的 key 创建了一个新 IPC 对象也几乎可以肯定对象被分配到的标识符是不同的,因为标识符的计算是根据保存在关联数据结构中的 seq 字段的值来进行的,而在同种类型的对象的创建过程中都会递增这个值
内核所采用的算法在 seq 的值达到(INT_MAX / IPCMNI)——即 2147483647 / 32768 = 65535——时会将 seq 的值重置为 0。因此如果在系统运行期间已经创建了 65535 个对象,那么新IPC 对象可能会与之前的对象拥有同样的标识符,从而导致新对象会重用之前的对象在 entries 数组中的位置(即在系统运行期间必须要释放之前的对象)。但发生这种情况的可能性非常小。
- 算法为entries数组的每个下标都生成一组不同的标识符值
- 由于常量IPCMNI为每种类型的system V对象的数量设定了一个上限,因此算法确保所有既有IPC对象都拥有一个唯一的标识符
- 给定一个标识符,使用下面这个等式可以快速计算出它在entries数组中对应的下标:
index = identifier % SEQ_MULTIPLIER
当一个进程在执行一个 IPC 系统调用(如 msgctl()、semop()、或 shmat())时传入了一个与既有对象不匹配的标识符,那么就会导致两个错误的发生。如果 entries 中相应下标处是空的,那么将会导致 EINVAL 错误的发生。如果下标指向了一个关联数据结构,但存储在该结构中的序号导致不会产生同样的标识符值,那么就假设这个数组下标指向的旧对象已经被删除了,该下标会被重用。通过错误 EIDRM 可以诊断出这种情况的发生。
获取所有 IPC 对象列表
Linux提供了两种获取系统上所有IPC对象列表的非标准方法
- /proc/sysvipc 目录中的文件会列出所有 IPC 对象。
- 使用 Linux 特有的 ctl 调用
/proc/sysvipc 目录中三个只读文件提供的信息与通过 ipcs 获取的信息是一样的。
- /proc/sysvipc/msg 列出所有消息队列及其特性
- /proc/sysvipc/sem 列出所有信号量集及其特性
- /proc/sysvipc/shm 列出所有共享内存段及其特性。
使用 ipcs 能够获取系统上 IPC 对象的信息。在默认情况下,ipcs 会显示出所有对象
获取给定种类的所有 IPC 对象的最佳可移植的做法是解析 ipcs(1)的输出。
ipcs和ipcrm命令
ipcs -a 是默认的输出信息 打印出当前系统中所有的进程间通信方式的信息
ipcs -m 打印出使用共享内存进行进程间通信的信息
ipcs -q 打印出使用消息队列进行进程间通信的信息
ipcs -s 打印出使用信号进行进程间通信的信息
ipcrm 命令
移除一个消息对象。或者共享内存段,或者一个信号集,同时会将与ipc对象相关链的数据也一起移除。当然,只有超级管理员,或者ipc对象的创建者才有这项权利啦
ipcrm用法
ipcrm -M shmkey 移除用shmkey创建的共享内存段
ipcrm -m shmid 移除用shmid标识的共享内存段
ipcrm -Q msgkey 移除用msqkey创建的消息队列
ipcrm -q msqid 移除用msqid标识的消息队列
ipcrm -S semkey 移除用semkey创建的信号
ipcrm -s semid 移除用semid标识的信号
删除所有共享内存???:
ipcs -m|awk '$2~/[0-9]+/{print $2}'|
while read s
do
ipcrm -m $s
done