《linux系统编程 —— 5.IPC对象之消息队列 》

1. IPC对象概述

        各种不同的IPC其实是在不同时期逐步引入的,在UNIX伯克利版本system-V(念作系统五,V是罗马数字,是Unix伯克利分支的版本号)中引入的三种通信方式(消息队列、共享内存和信号量组)被称为IPC对象,它们有较多共同的特性:

  • 在系统中使用所谓键值(KEY)来唯一确定,类似于文件系统中的文件路径。
  • 当某个进程创建(或打开)一个IPC对象时,将会获得一个整型ID,即上述的键值KEY,类似于文件描述符。
  • IPC对象属于系统,而不是进程,因此在没有明确删除操作的情况下,IPC对象不会因为进程的退出而消失。

也就是说,如果要实现多个进程间的通信管理,使之相互关联。可以通过 创建/打开同一个 IPC对象进行联调实现。

 

2. IPC对象相关命令

以下命令可以帮助更好了解系统IPC。

2.1 查看IPC对象

 查看所有ipc对象: ipcs -a

root@ubuntu:/mnt/hgfs/share_file# ipcs -a

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 425984     gec        600        67108864   2          目标       
0x00000000 524289     gec        600        524288     2          目标       
0x00000000 557058     gec        600        524288     2          目标       
0x00000000 589827     gec        600        524288     2          目标       
0x00000000 688132     gec        600        524288     2          目标       
0x00000000 720901     gec        600        524288     2          目标       
0x00000000 819206     gec        600        524288     2          目标       

--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems     

root@ubuntu:/mnt/hgfs/share_file# 

查看当前系统的消息队列对象:ipcs -q

root@ubuntu:/mnt/hgfs/share_file# ipcs -q

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息      

root@ubuntu:/mnt/hgfs/share_file# 

查看当前系统的共享内存:ipcs -m

root@ubuntu:/mnt/hgfs/share_file# ipcs -m

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 425984     gec        600        67108864   2          目标       
0x00000000 524289     gec        600        524288     2          目标       
0x00000000 557058     gec        600        524288     2          目标       
0x00000000 589827     gec        600        524288     2          目标       
0x00000000 688132     gec        600        524288     2          目标       
0x00000000 720901     gec        600        524288     2          目标       
0x00000000 819206     gec        600        524288     2          目标     

查看当前系统的信号量数组:ipcs -s

root@ubuntu:/mnt/hgfs/share_file# ipcs -s

--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems     

root@ubuntu:/mnt/hgfs/share_file# 

2.2 删除IPC对象

用大写的字母删除的加消息队列的名字,小写字母删除的加ID号

ipcrm -Q key : 删除指定的消息队列
ipcrm -q id : 删除指定的消息队列

ipcrm -M key : 删除指定的共享内存
ipcrm -m id: 删除指定的共享内存

ipcrm -S key : 删除指定的信号量
ipcrm -s id: 删除指定的信号量

 

3. 消息队列(FIFO)

3.1 基本逻辑

        消息队列是一种 “多对一” 的通信方式,它是一种先进先出(First In First Out)的数据结构,因此消息队列也叫(FIFO)。他类似于现实生活中的队列,进程可以向消息队列中发送消息,其他进程可以从消息队列中接收这些消息。

3.2 应用场景

        消息队列最常见的应用场合是作为读写不同步的两个进程间的缓冲区,比如一个进程从文件读取数据,另一个进程将数据发送到网络,那么如果网络带宽环境较差,数据发送进程可能没办法及时将数据发出,这时数据读取进程就可以通过消息队列将未来得及发出去的数据缓冲区来,不耽误数据的读取,发送者那边根据实际情况从消息队列读取出去再发出去。

        像这样通信双方读写速度不一致,需要的中间缓冲来协调的场合,在涉及硬件读写或网络读写时尤为明显,消息队列都可以作为非常好的中间过渡机制。消息队列实现了对消息发送方和消息接收方的解耦,使得双方可以异步处理消息数据,这是消息队列最重要的应用。

3.3 实现方法

3.3.1 创建/打开 IPC对象所需的键值

        第一步应该创建ipc对象对应键值,获取到这个键值才能创建消息队列,即给消息队列一个指定的通信环境,由同一个key创建的ipc对象直接可相互联调。

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

//ftok的接口
key_t ftok(const char *pathname/*路径*/, int proj_id/*序号*/);

