进程间通信
- 一.基本概念
- 二.简单的通信-管道(匿名管道)
- 1.建立通信信道
- 2.通信接口
- 三.命名管道
- 三.模拟命名管道通信(加上日志)
- 1.完整代码
- 2.基本使用
一.基本概念
是什么
两个或多个进程实现数据层面的交互。
因为进程独立性的存在,导致进程间的通信成本比较高。
为什么
因为我们有多进程协同的需求。
怎么办
a.进程间通信的本质:必须让不同的进程看到同一份"资源"。
b.“资源”?特定形式的内存空间。
c.这个"资源"谁提供?一般是操作系统。
d.我们进程访问这个空间,进行通信,本质就是访问操作系统!进程代表的就是用户,“资源”从创建,使用(一般),释放―–需要系统调用接口!一般操作系统会有一个独立的通信模块-隶属于文件系统-IPC通信模块。
二.简单的通信-管道(匿名管道)
管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
1.建立通信信道
管道是一个文件-内存级文件。
1.管道文件与一般文件不同,一般文件存在于磁盘里,管道文件存在于内存里。也就是说管道文件不需要将修改内容从内存刷新缓冲区到磁盘,这是它的特点。
2.一般管道文件只能具有血缘关系的进程间通信。因为只有具有血缘关系才能继承同一份files_struct。
3.一个父进程在创建管道文件时不能只是以读或者写的方式,必须两者都有。操作系统会把这个文件打开两次,分别用来读和写。但操作系统实际上只想让两个进程进行单向通信,因为如果一个进程又在读又在写,很容易会造成数据混淆,为了避免麻烦,规定只能一个进程写,另一个进程读。
这个文件不需要有名字,inode…让操作系统区分。所以这种文件也被称为匿名管道。
2.通信接口
pipe的作用就是帮助我们以读和写打开文件。它的参数是一个输出型参数,它会把分别以读和写的文件的文件描述符通过参数带出,供用户使用。pipefd[0]一般用于读,pipefd[1]一般用于写。
模拟
makefile
testPipe:TestPipe.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f testPipe
TestPipe.cpp(一个简单的通信,子进程向父进程里写信息)
#include<stdio.h>
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<string>
#include<cstdio>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
#define N 2
#define NUM 1024
//子进程
void Write(int wfd)
{
//任意写几个数据测试
string s="hello,i am a child";
pid_t id=getpid();
int num=0;
char buffer[NUM];
while(1)
{
snprintf(buffer,sizeof(buffer),"%s-%d-%d\n",s.c_str(),id,num++);//将数据都变成字符存在buffer里
//把数据写入管道
write(wfd,buffer,strlen(buffer));
sleep(1);
}
}
//父进程
void Read(int rfd)
{
char buffer[NUM]={0};
while(1)
{
ssize_t n=read(rfd,buffer,sizeof(buffer));
if(n>0)
{
cout<<buffer<<endl;
}
else if(n==0) break;
}
}
int main()
{
int pipefd[N]={0};
//创建管道
int n=pipe(pipefd);
//判断是否创建成功
if(n<0) return 1;
//创建子进程
pid_t id=fork();
if(id<0) return 2;
if(id==0)
{
//子进程
//关闭读功能
close(pipefd[0]);
//IPC code
Write(pipefd[1]);
//退出
close(pipefd[1]);
exit(0);
}
//父进程
//关闭写功能
close(pipefd[1]);
//IPC code
Read(pipefd[0]);
//退出
//回收子进程
pid_t rid=waitpid(id,nullptr,0);
if(rid<0) return 3;
close(pipefd[0]);
return 0;
}
管道的4中情况:
1.读写端正常,管道如果为空,读端就要阻塞 2读写端正常,管道如果被写满,写端就要阻塞 3.读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
4,写端正常写入,读端被关闭。操作系统就要杀掉正在写入的进程。如何干掉?通过信号杀掉。
管道的特征:
1.具有血缘关系的进程进行进程间通信。
2.管道只能单向通信。
3.父子进程是会进程协同的,同步与互斥的—保护管道文件的数据安全 4.管道是面向字节流的。
5.管道是基于文件的,而文件的生命周期是随进程的!
三.命名管道
很明显上面的匿名管道只使用于具有血缘关系的通信是远远不够的,为了解决这个问题,又有了命名管道的概念。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件。
makefile
创建一个命名管道命名为myfifo
三.模拟命名管道通信(加上日志)
1.完整代码
日志(log.hpp)
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
//初始化打印方式和路径
printMethod=Screen;
path="./log/";
}
~Log()
{}
//改变打印方式
void Enable(int methed)
{
printMethod=methed;
}
//把整形转换成字符串
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname=path+logname;//连接文件路径
int fd=open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);//打开文件
if (fd < 0) return;
//向文件里写入
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level,const std::string &logtxt)
{
std::string filename=LogFile+'.'+levelToString(level);//拼接路径
printOneFile(filename,logtxt);
}
void printLog(int level,const std::string &logtxt)
{
//选择打印方式
switch (printMethod)
{
case Screen://向屏幕打印
std::cout << logtxt << std::endl;
break;
case Onefile://向一个文件里打印
printOneFile(LogFile, logtxt);
break;
case Classfile://向多个文件里打印
printClassFile(level, logtxt);
break;
default:
break;
}
}
//重载括号,让其能像log s; s(...)一样使用
void operator()(int level,const char*format, ...)
{
//首先将时间加入日志
time_t t=time(nullptr);
struct tm*ctime=localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
//接着将用户输入部分加入
//处理可变参数
va_list s;
va_start(s,format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
//将两个部分合并
char logtxt[2*SIZE];
snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);
//打印到对应位置
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
创建管道(comm.hpp)
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1,
FIFO_DELETE_ERR,
FIFO_OPEN_ERR
};
class Init
{
public:
Init()
{
// 创建管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
管道通信读取方(server.cc)
#include "comm.hpp"
#include "log.hpp"
using namespace std;
// 管理管道文件
int main()
{
Init init;
Log log;
// log.Enable(Onefile);
log.Enable(Onefile);
// 打开管道
int fd = open(FIFO_FILE, O_RDONLY); // 等待写入方打开之后,自己才会打开文件,向后执行, open 阻塞了!
if (fd < 0)
{
log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Warning, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Fatal, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Debug, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
// 开始通信
while (true)
{
char buffer[1024] = {0};
int x = read(fd, buffer, sizeof(buffer));
if (x > 0)
{
buffer[x] = 0;
cout << "client say# " << buffer << endl;
}
else if (x == 0)
{
log(Debug, "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno);
break;
}
else
break;
}
close(fd);
return 0;
}
管道通信写入方(client.cc)
#include <iostream>
#include "comm.hpp"
using namespace std;
int main()
{
int fd = open(FIFO_FILE, O_WRONLY);
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "client open file done" << endl;
string line;
while(true)
{
cout << "Please Enter@ ";
getline(cin, line);
write(fd, line.c_str(), line.size());
}
close(fd);
return 0;
}
makefile
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -g -std=c++11
mkdir log
client:client.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f server client
rm -r log
2.基本使用
上面代码是向log.txt文件里写入。我们首先运行读文件,再运行写文件。任意通信,然后关闭进程,再打开log.txt,就可以看到日志已经写入。