Linux进程间通讯(上)之管道、消息队列实践

一、进程间通讯简述

进程间通讯的几种方式:无名管道、有名管道、消息队列、共享内存、信号、信号量、套接字(socket)。
linux

进程间通讯是不一样进程直接进行的一些接触,这种接触有简单,有复杂。机制不一样,复杂度也不一样。通讯是一个广义上的意 义,不只指大批量数据传送,还包括控制信息的传送,可是使用的方法都是大同小异的。ios

如图所示进程不是孤立的,不一样的进程须要进行信息的交互和状态的传递等,所以须要进程间通讯。web

二、管道

管道分为无名管道和有名管道两种方式。管道是一种半双工的通讯方式,数据只能单向流动,可是无名管道和有名管道的区别是无名管道只能在具备亲缘关系的进程间通讯,有名管道则是在无亲缘关系进程间通讯。进程的亲缘关系一般是指父子进程关系。管道是Linux支持的最初Unix IPC形式之一,管道与管道之间通讯其实就是一个文件,但它不是一个普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统并且只存在内存中。当一个进程向管道中写的内容被管道另外一端的进程读出;写入的内容每次都会被添加到管道缓冲区的末尾,而且每次都是从缓冲区的头部读出数据。以下图所示。编程

那么,如何建立一条管道呢?下面,咱们就来了解下FIFO函数。缓存

FIFO不一样于pipe函数,由于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样,即便与FIFO的建立进程不存在亲缘关系的进程,只要能够访问该路径就可以彼此经过FIFO互相通讯,所以,经过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进后出,和栈的原则同样,对管道以及FIFO的读老是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操做。微信

须要包含的头文件以下:数据结构

#include<sys/types.h>
#include<sys/stat.h>
#incldue<fcntl.h>
#include<unistd.h>

FIFO函数建立:app

函数原型:socket

int mkfifo(const char *pathname,mode_t mode);

函数返回值 :编辑器

成功0,失败-1

参数含义:

pathname为路径名,建立管道的名字(该函数的第一个参数是一个普通的路径名,也就是建立后FIFO的名字)。mode为建立fifo的权限(第二个参数与打开普通文件的open()函数中的mode参数相同)。

注:若是mkfido的第一个参数已是一个已经存在的路径名时,就会返回EEXIST错误,因此当咱们调用的时候首先会检查是否返回该错误,若是返回该错误那咱们只须要直接调用打开FIFO的函数便可。

FIFO比pipe函数打开的时候多了一个打开操做open;若是当时打开操做时为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操做将返回成功;不然,可能阻塞到有相应进程为写而打开该FIFO;或者,成功返回。另外一种状况就是为写而打开FIFO时,若已经有相应进程为读而打开该FIFO,则当前打开操做将成功返回;不然可能会阻塞直到有相应进程为读而打开该FIFO;或者,返回ENIO错误。

下面咱们使用FIFO实现进程间的通讯。

(1)打开一个文件,管道的写入端向文件写入数据;管道的读取端从文件中读取出数据。

fifo_write.c

#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define P_FIFO  "txt"

int main()
{
    int fd;
    //要写入有名管道的数据
    char buf[20] = "hello write_fifo";
    int ret=0;
    //建立有名管道,并赋予访问有名管道的权限
    ret = mkfifo(P_FIFO,0777);
    //建立失败
    if(ret < 0)
    {
        printf("create named pipe failed.\n");
        return -1;
    }
    fd = open(P_FIFO,O_WRONLY);
    if(fd < 0)
    {
        printf("open failed.\n");
        return -2;
    }
    //写入数据到有名管道
    //第一个参数为有名管道文件描述符
    //第二个参数为写入有名管道的数据
    //第三个参数为写入有名管道的数据长度
    write(fd,buf,sizeof(buf));
    //关闭有名管道
    close(fd);
    return 0;
}

fifo_read.c

#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define P_FIFO  "txt"

int main()
{
    int ret;
    int fd;
    char buf[20];
    //打开有名管道
    //第一个参数为有名管道文件路径
    //第二个参数代表是以读取方式并以非阻塞方式打开有名管道
    //O_RDONLY读取模式
    //O_NONBLOCK非阻塞方式
    fd = open(P_FIFO,O_RDONLY);
    if(fd<0)
    {
        printf("open fail\n");
        return -1 ;
    }
    //循环读取有名管道
    while(1)
    {
        memset(buf,0,sizeof(buf));
        if(read(fd,buf,sizeof(buf)) == 0)
        {
            printf("nodata.\n");
        }
        else
        {
            printf("getdata:%s\n",buf);
            break;
        }
   }
   close(fd);
   return 0;
}

下面先将fifo_write.c和fifo_read.c分别编译成fifo_write和fifo_read两个可执行程序:

接下来,先运行fifo_write,而后打开另外一个终端,接着运行fifo_read,运行fifo_write的时候,能够看到程序阻塞在终端:

下面打开另一个终端运行fifo_read

切换到另一个终端,在终端输入ls –l能够看到因为fifo_write中建立了管道文件txt,从前面的字串prwxr-xr-x中的p能够知道,这是一个管道文件,以下图所示:

运行fifo_read,这时候,能够看到从管道中获取的字符串hello write_fifo,以下图所示:

管道读取结束后,fifo_write这个程序也就不会在阻塞在终端了,以下图所示:

写管道程序还要注意,一旦咱们建立了FIFO,就能够用open去打开它,可使用open、read、close等去操做FIFO和pipe有相同之处,当打开FIFO时,非阻塞标志(O_NONBLOCK)将会对读写产生以下影响:

  • 一、没有使用O_NONBLOCK:访问要求没法知足时进程将阻塞。如试图读取空的FIFO,将致使进程阻塞;
  • 二、使用O_NONBLOCK:访问要求没法知足时不阻塞,当即出错返回,errno是ENXIO。

