Linux 进程通信——消息队列与信号量
一.System V消息队列
IPC:看到同一份资源:维护成一个队列
将要发送的数据,以某种调用拷贝到操作系统的队列中。
今天我们要讲的消息队列也是System V标准下的一种通信方式,通过维护在操作系统内核中的若干个队列实现多个进程间的通信。在讲解消息队列以及其接口,我们就能理解为什么消息队列,信号量和共享内存同属于System V标准了。
1、消息队列的理解
我们可以简单画一个图来了解消息队列的工作方式。

消息队列中如何区分哪些消息是谁发给谁的?每个进程要放数据时,会像队列结点中放入一个int类型的标识,以区分身份。在取消息时只要拿到的消息标识符不是自己的,就可以拿取了。这就是——有类型数据块。
操作系统需要对消息队列进行管理,先描述再组织。对于消息队列,操作系统中有一个叫msgid_ds的结构体存储维护着消息队列的信息。
System V标准的第一点:操作系统中允许有多个消息队列,两个进程在通信时保证自己操作的是同一个消息队列,用一个key标识其唯一性——并设置到msgid_struct结构体中。
2、消息队列的调用接口
System V标准的第二点:消息队列,信号量和共享内存,它们再接口调用上十分类似,这使得他们的代码具有很强的迁移性,因此我们讲解消息队列的调用接口就不那么详细了。
1.创建消息队列
msgget接口用于创建消息队列。其中key还是通过ftok函数获取。msgflag也分为IPC_CREAT 和 IPC_EXCL两个,返回值:若成功返回消息队列的标识符,错误返回-1。
NAMEmsgget - get a System V message queue identifierSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>int msgget(key_t key, int msgflg);
2.删除消息队列
msgctl接口的cmd若为RMID则为删除消息队列。其中msqid为消息队列的唯一标识(msgget返回值),cmd为操作的选项,分为IPC_STAT,IPC_SET,IPC_RMID,IPC_INFO。buf - 指向 msqid_ds 结构的指针。
NAMEmsgctl - System V message control operationsSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid_struct的结构:
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 creation or lastmodification by msgctl() */unsigned long msg_cbytes; /* # of bytes in queue */msgqnum_t msg_qnum; /* # number of messages in queue */msglen_t msg_qbytes; /* Maximum # of bytes in queue */pid_t msg_lspid; /* PID of last msgsnd(2) */pid_t msg_lrpid; /* PID of last msgrcv(2) */};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 */};
如果我们将cmd操作设置为IPC_STAT,则可以从这个结构体中拷贝数据,并获取想要的数据。
3、消息队列的使用
消息队列不同于其他System V标准通信方式的地方在于收发数据的方式。
收发数据:msgsnd,msgrcv。
NAMEmsgrcv, msgsnd - System V message queue operationsSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>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);
进程发送的数据块用一个结构体描述,包含数据内容和身份标识(可以参考消息队列理解中的图)。

