进程间通信(4)——消息队列

0. 前言

进程间通信——序  一文中说到 system V版本,AT&T引进了一种新形式的IPC 功能(信号量、消息队列以及共享内存)。

System IPC 中,对于每一个新建的信号量、消息队列以及共享内存,都有一个在整个系统中唯一的标识符。每个标识符都有唯一对应的关键字,关键字的数据类型由系统定义为key_t。

在终端命令行输入ipcs 命令,可以看到系统所有IPC信息:

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0x6d003256 73072640   shift      644        1024         1           

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0x00000000 5668864    lightdm    600        524288     2          dest         
0x00000000 5767169    lightdm    600        524288     2          dest         

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

第一列key 就是IPC 的关键字,第二列是IPC 的标识符。

在终端命令行输入ipcs -l来显示IPC 相关的限制:

------ Messages Limits --------
max queues system wide = 32000
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384

------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 18014398509465599
max total shared memory (kbytes) = 18014398442373116
min seg size (bytes) = 1

------ Semaphore Limits --------
max number of arrays = 32000
max semaphores per array = 32000
max semaphores system wide = 1024000000
max ops per semop call = 500
semaphore max value = 32767

这些限制中大多数可以通过重新配置内核来改变。

对于这3中IPC 结构,存在两个问题:

  • 该结构在系统范围内起作用,没有引用计数。所以如创建一个消息队列,在最后终止并不会主动删除该消息队列,消息队列中的内容还是存在的。并不像FIFO,虽然最后FIFO文件依然存在,但FIFO中的数据已经被删除。
  • 这些结构在系统中没有名字。所以不像FIFO 在最后可以通过rm 删除文件。

对于上述两个问题,IPC 提供了解决办法:

  • msgctl、semctl、shamctl对相应的IPC 进行删除
  • 命令ipcrm 进行删除对应的ipc

2. ftok()

ftok() 函数用于获得一个IPC 关键字,函数原型:

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

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

ftok函数返回一个以pathname 指向的文件相对应的文件和proj_id 来确定一个IPC 关键字key_t。pathname必须是一个已经存在并具有访问权限的文件,proj_id 只有最低的8个字节是有效的,所以通常用一个ASCII 字符来作为proj_id。

当pathname 和proj_id 完全相同时,每次调用ftok() 都会获取一个相同的关键字。

IPC 关键字可以通过ftok() 函数来获得,也可以设为IPC_PRIVATE,这时操作系统确保创建一个新的IPC,其标识符需要由进程自己记录并告诉其他进程。

3. 消息队列

消息队列是系统内核地址空间中的一个内部的链表。消息可以按照顺序发送到队列中,也可以以集中不同的方式从队列中读取。没一个消息队列用一个唯一的IPC 标识符表示。

消息队列特点:

  • 生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除
  • 消息队列可以双向通信
  • 克服了管道只能承载无格式字节流的缺点

来看下数据结构msgbuf,此数据结构需要用户自己定义,但了解系统中有这样一个数据结构是十分重要的,在<sys/msg.h>中,此结构是这样定义的:

struct msgbuf {
    __syscall_slong_t mtype;       /* type of received/sent message */
    char mtext[1];    /* text of the message */
}

数据结构msgbuf 中共有两个元素:

  • mtype 指消息的类型,它由一个整数来代表,并且只能是大于0的整数。
  • mtext 是消息的数据本身

当然,mtext 字段不但可以存储字符,还可以存储任何其他的数据类型。此字段可以说是完全任意的,由程序员自己定义,例如:

struct msgbuf {
    long mtype;       /* type of received/sent message */
    char request_id;    /* request identifier */
    struct client info;   /* client information structure */
}

mtype 是必须的,其他数据是自己定义的。

4. 消息队列限制

这个在前言中已经讲过,通过ipcs -l 可以查看到系统的限制。

也可以通过cat /proc/sys/kernel/msg* 来查看:

