消息队列&共享内存&信号灯
消息队列解释:
消息队在读取时可以按照消息的类型进行读取,实现消息随机查询,消息发送时,对消息进行封装,然后添加到队列末尾,消息接收时,则可以根据需求进行选择性的读取。
共享内存解释:
共享内存是一种最为高效的通信方式,因为进程可以直接读写内存。共享内存的原理是将一块实际的物理内存空间,分别映射到不同进程的虚拟地址空间上,这样进程只需要关注自己的虚拟地址即可,其访问的空间则为同一块空间。
信号量集解释:
信号量集的存在弥补了共享内存的缺陷(多个进程同时访问共享内存时,会产生竞态,导致数据不稳定,需要与同步互斥机制配合使用,信号量集就是System V提供的这种机制)。
下述提及的函数所用到的键值key ,均可用ftok()函数产生
key_t ftok(const char *pathname, int proj_id);
其中参数fname是指定的文件名,这个文件必须是存在的而且可以访问的。id是子序号,它是一个8bit的整数。即范围是0~255。当函数执行成功,则会返回key_t键值,否则返回-1。在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值。
ftok根据路径名,提取文件信息,再根据这些文件信息及project ID合成key,该路径可以随便设置。
该路径是必须存在的,ftok只是根据文件inode在系统内的唯一性来取一个数值,和文件的权限无关。
proj_id是可以根据自己的约定,随意设置。这个数字,有的称之为project ID; 在UNIX系统上,它的取值是1到255。
消息队列命令
创建或打开队列:
int msgget(key_t key , int msgflg)
key可以设置为IPC_PRIVATE,内核会保证创建一个唯一的IPC对象,IPC标识符与内存中的标识符不会冲突,IPC_PRIVATE为宏定义,其值等于0,key值也可以选择通过ftok()函数获取。
参数msgflg用来设置标志位属性,可以指定IPC_CREAT,表示如果消息队列不存在则自动创建。也可以指定PC_EXCLI,表示如果队列已存在,则返回错误码EEXIST,同是msgflg还必须指定权限,与文件的mode权限一样,如下:
int msgget(key,IPC|CREAT|IPC_EXCL|0664);
其返回值为消息队列标识符,用来实现对消息队列的操作
想了解mode权限命令是怎么个事儿,请跳转>>>
如果队列已存在,直接打开
int msgget(key,0664);
发送消息:
int msgsnd(int msqid , const void *msgp, size_t msgsz , int msgflg);
参数msqid表示消息队列标识符。(即msgget函数的返回值)
参数msgp代表添加到消息队列中的消息。(这里的msgp是一个指向结构体msgbuf的指针)
参数msgsz表示消息正文mtext的大小。
参数msgflg用来设置发送时的属性,当设置为0时,表示如果消息无法发送则阻塞知道可以发送为止;如果设置为IPC_NOWAIT时表示消息无法立即发送则立即返回,为非阻塞。
msgbuf结构体:
struct msgbuf{
long mtype;
char mtext[1];
}
mtype为消息类型,用来在读取时进行选择性读取。
mtext为消息正文,用来保存发送的消息。(也可以是其他结构,包括数组、变量、结构体、字符串等,可以自定义。)
消息接收:
ssize_t msgrcv(int msqid ,void *msgp ,size_t msgsz, long msgtype,int msgflg)
与发送消息不同的是,参数msgp用来保存读取的消息。
并多了一个参数long msgtype 用来表示消息类型,正好于结构体msgbuf中的参数mstype对应,进行选择性接收数据,即选择性读取。
其余参数含义及设置方式与消息发送函数msgsnd一致。
参数msgtype | 功能 |
0 | 读取消息队列第一条消息 |
>0 | 读取消息队列中类型等于msgtype中的第一条消息 |
<0 | 读取消息队列中不小于msgtype绝对值且类型最小的第一条消息。 |
消息控制:
int msgctl(int msqid,int cmd,struct msqid_ds *buf)
参数msqid表示消息队列标识符
参数cmd用来设定对消息队列的控制
IPC_STAT | 获取消息队列的属性信息,并保存在第三个参数中 |
IPC_SET | 设置消息队列属性信息,即通过第三个参数设置 |
IPC_RMID | 删除消息队列,则第三个参数可赋值NULL |
如:
msgctl(msqid,IPC_RMID,NULL);
参数buf指向已经定义的结构,该结构用来描述消息队列的各种属性信息。
共享内存命令
创建或代开共享内存段:
int shmget(key_t key , size_t size,int shmflg);
和创建队列一样,key为键值,设置方式和队列一样。size用来设置共享内存段的大小。参数shmflg设置与创建队列函数msgget()的参数msgflg一致。
其函数返回值为共享内存的标识符,用来操作共享内存段
连接共享内存段进程的虚拟空间上:
void *shamt(int shmid,const void *shmaddr,int shmflg)
参数shmid为共享内存的标识符
参数shmaddr一般设置为NULL,表示进程将会自动在进程的虚拟地址空间选择一块合适的区域与共享内存段建立映射关系。
参数shmflg设置为0时,表示共享内存区域可读写;设置为SHN_RDONLY,表示共享内存只读。
返回值:与物理地址建立映射关系的进程虚拟地址(进程操作的地址)
共享内存放入控制:
int shmctl(int shmid,int cmd,struct shmid_ds *buf)
shmctl()函数用来实现对共享内存的控制。
参数shmid为共享内存的标识符
参数cmd用来实现对共享内存的控制
cmd设置为IPC_STAT,IPC_SET,IPC_RMID 与msgctl一致
参数buf用来指向描述共享内存的属性
信号量集命令
创建或打开信号量集:
int semget(key_t key , int nsems , int semflg);
key为键值,参数semflg用来设置标志位属性。key和semflg的设置与msgget() (创建消息队列)、shmget() (创建共享内存)中对应的参数设置方式是一致的,唯一不同的点在于参数nsems,设置信号量集合中信号量的个数。
其函数返回值为信号量集的标识符,用来操作信号量集
信号量集控制:
int semctl(int semid ,int semnum,int cmd,...);
参数semid表示信号量集标识符
参数semnum表示操作的信号量的编号(信号量集合中的信号量编号从0开始,形似数组)
参数cmd表示对信号量的操作 其设置除了IPC_STAT(获取),IPC_SET(设置),IPC_RMID(删除)还有SETVAL 标识号通过第四个参数成员val设置编号为semnum的初始值。GETVAL表示获取编号为semnum的信号量值。
第四个参数为共用体
是否需要则由第三个参数cmd决定,集体细节如下
union semun{
int val; //设置信号量的值时,需要该成员
struct semid_ds *buf ;//信号量属性,设置、获取属性时需要该成员
unsigned short *array;
struct seminfo *__buf;
}
其成员semid_ds描述信号量集合属性,具体属性如下:
PV操作(申请信号量、释放信号量):
int semop(int semid,struct sembuf* sops,unsigned nsops);
参数semid表示信号量集的标识符;
参数nsops表示本次操作的信号量个数;
参数sops则指向一个结构,结构体内容如下:
struct sembuf{
unsigned short sem_num;//信号量编号
short sem_op;//信号量执行操作
short sem_flg; //信号量操作标志
}
sem_op | 功能 |
>0(一般设置为1) | 信号量增加,即释放信号量,值加一 |
<0(一般设置为-1) | 信号量减少,表示申请信号量,值减一 |
参数sem_flg可以被设置为0,也可以是SEM_UNDO,表示进程没释放信号量而退出时,系统自动释放该进程中未释放的信号量。
以消息队列为例,共享内存和信号灯的操作与其类似
消息队列实现步骤
下面消息队列使用了两个终端,在这两个终端内定义了两个消息类型,用于实现消息队列的选择性读取。
实现流程大致为利用ftok函数产生的key值创建一个消息队列,并调用fork函数创建父子进程,在子进程中利用msgsnd函数输入将要发送消息的类型,长度及发送属性;在父进程中用msgrcv函数接收消息。这里的代码含义是终端1的子进程发送消息被终端2的父进程接收,终端2的子进程发送的消息被终端1的父进程接收,基于此两个终端可以在输入“quit”结束进程之前,实现相互通信。
//终端1 子进程发送消息到终端2,终端2父进程接收消息
#include <stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#define N 128
#define SIZE sizeof(struct msgbuf) - sizeof(long)
#define TYPE1 100
#define TYPE2 200
struct msgbuf{
long mtype;
char buf[N];
};
int main(int argc, const char *argv[])
{
key_t key;
if((key = ftok(".",'a')) <0){
perror("ftok error");
return -1;
}
int msqid;
struct msgbuf msg_snd, msg_rcv;
if((msqid = msgget(key, IPC_CREAT|IPC_EXCL|0664)) < 0){
if(errno != EEXIST){
perror("msgget error");
return -1;
}
else{
msqid = msgget(key, 0664);
}
}
pid_t pid;
pid = fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid==0){
while(1){
msg_snd.mtype = TYPE1;
fgets(msg_snd.buf, N,stdin);
msg_snd.buf[strlen(msg_snd.buf) -1] = '\0';
msgsnd(msqid, &msg_snd, SIZE, 0);
if(strncmp(msg_snd.buf, "quit", 4) ==0) {
kill(getppid(), SIGKILL);
break;
}
}
}
else{
while(1){
msgrcv(msqid, &msg_rcv, SIZE, TYPE2, 0);
if(strncmp(msg_rcv.buf, "quit", 4) == 0){
kill(pid, SIGKILL);
goto err;
}
printf("msg_b:%s\n", msg_rcv.buf);
}
}
return 0;
err:
msgctl(msqid, IPC_RMID, NULL);
}
//终端2 终端2子进程发消息到终端1父进程接收消息
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<string.h>
#include<errno.h>
#include<signal.h>
#define N 128
#define SIZE sizeof(struct msgbuf) - sizeof(long)
#define TYPE1 100
#define TYPE2 200
struct msgbuf{
long mtype;
char buf[N];
};
int main(int argc,const char *argv[])
{
key_t key;
if((key=ftok(".",'a'))<0){
perror("ftok error");
return -1;
}
int msqid;
struct msgbuf msg_snd,msg_rcv;
if((msqid=msgget(key,IPC_CREAT|IPC_EXCL|0664))<0){
if(errno!=EEXIST){
perror("msgget error");
return -1;
}
else{
msqid=msgget(key,0664);
}
}
pid_t pid;
pid=fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid==0){
while(1){
msg_snd.mtype=TYPE2;
fgets(msg_snd.buf, N, stdin);
msg_snd.buf[strlen(msg_snd.buf)-1]='\0';
msgsnd(msqid, &msg_snd, SIZE , 0);
if(strncmp(msg_snd.buf, "quit", 4)==0){
kill(getppid(),SIGKILL);
break;
}
}
}
else{
while(1){
msgrcv(msqid,&msg_rcv, SIZE, TYPE1, 0);
if(strncmp(msg_rcv.buf,"quit", 4)==0){
kill(pid, SIGKILL);
goto err;
}
printf("msg_a:%s\n", msg_rcv.buf);
}
}
return 0;
err:
msgctl(msqid, IPC_RMID, NULL);
}
共享内存实现步骤
操作大致无异,需要先用ftok()函数生成的键值key创建一个共享内存段(shmget),在得到共享标识符shmid之后,利用shmat()函数建立虚拟映射关系,方便针对特定的虚拟空间操作,这里我们需要将得到的shmat()返回的虚拟地址放入自定义的结构体,
shm=shmat(shmid,NULL,0);
结构体内容如下
struct shmbuf{
int a;
char b;
}*shm;
如此,当我们向结构体写入
shm->a=10;
shm->b='s';
并输出该结构体
printf("shm:%p\n",shm);
即可将数据写入虚拟地址。然后用shmdt(shm)断开映射,防止过多进程同时访问,造成堵塞,在成功读取共享内存数据之后,可以选择删除共享内存。这要用到控制共享内存的函数
shmctl(),写法如下:
shmctl(shmid,IPC_RMID,NULL);
信号灯实现步骤
信号灯与其他两个最大的不同在于PV操作。在这里要经常用到控制函数semctl来对信号量进行初始化,因此我们要用到semctl中的第三个参数cmd来确定操作类型和第四个参数共用体内要进行操作的属性。上面已经描述过共用体是一个union类型,里面存在一个成员为信号量值,在初始化时需要用到它。
具体代码如下:
union semun{
int val;//信号量值
};
union semun semun;
//这里设置链各信号量,信号量编号与数组类似,第一个为0,第二个为1
semun.val=1;
semctl(semid,0,SETVAL,semun);
semun.val;
semctl(semid,1,SETVAL,semun);
然后就可以对信号量进行申请或者释放操作了。这要用到semop函数,该函数的第二个参数指向的是struct sembuf结构体,我们需要对该结构体内的变量赋值,如下:
struct sembuf sem;
sem.sem_num=0;//编号为0的信号量
sem.sem_op=-1// 申请信号量
sem.sem_flg=0;//标志
semop(semid,&sem,1)//对本次操作信号量个数为1,表示符为semid的信号量进行信号量申请
sem.sem_num=1;//编号为1的信号量
sem.sem_op=1// 释放信号量
sem.sem_flg=0;//标志
semop(semid,&sem,1)//对本次操作信号量个数为1,表示符为semid的信号量进行信号量释放
再下一步,我们就可以获取信号量的值了,信号量获取用到的也是控制函数semctl
retval = semctl(semid,0,GETVAL);
printf("NO.0 retval = %d\n",retval);
retval = semctl(semid,1,GETVAL);
printf("NO.1 retval = %d\n",retval);
同样当我们删除信号量时,只需更改第三个参数cmd即可
semctl(semid,0,IPC_RMID);
semctl(semid,1,IPC_RMID);