【Linux】 ——进程间通信方式优缺点比较

1、管道

1.1 匿名管道

主要用于有亲缘关系的进程间通信

我们先来看一条Linux语句:

test1 | grep 8080

如果你还没忘记Linux命名,那一定知道 “ | ”的意思,其实它就是管道的意思,他的作用就是把前一个命令的输出作为后一条命令的输入。

以上代码其实就是把test1的输出作为grep 8080这条命令的输入,这里的“ | ”其实是一个匿名管道,只能用于有亲缘关系进程的通信。

如果两个进程需要通信的话就得创建两个管道。因为这种通信方式是单向的以下是关于匿名管道的实例:

在程序中,我们创建了一个管道,父进程关闭了写通道,子进程关闭读通道,子进程向管道写入字符串,父进程向管道读取字符串并输出。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define MAX_LEN 128
int main(void)
{
    /*0为读,1为写*/
    int fd[2] = {0}; //描述符
    pid_t pid = 0;
    char line[MAX_LEN] = {0};
    int n = 0;

    /*创建管道,需要传入两个文件描述符*/
    if(pipe(fd) < 0)
    {
        perror("create pipe failed\n");
        return -1;
    }
    /*fork子进程*/
    if((pid = fork()) < 0)
    {
        perror("fork failed\n");
        return -1;
    }
    /*父进程*/
    else if(pid > 0)
    {
        /*关闭管道的写描述符*/
        close(fd[1]);

        /*从管道读取数据*/
        n = read(fd[0],line,MAX_LEN);
        printf("read %d bytes from pipe :%s\n",n,line);

    }
    /*子进程*/
    else
    {
        /*关闭管道的读描述符*/
        close(fd[0]);
        /*向管道写入数据*/
        write(fd[1],"test",sizeof("test"));
    }
    return 0;
}

1.2、命名管道

命名管道可以用于任意进程间通信。

以下是创建一个命名管道等方法:

mkfifo test

这条命令创建了一个名为test的管道,接下来我们用一个进程向这个管道写数据,然后另一个进程将写入的数据读出:

echo “this is a pipe” >test 

如果该管道的数据没有被读出,那它将一直等待,直到有其他进程经数据读出:

cat < test  //读数据

只有数据被其他进程读出,上一条写数据的才算结束。
这里也体现出管道的缺点——效率低下,我给你传数据,但是只有你把数据拿走了,我才能返回,这样是很慢的,不适合频繁通信的进程。但同时也有一个优点,那就是可以保证你是真的拿到了我的数据。

以下是一个关于命名管道完整代码的实例:

我们首先创建一个命名管道,如果成功,则关闭他的读端,向里面写入数据。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO "/tmp/fifo"
#define MAX_LEN 128
int main(void)
{
    int writeFd;
    char line[MAX_LEN] = {0};
    if(mkfifo(FIFO,S_IRUSR|S_IWUSR) < 0 && (errno != EEXIST))
    {
         perror("make fifo failed:");
         return -1;
    }
    /*关闭管道的读描述符*/
    writeFd = open(FIFO,O_WRONLY,0);
    /*向管道写入数据*/
    write(writeFd,"www.yanbinghu.com",sizeof("www.yanbinghu.com"));
    close(writeFd);
    return 0;
}

然后再新的终端打开一个刚才创建的命名管道,从里面读取数据:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include<fcntl.h>
#define FIFO "/tmp/fifo"
#define MAX_LEN 128
int main(void)
{
    int readFd,n;
    char line[MAX_LEN] = {0};
    /*打开FIFO,这里打开可能失败,应该要对返回值处理*/
    readFd = open(FIFO,O_RDONLY,0);
    /*从FIFO读取数据*/

    n = read(readFd,line,MAX_LEN);
    printf("read %d bytes from pipe :%s\n",n,line);
    close(readFd);
    /*删除FIFO*/
    unlink(FIFO);
    return 0;
}

然后运行写进程和读进程,这样两个没有亲缘关系的进程就可以通过匿名管道进行通信了。

总结:

  • 半双工,即就是不能够同时在两个方向上传输数据,有的系统可能支持全双工。
  • 匿名管道只能用于有亲缘关系的进程进行通信
  • 命名管道可以用于任意进程间通信

2、消息队列

学习了以上管道我们知道,a向b 写数据,只有b把数据读出,a才能返回,那我们能不能不等待,我把数据传给你,然后我立即返回呢?

其实是可以的,消息队列就可以解决这样的问题,a只要把给b的数据放进消息队列里面就可以了,b要用数据直接从消息队列取出。

  • 缺点:如果a给b发送的数据过大,并且通信频繁,那消息队列就不合适了,因为数据过大,a发送数据的时间长,b去拷贝数据花费时间也长,效率就会变低。
  • 优点:管道一旦相关进程退出,那里面的数据也就没有了,但消息队列不一样,一个进程向里面写入数据后退出,另一个进程仍然可以取数据。

3、共享内存

针对消息队列的缺点,共享内存允许多个进程共享一个给定的存储区,由于他们是共享一块内存数据,减少了内存拷贝的时间,因此速度非常快

但这里我们可能会问:每个进程不是独立的吗?怎么可以共享内存呢?

其实,系统加载一个进程时,分配给进程的内存并不是实际的物理内存,而是虚拟内存空间,我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样虽然两个进程有独立的虚拟地址空间,但有一部分是映射的相同的物理内存,这样就完成了内存共享机制,如下图所示:
在这里插入图片描述
缺点:多进程竞争内存,类似于我们平常说的线程安全。

4、信号量

一句话概括:信号量的本质就是一个计数器,用来实现进程之间的同步与互斥。

例如信号量的初始值为1,然后a进程来访问共享内存的时候,我们把信号量的值设为0,然后b进程来访问共享内存的时候,看到信号量的值为0,就知道已经有人来访问内存了,那b就访问不了了。所以说信号量也是进程间的一种通信方式。

5、Socket

以上我们所说的管道,消息队列,共享内存,信号量都是同一主机之间的通信,那两个相隔几千里的进程怎么进行通信呢?

答案就是Socket,例如平时我们通过浏览器发起一个http请求,然后服务器给你返回对应的数据,这种就是采用了Socket的通信方式,也就是说:它能用于不同计算机之间的不同进程间的通信。

总结

  • 对于管道:速度慢,最后一个引用它的进程终止的时候,留在管道的数据也会被删除。匿名管道用于有亲缘关系的进程,命名管道用于任意键进程通信。半双工通信,一个写数据,一个读数据,只能是单向的。
  • 消息队列:内核中的优先级队列,多个进程通过向同一个队列中放置队列结点或者获取节点实现通信
  • 共享内存:各个进程通过映射相同的物理地址实现通信,减少了内存拷贝时间。但是要注意多进程通信的安全
  • 信号量:通过计数器来检测内存的访问情况,如果有进程访问内存,计数变为0,没有进程访问,计数变为1.计数只有0/1两种状态。
  • Socket用于不同计算机之间不同进程的通信,是目前应用最广泛的进程间通信方式。
发布了42 篇原创文章 · 获赞 13 · 访问量 1763

猜你喜欢

转载自blog.csdn.net/Vicky_Cr/article/details/105294303