匿名管道与命名管道均是进程间通信的方式,属于Linux中比较古老的进程间通信的方式,但是很常用,在命令行上,管道的符号是|
,表示可以把一个进程处理之后的数据以管道的形式传递给另一个进程。
[slowstep@localhost day32]$ echo "aaaaa" | wc -l
1
管道通信是一种单向通信的方式,由一方发送数据,一方接收数据,管道通信的原理是让不同进程能够看到内存中的同一份文件。
当一个进程即以读的方式打开一个文件,又以写的方式打开一个文件时,在内核中对应如下:
一般而言,struct file在磁盘上有与之对应的文件,struct file中含有该文件的inode信息和内核缓冲区信息。
若一个进程以读和写的方式打开一个文件,然后该进程通过fork创建子进程,那么子进程会拷贝一份父进程的文件描述符表,此时父子进程就能看到同一份资源了,在内核中对应如下:
可以用如下代码实现:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); //以写方式打开
int fd2 = open("log.txt", O_RDONLY); //以读方式打开
pid_t id = fork();
if (id == 0)
{
//子进程
//子进程的3号文件描述符对应写端,4号对应读端
close(3); //子进程关闭写端
char buffer[1024] = {0};
while (true)
{
//子进程尝试一直读取内容
ssize_t num = read(4, buffer, sizeof(buffer) - 1);
if (num != 0)
{
buffer[num] = 0;
printf("子进程的pid是%d,ppid是%d,子进程从log.txt中读取到了内容,是%s\n", getpid(), getppid(), buffer);
}
}
cout << "---------------------" << endl;
close(4); //子进程关闭4号文件描述符
exit(0); //子进程读取完毕退出
}
close(4); //父进程关闭读端
//父进程向log.txt中写入数据
int cnt = 0;
int num = 10;
char message[1024] = {0};
while (num--)
{
snprintf(message, 1024, "%s %d", "父进程发送消息", cnt++);
write(3, message, strlen(message));
sleep(1);
}
close(3); //父进程关闭写端
wait(nullptr); //父进程尝试回收子进程
//当父子进程都退出时,系统会回收log.txt的struct file内核数据结构
return 0;
}
不过这种直接使用磁盘上的文件进行进程间通信具有很大的不足:
基于以上原因,操作系统提供了管道文件来专门进行进程间通信,管道文件分为匿名管道和命名管道。
匿名管道在内核中有struct file内核结构,但是在磁盘上没有与之对应的磁盘文件,没有对应的inode,匿名管道是纯内存级的文件,父子进程通过匿名管道进行通信,属于纯内存级的文件操作,不会将数据交互到磁盘,所有通信的中间过程都是在内存中完成的。
创建匿名管道使用系统调用pipe.
#include
#include
int pipe(int pipefd[2]);
参数pipefd[2]
:输出型参数,其中pipefd[0]对应读端的fd,pipefd[1]对应写端的fd.
返回值
:创建匿名管道成功返回0,失败返回-1,并且errno被设置。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int pipefd[2] = {0};
int x = pipe(pipefd); //创建匿名管道,让进程的文件描述符表与匿名管道建立关联
if (x == -1)
{
perror("创建匿名管道失败");
return 0;
}
// pipefd[0]对应读端,pipefd[1]对应写端
int id = fork(); //创建子进程
if (id == 0)
{
//子进程
close(pipefd[1]); //子进程关闭写端,保留读端
char buffer[1024] = {0};
while (true)
{
//子进程持续读取匿名管道中的内容
ssize_t num = read(pipefd[0], buffer, sizeof(buffer) - 1);
//1.如果父进程写端速度较慢,那么子进程会被阻塞,read函数不会返回0
//2.只有当父进程关闭读端而且子进程把匿名管道中的内容全部读取完毕,再次调用read才会返回0
if (num > 0)
{
//子进程通过pipefd[0](读端)读取到了数据
buffer[num] = 0;
cout << "子进程从匿名管道中读取到的内容为" << buffer << endl;
}
else
{
break; //表示已经没有进程会向匿名管道中写入内容了,同时匿名管道中已经没有数据了
}
}
//读取完毕,子进程关闭读端
close(pipefd[0]);
exit(0); //退出进程
}
//父进程
close(pipefd[0]); //父进程关闭读端,保留写端
int count = 0;
int num = 10;
char message[1024] = {0};
while (num--)
{
//父进程通过写端持续向匿名管道写入num条内容
snprintf(message, sizeof(message), "%s %d", "密码", ++count);
write(pipefd[1], message, strlen(message));
sleep(1);
}
close(pipefd[1]); //父进程关闭写端
int ret = waitpid(id, nullptr, 0); // fork给父进程返回子进程的pid,父进程回收子进程
if (ret == -1)
{
cout << "回收子进程失败" << endl;
}
else
{
cout << "成功回收子进程,子进程的pid是" << ret << endl;
}
return 0;
}
管道常用于具有血缘关系的进程之间进行通信,例如父子进程
管道的本质是内核中的缓冲区,操作系统为用户提供一段内核缓冲区,供用户进程进行通信
管道为进程间协同提供了访问控制
缺乏访问控制的例子:如显示器,当父子进程同时调用printf向显示器上打印内容时,是没有先后顺序的,这种行为称为缺乏访问控制
管道具有访问控制的表现(以父子进程为例):
通过管道进行通信的父子进程,当父进程的写入速度慢,而子进程的读取速度快时,子进程会被阻塞,等待父进程写入,此时子进程调用read不会进行返回,当父进程写入完毕,子进程由阻塞状态变为运行状态,调用的read函数返回。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int pipefd[2] = {0};
int x = pipe(pipefd); //创建匿名管道,进程的文件描述符表与匿名管道建立关联
if (x == -1)
{
cout << "创建匿名管道失败" << endl;
return 0;
}
// pipefd[0]对应读端,pipefd[1]对应写端
int id = fork(); //创建子进程
if (id == 0)
{
//子进程
close(pipefd[1]); //子进程关闭写端
char buffer[1024] = {0};
while (true)
{
//子进程非常快速的读取
ssize_t num = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (num > 0)
{
//子进程通过pipefd[0](读端)读取到了数据
buffer[num] = 0;
cout << "子进程从匿名管道中读取到的内容为" << buffer << endl;
}
else
{
break;
}
}
//读取完毕,子进程关闭读端
close(pipefd[0]);
exit(0); //退出进程
}
//父进程
close(pipefd[0]); //父进程关闭读端
int count = 0;
int num=10;
char message[1024] = {0};
while (num--)
{
sleep(5);
//父进程很慢的向管道中写入内容,此时子进程会在调用read的位置阻塞会等父进程向管道中写入内容
snprintf(message, sizeof(message), "%s %d", "密码", ++count);
write(pipefd[1], message, strlen(message));
printf("%d\n", count);
}
close(pipefd[1]); //父进程关闭写端
int ret = waitpid(id, nullptr, 0); // fork给父进程返回子进程的pid,父进程回收子进程
if (ret == -1)
{
cout << "回收子进程失败" << endl;
}
else
{
cout << "成功回收子进程,子进程的pid是" << ret << endl;
}
return 0;
}
当父进程的写入速度快,而子进程的读取速度慢,当父进程把管道写满时,父进程会被阻塞,等待子进程读取管道中的内容,当子进程读取了管道中的内容,管道有空间后,父进程由阻塞态变为运行态,继续向管道中写入内容。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int pipefd[2] = {0};
int x = pipe(pipefd); //创建匿名管道,进程的文件描述符表与匿名管道建立关联
if (x == -1)
{
cout << "创建匿名管道失败" << endl;
return 0;
}
// pipefd[0]对应读端,pipefd[1]对应写端
int id = fork(); //创建子进程
if (id == 0)
{
//子进程
close(pipefd[1]); //子进程关闭写端
char buffer[1024] = {0};
while (true)
{
sleep(10);
//子进程每隔10s读取一次
ssize_t num = read(pipefd[0], buffer, sizeof(buffer) - 1);
//当子进程还没有读取而且管道中的数据已经满了,父进程会等待子进程读取
if (num > 0)
{
//子进程通过pipefd[0](读端)读取到了数据
buffer[num] = 0;
cout << "子进程从匿名管道中读取到的内容为" << buffer << endl;
}
else
{
break;
}
}
//读取完毕,子进程关闭读端
close(pipefd[0]);
exit(0); //退出进程
}
//父进程
close(pipefd[0]); //父进程关闭读端
int count = 0;
int num=10000;
char message[1024] = {0};
while (num--)
{
//父进程快速的向管道中写入内容
snprintf(message, sizeof(message), "%s %d", "密码", ++count);
write(pipefd[1], message, strlen(message));
printf("%d\n",count);
}
close(pipefd[1]); //父进程关闭写端
int ret = waitpid(id, nullptr, 0); // fork给父进程返回子进程的pid,父进程回收子进程
if (ret == -1)
{
cout << "回收子进程失败" << endl;
}
else
{
cout << "成功回收子进程,子进程的pid是" << ret << endl;
}
return 0;
}
管道的访问控制可以体现在父子进程的读写是有规律的,而不是乱序的。
当向管道中写入的数据数据量不大于PIPE_BUF
的时候,linux保证写入的原子性,当向管道中写入的数据量大于PIPE_BUF
时,linux将不在保证写入的原子性。
原子性指的是一件事情要么做,要么不做,只有2种状态,没有中间状态,在多执行流的情况下,数据出现并发访问时,讨论原子性才是有意义的。
在管道通信时如果关闭写方,那么读方在将管道中的内容全部读取完毕以后就可以退出了,当读写双方都退出以后,就没有进程与匿名管道相关联了,此时匿名管道在内存中的资源就可以被操作系统回收了。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int pipefd[2] = {0};
int x = pipe(pipefd); //创建匿名管道,进程的文件描述符表与匿名管道建立关联
if (x == -1)
{
cout << "创建匿名管道失败" << endl;
return 0;
}
// pipefd[0]对应读端,pipefd[1]对应写端
int id = fork(); //创建子进程
if (id == 0)
{
//子进程
close(pipefd[1]); //子进程关闭写端
char buffer[1024] = {0};
while (true)
{
sleep(5);
ssize_t num = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (num > 0)
{
//子进程通过pipefd[0](读端)读取到了数据
buffer[num] = 0;
cout << "子进程从匿名管道中读取到的内容为" << buffer << endl;
}
else
{
break; //读端读到结尾了,停止
}
}
//读取完毕,子进程关闭读端
close(pipefd[0]);
exit(0); //退出进程
}
//父进程
close(pipefd[0]); //父进程关闭读端
int count = 0;
char message[1024] = {0};
while (true)
{
snprintf(message, sizeof(message), "%s %d", "密码", ++count);
write(pipefd[1], message, strlen(message));
printf("%d\n", count);
if (count == 10000)
{
close(pipefd[1]); //父进程关闭写端,此时读端读到文件结尾应该停止了
break;
}
}
int ret = waitpid(id, nullptr, 0); // fork给父进程返回子进程的pid,父进程回收子进程
if (ret == -1)
{
cout << "回收子进程失败" << endl;
}
else
{
cout << "成功回收子进程,子进程的pid是" << ret << endl;
}
return 0;
}
在管道通信时如果关闭读端,而写端依然在向管道中写入内容,那么当写端把管道写满时,由于没有进程来读取管道中的内容,操作系统会给写端进程发送SIGPIPE
信号,终止写端进程。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int pipefd[2] = {0};
int x = pipe(pipefd); //创建匿名管道,进程的文件描述符表与匿名管道建立关联
if (x == -1)
{
cout << "创建匿名管道失败" << endl;
return 0;
}
// pipefd[0]对应读端,pipefd[1]对应写端
int id = fork(); //创建子进程
if (id == 0)
{
//子进程
close(pipefd[1]); //子进程关闭写端
close(pipefd[0]); //子进程也关闭读端
exit(0); //退出进程
}
//父进程
close(pipefd[0]); //父进程关闭读端
int count = 0;
char message[1024] = {0};
while (true)
{
//子进程关闭读端,父进程还持续的向管道中写
snprintf(message, sizeof(message), "%s %d", "密码", ++count);
write(pipefd[1], message, strlen(message));
printf("%d\n", count);
}
printf("-------------------------\n"); //这句话不会被打印出来,父进程持续的向管道中写,但是已经没有读端了,当父进程把管道写满,会被操作系统终止
int ret = waitpid(id, nullptr, 0); // fork给父进程返回子进程的pid,父进程回收子进程
if (ret == -1)
{
cout << "回收子进程失败" << endl;
}
else
{
cout << "成功回收子进程,子进程的pid是" << ret << endl;
}
return 0;
}
管道是基于文件的,而文件的生命周期是随进程的,一个struct file,只要没有进程与之关联,操作系统就会回收它,因此,管道的生命周期也是随进程的,如果管道不关联任何进程,操作系统就可以回收这个管道的内存资源。
管道提供的是面向流式的通信服务,是面向字节流的通信服务,简单理解就是当写端写的很快,但是读端读的很慢时,读端可以一次从管道中读取大量的内容,读端在管道中一次最多可以读取的数据量是写端多次向管道中写入的数据量的总和。
管道是单向通信的,属于半双工通信的一种特殊情况。半双工:指的是要么在读,要么在写;全双工:即在读,也在写。
进程池:一个父进程可以通过fork创建多个子进程,父进程通过进程间通信可以给多个子进程派发任务,这种多进程协同工作就是进程池。
task.h
#pragma once
#include
#include
#include
#include
#include
#include
using TaskFunc = std::function<void()>; //定义函数类型
//任务
void readMySQL()
{
std::cout << "sub process[" << getpid() << " ] 执行访问数据库的任务\n"
<< std::endl;
}
void execuleUrl()
{
std::cout << "sub process[" << getpid() << " ] 执行url解析\n"
<< std::endl;
}
void cal()
{
std::cout << "sub process[" << getpid() << " ] 执行加密任务\n"
<< std::endl;
}
void save()
{
std::cout << "sub process[" << getpid() << " ] 执行数据持久化任务\n"
<< std::endl;
}
processpool.cxx
#include
#include
#include
#include
#include
#include
#include
#include "task.h"
#define CHILDNUM 5
using namespace std;
void SendTask(int fd, int childpid, int tasknum)
{
//通过写端fd向指定pid的子进程派发指定编号的任务,只需要把任务的编号发送过去即可
write(fd, &tasknum, sizeof(tasknum));
printf("父进程已经向pid为%d的子进程发送了任务,任务编号为%d\n", childpid, tasknum);
}
void CreateHashTask(unordered_map<int, TaskFunc> &hashtask)
{
hashtask[0] = readMySQL;
hashtask[1] = execuleUrl;
hashtask[2] = cal;
hashtask[3] = save;
}
int main()
{
vector<pair<int, int>> childtable; //存放子进程的pid和父进程与该子进程进行通信的写端fd
unordered_map<int, TaskFunc> hashtask; //存放编号与任务的映射
vector<int> fatherwrite;
CreateHashTask(hashtask);
for (int i = 0; i < CHILDNUM; i++)
{
//创建5个子进程和5个管道,父进程可以与5个子进程进行通信
int pipefd[2] = {0};
int x = pipe(pipefd);
if (x == -1)
{
cout << "创建管道失败" << endl;
exit(0);
}
//创建一个管道成功
pid_t id = fork();
if (id == -1)
{
cout << "创建子进程失败" << endl;
exit(0);
}
//子进程创建成功
if (id == 0)
{
//子进程关闭写端
close(pipefd[1]);
//父进程会把自己的文件描述符表拷贝给子进程,应该关闭父进程拷贝的写端
for (int i : fatherwrite)
{
close(i);
}
while (true)
{
//子进程一直读取父进程派发的任务并执行
int tasknum = 0;
ssize_t ret = read(3, &tasknum, sizeof(tasknum)); //从管道中读取任务的编号
printf("ret=%d\n", ret);
if (ret)
{
if (tasknum >= 4)
{
cout << "父进程给子进程派发了未知任务" << endl;
}
else
{
printf("pid为%d的子进程执行了父进程发送的编号为%d的任务\n", getpid(), tasknum);
hashtask[tasknum]();
}
}
else
{
printf("pid为%d的子进程没有收到任务,已经退出\n", getpid());
break;
}
}
//这里直接exit即可,因为管道的声明周期是随进程的
exit(0);
}
//只有父进程才会执行到这里
close(pipefd[0]); //父进程关闭读端
fatherwrite.push_back(pipefd[1]); //将父进程写端的文件描述符存到表中
childtable.push_back({pipefd[1], id}); //第一个参数是父进程的写端fd,第二个参数是对应的子进程的pid
}
//到这里父进程创建5个子进程成功
//父进程给子进程派发任务
srand((unsigned int)time(nullptr));
int cnt = 10;
while (cnt--)
{
sleep(1); //父进程每隔1s随机挑选一个子进程并派发任务
int who = rand() % childtable.size(); //随机选一个子进程
SendTask(childtable[who].first, childtable[who].second, rand() % 4); // first表示写端fd,second表示该子进程的pid,最后一个参数是派发几号任务
}
//派发任务完毕,父进程关闭写端
for (const auto &x : childtable)
{
close(x.first);
}
//父进程回收子进程
for (const auto &x : childtable)
{
int ret = waitpid(x.second, nullptr, 0);
if (ret == -1)
{
cout << "回收该子进程失败" << endl;
}
printf("成功回收了pid为%d的子进程\n", ret);
}
return 0;
}
在创建多个管道时,内核中对应情况如下:
因此,可以记录父进程有那些写端文件描述符,然后下一次创建子进程时,由于文件描述符表要进行拷贝,将该子进程拷贝到的父进程的写端文件描述符关闭即可(也可以不处理,因为只有父进程会向管道中写入数据)。
匿名管道是纯内存级的文件,主要用于有血缘关系的进程之间进行通信,如果想要2个毫不相关的进程之间进行通信,可以让它们都打开磁盘上的同一个文件,然后以该文件为中介进行通信。
但是这种方法是没有提供访问控制的,再者,采用该方法2个进程之间的通信就不再是纯内存级别的通信了,它们之间进行交换的数据可能会被写入到磁盘,进程间进行通信时,一旦数据被加载到了磁盘,势必会影响通信的效率。
因此操作系统提供了这样一种管道文件:它没有文件内容,向该管道文件中进行写入数据时,数据不会被加载到磁盘,这样的管道文件称之为命名管道。
通过close接口关闭文件实际上是减少了计数。
例如现在有进程A和进程B都打开一个文件,内核为该文件创建一个struct file,同时会维护一个计数(该计数是由Linux内核进行维护的),记录有多少个进程与该文件有关联。
下图所示就是有2个进程与该文件有关联,计数为2.
当一个进程close关闭文件,若该进程与文件之间已经没有关联,引用计数-1,当计数减为0时,表示该文件已经没有进程与之关联,操作系统可以回收该文件的内核数据结构(struct file).
一般而言当一个文件关联多个进程时,进程通过close关闭一个文件只是减少了计数,并未“真正”关闭该文件。
通过命令行创建命名管道文件使用mkfifo
.
[slowstep@localhost day31]$ mkfifo namedpipe
[slowstep@localhost day31]$ ls
namedpipe pipe Processpool
[slowstep@localhost day31]$ ll
total 0
prw-rw-r--. 1 slowstep slowstep 0 Dec 5 09:55 namedpipe
drwxrwxr-x. 2 slowstep slowstep 75 Dec 4 00:46 pipe
drwxrwxr-x. 3 slowstep slowstep 25 Dec 4 17:10 Processpool
命名管道的文件类型是p
,由于命名管道的作用就是专门用来进行进程间通信的,它是没有文件内容的,因此命名管道无法通过vim或nano等编辑器进行编辑。
如果一个进程向命名管道中进行写入内容,必须要有另外一个进程读取命名管道中的内容,否则写方进程会被阻塞。
[slowstep@localhost day31]$ echo "hello" > namedpipe
#没有人从命名管道中读取内容,写方进程被阻塞
如果一个进程读取管道中的内容,但没有进程向管道中进行写入,那么读端进程也会被阻塞。
[slowstep@localhost pipetest]$ cat < a.pipe
#读端进程被阻塞
man 3 mkfifo
#include
#include
int mkfifo(const char *pathname, mode_t mode);
pathname
:创建的管道文件的路径和名称,实际创建的文件的路径通过进程的工作目录(cwd)与pathname共同决定
mode
:创建的管道文件的权限
返回值
:mkfifo调用成功返回0,失败返回-1,并且errno被设置。
#include
#include
#include
using namespace std;
int main(){
if(mkfifo("mypipe",0666)<0){
perror("创建管道文件失败");
exit(1);
}
cout<<"创建管道文件成功"<<endl;
return 0;
}
当只有一方向命名管道中写入数据,没有进程读取数据,写方进程被阻塞
#include
#include
#include
#include
#include
#include
using namespace std;
int main(){
if(mkfifo("mypipe",0666)<0){
perror("创建管道文件失败");
exit(1);
}
cout<<"创建管道文件成功"<<endl;
int fd=open("mypipe",O_WRONLY);
if(fd==-1){
perror("打开管道文件失败");
exit(2);
}
//打开管道文件成功,向管道文件中写入
write(fd,"hello world",strlen("hello world"));
//如果可以走到这里。表示成功向管道文件中写入了数据
cout<<"成功向管道文件中写入数据!"<<endl;
return 0;
}
[slowstep@localhost namedpipe]$ ./a.out
创建管道文件成功
#没有打印出来"成功向管道文件中写入数据",因为没有进程读取管道中的内容,因此,写方进程被阻塞
unlink
man 3 unlink
#include
int unlink(const char *path);
参数
:要删除的管道文件的路径
返回值
:调用成功返回0,失败返回-1,并且errno被设置。
unlink实际上是删除了文件的计数。
Serve(服务端)
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
if (mkfifo("mypipe", 0666) < 0)
{
perror("创建管道文件失败");
exit(1);
}
cout << "创建管道文件成功" << endl;
int fd = open("mypipe", O_WRONLY);
if (fd == -1)
{
perror("打开管道文件失败");
exit(2);
}
//打开管道文件成功,向管道文件中写入
write(fd, "hello world", strlen("hello world"));
//如果可以走到这里。表示成功向管道文件中写入了数据
close(fd);
cout << "成功向管道文件中写入数据!" << endl;
unlink("./mypipe");//进程间通过管道通信完成以后,管道文件基本没有意义了,应该删除
return 0;
}
Client(客户端)
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int fd = open("mypipe", O_RDONLY);
if (fd == -1)
{
perror("打开管道文件失败");
exit(1);
}
char buffer[1024] = {0};
read(fd, buffer, sizeof(buffer) - 1);
cout << buffer << endl;
close(fd);
return 0;
}
进程之间通过命名管道进行通信,其方法和操作普通文件一样,管道是基于内存文件的ipc通信方案。