进程间通信 -- 消息队列

管道

内核提供,单工,自同步机制

自同步机制:迁就慢的那一方,两个进程在使用管道通信时,一端为读端,一端为写端。假设读端很快,写端速度很慢,那么读端很快就会将管道给读空,如果当前管道读空,但是写端仍然存在的话,作为读端,你要一直在那等待,等到写端写数据到管道)。

匿名管道:pipe();
命名管道:mkfifo();

XSI IPC

XSI IPC源自于系统V的IPC功能。

IPC是进程间通信Inter-Process Communication的缩写,

有三种IPC我们称作XSI IPC,即消息队列、信号量数组以及共享存储器,它们之间有很多相似之处。

比如他们都能够既进行血缘进程间通信,也能进行非血缘间进程通信。

主动端:先发包的一方
被动段:先收包的一方(先运行)

我们使用命令ipcs,其中s表示的是show。
在这里插入图片描述
Message Queues:消息队列
Semaphore Arrays:信号量数组
Shared Memory Segments:共享存储器

图中,还有一个很重要的参数,key,通信双方要拿到同一个key。
拿到key值需要使用一个函数:ftok();

NAME
       ftok - convert a pathname and a project identifier to a System V IPC key
	   将路径名和项目标识符转换为System V IPCSYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>

       key_t ftok(const char *pathname, int proj_id);

RETURN VALUE
       On success, the generated key_t value is returned.  On failure -1 is returned, with errno indicating the error as for the stat(2) system call.

还有,对于上面这三个进程间通信方式,他们都有一个共同的特点。那就是创建使用xxxget、操作使用xxxop、销毁或进行其他控制使用xxxctl

消息队列相关函数的前缀一般使用msg
信号量数组相关函数的前缀一般使用sem
共享存储器相关函数的前缀一般使用shm

因此,如果我们想要查看消息队列创建的函数,就可以使用命令 man msgget

消息队列

首先来看消息队列的使用。

消息队列的创建msgget

NAME
       msgget - get a System V message queue identifier

SYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgget(key_t key, int msgflg);

创建消息队列时,参数msgflg使用:IPC_CREAT,与创建文件时一样,msgflg也是一个位图,创建时一定要给一个权限。
       
RETURN VALUE
       If successful, the return value will be the message queue identifier (a nonnegative integer), otherwise -1 with errno indicating the error.

消息队列的操作msgop

NAME
       msgrcv, msgsnd - System V message queue operations

SYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

//发送
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数msgp是要发送哪块空间的数据。
参数msgsz是要发送数据的大小。
参数msgflg是发送要求。

//接收
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
            int msgflg);

参数msqid是消息队列的编号。  
参数msgp是要接收到哪块空间。  
注意这个参数的数据类型是void *,实际上并不是真正的void*,只是这个数据类型被隐藏起来了。

The msgp argument is a pointer to a caller-defined structure of the following general form:
msgp参数是一个指向调用者定义(我们事先定义好)的结构体的指针,其一般形式如下:
The msgp argument is a pointer to a caller-defined structure of the following general form:

           struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };
           
并不一定要叫做msgbuf这个名字,只是说成员应该大概与这里一样。
第一个参数long mtype; 必须是一个大于0的long整型数。
第二个参数char mtext[1];数组长度为1,这表明当前结构体是一个可变长的结构体。也就是说,数组长度定义成你需要的大小。
这些要在我们的协议中定义的结构体中定义。

第三个参数msgsz是当前要接收的有效字节数。

第四个参数long msgtyp是你是否要挑选消息来收取,比如说,当前消息队列中有10个包,将这个参数设置成你要挑选的消息对应的编号数值。 
          
第五个参数msgflg是一些特殊要求,由一些宏来指定。
比如IPC_NOWAIT,没有消息立马返回。
MSG_NOERROR
To truncate the message text if longer than msgsz bytes.
如果接收到的消息长于指定的msgsz字节,将被截断到msgsz字节。
                      
