什么是管道
管道是最简单的进程通信机制之一。它允许一个进程的输出直接成为另一个进程的输入。管道分为命名管道和命名管道(FIFO),匿名管道通常用于父子进程间通信,而命名管道也可以在非相关进程间通信。
管道的4种情况
- 正常情况(读写端正常)若管道中没有数据了,则读端必须等待,直到有数据为止
- 正常情况(读写端正常)若管道中被写满了,则写端必须等待,直到有空间为止,即数据被读走
- 写断关闭,读端一直读取,直到read返回值为0,即表示读到管道末尾
- 读端关闭,写端一直写入,此时写端的进程会直接被OS杀掉,因为写下去也就没有什么意义了,os通过向目标进程发送SIGPIPE(13)信号,终止目标进程
管道的5中特性
- 匿名管道:允许有血缘关系的进程之间进行进程通信 , 常用于父子进程 , 仅限于此
- 匿名管道,默认给读写端提供 同步机制(两个进程执行时具有一定的顺序性)
- 面相字节流,即无论写的时候是按多大写,按什么数据写, 都不影响读端想读多少就读多少
- 管道的生命周期是随进程的,进程结束,管道自动被操作系统回收
- 管道是单向通信的,半双工通信的一种特殊情况
匿名管道
含义
匿名管道通常通过 pipe() 系统调用创建,它返回两个文件描述符,一个用于读取管道数据,另一个用于写入管道数据。
然后,进程可以使用 read() 和 write() 等系统调用来实现数据的读取和写
特点
匿名管道有以下特点:
- 单向通信:匿名管道是单向的,即数据只能从一个进程流向另一个进程,不能反向传输数据。
- 半双工:匿名管道是半双工的,即数据只能在一个方向上进行传输,不能同时进行输入和输出。
- 相关进程间通信:匿名管道通常用于父子进程或兄弟进程之间的通信,因为它们是通过 fork() 系统调用创建的进程共享同一个文件描述符。
- 存储有限:匿名管道的数据传输是基于内存的,它的缓冲区是有限的,因此传输大量数据时需要注意防止缓冲区溢出。
代码示例
#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX 1024
using namespace std;
int main()
{
// 1. 建立管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n; // 防止编译器报警
cout << "pipefd[0]:" << pipefd[0] << " " << "pipefd[1]" << pipefd[1] << endl;
// 2. 创建子进程
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
// 3. 父子关闭不需要的读写fd,形成单向通信通道
if (id == 0)
{
// child
// 让子进程写入,关闭读端
close(pipefd[0]);
// 只向管道中写入
int cnt = 10;
while(cnt--)
{
//创建一个字符串池
char message[MAX];
snprintf(message,sizeof(message)-1, "hello father , i am child . pid : %d, cnt: %d \n" , getpid(), cnt);
//将信息写入管道:
write(pipefd[1],message , strlen(message));
//sleep(1);
}
cout<<"write over , child close w"<<endl;
close(pipefd[1]);
exit(0);
}
// parent
// 让父进程读取,关闭写端
close(pipefd[1]);
//接下来父进程开始读取管道中的内容:
char buffer[MAX];
while(true)
{
ssize_t n = read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n] = 0;
cout<<getpid()<<": child say:"<< buffer <<"to me"<<endl;
}
else if(n == 0)
{
cout<<"child quit , me too!"<<endl;
break;
}
}
pid_t rid = waitpid(id, nullptr, 0);
if (rid == id)
{
cout << "wait success" << endl;
}
return 0;
}
命名管道
含义
在使用命名管道时,需要使用 mkfifo 命令来创建一个命名管道文件,然后可以像操作普通文件一样在进程间进行通信。
进程可以通过打开这个文件来实现数据的读取和写入。命名管道通常用于需要多个进程之间进行通信的场景,比如数据传输、进程协作等。
特点
命名管道有以下特点:
- 基于文件系统:命名管道以文件的形式存在于文件系统中,通常位于 /tmp 或其他指定目录下。
- 全双工:命名管道是全双工的,允许同时进行读取和写入操作。
- 独立于进程:命名管道可以被多个进程访问,不像匿名管道那样限制在相关进程之间。
- 持久性:命名管道在创建后可以持续存在,直到被显式删除。
代码示例
这个例子中,有两个可执行程序 client和server
这两个程序通过创建一个fifo文件,通过这个文件来进行通信
Makefile
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f server client fifo
server.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
#include "comm.h"
bool Makefifo()
{
int n = mkfifo(FILENAME,0666);
if(n < 0)
{
std::cerr << "errno" << errno << ",errstring" << strerror(errno) << std::endl;
return false;
}
return true;
}
int main()
{
Start:
int rfd = open(FILENAME,O_RDONLY);
if(rfd < 0)
{
std::cerr << "errno" << errno << ",errstring" << strerror(errno) << std::endl;
if(Makefifo()) goto Start;
else return 1;
}
char buffer[1024];
while(true)
{
ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
std::cout<<"client say:"<< buffer << std::endl;
}
else if(s == 0)
{
std::cout<<"client quit, server quit,too..."<<std::endl;
break;
}
}
close(rfd);
return 0;
}
client.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <unistd.h>
#include <cstring>
#include <fcntl.h>
#include "comm.h"
int main()
{
int wfd = open(FILENAME,O_WRONLY);
if(wfd < 0)
{
std::cerr << "errno" << errno << ",errstring" << strerror(errno) << std::endl;
return 1;
}
std::string message;
while(true)
{
std::cout<<"cin>>"<<std::endl;
std::getline(std::cin , message);
ssize_t s = write(wfd,message.c_str(),message.size());
if(s < 0)
{
std::cerr << "errno" << errno << ",errstring" << strerror(errno) << std::endl;
break;
}
}
close(wfd);
return 0;
}