目录
- 进程间通信的介绍
- 文件级别通信原理
- 匿名管道通信原理
- 进行管道通信(代码)
- 管道的特点
- 实现进程池
- 命名管道
- system V通信–共享内存
- System V通信–共享内存、消息队列和信号量的关系
- 信号量
进程间通信的介绍
什么是进程间通信?
两个或者多个进程实现数据层面的交互。因为进程是具有独立性的,所以这必然会导致进程通信的成本比较高
为什么要进程间通信?
有些时候我们是需要多进程协同完成某种业务内容的
怎么实现进程间通信?
a.进程间通信的本质:必须让不同的进程看到同一份“资源”—“资源”是什么?是特定形式的内存空间
b.这个“资源”是谁提供?一般是操作系统。为什么不是我们两个进程中的一个呢?假设一个进程提供,那么这个资源属于谁?而且进程是具有独立性的,这样做必然会破坏进程的独立性
c.我们进程访问这个空间进行通信,本质就是访问操作系统,所以系统提供了系统调用接口。文件系统IPC通信模块定制的标准:POSIX–让通信可以跨主机、systemV–聚焦在本地通信(其中有共享内存、消息队列、信号量)
d.基于文件级别的通信方式–管道
文件级别通信原理
我们把不用打开磁盘文件,操作系统自己创建的struct_file叫做内存级文件
匿名管道通信原理
如何让两个进程看到同一份管道文件呢?fork创建子进程,这种方式形成的管道叫匿名管道
站在内核的角度看原理
因为只能进行单向通信,所以叫管道。
父子进程能管道通信,那么兄弟之间行不行?孙子之间行不行?都行,只要有血缘关系就行
进行管道通信(代码)
介绍pipe接口
pipefd[2]为输出型参数,pipe[0]对应的是读端文件描述符,pipe[1]对应的是写端文件描述符
成功返回0,错误返回-1
int main()
{
int pipefd[2];
int n = pipe(pipefd);
cout << "pipefd[0]:" << pipefd[0] << " " << "pipefd[1]:" << pipefd[1] << endl ;
return 0;
}
打印的结果为
写一个父子进程匿名管道通信的代码
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pipefd[2];
int n = pipe(pipefd);
if (n < 0)//一定要养成打印错误信息的好习惯,因为这是以后代码差错的关键
{
perror("create pipe failed");
exit(2);
}
pid_t pid = fork();
if (pid == 0)//子进程
{
close(pipefd[1]);//子进程关闭写端,只进行读取
char readBuffer[1024];
int m = read(pipefd[0], readBuffer, sizeof(readBuffer) - 1);//为什么要减1?返回的m最多是1023
if (m <= 0)
{
perror("read failed");
exit(1);
}
readBuffer[m] = 0;//如果不减一,则m可能是1024,readBuffer[1024]=0则会越界访问,这就是为什么要减一
printf("child process:%d getMessage:%s\n", getpid(), readBuffer);
close(pipefd[0]);//退出进程前,关闭读端文件描述符,当然也可以不关,因为进程退出会自动关闭描述符
exit(0);
}
//父进程
close(pipefd[0]);
char writeBuffer[1024];
snprintf(writeBuffer, sizeof(writeBuffer), "I am father proceess:%d", getpid());
printf("father process:%d send message\n", getpid());
write(pipefd[1], writeBuffer, strlen(writeBuffer));
int status = 0;
n = waitpid(pid, &status, 0);//获取子进程状态信息,0~6位存储了信号的信息,8~15位存储了退出码信息
if (n < 0)
{
perror("waitpid failed");
exit(3);
}
printf("father process waitpid success, child process exitcode: %d, sig: %d\n", (status>>8) & 0xFF, status & 0x7F);
return 0;
}
运行结果
匿名管道的特点
1.具有血缘关系的进程进行进程间通信
2.管道只能单向通信
3.父子进程是会进程协同、同步与互斥的---->能保护管道文件的数据安全
4.管道是面向字节流的
5.管道是基于文件的,而文件的生命周期是随进程的
6.管道是有固定大小的
验证管道是有固定大小
int main()
{
int pipefd[2];
int n = pipe(pipefd);
int count = 0;
while (true)
{
char c = 'a';
write(pipefd[1], &c, 1);
count++;
cout << count << endl;
}
return 0;
}
执行结果
所以管道的固定大小是65536个字节,64KB
命令行ulimit -a看到的pipe size是512*8=4096字节,这又是什么呢?
用man -7 pipe查看手册
这个是PIPE_BUF的大小,小于PIPE_BUF则是原子的读写操作
管道文件数据安全的体现,管道的4种情况
1.读写端正常,管道如果为空,读端就要阻塞
2.读写端正常,管道如果被写端,写端就要阻塞
3.读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾, 不会被阻塞
4.写端是正常写入,读端关闭了,操作系统就要杀掉正在写入的进程。如何杀掉?通过信号杀掉–因为读端关闭,写端将会毫无意义,操作系统是不会做低效浪费等类似的工作的
证明第4点
父进程关闭读端,子进程写内容。父进程获取子进程退出信息
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pipefd[2];
int n = pipe(pipefd);
if (n < 0)
{
perror("create pipe failed");
exit(2);
}
pid_t pid = fork();
if (pid == 0)//子进程
{
close(pipefd[0]);
char writeBuffer[1024];
printf("child process:%d send message\n", getpid());
snprintf(writeBuffer, sizeof(writeBuffer), "I am child proceess:%d", getpid());
write(pipefd[1], writeBuffer, strlen(writeBuffer));
sleep(3);//3秒后子进程关闭读端文件描述符
close(pipefd[1]);
exit(0);
}
//父进程
close(pipefd[1]);
close(pipefd[0]);
int status = 0;
n = waitpid(pid, &status, 0);
if (n < 0)
{
perror("waitpid failed");
exit(3);
}
printf("waitpid child success:exitcode: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
close(pipefd[0]);
return 0;
}
运行结果
父进程获取的子进程退出信号是13号,为管道的退出信号
命令行管道实现原理
ls进程的输出重定向到管道的写端,grep的输入重定向到管道的读端
实现进程池
池是什么意思?如你去打水,每天都要走两公里,每天都要走两公里,效率太低。你则修了个池子,一次就打很多水存到池子里,则不用天天打水,直接到池子里取水即可
操作系统也是一样的,系统调用函数效率很低,所以为了不用频繁调用某些系统调用接口如fork函数,所以有了进程池的概念。
进程池原理
进程池代码
//processPool.hpp文件
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <vector>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
namespace processPoolspace
{
int gnums = 10;//进程池进程的数量
//如果管理进程池?先描述,再组织
struct subproc
{
subproc(int writefd, int pid, string processName = "")
: _processName(processName), _writefd(writefd), _pid(pid)
{}
string _processName;//进程的名字
int _writefd;//进程对应管道的写端
int _pid;//进程的pid
};
class processPool
{
public:
processPool()
{}
void processPoolInit()
{
vector<int> oldfd;//子进程会继承父进程的写端,将父进程的写端记录下来
for (int i = 0; i < gnums; ++i)
{
int pipefd[2];
int m = pipe(pipefd);
if (m < 0)
{
perror("pipe create failed");
exit(1);
}
int n = fork();
if (n < 0)
{
perror("fork failed");
exit(1);
}
if (n == 0)
{
close(pipefd[1]);//子进程关闭写端
while (1)
{
//创建子进程先关闭从父进程继承下来的管道的写端
for (int i = 0; i < oldfd.size(); ++i)
{
close(oldfd[i]);
}
char readBuffer[1024];
int m = read(pipefd[0], readBuffer, sizeof(readBuffer) - 1);//读取父进程发过来的任务
if (m > 0)
{
readBuffer[m] = 0;
cout << "进程:"<< i + 1 <<"处理任务:"<< readBuffer << endl;
}
else if (m == 0)
{
//m == 0说明了写端关闭,则子进程也要关闭读端并结束程序了
cout << "进程:" << i + 1 << "写端关闭, me too" << endl;
close(pipefd[0]);
exit(0);
}
else
{
perror("read failed");
close(pipefd[0]);
exit(2);
}
}
}
close(pipefd[0]);//父进程关闭读端
string processName = "processName:" + to_string(i + 1);
nums.push_back(subproc(pipefd[1], n, processName));//将对象存入数组中,以后对进程池的管理就变成了对数组的管理
oldfd.push_back(pipefd[1]);//记录目前父进程的所有连接管道的写端
}
}
void quitProcess()
{
for (int i = 0; i < nums.size(); ++i)
{
close(nums[i]._writefd);
int n = waitpid(nums[i]._pid, nullptr, 0);
if (n < 0)
{
perror("waitpid failed");
exit(1);
}
else
{
cout << "waitpid:" << nums[i]._processName << "success" << endl;
}
}
}
public:
vector<subproc> nums;//对进程池的管理转换成了对数组的管理
};
}
//process.cc文件
#include "processPool.hpp"
#include <string.h>
using namespace std;
using namespace processPoolspace;
//任务菜单
void Menu()
{
std::cout << "################################################" << std::endl;
std::cout << "# 1. 刷新日志 2. 刷新出来野怪 #" << std::endl;
std::cout << "# 3. 检测软件是否更新 4. 更新用的血量和蓝量 #" << std::endl;
std::cout << "# 0. 退出 #" << std::endl;
std::cout << "#################################################" << std::endl;
}
//派发任务
void Task(const processPool& pp)
{
vector<string> task = {"", "刷新日志", "刷新出来野怪", "检测软件是否更新", "更新用的血量和蓝量"};
int input = 0;
int m = 0;
while (1)
{
Menu();
cin >> input;
if (input <= 0 || input >= 5)
{
break;
}
subproc process = pp.nums[rand() % pp.nums.size()];//随机挑选进程
cout << "进程" << process._processName << "处理任务" << input << endl;
m = write(process._writefd, task[input].c_str(), task[input].size());
if (m < 0)
{
perror("write failed");
continue;
}
}
}
int main()
{
//随机数,随机挑选进程来执行任务
srand(time(NULL));
processPool pp;
//初始化进程
pp.processPoolInit();
Task(pp);
//执行完任务后退出进程
pp.quitProcess();
return 0;
}
命名管道
匿名管道通信的前提是有血缘关系的两个进程,而命名管道可以在毫不相干的两个进程间通信
命令mkfifo [文件名] 创建命名管道
ehco输出重定向到管道
会阻塞
cat输入重定向到管道
可以看到达到了echo进程和cat进程通信的目的
问题:两个进程看到的同一份资源是什么?文件。你们两个进程怎么知道打开的是同一文件?
因为是同路径下同一个文件名,而路径+文件名具有唯一性
mkfifo函数,第一个参数是文件名,第二个参数是创建文件的权限
unlink函数,删除某一文件
不同进程的通信代码
//comm.hpp代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
void Init()
{
//在当前路径下创建一个管道文件
int n = mkfifo("./fifo", 0666);
if (n < 0)
{
perror("mkfifo failed");
exit(1);
}
}
void Writer()
{
int fd = open("fifo", O_WRONLY | O_TRUNC);
if (fd < 0)
{
perror("open failed");
exit(2);
}
while (1)
{
string s;
cout << "Enter#";
getline(cin, s);
int m = write(fd, s.c_str(), s.size());
if (m < 0)
{
perror("write failed");
exit(1);
}
}
}
void Read()
{
int fd = open("fifo", O_RDONLY);
if (fd < 0)
{
perror("open failed");
exit(2);
}
while (1)
{
char buffer[1024];
int n = read(fd, buffer, sizeof(buffer) - 1);
if (n < 0)
{
perror("read failed");
exit(1);
}
if (n == 0)
{
cout << "读端退出, 我也退出" << endl;
exit(1);
}
buffer[n] = 0;
cout << buffer << endl;
}
}
void Destroy()
{
//结束时删除管道文件
unlink("fifo");
}
//server.cc代码
#include “comm.hpp”
int main()
{
Init();
Read();
Destroy();
return 0;
}
//client.cc代码
#include "comm.hpp"
int main()
{
Writer();
return 0;
}
执行进程
若只打开管道的读端,写端没有打开。则会阻塞
若只打开管道的写端,读端没有打开。则会阻塞
有一端没打开,open就会阻塞
都打开了则可以开始通信了
system V通信–共享内存
通信的本质:让不同的进程看到同一份资源
共享内存的原理:
上面的操作都是进程做的不是,原因一样,进程管的是自己,不管别人。是由操作系统做的,所以操作系统一定会提供对应的系统调用接口
shmget函数的作用是申请一个共享内存
第一个key稍后再说,第二个size是申请共享内存的大小,第三个shmflg是选项,二进制标志位
最常用的两个选项
IPC_CREAT:如果不存在就创建它,如果存在就获取它
IPC_CREAT | IPC_EXCL:IPC_EXCL无法单独使用,如果不存在就创建它,如果存在就出错返回
再谈谈key:
1.key是一个数字,这个数字是几不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
2.第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了
3.对于已经创建好的共享内存,key在哪?key在共享内存的描述对象中
4.如何有这个key呢?
ftok函数不同的参数保证算出的key是不同的
第一个参数是文件名(可以为任意路径),第二个参数是id(一字节内的范围随便填)
问题:为什么系统不直接生成一个key呢?
如果系统生成了这个key,那这个进程如何把这个key交给另一个进程呢?所以这和另一个进程相当于是个约定,只要进程知道pathname和id就可以得到相同的key
共享内存的通信代码
第一个参数shmid是共享内存标识符,
第二个参数最常用的两个选项IPC_STAT获取共享内存的属性,IPC_RMID删除共享内存
第三个参数为输出型参数,配合第二个参数选项的IPC_STAT选项获取属性,如果不想获取属性可以填NULL
shmat函数对共享内存进行关联
第一个参数是共享内存标识符
第二个参数是你想把共享内存映射到共享区的哪个位置,一般不用填,填NULL即可
第三个参数你拿到共享内存后的权限,默认填0即可
shmdt函数对共享内存进行去关联
参数是该共享内存的起始地址
上面两个函数类似于C语音的malloc和free
//comm.hpp文件
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
const string path = "/home";//可以随便填
const int SIZE = 4096;//共享内存的大小
const int proj_id = 0x666;//随便填
//获取key值
int GetKey()
{
key_t key = ftok(path.c_str(), proj_id);
if (key < 0)
{
perror("ftok failed");
exit(1);
}
return key;
}
//创建共享内存
int CreateShm()
{
key_t key = GetKey();
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0)
{
perror("create shared memory failed");
exit(1);
}
return shmid;
}
//获取共享内存
int GetShm()
{
key_t key = GetKey();
int shmid = shmget(key, SIZE, IPC_CREAT);
if (shmid < 0)
{
perror("get shared memory failed");
exit(2);
}
return shmid;
}
//Server.cc的代码
#include "comm.hpp"
int main()
{
//创建共享内存
int shmid = CreateShm();
//和共享内存关联
char* start = (char*)shmat(shmid, NULL, 0);
while (1)
{
//读取共享内存的内容
printf("读取的内容是:%s\n", start);
sleep(1);
}
//和共享内存去关联
shmdt(start);
//释放共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
//client.cc的代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include "comm.hpp"
int main()
{
//获取同一份资源
int shmid = GetShm();
//和共享内存关联
char* start = (char*)shmat(shmid, NULL, 0);
const char* ch = "hello, I am process a";
string s = "hello, I am process a";
for (int i = 0; i < 10; ++i)
{
s += to_string(i);
//直接进行通信,不用缓冲区,直接拷贝到共享内存
memcpy(start, s.c_str(), s.size());
sleep(1);
}
//通信完成,和共享内存去关联
shmdt(start);
return 0;
}
运行结果
你可能在运行时出现了这种情况,原因是你之前创建的共享内存没有释放
那要怎么释放呢?
ipcs -m命令可以查看正在使用的共享内存
右边的数字是正在与此共享内存关联的进程有几个
ipcrm -m [shmid]可以删除该共享内存
查看共享内存的属性代码
#include "comm.hpp"
int main()
{
//创建共享内存
int shmid = CreateShm();
//和共享内存关联
char* start = (char*)shmat(shmid, NULL, 0);
while (1)
{
//读取共享内存的内容
printf("读取的内容是:%s\n", start);
struct shmid_ds statIpc;
shmctl(shmid, IPC_STAT, &statIpc);
cout << "shm size: " << statIpc.shm_segsz << endl;
cout << "shm nattch: " << statIpc.shm_nattch << endl;
printf("0x%x\n", statIpc.shm_perm.__key);
cout << "shm mode: " << statIpc.shm_perm.mode << endl;
shmdt(start);
sleep(1);
}
//和共享内存去关联
shmdt(start);
//释放共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
运行结果
当有其他进程也来关联的时候可看到关联数有1变为了2
共享内存的特性
1.共享内存没有同步互斥之类的保护机制
2.共享内存是所有的进程间通信中速度最快的
System V通信–共享内存、消息队列和信号量的关系
进程间通信本质:让不同的进程看到同一份资源
而这同一份资源的类别决定了你是什么样的通信方式,如:同一份资源是文件缓冲区—>管道。同一份资源是内存块—>共享内存。同一份资源是队列—>消息队列
消息队列原理
1.让不同的进程看到同一个队列
2.允许不同的进程向内核中发送带类型的数据块
消息队列的接口
和共享内存通信系统提供的接口很像近
进行发送数据块和接收数据块的接口
因为被历史淘汰了,所以不做过多介绍,知道有即可
同样信号量也有一批类似的接口
shmid、msgid、semid究竟是什么呢?
信号量
讲一些概念
数据不一致问题:A进程正在写入,写入了一部分,就被B进程拿走了,导致双方发和收的数据不完整,如共享内存会遇到。
共享资源:A和B进程看到的同一份资源。如果不加保护,会导致数据不一致问题
加锁、互斥:任何时刻,只允许一个执行流访问共享资源
临界资源:共享的,任何时刻只允许一个执行流访问的资源
临界区:正在访问临界资源的代码(客观意义上的正在访问临界资源,不是主观意义的访问临界资源的代码,一般是很少一部分的代码)
理解信号量
当我们看电影的时候,是先买票再去看电影,买票的时候我们看到了还剩100张票,买到票后就还剩99张票了,其中有一张票属于了我自己,即已经对电影院的其中一个座位(资源)进行了预订。
信号量就是计数器,记录资源(票)还剩多少。每买一张票,计数器就要减1,电影院里的资源就少一个。计数器为0就代表没有资源能申请了
信号量的好处是什么?
通过信号量申请到临界资源以后,其他进程也都申请到临界资源以后,就能并发的访问公共资源,因为访问的不是同一个公共资源,不会导致数据不一致的问题。效率高
扩展一下:如果电影院里面只有一个座位呢,我们只需要一个值为1的计数器,只有一个人能抢到票,只有一个人能进电影院,看电影期间只有一个执行流在访问临界资源–这就是互斥的概念
我们把值只能为0,1两态的计数器叫做二元信号量—本质就是一个锁
思考:要访问临界资源,先要申请信号量计数器资源,信号量计数器不也是共享资源吗??
有一百张票,每一人申请票就少一个。用代码表示int cnt = 100; cnt–;
而cnt–;c语言上是一条语句,汇编语言是三条语句。
1.将cnt变量的内容从内存中拿取CPU寄存器里面
2.CPU内进行减减操作
3.将CPU内的计算结果写回cnt变量的内存的位置
在这三步中的任何一步,都有可能进程在允许的时候,随时被切换走。是发生数据不一致问题
那么信号量如何保证自己的安全呢?在其内部将这三步设置为原子操作,即要么全部完成,要么不完成
申请信号量,本质是对计数器–,称为P操作
释放资源,释放信号量,本质是对计数器进行++操作,称为V操作
申请和释放PV操作–是原子的!
信号量凭什么是进程间通信的一种?
1.通信不仅仅是通信数据,互相协同也是
2.要协同,本质也是通信,信号量首先要被所有的通信进程看到!
关于信号量的接口及其使用部分,在多线程部分再进行操作说明