RETURN VALUE
       On failure both functions return -1 with errno indicating the error, otherwise msgsnd() returns 0 and msgrcv() returns the number  of  bytes  actuallycopied into the mtext array.

消息队列的销毁或其他操作msgctl

NAME
       msgctl - System V message control operations

SYNOPSIS
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgctl(int msqid, int cmd, struct msqid_ds *buf);

其中比较常用的cmd有:
IPC_RMID:作用是移除消息队列。使用IPC_RMID,后一个参数就不需要了。

消息队列是双工的,大家都可以往消息队列上发送消息,也可以从消息队列上接收消息。

现在,我们使用消息队列来实现学生信息的收发的功能。

我们新建一个proto.h的文件,用来定义通信双方的通信协议。
我们新建一个receiver.c的文件,作为接收方。
我们新建一个sender.c的文件,作为发送方。

代码如下:

proto.h

//制定通信协议

#ifndef _PROTO_H
#define _PROTO_H

//要想拿到同一个key,需要哪些参数

/*
    key_t ftok(const char *pathname, int proj_id);
*/

//是一个目录
#define KEYPATH "/work/thread_code/semaphore/msg/test/"

//任意写一个整型数据,比如
//#define KEYPROJ 123
//但是一般认为没写单位(前缀表明是几进制)的整型数,不确定它的含义.
//一般可以定义成字符型,这样转换到程序中一定是整型数.
#define KEYPROJ 'c'

#define NAMESIZE 32

//发送方要发送这样类型的结构体
//接收方要按照这种类型的结构体来解析接收到的数据
struct msg_st
{
    long mtype;
    char name[NAMESIZE];
    int math;
    int chinese;
};

#endif

receiver.c

//负责接收学生信息
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#include "proto.h"

int main()
{
    key_t key;
    int msgid;
    struct msg_st rbuf;

    //获得key值
    key = ftok(KEYPATH,KEYPROJ);
    if(key<0)
    {
        perror("ftok()");
        exit(1);
    }
    //为什么先写被动端,因为被动端先运行起来,
    //当被动方写了IPC_CREAT之后,主动方可以不用再写IPC_CREAT
    //但是,如果主动方写了IPC_CREAT,被动方仍然也要写.

    //创建消息队列
    msgid = msgget(key,IPC_CREAT|0600);
    if(msgid < 0)
    {
        perror("msgget()");
        exit(1);
    }
    
	while(1)
    {
        //接收来自消息队列的信息
        if(msgrcv(msgid,&rbuf,sizeof(rbuf)-sizeof(long),0,0)<0)
        {
            perror("msggrcv()");
            exit(1);
        }
        printf("NAME = %s.\n",rbuf.name);
        printf("MATH = %d.\n",rbuf.math);
        printf("CHINESE = %d.\n",rbuf.chinese);
    }
  
    return 0;
}

sender.c

//负责发送学生信息
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include "proto.h"
#include <string.h>
#include <math.h>
#include <sys/msg.h>

int main()
{
    key_t key;
    int msgid;
    struct msg_st sbuf;

    //获得key值
    key = ftok(KEYPATH,KEYPROJ);
    if(key<0)
    {
        perror("ftok()");
        exit(1);
    }

    //创建消息队列
    msgid = msgget(key,0);
    if(msgid < 0)
    {
        perror("msgget()");
        exit(1);
    }

    sbuf.mtype = 1;
    strcpy(sbuf.name,"chantui");
    sbuf.math=rand()%100;
    sbuf.chinese = rand()%100;

	//作为发送方,要调用msgsnd
    if(msgsnd(msgid,&sbuf,sizeof(sbuf)-sizeof(long),0)<0)
    {
        perror("msgsnd()");
        exit(1);
    }

    //谁创建谁销毁,我没创建,所以我也不用销毁
    //msgctl();
    puts("ok");
    return 0;
}