cat /proc/sys/kernel/msgmax  //消息最大长度,包括type
cat /proc/sys/kernel/msgmnb  //每个消息队列总长度
cat /proc/sys/kernel/msgmni  //系统中消息队列总数的上限

5. 创建消息队列

系统调用msgget() 来创建一个新的消息队列,或者存取一个已经存在的消息队列,原型:

#include <sys/msg.h>            //里面包含了#include <sys/ipc.h>

int msgget(key_t key, int msgflag);

参数: 

  • key:某个消息队列的名字,用ftok()产生 
  • msgflag:有两个选项IPC_CREAT和IPC_EXCL,单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。 

返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1。可以通过errno和perror函数来查看错误信息。
 

6. 控制消息队列

消息队列标识符的属性被记录在一个msgid_ds的结构体:

struct msqid_ds {
    struct ipc_perm msg_perm;        /* structure describing operation permission */
    time_t          msg_stime;       /* time of last msgsnd command */
    time_t          msg_rtime;       /* time of last msgrcv command */
    time_t          msg_ctime;       /* time of last change */
    unsigned log    __msg_cbytes;    /* current number of bytes on queue */
    msgqnum_t       msg_qnum;        /* number of messages currently on queue */
    msglen_t        msg_qbytes;      /* max number of bytes allowed on queue */
    pid_t           msg_lspid;       /* pid of last msgsnd() */
    pid_t           msg_lrpid;       /* pid of last msgrcv() */
    ...
}

通过msgctl() 可以对消息队列进行控制或者一些属性的修改,函数原型:

#include <sys/msg.h>          //里面包含了 #include <sys/ipc.h>

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

参数: 
msqid:由msgget函数返回的消息队列标识码 
cmd:有三个可选的值

  • IPC_STAT 把msqid_ds结构中的数据设置为消息队列的当前关联值
  • IPC_SET 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的值
  • IPC_RMID 删除消息队列

返回值: 成功返回0,失败返回-1
 

7. 发送消息

msgsnd() 向队列发送一条消息,函数原型:

#include <sys/msg.h>

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

参数: 

  • msgid:由msgget函数返回的消息队列标识码 
  • msgp:指针指向准备发送的消息 
  • msgze:msgp指向的消息的长度(不包括消息类型的long int长整型) 
  • msgflg:默认为0 ,即从队列中取出最长时间的一条消息。

返回值:成功返回0,失败返回-1

消息结构一方面必须小于系统规定的上限,另一方面必须以一个long int长整型开始,接受者以此来确定消息的类型。

8. 接受消息

msgrcv() 用于从消息队列中读取一条消息,函数原型:

#include <sys/msg.h>

int msgrcv(int msqid, const void *msgp, size_t msgsz, long msgtype int msgflg);

参数: 

  • msgid:由msgget函数返回的消息队列标识码 
  • msgp:指针指向准备发送的消息 
  • msgze:msgp指向的消息的长度(不包括消息类型的long int长整型) 
  • msgtype:指定读取的消息的type
  • msgflg:默认为0, 即从队列中取出最长时间的一条消息。

返回值:成功返回0,失败返回-1

注意,参数msgflg 取值为:

0:从队列中取出最长时间的一条消息。

IPC_NOWAIT:当队列没有消息时,调用会立即返回ENOMSG错误。否则,调用进程会被挂起,知道队列中的一条细细满足msgrcv() 的参数要求。

9. 实例

test_msgq.h

#ifndef __TEST_MESSAGE_QUEUE_INCLUDE__
#define __TEST_MESSAGE_QUEUE_INCLUDE__

//include 3 header files for message queue
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define msg_key "msgq_key"
#define msg_len(x,y) sizeof(x)-sizeof(y)

typedef struct{
    long type;
    char msg[1024];
} msg_buf_t;

#define SERVER_TYPE 123
#define CLIENT_TYPE 456

int get_msg_queue();
int create_msg_queue();
int remove_msg_queue(int msqid);
int send_msg(int msqid, char* msg, int who);
int receive_msg(int msqid, char*msg, int who);

