目录
一、进程间通信背景
1、进程间通信的理解
2、进程间通信的目的
3、进程间的必要性
二、管道
1、什么是管道
2、匿名管道
3、命名管道
4、管道通信的特点
三、System V IPC
1、共享内存
2、进程互斥
总结
进程运行具有独立性,进程想要直接进行进行通信难度比较大,进程间通信的前提是让不同的资源能够看到同一块资源。
单进程无法使用并发能力,无法实现多进程协同,传输数据,同步执行流,消息通知等。
想象现实中的管道,它一定是有一个出口有一个入口,用来传输资源,资源只能从一端流向另一端
进程间通信的管道也同理,一个进程发送数据另一个进程接收数据。
管道是由操作系统所提供的最古老的进程间通信的形式。
匿名管道只适用于具有亲缘关系的进程间通信——通常用于父子进程间的通信
一个进程创建子进程,父进程打开的文件也会被子进程所继承,因为是同一个文件,所以父子进程打开的同一个文件具有相同的struct file,因此父子进程就同时看到了同一份内存资源,也就可以借助这个文件来进行父子进程间的通信。
匿名管道创建步骤
1、分别以读写方式打开同一个文件
2、fork()创建子进程
3、双方进程各自关闭自己不需要的文件描述符
管道文件是不需要刷新到磁盘的,它是专门用来进行进程间通信的文件,刷新到磁盘,相当于进行IO使进程间通信的效率降低
我们可以使用pipe来创建匿名管道,这里要传入一个数组,它是输出线参数
下标为0的代表读端,1代表写端
这样前置工作就准备好了
接下来以一个比较复杂的例子来演示匿名管道
写一个简单的单机版负载均衡
创建一个进程池,父进程通过匿名管道随机向子进程派发任务
//task.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef std::function func;
std::vector callbacks;
std::unordered_map desc;
void readMySQL()
{
std::cout << "sub process [ " << getpid() << " ] 执行访问数据库任务\n" << std::endl;
}
void execuleUrl()
{
std::cout << "sub process [ " << getpid() << " ] 执行URL解析\n" << std::endl;
}
void cal()
{
std::cout << "sub process [ " << getpid() << " ] 执行加密任务\n" << std::endl;
}
void save()
{
std::cout << "sub process [ " << getpid() << " ] 执行持久化任务\n" << std::endl;
}
void Load()
{
desc.emplace(callbacks.size(), "readSQL: 读取数据库");
callbacks.emplace_back(readMySQL);
desc.emplace(callbacks.size(), "execuleURL: 进行url解析");
callbacks.emplace_back(execuleUrl);
desc.emplace(callbacks.size(), "cal: 进行加密计算");
callbacks.emplace_back(cal);
desc.emplace(callbacks.size(), "save: 进行数据的文件保存");
callbacks.emplace_back(save);
}
void showHandler()
{
for(const auto& iter : desc)
{
std::cout << iter.first << "\t" << iter.second << std::endl;
}
}
int handlerSize()
{
return callbacks.size();
}
//main.cpp
#include "task.hpp"
#define PROCESS_NUM 5
// 父进程给子进程派发任务
int waitCommind(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));
std::cout << "main process: call process " << who << "execute" << desc[command] << "through" << fd << std::endl;
}
int main()
{
Load();
// 简单的线程池
// 进程pid 及 文件描述符
std::vector> slots;
// 创建子进程
for (size_t i = 0; i < PROCESS_NUM; i++)
{
// 1、以读写方式打开文件
int fd[2] = {0};
int n = pipe(fd);
assert(n == 0);
pid_t id = fork();
if (id < 0)
{
std::cerr << "fork" << std::endl;
}
else if (id == 0)
{
// child
// 关闭写端
close(fd[1]);
while (true)
{
bool quit = false;
int command = waitCommind(fd[0], quit);
if (quit == true)
{
break;
}
if (command >= 0 && command < handlerSize())
{
callbacks[command];
}
else
{
std::cout << "非法command" << std::endl;
}
}
exit(1);
}
else
{
// father
// 关闭读端
close(fd[0]);
slots.emplace_back(id, fd[1]);
}
}
// 派发任务
size_t count = 0;
srand(time(0));
while (true)
{
int command = rand() % handlerSize();
int choice = rand() % slots.size();
sendAndWakeUp(slots[choice].first, slots[choice].second, command);
sleep(1);
count++;
if (count == 10)
{
break;
}
}
// 关闭文件描述符
for (const auto &slot : slots)
{
close(slot.second);
}
// 回收子进程
for (const auto &slot : slots)
{
waitpid(slot.first, nullptr, 0);
}
return 0;
}
命名管道与匿名管道类似,不过命名管道可以让不具有亲缘关系的进程通信
首先也是要让不同的进程看到同一份资源,双方进程就可以通过管道文件的路径看到同一份资源
管道文件首先是一个文件,它是有名字的可以被打开,但是不会将内存数据进行刷新到磁盘
mkfifo命名可以创建一个管道文件
我们通过echo命令将hello通过管道文件发送过去,echo命令就进入了阻塞状态
这时通过管道文件就可以读取资源了
这样就完成了echo和cat的通信
删除管道文件可以使用unlink或者rm
接下来看代码的方式创建管道文件
mkfifo可以创建管道文件
成功返回0,失败返回-1
接下来还是举一个例子来演示命名管道进程间通信
server创建管道文件,从client读取资源,结束通信后,client关闭管道文件并且删除管道文件
client获取管道文件,进行正常通信,向server发送数据
//comm.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MODE 0666
#define SIZE 128
#define PROCESS_NUM 3
std::string ipcPath = "./fifo.ipc";
//log.hpp
#include
#include
#include
#define Debug 0
#define Notice 1
#define Warning 2
#define Error 3
const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"};
std::ostream &Log(std::string message, int level)
{
std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
return std::cout;
}
//server.cpp
#include "log.hpp"
#include "comm.hpp"
static void GetMessage(int fd)
{
char buffer[SIZE];
while (true)
{
ssize_t s = read(fd, buffer, sizeof(buffer));
if (s > 0)
{
buffer[s] = '\0';
std::cout << "client call" << buffer << std::endl;
}
else if (s == 0)
{
std::cerr << "[ " << getpid() << " ]"
<< "read end of file server quit" << std::endl;
break;
}
else
{
std::cerr << "read" << std::endl;
break;
}
}
}
int main()
{
// 1、创建管道文件
if (mkfifo(ipcPath.c_str(), MODE) < 0)
{
std::cerr << "mkfifo" << std::endl;
exit(1);
}
Log("创建管道文件成功", Debug) << "step 1" << std::endl;
// 2、正常文件操作
int fd = open(ipcPath.c_str(), O_RDONLY);
if (fd < 0)
{
std::cerr << "open" << std::endl;
}
Log("打开文件成功", Debug) << "step 2" << std::endl;
// 3、正常通信代码
// 使用进程池
Log("进程间开始通信", Debug) << "step 3" << std::endl;
for (size_t i = 0; i < PROCESS_NUM; i++)
{
pid_t id = fork();
if (id < 0)
{
std::cerr << "fork" << std::endl;
}
else if (id == 0)
{
GetMessage(fd);
exit(1);
}
}
Log("进程间通信完成", Debug) << "step 4" << std::endl;
// 4、回收子进程
for (size_t i = 0; i < PROCESS_NUM; i++)
{
waitpid(-1, nullptr, 0);
}
Log("等待子进程成功", Debug) << "step 5" << std::endl;
// 5、关闭文件
close(fd);
Log("关闭文件成功", Debug) << "step 6" << std::endl;
// 5、删除文件
unlink(ipcPath.c_str());
Log("删除管道文件成功", Debug) << "step 7" << std::endl;
return 0;
}
//clinet.cpp
#include "comm.hpp"
int main()
{
//1、获取管道文件
int fd = open(ipcPath.c_str(), O_WRONLY);
if(fd < 0)
{
std::cerr << "open" << std::endl;
exit(1);
}
//2、IPC过程
std::string buffer;
while(true)
{
std::cout << "Please Enter Mseeage Line:> ";
std::getline(std::cin, buffer);
if(buffer == "quit")
{
break;
}
write(fd, buffer.c_str(), buffer.size());
}
//3、关闭管道文件,server自动停止读取
close(fd);
return 0;
}
命名管道多个进程竞争读取,没有任何问题,发送数据大小是小于4096字节就是原子的
1、管道具有通过让进程间协同,提供了访问控制
2、管道提供的是面向流式的通信服务——面向字节流——协议
3、管道是基于文件的,文件的声明周期是随进程的,管道的声明周期是随进程的
4、管道是单向通信的,就是半双工通信的一种特殊情况
管道的访问控制
1、写快,读慢,写满就不能再写了
2、写慢,读快,管道没有数据的时候,读必须等待
3、写关,读0,标识读到了文件结尾
4、读关,写继续,OS终止写进程
还要说一下通信的前提:让不同的进程能够看到同一份资源共享内存也是同样道理
不过它是直接向OS申请一块空间,然后映射到进程的共享区,这样就使得进程间通信的效率提升,因为不用使用系统调用多次拷贝资源了,进程可以直接访问那块共享的内存资源
1)申请空间
2)建立映射(多个进程映射同一块共享内存)
3)通信
4)去掉映射
5)释放空间
共享内存的提供者是操作系统,操作系统也要管理共享内存,通过先描述在组织
共享内存 = 共享内存块 + 对应的共享内存的内核数据结构
我们可以使用shmget函数来申请共享内存
它的返回值shmid是共享内存的用户标识符,类似于文件描述符fd
它的最后一个参数有两个选项IPC_CREAT and IPC_EXCL
一般两者是组合使用的,单独使用IPC_EXCL是没有意义的
单独使用IPC_CREAT:如果创建共享内存,底层已经存在,那么就获取它,不存在就创建它
两者组合使用:如果底层不存在就创建它,并且返回新创建的shmid,如果底层存在,出错返回
接下来就是shmget的第一个参数key
key可以保证与我们进程通信的进程就是要通信的进程,并且能够看到我创建的共享内存
我们可以使用ftok函数来创建key,ftok是一种加密算法,使用同样的算法规则,传入的参数相同就能够形成唯一值,ftok的第一个参数pathname最好设定我们有访问权限的路径
shmid vs key
只有创建的时候使用key,大部分情况用户访问共享内存,都是使用的shmid
当我们的程序运行结束,我们的共享内存,还存在
System V IPC资源的生命周期随内核
解决办法
1、手动删除
ipcs -m可以获取共享内存相关属性
使用命令 ipcrm -m + shmid 可以删除共享内存
2、代码删除
shmctl + 选项IPC_RMID可以删除共享内存
共享内存链接方法
使用shmat可以将共享内存与进程建立映射,这个函数类似于malloc,返回值是void*可以强制转换为char*当作数组来使用共享内存
共享内存去关联方法
去除关联的方式是使用shmdt
注意:创建共享内存的大小最好是页(4096bytes)的整数倍
如果你申请4097个字节,操作系统会给你申请4096 * 2bytes,剩下的4095字节就浪费了
总结:
shmget返回值是用户层标识符
key是内核层面标定共享内存的标识符
想要自己挂接共享内存等对共享内存的操作要使用shmid,同时共享内存的生命周期是随内核的
shmat:挂接共享内存
shmdt:去关联
shmctl :删除共享内存
共享内存是在堆栈之间的共享区,它是属于用户的
用户空间:不用经过系统调用就可以访问的空间
管道的通信方式是文件,它是在内核空间中,它是内核的一种特定的数据结构,是操作系统维护的在[3G, 4G]的内核空间中,用户访问需要使用系统调用
下面看一个简单的例子
//comm.hpp
#pragma once
#include
#include
#include
#include
#include "../named_pipe/log.hpp"
#include
#include
#include
#include
#include
#include
#define FIFO_NAME "./fifo"
#define PROJ_ID 0x66
#define SHM_SIZE 4096
#define PATH_NAME "/home/ww"
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
Log("创建管道文件成功", Notice) << "\n";
}
~Init()
{
unlink(FIFO_NAME);
Log("删除管道文件成功", Notice) << "\n";
}
};
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(std::string pathname, int falgs)
{
int fd = open(pathname.c_str(), falgs);
assert(fd >= 0);
return fd;
}
void Wait(int fd)
{
Log("等待中", Notice) << "\n";
uint32_t tmp = 0;
ssize_t s = read(fd, &tmp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
}
void Signal(int fd)
{
uint32_t tmp = 0;
ssize_t s = write(fd, &tmp, sizeof(tmp));
assert(s == sizeof(uint32_t));
Log("唤醒中", Notice) << "\n";
}
void CloseFIFO(int fd)
{
close(fd);
}
//server.cpp
#include "comm.hpp"
Init init;
int main()
{
// 1、生成相同的key
key_t key = ftok(PATH_NAME, PROJ_ID);
Log("生成key成功", Notice) << "key: " << key << std::endl;
// 2、申请共享内存
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
Log("申请共享内存失败", Error) << "shmid: " << shmid << std::endl;
exit(2);
}
Log("申请共享内存成功", Notice) << "shmid: " << shmid << std::endl;
// 3、共享内存与进程建立映射
char *adder = (char *)shmat(shmid, nullptr, 0);
Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;
// 4、正常通信
Log("开始通信", Notice) << std::endl;
// 共享内存是没有访问控制的,使用管道进行管道控制
int fd = OpenFIFO(FIFO_NAME, READ);
while (true)
{
Wait(fd);
printf("%s\n", adder);
if (strcmp(adder, "quit") == 0)
break;
}
// 5、取消映射
int ret = shmdt(adder);
if (ret < 0)
{
Log("共享内存去关联失败", Error) << std::endl;
}
// 6、删除共享内存
ret = shmctl(shmid, IPC_RMID, nullptr);
if (ret < 0)
{
Log("删除共享内存失败", Error) << std::endl;
exit(3);
}
return 0;
}
//client.cpp
#include "comm.hpp"
int main()
{
// 1、生成相同的key
key_t key = ftok(PATH_NAME, PROJ_ID);
Log("生成key成功", Notice) << "key: " << key << std::endl;
// 2、获取共享内存
int shmid = shmget(key, SHM_SIZE, 0);
if (shmid < 0)
{
Log("获取共享内存失败", Error) << "shmid: " << shmid << std::endl;
exit(2);
}
// 3、建立映射
char *adder = (char *)shmat(shmid, nullptr, 0);
Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;
// 4、通信
Log("开始通信", Notice) << std::endl;
int fd = OpenFIFO(FIFO_NAME, WRITE);
while (true)
{
ssize_t s = read(0, adder, SHM_SIZE - 1);
if (s > 0)
{
adder[s - 1] = 0;
Signal(fd);
if (strcmp(adder, "quit") == 0)
break;
}
}
// 5、取消映射
int ret = shmdt(adder);
if (ret < 0)
{
Log("取消共享内存失败", Error) << std::endl;
}
return 0;
}
为了让进程间通信,让不同的进程之间看到同一份资源,之前所说的所有通信方式,本质都是优先解决一个问题,让不同的进程看到同一份资源
这样就会带来一些时序问题,造成数据不一致问题
1、多个进程看到的公共的一份资源叫做临界资源
2、把自己的进程访问临界资源的代码叫做临界区
3、为了更好的保护临界区,可以让在任何时刻都只能有一个进程进入临界区——互斥
4、原子性:要么不做,要么做完,没有中间状态
以上就是今天要讲的内容,本文仅仅简单介绍了进程间通信的管道和共享内存