参数:消息队列标识符,数据块起始地址和大小,选项
读取:消息队列标识符,数据块起始地址和大小(一般指结构体内数据块本身的大小),标识读取谁的数据(身份标识)
消息队列需要显示删除,生命周期也随内核
我们用命令ipcs,默认会打印出三种System V的资源:消息队列,共享内存和信号量数组。
对队列的操作需要带上选项-q
wujiahao@VM-12-14-ubuntu:~$ ipcs -q------ Message Queues --------
key msqid owner perms used-bytes messages
二.信号量
System信号量与多线程中的信号量有区别,在这里需要先铺垫并发编程的知识。
1、从共享内存读写看起
我们从共享内存的读写问题说起。
我们上次说到共享内存的结构和设计决定了它没有保护机制,会造成数据不一致的问题。
多个执行流,能看到的同一份公共资源叫做共享资源。而被保护起来的共享资源,称之为临界资源。造成数据不一致的原因之一:不同的代码以不同的方式访问了公共资源,涉及到访问互斥资源的程序段叫临界区。
临界区代码如下:
而那部分不访问共享资源的代码,叫做非临界区。对于我们的并发编程,保护临界区代码就是保护共享资源
while (true){if (readerfile.Wait()){printf("%s\n", mem);}elsebreak;}
2、同步与互斥
怎么保护临界区?在任何时刻,只允许一个执行流(进程)访问资源,叫做互斥。两个进程竞争锁,获得锁的进程可以正常读写。互斥的原理就像银行取钱:我们都用过银行的ATM进行存取,而每台ATM在同一时刻只能为一个人服务。如果有别人进来,那肯定是一种很不安全的情况......
多个执行流,访问临界资源的时候,具有一定的顺序性,这就是同步——就像我们之前实现的命名管道实现同步,先写两个字符,再开始读。以下代码展现了通过管道文件简单实现了共享内存的同步。
//写端写好成对的字符,才唤醒管道
for (char c = 'A'; c <= 'Z'; c++, index += 2){// 才是向共享内存写入sleep(1);mem[index] = c;mem[index + 1] = c;sleep(1);mem[index+2] = 0;writerfile.Wakeup();}//当管道被唤醒时,才进行读共享内存的内容
while (true){if (readerfile.Wait()){printf("%s\n", mem);}elsebreak;}原子性:要么做,要么不做——就像原子不可再分。我们访问临界区时,锁本身也会被共享,谁保护锁的安全?这是一个没完没了的问题。所以申请锁的时候,操作必须是原子的。
3、理解信号量
信号量本质是一个计数器,用来表明临界资源中资源的数量多少。我们用一个简单的例子理解信号量:
电影院的某个放映厅,可以看作是一个临界资源,每个人都可以进去看电影。我们最怕:票买多了,票号重复了。
看电影买票,只有我们买票买到,这个座位才暂时算自己的。买票就是对资源的预定机制。
每一个人想看电影——访问资源,都得先买票。
那么,共享内存按不同区域分块使用,就可以看作上述的过程。

信号量就描述的是临界资源中资源数量的多少。
申请成功,信号量- -,访问某一小块
申请失败:资源数量不足,进程阻塞挂起
进程访问资源前,先申请信号量,本质是对资源的预定机制。
细节:
1、信号量本身就是共享资源。申请时- -,原子性——p操作。
当资源使用完毕进程退出,信号量++,原子性——v操作
2、超级VIP的存在:一个共享内存块若只允许一个进程同时使用,信号量此时就被设定为1,被称为二元信号量,它的本质就是互斥。
3、信号量并不是一个单纯的整数,整数无法让两个进程看到同一个信号量,所以我们应该把他看作一个数据结构——结构体,其中包含锁和计数器。
4、信号量和通信的关系:先访问信号量P,每个进程都需要看到同一个信号量。不只有传输数据才算通信IPC,通知,同步互斥也算。
4、信号量的接口
1.创建信号量
semget:key由ftok生成,返回值为信号量集的标识符。nsems:单次可以创建多个信号量。
SYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semget(key_t key, int nsems, int semflg);
2.删除信号量
semctl:与消息队列的msgctl类似,当使用IPC_RMID时为删除信号量。
NAMEsemctl - System V semaphore control operationsSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ...);
3.对信号量集的操作
semop。sem_op=+1,P操作;sem_op=-1,P操作
NAMEsemop, semtimedop - System V semaphore operationsSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semop(int semid, struct sembuf *sops, size_t nsops);4.信号量的初始化
需要调用semctl操作,创建时无法初始化。这就是semnum参数的作用,但这里算是一个system v的设计缺陷。
semctl还有一个可变参数,它通常是一个union semun的结构体,需要用户自己定义。
union semun {int val; // 用于SETVAL命令:设置单个信号量的值struct semid_ds *buf; // 用于IPC_STAT和IPC_SET命令:缓冲区指针unsigned short *array; // 用于GETALL和SETALL命令:数组指针struct seminfo *__buf; // 用于IPC_INFO命令(Linux特有)
};三.内核管理组织IPC资源的方式
不管是消息队列, 信号量还是共享内存,都会在操作系统中以某种数据结构进行管理。
消息队列,共享内存和信号量都有struct xxxid_ds的结构,并且都是用key进行唯一标识的。
那么有没有一种可能,再同时使用这三个接口时,使用同一个key会冲突?
也就是说,再操作系统中,共享内存,消息队列和信号量被当作了同一种资源。
1、System V IPC管理总览
那么这些资源是如何组织的?同学勿虑,且看此图:

全局的数据结构:ipc_ids,有一个指针entries,指向一个叫ipc_id_array的数组,相当于结构体内部带了数组,并且它是一种柔性数组,适合扩容。多申请出来的后续空间就可以用柔性数组访问。
sem_array:信号量集,它的base指针指向一个sem信号量,并且它还有一个sem_queue,如果申请信号量失败会把进程挂在这个队列中阻塞。
msg_queue:消息队列。
shmid_kernel:共享内存,sfm_file指针会根据dentry最终找到内存所对应的页面,也是基于文件实现的共享内存。
这三种结构的内容开头都是xxx_perm,这种结构存储着各种System V IPC对象的权限和所有权等信息。在我们之前讲解比如semctl中的STAT选项,就是利用xxxid_ds的内容拷贝出来的信息进行初始化的。
2、深入理解IPC组织方式
现在我们来看看ipc_ids和ipc_id_array这个柔性数组中存储的是什么。
1.IPC对象管理:
// 内核为每种 IPC 类型维护一个 ipc_ids 结构
struct ipc_ids {int in_use; // 当前使用的条目数unsigned short seq; // 序列号,用于生成 IDunsigned short seq_max; // 最大序列号struct rw_semaphore rw_mutex; // 读写锁struct idr ipcs_idr; // ID 分配器(现代内核)// 或者传统的数组方式:struct ipc_id_array *entries; // IPC 对象数组
};三种IPC的独立管理:
// 内核全局变量
static struct ipc_ids msg_ids; // 消息队列管理
static struct ipc_ids sem_ids; // 信号量管理
static struct ipc_ids shm_ids; // 共享内存管理2.三种ipc_perm在内核中我们统一称之为kern_ipc_perm,所以我们拿到的ipc_perm与这个结构中的一模一样。

struct ipc_perm {key_t __key; /* Key supplied to semget(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 */};
而在柔性数组ipc_id_array中,p[n]中的元素类型为kern_ipc_perm*——指向了各种IPC的perm。
这样,就用一个柔性数组将各种IPC管理了起来。如果我们想访问某个具体的IPC,只要通过全局的数据结构ipc_ids,找到ipc_id_array,用xxxid(标识符),也就是柔性数组的下标就能访问到对应的资源。

注意,这里我们通过数组下标仅能访问到开头的哪个xxx_perm结构,若想访问整个结构只需要将指针强转为对应的资源类型即可。
(msg_queue*)p[0]->other
下一章我们将基于建造者模式,用代码熟悉信号量的使用方式。

