Linux C编程--进程间通信(IPC)5--System V IPC 机制1--消息队列

时间:2021-03-05 14:53:50

System V IPC 机制

1.基本概念

IPC对象一经创建,系统内核即为该对象分配相关的数据结构。为方便对IPC对象的管理,Linux提供了专门的IPC控制命令,主要包括查看IPC对象信息的ipcs和删除IPC对象的ipcrm。

查看IPC对象信息
命令:ipcs [-aqms]
参数说明:
1)-a:查看全部IPC对象信息。
2)-q:查看消息队列信息。
3)-m:查看共享内存信息。
4)-s:查看信号量信息。

 删除IPC对象
命令1:ipcrm -[qms] ID
命令2:ipcrm -[QMS] key
参数说明:
1)-q或-Q:删除消息队列信息。
2)-m或-M:删除共享内存信息。
3)-s或-S:删除信号量信息。
注意事项:
        如果指定了qms,则用IPC对象的标识符(ID)作为输入;如果指定了QMS,则用IPC对象的键值(key)作为输入。

进程将通信关键字
        与文件系统中的文件一样,为区别不同的进程间通信对象,Linux的每个IPC对象都有一个名字,成为“键”(key)。这个关键字是全局惟一的,类似于文件系统中的文件名称。
        进程间通信关键字是一个32位的长整型数据。在使用过程中,不同的进程直接使用key去创建IPC对象容易引起混淆,并且不同的应用之间可能会因为使用同一个key而产生冲突。为此,Linux系统提供了如下机制产生惟一的关键字。
1)创建IPC对象时,指定关键字为IPC_PRIVATE。通过该参数创建的IPC对象的关键字值是0,所以无法在其他进程中通过关键字对该对象进行访问,只能通过返回的标识符进行访问。
2)调用函数ftok产生一个惟一的关键字值。通过IPC进行通信的进程,只需要按照相同的参数调用ftok即可产生惟一的参数。通过该参数可有效解决关键字的产生及惟一性问题。

 进程间通信标识符
        虽然IPC机制提供了“键”来惟一标识一个IPC对象,但是对IPC对象的访问并不是通过“键”,而是通过标识符进行的。IPC机制提供了相应的编程接口根据IPC键值获取标识符。对于一个IPC对象,在打开时返回一个标识符,而关闭后再次打开同一个IPC对象时,该标识符将顺序加1.这就是下面将要介绍的IPC权限许可结构中的seq字段的作用。

 IPC权限许可结构
        在IPC对象创建成功后,Linux系统在内核为该对象分配了权限许可结构并初始化,该结构定义了对该IPC对象进访问的许可权。IPC对象创建后,可以通过相应的IPC控制函数(msgctl、shmctl、semctl等)对权限许可及属主ID等进行修改。这一过程类似于文件系统中的chmod和chown。该结构的定义位于头文件<bits/ipc.h>中,如下所示。
/* Data structure used to pass permission information to IPC operations.  */
struct ipc_perm
{
    __key_t __key;        /* Key.  */
    __uid_t uid;                /* Owner's user ID.  */
    __gid_t gid;                /* Owner's group ID.  */
    __uid_t cuid;        /* Creator's user ID.  */
    __gid_t cgid;        /* Creator's group ID.  */
    unsigned short int mode;/* Read/write permission.  */
    unsigned short int __pad1;
    unsigned short int __seq;/* Sequence number.  */
    unsigned short int __pad2;
    unsigned long int __unused1;
    unsigned long int __unused2;
};

2.消息队列

一个或多个进程可向消息队列写入消息,而一个或多个进程可从消息队列中读取消息,这种进程间通讯机制通常使用在客户/服务器模型中,客户向服务器发送请求消息,服务器读取消息并执行相应请求。在许多微内核结构的操作系统中,内核和各组件之间的基本通讯方式就是消息队列。例如,在 MINIX 操作系统中,内核、I/O 任务、服务器进程和用户进程之间就是通过消息队列实现通讯的。 

