XSI IPC
有三种称作 XSI IPC 的 IPC 机制:消息队列、信号量以及共享内存,它们有很多相似之处,本节首先介绍它们相似的特征。
XSI 标识符和键
每个内核中的 IPC 结构(消息队列、信号量或共享内存)都用一个非负整数的标识符加以引用。例如,向一个消息队列发送消息或者从一个消息队列取消息,只需要知道其队列标识符。与文件描述符不同,IPC 标识符不是小的整数。当一个 IPC 结构被创建,又被删除时,与这种结构相关的标识符连续加 1,直至达到一个整型数的最大正值,再又回转到 0。
标识符是 IPC 对象的内部名。为使多个合作进程能够在同一个 IPC 对象上汇聚,需要提供一个外部命名方案。为此,每个 IPC 对象都与一个键相关联,将这个操作为该对象的外部名。
无论何时创建 IPC 结构(调用 msgget、semget 或者 shmget 创建),都应指定一个键,键的数据类型是基本系统数据类型 key_t。
有多种方法使客户进程和服务器进程在同一个 IPC 结构上汇聚:
- 服务器进程可以指定键 IPC_PRIVATE 创建一个新 IPC 结构,将返回的标识符存放在某处(如一个文件)以便客户进程取用。键 IPC_PRIVATE 保证服务器进程创建一个新 IPC 结构。
- 可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键,然后服务器进程指定此键创建一个新的 IPC 结构。
- 客户进程和服务器进程认同一个路径名和项目 ID(项目 ID 是 0~255 之间的字符值),接着调用函数 ftok 将这两个值变换为一个键,然后在上述方法中使用该键。
#include <sys/ipc.h>
key_t ftok(const char *path, int id);
函数 ftok 原型分析:
- 此函数成功时返回 key 值,失败时返回-1 并设置错误号。
- 参数 path 引用一个现有的文件。
- 参数 id 是项目 ID(该参数只使用低 8 位)。
ftok 函数创建的键通常是使用 path 取得 stat 结构,用 stat 结构中的 st_dev 和 st_ino 成员值与项目 ID 组合起来。在 SylixOS 中,st_dev 和 st_ino 是一种设备标识,因此在同一个文件系统中不同文件的键可能相同(当项目 ID 相同时)。
msgget、semget、shmget 都有两个类似的参数,一个 key 和一个整型 flag。在创建一个新队列结构时,如果 key 是 IPC_PRIVATE 或者和当前某种类型的 IPC 结构无关,则需要指明 flag 的 IPC_CREAT 标志位。为了引用一个现有队列,key 必须等于队列创建时指明的 key 的值,并且 IPC_CREAT 不能被指定。
标志 | 含义 |
---|---|
IPC_CREAT | 如果 key 不存在则创建 |
IPC_EXCL | 如果 key 存在则出错 |
IPC_NOWAIT | 非阻塞 |
需要注意的是,不能指定 IPC_PRIVATE 作为键来引用一个现有队列,因为这个特殊的键总是用于创建一个新队列。
如果希望创建一个新的 IPC 结构,而且要确保没有引用具有同一标识符的一个现有 IPC 结构,那么必须在 flag 中同时指定 IPC_CREAT 和 IPC_EXCL 位。这样设置以后,如果 IPC 结构已经存在将出错返回,并设置 errno 为 EEXIST。
XSI 权限结构
XSI IPC 为每一个 IPC 结构关联了一个 ipc_perm 结构。该结构规定了权限和所有者,SylixOS 中该结构定义如下:
struct ipc_perm {
uid_t uid; /* owner's effective user ID */
gid_t gid; /* owner's effective group ID */
uid_t cuid; /* creator's effective user ID */
gid_t cgid; /* creator's effective group ID */
mode_t mode; /* read/write permission */
};
在创建 IPC 结构时,对所有成员都需要赋初值,之后可以调用 msgctl、semctl 或 shmctl 函数修改 uid、gid 和 mode 成员。为了修改这些值,调用进程必须是 IPC 结构的创建者或超级用户,修改这些成员值类似于对文件调用 chown 函数和 chmod 函数。
XSI IPC 信号量
XSI IPC 信号量与管道、命名管道、消息队列不同,它是一个计数器,用于为多个进程提供对共享数据对象的访问。
为了获得共享资源,进程需要执行下列操作:
- 测试控制该资源的信号量。
- 若此信号量的值为正,则进程可以使用该资源。在这种情况下,进程会将信号量值减 1,表示它使用了一个资源单位。
- 否则,若此信号量的值为 0,则进程进入休眠状态,直至信号量值大于 0,进程被唤醒后,它返回至第一步。
当进程不再使用由一个信号量控制的共享资源时,该信号量值增 1。如果有进程正在休眠等待此信号量,则唤醒它们。
为了正确地实现信号量,信号量值的测试及减 1 操作应当是原子操作。为此,信号量通常是在内核中实现的。
常用的信号量形式被称为二元信号量,它控制单个资源,其初始值为 1,但是,一般而言,信号量的初始值可以是任意一个正值,该值表明有多少个共享资源单位可供共享应用。
内核为每个信号量集合维护者一个 semid_ds 结构:
struct semid_ds {
struct ipc_perm sem_perm; /* operation permission structure */
u_short sem_nsems; /* number of semaphores in set */
time_t sem_otime; /* last semop^) time */
time_t sem_ctime; /* last time changed by semctl() */
……
};
在 SylixOS 下,每一个信号量由下面结构定义:
struct sem {
unsigned short semval; /* semaphore value */
pid_t sempid; /* pid of last operation */
unsigned short semncnt; /* # awaiting semval > cval */
unsigned short semzcnt; /* # awaiting semval == 0 */
};
当使用一个 XSI IPC 信号量时,需要首先调用 semget 函数:
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
函数 semget 原型分析:
- 此函数成功时返回信号量 ID,失败时返回-1 并设置错误号。
- 参数 key 是 ftok 函数返回的键。
- 参数 nsems 是该集合中的信号量数。
- 参数 flag 是信号量标志,如下表所示。
semctl 函数包含了多种信号量操作,如下表所示。
标志 | 含义 |
---|---|
IPC_CREAT | 如果 key 不存在则创建 |
IPC_EXCL | 如果 key 存在则出错 |
IPC_NOWAIT | 非阻塞 |
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 semid 是信号量 ID。
- 参数 semnum 是信号量数。
- 参数 cmd 是命令。
- 参数 ... 是可变参数。
semctl 函数可变参数根据 cmd 是可选的,其类型是 union semun,它是多个命令特定参数的联合:
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
unsigned short *array; /* array for GETALL & SETALL */
};
函数 semop 自动执行信号量集合上的操作数组:
#include <sys/sem.h>
int semop(int semid, struct sembuf *semoparray, size_t nops);
函数 semop 原型分析:
- 参数 semid 是信号量 ID。
- 参数 semoparray 指向一个由 sembuf 结构表示的信号量操作数组。
- 参数 nops 是信号量操作数组的数量。
以下是 struct sembuf 结构的类型定义:
struct sembuf {
u_short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
XSI IPC 消息队列
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。msgget 函数用于创建一个新队列或打开一个现有队列。msgsnd 函数将新消息添加到队列尾端。每个消息包含一个正的长整型类型的字段、一个非负的长度以及实际数据字节数,所有这些都在将消息添加到队列时,传送给 msgsnd 函数。msgrcv 函数用于从队列中取消息,我们并不一定要以先进先出次序取消息,也可以按消息的类型取消息。
每个队列都有一个 msgid_ds 结构与其相关联:
struct msqid_ds {
struct ipc_perm msg_perm; /* msg queue permission bits */
msgqnum_t msg_qnum; /* number of msgs in the queue */
msglen_t msg_qbytes; /* max # of bytes on the queue */
pid_t msg_lspid; /* pid of last msgsnd() */
pid_t msg_lrpid; /* pid of last msgrcv() */
time_t msg_stime; /* time of last msgsnd() */
time_t msg_rtime; /* time of last msgrcv() */
time_t msg_ctime; /* time of last msgctl() */
……
};
此结构定义了队列的当前状态,不同的系统可能包含不同的成员。
XSI IPC 消息队列调用的第一个函数是 msgget 函数,该函数可以打开一个现有队列或者创建一个新的队列。
#include <sys/msg.h>
int msgget(key_t key, int flag);
函数 msgget 原型分析:
- 此函数成功时返回非负队列 ID,失败时返回-1 并设置错误号。
- 参数 key 是由 ftok 函数创建的键或者 IPC_PRIVATE 指定创建新 IPC 结构。
- 参数 flag 是消息创建标志,如下表所示。
msgctl 函数对队列执行多种操作,类似于 ioctl 函数。XSI IPC 标志位如下表所示。
标志 | 含义 |
---|---|
IPC_CREAT | 如果 key 不存在则创建 |
IPC_EXCL | 如果 key 存在则出错 |
IPC_NOWAIT | 非阻塞 |
#include <sys/msg.h>
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 msgid 是 msgget 函数返回的消息 ID。
- 参数 cmd 是命令,XSI IPC 命令如下表所示。
- 参数 buf 是 msgid_ds 结构指针。
命令 | 含义 |
---|---|
IPC_STAT | 取此队列的 msgid_ds 结构,并将它存放在 buf 中 |
IPC_SET | 将 buf 中的成员 msg_perm.uid、msg_perm.gid 和 msg_perm.mode 赋值给与这个队列相关的 msqid_ds 结构中 |
IPC_RMID | 从系统中删除该消息队列以及仍在该队列中的所有数据 |
调用 msgsnd 函数将数据放到消息队列中:
#include <sys/msg.h>
int msgsnd(int msgid, const void *ptr, size_t nbytes, int flag);
函数 msgsnd 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 msgid 是 msgget 函数返回的消息 ID。
- 参数 ptr 是消息指针。
- 参数 nbytes 是消息体中消息字节数。
- 参数 flag 是消息标志。
正如前面提及的,每个消息都由 3 部分组成,一个正的长整型类型的字段,一个非负的长度(nbytes)以及实际数据字节数(对应于长度),消息总是放在队列尾端。
参数 ptr 是指向 mymesg 结构的指针,该结构包含了长整型的消息类型和消息数据,如下定义一个 512 字节的消息结构:
struct mymesg {
long mtype; /* 消息类型 */
char mtest[512]; /* 消息体 */
};
msgrcv 函数从队列中取用消息。
#include <sys/msg.h>
ssize_t msgrcv(int msgid, void *ptr, size_t nbytes, long type, int flag);
函数 msgrcv 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 msgid 是 msgget 函数返回的消息 ID。
- 参数 ptr 是消息指针。
- 参数 nbytes 是消息缓冲区长度。
- 参数 type 是消息类型。
- 参数 flag 是消息标志。
同 msgsnd 函数一样,ptr 参数指向一个长整型数(其中存储的是返回的消息类型),其后是存储实际消息数据的缓冲区。参数 type 可以指定想要哪一种消息:
- type == 0 返回队列中的第一个消息。
- type > 0 返回队列中消息类型为 type 的第一个消息。
- type < 0 返回队列中消息类型值小于等于 type 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。
type 值非 0 用于以非先进先出次序读消息。例如,若应用程序对消息赋予优先权,那么 type 就可以是优先权值。如果一个消息队列由多个客户进程和一个服务进程使用,那么 type 字段可以用来包含客户进程的进程 ID(只要进程 ID 可以存放在长整型中)。
msgrcv 成功执行时,内核会更新与该消息队列相关联的 msgid_ds 结构,以指示调用者的进程 ID 和调用时间,并指示队列中的消息数减少了 1 个。
XSI IPC 共享内存
共享存储允许两个或多个进程共享一个给定的存储区,因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种 IPC。使用共享存储时要掌握的唯一窍门是,在多个进程之间同步访问一个给定的存储区。若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据。通常,信号量用于同步共享存储访问,XSI 共享内存和内存映射的文件的不同之处在于,前者没有相关的文件,XSI 共享内存段是内存的匿名段。
内核为每个共享内存段维护着一个结构,SylixOS 中实现该结构如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation permission structure */
size_t shm_segsz; /* size of segment in bytes */
pid_t shm_lpid; /* process ID of last shared memory op */
pid_t shm_cpid; /* process ID of creator */
shmatt_t shm_nattch; /* number of current attaches */
time_t shm_atime; /* time of last shmat() */
time_t shm_dtime; /* time of last shmdt() */
time_t shm_ctime; /* time of last change by shmctl() */
void *shm_internal;
};
XSI IPC 调用的第一个函数是 shmget 函数,它获得一个共享内存标识符:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
函数 shmget 原型分析:
- 此函数成功时返回共享内存 ID,失败时返回-1 并设置错误号。
- 参数 key 是 ftok 函数返回键。
- 参数 size 是共享内存区的长度。
- 参数 flag 是共享内存标志。
参数 size 是该共享内存区的长度,以字节为单位,实现通常将其向上取为系统页长的整数倍。但是,若应用指定的 size 值并非系统页长的整数倍,那么最后一页的余下部分是不可使用的。如果正在创建一个新段,则必须指定其 size。如果正在引用一个现存的段,则将 size 指定为 0。当创建一个新段时,段内的内容初始化为 0。
shmctl 函数对共享存储段执行多种操作:
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数 shmctl 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 shmid 是共享内存 ID。
- 参数 cmd 是命令。
- 参数 buf 是结构 shmid_ds 结构指针。
一旦创建一个共享内存段,进程就可调用 shmat 函数将其连接到它的地址空间中。
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
函数 shmat 原型分析:
- 此函数成功返回映射的内存地址,失败返回 MAP_FAILED 并设置错误号。
- 参数 shmid 是共享内存 ID。
- 参数 addr 必须为 NULL。
- 参数 flag 是共享内存标志。
如果在 flag 中指定了 SHM_RDONLY 位,则以只读方式连接此段。否则以读写方式连接此段。
shmat 函数的返回值是该段所连接的实际地址,如果出错则返回 MAP_FAILED。如果 shmat 函数成功执行,那么内核将使该共享内存段 shmid_ds 结构中的 shm_nattach 计数器值加 1。当对共享存储段的操作已经结束时,则调用 shmdt 函数脱接该段。注意,这并不从系统中删除其标识符以及数据结构。该标识符仍然存在,直至某个进程调用 shmctl(带命令 IPC_RMID)特地删除它。
#include <sys/shm.h>
int shmdt(const void *addr);
函数 shmdt 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 addr 是要脱接的内存地址。
addr 参数是以前调用 shmat 函数时的返回值。如果成功,shmdt 函数将使相关 shmid_ds 结构中的 shm_nattach 计数器值减 1。
内核将共享内存区放在什么位置上与系统密切相关。下面程序打印各数据内存位置信息。
#include <stdlib.h>
#include <mman.h>
#include <sys/shm.h>
#define ARRAY_SIZE (4096)
#define MALLOC_SIZE (4096)
#define SHM_SIZE (4096)
#define SHM_MODE (0666)
static char array[ARRAY_SIZE];
int main (int argc, char *argv[])
{
int shmid;
char *ptr, *shmptr;
fprintf(stdout, "global data area from [0x%lx] to [0x%lx]\n",
(unsigned long)&array[0],
(unsigned long)&array[ARRAY_SIZE]);
fprintf(stdout, "stack area [0x%lx]\n", (unsigned long)&shmid);
ptr = malloc(MALLOC_SIZE);
if(ptr == NULL) {
fprintf(stderr, "malloc error");
return (-1);
}
fprintf(stdout, "heap area from [0x%lx] to [0x%lx]\n", (unsigned long)ptr,
(unsigned long)ptr + MALLOC_SIZE);
if((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0) {
fprintf(stderr, "shmget error");
return (-1);
}
if((shmptr = shmat(shmid, 0, 0)) == MAP_FAILED) {
fprintf(stderr, "shmat error");
return (-1);
}
fprintf(stdout, "shared memory area from [0x%lx] to [0x%lx]\n",
(unsigned long)shmptr, (unsigned long)shmptr + SHM_SIZE);
if(shmctl(shmid, IPC_RMID, 0) < 0) {
fprintf(stderr, "shmctl error");
return (-1);
}
return (0);
}
在 SylixOS 下的运行结果:
# ./Shared_Memory_Distribution
global data area from [0xc00107cc] to [0xc00117cc]
stack area [0x30915dc4]
heap area from [0xc0032400] to [0xc0033400]
shared memory area from [0xc0006000] to [0xc0007000]
从运行结果可以看出 SylixOS 的共享内存区在堆内存区之下。注意,XSI IPC 的共享内存映射的内存并没有与具体的文件相关联,而 mmap 函数映射的内存是与具体的文件相关联。