三、消息队列

消息队列(也叫作报文队列)提供了一个进程向另外一个进程发送一个数据块的方法。每一个数据块都被认为含有一个类型,接收进程能够独立地接收含有不一样类型的数据结构。咱们能够经过发送消息来避免命名管道的同步和阻塞问题。可是消息队列与命名管道同样,每一个数据块都有一个最大长度的限制。

打开或者建立消息队列的内核持续性要求每一个消息队列都在系统范围内对应惟一的键值,因此,要得到一个消息队列的描述字,只须要提供该消息队列的键值便可。

消息读写操做很是简单,对于开发人员来讲,每一个消息都相似以下的数据结构:

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

3.一、msgget函数

该函数用来建立或者访问一个消息队列。

int msgget(key_t key, int msgflg);

与其余的IPC机制同样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限同样。msgflg能够与IPC_CREAT作或操做,表示当key所命名的消息队列不存在时建立一个消息队列,若是key所命名的消息队列存在时,IPC_CREAT标志会被忽略,成功则返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1。

3.二、msgsnd函数

该函数用来向消息队列发送一个消息。

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

将发送的消息存储在msgp指向的msgbuf结构中,消息大小由msgsz指定。对发送的消息来讲,有意义的msgflg标准为IPC_NOWAIT,指明在消息队列没有足够的空间容纳要发送的消息时,msgsnd是否等待。形成msgsnd()等待的条件有两种:当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;当前消息队列的消息数不小于消息队列的总容量,此时,虽然消息队列中的消息数目并很少,但基本上都只有一个字节。调用成功的时候返回0,失败返回-1.

3.三、msgrcv函数

该函数用来从一个消息队列获取消息。

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

msgrcv函数前面三个参数和msgsnd函数的三个参数同样不作讲解。msgtype能够实现一种简单的接收优先级。若是msgtype为0,就获取队列中的第一个消息。若是它的值大于零,将获取具备相同消息类型的第一个信息。若是它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。msgflg用于控制当队列中没有相应类型的消息能够接收时将发生的事情。当调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,而后删除消息队列中对应的消息;失败则返回-1.

3.四、msgctl函数

该函数用来控制消息队列。

int msgctl(int msgid, int command, struct msgid_ds *buf);

该系统调用对由msqid标识的消息队列执行cmd操做,共有三种cmd操做:

  • IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
  • IPC_SET:若是进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值。
  • IPC_RMID:删除消息队列。buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。成功返回0,不然返回-1。

经过上面的函数咱们清楚如何去建立一个消息队列那咱们简单的来看一个案例。

(1)建立一条消息队列msg_get.c

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

int main(void)
{
 int  msgid ; 
 //建立消息队列,注意,建立后面要有IPC_CREAT标志
 msgid = msgget(0x123456 , IPC_CREAT | 0777);
 if(msgid < 0)
 {
  perror("msgget fail");
  return -1 ; 
 }
 printf("success ... ! \n");
 return 0  ;
}

运行结果:

那消息队列呢?怎么查看?使用ipcs –q命令能够查看到刚刚咱们建立的消息队列0x123456。

(2)向消息队列发送消息 msgsend.c

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
    int msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid == -1)
    {
        perror("create msg queue fail");
        return -1 ;
    }
    printf("open msg success ... \n");
    int ret ;
    char *p = "hello world" ;
   //发送hello world到消息队列0x123456
   //在这里能够直接发送
    ret = msgsnd(msgid , p , strlen(p) , 0);
    if(ret == -1)
    {
        perror("send msgid fail");
        return -2 ;
    }
    return 0 ;
}

运行结果:

使用ipcs –p命令查看:

(3)获取消息队列中的信息 msgrecv.c 在上面msgsend.c的基础上,这个例程将上面发送到消息队列的信息读取回来。

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
    int msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid == -1)
    {
        perror("create msg queue fail");
        return -1 ;
    }
    printf("open msg success ... \n");
    int ret ;
    char buffer[1024] = {0};
    //接收消息队列中的信息
    ret = msgrcv(msgid , buffer , 11 , 0 , 0);
    if(ret == -1)
    {
        perror("recv msgid fail");
        return -2 ;
    }
    printf("ret: %d  buffer:%s \n" , ret , buffer);
    return 0 ;
}

运行结果,如图所示:

那么,如何删除一个消息队列呢?先用ipcs –q查看消息队列,如图所示:

有两种方法:

  • 一、使用命令ipcrm –q msqid 删除消息队列,如图所示

  • 二、使用msgctl函数,写IPC_RMID标志删除消息队列

(4)删除消息队列 msgrm.c

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

int main(void)
{
    int  msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid < 0)
    {
        perror("msgget fail");
        return -1 ;
    }
    printf("success ... ! msgid:%d \n" , msgid);
    //写IPC_RMID标志
    if(msgctl(msgid , IPC_RMID , NULL) == 0)
    {
        printf("remove success ... \n");
    }
    return 0  ;
}

运行结果,如图所示:

使用系统提供的API的方式,能够将消息队列删除。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,相对于管道通讯有很大的改观,并且消息队列对数据的顺序处理也是很是有条理性的不会产生混杂性。

往期精彩

Linux 进程必知必会

【Linux系统编程】IO标准缓冲区

【Linux系统编程】可重入和不可重入函数

韦东山:6000字长文告诉你如何学习linux

会C/C++就能够开发Linux/Android应用程序?Yoxios了解一下!

以为本次分享的文章对您有帮助,随手点[在看]并转发分享,也是对个人支持。

本文分享自微信公众号 - 嵌入式云IOT技术圈(gh_d6ff851b4069)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索