1、管道(pipe)
管道是进程间通信的主要手段之一。一个管道实际上就是个只存在于内存中的文件,对这个文件的操作要通过两个已经打开文件进行,它们分别代表管道的两端。管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构。根据管道的适用范围将其分为:无名管道和命名管道。
● 无名管道
主要用于父进程与子进程之间,或者两个兄弟进程之间。在linux系统中可以通过系统调用建立起一个单向的通信管道,且这种关系只能由父进程来建立。因此,每个管道都是单向的,当需要双向通信时就需要建立起两个管道。管道两端的进程均将该管道看做一个文件,一个进程负责往管道中写内容,而另一个从管道中读取。这种传输遵循“先入先出”(FIFO)的规则。
● 命名管道
命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。
2、环形缓冲区
每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者”进程就要睡眠等待,具体过程如下图所示。
2.1环形缓冲区实现原理
环形缓冲区是嵌入式系统中一个常用的重要数据结构。一般采用数组形式进行存储,即在内存中申请一块连续的线性空间,可以在初始化的时候把存储空间一次性分配好。只是要模拟环形,必须在逻辑上把数组的头尾相连接。只要对数组最后一个元素进行特殊的处理——访问尾部元素的下一元素时,重新回到头部元素。对于从尾部回到头部只需模缓冲长度即可(假设maxlen为环形缓冲的长度,当读指针read指向尾部元素时,只需执行read=read%maxlen即可使read回到头部元素)。
2.2读写操作
环形缓冲区要维护写端(write)和读端(read)两个索引。写入数据时,必须先确保缓冲区没有满,然后才能将数据写入,最后将write指针指向下一个元素;读取数据时,首先要确保缓冲区不为空,然后返回read指针对应得元素,最后使read指向下一个元素的位置。读写操作伪代码:
2.3判断“满”和“空”
当read和write指向同一个位置时环形缓冲区为空或满。为了区别环满和空,当read和write重叠的时候环空;而当write比read快,追到距离read还有一个元素间隔的时候,就认为环已经满了。环形缓冲区原理图如图3所示。
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循环中的代码均属于临界区,需要互斥锁进行保护。
有以下几种情况才会退出:
▲ 完成数据的读出;
▲ Pipe没有writer进程
▲ 进程设置了O_NONBLOCK标志
325行将buffer中的数据读出。完成后,紧接着调整buffer中指针的位置
其中,348行设置标志,do_wakeup为1,说明buffer中已经有空位置可以写入数据,这时,可以唤醒等待队列中的睡眠的写进程。
如果没有退出,或者成功读取数据,读进程会主动调用pipe_wait函数进行睡眠等待,直到有writer进程写入数据并将其唤醒。
![Linux内核中无名管道pipe和有名管道fifo的分析 - xychenbaihu@yeah - 无影的博客](http://home.lupaworld.com/data/attachment/album/201011/30/1004343z6mik2m0imz3cc4.jpg)
当进程从临界区中退出后会释放互斥锁。
![Linux内核中无名管道pipe和有名管道fifo的分析 - xychenbaihu@yeah - 无影的博客](http://home.lupaworld.com/data/attachment/album/201011/30/100433n3115fkpkpskksnk.jpg)
最后,为了防止reader进程是因为收到信号量而退出,再给睡眠的writer进程一次机会,检查do_wakeup,如果为1就唤醒睡眠的writer进程。
● pipe_write(fs/pipe.c)
首先,与pipe_read相同,pipe_write采用互斥锁对临界区进行保护。写操作也放在死循环中,退出条件也与read相同。
![Linux内核中无名管道pipe和有名管道fifo的分析 - xychenbaihu@yeah - 无影的博客](http://home.lupaworld.com/data/attachment/album/201011/30/100431q85h8qlb7b55u7hq.jpg)
与pipe_read不同,writer进程不总是睡眠等待,在调用pipe_wait进行睡眠后,如果有read进程读走某些数据,write进程会随时进行写操作。
FIFO文件的操作方法只有open方法(具体实现在fs/fifo.c)。但是,这并不是fifo文件真正的操作方法,其真正的读写方法是根据不同的打开方式而决定的。
FIFO文件的打开操作
第一次打开fifo文件的进程调用fifo_open时,该命名管道的缓冲页面还没有分配,
因此43行中alloc_pipe_info()函数会被执行。
分配所需要的pipe_inode_info数据结构和缓冲页面。以后打开该文件的进程会跳过该部分。
![Linux内核中无名管道pipe和有名管道fifo的分析 - xychenbaihu@yeah - 无影的博客](http://home.lupaworld.com/data/attachment/album/201012/01/203344wzbuegsszqg5jbcw.jpg)
Fifo可以以“只读”、“只写”、“读写”三种方式打开。另外,open系统调用中有flag参数,如果调用fifo_open的进程开始时设置了flag中的O_NONBLOCK参数为1,则在打开的过程中无论是否可以正常打开,进程都不能进入睡眠,必须立即返回。下面具体分析每个打开方式的不同操作。
◆ 以“只读”方式打开。即命名管道的读端的几种情况:
a) 如果命名管道的写端已经打开,那么管道的创建就完成了。这时,一般写端(生产者)一般都在睡眠,因此要调用wake_up_partner()将其唤醒。
b) 如果写端没有打开,而且设置了O_NONBLOCK标志,此时尽管读端已经打开,但是没有完成管道的打开,由于进程要求不能等待,因此必须立即返回。
c) 写读没有打开,但是没有设置O_NONBLOCK标志,读进程调用wait_for_partner()函数,进入睡眠状态等待写读打开后将其唤醒。
◆ 以“只写”方式打开。即打开命名管道写端的几种情况:
a) 如果命名管道的读端没有打开,并且设置了O_NONBLOCK标志,写端进程就要直接跳转到err处(判断是否有读进程或写进程在睡眠,如果没有就释放pipe_inode_info)执行。否则,让filp的f_op指向write_pipefifo_fops方法。然后管道的写进程计数加1
b) 如果命名管道的读端已经打开,那么写端就完成了命名管道的打开。此时,读端一般都在睡眠等待,应该调用wake_up_partner()将其唤醒。
c) 如果命名管道读端没有打开,那么写端就要调用wait_for_partner()进入睡眠等待,直到读端打开,将其唤醒之后才能返回。
◆ 以“读写”方式打开
![Linux内核中无名管道pipe和有名管道fifo的分析 - xychenbaihu@yeah - 无影的博客](http://home.lupaworld.com/data/attachment/album/201012/01/203352lsy0xftuxiushn6m.jpg)
读写的方式打开命名管道,相当于同一个进程打开了命名管道的两端,因此不需要等待。但是,也有可能已经有进程已经打开了某一端正在睡眠等待,因此,任意一端第一次打开,就唤醒了正在睡眠的进程。这种打开方式下,真正的操作方法是rdwr_pipefifo_fops。
命名管道一旦建立,以后的读写以及关闭都与普通管道相同。尽管FIFO文件的inode节点是在磁盘上,但是数据只是存在于内存缓存中,与普通管道相同。
分析完FIFO文件的不同打开方式之后,接下来分析各自对应的操作方法。具体实现在
Fs/pipe.c文件中。
![Linux内核中无名管道pipe和有名管道fifo的分析 - xychenbaihu@yeah - 无影的博客](http://home.lupaworld.com/data/attachment/album/201012/01/20335070kgetf0e88fetzv.jpg)
首先,我们可以看到在三个操作方法中llseek都调用的是no_llseek。
![Linux内核中无名管道pipe和有名管道fifo的分析 - xychenbaihu@yeah - 无影的博客](http://home.lupaworld.com/data/attachment/album/201012/01/203348dx2hex8wzwxwbkz7.jpg)
从代码中,可以发现no_llseek并没有做任何事情,只是在被调用的时候返回错误代码。也就是在FIFO文件中是不能使用seek方法的,对文件的读写只能根据先进先出的顺序进行访问。
接下来,在read_pipefifo_fops和write_pipefifo_fops中分别调用了bad_pipe_w和bad_pipe_r函数:
从代码中,可以看到它们也只是返回错误码,也就是说在pipe中读端写操作是禁止的,在写端读操作同样也是禁止的。