消息队列、信号量和共享存储是IPC(进程间通信)的三种形式,它们功能不同,但有相似之处,下面先介绍它们的相似点,然后再逐一说明。
1、相似点
每个内核中的IPC结构(消息队列、信号量和共享存储)都用一个非负整数的标识符加以引用,与文件描述符不同,当一个IPC结构被创建,以后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正直,然后又回转到0。标识符是IPC对象的内部名,还有一个外部名称为键,数据类型是key_t,通常在头文件
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
消息队列、信号量和共享存储都有自己的get函数,msgget、semget和shmget,用于创建IPC对象,它们都设置了自己的ipc_perm结构,在头文件
struct ipc_perm {
key_t __key; /* Key supplied to msgget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
消息队列、信号量和共享存储都有自己的内置限制,这些限制的大多数可以通过重新配置内核而加以更改,如sysctl命令,可以配置运行时内核参数,在Linux(Ubuntu)上,运行命令“ipcs -l”可查看相关限制,如下:
$ ipcs -l
------ Messages Limits --------
max queues system wide = 32000
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384
------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 18014398509465599
max total shared memory (kbytes) = 18014398442373116
min seg size (bytes) = 1
------ Semaphore Limits --------
max number of arrays = 32000
max semaphores per array = 32000
max semaphores system wide = 1024000000
max ops per semop call = 500
semaphore max value = 32767
需要注意的是,IPC对象是在系统范围内起作用的,没有访问计数,不同于普通文件。例如,如果进程创建了一个消息队列,在该队列中放入了几则消息,然后终止,但是该消息队列及其内容并不会被删除,它们余留在系统中直至出现下述情况:由某个进程调用msgrcv读消息或msgctl删除消息队列;或某个进程执行ipcrm命令删除消息队列;或由正在再启动的系统删除消息队列。将此与管道相比,当最后一个访问管道的进程被终止时,管道就被完全地删除了。对于FIFO而言,虽然当最后一个引用FIFO的进程终止时其名字仍保留在系统中,直至显式地删除它,但是留在FIFO中的数据却在此时被全部删除。
2、消息队列
消息队列即message queue,存放在内核中并由消息队列标识符标识,涉及如下函数和数据结构。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
msgget用于创建一个新队列或打开一个现存的队列,参数key可自定义或通过ftok生成,或者使用IPC_PRIVATE,需要保证的是key值没有与现存的队列相关联,msgflag为O_CREAT时创建新队列,排它性使用O_EXCL。msgsnd将新消息添加到队列尾端,参数msqid为消息队列id,msgp比较特殊,需要包括两部分内容,消息类型和实际的消息数据,如上面的msgbuf结构,msgsz指定消息长度,msgflag可以设置为IPC_NOWAIT,表示非阻塞。msgrcv用于从队列中取消息,参数msgsz表示缓冲区长度,当消息长度大于msgsz时,若msgflg设置了MSG_NOERROR则截短消息,否则出错E2BIG,msgtyp为0时获取第一个消息,大于0时获取类型为msgtyp的第一个消息,小于0时获取类型小于等于msgtyp的类型值最小的第一个消息。每个消息队列都有一个msqid_dt结构与其相关联,规定了队列的当前状态,msgctl则可以对消息队列的这种结构进行操作,参数cmd可以是IPC_STAT、IPC_SET、IPC_RMID,分别表示获取状态、设置状态、移除消息队列。
下面例子说明消息队列的用法,msgsnd.c发送消息,msgrcv.c接收消息,当输入“quit”时结束。
// msgsnd.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
#include <unistd.h>
struct msg_st
{
long int msg_type;
char text[BUFSIZ];
};
int main(void)
{
struct msg_st data;
data.msg_type = 1;
char buf[BUFSIZ];
key_t akey = 1000;
int msgid = -1;
bool running = true;
// create message queue
msgid = msgget(akey, 0666 | IPC_CREAT);
if (-1 == msgid) {
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}
// loop for sending data to message queue
while (running) {
printf("Input text: ");
fgets(buf, BUFSIZ, stdin);
strcpy(data.text, buf);
// send data
if (-1 == msgsnd(msgid, (void*)&data, BUFSIZ, 0))
{
fprintf(stderr, "msgsnd failed\n");
exit(EXIT_FAILURE);
}
// input "quit" to finish
if(0 == strncmp(buf, "quit", 4)) {
running = false;
}
sleep(1);
}
exit(EXIT_SUCCESS);
}
// msgrcv.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
#include <unistd.h>
struct msg_st
{
long int msg_type;
char text[BUFSIZ];
};
int main(void)
{
struct msg_st data;
data.msg_type = 0;
key_t akey = 1000;
int msgid = -1;
bool running = true;
// create messge queue
msgid = msgget(akey, 0666 | IPC_CREAT);
if (-1 == msgid) {
fprintf(stderr, "msgget failed with error: %d\n", errno);
exit(EXIT_FAILURE);
}
// loop for getting data from message queue
while (running) {
// receive data
if(-1 == msgrcv(msgid, (void*)&data, BUFSIZ, data.msg_type, 0))
{
fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
exit(EXIT_FAILURE);
}
printf("Receive text: %s\n",data.text);
// receive "quit" to finish
if(0 == strncmp(data.text, "quit", 4)) {
running = false;
}
}
// delete message queue
if (-1 == msgctl(msgid, IPC_RMID, 0))
{
fprintf(stderr, "msgctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
3、信号量
信号量semaphore确切的说是一种同步方式,涉及如下函数和数据结构。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, ...);
int semop(int semid, struct sembuf *sops, unsigned nsops);
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
struct sembuf
{
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
};
struct
{
unsigned short semval; /* semaphore value */
unsigned short semzcnt; /* # waiting for zero */
unsigned short semncnt; /* # waiting for increase */
pid_t sempid; /* ID of process that did last op */
}
信号量是一个计数器,用于多进程对共享数据对象的访问。为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若此信号量的值为正,则进程可以使用该资源,进程将信号量值减1,表示它使用了一个资源单位。
(3)若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程被唤醒后,它返回第(1)步。
当进程不再使用由一个信号量控制的共享资源时,该信号量值增1,如果有进程正在休眠等待此信号量,则唤醒它们。为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作,为此,信号量通常是在内核中实现的。常用的信号量形式被成为二元信号量或双态信号量,它控制单个资源,初始值为1,但是一般而言,信号量的初值可以是任一正值,该值说明有多少个共享资源单位可供共享使用。需要注意的是,信号量并非是单个非负值,为一个包含了一个或多个信号量值的信号量集,创建信号量需要指定信号量集中的信号个数。
semget用于获取信号量集标识符,参数nsems表示信号量个数,创建新的信号量集时必须大于0,获取已有的则为0。semctl对信号量进行操作,第四个参数可选,类型为semun联合,semnum指定信号量集中的某个信号,cmd同消息队列的msgctl一样也可以是IPC_STAT、IPC_SET、IPC_RMID,还有形如GETXXX的值。semop函数是个原子操作,自动执行信号量集合上的操作数组sops,sops为sembuf结构体,成员sem_op可以为0、正数、负数,sem_flg为IPC_NOWAIT或SEM_UNDO,后者表示进程终止时自动处理还未处理的信号量,参数nsops规定该数组中操作的数量。
先来看一个不使用信号量的例子:
// semaphore2.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
if (argc > 1) {
message = argv[1][0];
}
for (i = 0; i < 10; ++i) {
printf("%c", message);
fflush(stdout);
sleep(rand() % 3);
printf("%c", message);
fflush(stdout);
sleep(rand() % 3);
}
sleep(10);
printf("\n%d - finished\n", getpid());
exit(EXIT_SUCCESS);
}
编译运行:
$gcc -o sem semaphore2.c
$./sem A & ./sem
[1] 5647
AXAXAXAXXAAXAXAXAAXXAXXAXAAXAXAXAXAXAXXA
5648 - finished
5647 - finished
[1]+ Done ./sem A
一个进程在for循环中连续两次输出A,并启动到后台运行,另一个进程在for循环中连续两次输出X,从上面的结果可以看出,它们相互竞争,结果是乱序的,并不是两个连续的A或者X,下面用信号量改写上面的例子:
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
static int sem_id = 0;
int main(int argc, char *argv[])
{
key_t akey = 1000;
char message = 'X';
int i = 0;
// create semaphore
sem_id = semget(akey, 1, 0666 | IPC_CREAT);
if (-1 == sem_id) {
fprintf(stderr, "Failed to create semaphore\n");
exit(EXIT_FAILURE);
}
if (argc > 1) {
// semaphore initialization, must
union semun sem_union;
sem_union.val = 1;
if (-1 == semctl(sem_id, 0, SETVAL, sem_union)) {
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
message = argv[1][0];
sleep(1);
}
for (i = 0; i < 10; ++i) {
// go into critical zone
struct sembuf sem_i;
sem_i.sem_num = 0;
sem_i.sem_op = -1;
sem_i.sem_flg = SEM_UNDO;
if (-1 == semop(sem_id, &sem_i, 1))
{
perror("semop in failed\n");
exit(EXIT_FAILURE);
}
printf("%c", message);
fflush(stdout);
sleep(rand() % 3);
printf("%c", message);
fflush(stdout);
// leave critical zone
struct sembuf sem_o;
sem_o.sem_num = 0;
sem_o.sem_op = 1;
sem_o.sem_flg = SEM_UNDO;
if (-1 == semop(sem_id, &sem_o, 1)) {
perror("semop out failed\n");
exit(EXIT_FAILURE);
}
sleep(rand() % 3);
}
sleep(10);
printf("\n%d - finished\n", getpid());
if (argc > 1) {
// delete samaphore
sleep(3);
union semun sem_union;
if (-1 == semctl(sem_id, 0, IPC_RMID, sem_union)) {
fprintf(stderr, "Failed to delete semaphore\n");
}
}
exit(EXIT_SUCCESS);
}
执行结果如下:
XXAAXXAAXXAAXXAAXXAAXXAAXXAAXXAAXXXXAAAA
可见,使用了信号量,输出结果符合预期,两个A或者两个X连在了一起。
4、共享存储
共享存储允许两个或多个进程共享一给定的存储区,因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。使用共享存储时需要掌握的唯一窍门是多个进程之间对一给定存储区的同步访问,若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据,通常,信号量被用来实现对共享存储访问的同步。下面是相关的几个函数和数据结构:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
shmget用于获取共享存储标识符,参数size为共享存储区的长度,单位是字节,实现通常将其向上取为系统页长的整数倍,若size并非系统页长的整数倍,那么最后一页的余下部分是不可用的,创建一个新的共享存储区时size需要大于0,引用已有的共享存储区则将size设置为0。shmctl可操作共享存储区,同样可以是IPC_STAT、IPC_SET、IPC_RMID等。
shmat用于将共享存储段连接到调用进程指定的地址shmaddr上,但一般应指定shmaddr为0,内核会自动选择合适的地址,shmflg可选SHM_RND即地址取整,SHM_RDONLY只读,默认读写。当对共享存储段的操作结束时,调用shmdt取消当前进程与共享存储段的连接。
下面是一个使用了shm的例子,程序中fork之后,子进程sleep保证父进程先执行,父进程取得共享存储区以后写入“hello world”,子进程同样也取得了这个共享存储区,然后访问同一块地址,读到了“hello world”。
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#define SIZE 1024
#define exit_err(str) do { perror(str); exit(EXIT_FAILURE); } while (0);
#define uint32 unsigned long
int main(void)
{
int shmid;
char *shmptr;
key_t key;
pid_t pid;
if ((pid = fork()) < 0) {
exit_err("fork error");
}
if(0 == pid) {
printf("child process\n");
if ((key = ftok("/dev/null", O_RDWR)) < 0) {
exit_err("ftok error");
}
if ((shmid = shmget(key, SIZE, 0600 | IPC_CREAT)) < 0) {
exit_err("shmget error");
}
if ((shmptr = (char*)shmat(shmid, 0, 0)) == (void*)-1) {
exit_err("shmat error");
}
sleep(1);
printf("child pid is %d, share memory from %lx to %lx, content: %s\n",getpid(), (uint32)shmptr, (uint32)(shmptr + SIZE), shmptr);
sleep(1);
if ((shmctl(shmid, IPC_RMID, 0) < 0)) {
exit_err("shmctl error");
}
exit(EXIT_SUCCESS);
}
else {
printf("parent process\n");
if ((key = ftok("/dev/null", O_RDWR)) < 0) {
exit_err("ftok error");
}
if ((shmid = shmget(key, SIZE, 0600 | IPC_CREAT | IPC_EXCL)) < 0) {
exit_err("shmget error");
}
if((shmptr = (char*)shmat(shmid, 0, 0)) == (void*)-1) {
exit_err("shmat error");
}
memcpy(shmptr, "hello world", sizeof("hello world"));
printf("parent pid is %d, share memory from %lx to %lx, content: %s\n",getpid(),(uint32)shmptr, (uint32)(shmptr + SIZE), shmptr);
}
waitpid(pid, NULL, 0);
exit(EXIT_SUCCESS);
}
执行结果如下:
parent process
parent pid is 7275, share memory from 7fb2ebf4a000 to 7fb2ebf4a400, content: hello world
child process
child pid is 7276, share memory from 7fb2ebf4a000 to 7fb2ebf4a400, content: hello world