消息队列的实质是一个存放消息的链表,该链表由内核维护;消息队列中的每个消息可以视为一条记录,消息包括一个长整型的类型字段和需要传递的数据。消息队列由消息队列标识符(queue ID)标识,对消息队列有读权限的进程可以从队列中读取消息,对消息队列有写权限的进程可以按照规则,向其中添加消息。
与管道相比,消息队列的通信方式更为灵活:它提供有格式的字节流,无需通信双方额外约定数据传输格式;其中的消息设定为不同类型,又被分配了不同的优先级,新添加的消息总是在队尾,但接收消息的进程可以读取队列中间的数据。此外,消息队列也降低了读写进程间的耦合强度:若接收消息的进程没有接收到消息,发送消息的进程无须等待,可以继续发送消息,消息的读写双方只需关注各自功能的实现情况即可。
与FIFO类似,消息队列可以实现无亲缘关系进程间的通信,且独立于通信双方的进程之外,若没有删除内核中的消息队列,即便所有使用消息队列的进程都已终止,消息队列仍存在于内核中,直到内核重新启动、管理命令被执行或调用系统接口删除消息队列时,消息队列才会真正被销毁。
系统中的最大消息队列数与系统中最大消息数都有一定限制,分别由宏MSGMNI和宏MSGTOL定义;消息队列的每个消息中所含数据块的长度以及队列中所含数据块的总长度也有限制,分别由宏MSGMAX和MSGMNB定义。
使用消息队列实现进程间通信的步骤如下:
(1)创建消息队列;
(2)发送消息到消息队列;
(3)从消息队列中读取数据;
(4)删除消息队列。
Linux内核提供了四个系统调用,用于实现以上步骤,这四个系统调用接口分别为:msgget()、msgsnd()、msgrcv()和msgctl(),下面分别对这四个系统调用进行讲解。
① msgget()
msgget()函数的功能为创建一个消息队列,或获取一个已经存在的消息队列,该函数存在于函数库sys/msg.h中,其函数声明如下:
若该函数调用成功,则返回消息队列的标识符,否则返回-1,并设置errno。
msgget()中的参数key表示消息队列的键值,通常为一个整数,若键值为IPC_PRIVATE,将会创建一个只能被创建消息队列的进程读写的消息队列;参数msgflg类似于open()函数中标志位的功能,用于设置消息队列的创建方式或权限,它通常由一个9位的权限与以下值进行位操作后获得:
a) 当msgflg=mask|IPC_CREAT时,若内核中不存在指定消息队列,该函数会创建一个消息队列;若内核中已存在指定消息队列,则获取该消息队列;
b) 当msgflg= mask|IPC_CREAT|IPC_EXCL时,消息队列不存在时会被创建,已存在时msgset()调用失败,返回-1,设置errno为EEXIST。
② msgsnd()
msgsnd()函数的功能为向指定消息队列中发送一个消息,该函数存在于函数库sys/msg.h中,其函数声明如下:
若该函数调用成功,则返回消息队列的标识符,否则返回-1,并设置errno。
msgsnd()函数发送的消息受两项约束:一是消息长度必须小于系统规定上限;二是消息必须以一个长整形成员变量开始,因为需要利用此变量先确定消息的类型。Linux系统中定义了一个模板数据结构,其形式如下:
msgsnd()中的参数msgid表示消息队列标识符,即msgget()调用成功时的返回值;参数msgp表示指向消息缓冲区的指针;参数msgsz表示消息中数据的长度,这个长度不包括长整型成员变量的长度;参数msgflg为标志位,可以设置为0或IPC_NOWAIT,若消息队列已满或系统中的消息数量达到上限,即当msgflg设置为IPC_NOWAIT时,函数立即返回(返回值为-1);当msgflg设置为0时,调用函数的进程会被挂起,直到消息写入消息队列为止。
③ msgrcv()
msgrcv()函数的功能是从消息队列中读取消息,被读取的消息会从消息队列中消失,该函数存在于函数库sys/msg.h中,其函数声明如下:
若该函数调用成功,则返回消息队列的标识符,否则返回-1,并设置errno。
msgrcv()中的参数msgid表示消息队列的id号,通常由msgset()函数返回;参数msgp为指向所读取消息的结构体指针;msgsz表示消息的长度,这个长度不包含整型成员变量的长度;参数mtype表示从消息队列中读取的消息类型,其取值以及各值代表的含义分别如下:
● 若mtype=0,表示获取队列中的第一个可用消息;
● 若mtype>0,表示获取队列中与该值类型相同的第一个消息;
● 若mtype<0,表示获取队列中消息类型小于或等于其绝对值的第一个消息。
最后一个参数msgflg依然为标志位,msgflg设置为0时,进程将阻塞等待消息的读取;msgflg设置为IPC_NOWAIT时,进程未读取到指定消息时将立刻返回-1。
④ msgctl()
msgctl()函数的功能是对指定消息队列进行控制,该函数存在于函数库sys/msg.h中,其函数声明如下:
若该函数调用成功,则返回消息队列的标识符,否则返回-1,并设置errno,若进程正因调用msgsnd()或msgrcv()而产生阻塞,这两个函数将以失败返回。
msgctl()函数功能的选择与参数有关,其中参数msgid表示消息队列的id,通常由msgset()返回;参数cmd表示消息队列的处理命令,通常有以下几种取值:
● IPC_RMID。该取值表示msgctl()函数将从系统内核中删除指定命令,使用命令“ipcrm –q id”可实现同样功能;
● IPC_SET。该取值表示若进程有权限,就将内核管理的消息队列的当前属性值设置为参数buf各成员的值;
● IPC_STAT。该取值表示将内核所管理的消息队列的当前属性值复制给参数buf。
参数buf是一个缓冲区,用于传递属性值给指定消息队列,或从指定消息队列获取属性值,其功能视参数cmd而定。其数据类型struct msqid为一个结构体,内核为每个消息队列维护了一个msqid_ds结构,用于消息队列的管理,该结构体的定义在sys/ipc.h中,详细信息如下:
下面通过案例来展示消息队列相关的系统调用接口的使用方法。
案例4:使用消息队列实现不同进程间的通信。因为要实现不同进程间的通信,所以此处使用两个程序msgsend.c、msgrcv.c分别作为消息的发送端和接收端。案例实现如下:
msgsend.c //发送端
msgrcv.c //接收端
代码中的BUFSIZ为Linux系统定义的宏,定义在stdio.h中,表示默认的缓冲大小。程序msgsend.c作为消息发送方,向创建的消息队列中发送消息;程序msgrcv.c作为消息接收方,从消息队列中读取数据。编译以上两段代码,分别在不同的终端执行,当进程msgsend.c有消息输入时,进程msgrcv.c所在的终端会将消息从消息队列中读出。代码中设置发送消息的进程发送4条消息,执行程序后根据提示在终端输入如下4条信息,信息输入完毕后进程终止:
每当有一条信息输入,在接收进程所在终端会打印出一条信息,接收到4条信息后进程终止。接收进程所在终端中打印的信息如下:
多学一招:键值与标识符
对多个进程来说,要通过消息队列机制实现进程间通信,必须能与相同消息队列进行关联,键值(key)就是实现进程与消息队列关联的关键。当在进程中调用msgget()函数创建消息队列时,传入的key值会被保存到内核中,与msgget()函数创建的消息队列一一对应;若进程中调用msgget()函数获取已存在的消息队列,只需向msgget()函数中传入键值,就能获取到内核中与键值对应的消息队列。也就是说,键值是消息队列在内存级别的唯一标识。
对单个进程来说,可能需要实现与多个进程间的通信,因此会与多个消息队列关联,当多次调用msgget()函数与多个消息队列进行关联时,每个msgget()函数都会返回一个非负整数,这个非负整数就是进程对消息队列的标识,标识符是消息队列在进程级别的唯一标识。
除消息队列外,其它System V IPC类型(信号量、共享内存)的通信方式也使用与消息队列类似的编程接口,信号量通信与共享内存通信中用到的键值和标识符,与消息队列中键值及标识符的功能类似。