函数接口:

        const char *pathname :文件路径

        int proj_id:序号

        返回值:成功返回对象键值,失败-1

示例:

key_t key = ftok("./",199);

注意事项:

  • 对于ftok()函数参数,首先需要说明的一点是,路径和序号一样的情况下,产生的键值key也是一样的。那么,由于项目开发中,需要互联互通的进程一般会放在同一目录下,而其他无关的进程则不会放在一起,一起使用路径来产生键值是有效避免键值误撞的手段,也就是我们经常会使用"./"作为路径,序号是为了以防在某路径下需要产生多个IPC对象的情况。

  • 最后需要再重申一点的是,ftok()函数参数中的路径仅仅是产生键值key的参数,与实际文件系统并无关系。

3.3.2 创建或打开MSG对象

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

int msgget(key_t key, int msgflg);

接口说明:

  • 返回值:消息队列MSG对象ID
  • 参数key:键值,全局唯一标识,可由ftok()产生
  • 参数msgflg:操作模式与读写权限,与文件操作函数open类似。
  • 如果 key 的值为 IPC_PRIVATE,或者 key 值不存在且 msgflg 包含 IPC_CREAT 标志,则创建一个新的消息队列,并返回与其关联的消息队列标识符。
  • 如果 msgflg 同时包含 IPC_CREAT 和 IPC_EXCL 标志,并且 key 已经存在一个消息队列,那么 msgget() 将失败,并将 errno 设置为 EEXIST。这类似于 open() 函数中使用 O_CREAT | O_EXCL 组合标志的效果。

注意事项:

        多个进程中调用 msgget() 函数,使用相同的 keymsgflg 参数,可以确保获得相同的消息队列标识符 msgId。因此,在这种情况下,msgId 在多次调用中是唯一的。但是需要注意的是,如果不同进程使用相同的 key 但是指定了不同的 msgflg 参数,例如一个进程使用了 IPC_CREAT 标志,而另一个进程没有使用 IPC_CREAT 标志,那么它们将获得不同的消息队列标识符,即使 key 是相同的。 此外,如果之前已经存在使用相同 key 的消息队列,而且没有指定 IPC_CREATIPC_EXCL 标志,那么多次调用 msgget() 函数将获得相同的已存在的消息队列标识符。

        因此,在使用 msgget() 函数时,需要根据具体的场景和需求来选择合适的 keymsgflg 参数,以确保获得所需的唯一消息队列标识符。

示例代码:

int main()
{
    // 以当前目录和序号1为系数产生一个对应的键值
    key_t key = ftok(".", 1);

    // 创建(若存在则报错)key对应的MSG对象
    // int msgid = msgget(key, IPC_CREAT|IPC_EXCL|0666);
    
    // 创建,若存在则直接获取当前消息队列的msgid
    int msg_id = msgget(key, IPC_CREAT|0666);


}

3.3.3 创建消息结构体

在后小节要讲的消息队列的收(msgrcv )发(msgsnd )函数中,规定用于传输数据的结构体要含有有以下类型参数。

typedef struct message {
    long type;
    char text[TEXTBUFMAX];
}message;

type 含义为类型,由用户自行定义。用于区分身份,msgsnd函数用于向消息队列发送消息,并附带自己的type识别号。msgrcv函数可以根据队列中是否存在对应的type号,从而接收指定的消息。

例如我要创建一个消息结构体,需要做如下初始化:

#define TEXTBUFMAX 100

typedef struct message {
    long type;
    char text[TEXTBUFMAX];
}message;

//定义消息结构体,清空
message msg;
memset(&msg,0,sizeof(message));  

msg.type = 999;  

3.3.4 消息收发函数

1、msgsnd() 函数

  • msqid:消息队列的标识符,由 msgget() 函数返回。它指示要发送消息的特定消息队列。
  • msgp:指向消息的指针。消息的格式由用户定义的消息结构体确定。
  • msgsz:消息的大小(以字节为单位)填写的是消息的实际长度,而不是结构体大小。
  • msgflg:发送消息时使用的标志。可以使用以下标志按位组合:
    • IPC_NOWAIT:如果消息队列已满,立即返回,并设置errno为ENOMSG。不会阻塞等待空间可用。
    • MSG_COPY(自Linux 3.8起):在消息队列中按位置顺序非破坏性地获取消息。必须与IPC_NOWAIT一起使用,如果在给定位置没有可用的消息,则立即失败并返回错误ENOMSG。
    • MSG_EXCEPT:当msgtyp大于0时,用于读取消息队列中类型与msgtyp不同的第一条消息。
    • MSG_NOERROR:如果消息文本超过msgsz字节,则截断消息文本,而不返回错误。
    • 0:默认值,大多数清空我们都写0,将 msgflg参数设置为0 等效于不指定任何标志位,msgsnd()函数会被阻塞或将等待消息队列中有足够的空间可用时再发送消息。

