之前分析了pipe调用后,内核返回的几个数据结构及他们之间的关系,这次就根据源码来好好分析一下pipe到底是如何实现数据读写操作的。内核分配给pipe的缓冲区是通过调用底层函数get_free_page实现的,此函数是一个宏声明,其实质是汇编代码实现,从内存中找出一个空闲页面返回给pipe使用,通常有PAGE_SIZE=4096。我们可以把这块内存看成是一个大小为size=4096的数组,且整个页面是被清0后返回的。
pipe通常还有几个指针,指向内存真实地址的缓冲区指针,有效数据头指针(head)和有效数据尾指针(tail),形如下面的图:
如果想具体了解,还请参见en.wikipedia.org/wiki/Circular_buffer。下面我主要还是分析一下linux-v0.11中是如何实现ring_buffer的。
代码中分配内存缓冲语句为:inode->i_size = get_free_page(),返回值为unsigned long类型,存放了实际的物理内存地址,赋值给了inode(内存索引节点)中i_size字段,也就是说i_size中现在已经保存了缓冲区指针,以后读写管道就直接索引i_size就可以了。
下面分析以下头指针和尾指针,通过分析源码后,内核用这两个指针来表示有效数据的范围,这两个指针是用两个额外的short变量表示管道中已经读写字节的个数。内核首先是把两个变量都初始化为0,代码如下:
PIPE_HEAD(* inode) - PIPE_HEAD(* inode) = 0;
这里就需要看看几个宏定义
#define PIPE_HEAD(inode) ((inode).i_zone[0])
#define PIPE_TAIL(inode) ((inode).i_zone[1])
#define PIPE_SIZE(inode) ((PIPE_HEAD(inode)-PIPE_TAIL(inode)) & (PAGE_SIZE - 1))
#define PIPE_EMPTY(inode) (PIPE_HEAD(inode) == PIPE_TAIL(inode))
#define PIPE_FULL(inode) (PIPE_SIZE(inode) == (PAGE_SIZE - 1))
看过宏定义后就可以知道上面只是用inode中的i_zone的前两个元素保存写入管道的字节个数和读出管道的字节个数。下面就分析一下内核是如何解决ring_buffer中如何判断下面情况下
与上面空缓存时是如何区别这两种情况的,即两个指针指向同一个位置。
首先说一下,内核是通过总是让返回给写可用空间的字节数总比实际可写空间少1个字节来实现的,最后一个可用的字节不返回,若写进程尝试写,就会收到SIGPIPE信号。即上面链接中的第一个方法:always keep one slot open.
假设现在只有写没有读管道,也就是说缓冲区满的时候实际上的图应该像下面这样:
让我们验证一下上面的宏是否正确:#define PIPE_FULL(inode) (PIPE_SIZE(inode) == (PAGE_SIZE - 1)),判别式右边是PAGE_SIZE - 1=4095,PIPE_SIZE(inode)=((PIPE_HEAD(inode)-PIPE_TAIL(inode)) & (PAGE_SIZE - 1))也即(写的字节数-读的字节数)mode(PAGE_SIZE),此时为(4095-0)mode 4096 =4095,这样左边==右边,这样判断出缓冲区满。而PIPE_EMPTY(inode)直接通过写的自节数是否等于读的字节数就可以判断出管道是否为空了。
下面就具体分析一下内核是怎么写管道的,代码如下:
1 int write_pipe(struct m_inode * inode,char * buf,int count)
2 {
3 int chars,size,written = 0;
4 while(count > 0){
5 while(!(size = (PAGE_SIZE - 1) - PIPE_SIZE(* iinode))){
6 wake_up(&inode->i_wait);
7 if(inode -> i_count != 2){
8 current->signal |= (1<<(SIGPIPE - 1));
9 return written ? written: -1;
10 }
11 sleep_on(&inode->i_wait);
12 }
13 chars = PAGE_SIZE - PIPE_HEAD(*inode);
14 if(chars > count)
15 chars = count;
16 if(chars > size)
17 chars = size;
18 count -= chars;
19 written += chars;
20 size = PIPE_HEAD(* inode);
21 PIPE_HEAD(*inode) += chars;
22 PIPE_HEAD(*inode) &= (PAGE_SIZE - 1);
23 while(chars-->0)
24 ((char *)inode->i_size)[size++] = get_fs_byte(buf++);
25 }
26 }
第5行首先计算是否还有空间可写,如果size=0(灰色的字节是不会返回给写端的),并且判断没有读进程,则发送SIGPIPE信号并返回本次调用已经写管道的字节数。示意图如下:
(依据上面示意图分析)否则size就等于[(4096-1)- (3455 - 1233)] = 1873,也就是标记为-1的已读字节和标记为0的可写的字节数之和,其意义就是管道中可写字节的总和。第13行计算出标记为0并且包含最后一个不可写标记的总字节数,此例中chars = 4096 - 3456 = 640。
现在让我们先假设此次调用count = 640的临界情况,14行if不成立,16行也不成立,所以20行首先保存PIPE_HEAD到size里,给后面的24行使用,然后更新PIPE_HEAD,第22行执行的效果应该就相当把PIPE_HEAD对PAGE_SIZE取模运算,此时PIPE_HEAD就变成0,循环到起点。此时count = 0,结束循环。
若假设count = 1873,此时第一个循环首先将右端标记为0的可写字节写如数据,此时count = 1873 - 640 = 1233,第二个循环size = 4095 - [(0 - 1233)%4096] = 1232,即把下标是1233处可写的字节不返回给写端,13行chars = 4096,第15行将chars设置为1233,第17行将chars设置为1232,18行后count就变成1了,下一个循环后,第五行if 成立,发射信号,并返回1872。
读管道程序逻辑相差不大,类比一下写管道很容易就分析清楚了。