简介:本文主要介绍了管道(pipe)的基本概念和用途;分析了环形缓冲区的存储、访问及其实现方法;分析并发访问可能引发的问题,并给出解决方法;分析了linux2.6.29内核中pipe的读写函数。
1、管道(pipe)
管道是进程间通信的主要手段之一。一个管道实际上就是个只存在于内存中的文件,对这个文件的操作要通过两个已经打开文件进行,它们分别代表管道的两端。管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构。根据管道的适用范围将其分为:无名管道和命名管道。
●
主要用于父进程与子进程之间,或者两个兄弟进程之间。在linux系统中可以通过系统调用建立起一个单向的通信管道,且这种关系只能由父进程来建立。因此,每个管道都是单向的,当需要双向通信时就需要建立起两个管道。管道两端的进程均将该管道看做一个文件,一个进程负责往管道中写内容,而另一个从管道中读取。这种传输遵循“先入先出”(FIFO)的规则。
●
命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。
2、环形缓冲区
每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者”进程就要睡眠等待,具体过程如下图所示。
图1
2.1环形缓冲区实现原理
环形缓冲区是嵌入式系统中一个常用的重要数据结构。一般采用数组形式进行存储,即在内存中申请一块连续的线性空间,可以在初始化的时候把存储空间一次性分配好。只是要模拟环形,必须在逻辑上把数组的头尾相连接。只要对数组最后一个元素进行特殊的处理——访问尾部元素的下一元素时,重新回到头部元素。对于从尾部回到头部只需模缓冲长度即可(假设maxlen为环形缓冲的长度,当读指针read指向尾部元素时,只需执行read=read%maxlen即可使read回到头部元素)。
图2
2.2读写操作
环形缓冲区要维护写端(write)和读端(read)两个索引。写入数据时,必须先确保缓冲区没有满,然后才能将数据写入,最后将write指针指向下一个元素;读取数据时,首先要确保缓冲区不为空,然后返回read指针对应得元素,最后使read指向下一个元素的位置。读写操作伪代码:
2.3判断“满”和“空”
当read和write指向同一个位置时环形缓冲区为空或满。为了区别环满和空,当read和write重叠的时候环空;而当write比read快,追到距离read还有一个元素间隔的时候,就认为环已经满了。环形缓冲区原理图如图3所示。
图3
3
考虑到在不同环境下,任务可能对环形缓冲区的访问情况不同,需要对并发访问的情况进行分析。
在单任务环境下,只存在一个读任务和一个写任务,只要保证写任务可以顺利的完成将数据写入,而读任务可以及时的将数据读出即可。如果有竞争发生,可能会出现如下情况:
Case1:假如写任务在“写指针加1,指向下一个可写空位置”执行完成时被打断,如图3所示,此时写指针write指向非法位置。当系统调度读任务执行时,如果读任务需要读多个数据,那么不但应该读出的数据被读出,而且当读指针被调整为0是,会将以前已经读出的数据重复读出。
图4
Case2:假设读任务进行读操作,在“读指针加1”执行完时被打断,如图4所示,此时read所处的位置是非法的。当系统调度写任务执行时,如果写任务要写多个数据,那么当写指针指到尾部时,本来缓冲区应该为满状态,不能再写,但是由于读指针处于非法位置,在读任务执行前,写任务会任务缓冲区为空,继续进行写操作,将覆盖还没有来的及读出的数据。
图5
为了避免上述错误的发生,必须保证读写指针操作是原子性的,读写指针的值要么是没有修改的,要么是修改正确的。可以引入信号量,有效的保护临界区代码,就可以避免这些问题。在单任务环境下,也可以通过采取适当的措施来避免信号量的使用,从而提高程序的执行效率。
4.linux内核中pipe的读写实现
Linux内核中采用struct pipe_inode_info结构体来描述一个管道。
其中,当pipe为空/满时,采用等待队列,该队列使用自旋锁进行保护。
用struct Pipe_buffer数据结构描述pipe的缓冲(buffer)
本文重点针对pipe实现中对环形缓冲区的操作方法,目的是借鉴学习其互斥访问方法。因此,着重分析pipe_read和pipe_write方法。
●Pipe_read(fs/pipe.c)
访问pipe对应的inode必须获得相应的互斥锁,防止并发访问。
数据的读出放在一个死循环中,整个for循环中的代码均属于临界区,需要互斥锁进行保护。
有以下几种情况才会退出:
▲
▲
▲
325行将buffer中的数据读出。完成后,紧接着调整buffer中指针的位置
其中,348行设置标志,do_wakeup为1,说明buffer中已经有空位置可以写入数据,这时,可以唤醒等待队列中的睡眠的写进程。
如果没有退出,或者成功读取数据,读进程会主动调用pipe_wait函数进行睡眠等待,直到有writer进程写入数据并将其唤醒。
当进程从临界区中退出后会释放互斥锁。
最后,为了防止reader进程是因为收到信号量而退出,再给睡眠的writer进程一次机会,检查do_wakeup,如果为1就唤醒睡眠的writer进程。
●
首先,与pipe_read相同,pipe_write采用互斥锁对临界区进行保护。写操作也放在死循环中,退出条件也与read相同。
与pipe_read不同,writer进程不总是睡眠等待,在调用pipe_wait进行睡眠后,如果有read进程读走某些数据,write进程会随时进行写操作。
下面再来看看,系统调用pipe的例子。在下面的例子中,父进程通过管道向子进程发送了一个字符串。
子进程将它显示出来:
1.创建pipe文件
2.读fd[0]
3.写fd[1]
4.给个可以运行的代码
int main(void){
}