以下是伪代码示例:

#define TEXTBUFMAX 100

typedef struct message {
    long type;
    char text[TEXTBUFMAX];
}message;

//定义消息结构体,清空
message msg;
memset(&msg,0,sizeof(message));  

scanf("%s",msg.text);
msgsnd(msg_id,&msg,strlen(msg.text),0 );

上述示例中,msg_id是由3.3.2小节创建而来。

2、msgrcv() 函数

  • msqid:消息队列的标识符,由 msgget() 函数返回。它指示要发送消息的特定消息队列。
  • msgp:指向消息的指针。消息的格式由用户定义的消息结构体确定。
  • msgsz:消息的大小(以字节为单位)填写的是消息的实际长度,而不是结构体大小。
  • msgtyp:欲接收消息的类型:
    • 0:不区分类型,直接读取MSG中的第一个消息。
    • 大于0:读取类型为指定msgtyp的第一个消息(若msgflg被配置了MSG_EXCEPT则读取除了类型为msgtyp的第一个消息)。
    • 小于0:读取类型小于等于msgtyp绝对值的第一个具有最小类型的消息。例如当MSG对象中有类型为3、1、5类型消息若干条,当msgtyp为-3时,类型为1的第一个消息将被读取。
  • msgflg:接收选项:
    • 0:默认接收模式,在MSG中无指定类型消息时阻塞。
    • IPC_NOWAIT:非阻塞接收模式,在MSG中无指定类型消息时直接退出函数并设置错误码为ENOMSG.
    • MSG_EXCEPT:读取除msgtyp之外的第一个消息。
    • MSG_NOERROR:如果待读取的消息尺寸比msgsz大,直接切割消息并返回msgsz部分,读不下的部分直接丢弃。若没有设置该项,则函数将出错返回并设置错误码为E2BIG。

以下是伪代码示例:

#define TEXTBUFMAX 100

typedef struct message {
    long type;
    char text[TEXTBUFMAX];
}message;

//定义消息结构体,清空
message msg;
memset(&msg,0,sizeof(message));  

//由于不知道对方发来的消息有多大,我们直接填写最大值
msgrcv(msg_id,&msg,sizeof(message)-sizeof(long),999,0 );//接收999这个类型的数据

要注意的是,调用msgrcv函数实际上算已经完成了出队操作,即不会再占用队列资源。

在学习了上述msgsnd和msgrcv后我们可以自行封装函数,使得主程序部分更简洁。方式如下:

/**
 * @description: 发送信息到消息队列当中
 * @param {int} msg_id  消息队列对象id
 * @param {char} *text  发送的内容
 * @param {int} length  长度
 * @param {int} my_type 自己的类型号
 * @return {*}  无
 */
void MsgSequence_Snd(int msg_id, char *text, int length, int my_type)
{
    struct message {
        long type;
        char text[1024];
    };

    struct message msg;
    memset(&msg,0,sizeof(msg));

    msg.type = my_type;
    memcpy(msg.text,text,length);
    msgsnd(msg_id,&msg,length,0 );
} 


/**
 * @description: 接收消息队列的指定类型号的信息
 * @param {int} msg_id      消息队列对象id
 * @param {int} rec_type    接收的消息类型号
 * @return {char*}  指向信息的指针,使用完记得free
 */
char* MsgSequence_Rec(int msg_id, int rec_type)
{
    struct message {
        long type;
        char text[1024];
    };

    struct message msg;
    memset(&msg,0,sizeof(msg));

    msgrcv(msg_id,&msg,sizeof(msg)-sizeof(long),rec_type,0);

    char *rec_buf = malloc( strlen(msg.text) );
    bzero(rec_buf,strlen(msg.text));
    memcpy(rec_buf,msg.text,strlen(msg.text));

    return rec_buf;
}

 3.3.4 对MSG对象其余操作

        IPC对象是一种持久性资源,如果没有明确的删除掉他们,他们是不会自动从内存中消失的,除了可以使用命令的方式删除,可以使用函数来删除。比如,要想显式地删除掉MSG对象,可以使用如下接口:

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

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

