进程通信
如何通信
进程运行具有独立性,通信成本高
进程间通信的前提:先让不同进程看到同一份内存空间/资源,同时这块内存不能属于任何一方进程
为什么让进程通信
单进程,无法使用并发能力,更加无法实现多进程协同.
例如:要实现1个进程处理完,让另一个进程对数据进行二次加工,需要使用进程间通信
进程间通信的标准
1 linux原生提供的管道(匿名管道、命名管道),本机通信
2 SystemV,多进程,用于本地通信
共享内存
消息队列
信号量
3 posix,多线程,用于网络通信
管道
原理
管道是Unix中最古老的进程间通信的形式,只能单向通信
在使用行文本过滤工具grep时,经常需要让进程输出的结果经过管道传递grep进程进行过滤
1 父进程以读写方式打开同一个文件,3号文件描述符是读,4号文件描述符是写
2 fork创建子进程,创建子进程的内核数据结构,父进程的文件描述符表的内容拷贝给子进程
此时不同的进程看到了同一份资源 —— struct file文件对象(里面有内核缓冲区)
3 双方进程各自关闭自己不需要的文件描述符.
例如:父进程进行写入,子进程进行读取 —— 父进程关闭读端,子进程关闭写端
单向通信,这种方式就是管道
4 两个进程在通信,通信数据要不要刷新到磁盘?
管道通信不会将数据保存到磁盘中的,这些数据都是临时数据
以下面的sleep命令为例子:它们的父进程PID都是20084
管道的本质就是文件
匿名管道
os内核创建一个struct file对象,不需要在磁盘上有对应的文件,仅仅用来进程间通信,这就是匿名管道
fork让子进程继承父进程的文件描述符表的内容,让父子进程看到同一份struct file文件对象
int pipefd[2]:输出型参数,通过调用函数获得被打开文件的读写端各自的fd
pipefd[0]是读端,pipefd[1]是写端
int main()
{
// 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd); // pipefd[0]读端 pipefd[1]写端
assert(n != -1);
//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程
//构建单向通信的信道,父进程写入,子进程读取
//关闭子进程不需要的文件描述符
close(pipefd[1]);
char buffer[1024];
while(true)
{
//像读文件一样读取
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = '\0';
std::cout << "father say# " << buffer << std::endl;
}
}
exit(0);
}
//关闭父进程不需要的文件描述符
close(pipefd[0]);
std::string message = "i am father process";
int count = 0;
char send_buffer[1024];
while(true)
{
//将特定的内容格式化显示到send_buffer中
snprintf(send_buffer,sizeof(send_buffer),"%s : %d",message.c_str(),count++);
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
}
return 0;
}
匿名管道的特点(就是|)
(1) 管道是用来让具有血缘关系的进程进行进程间通信 —— 常用于父子通信
(2) 管道为进程间协同,提供访问控制
父进程写得慢,管道空了,子进程必须等父进程写入数据,才能读取
子进程读得慢,管道满了,父进程必须等管道的内核缓冲区有空间了,才能往里面写
读端会一次性把管道内的数据全读出来,只要读端的buffer够大
int pipefd[2] = {0};
int n = pipe(pipefd); // pipefd[0]读端 pipefd[1]写端
assert(n != -1);
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//构建单向通信的信道,父进程写入,子进程读取
close(pipefd[1]);
char buffer[1024];
while(true)
{
//读端慢,会一次性把管道内的数据全读出来
sleep(5);
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = '\0';
std::cout << "father say# " << buffer << std::endl;
}
}
exit(0);
}
close(pipefd[0]);
std::string message = "i am father process";
int count = 0;
char send_buffer[1024];
while(true)
{
snprintf(send_buffer,sizeof(send_buffer),"%s : %d\n",message.c_str(),count++);
write(pipefd[1],send_buffer,strlen(send_buffer));
}
(3) 管道是面向字节流式的通信服务,即写端可能写了10次、100次,但是读端一次性就把全部数据读上来了,写的次数和读的次数没有关系.
(4) 管道是基于文件的:
写入的一方,fd没有关闭,如果有数据就读,没有数据就等
写入的一方,fd关闭,读取的一方,read会返回0,表示读到了文件结尾
读取的一方,fd关闭,os会自动终止写端进程(因为写的数据没有人来读了)
// 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd); // pipefd[0]读端 pipefd[1]写端
assert(n != -1);
//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程
//构建单向通信的信道,父进程写入,子进程读取
//关闭子进程不需要的文件描述符
close(pipefd[1]);
char buffer[1024];
while(true)
{
//写入的一方关闭fd后,read会返回0
ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = '\0';
std::cout << "father say# " << buffer << std::endl;
}
else if(s == 0)
{
std::cout << "父进程退出"<< std::endl;
break;
}
}
exit(0);
}
//关闭父进程不需要的文件描述符
close(pipefd[0]);
std::string message = "i am father process";
int count = 0;
char send_buffer[1024];
while(true)
{
//将特定的内容格式化显示到send_buffer中
snprintf(send_buffer,sizeof(send_buffer),"%s : %d\n",message.c_str(),count++);
write(pipefd[1],send_buffer,strlen(send_buffer));
if(count == 5)
break;
sleep(1);
}
close(pipefd[1]);
waitpid(id,nullptr,0);
(5) 管道的生命周期是随进程的,因为这个管道文件只给这两个进程使用,所以这两个通信进程退出,管道文件自然会被os释放掉
(6) 管道是单向通信的,是半双工通信的一种特殊情况
进程间通信一定是有人收,有人发。
作为通信的一方,不能同时进行收和发数据,在一个时刻要么在收要么在发 —— 半双工
作为通信的一方,收和发可以同时进行 —— 全双工
进程池
父进程,创建若干个子进程,跟这些子进程都建立管道,子进程内部都预先放置很多处理任务的方法
子进程读取父进程发送的指令,执行对应任务
如果用户来了一个任务,父进程可以把任务指派给某一个子进程(随机挑选),让某一个子进程帮我们去完成这个任务
1 父进程给子进程发送特定指令,用不同的数字表示让进程执行不同的任务。
2 往哪个管道里写,就让哪个进程执行任务。
ProcessPool.cc
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>
#include "Task.hpp"
#include <time.h>
#define PROCESS_NUM 5
int waitCommand(int fd, int& quit)
{
uint32_t command = 0;
ssize_t s = read(fd,&command,sizeof(command));
if(s == 0)
{
//写端关闭文件描述符
quit = true;
return -1;
}
return command;
}
int main()
{
load();
//让父进程在一张表里,选择让哪个子进程来执行任务
//放的是写端的fd
std::vector<int> table;
//先创建多个子进程
for(int i = 0;i < PROCESS_NUM;++i)
{
int pipefd[2] = {0};
pipe(pipefd);
pid_t id = fork();
if(id == 0){
std::cerr << "子进程创建成功,正在等待命令"<<std::endl;
//子进程关闭写端
close(pipefd[1]);
int quit = false;
while(!quit)
{
//等命令
int command = waitCommand(pipefd[0], quit);
if(quit == true)break;
//执行命令
if(command >= 0 && command < callbacks.size())
{
callbacks[command]();
}
else{
std::cerr << "非法命令"<<std::endl;
}
}
exit(0);
}
//父进程关闭读端
close(pipefd[0]);
//把写端插入表中
table.push_back(pipefd[1]);
}
sleep(2);
//父进程派发任务
//要较为均衡的把任务派发给每一个子进程 —— 负载均衡
srand((unsigned long)time(nullptr));
while(true)
{
uint32_t command;
int select;
std::cout << "#######################################"<<std::endl;
std::cout << "1 查看可执行任务 2 发送指令执行特定任务 3 quit"<<std::endl;
std::cout << "#######################################"<<std::endl;
std::cout << "请输入# ";
std::cin >> select;
if(select == 1) showDesc();
else if(select == 2)
{
std::cout << "输入指令# ";
//选择要执行的任务
std::cin >> command;
//随机在table中选择一个进程执行该任务
write(table[rand()%PROCESS_NUM],&command,sizeof(command));
}
else if(select == 3)
{
break;
}
}
for(int fd:table)
close(fd);
//回收所有子进程
for(int i = 0; i < PROCESS_NUM;++i)
{
pid_t ret = waitpid(-1,nullptr,0);
printf("%d被回收\n",ret);
}
return 0;
}
Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <vector>
#include <unordered_map>
#include <stdio.h>
typedef std::function<void()> func;
//方法列表
std::vector<func> callbacks;
//方法列表描述
std::unordered_map<int,std::string> desc;
void readMySQL()
{
std::cout << "["<<getpid()<<"] :" << "执行访问数据库任务" << std::endl;
}
void parseURL()
{
std::cout << "[" << getpid() << "] :" << "执行解析URL任务" << std::endl;
}
void save()
{
std::cout << "[" << getpid() << "] :" << "执行数据持久化任务" << std::endl;
}
void test()
{
std::cout << "[" << getpid() << "] :" << "执行测试任务" << std::endl;
}
//加载任务 和 任务描述
void load()
{
desc.insert(std::make_pair(callbacks.size(),"读取数据库数据"));
callbacks.push_back(readMySQL);
desc.insert(std::make_pair(callbacks.size(),"解析网页URL"));
callbacks.push_back(parseURL);
desc.insert(std::make_pair(callbacks.size(),"保存数据到文件中"));
callbacks.push_back(save);
desc.insert(std::make_pair(callbacks.size(),"测试数据"));
callbacks.push_back(test);
}
void showDesc()
{
for(const auto& iter: desc)
{
printf("%d : %s\n",iter.first,iter.second.c_str());
}
}
命名管道
匿名管道只能让有亲缘关系的进程来通信
如果想让两个毫不相关的进程通信,可以使用命名管道(先让不同进程看到同一份资源)
两个进程打开同一个文件,这样在这两个进程的文件描述符表中,一定看到了同一份struct file对象
在磁盘上可以创建一种管道文件,可以被多个进程打开,但是不会将内存数据刷新到磁盘
这种管道文件就叫命名管道文件(没有文件内容)
命令行上创建一个命名管道文件:
mkfifo name_pipe
往命名管道里写数据,但是因为还没有人打开读取,所以会阻塞
另一个进程可以读取命名管道内部的数据
common.hpp
#ifndef _COMMON_HPP
#define _COMMON_HPP
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <assert.h>
#include <unistd.h>
using namespace std;
string ipcPath = "./pipe.ipc";
#endif
server.cc
#include "common.hpp"
int main()
{
//1 创建管道文件
mkfifo(ipcPath.c_str(),0666);
//2 正常文件操作
int fd = open(ipcPath.c_str(),O_RDONLY); //要等客户端也打开管道文件,否则会一直阻塞
assert(fd >= 0);
cout << "打开管道文件成功"<<endl;
//3 正常通信代码
char buffer[1024];
while(true)
{
ssize_t s = read(fd,buffer,sizeof(buffer) - 1);
if(s == 0)
{
cout << "客户端关闭连接"<<endl;
break;
}
else if(s > 0)
{
buffer[s] = '\0';
cout << "client send# " << buffer << endl;
}
else{
cout << "read error" << endl;
perror("read");
break;
}
}
close(fd);
//删除管道文件
unlink(ipcPath.c_str());
return 0;
}
client.cc
#include "common.hpp"
int main()
{
//打开管道文件,服务端一定已经创建好管道文件了
int fd = open(ipcPath.c_str(),O_WRONLY);
assert(fd >= 0);
string buffer;
while(true)
{
cout << "请输入要发送给服务端的信息# ";
getline(cin,buffer);
write(fd,buffer.c_str(),buffer.size());
}
close(fd);
return 0;
}
共享内存
原理
os在内存里申请一份空间,将这个空间经过通信进程的页表,映射到共享区的地址
返回映射到共享区的起始地址,该虚拟地址经过页表映射,就访问到了同一块内存资源。
内核中可能存在多对进程,都在用共享内存通信 —— 先描述,再组织,将它管理起来
共享内存 = 共享内存块 + 对应共享内存的内核数据结构
代码
共享内存的创建
1 size_t size:要创建的共享内存的大小
共享内存的大小,最好是页的整数倍(PAGE:4096byte)
os管理物理内存时,页的大小是以4KB为单位的(4096byte)
如果申请了4097byte,os在底层会申请4096*2byte的空间,剩下的4095byte浪费了
inux文件系统模块和磁盘进行IO的基本单位是4KB
2 int shmflg:
(1) IPC_CREAT单独使用
创建共享内存,如果已经存在,获取并返回;如果不存在,就创建并返回
(2) IPC_EXCL单独使用无意义,需要和IPC_CREAT配合
如果底层不存在,创建并返回;底层存在,出错返回
IPC_EXCL | IPC_CREAT 返回成功,一定是全新的共享内存
(3) 共享内存刚创建出来,默认没有进程有权限使用
所以刚创建时需要写明权限perms
int shmid = shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
3 返回值:返回共享内存的用户层标识符,类似于fd
4 key_t key: server&&client使用同一个key,就能看到同一个共享内存
例如:本机的两个不同进程client和server要通信,
(1) 在底层用同一套算法形成一个相同唯一值key
(2) server调用接口,在底层创建共享内存;
(3) shmget会把key值写到描述共享内存的结构体里
(4) 然后client拿着这个数字在底层找到匹配的共享内存,找到返回标识该共享内存的标识符
ftok内部是一套算法,将一个路径和数字组合形成一个唯一值.
拿文件的inode编号和数字进行一些运算,形成随机值
不一定会创建成功,因为key必须唯一标识一个共享内存
common.hpp
#pragma once
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <assert.h>
#define PATH_NAME "/home/lyh"
#define PROJ_ID 11
#define SHM_SIZE 1024
using std::endl;
using std::cin;
using std::cout;
server.cc
//1 创建公共key值
key_t key = ftok(PATH_NAME,PROJ_ID);
assert(key != -1);
cout << "server key:" << key <<endl;
//2 创建共享内存 —— 双方通信的发起者,要创建全新的共享内存
int shmid = shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL);
assert(shmid != -1);
第一次运行,会创建共享内存并返回;
第二次运行,共享内存已经存在,会创建失败
当进程运行结束,共享内存仍然存在,共享内存的生命周期是随os的,需要手动释放
查看所有共享内存:ipcs -m
共享内存的删除
删除共享内存:ipcrm -m shmid
代码删除共享内存
int cmd:对共享内存执行什么操作,删除是 IPC_RMID
struct shmid_ds * buf:描述共享内存的结构体,删除设为nullptr即可
// finally:删除共享内存
shmctl(shmid,IPC_RMID,nullptr);
关联/去关联共享内存
创建好共享内存后,需要把共享内存映射进当前进程的进程地址空间的共享区里
const void* shmaddr:共享内存创建后,要把共享内存挂接到指定的虚拟地址处,但一般设为nullptr,表示让os帮进程挂接
shmflg:挂接方式,0表示以读写方式挂接
返回值:成功返回 挂接好的共享内存虚拟地址的起始位置(类似malloc)
//将指定的共享内存,挂接到自己的地址空间
char* shmaddr = (char*)shmat(shmid,nullptr,0);
//将指定的共享内存,和自己的地址空间去关联
shmdt(shmaddr);
// finally:删除共享内存(即使有进程和当下的shm挂接,依旧删除共享内存)
shmctl(shmid,IPC_RMID,nullptr);
使用共享内存通信
共享内存一旦映射到进程地址空间,双方进程要通信,直接往虚拟地址读写即可
之前的管道都要用read和write要通信,因为 公共资源属于文件,文件是在内核的数据结构,os维护,必须通过系统调用接口才能访问。
而共享内存是创建在共享区,在用户空间,用户不需要调用任何os接口,可以直接对共享内存进行读写,所以将共享内存当成一个大字符串即可。另外,共享内存被创建,默认清0
1 通信双方,直接把共享内存当成一个char数组,直接写入
2 共享内存是所有进程间通信IPC速度最快的
管道:至少有两次拷贝,需要调用write将数据写到struct file的内核缓冲区中,还要调用read从缓冲区中读取数据
而共享内存的一方将数据拷贝到共享内存,另一方直接看到共享内存的数据
server.cc
#include "common.hpp"
int main()
{
//1 创建公共key值
key_t key = ftok(PATH_NAME,PROJ_ID);
assert(key != -1);
cout << "server key:" << key <<endl;
//2 创建共享内存 —— 双方通信的发起者,要创建全新的共享内存
int shmid = shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
assert(shmid != -1);
//3 将指定的共享内存,挂接到自己的地址空间
char* shmaddr = (char*)shmat(shmid,nullptr,0);
//4 通信逻辑
while(true)
{
printf("%s\n",shmaddr);
if(strcmp(shmaddr,"quit") == 0)break;
sleep(1);
}
//5 将指定的共享内存,和自己的地址空间去关联
shmdt(shmaddr);
//finally:删除共享内存(即使有进程和当下的shm挂接,依旧删除共享内存)
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
client.cc
#include "common.hpp"
int main()
{
//1 创建唯一的key
key_t key = ftok(PATH_NAME,PROJ_ID);
assert(key != -1);
//2 获取服务端创建的共享内存
int shmid = shmget(key,SHM_SIZE,IPC_CREAT);
assert(shmid != -1);
//3 将当前进程的地址空间和共享内存关联
char* shmaddr =(char*)shmat(shmid,nullptr,0);
assert(shmaddr != nullptr);
//4 通信逻辑
//client将共享内存看作一个char类型的buffer
while(true)
{
cout << "请输入发送给服务端的数据# ";
cin >> shmaddr;
if(strcmp(shmaddr,"quit") == 0)break;
}
//5 将当前进程的地址空间和共享内存去关联
shmdt(shmaddr);
return 0;
}
3 如果管道里没数据,读端会阻塞;管道满了,写端会阻塞 —— 有访问控制
但是共享内存没有访问控制.无论共享内存有无数据,读端都可以一直读,写端也可以一直写
用管道帮助共享内存实现简单的访问控制:
客户端写数据期间,服务端必须一直等待;
当客户端发送数据时,唤醒服务端,让服务端读取,读取完继续等待;
common.hpp
#pragma once
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define PATH_NAME "/home/lyh"
#define PROJ_ID 11
#define SHM_SIZE 4096
#define PIPE_FILE "./pipe.ipc"
using std::endl;
using std::cin;
using std::cout;
class Init{
public:
Init()
{
umask(0);
mkfifo(PIPE_FILE,0666);
}
~Init()
{
unlink(PIPE_FILE);
}
};
//管道里写入或读取的数据不重要,重要的是管道的阻塞机制
int openPIPE(int flag)
{
return open(PIPE_FILE,flag);
}
void wait(int fd)
{
cout << "正在等待"<<endl;
uint32_t tmp = 0;
ssize_t s = read(fd,&tmp,sizeof(tmp));
}
void wake(int fd)
{
cout << "开始唤醒"<<endl;
uint32_t tmp = 1;
write(fd,&tmp,sizeof(tmp));
}
void closePIPE(int fd)
{
close(fd);
}
server.cc
#include "common.hpp"
Init init;
int main()
{
//1 创建公共key值
key_t key = ftok(PATH_NAME,PROJ_ID);
assert(key != -1);
cout << "server key:" << key <<endl;
//2 创建共享内存 —— 双方通信的发起者,要创建全新的共享内存
int shmid = shmget(key,SHM_SIZE,IPC_CREAT | IPC_EXCL | 0666);
assert(shmid != -1);
//3 将指定的共享内存,挂接到自己的地址空间
char* shmaddr = (char*)shmat(shmid,nullptr,0);
//打开管道文件
int fd = openPIPE(O_RDONLY);
assert(fd >= 0);
//4 通信逻辑
while(true)
{
//等待
wait(fd);
printf("%s\n",shmaddr);
if(strcmp(shmaddr,"quit") == 0)break;
sleep(1);
}
closePIPE(fd);
//5 将指定的共享内存,和自己的地址空间去关联
shmdt(shmaddr);
//finally:删除共享内存(即使有进程和当下的shm挂接,依旧删除共享内存)
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
client.cc
#include "common.hpp"
int main()
{
//1 创建唯一的key
key_t key = ftok(PATH_NAME,PROJ_ID);
assert(key != -1);
//2 获取服务端创建的共享内存
int shmid = shmget(key,SHM_SIZE,IPC_CREAT);
assert(shmid != -1);
//3 将当前进程的地址空间和共享内存关联
char* shmaddr =(char*)shmat(shmid,nullptr,0);
assert(shmaddr != nullptr);
//4 通信逻辑
int fd = openPIPE(O_WRONLY);
while(true)
{
cout << "请输入发送给服务端的数据# ";
cin >> shmaddr;
wake(fd);
if(strcmp(shmaddr,"quit") == 0)break;
}
//5 将当前进程的地址空间和共享内存去关联
shmdt(shmaddr);
closePIPE(fd);
return 0;
}
注意:不同的两个终端下的进程