“IPC三剑客”,我们把它们放在一起讲,原因是无论在POSIX还是XSI标准中,这三种IPC类型的访问函数和描述信息有很多相似的地方。
本质上,这三种IPC又不尽相同,因为:
- message queue 可以和pipe、FIFO划归一类,属于message passing(消息传递);
- semaphore 可以和各类锁划归一类,属于synchronization(同步);
- shared memory 可以和memory map I/O联系起来,但常常又掺杂着信号量的使用;
我们事先给出POSIX IPC和System V IPC在接口函数上的区别(当然在《IPC基础》中已经给过),但本文并不会陷入具体接口函数的区别中。下面是两种标准的IPC函数:
- 注意两种IPC标准中header与functions的一些差别。
讲“IPC三剑客”时再重温一下前面的两个点,
首先,他们是named,所以能提供unrelated processes之间的访问;
其次,它们至少是kernel-persistent的,所以除非显式的删除,否则进程终止的时候它们仍然存在。
那么,下面我们就分别来看这三部分。
1. Message Queue
(1)什么是消息?
要看消息队列,就要首先弄清楚什么是message。那我们就看看stream 和 message:
- byte stream字节流:这是UNIX原生的I/O模型;No record boundaries exist--reads and writes do not examine the data at all. 举例来说,从FIFO中读100个byte,它无法判定另一个进程写入FIFO时,是进行了100次写1 byte、2次写50 byte还是其它组合情况;
- standart I/O stream:FILE structure;
- message消息:更为结构化,每个消息都有length 和 priority ("type" in system V),由发送者指定;每个message是一个record(记录),类似于UDP datagrams(数据报)。
(2)消息队列的结构是怎样的?
从图中可以清晰的看出,消息队列以链表形式连了起来,每个message主要包括length(长度)、priority(or "type")和 data。
正如前面所提到的,message queue一经创建,除非关闭系统或显式地删除,否则一直存在;既然这个队列一直存在,那么必然有一个name:
- POSIX中的name:mq_open()有一个参数为char *name,它就是message queue name;该函数返回message queue descriptor(mqd_t类型,实现为指针,所以称之为descriptor并不恰当),用于队列的操作;
- System V中的name:是一个key值(key_t类型,<sys/types.h>),由ftok()将pathname和project id结合生成的一个key;另外,还有一个identifier(标识符)用于操作队列;(每个XSI IPC结构都有一个identifier);
这样看来,POSIX中的message queue name对应System V中的key,用于不同进程间找到通信IPC结构;POSIX中的message queue descriptor对应System V中的identifier,用于IPC结构的操作。
(3)如何进行操作?
首先请对比最上面一幅图中两种标准的函数。
在POSIX标准里,关于message queue的open、close、unlink等操作像极了file的基本操作,mq_send、mq_receive对应msgsnd、msgrcv,当然,POSIX中还有一个XSI没有实现的mq_notify函数,该函数允许了asynchronous event notification(异步事件通知),避免了XSI中的轮询(polling),使进程不必时刻循环等待message的到达,极大减少了CPU的浪费。
关于POSIX消息队列的异步事件通知,mq_notify告知何时有个消息放到了某个空的消息队列中,这种通知有两种方式可供选择:
- 产生一个信号;
- 创建一个thread执行处理函数;
关于signal的notification我们在信号中说过,这里就不细说了。下面通过POSIX的逻辑流程来说明message queue的IPC通信机制:
分为mqcreate,mqsend与mqreceive三个阶段,
/* mqcreate: create message queue, run command is "./mqcreate /home/mq_name"(note: /home/mq_name is the name) */
#include "unpipc.h"
int main(int argc, char *argv[])
{
int c, flags;
mqd_t mqd;
/* 1. init code of mq_open() arguments ...*/
flags = O_RDWR | O_CREAT;
/* 2. open mq */
mqd = mq_open(argv[1]/*mq_name*/, flags, FILE_MODE, NULL);
/* 3. close mq */
mq_close(mqd);
exit(0);
}
/* mqsend: send message, run command is "./mqsend /home/mq_name <#bytes(i.e. length)> <priority>" */
int main(int argc, char *argv[])
{
/* 1. variable init ... */
mqd_t mqd; //message queue descriptor
void *msg_ptr; //point to the message buffer
int length; //<#bytes>
/* 2. open mq */
mqd = mq_open(mq_name, O_WRONLY);
/* 3. allocate the memory */
mes_ptr = calloc(length, sizeof(char));
/* 4. send message in the buffer */
mq_send(mqd, ptr, length, priority);
exit(0);
}
/* mqreceive: receive message, run command is "./mqreceive /home/mq_name" */
int main(int argc, char *argv[])
{
/* 1. init code */
mqd_t mqd;
uint_t priority;
void *buff;
struct mq_attr attr;
/* ... */
/* 2. open mq */
mqd = mq_open(mq_name, flags);
/* 3. allocate message buffer */
mq_getattr(mqd, &attr);
buff = malloc(attr.mq_msgsize);
/* 4. receive message */
n = mq_receive(mqd, buff, attr.mq_msgsize, &priority);
exit(0);
}
- mqcreate创建消息队列;
- mqsend打开消息队列,向里面写数据;
- mqreceive打开消息队列,从里面读数据;
- 这里面没有到mq_notify,但最基本的流程是上面的情况
在关于XSI IPC message queue的代码调试中遇到了两个新的知识点,这里提一下:
(1)volatile关键字
作用是告诉编译器不要优化到寄存器中,直接从内存里读数。【C语言中volatile关键字的作用: http://blog.csdn.net/tigerjibo/article/details/7427366】
(2)atexit(void (*func)(void))函数
register func函数:当进程正常退出时,开始执行func;比如atexit(remove_msg)意思是,当进程正常退出时,执行删除消息的函数;【atexit函数: http://blog.csdn.net/huhaihong/article/details/2189709】
2. 信号量
semaphore是IPC同步原语,UNPv2中讨论了三种信号量:
a. POSIX named semaphore(kernel-persistent);
b. POSIX memory-based semaphore(process-persistent);
c. System V semaphore(kernel-persistent)。
当然我们还是求同存异,具体差别请看UNPv2,我们主要讨论信号量的使用问题。
(1)semaphore与mutex&cond_var有什么差别?
都属于同步机制,所以肯定有很多人会把他们搞混,那我们来看他们的差别:
- 1首先是最容易想到的,semaphore可以是二值互斥状态,也可以是多值,而mutex只能是二值的互斥量;
- 第2点也很容易,在前面反复提到过,semaphore是kernel-persistent,而mutex&cond_var是process-persistent;
- 第3点是,mutex是谁给它上锁谁就得给它解锁,进程终止自动解锁;而semaphore可以是A给临界区上锁(sem_wait)然后B解锁(sem_post);
- 4是关于信号的,semaphore的计数值是与其关联的状态,只要有进程post,计数就会+1,不管有没有进程阻塞wait;而cond_var则不一样,如果没有线程阻塞在pthread_cond_wait,那么调用pthread_cond_signal的时候,信号将丢失。
- 5在很多同步技术中(互斥量、条件变量、读写锁、信号量,)只有sem_post()是唯一可以在signal handler中被安全调用的;
以上五点如果理解了信号量作为一种IPC同步的本质并不难懂,但我们还要从设计目的上说一说几种同步技术的区别:
- mutex是为了locking而优化的,condition variable是为waiting而优化的,而信号量呢?它既可以用于上锁,也可以用于等待,但导致的开销和复杂性变得更高;
- 时间上,共享存储中的mutex和record locking比信号量快;不使用mutex的原因是,虽然mutex是最快的,但进程共享中的互斥量没有得到普遍支持;我们倾向于使用记录锁;
(2)信号量的操作
关于具体的操作,POSIX和XSI当然不同,信号量也许是这两种标准在“IPC三剑客”中差别最大的。但当然我们不去关注细节,具体操作会在随后的程序中给出示例。
3. 共享内存
(1)什么是共享内存?
我们从pipe、FIFO和message看起,它们的通信方式是这样的:
而shared memory则是在用户空间的,
从两幅图的对比中可以看出,shared memory同时出现在client address space与server address space中;在POSIX和System V这两个标准中,关于shared memory的接口是最类似的;
有一点值得注意的是,fork出的子程序并不与其父进程共享内存区,假如对一个全局变量static int count = 0; 在fork之后,父子进程各自对其+1.那么父子进程内各自的count副本都会为1.(见UNPv2 12.1)
(2)memory map I/O(存储映射I/O)
简单的说,memory map把file或shared memory映射到进程地址空间中。
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
/* Returns: starting address of mapped region if OK, MAP_FAILED on error */
mmap函数是个很重要的函数,但这里我们只关注三个参数:
mmap将文件描述符fd映射到进程地址空间中的addr;flags有多种,但MAP_PRIVATE(进程对映射数据改动时,不改变其底层支撑对象——文件对象或共享内存区对象)和MAP_SHARED(改动映射数据时底层也相应改动)必须选一个。
(3)共享内存与信号量的操作
该代码的目标是:多个client给共享的计数器持续+1,server用于创建和维护shm、sem;
- server:创建shm --> mmap映射 --> 创建sem
- client:fd=shm_open打开fd --> ptr=mmap映射--> close关闭fd --> sem_open创建二值信号量 --> 用sem维护共享count++;
/* server */
#include "unpipc.h"
struct shmstruct { /* struct stored in shared memory */
int count;
};
sem_t *mutex; /* pointer to named semaphore */
int
main(int argc, char **argv)
{
int fd;
struct shmstruct *ptr;
if (argc != 3)
err_quit("usage: server1 <shmname> <semname>");
shm_unlink(Px_ipc_name(argv[1])); /* OK if this fails */
/* create shm, set its size, map it, close descriptor */
fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR | O_CREAT | O_EXCL, FILE_MODE);
Ftruncate(fd, sizeof(struct shmstruct)); // set size
ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
Close(fd);
sem_unlink(Px_ipc_name(argv[2])); /* OK if this fails */
mutex = Sem_open(Px_ipc_name(argv[2]), O_CREAT | O_EXCL, FILE_MODE, 1);
Sem_close(mutex);
exit(0);
}
/* one client */
#include "unpipc.h"
struct shmstruct { /* struct stored in shared memory */
int count;
};
sem_t *mutex; /* pointer to named semaphore */
int
main(int argc, char **argv)
{
int fd, i, nloop;
pid_t pid;
struct shmstruct *ptr;
if (argc != 4)
err_quit("usage: client1 <shmname> <semname> <#loops>");
nloop = atoi(argv[3]);
fd = Shm_open(Px_ipc_name(argv[1]), O_RDWR, FILE_MODE);
ptr = Mmap(NULL, sizeof(struct shmstruct), PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
Close(fd); /* no use again */
mutex = Sem_open(Px_ipc_name(argv[2]), 0);
pid = getpid();
for (i = 0; i < nloop; i++) {
Sem_wait(mutex);
printf("pid %ld: %d\n", (long) pid, ptr->count++);
Sem_post(mutex);
}
exit(0);
}
好,下一篇我们会通过经典的producer-consumer problem(生产者-消费者问题)来把各种同步和IPC机制联系起来。