接口说明:

  • msqid:MSG对象ID
  • cmd:控制命令字
    • IPC_STAT:获取该MSG的信息,储存在结构体msqid_ds中
    • IPC_SET:设置该MSG的信息,储存在结构体msqid_ds
    • IPC_RMID:立即删除该MSG,并且唤醒所有阻塞在该MSG上的进程,同时忽略第三个参数

在程序中如果不再使用MSG对象,为了节省系统资源,应用如下代码删除:

msgctl(id, IPC_RMID, NULL);

4.项目实战:

实现rose和jack的通信,当双方有一方输入quit时,退出并删除消息队列。

jack.c代码如下

/*
 * @Author: Fu Zhuoyue
 * @Date: 2023-07-15 10:35:45
 * @LastEditors: Fu Zhuoyue
 * @LastEditTime: 2023-07-15 15:55:06
 * @Description: pri
 * @FilePath: /share_file/系统编程/4.IPC对象/jack.c
 */
#include "../syshead.h"
key_t key_access;
int msg_id;
long type_id_01 = 100;
long type_id_02 = 200;

/**
 * @description: 发送信息到消息队列当中
 * @param {int} msg_id  消息队列对象id
 * @param {char} *text  发送的内容
 * @param {int} length  长度
 * @param {int} my_type 自己的类型号
 * @return {*}  无
 */
void MsgSequence_Snd(int msg_id, char *text, int length, int my_type)
{
    struct message {
        long type;
        char text[1024];
    };

    struct message msg;
    memset(&msg,0,sizeof(msg));

    msg.type = my_type;
    memcpy(msg.text,text,length);

    msgsnd(msg_id,&msg,length,0 );
} 

/**
 * @description: 接收消息队列的指定类型号的信息
 * @param {int} msg_id      消息队列对象id
 * @param {int} rec_type    接收的消息类型号
 * @return {char*}  指向信息的指针,使用完记得free
 */
char* MsgSequence_Rec(int msg_id, int rec_type)
{
    struct message {
        long type;
        char text[1024];
    };

    struct message msg;
    memset(&msg,0,sizeof(msg));

    msgrcv(msg_id,&msg,sizeof(msg)-sizeof(long),rec_type,0);

    char *rec_buf = malloc( strlen(msg.text) );
    bzero(rec_buf,strlen(msg.text));
    memcpy(rec_buf,msg.text,strlen(msg.text));

    return rec_buf;

}

void Father_AskSon_Return(int sig_num)
{  
    SLEEP_MS(10);
    printf("子进程退出\n");
    exit(0);        //子进程直接退
}

void Son_AskFather_Return(int sig_num)
{
    wait(NULL);     //父进回收
    SLEEP_MS(10);   
    msgctl(msg_id, IPC_RMID, NULL); //关队列
    printf("父进程退出\n");
    exit(0);        //父再退
}

int main()
{
    key_access = ftok(".",10086); 
    printf("%s,key_access_01 = %d\n",__FILE__,key_access);
    msg_id = msgget(key_access,0666|IPC_CREAT);
    
    printf("本次消息队列id是%d\n",msg_id);
    if(msg_id == -1)
    {
        printf("失败\n");
        return -1;
    }

    pid_t child_pid = fork();   //创建多进程

    if(child_pid == 0)  //子进程收对方的信息
    {
        signal(SIGUSR1, Son_AskFather_Return); //注册响应函数,父进程要子退
        while(1)
        {
            // msgrcv(msg_id,&msg,sizeof(message)-sizeof(long),type_id_02,0);
            char *rec_buf = MsgSequence_Rec( msg_id, type_id_02 );
            if(  ( strstr(rec_buf,"quit") != NULL)  )
            {
                printf("b_msg.text = %s\n",rec_buf);
                printf("进入退出\n");

                free(rec_buf);
                kill(getppid(),SIGUSR1);    //给父进程发SIGUSR1
                SLEEP_MS(10);
                exit(0);
            }

            printf("Jack收到的数据是%s\n",rec_buf);
            free(rec_buf);
        }
    }
    else if( child_pid > 0 )    //父亲发
    {
        signal(SIGUSR2, Son_AskFather_Return);   //注册响应函数,子进程要父亲退
        
        while (1)
        {
            char buf[100] = "";
            printf("请输入你要发送给ROSE的数据:\n");   //发送给对方子进程的消息
            // fputs(a_msg.text,stdin);
            scanf("%s",buf);
            MsgSequence_Snd(msg_id, buf, strlen(buf),type_id_01);
            //如果父亲自己想退
            if( (strstr(buf,"quit") != NULL))
            {
                //那就告诉子进程要退
                kill(child_pid,SIGUSR2);
                wait(NULL);
                msgctl(msg_id, IPC_RMID, NULL); //关队列
                exit(0);
            }

        }
    }
    return 0;
}

