一、本质
不同进程看到同一份资源
二、目的
让多个进程协同完成同一个事情
数据传输 | 在一个在线购物网站中,一个进程负责从数据库中获取用户的购物车数据,另一个进程负责根据购物车数据计算总价和运费,这就需要数据在两个进程间传输 |
资源共享 | 在一个打印服务器中,多个进程都需要使用打印机这个资源。通过进程间通信来协调打印机的使用,避免多个进程同时访问导致冲突,实现资源的共享和合理分配 |
事件通知 | 在一个监控系统中,当传感器检测到异常情况(如温度过高)时,一个进程负责收集这个事件信息,并通过进程间通信通知另一个进程采取相应的控制措施(如启动风扇降温) |
进程控制 | gdb 调试 进程,gdb 这个进程需要与被调试进程进行数据通信,信息传递 |
三、进程间通信方式
1. 进程间通信的设计思路
进程一定在保证各自的独立性的基础上,进行通信。这就表明了进程间通信不可以直接访问进程内部,而是要通过一个双方都可以访问的空间来进行通信。
该空间不可以由通信进程来提供,因为如果由通信进程来提供,则也属于进程的资源,不能保证进程独立性。
进程间通信的设计思路为:
- 有一个可以交换进程数据的空间
- 该空间不可以由通信进程提供,而是由 OS 提供
进程间通信的本质:
让不同进程看到同一份资源
2. 四种通信方式
OS 提供的空间结构不同(如:数组、链表、栈、队列等),就决定了有不同的通信形式
(1)管道(采用队列结构)
a. 前置知识
若分别以 “ r ”,“ w ”方式打开同一个文件,那么struct_file就回存在两份,但是这两份中的文件缓冲区为同一份,此时向该缓冲区进行读写,就会导致读写错乱
至于为什么会读写错乱,当我们在内部创建线程时,无法保证我们读到的,是我们希望得到的数据,而是有可能为其他线程写入的数据
如何解决?
创建子进程,让父进程 写 / 读,子进程 读 / 写
在父进程同时以读写来打开文件描述符后,创建子进程,此时子进程就会将父进程的相关内核数据结构都复制一份,所以子进程的文件描述符表也是指向父进程的文件描述符表指向的 struct file 上,我们此时只需要关掉子进程的写/读端,父进程的读/写端,就可以在保证读写不错乱的情况下,来让父子进程进行通信,因为此时父子进程已经看到了同一份资源(struct file中的缓冲区),并且这不属于通信进程的资源,而是 OS 创建的。
这种基于文件的,让不同进程看到同一份资源的通信方式,叫做管道!
(管道只能单向通信,这是因为管道是一个缓冲区,如果是全双工,我们保证不了读写的独立性,即缓冲区只能被一方读取,另一方写入)
【注】:
struct file 结构体允许被多个进程的文件描述符表中的指针指向,我们关闭一个 fd ,实际上是将struct file 结构体的引用计数减一,同时断开指向。直到 struct file 结构体的引用计数为 0 ,此时 OS才会清除这个 struct file 结构体。
b. 匿名管道
原理
使用 pipe 系统调用,创建一个内存级文件 |
创建子进程,让父子进程看到相同的信息,关闭父子进程不需要的文件描述符 |
int pipe(int pipefd[2]);
pipefd : 输出型参数,pipefd[0] 为“ r ”,pipefd[1] 为 “ w ”
【注】:
- 匿名管道 被读取后,数据将不再存在于匿名管道中
四种情况
I:写端休眠,读端正常
此时管道内无数据,并且写端不关闭 fd ,那读端就要阻塞等待,直到管道中有数据
II:管道被写满 && 读端不关闭 fd
写端写满后,就要阻塞等待,直到读端去读
III:写端不写 && 关闭 fd
读端会把管道的数据读完,最后 read 返回值为 0 ,表示读结束
IV:读端不读 && 读端关闭
写端去向管道内写入数据,此时 OS 会直接发送 13 号信号(SIGPIPE)终止写端进程,因为一个永远不会被读的缓冲区,写入就没有意义。
五种特性
I:只能单向通信(半双工)
这是因为我们要完成父子进程之间的数据传输,就必须要保证读端读到的数据都是写端写入的,若是全双工,则无法保证读端读到的数据就是写端写入的,还有可能是自己写入的,就会造成数据混乱。
所以若想双向通信,则可以建立两个匿名管道
II:自带同步机制
只有匿名管道中有数据,才可以读,否则读端阻塞等待;只有匿名管道中没被写满,写端才能写入,否则写端阻塞等待。
这就是同步机制,不像????
III:只适用于有血缘关系的进程来通信
IV:父子进程退出,管道自动释放
因为文件的生命周期是随进程的,而管道是内存级文件,只有struct file ,没有磁盘对应的实体。
V:管道是面向字节流的
以字节为单位进行读取
知识迁移:命令行的管道命令
cat file.c | grep .h
bash 会创建一个管道,两个子进程,“ | ” 前的关闭读端,“ | ” 后的关闭写端,通过管道传输数据。
代码实现
/**
* 匿名管道的使用
*/
#include "Log.hpp"
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 创建管道,pipefd[0] -> r、pipefd[1] -> w
int pipefd[2] = {0};
int ret = pipe(pipefd);
if(ret == -1)
{
_log.LogMessage(ClassFile, Error, "pipe error : %s\n", strerror(errno));
}
// 创建子进程
int pid = fork();
if(pid == -1)
{
_log.LogMessage(ClassFile, Error, "fork error : %s\n", strerror(errno));
}
else if(pid == 0) // 子进程负责写
{
close(pipefd[0]);
char buffer[1024];
while(1)
{
std::cin.getline(buffer, 1024);
if(strcmp(buffer, "quit") == 0)
{
std::cout << "写端退出" << std::endl;
break;
}
int ret_write = write(pipefd[1], buffer, 1024);
if(ret_write == -1)
{
_log.LogMessage(ClassFile, Error, "write error : %s\n", strerror(errno));
std::cout << "写数据失败" << std::endl;
break;
}
}
close(pipefd[1]);
exit(0);
}
// 父进程负责读
close(pipefd[1]);
char buffer[1024];
while(1)
{
int ret_read = read(pipefd[0], buffer, sizeof(buffer));
if(ret_read == -1) // 读取失败
{
_log.LogMessage(ClassFile, Error, "read error : %s\n", strerror(errno));
break;
}
else if(ret_read == 0) // 对端关闭,读到末尾
{
std::cout << "对端退出,读到结尾了" << std::endl;
break;
}
else // 读取成功
{
buffer[ret_read] = 0;
std::cout << "写端说 : " << buffer << std::endl;
}
}
close(pipefd[0]);
// 父进程回收子进程资源
int ret_wait = wait(nullptr);
if(ret_wait == -1)
{
_log.LogMessage(ClassFile, Error, "wait error : %s\n", strerror(errno));
}
else
{
std::cout << "等待成功,父进程退出" << std::endl;
}
return 0;
}
c. 命名管道(会用)
原理
让不同的进程通信
【注】
- 不同的进程使用的命名管道文件的 struct file 为同一个
- 命名管道文件只用于进程间通信,不会将缓冲区的内容刷新到磁盘上
- 先打开读端会阻塞等待,直到写端打开并写入数据
创建命名管道文件的方式
命令行方式:
mkfifo file_name
库函数实现:
// 创建命名管道文件
int mkfifo(const char *pathname, mode_t mode);
// 删除文件
int unlink(const char *pathname);
代码实现
// 读端代码read.cc
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
const int SIZE = 1024;
const char* PIPE = "./file.pipe";
int main()
{
umask(0);
//1. 建立命名管道文件
int ret = mkfifo(PIPE, 0666);
if(ret < 0)
{
std::cout << "mkfifo error : " << strerror(errno) << std::endl;
exit(-1);
}
// 2. 以读的方式打开
int rfd = open(PIPE, O_RDONLY);
if(rfd < 0)
{
std::cout << "open error : " << strerror(errno) << std::endl;
unlink(PIPE);
exit(-1);
}
char buffer[SIZE];
// 3. 一直从命名管道中读取
while(true)
{
int read_ret = read(rfd, buffer, sizeof(buffer) - 1);
if(read_ret < 0)
{
std::cout << "read error : " << strerror(errno) << std::endl;
break;
}
else if(read_ret == 0)
{
std::cout << "writer is closed && the size of buffer is null" << std::endl;
std::cout << "reader exit" << std::endl;
break;
}
else
{
buffer[read_ret] = 0;
std::cout << "writer send : " << buffer << std::endl;
}
}
close(rfd);
unlink(PIPE);
return 0;
}
// 写端代码write.cc
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
const int SIZE = 1024;
const char *PIPE = "./file.pipe";
int main()
{
// 1. 以写的方式打开
int wfd = open(PIPE, O_WRONLY);
if (wfd < 0)
{
std::cout << "open error : " << strerror(errno) << std::endl;
exit(-1);
}
// 2. 向命名管道中写入
char buffer[SIZE];
while(true)
{
std::cout << "enter# ";
std::cin.getline(buffer, sizeof(buffer));
if(strcmp(buffer, "quit") == 0)
{
std::cout << "writer exit" << std::endl;
break;
}
int write_ret = write(wfd, buffer, strlen(buffer));
if(write_ret < 0)
{
std::cout << "write error : " << strerror(errno) << std::endl;
break;
}
}
close(wfd);
return 0;
}
d. 共享内存(会用)
原理
OS 在内存中开辟一段空间,将这段空间通过页表映射到进程地址空间的共享区中,让不同的进程看到同一份资源,这段资源/空间就叫共享内存 shm。
每个 shm 在内核中都有唯一性标识符
问题
OS 一定会管理内存中存在的很多共享内存(先描述,再组织),但我们如何保证不同的进程指向的是希望指向的共享内存呢?
通过共享内存的唯一性标识符
共享内存的生命周期?
shm 的生命周期随 OS,即我们创建后不主动释放,则一直存在,直到 OS 退出,因为 OS 相当于 shm 的管理者。
这里与文件操作不同,文件的生命周期随进程,因为我们在打开一个文件时,首先会将文件加载到内存中,struct file 的引用计数自增,返回给上层一个 fd ,即使我们不主动关闭 fd,当进程退出时,进程的内核数据结构都会释放,fd也就会被释放,此时 OS 检测到struct file 的引用计数为0,就会释放该文件。
共享内存创建流程
创建共享内存 | shmget |
建立映射关系 | shmat |
传输信息 | 使用映射后的起始地址 |
删除映射关系 | shmdt |
删除共享内存 | shmctl |
优化:使用pipe来提供同步机制 |
创建共享内存:shmget
int shmget(key_t key, size_t size, int shmflg);
key | 使用 ftok 函数 来形成唯一 key(极大情况唯一),ftok 函数通过算法来形成的 key ,ftok 的参数随便写。 |
size | 开辟共享内存的空间大小,单位是字节,但是在内核(kernel)中,shm 的大小是以 4kb 为基本单位的,所以建议申请 4nkb |
shmflg | IPC_CREAT :shm 不存在则创建,反之获取该shm IPC_CREAT | IPC_EXCL:shm 不存在则创建,反之出错 |
返回值 | 成功返回 shm 的标识符,类似于 fd 失败返回-1 |
- OS 无法做到 key 的唯一性,必须由用户传入
- 不同的进程约定使用相同的 ftok 函数即可看到同一个共享内存shm
建立映射关系:shmat
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid | shm 的 id |
shmaddr | 用户指明将 shm 映射到哪个虚拟地址 填 nullptr 由 OS 自由选择 |
shmflg | 设置为 0 |
返回值 | 成功返回 shm 映射到的地址空间中的起始地址 失败返回(void*)-1 |
传输信息:
直接通过建立映射后的地址来读写
删除映射关系:shmdt
int shmdt(const void *shmaddr);
shmaddr | 映射到的地址空间的起始地址(shmat的返回值) |
返回值 | 成功返回0,失败返回-1 |
删除共享内存:shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid | shm 的 id |
cmd | 对 shm 的操作 IPC_RMID:删除 shm |
返回值 | 成功返回0,失败返回-1 |
优化:使用匹配提供同步机制
/**/
【注】
- 默认情况,shm 的读端不会管写端,共享内存不提供同步机制,会导致数据不一致
- shm 是所有进程间通信最快的
因为在建立映射的时候,我们拿到的是进程地址空间中共享区的起始地址,直接可以使用起始地址通过页表来找到 shm,不用使用系统调用!而管道则要通过使用系统调用来进行数据的读写,共享内存直接使用地址来进行读写。
- key 是用来让 OS 来区分 shm 的唯一性
- shmid 是用来在代码级 和 指令级 控制 shm 的
共享内存的指令
ipcs | 查看当前的消息队列、共享内存、信号量 -q 查看消息队列 -s 查看信号量 perms:shm的读写权限 nattch:几个进程与该shm关联 |
ipcrm | -m 删除共享内存 -q 删除消息队列 -s 删除信号量 |
代码实现
/*用于封装共享内存读端实现接口*/
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include "Log.hpp"
namespace my_read_shm
{
enum ExitError
{
FtokError = 1,
ShmgetError,
ShmatError,
ShmdtError,
ShmctlError,
MkfifoError,
OpenError,
ReadError
};
class Reader_Shm
{
public:
Reader_Shm(const std::string &pathname, const int id, int shm_size)
: _shmid(0)
, _size(shm_size)
, _key(0)
, _read_buffer(nullptr)
, _pipe_file("./file.pipe")
, _rfd(-1)
{
// 1. 构建shm唯一性标识key
_key = ftok(pathname.c_str(), id);
if (_key == -1)
{
_log.LogMessage(ClassFile, Error, "read ftok error : %s\n", strerror(errno));
exit(FtokError);
}
_log.LogMessage(ClassFile, Info, "read ftok success\n");
// 2. 创建共享内存
CreShm();
// 3. 建立映射关系
CreMap();
}
~Reader_Shm()
{
// 1. 删除映射关系
DelMap();
// 2. 删除共享内存
DelShm();
// 3. 关闭并删除pipe
DelPipe();
}
void Read()
{
TouchPipe();
char buffer[_size];
while (true)
{
read(_rfd, buffer, 1);
strcpy(buffer, (const char *)_read_buffer);
if (strcmp(buffer, "quit") == 0)
{
std::cout << "writer quit, so I quit" << std::endl;
break;
}
std::cout << "writer send# " << buffer << std::endl;
}
}
private:
// 创建共享内存
void CreShm()
{
_shmid = shmget(_key, _size, IPC_CREAT | IPC_EXCL);
if (_shmid < 0)
{
_log.LogMessage(ClassFile, Error, "read CreShm error : %s\n", strerror(errno));
exit(ShmgetError);
}
_log.LogMessage(ClassFile, Info, "read CreShm success\n");
}
// 创建映射关系
void CreMap()
{
_read_buffer = shmat(_shmid, nullptr, 0);
if (_read_buffer == (void *)-1)
{
_log.LogMessage(ClassFile, Error, "read CreMap error : %s\n", strerror(errno));
exit(ShmatError);
// 删除共享内存
DelShm();
}
_log.LogMessage(ClassFile, Info, "read CreMap success\n");
}
// 删除共享内存
void DelShm()
{
int ret_shmctl = shmctl(_shmid, IPC_RMID, nullptr);
if (ret_shmctl < 0)
{
_log.LogMessage(ClassFile, Error, "read DelShm error : %s\n", strerror(errno));
exit(ShmctlError);
}
_log.LogMessage(ClassFile, Info, "read DelShm success\n");
}
// 删除映射关系
void DelMap()
{
int ret_shmdt = shmdt(_read_buffer);
if (ret_shmdt < 0)
{
_log.LogMessage(ClassFile, Error, "read DelMap error : %s\n", strerror(errno));
DelShm();
exit(ShmdtError);
}
_log.LogMessage(ClassFile, Info, "read DelMap success\n");
}
// 创建命名管道
void TouchPipe()
{
// 建立命名管道用于同步机制
int ret_mkfifo = mkfifo(_pipe_file.c_str(), 0666);
if (ret_mkfifo < 0)
{
_log.LogMessage(ClassFile, Error, "read mkfifo error : %s\n", strerror(errno));
exit(MkfifoError);
}
_rfd = open(_pipe_file.c_str(), O_RDONLY);
if (_rfd < 0)
{
_log.LogMessage(ClassFile, Error, "read open error : %s\n", strerror(errno));
exit(OpenError);
}
}
// 关闭文件描述符,删除命名管道
void DelPipe()
{
close(_rfd);
unlink(_pipe_file.c_str());
}
private:
int _shmid; // 代码中操作使用的shmid
int _size; // shm空间大小
key_t _key; // shm在内核中的唯一性标识
void *_read_buffer; // shm的虚拟地址的起始地址
std::string _pipe_file; // 命名管道名称
int _rfd;
};
}
/*用于封装共享内存写端实现接口*/
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include "Log.hpp"
namespace my_write_shm
{
enum ExitError
{
FtokError = 1,
ShmgetError,
ShmatError,
ShmdtError,
ShmctlError,
OpenError,
WriteError
};
class Writer_Shm
{
public:
Writer_Shm(const std::string &pathname, const int id, int size)
: _shmid(0)
, _key(0)
, _size(size)
, _write_buffer(nullptr)
, _pipe_file("./file.pipe")
, _wfd(-1)
{
// 1. 构建shm唯一性标识key
_key = ftok(pathname.c_str(), id);
if (_key == -1)
{
_log.LogMessage(ClassFile, Error, "write ftok error : %s\n", strerror(errno));
exit(FtokError);
}
_log.LogMessage(ClassFile, Info, "write ftok success\n");
// 2. 获取 shm
GetShm();
// 3. 建立映射关系
CreMap();
}
~Writer_Shm()
{
// 删除映射
DelMap();
DelPipe();
}
void Write()
{
OpenPipe();
char buffer[1024];
while (true)
{
std::cout << "Please enter# ";
std::cin.getline(buffer, sizeof(buffer));
strcpy((char *)_write_buffer, buffer);
write(_wfd, buffer, 1);
if (strcmp(buffer, "quit") == 0)
{
std::cout << "writer exit" << std::endl;
break;
}
}
}
private:
// 获取shm
void GetShm()
{
_shmid = shmget(_key, _size, IPC_CREAT);
if (_shmid < 0)
{
_log.LogMessage(ClassFile, Error, "write GetShm error : %s\n", strerror(errno));
exit(ShmgetError);
}
_log.LogMessage(ClassFile, Info, "write GetShm success\n");
}
// 建立映射
void CreMap()
{
_write_buffer = shmat(_shmid, nullptr, 0);
if (_write_buffer == (void *)-1)
{
_log.LogMessage(ClassFile, Error, "write CreMap error : %s\n", strerror(errno));
exit(ShmatError);
}
_log.LogMessage(ClassFile, Info, "write CreMap success\n");
}
// 删除映射
void DelMap()
{
int ret_shmdt = shmdt(_write_buffer);
if (ret_shmdt < 0)
{
_log.LogMessage(ClassFile, Error, "write DelMap error : %s\n", strerror(errno));
exit(ShmdtError);
}
_log.LogMessage(ClassFile, Info, "write DelMap success\n");
}
// 打开命名管道文件
void OpenPipe()
{
_wfd = open(_pipe_file.c_str(), O_WRONLY);
if (_wfd < 0)
{
_log.LogMessage(ClassFile, Error, "write open error : %s\n", strerror(errno));
exit(OpenError);
}
}
// 关闭文件描述符
void DelPipe()
{
close(_wfd);
}
private:
int _shmid; // 代码中操作使用的shmid
key_t _key; // shm在内核中的唯一性标识
int _size;
void *_write_buffer; // shm的虚拟地址的起始地址
std::string _pipe_file;
int _wfd;
};
}
c. 消息队列(知道即可)
已经被淘汰了
d. 信号量
同步与互斥中重点介绍
四、重谈进程间通信
共享内存、消息队列和信号量是 OS 特意设计的,但是这三者本质却没有与“Linux下一切皆文件”的理念靠拢,所以根据时代的发展,逐渐边缘化。
而管道的这种方式,是使用了文件的,所以提供了同步的功能。