文章目录
前置知识
一个进程在创建时,会默认打开三个文件,分别是:stdin,stdout,stderr
进程中有一个维护进程所打开的文件的文件描述对象结构体struct files_struct
该文件描述对象结构体中包含一个fd_array,文件描述符表,这个文件描述符表存储的是对应打开的文件的文件描述对象的地址。也就是说,每一个文件都有对应的文件对象,来记录该文件的各种属性struct file
。而进程对应的是文件描述对象,两者不同。
fd_array中存储的就是struct file*
类型。
默认打开的三个文件中,stdin,stdout,stderr对应的分别是键盘文件,显示器文件,显示器文件,占用了fd_array文件描述符表中的0,1,2下标。
所以,进程再次创建文件时,会默认从3号下标开始记录。
一、管道的原理
父进程创建管道文件时,默认打开读端和写端,读端的文件fd存在3号下标中,写端文件存在4号下标中。
子进程被创建时会继承父进程的管理文件的对象,所以子进程的fd_array的3号和4号下标也记录了管道文件的读写端。
为了保证父子进程之间的通信,假设是父进程进行读取,子进程进行写入。
所以需要关闭父进程的写端,关闭子进程的读端。
子进程进行写入,父进程进行读取,就能实现通信了。
问题:为什么父进程不直接把要发送给子进程的数据保存一份,子进程在创建时就会继承这份数据了。
这种通信方式不是不可以,但只能静态通信。
实际上,在创建管道文件时,会创建两个文件对象,它们存储同一个inode
,指向同一块缓冲区,这样就能实现子进程通过写端的struct file
和父进程的读端的struct file
进而看到同一个文件缓冲区,也就是让不同的进程看到同一份资源。
所以管道通信只能进行单向通信!!!
二、管道的特性
Linux中,管道的大小一般是4096字节(4KB)
管道的本质就是内存级文件。
- 1.进程之间使用管道通信,必须具有血缘关系。常用于父子关系。
- 2.管道通信只能进行单向通信。
- 3.管道是基于文件的,而文件是随进程的,所以管道的生命周期随进程。
- 4.这个管道文件,没有路径,没有名字,更没有inode,因为使用该管道文件,是由操作系统创建并管理的,而父子进程之间通过该管道进行通信的原因是继承 ,所以该管道就叫做匿名管道。
- 5.父子进程是会进行进程协同,同步与互斥的。我的理解是:父子进程要向管道文件中读写内容,就要调用write和read系统调用,而该函数会进行阻塞地等待或读取。
-
- 由此可知,管道的读写中有4种情况:
-
- 1.读写端正常,如果管道为空,读端就要阻塞。
-
- 2.读写端正常,如果管道被写满了,写端就要阻塞。
-
- 3.读端正常读,写端关闭,读端就会读到0,表明读到了文件结尾,不会被阻塞。
-
- 4.写端正常写,读端关闭,写端不会再写了,没有意义了,因为没人读。
操作系统所做的这一切,本质就是让不同的进程看到同一份资源。
三、管道的接口
该系统接口的参数是一个数组,数组有两个元素,记录的就是打开的管道文件的读端和写端在fd_array中的位置。
所以我们只需要传一个数组过去即可。
如果成功返回0,失败返回-1,且错误码被设置。
所以该参数叫做输出型参数
因为会把用户传进来的参数进行设置修改,所以用户可以再次使用该参数。
使用方法:
cpp
#define SIZE 2
int pipefd[SIZE] = {0};
int n = pipe(pipefd);
这是父进程申请管道文件,父进程需要读取,所以关闭写端
cpp
clode(pipefd[1]);
附带的一个函数:
printf函数我们熟悉,向显示器中打印格式化内容。
snprintf函数是printf函数的变形,本应该向显示器文件中打印的内容,变成向str指针指向的文件中打印size大小的格式化内容。
cpp
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,cnt);
匿名管道的测试代码
cpp
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 2
#define NUM 1024
using namespace std;
// 1.先创建管道文件
// 2.创建子进程
// 3.子进程进行写入,父进程进行读取
//向指定文件描述符对应文件写入
void Write(int wfd)
{
string s = "Hello , i am child";
char buffer[NUM];
//getline(cin,buffer);
pid_t self = getpid();
int cnt = 5;
while(cnt--)
{
buffer[0] = 0; // 告诉读者我的buffer当作字符串来用
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,cnt);
cout << buffer << endl;
write(wfd,buffer,strlen(buffer));
sleep(1);
}
}
void Read(int rfd)
{
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd,buffer,sizeof(buffer));//n是读取到的个数
if(n > 0)
{
buffer[n] = '\0';
cout << "father-" << getpid() << "get a message from child:[" << buffer << "]#" << endl;
}
else if(n == 0)
{
cout << "father read file done!" << endl;
break;
}
else break;
sleep(1);
}
}
int main()
{
int pipefd[SIZE] = {0};
int n = pipe(pipefd);
//成功返回0,失败返回-1
if (n < 0) // 管道创建失败
{
perror("pipefd fail");
return 1;
}
// 管道创建成功
cout << "pipefd[0] : " << pipefd[0] << " pipefd[1] : " << pipefd[1] << endl;
//创建子进程
pid_t id = fork();
if (id < 0)
{
perror("fork fail");
return 2;
}
// child : write
else if (id == 0)
{
//关闭读端
close(pipefd[0]);
//写入
Write(pipefd[1]);
//写入完成关闭写端
close(pipefd[1]);
exit(1);
}
// father : read
close(pipefd[1]);
Read(pipefd[0]);
int status = 0;
pid_t rid = waitpid(id,&status,0); // 阻塞等待
if(rid < 0)
return 3;
else if(rid > 0)
cout << "wait child process success!" << endl;
close(pipefd[0]);
return 0;
}
四、使用管道实现简单的进程池
进程池:一个父进程通过创建多个子进程,然后将不同的任务派发给不同的进程,从而提高工作效率。
相比于接到一个任务后,再创建子进程,然后再将该任务交给子进程去做。
进程池的方法是一次创建多个子进程来待命,只要有任务,就可以立即派发,多个任务也能实现并行。
而父进程与子进程实现通信的方式就是管道通信。
解决进程池的一个小问题
在父进程创建子进程时,子进程会继承父进程的struct files_struct,所以在创建第二个子进程时,由于它继承了父进程的信息,导致第二个子进程有能力去修改父进程与第一个子进程进行通信的管道文件。
所以在父进程不断创建子进程的过程中,子进程的fd_array空间被占用越来越多,意味着后面的子进程能修改前面的管道文件。
解决办法,在父进程创建第二个子进程开始,把该子进程中指向第一个管道文件的写端全部关闭。