IPC under Linux-Pipe

本文转自:http://edsionte.com/techblog/archives/1144

管道是一种最基本的IPC方式,本文以笔记形式以及典型程序来描述管道的特点。
1.管道特点关键字
[list]

[]半双工通信
[
]亲缘进程间通信
[]内存中的特殊文件
[
]在末尾写数据,在头部读数据
[/list]2.管道创建
先创建管道,再创建进程。即先pipe,再fork。
3.从管道中读数据
[list]

[]管道的写端不存在,那么进程认为已经读到了数据的末尾,读函数返回的字节数为0。
[
]这里有三个名词:PIPE_BUF,请求读取字节数(request_num),管道中数据量(pipe_num);那么关于这三个量之间的关系如下图。
[/list]下面这个程序可以加深你对上述读规则的理解,实现代码如下:

int main() 
{ 
int fd[2]; 
int ret,request; 
char rbuf[4096]; 
char wbuf[5000]; 
char *msg="hello,pipe!",*msg2="hello,I come here ,too!"; 
pid_t pid; 
int stat_val; 

if(pipe(fd)\<\0) 
{ 
printf("pipe error\n"); 
exit(1); 
} 
if((pid=fork())==0) 
{ 
//child process write 
close(fd[0]); 
ret=write(fd[1],msg,strlen(msg)+1); 
printf("I am writer and I write %d Byte data\n",ret); 
sleep(5); 
ret=write(fd[1],msg2,strlen(msg2)+1); 
printf("I am writer and I write %d Byte data\n",ret); 
sleep(5); 
close(fd[1]); 
exit(0); 
} 
else if(pid\>\0) 
{ 
//parent process read 
close(fd[1]); 
request=5000; 
ret=read(fd[0],rbuf,request); 
printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret); 
request=10; 
ret=read(fd[0],rbuf,request); 
printf("when reader requesr %d Byte,the actual return Bytes is %d\n",request,ret); 
request=100; 
ret=read(fd[0],rbuf,request); 
printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret); 
request=1; 
ret=read(fd[0],rbuf,request); 
printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret); 
close(fd[0]); 
exit(0); 
} 
} 

该程序中父进程读数据,子进程去写数据。至于运行过程请你亲自去尝试,因为我觉得我在这里说的多么具体,都不及你亲自去感受。
首先你可能会想:是否应该在父进程所要执行的代码前加一个sleep(1)?因为在父进程读之前,管道内至少得有数据啊!其实可以不用加。因为如果管道内无数据可读,父进程自动阻塞,直至有数据或者不存在写端时才会继续运行。
在子进程第一次写操作后,就睡眠5秒,此时父进程刚好读完管道内的所有数据,因为请求的数据量为5000,而PIPE_BUF为4960。现在管道内已经无数据可读了,因此父进程阻塞。5秒过后,子进程继续写入24Byte。父进程第二次请求10Byte,因此根据上述我们所说的读规则,read函数返回10。父进程第三次请求读100Byte的数据,根据上述读规则,他将返回管道内所有数据的字节数,因此read函数返回14Byte。
父进程第四次请求读前,管道已经空了,按照我们刚所说的,父进程现在应该阻塞了。因此在子进程睡眠5秒后,父进程发现读端已经关闭,因此读函数返回0。
4.从管道中写数据
[list]

[]只有管道读端存在时,管道写数据才有意义。
[
]写数据时,Linux不能保证写入的原子性。
[/list]关于写数据的第一条规则,很好去验证,比如上述程序,如果在父进程一开始就关闭两个端,那么子进程将会自动退出。但是如果将子进程的读端不关闭,那么子进程是可以正常写入pipe的。因此,在写管道时,应该至少保证一个进程的的读端是打开的。
关于写数据的非原子性是指,如果要写入A字节的数据,而此时管道内只有B字节的缓冲区,而且A>B。那么此时写端的进程就会分批写入数据:先写入B字节,然后再写入(A-B)字节。特殊的,当A-B仍然大于PIPE_BUF的时候,那么还是先写PIPE_BUF字节,然后再写(A-B-PIPE_BUF)字节的数据。如果当前在写入最后一批数据前,可以一次性写入管道,那么此时写函数将返回写入的字节数,而不是等所有数据都写完毕后再返回写入的字节数。
下面这个程序请务必亲自动手验证。

