进程间通信 —— 消息队列

Sometimes, you don’t want to think too much. Follow your heart and go wherever you go.
有时候,你不要想太多,跟着自己的心走,走到哪算哪。

在这里插入图片描述

消息队列是消息的链表,存放在内核中并由消息队列标识符标识。(消息队列的标识符的基本类型是key_t,在头文件中定义为长整型(long int),与文件描述符不同的是,消息队列的描述符是用户指定一个ID值然后通过调用ftok()函数合成的
消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。
消息队列是 UNIX 下不同进程之间可实现共享资源的一种机制,UNIX允许不同进程将格式化的数据流以消息队列形式发送给任意进程。
对消息队列具有操作权限的进程都可以使用 msgget 完成对消息队列的操作控制,通过使用消息类型,进程可以按任何顺序读信息,或为消息安排优先级顺序。

  • 头文件

    • #include

      该头文件是 Unix/Linux 系统的基本系统数据类型的头文件,含有 size_t,time_t,pid_t,key_t,mode_t 等类型,在这里主要用到其中的 key_t(消息队列的标识符基本类型)

    • #include

      该头文件主要保存的是 进程间通信的访问IPC结构数据
      ,使用一个通用的结构类型 ipc_perm来传递用于确定执行IPC操作的权限的信息。主要记住下面几个模式位(Mode bits)的含义即可

      • IPC_CREAT:如果键不存在,则创建它

      • IPC_EXCL:如果键存在,则报错

      • IPC_NOEAIT:如果请求必须等待,则报错

      • IPC_RMID:删除标识符

      • IPC_SET:设置选项

      • IPC_STAT:获取设置选项

    • #include

      该头文件是消息队列实现要用到的具体函数所在头文件 msgget、msgsnd、msgrcv、msgctl

  • 源码函数剖析

    消息队列主要用到了四个函数 msgget、msgsnd、msgrcv、msgctl

    //系统IPC键值的格式转换函数 
    key_t ftok( const char * fname, int id );
    
    // 创建或打开消息队列:成功返回队列ID,失败返回-1
    int msgget(key_t key, int flag);
    
    // 添加消息:成功返回0,失败返回-1
    int msgsnd(int msqid, const void *ptr, size_t size, int flag);
    
    // 读取消息:成功返回消息数据的长度,失败返回-1
    int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
    
    // 控制消息队列:成功返回0,失败返回-1
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
    • msgget() 函数

      • ket_t key:程序必须提供一个键来命名某个特定的消息队列,通常要求此值来源于 ftok() 返回的IPC键值。(由此可见,只要保证key值是整个系统中唯一确定即可,所以key 值可以由用户自己指定,并赋予 key_t类型即可,并不一定要通过 ftok() 函数转换)

      • flag:权限标志,表示消息队列的访问权限,它与文件的访问权限一样。flg 可以与IPC_CREAT 做或操作,如 0666 | IPC_CREAT
        表示当 key 所命名的消息队列不存在时创建一个消息队列,队列的权限为可读可写,如果key 所命名的消息队列存在时,IPC_CREAT 标志会被忽略,而只返回一个标识符。

    • msgsnd() 函数

      • msqid:是由msgget函数返回的消息队列标识符

      • ptr:是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针 ptr 所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样:

          struct my_message 
          {
          	long int message_type;
          	/* The data you wish to transfer */
          };
        
      • size:是 ptr 中消息的长度,不是整个结构体的长度,即不包含 message_type

      • flag:用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情

        • 0:当消息队列满时,msgsnd() 将会阻塞,知道消息能写进消息队列或者消息队列被删除

        • IP_NOWAIT:当消息队列满时,msgsnd() 将不会等待,立即报错返回错误码 EAGAIN

    • msgrcv() 函数

      • 前三个参数与 msgsnd() 中一致

      • type:用于标识当前 msgrcv() 中消息接收的优先级规则,通常与对应的 msgsnd() 中 ptr 结构体中的 message_type 值对应

        • 0:直接获取消息队列中的第一个消息

        • 大于0:获取与该值相同消息类型的第一个消息

        • 小于0:获取类型等于或小于 type 绝对值的第一个消息

      • flag:与 msgsnd() 中的 flag 一致,但可选参数多一个 IPC_EXCEPT

        • IPC_EXCEPT:与 type 配合使用,返回队列中第一个类型不为 type 的消息数据
    • msgctl() 函数

      • msqid:与前两个函数一致

      • cmd:将要采取的动作,它可以取3个值,常用删除动作

        • IPC_STAT:把 msqid_ds 结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖 msqid_ds 的值

        • IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为 msqid_ds 结构中给出的值

        • IPC_RMID:删除消息队列

      • buf:指向 msqid_ds 结构的指针,它指向消息队列模式和访问权限的结构,包含一下成员

      struct msgid_ds
      {
         uid_t shm_perm.uid;
         uid_t shm_perm.gid;
         mode_t shm_perm.mode;
      };
      
  • 流程图

进程间通信 —— 消息队列_第1张图片

图1 线程间通信 —— 消息队列流程图

进程间通信 —— 消息队列_第2张图片

图2 进程间通信 —— 消息队列流程图
  • 实例代码

    下面以两个线程间(按键 && LCD)的通信为例,实现消息队列的创建、发送、接收接口函数

      #include 
      #include 
      #include 
      #include 
      #include 
      #include 
    
      #define LCDMSG_TYPE         1
      #define LCD_KEY             1234
      static int Lcd_mq = -1;
    
      typedef _KEY_EVENT
      {
      	KEY_EVENT_UP,
      	KEY_EVENT_DOWN,
      	KEY_EVENT_LEFT,
      	KEY_EVENT_RIGHT,
      	KEY_EVENT_MAIN,
      	KEY_EVENT_EXIT,
      }KEY_EVENT;
    
      struct ev_pakcet
      {
      	long int msg_type;
      	KEY_EVENT ev;
      };
    
      int Lcd_Event_MessageQueue_Init(void)
      {
      	Lcd_mq = msgget((key_t)LCD_KEY, 0666 | IPC_CREAT);
      	if (Lcd_mq == -1)
      	{
      		printf("msgget error\n");
      		return -1;
      	}			
      	return 0;
      }
    
      int Key_Send_Event(KEY_EVENT ev)
      {
      	ev_pakcet ev_pak;
      	ev_pak.msg_type = LCDMSG_TYPE;
      	ev_pak.ev = ev;
    
      	if (Lcd_mq <= 0)
      	{
          	printf("Message queue is not exist\n");
          	return -1;
     		}
      	if (-1 == msgsnd(Lcd_mq, (void*)&ev_pak, sizeof(KEY_EVENT), 0))
      	{
      		printf("msgsnd error\n");
      		return -1;
      	}
      	return 0;
      }
    
      int Lcd_Receive_Event(KEY_EVENT *ev)
      {
      	ev_pakcet ev_pak;
      	int ret =  msgrcv(lcd_mq, (void*)&ev_pak, sizeof(KEY_EVENT), LCDMSG_TYPE, 0);
      	if (ret <= 0)
      	{
          	printf("msgrcv error\n");
      	}
      	*ev = ev_pak.ev;
      	return ret;	
      }
    
      int Lcd_Remove_MessageQueue(int msqid)
      {
      	if (-1 == msgctl(msqid, IPC_RMID, 0))
      	{
         		printf("msgctl(IPC_RMID) failed\n");
          	return -1;
      	}
      	return 0;
      }
    

    在按键线程中,调用在LCD线程中定义实现的 Key_Send_Event()进行发送按键事件,在LCD线程中调用 Lcd_MessageQueue_Init()进行消息队列初始化,调用 Lcd_Receive_Event()进行接收按键线程发送的按键事件。这里因为只要设备还在线,那么按键、LCD两个线程就一致在线,所以并不需要对消息队列执行 msgctl()

    那如果按键和LCD是作为两个独立的进程怎么办?

    同一个进程下的不同线程可以共享该进程的堆、全局变量、静态变量、文件资源。即进程代码段、进程的公有数据、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID

    消息队列在线程间通信和进程间通信的区别在于:

    • 线程间通信可以通过进程资源共享的方式,将线程A中创建的消息队列及其接口提供给线程B进行使用;进程间通信则必须在每个进程中都必须执行一遍 msgget(),以保证这个消息队列一定存在

      注意:两个进程都调用一次 msgget(),并不是说创建了两个消息队列,当进程A创建了消息队列C,那么当进程B调用 msgget() 创建消息队列C时,因为C已经存在,则忽略创建动作,直接转为打开这个消息队列C)

  • 总结

    原理上可以看出,消息队列和命名管道有很大的相似之处。消息队列与管道进行通信的进程都可以是不相关的进程,都是通过发送和接收的方式来传递数据的。前者发送数据用 msgsnd(),接收数据用 msgrcv(),后者发送数据用 write(),接收数据用read(),且对每个数据都有一个最大长度的限制。

    与命名管道相比,消息队列的优势在于:

    • 存在形式:可以独立于进程存在,消除了同步管道的打开和关闭时可能产生的困难;

    • 发送:避免了管道的同步和阻塞问题,不需要由进程自己来提供同步方法;

    • 接收:可以通过消息类型有选择地接收数据,而管道只能默认地接收。

进程间通信 —— 消息队列_第3张图片

你可能感兴趣的:(Linux,网络编程,嵌入式,c++,数据结构,linux,进程间通信,消息队列)