编译完成之后,先运行receiver程序,程序运行后,由于当前队列中没有消息,因此会在运行等待。
在这里插入图片描述
我们再重新打开一个终端来运行sender程序。
在这里插入图片描述
此时在receiver会接收到消息。
在这里插入图片描述
我们执行命令ipcs会看到消息队列的一些信息。
在这里插入图片描述
假如,我先运行sender程序,
在这里插入图片描述
再来运行receiver程序,又会怎么样呢?
在这里插入图片描述
也能接收到,那这是为什么呢?

我们可以把消息队列理解成有一个有缓存消息的能力。那这个缓存能力有多大呢?

我们可以通过命令 ulimit -a 这个命令来查看。
在这里插入图片描述
我们也可以使用命令 ulimit -q 来对这个空间大小进行修改。

还有一个问题,当我们使用ctrl+c来终止程序后,为什么还能够查看到key的值。
在这里插入图片描述
答:我们使用ctrl+c来终止程序,程序相当于是异常终止。
我们在程序中使用的是用while循环来接收消息。程序没有机会执行到下面的代码。

//销毁消息队列
msgctl(msgid,IPC_RMID,NULL);

消息队列自然就继续存在。
那怎么才能解决这个问题呢?
1、我们可以在程序中使用信号的机制(增加信号捕获和处理函数)。
2、我们可以使用命令ipcrm

我们使用man ipcrm来查看手册看下ipcrm命令的具体使用方法。

-M, --shmem-key shmkey
      Remove the shared memory segment created with shmkey after the last detach is performed.
根据共享内存的Key,来删除共享内存。

-m, --shmem-id shmid
      Remove the shared memory segment identified by shmid after the last detach is performed.
根据共享内存的id,来删除共享内存。

-Q, --queue-key msgkey
      Remove the message queue created with msgkey.
根据消息队列的key ,来删除消息队列。

-q, --queue-id msgid
      Remove the message queue identified by msgid.
根据消息队列的id,来删除消息队列。

-S, --semaphore-key semkey
      Remove the semaphore created with semkey.
根据信号量的key,来删除信号量。

-s, --semaphore-id semid
      Remove the semaphore identified by semid.
根据信号量的id,来删除信号量。

比如我们根据消息队列的id来删除这个消息队列,我们就可以使用命令ipcrm -q 0
在这里插入图片描述
在使用ipcs命令来查看下。
在这里插入图片描述
已经被删除了。

消息队列–ftp实例

在上面的例子中还存在两个问题。
1、结构体成员long mtype的用处。
2、互动的问题(snder方只负责发,receiver方只负责接收),这里要做成双方既可以接收也可以发送。

在下面将会得以解答。

我们这里来完成一个ftp实例。
在这里插入图片描述
C:客户端Client
S:服务器端Server

如图所示,首先,C端先向S端发送一个请求,请求其某个路径下的某个文件,我们把它简称为PATH,然后,S端找到这个文件之后呢,就把这个文件(数据)发送给C端。当然,有可能这个文件很大,不能一次性发送过去,需要分n次发送才能完成,因此我们还要在文件的结尾再发送一个EOF(End of Transmission)标志。

当然,C端向S端请求的文件必须是有权限的常规文件,S端找到这个文件之后呢,首先要按规定的大小,一步步读取到S端的进程中,然后再发送出去。

当程序写好之后,C端和S端哪个应该先运行呢?
答:我们从主动端和被动端来考虑这个问题,C端是发送请求的,S端是接收请求的,所以S端应该是被动端,所以S端应该先运行起来。此外,最好还应该把S端做成守护进程的形式,让它可以一直在后台运行。这样我就可以随时来请求文件。

当然,他们两个应该运行在两个终端上,但这里模拟时,两个程序都运行在同一个设备上,也就是自己既是C端也是S端。

我们这里尝试使用状态机来实现这一功能。

在这里插入图片描述
在这里插入图片描述
对于网络通信和两个无血缘关系的进程通信时,我无法知道你的下一个包是什么,那怎么办呢?

下面提出两种规划。

规划一:使用共用体

新建 proto.h 文件,