int main() 
{ 
int fd[2]; 
int ret,request; 
char rbuf[50000]; 
char wbuf[65000]; 
char wbuf2[100000]; 
char *msg="hello,pipe!",*msg2="hello,I come here ,too!"; 
pid_t pid; 
int stat_val; 
if(pipe(fd)\<\0) 
{ 
printf("pipe error\n"); 
exit(1); 
} 

if((pid=fork())==0) 
{ 
//child process write 
close(fd[0]); 
ret=write(fd[1],wbuf,65000); 
printf("I am writer and I write %d Byte data\n",ret); 
ret=write(fd[1],wbuf2,100000); 
printf("I am writer and I write %d Byte data\n",ret); 
printf("writer has over\n"); 
close(fd[1]); 
exit(0); 
} 
else if(pid\>\0) 
{ 
//parent process read 
// sleep(3); 
close(fd[1]); 
while(1) 
{ 
sleep(1); 
ret=read(fd[0],rbuf,10000); 
printf("I am reader and the num of reading data is %d\n",ret); 
} 
close(fd[0]); 
exit(0); 
} 
} 

在程序运行后,当第七次显示已经读了10000字节时,就说明了写管道时的非原子性。因为如果写是原子性的,即便先写入的65000字节被读走,那么也不可能一次性全部写入100000字节(因为PIPE_BUF为65536)。当第七次提示已读入10000字节时,说明先将一部分数据写入了管道内的空闲区,否则第七次读入提示只显示已读入5000字节。 当第11次读入提示显示后,就会接着显示已写入100000字节的提示。因为前11次读中:先用7次(第七次的10000字节中既有第一次写入的5000字节,又有第二次的一部分数据)读走了第一次写入的65000字节,然后用5次读走第二次要写入100000字节的前45000字节,当剩余65000字节时,已经可以一次性写入管道,因此在最后一次写之前返回写入的字节数100000。这与上述我们概述的原理符合。
以上解释需要多次实验和斟酌。
5.管道的局限性关键词
[list]

[]单向流动
[
]缓冲区局限性
[]亲缘进程间通信局限性
[
]字符流传送
[/list]对于管道半双工流动的特点,如果使用两条管道,那么就可以实现两个进程间全双工通信。具体例子《LinuxC编程实战》一书中有详解。不过我这里想特别说明的是,由于管道的读端在无数据可读时会自动阻塞,因此如果两个管道都首先进行读数据,那么此程序会发生死锁,因为父子进程都在等待数据的出现(当然它们等待的管道不同)。因此解决的办法是父子进程都先去写数据或和一个先读一个先写。
对于管道的另一个应用,就是父进程创建子进程后向子进程传递参数(具体可见《实战》P250)。对于这个应用,下面仅说明如何传递。运行监控端程序时,会输入相应的参数(argv[1]),父进程将其写入管道。按照以往的读管道规则,子进程直接从管道读就可以了,但是在本应用中,子程序会去执行一个新的程序(ctrlpocess.c)。我们知道子进程在执行exec函数后,它的“前身”除了pid,几乎一点都不留。所以此刻在新进程中我们直接使用fd[0],fd[1]显然不行。
现在该如何解决?我们既要从管道中读数据,还得保证有一个可用的文件描述符。显然我们应该在子进程“离别”前(执行exec前)将读端(fd[0])定向到标准输入端。这样子进程在执行新的程序时就可以在标准出入端读数据了。
最后需要说明的是,本文仅仅阐述了管道部分的基本知识点,要深入理解管道则需要多次实践。毕竟实践出真知。