管道(PIPE)
管道通信方式的中间介质是文件,通常称这种文件为管道文件。两个进程利用管道文件进行通信时,一个进程为写进程,另一个进程为读进程。写进程通过写端(发送端)往管道文件中写入信息;读进程通过读端(接收端)从管道文件中读取信息。两个进程协调不断地进行写、读,便会构成双方通过管道传递信息的流水线。
管道分为匿名管道和命名管道。
(1)匿名管道:管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
(2)命名管道:可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。
不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。
利用系统调用pipe()创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用mknod()创建一个命名管道文件,通常称为有名管道或FIFO。PIPE是一种非永久性的管道通信机构,当它访问的进程全部终止时,它也将随之被撤消;它也不能用于不同族系的进程之间的通信。而FIFO是一种永久的管道通信机构,它可以弥补PIPE的不足。管道文件被创建后,使用open()将文件进行打开,然后便可对它进行读写操作,通过系统调用write()和read()来实现。通信完毕后,可使用close()将管道文件关闭。因为匿名管道的文件是内存中的特殊文件,而且是不可见的,命名管道的文件是硬盘上的设备文件,是可见的。
消息队列(message queue)
消息队列与命名管道类似,但少了打开和关闭管道方面的复杂性。使用消息队列并未解决我们在使用命名管道时遇到的一些问题,如管道满时的阻塞问题。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。与命名管道相比:消息队列的优势在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。而且,每个数据块被认为含有一个类型,接收进程可以独立地接收含有不同类型值的数据块。
============================================================================
共享内存比管道和消息队列效率高的原因
共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 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.返回写入的字节数目(如果没有写入 则返回错误码) //写了这么多