一、通信的概念
很容易理解就是两个进程可以相互进行数据的输入输出,对一块资源的共同使用,或者是一个进程让另外一个进程执行不同任务等都是通信的目的
我们知道,进程是有独立性的,那肯定不可能是在自己的进程内部的资源,因为其它进程看不到,所以一定是有一块公共空间,能让不同进程进行通信
二、管道通信
就是我们以前命令用过的 比如 ls | head -1 的竖划线就是管道,为什么叫管道?因为管道通信都是单向通信,一个进程只能写入数据,另外一个只能读数据像我们现实中管道一样,一边工厂给你输入水,你自己用水龙头使用。
2.1匿名管道
特点:严格的单向通信(一个进程只做输入或输出)
管道结构:它其实是一个内存级的file文件,也就是将来的数据不会刷新到磁盘上。file文件中的内核缓冲区就是管道,它与普通文件的区别就是它没有名字(也就是没有路径),其他的inode、fd等结构都有
也就是说这个file也能存在在我们进程的文件描述符数组里面,所以说,管道的创建思想是借用了文件创建的思想也就是复用内核的结构
父子进程之间的通信
我们知道,父子进程保持独立性的原因主要是一旦在代码中修改,就会发生写实拷贝,父子进程在物理内存的代码各一份,所以缺点很明显:数据只能提前搞好,一旦修改以后资源就不同
但管道通信还真是只能父子之间的,那是如何做到的呢?
所以父子进程就能共用一块资源做到通信,
如何代码实现通信呢?
那首先我们得知道,一个管道文件是被进程的两个fd指向的
那肯定要系统调用创建管道-pipe(int pipefd ),需要传一个两个元素的数组,其中pipe创建管道后 fd0对应读端,放到fd1对应写端,为什么要这样,不是说单向通信吗?因为你不知道是将来的子进程做输入还是做输出,并且将来你自己是做输入输出,所以创建的子进程会拷贝父进程,那它的文件描述符数组是和父进程一样的,它也有两个fd指向管道分别为读端和写端,将来谁要做写端,那就把另外一个进程的写端给close关闭调,然后自己的读端close掉,不就是单向通信么
下面是代码的小demo:
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd); // 创建管道
if (n < 0)
{
perror("pipe");
}
printf("pipefd[0]: %d,pipefd[1]:%d\n", pipefd[0], pipefd[1]); // 0为读,1为写
int id = fork(); // 创建子进程
if (id == 0)
{
// child
close(pipefd[0]);
char outbuff[1024];
int cnt = 5;
char *msg = "hello pipe!";
while (cnt)
{
snprintf(outbuff, sizeof(outbuff), "%s %d %d\n", msg, cnt--, getpid()); // snprintf会自动在末尾加\0
write(pipefd[1], outbuff, strlen(outbuff));
// close(pipefd[1]);
// break;
sleep(1);
}
printf("write pip over\n");
exit(0);
}
close(pipefd[1]);
char inbuff[1024];
while (1)
{
// inbuff[0]=0;
ssize_t n = read(pipefd[0], inbuff, sizeof(inbuff) - 1); // 减一是给\0留位置
if (n > 0)
{
inbuff[n] = '\0';
printf("%s", inbuff);
close(pipefd[0]);
}
// printf("%s:%ld\n",inbuff,n);
// sleep(1);
else if (n == 0)
{
printf("read over");
break;
}
else
{
perror("read");
break;
}
}
// close(pipefd[0]);
int status = 0;
int rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("%d,exit_code;%d,signal_code;%d\n",rid,WEXITSTATUS(status),status & 0x7f);
}
return 0;
}
下面是总结的匿名管道的特性与情况
特性 :
1、管道是单向通信的
2、匿名管道是具有"血缘关系"的进程,常用父子通信(继承内核资源)
3、管道随进程结束而结束(本质就是判断管道的进程引用引用计数)
4、管道是面向字节流的(也就是管道里的数据不一定是字符,图片、音频等都有可能)
字节流的说明:也就是你往管道里写多次数据,但输出的数据不一定是全部的数据,它是按需输出,就类似于语言级缓冲区数据刷新进内核级缓冲区的特性
5、管道通信对于多进程而言,是有同步和互斥的关系同步:就是访问数据具有一定的顺序性,也就是访问数据需要排队(这只是简单的理解)
互斥:任何时刻只允许一个进程访问资源情况(父进程为写端,子进程为读端):
1、父进程写的慢,子进程就要阻塞(管道为空,读就要阻塞),等管道有数据才读
2、父进程写的快,管道为满时,父进程要阻塞,子进程要读管道里的数据
3、读端在读,写端关闭,读端把管道数据读完后,read=0说明读到了管道文件末尾
4、读端关闭,OS就会把写端进程给杀掉(避免浪费资源)
三、命名管道
命名管道有自己的路径,跟普通文件不同的是它不做刷新内核缓冲区给磁盘,它的特性与匿名管道几乎差不多,除了两个进程通信是毫无相关的进程,无血缘关系的
实现命名管道的编写
创建命名管道
系统调用函数:mkfifo(char* path,mode)命令创建:mkfifo 文件名
c++创建命名管道:
框架如下:
cpp
#define MODE 0666
#define TO_WRITE 1
#define TO_READ 2
class named_pipe
{
public:
named_pipe(std::string pfile_path = "./fifo")
: _pfile_path(pfile_path)
{
}
~named_pipe() {}
private:
std::string _pfile_path;
pid_t _fd = -1;
};
创建管道文件的路径可以随意给我这里就是当前路径
1.创建管道stat函数是判断你给的这个文件是否已经存在,struct stat结构体是会返回里面的属性,你需要可以通过这个结构体拿到
这里有个坑是不能用open函数用只读的方式打开文件看返回值的,因为对管道文件很特殊如果文件存在它会一直卡在这里,直到有进程以写的方式打开了管道文件才会往后走,所以如果这里用open会有bug
cpp
void Build_pipe()
{
if (IF_EXIST())
{
std::cout << "命名管道已存在,不需要再次创建!!!" << std::endl;
return;
}
int n = mkfifo(_pfile_path.c_str(), MODE);
if (n < 0)
{
std::cerr << "mkfifo errno:" << errno << strerror(errno) << std::endl;
}
std::cout << "命名管道创建成功!!!" << std::endl;
}
bool IF_EXIST()
{
// 能拿到文件的属性
struct stat st;
int n = stat(_pfile_path.c_str(), &st);
if (!n)
return true;
else
return false;
}
2.让进程拿到不同的权限
cpp
void Get_Power(int mode)
{
if (mode == TO_WRITE)
{
_fd = open(_pfile_path.c_str(), O_WRONLY);
if(_fd < 0)
{
std::cout<< "open write end failure!!!" << std::endl;
}
}
else if (mode == TO_READ)
{
_fd = open(_pfile_path.c_str(), O_RDONLY);
if(_fd < 0)
std::cout<< "open read end failure!!!" << std::endl;
}
else
{
std::cout << "函数传参错误" << std::endl;
}
//下面是测试如果为管道文件,使用open打开文件时,不打开写端,open函数会一直堵塞
if(_fd > 0)
{
std::cout << "open over" << std::endl;
}
}
3.删除管道文件
cpp
void Delete_namedfile()
{
if(!(IF_EXIST()))
return;
int n = unlink(_pfile_path.c_str());
std::cout << "删除管道文件成功!!!" << std::endl;
}
后面就可以创建两个进程来看了,这里就展示进程server.cpp和client.cpp的代码来模拟客户端向服务端发送消息
cpp
#include "named_pipe.hpp"
//server.cpp
int main()
{
named_pipe np;
np.Build_pipe();
np.Get_Power(TO_READ);
std::string msg;
while(true)
{
char outbuff[128];
ssize_t n = read(np.get_fd(),outbuff,sizeof(outbuff)-1);
if(n > 0)
{
outbuff[n]= '\0';
std::cout << outbuff << std::endl;
}
if(n==0)
{
//客户端关闭了,说明我也要结束了
std::cout << "读取客户数据完毕!!!" << std::endl;
break;
}
else
{
std::cerr << "read erro" << std::endl;
}
}
np.Delete_namedfile();
return 0;
}
cpp
#include "named_pipe.hpp"
//client.cpp
int main()
{
named_pipe np;
np.Get_Power(TO_WRITE);
while(true)
{
std::string msg;
getline(std::cin,msg);
ssize_t n = write(np.get_fd(),msg.c_str(),msg.size());
if(n > 0)
{
std::cout << "当前客户端向服务端输入" << n << "个字节" << std::endl;
}
}
return 0;
}