rose.c代码如下:

/*
 * @Author: Fu Zhuoyue
 * @Date: 2023-07-15 10:36:24
 * @LastEditors: Fu Zhuoyue
 * @LastEditTime: 2023-07-15 16:02:27
 * @Description: 
 * @FilePath: /share_file/系统编程/4.IPC对象/rose.c
 */
#include "../syshead.h"

/**
 * @description: 发送信息到消息队列当中
 * @param {int} msg_id  消息队列对象id
 * @param {char} *text  发送的内容
 * @param {int} length  长度
 * @param {int} my_type 自己的类型号
 * @return {*}  无
 */
void MsgSequence_Snd(int msg_id, char *text, int length, int my_type)
{
    struct message {
        long type;
        char text[1024];
    };

    struct message msg;
    memset(&msg,0,sizeof(msg));

    msg.type = my_type;
    memcpy(msg.text,text,length);
    msgsnd(msg_id,&msg,length,0 );
} 

/**
 * @description: 接收消息队列的指定类型号的信息
 * @param {int} msg_id      消息队列对象id
 * @param {int} rec_type    接收的消息类型号
 * @return {char*}  指向信息的指针,使用完记得free
 */
char* MsgSequence_Rec(int msg_id, int rec_type)
{
    struct message {
        long type;
        char text[1024];
    };

    struct message msg;
    memset(&msg,0,sizeof(msg));

    msgrcv(msg_id,&msg,sizeof(msg)-sizeof(long),rec_type,0);

    char *rec_buf = malloc( strlen(msg.text) );
    bzero(rec_buf,strlen(msg.text));
    memcpy(rec_buf,msg.text,strlen(msg.text));

    return rec_buf;

}

key_t key_access;
long type_id_01 = 100;
long type_id_02 = 200;
int msg_id;

void Father_AskSon_Return(int sig_num)
{  
    SLEEP_MS(10);
    exit(0);        //子进程直接退
}

void Son_AskFather_Return(int sig_num)
{
    wait(NULL);     //父进回收
    SLEEP_MS(10);   
    msgctl(msg_id, IPC_RMID, NULL); //关队列
    exit(0);        //父再退
}

int main()
{
    key_access = ftok(".",10086); 
    printf("%s,key_access_01 = %d\n",__FILE__,key_access);

    int msg_id = msgget(key_access,0666|IPC_CREAT);
    printf("本次消息队列id是%d\n",msg_id);
    
    if(msg_id == -1)
    {
        printf("失败\n");
        return -1;
    }
    pid_t child_pid = fork();   //创建多进程

    if(child_pid == 0)  //子进程收对方的信息
    {
        signal(SIGUSR1, Son_AskFather_Return); //注册响应函数,父进程要子退
        while(1)
        {
            char *rec_buf = MsgSequence_Rec(msg_id, type_id_01);

            if((strstr(rec_buf,"quit") != NULL))
            {
                free(rec_buf);
                kill(getppid(),SIGUSR1);    //给父进程发SIGUSR1
                SLEEP_MS(10);
                exit(0);
            }

            printf("Jack收到的数据是%s\n",rec_buf);
            free(rec_buf);
        }
    }
    else if( child_pid > 0 )    //父亲发
    {
        signal(SIGUSR2, Son_AskFather_Return);   //注册响应函数,子进程要父亲退

        while (1)
        {
            char tmp_buf[100] = "";
            printf("请输入你要发送给JAKE的数据:\n");   //发送给对方子进程的消息
            // fputs(b_msg.text,stdin);
            
            scanf("%s",tmp_buf);
            MsgSequence_Snd(msg_id, tmp_buf, strlen(tmp_buf), type_id_02);

            //如果父亲自己想退
            if( (strstr(tmp_buf,"quit") != NULL))
            {
                //那就告诉子进程要退
                kill(child_pid,SIGUSR2);
                wait(NULL);
                msgctl(msg_id, IPC_RMID, NULL); //关队列
                exit(0);
            }

        }
    }
    return 0;
}

运行结果如下:

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值