- 管道命令
- 管道概述
- 管道文件描述符
- 为什么需要两个进程
- 练习1 双向管道
- 练习2 顺序通信的并发进程
在Unix/Linux中,管道命令:cmd1 | cmd2包含一个管道符 “|”,cmd1是写入端,cmd2是读取端。
shell将通过一个进程运行cmd1,通过另一个进程运行cmd2,它们通过管道连接在一起,cmd1是cmd2的输入。
处理过程
(1)shell从标准输入获取命令行cmd1|cmd2时,会fork出一个子进程shell,并等待子进程shell终止。
(2)子进程shell:解析命令发现有|符号,于是将命令划分为head=cmd1,tail=cmd2。
(3)子进程执行以下代码段:
int pd[2];
pipe(pd); // 系统调用,创建管道
pid=fork(); // 创建子进程
if(pid){ // 父进程作为写入端
close(pd[0]); // 写入端必须关闭pd[0]
close(1); // 关闭标准输入
dup(pd[1]); // 复制pd[1]为标准输入
close(pd[1]); // 关闭pd[1]
exec(head); // 执行cmd1
}
else{ // 子进程作为读取端
close(pd[1]); // 读取端必须关闭pd[1]
close(0); // 关闭标准输出
dup(pd[0]); // 复制pd[0]为标准输出
close(pd[0]); // 关闭pd[0]
exec(tail); // 执行cmd2
}
管道概述
管道是用于进程交换数据的单向进程间通信通道。管道有一个读取端和一个写入端,可以从读取端读取写入端写入的数据,读取和写入通道通常是同步、阻塞的。
工作方式
(1)当读进程从管道上读取数据时,如果管道上有数据,读进程会根据需要读取并返回读取的字节数;如果管道没有数据,但仍然有写进程,读进程会阻塞等待数据;如果既没有数据也没有写进程,读进程返回0,并停止从管道中读取。
(2)当写进程将数据写入管道时,如果管道有空间,它会根据需要尽可能多地写入,直至管道写满,并唤醒阻塞的读进程,使它们继续读取;如果管道没有空间,但仍然有读进程,写进程会阻塞等待空间,直到读进程从管道读取数据来释放更多空间以此唤醒阻塞的写进程;但是如果管道不再有读进程,写进程将其视为管道中断错误,中止写入。
在内核中创建一个管道,并在pd[2]中返回两个文件描述符,pd[0]为读取端,pd[1]为写入端。
int pd[2]; int r = pipe(pd); // 返回1表示成功,-1表示失败为什么需要两个进程
管道不是为单进程创建的,设计管道的意图是为两个进程提供通信的方式。在创建管道之后,必须fork一个子进程,父进程和子进程分别作为写入端和读取端(顺序不强求),否则会陷入死锁。例如:在创建管道后,如果进程试图读取数据,此时管道中若没有数据,但是有一个写进程,该进程会阻塞等待写入端写入数据;然而写入端是其本身,所以进程等待自己,把自己锁起来了。
因此,创建管道后,进程需要fork一个子进程共享管道,内核的机制保证了子进程继承父进程所有打开的文件描述符。
假设父进程作为写入端,子进程作为读取端,各自需要关闭不需要的管道描述符。在这种情况下,如果父进程没有数据要再写入,可以先终止,读进程可以继续读取,读完管道中的所有数据后,发现没有写进程,读取端也终止。
使用UNIX的系统调用写一个叫“pingpong”的程序,在两个进程之间通过管道互相传递一个字节。父进程给子进程发送一个字节,子进程打印“pid: received ping”,pid是子进程的ID。子进程通过管道给父进程发送一个字节并退出,父进程从子进程读取该字节,打印“pid: received pong”并退出。
管道是单向的,因此需要创建两个管道。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc, char *argv[])
{
int pd_ptoc[2]; // pipe from parent to child process
int pd_ctop[2]; // pipe from child to parent process
char buf[4];
if(pipe(pd_ptoc)<0 || pipe(pd_ctop)<0){
write(2, "create pipe failedn", 19);
exit(1);
}
if(fork()){ // parent
close(pd_ptoc[0]);
close(pd_ctop[1]);
write(pd_ptoc[1], "ping", 4);
close(pd_ptoc[1]);
read(pd_ctop[0], buf, 4);
printf("%d: received pongn",getpid());
close(pd_ctop[0]);
}else{ // child
close(pd_ptoc[1]);
close(pd_ctop[0]);
read(pd_ptoc[0], buf, 4);
close(pd_ptoc[0]);
printf("%d: received pingn",getpid());
write(pd_ctop[1], "pong", 4);
close(pd_ctop[1]);
}
exit(0);
}
question:怎么实现read的阻塞机制?
练习2 顺序通信的并发进程使用管道写一个并发版本的质数过滤器。
参考资料:https://swtch.com/~rsc/thread/
问题描述:使用pipe和fork开启一个管道。第一个进程将数字2~35输入管道。对于每个质数,您将安排创建一个进程,该进程通过一个管道从它的左邻居读取数据,通过另一个管道向它的右邻居写入数据。
输出例子:
$ primes prime 2 prime 3 prime 5 prime 7 prime 11 prime 13 prime 17 prime 19 prime 23 prime 29 prime 31 $
注意:
(1)及时关闭进程不需要的文件描述符,否则可能会很快耗尽资源。
(2)一旦第一个进程达到35,它应该等待,直到整个管道终止,包括所有的子进程、孙子进程。因此,主质数进程应该只在所有输出输出完成后退出,并且在所有其他质数进程退出后退出。可以用wait实现。
(3)每两个进程之间的管道是单向独立的,需要单独分配。
(4)管道左边的进程写完后要及时关闭写入端,避免管道右边的进程处于阻塞状态。
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
void prime(int *pd_in);
int main(int argc, char*argv[])
{
int pd[2];
if(pipe(pd)<0){
write(2, "create pipe failn", 17);
exit(1);
}
if(fork()){
close(pd[0]);
for(int i=2;i<=35;i++)
write(pd[1], &i, 4);
close(pd[1]);
wait(0);
}else{
prime(pd);
}
exit(0);
}
void prime(int *pd_in){
int pd_out[2];
int num_in1,num_in2;
close(pd_in[1]);
if(pipe(pd_out)<0){
write(2, "create pipe failn", 17);
exit(1);
}
if(read(pd_in[0], &num_in1, 4)){
printf("prime %dn",num_in1);
}else exit(0);
if(read(pd_in[0], &num_in2, 4)){
if(fork()){
close(pd_out[0]);
write(pd_out[1], &num_in2, 4);
while(read(pd_in[0], &num_in2, 4)){
if(num_in2%num_in1){
write(pd_out[1], &num_in2, 4);
}
}
close(pd_out[1]);
close(pd_in[0]);
wait(0);
}else{
close(pd_in[0]);
prime(pd_out);
}
}else exit(0);
}



