Linux 환경 프로 세 스 간 통신 (3): 메시지 큐

본 시리즈 의 앞의 두 부분 에서 우 리 는 파이프 와 신호 두 가지 통신 체 제 를 연구 하고 본 고 는 세 번 째 부분 에 깊이 들 어가 시스템 V 메시지 큐 와 해당 하 는 API 를 소개 할 것 이다.
메시지 큐 (메시지 큐 라 고도 함) 는 초기 유 닉 스 통신 체제 의 일부 단점 을 극복 할 수 있다.초기 에 유 닉 스 통신 체제 중 하나 로 서 신 호 를 전송 할 수 있 는 정 보 량 이 제한 되 었 다. 그 후에 POSIX 1003.1b 는 신호 의 실시 성 측면 에서 확대 되 었 고 신 호 는 정 보 량 을 전달 하 는 데 어느 정도 개선 되 었 으 나 신호 와 같은 통신 방식 은 '실시 간' 통신 방식 과 같 아서 신 호 를 받 는 과정 이 특정한 시간 범위 에서 신호 에 반응 하도록 요구 했다.따라서 이 신 호 는 신호 프로 세 스 를 받 아들 이 는 생명주기 내 에 만 의미 가 있다. 신호 가 전달 하 는 정 보 는 프로 세 스에 따라 지속 되 는 개념 (process - persistent) 에 가깝다. 부록 1 참조.파이프 와 유명 파이프 와 유명 파 이 프 는 프로 세 스에 따라 IPC 를 지속 하 는 전형 적 인 것 이 고 형식 이 없 는 바이트 만 전송 할 수 있 기 때문에 응용 프로그램 개발 에 불편 을 줄 수 있다. 또한 버퍼 크기 도 제한 을 받는다.
메시지 큐 는 메시지 의 링크 이다.정 보 를 하나의 기록 으로 볼 수 있 고 특정한 형식 과 특정한 우선 순 위 를 가진다.메시지 큐 에 쓰기 권한 이 있 는 프로 세 스 는 일정한 규칙 에 따라 새 메 시 지 를 추가 할 수 있 습 니 다.메시지 큐 에 읽 기 권한 이 있 는 프로 세 스 는 메시지 큐 에서 메 시 지 를 읽 을 수 있 습 니 다.메시지 큐 는 커 널 에 따라 계 속 됩 니 다 (부록 1 참조).
현재 주로 두 가지 유형의 메시지 큐 가 있 습 니 다. POSIX 메시지 큐 와 시스템 V 메시지 큐, 시스템 V 메시지 큐 는 현재 대량으로 사용 되 고 있 습 니 다.프로그램의 이식 성 을 고려 하여 새로 개 발 된 응용 프로그램 은 가능 한 한 POSIX 메시지 큐 를 사용 해 야 한다.
이 시리즈 의 주제 순서 (Linux 프로 세 스 간 통신 (IPC) 를 깊이 이해 함) 에서 메시지 대기 열, 신호등, 공유 메모리 구역 에 있어 두 가지 실현 버 전이 있다 고 언급 했다. POSIX 와 시스템 V 의 것 이다.리 눅 스 커 널 (커 널 2.4.18) 은 POSIX 신호등, POSIX 공유 메모리 및 POSIX 메시지 큐 를 지원 하지만 주류 리 눅 스 발행 버 전 중 하나 인 redhad 8.0 (커 널 2.4.18) 에 대해 서 는 POSIX 프로 세 스 간 통신 API 에 대한 지원 이 제공 되 지 않 았 지만 시간 적 인 일 일 것 이다.
따라서 본 고 는 시스템 V 메시지 큐 와 해당 API 를 소개 한다.성명 없 이 다음 토론 에 서 는 시스템 V 메시지 큐 를 말한다.
1. 메시지 큐 기본 개념
  • 시스템 V 메시지 큐 는 커 널 에 따라 지속 되 며, 커 널 이 다시 시작 되 거나 삭 제 된 메시지 큐 를 표시 할 때 만 이 메시지 큐 가 진정 으로 삭 제 됩 니 다.따라서 시스템 에 메시지 대기 열 을 기록 하 는 데이터 구조 (struct ipc ids msg ids) 는 커 널 에 있 고 시스템 의 모든 메시지 대기 열 은 구조 msg 에 있 습 니 다.ids 에서 접근 입 구 를 찾 았 습 니 다.
  • 메시지 대기 열 은 바로 메시지 의 링크 이다.모든 메시지 큐 에 하나의 큐 헤드 가 있 습 니 다. 구조 struct msgqueue 에서 설명 합 니 다 (부록 2 참조).대기 열 헤더 에는 메시지 대기 열 키, 사용자 ID, 그룹 ID, 메시지 대기 열 메시지 수 등 이 포함 되 어 있 으 며, 최근 메시지 대기 열 읽 기 쓰기 프로 세 스 의 ID 까지 기록 되 어 있 습 니 다.독 자 는 이런 정 보 를 방문 할 수도 있 고 그 중의 일부 정 보 를 설정 할 수도 있다.
  • 다음 그림 은 커 널 과 메시지 큐 가 어떻게 연결 되 는 지 설명 한다. 그 중에서 struct ipcids msg_ids 는 커 널 에 메시지 큐 를 기록 하 는 전역 데이터 구조 입 니 다.struct msg_queue 는 모든 메시지 큐 의 큐 헤드 입 니 다.

  • 위의 그림 에서 보 듯 이 전체 데이터 구조 struct ipcids msg_ids 는 모든 메시지 큐 의 첫 번 째 멤버 에 게 접근 할 수 있 습 니 다: struct kernipc_perm;그리고 모든 struct kernipc_perm 이 구체 적 인 메시지 큐 와 대응 할 수 있 는 것 은 이 구조 에 key 가 있 기 때 문 입 니 다.t 형식 구성원 key, key 는 메시지 대기 열 을 유일 하 게 확인 합 니 다.kern_ipc_perm 구 조 는 다음 과 같다.
    struct kern_ipc_perm{   //                msg_ids        ;
    key_t key; //
    uid_t uid;
    gid_t gid;
    uid_t cuid;
    gid_t cgid;
    mode_t mode;
    unsigned long seq;
    }

     

    二、操作消息队列

    对消息队列的操作无非有下面三种类型:

    1、 打开或创建消息队列
    消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需提供该消息队列的键值即可;

    注:消息队列描述字是由在系统范围内唯一的键值生成的,而键值可以看作对应系统内的一条路经。

    2、 读写操作

    消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构:

    struct msgbuf{
    long mtype;
    char mtext[1];
    };

     

    mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容,当然长度不一定为1。因此,对于发送消息 来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息 读入该缓冲区即可。

    3、 获得或设置消息队列属性:

    消息队列的信息基本上都保存在消息队列头中,因此,可以分配一个类似于消息队列头的结构(struct msqid_ds,见附录 2 ),来返回消息队列的属性;同样可以设置该数据结构。

     

     

    消息队列API

    1、文件名到键值

    #include <sys/types.h>
    #include <sys/ipc.h>
    key_t ftok (char*pathname, char proj);

     

    它返回与路径pathname相对应的一个键值。该函数不直接对消息队列操作,但在调用ipc(MSGGET,…)或msgget()来获得消息队列描述字前,往往要调用该函数。典型的调用代码是:

       key=ftok(path_ptr, 'a');
    ipc_id=ipc(MSGGET, (int)key, flags,0,NULL,0);

     

    2、linux为操作系统V进程间通信的三种方式(消息队列、信号灯、共享内存区)提供了一个统一的用户界面:
    int ipc (unsigned int call , int first , int second , int third , void *ptr , long fifth );

    第一个参数指明对IPC对象的操作方式,对消息队列而言共有四种操作:MSGSND、MSGRCV、MSGGET以及MSGCTL,分别代表向消息 队列发送消息、从消息队列读取消息、打开或创建消息队列、控制消息队列;first参数代表唯一的IPC对象;下面将介绍四种操作。

    • int ipc (MSGGET, int first, int second, int third, void *ptr, long fifth);
      与该操作对应的系统V调用为:int msgget( (key_t)first,second)。
    • int ipc (MSGCTL, int first, int second, int third, void *ptr, long fifth)
      与该操作对应的系统V调用为:int msgctl( first,second, (struct msqid_ds*) ptr)。
    • int ipc (MSGSND, int first, int second, int third, void *ptr, long fifth);
      与该操作对应的系统V调用为:int msgsnd( first, (struct msgbuf*)ptr, second, third)。
    • int ipc (MSGRCV, int first, int second, int third, void *ptr, long fifth);
      与该操作对应的系统V调用为:int msgrcv( first,(struct msgbuf*)ptr, second, fifth,third),

     

    注:本人不主张采用系统调用ipc(),而更倾向于采用系统V或者POSIX进程间通信API。原因如下:

    • 虽然该系统调用提供了统一的用户界面,但正是由于这个特性,它的参数几乎不能给出特定的实际意义(如以first、second来命名参数),在一定程度上造成开发不便。
    • 正如ipc手册所说的:ipc()是linux所特有的,编写程序时应注意程序的移植性问题;
    • 该系统调用的实现不过是把系统V IPC函数进行了封装,没有任何效率上的优势;
    • 系统V在IPC方面的API数量不多,形式也较简洁。

     

    3.系统V消息队列API
    系统V消息队列API共有四个,使用时需要包括几个头文件:

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

     

    1)int msgget(key_t key, int msgflg)

    参数key是一个键值,由ftok获得;msgflg参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。

    在以下两种情况下,该调用将创建一个新的消息队列:

    • 如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;
    • key参数为IPC_PRIVATE;

     

    参数msgflg可以为以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或结果。

    调用返回: 成功返回消息队列描述字,否则返回-1。

    注:参数key设置成常数IPC_PRIVATE并不意味着其他进程不能访问该消息队列,只意味着即将创建新的消息队列。

    2)int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);
    该系统调用从msgid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。

    msqid为消息队列描述字;消息返回后存储在msgp指向的地址,msgsz指定msgbuf的mtext成员的长度(即消息内容的长度),msgtyp为请求读取的消息类型;读消息标志msgflg可以为以下几个常值的或:

    • IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG
    • IPC_EXCEPT 与msgtyp>0配合使用,返回队列中第一个类型不为msgtyp的消息
    • IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失。

     

    msgrcv手册中详细给出了消息类型取不同值时(>0; <0; =0),调用将返回消息队列中的哪个消息。

    msgrcv()解除阻塞的条件有三个:

    1. 消息队列中有了满足条件的消息;
    2. msqid代表的消息队列被删除;
    3. 调用msgrcv()的进程被信号中断;

     

    调用返回: 成功返回读出消息的实际字节数,否则返回-1。

    3)int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);
    向msgid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中,消息的大小由msgze指定。

    对发送消息来说,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。造成msgsnd()等待的条件有两种:

    • 当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;
    • 当前消息队列的消息数(单位"个")不小于消息队列的总容量(单位"字节数"),此时,虽然消息队列中的消息数目很多,但基本上都只有一个字节。


    msgsnd()解除阻塞的条件有三个:

    1. 不满足上述两个条件,即消息队列中有容纳该消息的空间;
    2. msqid代表的消息队列被删除;
    3. 调用msgsnd()的进程被信号中断;

     

    调用返回: 成功返回0,否则返回-1。

    4)int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    该系统调用对由msqid标识的消息队列执行cmd操作,共有三种cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。

    1. IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;
    2. IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。
    3. IPC_RMID:删除msqid标识的消息队列;

     

    调用返回: 成功返回0,否则返回-1。

    三、消息队列的限制
    每个消息队列的容量(所能容纳的字节数)都有限制,该值因系统不同而不同。在后面的应用实例中,输出了redhat 8.0的限制,结果参见附录 3 。

    另一个限制是每个消息队列所能容纳的最大消息数:在redhad 8.0中,该限制是受消息队列容量制约的:消息个数要小于消息队列的容量(字节数)。

    注:上述两个限制是针对每个消息队列而言的,系统对消息队列的限制还有系统范围内的最大消息队列个数,以及整个系统范围内的最大消息数。一般来说,实际开发过程中不会超过这个限制。

    四、消息队列应用实例
    消息队列应用相对较简单,下面实例基本上覆盖了对消息队列的所有操作,同时,程序输出结果有助于加深对前面所讲的某些规则及消息队列限制的理解。

    #include <sys/types.h>
    #include <sys/msg.h>
    #include <unistd.h>
    void msg_stat(int,struct msqid_ds );
    main()
    {
    int gflags,sflags,rflags;
    key_t key;
    int msgid;
    int reval;
    struct msgsbuf{
    int mtype;
    char mtext[1];
    }msg_sbuf;
    struct msgmbuf
    {
    int mtype;
    char mtext[10];
    }msg_rbuf;
    struct msqid_ds msg_ginfo,msg_sinfo;
    char* msgpath="/unix/msgqueue";
    key=ftok(msgpath,'a');
    gflags=IPC_CREAT|IPC_EXCL;
    msgid=msgget(key,gflags|00666);
    if(msgid==-1)
    {
    printf("msg create error/n");
    return;
    }
    // ,
    msg_stat(msgid,msg_ginfo);
    sflags=IPC_NOWAIT;
    msg_sbuf.mtype=10;
    msg_sbuf.mtext[0]='a';
    reval=msgsnd(msgid,&msg_sbuf,sizeof(msg_sbuf.mtext),sflags);
    if(reval==-1)
    {
    printf("message send error/n");
    }
    // ,
    msg_stat(msgid,msg_ginfo);
    rflags=IPC_NOWAIT|MSG_NOERROR;
    reval=msgrcv(msgid,&msg_rbuf,4,10,rflags);
    if(reval==-1)
    printf("read msg error/n");
    else
    printf("read from msg queue %d bytes/n",reval);
    // ,
    msg_stat(msgid,msg_ginfo);
    msg_sinfo.msg_perm.uid=8;//just a try
    msg_sinfo.msg_perm.gid=8;//
    msg_sinfo.msg_qbytes=16388;
    // msg_qbytes
    //
    reval=msgctl(msgid,IPC_SET,&msg_sinfo);
    if(reval==-1)
    {
    printf("msg set info error/n");
    return;
    }
    msg_stat(msgid,msg_ginfo);
    //
    reval=msgctl(msgid,IPC_RMID,NULL);//
    if(reval==-1)
    {
    printf("unlink msg queue error/n");
    return;
    }
    }
    void msg_stat(int msgid,struct msqid_ds msg_info)
    {
    int reval;
    sleep(1);//
    reval=msgctl(msgid,IPC_STAT,&msg_info);
    if(reval==-1)
    {
    printf("get msg info error/n");
    return;
    }
    printf("/n");
    printf("current number of bytes on queue is %d/n",msg_info.msg_cbytes);
    printf("number of messages in queue is %d/n",msg_info.msg_qnum);
    printf("max number of bytes on queue is %d/n",msg_info.msg_qbytes);
    // ( ) MSGMNB, 。 ,//msg_qbytes MSGMNB
    printf("pid of last msgsnd is %d/n",msg_info.msg_lspid);
    printf("pid of last msgrcv is %d/n",msg_info.msg_lrpid);
    printf("last msgsnd time is %s", ctime(&(msg_info.msg_stime)));
    printf("last msgrcv time is %s", ctime(&(msg_info.msg_rtime)));
    printf("last change time is %s", ctime(&(msg_info.msg_ctime)));
    printf("msg uid is %d/n",msg_info.msg_perm.uid);
    printf("msg gid is %d/n",msg_info.msg_perm.gid);
    }

    程序输出结果见附录 3 。

    小结:
    消息队列与管道以及有名管道相比,具有更大的灵活性,首先,它提供有格式字节流,有利于减少开发人员的工作量;其次,消息具有类型,在实际应用中,可作为 优先级使用。这两点是管道以及有名管道所不能比的。同样,消息队列可以在几个进程间复用,而不管这几个进程是否具有亲缘关系,这一点与有名管道很相似;但 消息队列是随内核持续的,与有名管道(随进程持续)相比,生命力更强,应用空间更大。

    附录 1 在参考文献[1]中,给出了IPC随进程持续、随内核持续以及随文件系统持续的定义:

    1. 随进程持续:IPC一直存在到打开IPC对象的最后一个进程关闭该对象为止。如管道和有名管道;
    2. 随内核持续:IPC一直持续到内核重新自举或者显示删除该对象为止。如消息队列、信号灯以及共享内存等;
    3. 随文件系统持续:IPC一直持续到显示删除该对象为止。

     

    附录 2
    结构msg_queue用来描述消息队列头,存在于系统空间:

    struct msg_queue {
    struct kern_ipc_perm q_perm;
    time_t q_stime; /* last msgsnd time */
    time_t q_rtime; /* last msgrcv time */
    time_t q_ctime; /* last change time */
    unsigned long q_cbytes; /* current number of bytes on queue */
    unsigned long q_qnum; /* number of messages in queue */
    unsigned long q_qbytes; /* max number of bytes on queue */
    pid_t q_lspid; /* pid of last msgsnd */
    pid_t q_lrpid; /* last receive pid */
    struct list_head q_messages;
    struct list_head q_receivers;
    struct list_head q_senders;
    };

     

    结构msqid_ds用来设置或返回消息队列的信息,存在于用户空间;

    struct msqid_ds {
    struct ipc_perm msg_perm;
    struct msg *msg_first; /* first message on queue,unused */
    struct msg *msg_last; /* last message in queue,unused */
    __kernel_time_t msg_stime; /* last msgsnd time */
    __kernel_time_t msg_rtime; /* last msgrcv time */
    __kernel_time_t msg_ctime; /* last change time */
    unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
    unsigned long msg_lqbytes; /* ditto */
    unsigned short msg_cbytes; /* current number of bytes on queue */
    unsigned short msg_qnum; /* number of messages in queue */
    unsigned short msg_qbytes; /* max number of bytes on queue */
    __kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
    __kernel_ipc_pid_t msg_lrpid; /* last receive pid */
    };

    //可以看出上述两个结构很相似。

    附录 3 消息队列实例输出结果:

    current number of bytes on queue is 0
    number of messages in queue is 0
    max number of bytes on queue is 16384
    pid of last msgsnd is 0
    pid of last msgrcv is 0
    last msgsnd time is Thu Jan 1 08:00:00 1970
    last msgrcv time is Thu Jan 1 08:00:00 1970
    last change time is Sun Dec 29 18:28:20 2002
    msg uid is 0
    msg gid is 0
    //
    current number of bytes on queue is 1
    number of messages in queue is 1
    max number of bytes on queue is 16384
    pid of last msgsnd is 2510
    pid of last msgrcv is 0
    last msgsnd time is Sun Dec 29 18:28:21 2002
    last msgrcv time is Thu Jan 1 08:00:00 1970
    last change time is Sun Dec 29 18:28:20 2002
    msg uid is 0
    msg gid is 0
    read from msg queue 1 bytes
    //
    current number of bytes on queue is 0
    number of messages in queue is 0
    max number of bytes on queue is 16384 // ( )
    pid of last msgsnd is 2510
    pid of last msgrcv is 2510
    last msgsnd time is Sun Dec 29 18:28:21 2002
    last msgrcv time is Sun Dec 29 18:28:22 2002
    last change time is Sun Dec 29 18:28:20 2002
    msg uid is 0
    msg gid is 0
    current number of bytes on queue is 0
    number of messages in queue is 0
    max number of bytes on queue is 16388 //
    pid of last msgsnd is 2510
    pid of last msgrcv is 2510 //
    last msgsnd time is Sun Dec 29 18:28:21 2002
    last msgrcv time is Sun Dec 29 18:28:22 2002
    last change time is Sun Dec 29 18:28:23 2002 //msgctl() msg_ctime
    msg uid is 8
    msg gid is 8

    参考文献:

    • UNIX网络编程第二卷:进程间通信,作者:W.Richard Stevens,译者:杨继张,清华大学出版社。对POSIX以及系统V消息队列都有阐述,对Linux环境下的程序开发有极大的启发意义。
    • linux内核源代码情景分析(上),毛德操、胡希明著,浙江大学出版社,给出了系统V消息队列相关的源代码分析。
    • http://www.fanqiang.com/a4/b2/20010508/113315.html ,主要阐述linux下对文件的操作,详细介绍了对文件的存取权限位,对IPC对象的存取权限同样具有很好的借鉴意义。
    • msgget、msgsnd、msgrcv、msgctl手册

    :http://hi.baidu.com/promisejohn/blog/item/7e0e6259a561e42a2834f015.html

    좋은 웹페이지 즐겨찾기