#endif //#ifndef __TEST_MESSAGE_QUEUE_INCLUDE__
  • 定义了通过ftok() 获取的文件名:msgq_key
  • 定义了消息的数据结构:msg_buf_t
  • 定义了server、client的type,用于读取消息

test_msgq.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "test_msgq.h"

static int create_msg_queue_common(int flags)
{
    printf("create message queue\n");

    key_t key = ftok(msg_key, 'm');
    if (key < 0) {
        perror("ftok error.\n");
        return -1;
    }
    printf("key is 0x%x\n", key);

    int msqid = msgget(key, flags);
    if (msqid == -1) {
        perror("msgget error\n");
    }
    printf("msqid is %d\n", msqid);

    return msqid;
}

int create_msg_queue()
{
    return create_msg_queue_common(IPC_CREAT | IPC_EXCL | 0644);
}

int get_msg_queue()
{
    return create_msg_queue_common(IPC_CREAT);
}

int remove_msg_queue(int msqid)
{
    printf("remove message queue, msqid is %d\n", msqid);
    if(msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl error\n");
        return -1;
    }

    return 0;
}

int send_msg(int msqid, char* msg, int who)
{
    msg_buf_t msg_buf;
    msg_buf.type = who;
    strcpy(msg_buf.msg, msg);

    if(msgsnd(msqid, (void*)&msg_buf, msg_len(msg_buf_t, msg_buf.type), 0) == -1) {
        perror("msgsnd error\n");
        return -1;
    }

    return 0;
}

test_read.c

#include <stdio.h>
#include <unistd.h>
#include <memory.h>

#include "test_msgq.h"


// server for testing.
int main()
{
    printf("main for read message.\n");

    int msqid = create_msg_queue();

    char msg[1024] = {0};

    int n = 40;
    while(n > 20) {
        receive_msg(msqid, msg, CLIENT_TYPE);
        printf("receive from client: %s\n", msg);

        memset(msg, 0, sizeof(msg));
        sprintf(msg, "send %d from server", n);

        send_msg(msqid, msg, SERVER_TYPE);
        sleep(1);

        n--;
    }

    return 0;
}

test_read.c 代表server 端。首先收取从client 端发出的消息,同时发送一个server 的消息给client端。

test_write.c

#include <stdio.h>
#include <unistd.h>
#include <memory.h>

#include "test_msgq.h"

//client for testing.
int main()
{
    printf("main for write message.\n");

    int msqid = create_msg_queue();

    char msg[1024] = {0};

    int n = 20;
    while(n) {
        sprintf(msg, "send %d from client", n);
        send_msg(msqid, msg, CLIENT_TYPE);
        sleep(1);

        memset(msg, 0, sizeof(msg));
        receive_msg(msqid, msg, SERVER_TYPE);
        printf("receive from server: %s\n", msg);

        n--;
    }

    return 0;
}

先发送一个消息给server端,然后接受来自server 的消息。

运行结果:

./test_read 
main for read message.
create message queue
key is 0x6d003256
msqid is 73072640
receive from client: send 20 from client
receive from client: send 19 from client
receive from client: send 18 from client
receive from client: send 17 from client
receive from client: send 16 from client
receive from client: send 15 from client
receive from client: send 14 from client
receive from client: send 13 from client
^C
./test_write 
main for write message.
create message queue
key is 0x6d003256
msqid is 73072640
receive from server: send 21 from server
receive from server: send 40 from server
receive from server: send 39 from server
receive from server: send 38 from server
receive from server: send 37 from server
receive from server: send 36 from server
receive from server: send 35 from server
^C

注意,第一条信息是消息队列曾经存在的最后一次的消息。

相关博文:

进程间通信(0)——序 

进程间通信(1)——信号(Signal)

进程间通信(2)——管道(PIPE)

进程间通信(3)——命名管道(FIFO)

进程间通信(4)——消息队列

进程间通信(5)——共享内存

进程间通信(6)——信号量(semaphore​​​​​​)

进程间通信(7)——套接字(socket)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

私房菜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值