管道和消息队列的区别

#1、Linux 管道通信机制

管道是Linux及Unix 都提供的一种进程间的通信机制,它是进程之间的一个单向数据流。根据Liunux的设计思想:“一切皆文件”, 其本质是一个伪文件(实为内核缓冲区),通常称这种文件为管道文件。管道由两个文件描述符引用,一个表示读端,一个表示写端,一个进程可向管道写入数据(写端),另一个进程则可以从管道中读取数据(读端),从而达到数据交换的目的。Linux 的管道通信机制有无名管道和有名管道两种机制。

1)无名管道
(1)无名管道的基本概念

  • 无名管道只能用于具有亲缘关系的进程,如父子进程或者兄弟进程之间的通信。
  • 无名管道是半双工的,具有固定的读端和写端,虽然pipe() 系统调用返回了两个文件描述符。但每个进程在使用一个文件描述符之前,应该先将另一个文件描述符关闭。如果需要双向的数据流,则必须通过二次 pipe() 建立起两个管道。
  • 无名管道可以看成一种特殊的文件,由一组VFS对象(虚拟文件系统)来实现。没有对应的磁盘映射只存在于内存的高级缓冲区中。Linux在2.6以后的版本中,把管道相关的VFS对象组织成一种特殊文件系统pipefs 进行管理,但他在系统目录树中没有安装点,因此用户看不到它。对管道的读写与普通文件的读写一样,使用通用的read,write等函数,但进程最终会调用管道文件的读写操作函数。
    (2)创建无名管道:pipe() 系统调用
  • 函数原型:
    int pipe(int filedes[2]); //需包含头文件<unistd.h>
  • 参数:
    filedes[2] 是一个输出参数,它返回两个文件描述符,其中filedes[0] 用于读管道,filedes[1] 用于写管道。
  • 功能:
    从内存缓冲区中创建一个管道,主要是建立相关的VFS 对象,并将读写该管道的一对文件描述符保存在filedes[2] 中。不再使用管道时,只需关闭两个文件描述符即可。
  • 返回值:
    成功返回0,失败返回-1,并在error 中存入错误码。
    (3)从管道中读取数据。
    使用read() 系统调用从管道中读取数据,内核最终会调用pipe_read() 函数来实现。在Linux2.6.10以前每个管道仅有一个缓冲区(4 KB);而在2.6.11 之后,每个管道最多可有16个缓冲区(64 KB)。大大提高了单次传输的数据量。从管道中读取数据有两种方式:阻塞型和非阻塞性读,下面分别介绍。
  • 阻塞型读:
    当读字节少于管道缓冲区中的字节,直接读出,缓冲区还剩一部分。当要读的字节数多于管道缓冲区就阻塞,等待写进程写入数据。
  • 非阻塞性读:
    非阻塞操作通常都是在 **open()**系统调用中指定 O_NONBLOCK(非阻塞性标志)进行请求,但这个方法并不适合无名管道,因为他没有open() 操作,不过进程可以通过对相应的文件描述符发出fcntl() 系统调用来请求对管道执行非阻塞操作。 在非阻塞情况下,如果管道大小p 小于n ,则读取p 个字节并返回p, 读操作完成;否则,读取n 返回n ,读操作完成。

2)、有名管道
不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。

 利用系统调用pipe()创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用mknod()创建一个命名管道文件,通常称为有名管道或FIFO。PIPE是一种非永久性的管道通信机构,当它访问的进程全部终止时,它也将随之被撤消;它也不能用于不同族系的进程之间的通信。而FIFO是一种永久的管道通信机构,它可以弥补PIPE的不足。管道文件被创建后,使用open()将文件进行打开,然后便可对它进行读写操作,通过系统调用write()和read()来实现。通信完毕后,可使用close()将管道文件关闭。因为匿名管道的文件是内存中的特殊文件,而且是不可见的,命名管道的文件是硬盘上的设备文件,是可见的。

# Linux 的 IPC 消息队列通信机制

在IPC 消息队列通信机制中,若干个进程可以共享一个消息队列,系统允许其中的一个或多个进程想消息队列写入消息,同时也允许一个或多个进程从消息队列中读取消息,从而完成进程之间的信息交换。
消息队列机制是客户| 服务器 模型中常有的进程通信方式:客户向服务发送请求数据,服务器读取消息并执行相应的请求。消息可以是命令,也可以是数据。

优点:
A. 我们可以通过发送消息来几乎完全避免命名管道的同步和阻塞问题。
B. 我们可以用一些方法来提前查看紧急消息。