Linux中的消息可以被描述成在内核地址空间的一个内部链表,每一个消息队列由一个IPC的标识号唯一的标识。Linux 为系统中所有的消息队列维护一个 msgque 链表,该链表中的每个指针指向一个 msgid_ds 结构,该结构完整描述一个消息队列。 

1. 数据结构 

(1)消息缓冲区(msgbuf) 
我们在这里要介绍的第一个数据结构是msgbuf结构,可以把这个特殊的数据结构看成一个存放消息数据的模板,它在include/linux/msg.h中声明,描述如下: 

/* msgsnd 和msgrcv 系统调用使用的消息缓冲区*/ 
struct msgbuf { 
long mtype; /* 消息的类型,必须为正数 */ 
char mtext[1]; /* 消息正文 */ 
};


注意:对于消息数据元素(mtext),不要受其描述的限制。实际上,这个域(mtext)不仅能保存字符数组,而能保存任何形式的任何数据。这个域本身是任意的,因为这个结构本身可以由应用程序员重新定义: 

struct my_msgbuf { 
long mtype; /* 消息类型 */ 
long request_id; /* 请求识别号 */ 
struct client info; /* 客户消息结构 */ 
};


我们看到,消息的类型还是和前面一样,但是结构的剩余部分由两个其它的元素代替,而且有一个是结构。这就是消息队列的优美之处,内核根本不管传送的是什么样的数据,任何信息都可以传送。 

但是,消息的长度还是有限制的,在Linux中,给定消息的最大长度在include/linux/msg.h中定义如下: 

#define MSGMAX 8192 /* max size of message (bytes) */


消息总的长度不能超过8192字节,包括mtype域,它是4字节长。 

(2)消息结构(msg) 

内核把每一条消息存储在以msg结构为框架的队列中,它在include/linux/msg.h中定义如下: 

struct msg { 
struct msg *msg_next; /* 队列上的下一条消息 */ 
long msg_type; /*消息类型*/ 
char *msg_spot; /* 消息正文的地址 */ 
short msg_ts; /* 消息正文的大小 */ 
};


注意:msg_next是指向下一条消息的指针,它们在内核地址空间形成一个单链表。 

(3)消息队列结构(msgid_ds) 

当在系统中创建每一个消息队列时,内核创建、存储及维护这个结构的一个实例。 

/* 在系统中的每一个消息队列对应一个msqid_ds 结构 */ 
struct msqid_ds { 
struct ipc_perm msg_perm; 
struct msg *msg_first; /* 队列上第一条消息,即链表头*/ 
struct msg *msg_last; /* 队列中的最后一条消息,即链表尾 */ 
time_t msg_stime; /* 发送给队列的最后一条消息的时间 */ 
time_t msg_rtime; /* 从消息队列接收到的最后一条消息的时间 */ 
time_t msg_ctime; /* 最后修改队列的时间*/ 
ushort msg_cbytes; /*队列上所有消息总的字节数 */ 
ushort msg_qnum; /*在当前队列上消息的个数 */ 
ushort msg_qbytes; /* 队列最大的字节数 */ 
ushort msg_lspid; /* 发送最后一条消息的进程的pid */ 
ushort msg_lrpid; /* 接收最后一条消息的进程的pid */ 
};


2. 系统调用: msgget() 

为了创建一个新的消息队列,或存取一个已经存在的队列,要使用msgget()系统调用。 

原型: int msgget ( key_t key, int msgflg ); 
返回: 成功,则返回消息队列识别号,失败,则返回-1, 

semget()中的第一个参数是键,这个键值要与现有的键值进行比较,现有的键值指在内核中已存在的其它消 
息队列的键值。对消息队列的打开或存取操作依赖于msgflg参数的取值: 

IPC_CREAT :如果这个队列在内核中不存在,则创建它。 
IPC_EXCL :当与IPC_CREAT一起使用时,如果这个队列已存在,则创建失败。 

