一个进程在创建时,会默认打开三个文件,分别是: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)
管道的本质就是内存级文件。
操作系统所做的这一切,本质就是让不同的进程看到同一份资源。
该系统接口的参数是一个数组,数组有两个元素,记录的就是打开的管道文件的读端和写端在fd_array中的位置。
所以我们只需要传一个数组过去即可。
如果成功返回0,失败返回-1,且错误码被设置。
所以该参数叫做输出型参数
因为会把用户传进来的参数进行设置修改,所以用户可以再次使用该参数。
使用方法:
#define SIZE 2
int pipefd[SIZE] = {0};
int n = pipe(pipefd);
这是父进程申请管道文件,父进程需要读取,所以关闭写端
clode(pipefd[1]);
附带的一个函数:
printf函数我们熟悉,向显示器中打印格式化内容。
snprintf函数是printf函数的变形,本应该向显示器文件中打印的内容,变成向str指针指向的文件中打印size大小的格式化内容。
snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,cnt);
匿名管道的测试代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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空间被占用越来越多,意味着后面的子进程能修改前面的管道文件。
解决办法,在父进程创建第二个子进程开始,把该子进程中指向第一个管道文件的写端全部关闭。