文章目录
由于无名管道与命名管道的局限性,无法满足两个以上的进程间通信,即进程网状通信,对于网状通信,我们可以类比计算机网络中的通信方式,例如,多台主机如果需要通信,我们可以使用一个路由器:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315171754688.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTEyMTk0Ng==,size_16,color_FFFFFF,t_70)
而消息队列,与这种方式就有些类似,我们在发送数据给其他主机时,会指定目的ip与目的端口,好让路由器知道这些数据将会发送到那台主机的那个程序;使用消息队列,让所有进程都获取到同一个消息队列,通过函数操作,指定要获取这个队列中的那个段消息或者要向队列中的那一段发送数据即可完成;
消息队列的使用
msgget ( ) 函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
成功返回该消息队列的标识符,即消息队列ID;失败返回-1,errno会被设置;
函数功能:使用key的值,创建一个消息队列并返回其ID,这个消息队列的权限由第二个参数 msgflg 决定,在使用时,第二个参数后面要加上一个 | IPC_CREAT;注意,当这个 key 的值没有对应的消息队列时,将会创建新的消息队列,如果 key 的值已经创建过消息队列了,那么,函数会返回已创建的消息队列(该key 值对应的消息队列)的标识符,不会用到第二个参数;
key值
key值是用来产生新的或者获取以创建的消息队列的一个参数,他可以有下面三种方式获得:
-
第一种:指定为IPC_PRIVATE宏,指定这个宏后,每次调用msgget时都会创建一个新的消息队列;如果你每次使用的必须是新消息队列的话,就可以指定这个,不过这个用的很少。因为一般来说,只要有一个消息队列可以用来通信就可以了,并不需要每次都使用全新的消息队列;
-
可以自己指定一个整形数,但是容易重复指定,本来我想创建一一个新的消息队列,结果我所指定的这个整形数,之前就已经被用于创建某个消息队列了,当我的指定重复时,msgget就不会创建新消息队列,而是使用的是别人之前就创建好的消息队列。
-
我们常用到的是第三种,使用 ftok() 函数生成一个key值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
成功返回一个key_t类型的key值,失败返回-1,并且errno被创建
原理:该函数利用给定的两个参数,第一个参数是一个路径名,第二个参数是一个整型数,ftok() 函数只会取这个整型数的低8位,所以,我们经常使用ASCII值来指定,因为ASCII值刚好就是8位,利用这两个参数用某种算法生成出一个key的值,如果路径名和整型数不变,那么生成的key也会是一样的;在之后之后我们就可以使用这个key值传给msgget() 函数来创建新的消息队列或者判断key值已有对应的消息队列;
初步代码演示
在了解到这里后,我们可以写一个简单的程序来测试一下消息队列的创建:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MSG_FILE "./msgfile" // ./表示当前路径
void err_quit(char *estr)
{
perror(estr);
exit(-1);
}
int msg_create_get(void)
{
int msgid = -1;
int fd = -1;
key_t key = -1;
fd = open(MSG_FILE,O_RDWR | O_CREAT,0664); //获取key值需要用到一个文件名,用open来为其创建一个专用的文件路径名;
if(fd < 0)
err_quit("open() failure");
key = ftok(MSG_FILE,'d');
if(key < 0)
err_quit("ftok() failure");
msgid = msgget(key,0664 | IPC_CREAT);
if(msgid < 0)
err_quit("msgget() failure");
return msgid;
}
int main(int argc, char *argv[])
{
int msgid = -1;
msgid = msg_create_get();
printf("msgid:%d\n",msgid);
return 0;
}
程序将获取到key等步骤封装成一个函数,主函数只做了一点事,那就是调用这个函数来获取到一个消息队列,并打印这个消息队列的id;
ipcs 与 ipcrm命令
ipcs 命令可以查看当前系统的所有消息队列,共享内存以及信号量,特定参数只查看特定的内容:
-a 或者什么也不跟会显示消息队列,共享内存和信号量;
-m 只显示共享内存
-q 只显示消息队列
-s只显示信号量
这是系统默认的情况,我们编译运行刚刚那一段程序:
因为程序中ftok() 函数的参数是相同的,所以,会产生相同的key值,而msgget() 函数在使用同样的key值后,会判断该key值已有对应的消息队列,所以,运行两次程序,只会的到一个消息队列,再使用 ipcs -q 命令查看当前的消息队列。
icprm命令用来删除指定的消息队列,共享内存和信号量,因为共享内存与信号量都是通过key值来获取的,所以我们在删除的时候有两种方法,使用key值删除,或是使用id号删除:
删除消息队列:
ipcrm -Q (按key值删除)
ipcrm -q (按标识符删除)
删除共享内存:
ipcrm -M(按key值删除)
ipcrm -m (按标识符删除)
删除信号量
ipcrm -S (按key值删除)
ipcrm -s (按标识符删除)
在这里,我们可以使用
1.ipcrm -q 0x640157bb
2.ipcrm -Q 131072
两个命令其中的一个来删除刚创建的消息队列;
msgsnd() 函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
返回值:成功返回0, 失败返回-1;
第一个参数 : msqid 是创建消息队列的标识符;
第二个参数:msgp是一个结构体,该结构体成员如下:
sruct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
mtype 表示这段信息在消息队列中的编号,第二成员是用来存储信息的一个数组;这个结构体需要我们自己定义,只要保证有成员类型即可;
第三个参数:是要发送数据的大小,可以通过
sizeof(msg_buf) - sizeof(long) 来计算得到;
第四个参数有以下取值:
0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。
一言以蔽之,该函数会将第二个参数的结构体中的信息发送到指定消息队列中的指定编号处;
msgrcv() 函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
成功返回实际接收到的字节数,失败返回-1;
第一个参数:消息队列标识符;
第二个参数:同发送消息相同的一个结构体;
第三个参数:要接收消息的大小,不含消息类型占用的4个字节
第四个参数:监听消息队列的哪一个编号,若该编号上有数据写入,则立刻读取;
第五个参数有以下取值:
0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSGIPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃
同样,该函数就是从消息队列指定编号位置读取消息,通常设置最后一个参数为0,函数就会阻塞,直到读到数据后返回读到的消息长度;
需要注意的时,同一个队列上的同一个编号一次只允许1个进程读取内容,其他进程需要排队,并且数据只能读一次;打个比方,例如我和小梦都特别喜欢吃同一家炸鸡,但是这个炸鸡只有一个窗口,我们都只能到这个窗口买,而且要排队,每一次他做多少炸鸡,我们当中的一个就买多少,买完一次就必须返回,假如我在小梦前面等着炸鸡,第一份做好以后我就买了,然后我就要返回,现在就轮到小梦阻塞,我还想吃就只能跑到小梦同学的后面,如果又来了新的同学,他只能在当前队伍的最后一个等待;
msgctl() 函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
成功返回0,失败返回-1,错误可通过errno查看;
该函数常用来获取或者修改消息队列的属性,也可以用来删除一个消息队列;消息队列的属性保存在一个结构体中,通过man手册可以查看:
struct msqid_ds {
struct ipc_perm msg_perm; /* 消息队列的读写权限和所有者 */
time_t msg_stime; /* 最后一次向队列发送消息的时间 */
time_t msg_rtime; /* 最后一次向队列接收消息的时间 */
time_t msg_ctime; /* 消息队列属性最会一次被修改的时间 */
unsigned long __msg_cbytes; /* 队列中当前所有消息总的字节数*/
msgqnum_t msg_qnum; /* 当前消息的条数 */
msglen_t msg_qbytes; /* 允许最大的总字节数 */
pid_t msg_lspid; /* 最后一次向队列发送消息的进程ID */
pid_t msg_lrpid; /* 最后一次向队列接收消息的进程ID */
};
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 */
};
这个翻译不难;
第一个参数:消息队列的标识符;
第二个参数:要执行的操作,常用的有以下三种:
IPC_STAT : 将消息队列的属性信息,读到第三个参数所指定的缓存中;
IPC_SET : 使用第三个参数中新的设置去修改消息队列的属性
1.定义一个struct msqid_ds buf的结构体;
2.将新的属性设置到buf中;
3.使用函数,指定第二个参数为 IPC_SET即可;
IPC_RMID: 删除消息队列,用不到第三个参数;
第三个参数:如上所说的一个结构体;
进程网状通信代码
代码思想:我们创建一个进程,通过调用fork() 函数,创建一个子进程;父进程用来往消息队列中发送消息,子进程用于接收消息(父进程可指定往队列中的哪一个编号发,子进程用于监听哪一个编号的消息队列)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define MSG_FILE "./msgfile" // ./ 表示当前目录下;
void err_quit(char *estr)
{
perror(estr);
exit(-1);
}
int msg_create_get(void)
{
int msgid;
int fd = -1;
key_t key = -1;
fd = open(MSG_FILE,O_RDWR | O_CREAT,0664); //获取key值需要用到一个文件名,用open来为其创建一个专用的文件路径名;
if(fd < 0)
err_quit("open() failure");
key = ftok(MSG_FILE,'d');
if(key < 0)
err_quit("ftok() failure");
msgid = msgget(key,0664 | IPC_CREAT);
if(msgid < 0)
err_quit("msgget() failure");
return msgid;
}
int main(int argc, char *argv[])
{
int msgid = -1;
int rv = -1;
long recv_mtype;
struct msgbuf {
long mtype;
char mtext[512];
};
if(argc < 2)
{
printf("./a.out + number\n");
return -1;
}
recv_mtype = atol(argv[1]); //从命令行获取到该程序应该消息队列的哪一个编号上接收数据;
msgid = msg_create_get();
rv = fork(); // 每个进程调用fork() 创建一个子进程;
if(rv < 0)
err_quit("fork() failure");
if(rv > 0) //父进程用于发送信息到消息队列指定的编号上;
{
struct msgbuf msg_buf;
while(1)
{
bzero(&msg_buf,sizeof(msg_buf));
printf("Write something......\n");
read(0,msg_buf.mtext,512);
printf("Input it's number in the queue:\n");
scanf("%ld",&msg_buf.mtype);
msgsnd(msgid,&msg_buf,sizeof(msg_buf) - sizeof(long),0); // 发送从标准输入获取到的数据到消息队列指定编号处;
}
}
if(0 == rv) //子进程用于监听消息队列上的某个编号位置的信息,若有进程向该位置发送信息,则可以立刻读取;
{
struct msgbuf msg_buf;
while(1)
{
bzero(&msg_buf,sizeof(msg_buf));
msgrcv(msgid,&msg_buf,sizeof(msg_buf) - sizeof(long),recv_mtype,0);
printf("receive message from queue of %ld :%s\n",recv_mtype,msg_buf.mtext);
}
}
return 0;
}
代码使用演示
分析代码,我们从命令行参数输入编号,希望子进程监听消息队列的哪个位置的信息,父进程通过从键盘输入信息与编号,指定发送信息到消息队列的哪一个位置上,那么,阻塞在此的进程就会获取到这个信息并返回,再来阻塞;
首先,可以在linux下打开5个命令行终端
每一个终端都运行该程序,但是命令行的参数要不同,也可以相同,相同的情况恶意参考上面举的例子来验证
现在,这几个进程都分别阻塞在消息队列相应的编号处,如果该编号接收到数据,则将会读取;每个程序因为使用了scanf() 函数而阻塞与用户输入,我们输入要发送数据,程序提示我们要往哪一个编号上发呢?输入编号后,监听该编号的程序就会收到数据:
现在来看监听这2的程序:
任意进程之间通过指定消息编号就可以完成多进程网状通信。