如果IPC_CREAT单独使用,semget()为一个新创建的消息队列返回标识号,或者返回具有相同键值的已存在队列的标识号。如果IPC_EXCL与IPC_CREAT一起使用,要么创建一个新的队列,要么对已存在的队列返回-1。 
IPC_EXCL单独是没有用的,当与IPC_CREAT结合起来使用时,可以保证新创建队列的打开和存取。 

与文件系统的存取权限一样,每一个IPC对象也具有存取权限,因此,可以把一个八进制与掩码或,形成对消息队列的存取权限。 

让我们来创建一个打开或创建消息队列的函数: 

int open_queue( key_t keyval ) 

int qid; 
if((qid = msgget( keyval, IPC_CREAT | 0660 )) == -1) 

return(-1); 

return(qid); 
}


注意,这个例子显式地用了0660权限。这个函数要么返回一个消息队列的标识号,要么返回-1而出错。键值作为唯一的参数必须传递给它。 

3. 系统调用: msgsnd() 

一旦我们有了队列识别号,我们就可以在这个队列上执行操作。要把一条消息传递给一个队列,你必须用msgsnd()系统调用。 

原型:int msgsnd ( int msqid, struct msgbuf *msgp, int msgsz,int msgflg ); 

返回:成功为0,失败为-1。 

msgsnd()的第一个参数是队列识别号,由msgget()调用返回。第二个参数msgp是一个指针,指向我们重新声和装载的消息缓冲区。msgsz参数包含了消息以字节为单位的长度,其中包括了消息类型的4个字节。 

msgflg参数可以设置成0(忽略),或者: 

IPC_NOWAIT :如果消息队列满,消息不写到队列中,并且控制权返回给调用进程(继续执行)。如果不指定IPC_NOWAIT,调用进程将挂起(阻塞)直到消息被写到队列中。 

让我们来看一个发送消息的简单函数: 

int send_message( int qid, struct mymsgbuf *qbuf ) 

int result, length; 
/* mymsgbuf结构的实际长度 */ 
length = sizeof(struct ) - sizeof(long); 
if((result = msgsnd( qid, qbuf, length, 0)) == -1) 

return(-1); 

return(result); 
}


这个小函数试图把缓冲区qbuf中的消息,发送给队列识别号为qid的消息队列。 

现在,我们在消息队列里有了一条消息,可以用ipcs命令来看你队列的状态。如何从消息队列检索消息,可以用msgrcv()系统调用。 

4.系统调用:msgrcv() 

原型:int msgrcv ( int msqid, struct msgbuf *msgp, int msgsz,long mtype, int msgflg ); 

返回值:成功,则为拷贝到消息缓冲区的字节数,失败为-1。 

很明显,第一个参数用来指定要检索的队列(必须由msgget()调用返回),第二个参数(msgp)是存放检索到消息的缓冲区的地址,第三个参数(msgsz)是消息缓冲区的大小,包括消息类型的长度(4字节)。 

第四个参数(mtype)指定了消息的类型。内核将搜索队列中相匹配类型的最早的消息,并且返回这个消息的一个拷贝,返回的消息放在由msgp参数指向的地址。这里存在一个特殊的情况,如果传递给mytype参数的值为0,就可以不管类型,只返回队列中最早的消息。 

如果传递给参数msgflg的值为IPC_NOWAIT,并且没有可取的消息,那么给调用进程返回ENOMSG错误消息,否则,调用进程阻塞,直到一条消息到达队列并且满足msgrcv()的参数。如果一个客户正在等待消息,而队列被删除,则返回EIDRM。如果当进程正在阻塞,并且等待一条消息到达但捕获到了一个信号,则返回EINTR。 

让我们来看一个从我们已建的消息队列中检索消息的例子 

int read_message( int qid, long type, struct mymsgbuf *qbuf ) 

int result, length; 
* 计算mymsgbuf结构的实际大小*/ 
length = sizeof(struct mymsgbuf) - sizeof(long); 
if((result = msgrcv( qid, qbuf, length, type, 0)) == -1) 

