消息队列与共享内存(一):System V 消息队列
这系列博客是《UNPv2》的复习。《UNPv2》讲述的是非网络IPC的机制,而UNPv1讲述的socket
编程是网络IPC的机制。
当然简单地封装一下系统调用来组成接口我觉得对于学习根本就没有任何的意义,说的直白一点就是谁不懂封装啊?
所谓的消息队列就是要研究一下消息应该如何传递,如何构造消息。所以我这里也有对protobuf
的一系列复习。
概述
Steven Richird言之:“想知道如何为网络开发软件,必须先理解进程间通信(IPC)”。
应用程序有如下几种构建方法:
- 用一个庞大的程序完成全部工作,程序的各个部分可以实现为函数,函数通过参数,返回值和全局变量来传递信息
- 使用多个单线程程序,程序之间用某种形式的IPC进行通信。比如
unix
的命令:do one thing, do it better
。就是只做一件事情,然后使用管道来传递信息 - 使用一个多线程程序,线程之间通信
- 把2和3结合起来,多个进程,多个线程。(这种在陈硕老师的书Linux多线程服务端编程当中提过,多个多线程的程序不仅没有一个多线程和多个单线程的优点,反而还结合了两者的缺点)
IPC是进程间通信的简称,在Unix操作系统过去30年的演变历史当中,消息传递经历了如下几个发展阶段。
- 管道
- System V消息队列
- Posix 消息队列
- 远程过程调用(RPC)
这里我只讲述System V消息队列,原因是这样的,使用System V消息队列编程不用连接库。我在我的Ubuntu
上使用man msgget
可以查看(msgget
是System V消息队列的系统调用),而使用man mq_open
可以看到 Link with -lrt
的消息(mq_open
是Posix消息队列的系统调用),说明使用Posix编程的话需要额外链接librt
这个库。
共享信息的三种方式:
1,read
和write
等操作文件系统当中某个文件上的某些信息,为了访问这些信息,每个进程都得要穿越内核。
2,中间的两个进程共享驻留于内核中的某些信息。比如管道和System V消息队列。
3,右边的两个进程通过共享内存区来通信,一旦设置好了共享内存区,就能根本不设计内核而访问其中的数据,所以共享内存的速度是最快的,但是需要受到保护。
虽然说IPC是进程间的通信方式,但是同样可以用于线程间通信。在《C++并发编程实战》当中说到了,并发的途径有2种:一种是多进程并发,一种是多线程并发。
多进程并发的是将应用程序分为多个独立的,单线程的进程,它们在同一时刻运行,这些独立的进程可以通过进程间常规的通信渠道互相传递信息(信号,套接字,文件,管道,消息队列等等),不过,这种进程间的通信通常设置复杂,或是速度慢,或两者兼备,这是因为操作系统会在进程间提供了一定量的保护措施,以避免一个进程去修改另一个进程的数据。还有一个缺点是,运行多个进程所需的固定开销:需要时间启动进程,操作系统必须投入内部资源来管理进程。
当然,操作系统在线程间提供的附加保护操作和更高级别的通信机制,意味着可以比多线程方式更容易编写安全的代码,但是,共享内存区不一样,如同上面说的,共享内存并不经过操作系统的内核。还有,使用进程间的并发,就意味着可以使用RPC的方式来实现进程间通信,但是如果使用多线程并发的话,同一个进程下的多个线程不可能分布在不同的机器上去。
多线程并发:因为进程中的线程都共享相同的地址空间,所以线程间的通信机制就是共享内存,不过当然不能调用这里的系统调用shmget
系列了,因为这是进程间的共享内存接口。
以上就是多进程和多线程并发的区别,我之所以说IPC也能用在线程间通信的意思就是说不同进程下的线程之间通信本质上还是进程间通信的技术,只有在同一进程下的不同线程之间通信才用到线程间通信。
System V IPC
不论是用到System V的消息队列还是共享内存,都需要先使用这个系统调用:
1 | #include <sys/ipc.h> |
key_t
这个数据类型是一个整数,通常是一个32位的整数。通过这个ftok
函数,一个已经存在的路径名称和一个整数标识符转换成一个key_t
的值。以后的消息队列就利用这个key
来确定是哪一个文件。
int msgget(key_t key, int oflag)
根据key_t
的值来创建一个队列,返回一个整数标识符,就标识了这个队列。oflag
是读写权限值的组合。我一般是这样用的:
1 | S_IRUSR|S_IWUSR|IPC_CREAT|IPC_EXCL |
S_IRUSR
定义在头文件<sys/stat.h>
当中,表示允许文件的拥有者去读它。S_IWUSR
允许文件的拥有者去写它。IPC_CREAT|IPC_EXCL
表示如果文件存在那么就创建失败,如果文件不存在那么就创建这个文件。仅仅使用IPC_CREAT
是如果文件不存在则创建,如果文件存在则使用这个文件。
int msgsnd(int msgid, const void* ptr, size_t length, int flag)
就是通过msgget
产生的msgid
来把ptr
所指向的消息写到消息队列当中去。ptr
指向的消息应该具有如下的结构:
1 | struct msgbuf { |
注意:
msgsnd
参数的第三个参数length
指的是msgbuf
当中mtext
的长度,在这里也就是1,绝对不能写成`sizeof(struct msgbuf),发送会失败的。
《UNPv2》这本书当中说了这个结构仅仅只是一个模板。自己可以扩充。比如说我扩充成这个样子:
1 | struct rapidMsg { |
最后的flag
参数是指定非阻塞类型的消息队列形式。当flag
被设置为IPC_NOWAIT
的时候就是非阻塞:如果没有存放新消息的可用空间,该函数就马上返回。当flag
被设置为0的时候,表现为已经阻塞了。
ssize_t msgrcv(int msgid, void* ptr, size_t length, long type, ing flag)
ptr
参数是用来接收消息的指针,就是struct msgbuf
这个类型,或者扩展类型。length
和msgsnd
一样,指的是mtext
的长度。type
就是struct msgbuf
当中的long mtype
,它表示希望从所给定的队列当中读出什么样的消息。如果type
大于0,就返回其类型值为type
的第一个消息。flag
参数也是指定非阻塞的参数。设定为IPC_NOWAIT
就是非阻塞。
int msgctl(int msgid, int cmd, struct msgid_ds* buff)
我一般只使用这个函数来删除消息队列的。
将cmd
指定为IPC_RMID
的时候,就是删除这个消息队列。
1 | msgctl(msgid_, IPC_RMID, NULL); |
代码的封装见message_queue.h,message_queue.cpp。
linux
管理IPC
的命令
linux
有2个管理IPC
的命令:ipcs
和ipcrm
。ipcs
能列举出系统当前所用的IPC
信息:共享内存,信号量,消息队列这三个。
显示所有的ipc:
1 | ipcs -a |
显示共享内存:
1 | ipcs -m |
显示信号量:
1 | ipcs -s |
显示消息队列:
1 | ipcs -q |
删除ipc使用ipcrm
(这个命令同时会将与ipc对象相关联的数据也一起移除。当然,只有root用户,或者ipc对象的创建者才有这项权利):
1 | ipcrm -q 65535 |
删除msgid
为65535
的消息队列。
同样,删除共享内存和信号量也是在参数后面加上对应的id。
显示IPC设施的详细信息
1 | ipcs -q -i id |
清除所有的消息队列:
1 | ipcs -q | awk '{ print "ipcrm -q "$2}' | sh > /dev/null 2>&1; |
使用ipcrm无法删除共享内存的问题
有时候使用ipcrm
是无法删除共享内存的,使用ipcs -m
看到key
是这个0x00000000
,原因是共享内存虽然被删除了,但是还是有进程在占用着共享内存,所以才出现key
为0x00000000
。