缺点:
A. 与管道一样,每个数据块有一个最大长度的限制。
B. 系统中所有队列所包含的全部数据块的总长度也有一个上限。

Linux系统中有两个宏定义:
MSGMAX, 以字节为单位,定义了一条消息的最大长度。
MSGMNB, 以字节为单位,定义了一个队列的最大长度。

限制:
由于消息缓冲机制中所使用的缓冲区为共用缓冲区,因此使用消息缓冲机制传送数据时,两通信进程必须满足如下条件。
(1)在发送进程把写入消息的缓冲区挂入消息队列时,应禁止其他进程对消息队列的访问,否则,将引起消息队列的混乱。同理,当接收进程正从消息队列中取消息时,也应禁止其他进程对该队列的访问。
(2)当缓冲区中无消息存在时,接收进程不能接收任何消息;而发送进程是否可以发送消息,则只由发送进程是否能够申请缓冲区决定。

============================================================================

共享内存比管道和消息队列效率高的原因

共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
因为系统内核没有对访问共享内存进行同步,您必须提供自己的同步措施。例如,在数据被写入之前不允许进程从共享内存中读取信息、不允许两个进程同时向同一个共享内存地址写入数据等。解决这些问题的常用方法是通过使用信号量进行同步。

共享内存块提供了在任意数量的进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。不幸的是,Linux无法严格保证提供对共享内存块的独占访问,甚至是在您通过使用IPC_PRIVATE创建新的共享内存块的时候也不能保证访问的独占性。 同时,多个使用共享内存块的进程之间必须协调使用同一个键值。     



   共享内存区是最快的可用IPC形式,一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不再通过执行任何进入内核的系统调用来传递彼此的数据,节省了时间。
   共享内存和消息队列,FIFO,管道传递消息的区别:
   ——消息队列,FIFO,管道的消息传递方式一般为
      1:服务器得到输入
      2:通过管道,消息队列写入数据,通常需要从进程拷贝到内核。
      3:客户从内核拷贝到进程
      4:然后再从进程中拷贝到输出文件
  上述过程通常要经过4次拷贝,才能完成文件的传递。
   ——共享内存只需要
       1:从输入文件到共享内存区
       2:从共享内存区输出到文件

上述过程不涉及到内核的拷贝,所以花的时间较少。

================= 管道实现机制 =========================

在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。
在这里插入图片描述

当然 这和网上大部分解释一样,并不能令人满意。

深入理解linux内核 P786 中有很好的解释

一下是我自己的理解,不清楚的 请查看上面的书籍。

管道创建 撤销
在这里插入图片描述

1.get_pipe_inode() 为pipefs中的管道 分配索引节点,并初始化
2.为 读/写通道 分配一个文件对象和文件描述符,并设置对应的读写权限
3.把两个文件对象和索引节点连在一起
4.两个文件 描述符返回给用户态进程

撤销
1.用户态进程对和管道相关的一个文件描述符调用close()系统调用,引用计数-1
2.当计数为0 ,调用release()方法 ,释放 管道缓冲页框

2.管道的读写
pipe_read()
1)获取索引节点的i_sem信号量 //我要读了
2)判断缓冲区 是否 空 ,或阻塞 //空不空啊?
阻塞: a .调用prepare_wait() 把当前进程(cur)加到等待队列
b.释放索引节信号量
c.调用 schedule()
d.cur一旦被唤醒,从等待队列中删除 。拷贝所有请求字节 //读
3.释放索引节点的i_sem信号量 //我读完了
4.唤醒管道中所有的写者进程 //你来写吧
5.返回 已经拷贝的字节数目 //读了这么多

pipe_write()
1.获取索引节点的i_sem信号量 //我要写了
2.检查是否至少有一个读进程, //有没有人正在读啊
如果不是,向当前进程发送 SIGPIPE信号,释放i_sem信号量 ,并返回-EPIPE
3.判断是否有足够的写空间, //满不满啊?
是,则向缓冲区 拷贝数据。如果不是非阻塞,释放索引节点并返回—EAGAIN; 如果不是且阻塞, 将当前写操作放入等待队列,释放信号量 ,调用schedule(),一旦被唤醒,返回 3 操作。
4.写入 所有请求的字节 //不满就写
5.释放索引节点信号量 //我写完了
6.唤醒所有等待队列的读进程 //快来读吧
7.返回写入的字节数目(如果没有写入 则返回错误码) //写了这么多


版权声明:本文为qq_44222849原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_44222849/article/details/106699394