apue学习第二十三天——消息队列、信号量与共享内存

“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机制联系起来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
习题 9.2: 编写一个程序,创建一个共享内存区,然后将一个文件映射到该共享区中。编写两个程序,一个程序将数据写到共享区中,另一个程序读取共享区中的数据,并将结果写到标准输出。使用信号量进行同步。 解答: 以下是一个简单的实现: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/shm.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <semaphore.h> #define SHM_SIZE 1024 // 共享内存区大小 #define SEM_NAME "/mysem" // 信号量名称 int main(int argc, char *argv[]) { int shm_fd; void *shm_ptr; sem_t *sem_ptr; int value = 0; // 创建共享内存区 shm_fd = shm_open("myshm", O_CREAT | O_RDWR, 0666); ftruncate(shm_fd, SHM_SIZE); // 映射共享内存区 shm_ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); // 创建信号量 sem_ptr = sem_open(SEM_NAME, O_CREAT, 0666, 1); // 写入数据 sem_wait(sem_ptr); // 申请信号量 sprintf(shm_ptr, "%s", "Hello, world!"); sem_post(sem_ptr); // 释放信号量 // 读取数据 sem_wait(sem_ptr); // 申请信号量 printf("%s\n", (char*)shm_ptr); sem_post(sem_ptr); // 释放信号量 // 销毁信号量 sem_close(sem_ptr); sem_unlink(SEM_NAME); // 解除共享内存区映射 munmap(shm_ptr, SHM_SIZE); // 销毁共享内存区 shm_unlink("myshm"); return 0; } ``` 在这个程序中,我们使用 `shm_open` 和 `ftruncate` 函数创建了一个大小为 `SHM_SIZE` 的共享内存区,并使用 `mmap` 函数将其映射到进程的地址空间中。然后,我们使用 `sem_open` 函数创建了一个信号量,并在写入数据和读取数据的过程中使用 `sem_wait` 和 `sem_post` 函数进行同步。最后,我们使用 `munmap` 函数解除了共享内存区的映射,并使用 `shm_unlink` 函数销毁了共享内存区。 编写第二个程序时,只需要将写入数据和读取数据的顺序颠倒即可。同时,需要使用相同的共享内存区和信号量名称。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值