实验目的
首先来对管道(pipe)有一个基本了解:
管道
管道(Pipe)是一种用于进程间通信(IPC)的简单而有效的方式。在UNIX和类UNIX操作系统(如Linux)中,管道提供了一种让一个进程将其输出发送给另一个进程的输入的机制。管道通常用于数据流的单向传输。
在底层,管道其实是一个由操作系统内核维护的缓冲区。一个进程向管道的一端(写端)写入数据,而另一个进程可以从管道的另一端(读端)读取数据。
管道的特点是只有管道为满的时候,才能读取.在管道为空的时候才能读入,不存在两个端口都开放的情况,在cpp中的体现为:只有用close关闭指定的端口,另一个端口才能正常工作
Pipe系统调用
Pipe系统调用的语法如下,这也是一个函数原型:
#include <unistd.h>
int pipe(int pipefd[2]);
这里的 pipefd
是一个整型数组,有两个元素。pipe()
调用成功后,会将管道的读端和写端的文件描述符分别存储在 pipefd[0]
和 pipefd[1]
中。其中,pipefd[0]
用于从管道中读取数据,pipefd[1]
用于向管道中写入数据。
`pipe()` 函数的返回值是一个整型,用于表示函数调用是否成功。
如果调用成功,返回值为0;如果出错,返回值为-1,并设置 `errno` 来指示错误类型。
Pipe工作原理
- 数据流向:数据从pipefd[1](写端)流向pipefd[0](读端)。
- 阻塞和非阻塞:pipe()通常是阻塞的。也就是说,读操作会阻塞,直到有数据写入;写操作也会阻塞,直到读端读取了数据。
- 数据缓冲:数据首先被写入内核缓冲区,然后由读操作从缓冲区中读取。
- 文件描述符的继承:pipe()创建的文件描述符可以在fork()之后由子进程继承,这使得pipe()非常适用于父子进程或兄弟进程之间的通信。
- 关闭规则:当写端被关闭后,任何尝试从读端读取的操作将立即返回,读取到的数据长度为0(表示EOF)。当读端被关闭后,任何尝试写入写端的操作都将导致发送SIGPIPE信号
Pipe示例
如果你想创建一个管道,然后将其用于父进程向子进程发送数据,可以像下面这样使用pipe()
:
这段代码创建了一个管道,然后 fork 了一个子进程。父进程向管道中写入了 "Hello, child!",而子进程从管道中读取数据并将其打印出来
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
char buffer[20];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
// 子进程
close(pipefd[1]); // 关闭写端
read(pipefd[0], buffer, sizeof(buffer));
printf("子进程收到消息: %s\n", buffer);
close(pipefd[0]);
} else {
// 父进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello, child!", 13);
close(pipefd[1]);
}
return 0;
}
【注】当一个进程(父进程)调用fork()创建子进程时,子进程会继承父进程的文件描述符表。这意味着pipefd[0]和pipefd[1]在子进程中的值将与父进程中的值相同,它们指向同一个管道。
具体地说,父进程和子进程将拥有指向同一个内核管道对象的文件描述符。这使得父子进程可以通过这个管道进行通信。
因为子进程继承了父进程的文件描述符,所以:
在子进程中,pipefd[0]仍然是管道的读端。
在子进程中,pipefd[1]仍然是管道的写端。
这就是为什么在创建管道和fork()之后,通常会看到一些close()调用:每个进程通常只需要管道的一端,所以会关闭不需要的那一端。这样做有助于避免潜在的死锁和资源泄漏。
示例实验分析
下面是实验指导书给出的示例实验,以下示例实验程序要实现并发的父子进程合作将整数 X 的值从 1 加到 10 的功能。它们通过管道相互将计算结果发给对方。
1.pipe.c文件
//在新建文件夹中建立以下名为 ppipe.c 的 C 语言程序
/*
* Filename : ppipe.c
* copyright : (C) 2006 by zhanghonglie
* Function : 利用管道实现在父子进程间传递整数
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int pid; //进程号
int pipe1[2]; //存放第一个无名管道标号
int pipe2[2]; //存放第二个无名管道标号
int x; // 存放要传递的整数
//使用 pipe()系统调用建立两个无名管道。建立不成功程序退出,执行终止
if(pipe(pipe1) < 0){
perror("pipe not create");
exit(EXIT_FAILURE);
}
if(pipe(pipe2) < 0){
perror("pipe not create");
exit(EXIT_FAILURE);
}
//使用 fork()系统调用建立子进程,建立不成功程序退出,执行终止
if((pid=fork()) <0){
perror("process not create");
exit(EXIT_FAILURE);
}
//子进程号等于 0 表示子进程在执行,
else if(pid == 0){
//子进程负责从管道 1 的 0 端读,管道 2 的 1 端写,
//所以关掉管道 1 的 1 端和管道 2 的 0 端。
close(pipe1[1]);
close(pipe2[0]);
//每次循环从管道 1 的 0 端读一个整数放入变量 X 中,
//并对 X 加 1 后写入管道 2 的 1 端,直到 X 大于 10
do{
read(pipe1[0],&x,sizeof(int));
printf("child %d read: %d\n",getpid(),x++);
write(pipe2[1],&x,sizeof(int));
}while( x<=9 );
//读写完成后,关闭管道
close(pipe1[0]);
close(pipe2[1]);
//子进程执行结束
exit(EXIT_SUCCESS);
}
//子进程号大于 0 表示父进程在执行,
else{
//父进程负责从管道 2 的 0 端读,管道 1 的 1 端写,
//所以关掉管道 1 的 0 端和管道 2 的 1 端。
close(pipe1[0]);
close(pipe2[1]);
x=1;
//每次循环向管道 1 的 1 端写入变量 X 的值,并从
//管道 2 的 0 端读一整数写入 X 再对 X 加 1,直到 X 大于 10
do{
write(pipe1[1],&x,sizeof(int));
read(pipe2[0],&x,sizeof(int));
printf("parent %d read: %d\n",getpid(),x++);
}while(x<=9);
//读写完成后,关闭管道
close(pipe1[1]);
close(pipe2[0]);
}
//父进程执行结束
return EXIT_SUCCESS;
}
注意,这里的`getpid()` 是一个系统调用,用于获取当前进程的进程ID(PID)。在多进程编程中,每个进程都有一个唯一的PID,用于标识进程的身份。
在此程序中,`getpid()` 主要用于输出当前进程的PID,以便在输出语句中显示当前是父进程还是子进程在执行。通过输出PID,可以更好地理解程序的执行流程,以及父子进程之间的交互。
在父进程和子进程中,`getpid()` 函数被用来获取当前进程的PID,并打印出相应的信息,以便于调试和理解程序的运行过程。
由于管道的读取和写入操作是阻塞的,因此在每次循环中,如果没有数据可读或者写入端已关闭,进程会阻塞等待对应的操作完成。这样就确保了进程能够在适当的时候等待对方的动作,从而实现了交替进行的循环。
2.makefile文件
//通过变量完成指定编译器和文件名
srcs = ppipe.c
objs = ppipe.o
//变量完成编译选项,-g用于调试信息
opts = -g -c
//默认目标
all:
ppipe
//构建目标
ppipe: $(objs)
gcc $(objs) -o ppipe
//从源文件生成目标文件的规则
ppipe.o: $(srcs)
gcc $(opts) $(srcs)
//清理编译生成的文件
clean:
rm ppipe *.o
附:fork()调用
#include <unistd.h>
pid_t fork(void);
pid_t
是一个整型类型,用于表示进程ID(PID)。fork()
调用成功时,父进程中返回子进程的PID,子进程中返回0。- 在出错时,
fork()
返回-1,表示创建新进程失败。 - 当程序调用
fork()
时,操作系统会复制父进程的地址空间和所有相关资源,并分配给子进程。这意味着子进程会获得父进程的完整副本,包括代码段、数据段、堆和栈等。但是,子进程的地址空间是独立于父进程的,所以对于变量的修改在父子进程之间是相互独立的。在调用fork()
之后,父子进程会在不同的执行路径上继续执行程序的代码。
父进程和子进程之间的主要区别是,fork()
在子进程中返回0,在父进程中返回子进程的PID。这样,通过检查fork()
的返回值,可以确定当前是父进程还是子进程,从而在程序中采取不同的操作。fork()
被广泛用于创建新的进程,例如在并发编程中,父子进程可以并行执行不同的任务;在后台任务中,子进程可以执行一些耗时的操作,而父进程继续响应用户输入;在服务器编程中,父进程可以监听端口,而子进程处理客户端请求等。
独立实验分析
02pipe.c文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
//f(x)
int fx(int x)
{
//记得考虑一下x不要小于0
if(x <= 0)
{
printf("wrong number!!!");
return 0;
}
else if(x == 1)
{
return 1;
}
else if(x > 1)
{
return fx(x-1) * x;
}
}
//f(y)
int fy(int y)
{
if(y <= 0)
{
printf("wrong number!!!");
return 0;
}
else if(y == 1 || y == 2)
{
return 1;
}
else if(y > 2)
{
return fy(y-1) + fy(y-2);
}
}
//f(x,y)
int fxy(int fx, int fy)
{
return fx + fy;
}
int main()
{
/*
建立三个进程,一个子进程负责计算f(x)的值,一个子负责计算f(y)的值,另一个负责计算f(x,y)的值
f(x),f(y)的值通过管道传输
*/
//建立两个管道
int pipe1[2];
int pipe2[2];
//两个子进程的进程号
int pid1;
int pid2;
//数值x和y
int x;
int y;
//从键盘输入x和y
printf("please input number x: ");
scanf("%d",&x);
printf("\n");
printf("please input number y: ");
scanf("%d",&y);
printf("\n");
//保存f(x)和f(y)的变量
int temp1,temp2;
//使用pipe()系统调用建立两个无名管道。建立不成功程序退出,执行终止
if(pipe(pipe1) < 0)
{
perror("pipe not create");
exit(EXIT_FAILURE);
}
if(pipe(pipe2) < 0)
{
perror("pipe not create");
exit(EXIT_FAILURE);
}
pid1 = fork();
//使用fork()系统调用建立子进程,建立不成功程序退出,执行终止
if(pid1 <0)
{
perror("process not create");
exit(EXIT_FAILURE);
}
//第一个子进程,pipe1[1]用来写
if(pid1 == 0)
{
//关掉pipe1[0]端
close(pipe1[0]);
//计算f(x)
temp1 = fx(x);
printf("pid1 f(x) = %d\n",temp1);
//发送消息
write(pipe1[1],&temp1,sizeof(int));
//发送完成后关闭管道
close(pipe1[1]);
}
else
{
close(pipe1[1]);
//读从管道1获得的数值
read(pipe1[0],&temp1,sizeof(int));
close(pipe1[0]);
pid2 = fork();
//使用fork()系统调用建立子进程,建立不成功程序退出,执行终止
if(pid2 <0)
{
perror("process not create");
exit(EXIT_FAILURE);
}
//第二个子进程,pipe2[1]用来写
if(pid2 == 0)
{
//关掉pipe2[0]端
close(pipe2[0]);
//计算f(y)
temp2 = fy(y);
printf("pid2 f(y) = %d\n",temp2);
//发送消息
write(pipe2[1],&temp2,sizeof(int));
close(pipe2[1]);
}
//父进程
else
{
wait(NULL);
close(pipe2[1]);
//接受第二个子进程从管道里发来的信息
read(pipe2[0],&temp2,sizeof(int));
int result = fxy(temp1,temp2);
printf("f(x) = %d\n",temp1);
printf("f(y) = %d\n",temp2);
printf("f(x,y) = %d\n",result);
//读完成后关闭管道
close(pipe2[1]);
//父进程执行结束
return EXIT_SUCCESS;
}
}
}
makefile文件
srcs = 02pipe.c
objs = 02pipe.o
opts = -g -c
all:
02pipe
02pipe: $(objs)
gcc $(objs) -o 02pipe
02pipe.o: $(srcs)
gcc $(opts) $(srcs)
clean:
rm 02pipe *.o