return(-1); 

return(result); 
}


当从队列中成功地检索到消息后,这个消息将从队列删除。 


下面给出两个实例操作消息队列

1.根据用户输入关键字,以权限-rw-rw---创建或打开一个消息队列

#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int main() { key_t key; int msqid; printf("Enter the desired key in hex ="); scanf("%x",&key); printf("\nkey=0x%x", key); if((msqid=msgget(key,IPC_CREAT|0660))==-1) { printf("The msgget failed.\n"); exit(1); } printf("The msgget succeeded:msqid=%d \n",msqid); exit(0); } 

2.这个程序包括两个文件,一个用于接收消息,一个用于发送消息

2.1接收消息

#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> struct my_msg { long int my_msg_type; char text[BUFSIZ]; } msgbuf; int main() { int running =1; int msgid; long int msg_to_receive=0; msgid=msgget((key_t)1234,0666 |IPC_CREAT); if(msgid==-1) { printf("msgget failed!\n"); exit(1); } while(running) { if(msgrcv(msgid,(void *)&msgbuf, BUFSIZ,msg_to_receive, 0)==-1) { printf("msgrcv failed!\n"); exit(1); } printf("You wrote : %s", msgbuf.text); if(strncmp(msgbuf.text,"end",3)==0) running=0; } if(msgctl(msgid, IPC_RMID, 0)==-1) { printf("msgct(IPC_RMID) failed!\n"); exit(1); } return 0; } 

2.2发送消息

#include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> struct my_msg { long int my_msg_type; char text[BUFSIZ]; } msgbuf; int main() { int running =1; int msgid; msgid=msgget((key_t)1234,0666 |IPC_CREAT); if(msgid==-1) { printf("msgget failed!\n"); exit(1); } while(running) { printf("Enter some text: "); fgets(msgbuf.text,BUFSIZ,stdin); msgbuf.my_msg_type=1; if(msgsnd(msgid,(void *)&msgbuf, BUFSIZ, 0)==-1) { printf("msgsnd failed!\n"); exit(1); } if(strncmp(msgbuf.text,"end",3)==0) running=0; } return 0; } 

系统函数 msgctl

我们可以使用系统调用msgctl ( )来控制对消息队列的操作。 
系统调用: msgctl( ) ;
调用原型: int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
头文件: linux/msg.h
返回值: 0 ,如果成功。
- 1,如果失败:errno = EACCES (没有读的权限同时cmd 是IPC_STAT )
EFAULT (buf 指向的地址无效)
EIDRM (在读取中队列被删除)
EINVAL (msgqid无效, 或者msgsz 小于0 )
EPERM (IPC_SET或者IPC_RMID 命令被使用,但调用程序没有写的权限)
下面我们看一下可以使用的几个命令:
IPC_STAT
读取消息队列数据结构msqid_ds,并将其存储在b u f指定的地址中。
IPC_SET
设置消息队列数据结构msqid_ds中的ipc_perm元素的值。这个值取自buf参数。
IPC_RMID
从系统内核中移走消息队列
我们在前面讨论过了消息队列数据结构(msqid_ds)。系统内核中为系统中的每一个消息队列保存一个此数据结构的实例。通过使用IPC_STAT命令,我们可以得到一个此数据结构的副本。下面的程序就是实现此函数的过程: int get_queue_ds( int qid, struct msgqid_ds *qbuf )
{
if( msgctl( qid, IPC_STAT, qbuf) == -1)
{
return(-1);
}
return(0);
} 如果不能复制内部缓冲区,调用进程将返回-1。如果调用成功,则返回0。缓冲区中应该包括消息队列中的数据结构
消息队列中的数据结构中唯一可以改动的元素就是ipc_perm。它包括队列的存取权限和关于队列创建者和拥有者的信息。你可以改变用户的id、用户的组id以及消息队列的存取权限。