#ifndef _PROTO_H_
#define _PROTO_H_

//为了获得同一个Key值
#define KEYPATH "/etc/services"
#define KEYPROJ 'a'

#define PATHMAX 1024 //路径名长度最多为1024
#define DATAMAX 1024 //数据包大小最多为1024

//会产生三种可能的数据包
enum
{
    MSG_PATH=1,
    MSG_DATA,
    MSG_EOT
};

//path包是S端有可能接收到的包
//C端可能接到EOT包,也可能接到data包,无法确定.

//路径包
struct msg_path_st
{
    long mtype;         /* must be MSG_PATH */
    char path[PATHMAX]; /* ASCIIZ带尾0的串  */
}msg_path_t;

//文件数据包
struct msg_data_st
{
    long mtype;        /* must be MSG_DATA */
    char data[DATAMAX];/*带尾0的数据包*/
    //如果发过来的是空洞文件(文件中有一部分或者全部都是0的情况)
    //因此,让它自述长度,也就是使用datalen来描述data包中有效数据是多少
    int datalen;
}msg_data_t;

//表示文件结束传输的EOT包
typedef struct msg_eot_st
{
    long mtype;    /* must be MSG_EOT */
}msg_eot_t;

//两个无血缘关系的进程通信时,我无法知道你的下一个包是什么
//那怎么办呢?
//可以使用联合体
/*
    结构体与共用体相比,最大的特点是成员属于合作关系。内存空间大小是按照结构体成
员定义顺序来分配的。但是,共用体则不同,共用体中的成员是敌我关系。同一时刻,只能
有一个成员生效。根据当前成员所占内存最大的那个来分配空间大小
*/

/*
因为接收端同一时刻只能接收到一种包
虽然同一时刻这三者只会存在一者,但是不管我接收哪一种包,前四个字节都是mtype,于是>根据该值就可以判断出是哪一种包.
*/
union msg_s2c
{
    long mtype;
	msg_data_t datamsg;
    msg_eot_t eotmsg
};

#endif

规划二:将DATA 包和 EOT 包综合为1个包(有缺陷)

在当前,DATA 包和 EOT 包实际上有很多地方是共通的,比如都有 long mtype,因此可以将这两个包给综合为1个包。之后再根据datalen的值进行判断,比如这里规定 datalen > 0 为 data包。datalen == 0 为 eot包 。

这种写法是有缺陷的,是不如协议一中使用共用体所规划的。因为这里只有两种包即MSG_DATA or MSG_EOT,如果再多一种包就不行了。

新建 proto2.h 文件,

#ifndef _PROTO2_H_
#define _PROTO2_H_

//为了获得同一个Key值
#define KEYPATH "/etc/services"
#define KEYPROJ 'a'

#define PATHMAX 1024 //路径名长度最多为1024
#define DATAMAX 1024 //数据包大小最多为1024

//会产生三种可能的数据包
enum
{
    MSG_PATH=1,
    MSG_DATA,
    MSG_EOT
};

//path包是S端有可能接收到的包
//C端可能接到EOT包,也可能接到data包,无法确定.

//路径包
struct msg_path_st
{
    long mtype;         /* must be MSG_PATH */
    char path[PATHMAX]; /* ASCIIZ带尾0的串  */
}msg_path_t;

//文件数据包
//这种写法是有缺陷的,是不如协议一中使用共用体所规划的。
//因为这里只有两种包即MSG_DATA or MSG_EOT,如果再多一种包就不行了。
struct msg_s2c_st
{
    long mtype;        /* must be MSG_DATA or MSG_EOT */
    char data[DATAMAX];/*带尾0的数据包*/
    //如果发过来的是空洞文件(文件中有一部分或者全部都是0的情况)
    //因此,让它自述长度,也就是使用datalen来描述data包中有效数据是多少
    int datalen;
    /*
    *  datalen >  0   :  data包
    *  datalen == 0   :  eot包 
    */
}msg_data_t;

#endif

对于这个的实现,放在了网络通信那里。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuechanba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值