单进程,无法使用并发能力,更加无法实现多进程协同,如传输数据、同步执行流、消息通知等;
(1)进程是具有独立性的;虚拟地址空间 + 页表保证进程运行的独立性(进程内核数据结构 + 进程的代码和数据);
(2)通信成本会比较高;
(1)进程间通信的前提,首先是要让不同的进程看到同一块内存空间(特定的结构组织的);
(2)所谓的看到同一块空间,这块空间应该隶属于哪一个进程呢?应该不能隶属于任何进程,而应该强调共享;
管道通信是进程之间通过管道进行通信,管道就是两个进程能够共享的一块内存空间;
管道的本质就是一个文件:
(1)父进程分别以读写的方式打开一个文件;
完成后,父进程中分别由两个文件描述符对应的文件指针指向同一个文件,一个用来读,一个用来写,这个文件就是管道;
(2)fork创建子进程;
在父进程fork出子进程后,子进程会拷贝父进程PCB的信息,因此在子进程的文件序列中,也会有同样的文件描述符对应的文件指针指向父进程创建的管道;
(3)双方进程各关闭自己不需要的文件描述符;
在确定好父子进程的读写后,比如父进程写,子进程读,那就关闭父进程读和子进程写对应的fd,到此,一个管道就形成了;
注:
(1)创建子进程时,只拷贝和进程相关的数据,PCB,文件相关的不会拷贝,拷贝完成后,父子进程指向的文件是一样的;
(2)进程间通信都是基于内存的,效率高;
(3)我们在命令行使用的 | 就是管道;
三个sleep都是进程, 是兄弟进程,一个进程处理完数据,通过管道交给下一个进程;
pipe函数:创建管道,相当于完成了父进程以读写方式打开一个文件;
参数:
pipefd[2]:输出型参数,期望通过这个参数,得到被打开文件的fd(读和写各一个fd),pipefd[0]是读端,pipefd[1]是写端;
返回值:
成功返回0,失败返回-1;
makefile:
pipe-use:pipe-use.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f pipe-use
#include
#include
#include //在c++中更好兼容c语言的头文件
#include
#include
#include
#include
#include
using namespace std;
int main()
{
//1.创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void)n; //debug模式下assert是有效的,而release模式下assert就无效了
//(void)n就是将n使用以下,避免release模式下报错
//条件编译,打印出pipefd中的内容
//如果想要执行这段代码,在g++编译选项中加上-DEGUB即可
#ifdef DEBUG
cout << "pipefd[0]" << pipefd[0] << endl;
cout << "pipefd[1]" << pipefd[1] << endl;
#endif
//2.创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程 - 读
//3.构建单向通信的管道,父进程写入,子进程读取
//3.1 关闭子进程不需要的fd
close(pipefd[1]);
char buffer[1024];
while(true)
{
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);//从管道中读取数据
if(s > 0)
{
buffer[s] = 0;
cout << "child get a message[" << getpid() << "]father# " << buffer << endl;
}
}
close(pipefd[0]);//关闭子进程读取fd。可以不关闭,因为子进程退出时会关闭其所有fd
exit(0);
}
//父进程
//3.构建单向通信的管道,父进程写入,子进程读取
//3.1关闭父进程不需要的fd
close(pipefd[0]);
string message = "我是父进程,我正在给你发消息";
int count = 0;
char send_buffer[1024];
while(true)
{
//3.2构建一个变化的字符串
//sprintf是向字符串中格式化显示内容
snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
message.c_str(), getpid(), count++);
//3.3写入
write(pipefd[1], send_buffer, sizeof(send_buffer));
//3.4sleep
sleep(1);
}
pid_t ret = waitpid(id, nullptr, 0);
assert(ret < 0);
(void)ret;
close(pipefd[1]);
return 0;
}
运行结果:
我们可以看到,在父进程将管道写满后,就阻塞在这里了,等待子进程的读取;
当子进程读取了一定的数据后,父进程才能继续写入;
总结:
a. 写快,读慢,写满管道后就不能再写了;
b. 写慢,读快,管道没有数据的时候,读必须等待;
c. 写关,读就会返回0,标识读到了文件的结尾;
d. 读关,写继续写,OS会终止进程;
父进程创建四个子进程,使用四个管道进行进程间通信,为子进程派发任务,单机版的负载均衡;
process-pool:process-pool.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f process-pool
#include
#include
#include
#include
#include
#include
#include
#include
#include"Task.hpp"
#define PROCESS_NUM 5 //子进程数量
using namespace std;
int waitCommand(int waitFd, bool &quit) //如果对方不发,我们就阻塞
{
uint32_t command = 0;
ssize_t s = read(waitFd, &command, sizeof(command));
if (s == 0)
{
quit = true;
return -1;
}
assert(s == sizeof(uint32_t));
return command;
}
void sendAndWakeup(pid_t who, int fd, uint32_t command)
{
write(fd, &command, sizeof(command));
cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}
int main()
{
//加载方法
load();
//保存管道信息
vector<pair<pid_t, int>> slots; //pid : pipefd
//先创建多个进程
for(int i = 0; i < PROCESS_NUM; i++)
{
//创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void)n;
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//child
//子进程读取
close(pipefd[1]);
while(true)
{
//等命令
bool quit = false;//进程退出判断
int command = waitCommand(pipefd[0], quit);//如果对方不发,我们就阻塞
if(quit)//如果父进程停止写入,子进程也停止
{
break;
}
//执行对应的命令
if(command >= 0 && command < handlerSize())
{
callbacks[command];
}
else
{
cout << "非法command" << command << endl;
}
}
exit(1);
}
// father
// 父进程写入
close(pipefd[0]);
slots.push_back(make_pair(id, pipefd[1])); // 保存管道信息
}
// 父进程派发任务(均衡的派发给每一个子进程)
srand((unsigned long)time(nullptr) ^ getpid() ^ 23323123123L); // 让数据源更随机
while (true)
{
int slect;
int command;//任务编号
cout << "##########################################" << endl;
cout << "## 1.show functions 2.send command ##" << endl;
cout << "##########################################" << endl;
cout << "Please Slect> ";
cin >> slect;
if (slect == 1)
{
showHandler();
}
else if (slect == 2)
{
cout << "Enter Your Command> ";
// 选择任务
cin >> command;
// 选择进程
int choice = rand() % slots.size();
// 把任务发送给指定的进程
sendAndWakeup(slots[choice].first, slots[choice].second, command);
}
else
{
}
//关闭fd,所有的子进程都会退出
for(const auto &slot : slots)
{
close(slot.second);
}
//回收所有的子进程信息
for(const auto &slot : slots)
{
waitpid(slot.first, nullptr, 0);
}
}
return 0;
}
#include
#include
#include
#include
#include
typedef std::function<void()> func; //定义函数类型,实现函数回调
std::vector<func> callbacks;//存储回调函数
std::unordered_map<int, std::string> desc;
void readMySQL()
{
std::cout << "process[" << getpid() << "]执行访问数据库的任务" << std::endl;
}
void executeUrl()
{
std::cout << "process[" << getpid() << "]执行解析Url" << std::endl;
}
void cal()
{
std::cout << "process[" << getpid() << "]执行加密任务" << std::endl;
}
void save()
{
std::cout << "process[" << getpid() << "]执行数据持久化任务" << std::endl;
}
//加载方法和对应的描述
void load()
{
desc.insert(callbacks.size(), "readMySQL: 读取数据库");
callbacks.push_back(readMySQL);
desc.insert(callbacks.size(), "executeUrl: 进行url解析");
callbacks.push_back(executeUrl);
desc.insert(callbacks.size(), "cal: 进行加密计算");
callbacks.push_back(cal);
desc.insert(callbacks.size(), "save: 进行文件保存");
callbacks.push_back(save);
}
void showHandler()
{
for(const auto &iter : desc)
{
std::cout << iter.first << "\t" << iter.second << std::endl;
}
}
int handlerSize()
{
return callbacks.size();
}
匿名管道是基于创建子进程的进程信息拷贝来通信的,只能用于有血缘关系的进程间通信;
而命名管道可以实现毫不相关的进程之间的通信;
不同的进程打开同一个文件,操作系统会检测文件路径,不会再将文件内容加载到内存中,而是将进程指向同一个文件结构体;
这就是命名管道,这是一种内存级文件,但是在磁盘中构建了一个文件名,在进程访问的时候访问同一路径下的文件名;
命名管道只是在磁盘中建立了一个符号,只是为了通信双方看到同一份资源,其数据都是内存级的,不会向磁盘中写入数据;
mkfifo:使用系统接口创建命名管道:
参数:
pathname:指定路径;
mode:指定管道文件的权限;
返回值:成功返回0;失败返回-1;
.PHONY:all
all:mutiServer client
mutiServer:mutiServer.cpp
g++ -o $@ $^ -std=c++11
client:client.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
claen:
rm -f mutiServer client
#ifndef _COMM_H_
#define _COMM_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
using namespace std;
#define MODE 0666 //管道文件的权限
#define SIZE 128
string ipcPath = "./fifo.ipc";//管道文件的路径
#endif
#ifndef _LOG_H_
#define _LOG_H_
#include
#include
#define DeBug 0
#define Notice 1
#define Waring 2
#define Error 3
const std::string msg[] = {
"DeBug",
"Notice",
"Waring",
"Error"
};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
#endif
这是打印日志的头文件;
#include "comm.hpp"
int main()
{
// 1.创建管道文件
if (mkfifo(ipcPath.c_str(), MODE) < 0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", DeBug) << "step 1" << endl;
// 2.正常的文件操作
int fd = open(ipcPath.c_str(), O_RDONLY); // 服务端读取信息
if (fd < 0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功", DeBug) << "step 2" << endl;
// 3.编写正常的通信代码
char buffer[SIZE];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(fd, buffer, sizeof(buffer));
if (s > 0)
{
cout << '[' << getpid() << "] client say: " << buffer << endl;
}
else if (s == 0)
{
cerr << '[' << getpid() << "] read end of file, client quit, server quit" << endl;
break;
}
else
{
perror("read");
break;
}
}
// 4.关闭文件
close(fd); // 关闭管道文件
Log("关闭管道文件成功", DeBug) << "step 3" << endl;
unlink(ipcPath.c_str()); // 删除管道文件
Log("删除管道文件成功", DeBug) << "step 4" << endl;
return 0;
}
这条代码执行后会创建命名管道文件;
ulink是删除文件的系统调用接口;
在关闭管道文件后直接删除;
#include "comm.hpp"
int main()
{
//客户端不需要创建管道了
//1.打开管道文件
int fd = open(ipcPath.c_str(), O_WRONLY);
if(fd < 0)
{
perror("open");
exit(1);
}
//2.通信操作
string buffer;
while(true)
{
cout << "Please Enter Command >";
getline(cin, buffer);
write(fd, buffer.c_str(), buffer.size());
}
//3.关闭管道文件
close(fd);
return 0;
}
客户端就不要创建管道文件,直接获取,然后打开,向管道中写信息;
如果在服务端创建多个子进程来处理客户端请求:
#include "comm.hpp"
void getMessage(int fd)
{
char buffer[SIZE];
while (true)
{
memset(buffer, '\0', sizeof(buffer));
ssize_t s = read(fd, buffer, sizeof(buffer));
if (s > 0)
{
cout << '[' << getpid() << "] client say: " << buffer << endl;
}
else if (s == 0)
{
cerr << '[' << getpid() << "] read end of file, client quit, server quit" << endl;
break;
}
else
{
perror("read");
break;
}
}
}
int main()
{
// 1.创建管道文件
if (mkfifo(ipcPath.c_str(), MODE) < 0)
{
perror("mkfifo");
exit(1);
}
Log("创建管道文件成功", DeBug) << "step 1" << endl;
// 2.正常的文件操作
int fd = open(ipcPath.c_str(), O_RDONLY); // 服务端读取信息
if (fd < 0)
{
perror("open");
exit(2);
}
Log("打开管道文件成功", DeBug) << "step 2" << endl;
// 3.编写正常的通信代码
int pnums = 4;
for(int i = 0; i < pnums; i++)
{
pid_t id = fork();
if(id == 0)
{
getMessage(fd);
exit(1);
}
}
for(int i = 0; i < pnums; i++)
{
pid_t ret = waitpid(-1, nullptr, 0);
}
// 4.关闭文件
close(fd); // 关闭管道文件
Log("关闭管道文件成功", DeBug) << "step 3" << endl;
unlink(ipcPath.c_str()); // 删除管道文件
Log("删除管道文件成功", DeBug) << "step 4" << endl;
return 0;
}
注:.hpp文件与.h文件的区别
hpp,其实质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该hpp文件即可,无需再将cpp加入到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用 project中的cpp文件数与编译次数,也不用再发布烦人的lib与dll,因此非常适合用来编写公用的开源库。