进程间通信
1. 理解进程间通信
1.0 进程间通信的前提
- 因为进程是具有独立性的,两个进程之间想要通信时无疑增加了通信的成本
- 要让两个不同的进程,进行通信的前提条件: 先让两个进程,看到同一份"资源" (OS直接或间接提供)
- 任何进程通信手段:
(1) 想办法,先让不同的进程看到同一份资源
(2) 让一方写入,一方读取,完成通信过程,至于通信目的与后续工作,要结合具体场景
1.1 进程间通信的目的
- 数据传输: 一个进程需要将它的数据发送给另一个进程
- 资源共享: 多个进程之间共享同样的资源。
- 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.2 进程间通信发展
- 管道
- System V进程间通信
- POSIX进程间通信
1.3 进程间通信分类
- 管道
匿名管道pipe
命名管道
- System V IPC
System V 消息队列
System V 共享内存
System V 信号量
- POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
比
2. 管道
2.0 什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
而对于管道,分为两种:一种是匿名管道、另一种是命名管道。
2.1 匿名管道
2.1.1 管道的原理
在文件系统中,有这样的结构:PCB—task_struct(进程控制块)中包含一个files指针,他指向一个属于进程和文件对应关系的一个结构体:
struct files_struct
,而这个结构体里面包含了一个数组叫做struct file* fd _array[]
的指针数组, 可以通过特定的文件描述符找到磁盘加载到内存中对应的文件。fork创建创建子进程后,不会拷贝磁盘中的文件,而是拷贝一份struct files_struct同样指向父进程对应的
struct file
struct file是从磁盘加载到内存的,而父子进程的每一次写入,struct file不会从内存中刷新到磁盘,虽然通过一定的操作是可行的,但进程与进程之间的通信是从内存到内存的,没有必要牵扯到磁盘。一旦刷新到磁盘,就会大大降低通信的速度。所以管道文件是一个内存级别的文件,不会进行磁盘刷新。
如何让两个进程看到同一个管道文件呢?---->通过fork创建子进程完成。但当前这个管道文件并没有名字,所以被称为匿名管道。
整个过程:
为什么父进程要以读和写两种方式打开同一个文件?只有父进程打开读和写,产生的文件描述符才会被子进程继承,子进程才能有读和写的功能。以一种方式打开读写端时,被继承下去后最终看到的是同性质的读写端,两个读或两个写时双方是无法进行数据交互的。
2.1.2 管道的实例
函数介绍
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码比特
实例
创建管道,让父进程进行读取,子进程进行写入,实现两个进程间的管道通信
#include<cerrno>
#include<iostream>
#include<unistd.h>
#include<string.h>
#include<string>
#include<assert.h>
#include<sys/types.h>
using namespace std;
int main()
{
//让不同的进程看到同一份资源!!!
//任何一种进程间的通信中, 一定先要保证不同的进程之间看到同一份资源
int pipefd[2]={0};
//1. 创建管道
int n=pipe(pipefd);
if(n<0)
{
cout<<"pipe error, "<< errno << ": " << strerror(errno)<<endl;
return 1;
}
cout<<"pipefd[0]: " << pipefd[0] <<endl; //读端 0 -> 嘴巴 -> 读书
cout<<"pipefd[1]: " << pipefd[1] <<endl; //写端 1 -> 笔 -> 写东西
//2. 创建子进程
pid_t id=fork();
assert(id != -1); //正常应该用判断, 这里用断言: 意料之外用if, 意料之中用assert
//子进程
if(id==0)
{
//3. 关闭不需要的fd, 让父进程进行读取,子进程进行写入
close(pipefd[0]);
//4. 开始通信 -- 结合某种场景
const string namestr="hello, 我是子进程";
int cnt=1;
char buffer[1024];
while(true)
{
snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++,getpid());
write(pipefd[1], buffer,strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//3. 关闭不需要的fd, 让父进程进行读取,子进程进行写入
close(pipefd[1]);
//4. 开始通信 -- 结合某种场景
char buffer[1024];
while (true)
{
int n= read(pipefd[0], buffer, sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
cout<<"我是父进程, child give me message:"<< buffer <<endl;
}
}
close(pipefd[0]);
return 0;
}
运行结果:
上述代码的子进程没有打印任何的消息,而是我们的父进程获取读取消息并打印出来,这种通信就被成为管道通信。
根据上述代码,结合4种场景:
- 如果我们read读取完毕了所有的管道数据, 如果对方不发,我就只能等待
- 如果我们writer端将管道写满了,我们还能写吗?不能
- 如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾
- 写端一直写,读端关闭,会发生什么呢? 没有意义。OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程!OS会通过信号来终止进程(13, SIGPIPE)。
2.1.3 管道的特点
- 单项通信,管道是半双工的一种特殊情况
- 管道的本质是文件,因为fd(文件描述符)的生命周期随进程, 所以管道的生命周期也是随进程的
- 管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信,常用于父子通信。pipe函数打开管道时,并不清楚管道的名字,所以叫匿名管道。
- 在管道通信中,写入的次数和读取的次数不是严格匹配的,读写次数的多少没有强相关,表现为字节流
- 具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信,管道自带同步机制
2.1.4 匿名管道实现进程池
用管道实现父进程控制多个子进程
父进程可以实现向任意一个子进程中写入,我们可以让父进程向任何进程中写入一个四字节的命令操作码,称之为command,即现在想让哪一个进程运行,就向哪一个进程发送数据,举个例子:如果发送是1,就让子进程下载,发送是2,就让子进程做特定的计算……;那为什么可以这样随意控制子进程是否运行呢?这是因为如果我们不将数据写入或者写的慢,那么子进程就需要等,产生阻塞,所以根据这样的思想设计如下代码:
task.hpp
#pragma once
#include<cerrno>
#include<iostream>
#include<unistd.h>
#include<vector>
#include<string>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
using namespace std;
typedef void (*fun_t)(); //函数指针
void PrintLog()
{
cout<<"pid: "<< getpid()<<"打印日志任务, 正在被执行..."<<endl;
}
void InsertMySQL()
{
cout<< "执行数据库任务,正在被执行..." <<endl;
}
void NetRequest()
{
cout << "执行网络请求任务,正在被执行..." <<endl;
}
//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
if(command>=0 && command<funcs.size()); funcs[command]();
}
~Task()
{}
public:
vector<fun_t> funcs;
};
ctrlProcess.cc
#include "task.hpp"
const int gnum=3; //代表要创建几个子进程(创建几个管道)
Task t;
//父进程要管理自己创建的管道和进程 --- 先描述再组织
class EndPoint
{
private:
static int number;
public:
pid_t _child_id;
int _write_fd; //文件描述符
string processname; //设置的进程名
public:
EndPoint(int id,int fd): _child_id(id), _write_fd(fd)
{
//process-0[pid:fd] //进程名要设置的格式
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
processname=namebuffer;
}
const string name()const
{
return processname;
}
~EndPoint()
{}
};
int EndPoint::number=0;
//子进程要执行的方法
void WaitCommand()
{
while (true)
{
int command=0; //整数转成要执行哪个任务
int n = read(0, &command, sizeof(int)); // 从标准输入中读
if (n == sizeof(int))
{
t.Execute(command);
}
else if (n == 0)
{
cout<<"父进程让我退出,我就退出了: "<<getpid()<<endl;
break;
}
else
{
break;
}
}
}
void createProcesses(vector<EndPoint>* end_points)
{
vector<int> fds;
for (int i = 0; i < gnum; ++i)
{
// 1.1 创建管道
int pipefd[2] = {0}; //先有管道, 再有子进程
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 1.2 创建进程
pid_t id = fork();
assert(id != -1);
// 一定是子进程
if (id == 0)
{
// 建立真正的单进程通信
for(auto&fd: fds)
{
close(fd);
}
// 1.3 关闭不需要的fd
close(pipefd[1]);
// 我们期望, 所有的子进程读取"指令"的时候, 都从标准输入读取
// 1.3.1 输入重定向(可以不做)
dup2(pipefd[0], 0);
// 1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
// 一定是父进程
// 1.3 关闭不需要的fd
close(pipefd[0]);
// 1.4 将新的子进程和它的管道写端, 构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]); //保存写端的文件描述符
}
}
//显示面板
int ShowBoard()
{
cout<<"#########################################"<<endl;
cout<<"### 0. 执行日志任务 1. 执行数据库任务 ###"<<endl;
cout<<"### 2. 执行请求任务 3. 退出 ###"<<endl;
cout<<"#########################################"<<endl;
cout<<"请选择: ";
int command=0;
cin>>command;
return command;
}
void ctrlProcess(const vector<EndPoint> &end_points)
{
//我们可以自动化的, 也可以搞成交互式的
int cnt=0;
int num=0;
while(true)
{
//1. 选择任务
int command=ShowBoard();
if(command==3) break;
if(command<0 || command>2) continue;
//2. 选择进程
//随机数式的
//int index=rand()%end_points.size();
//按顺序轮询式的
int index=cnt++;
cnt%=end_points.size(); //要保证是轮询的(当数组越界会置0)
cout<<"选择进程: "<< end_points[index].name() <<" | 处理任务: " <<command<<endl;
//3. 下发任务
write(end_points[index]._write_fd, &command,sizeof(command));
sleep(1);
}
}
void waitProcess(const vector<EndPoint>& end_points)
{
//回收所有进程
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的 write fd就可以
//对应场景: 如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾
//2. 建立真正的单进程通信
for(int end= 0;end<end_points.size(); end++)
{
cout<< "父进程让子进程退出:" << end_points[end]._child_id<<endl;
close(end_points[end]._write_fd);
waitpid(end_points[end]._child_id,nullptr,0);
cout<< "父进程回收了子进程:" <<end_points[end]._child_id<<endl;
}
sleep(10);
}
int main()
{
// 1. 先进行构建控制结构, 父进程写入, 子进程读取
vector<EndPoint> end_points; //管理进程的结构
createProcesses(&end_points);
//2. 我们得到了什么? end_points
ctrlProcess(end_points);
//3. 处理所有退出问题
waitProcess(end_points);
return 0;
}
运行测试:
2.2 命名管道
2.2.1 什么是命名管道
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件
2.2.2 创建一个命名管道
(1) 命令行创建命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo 文件名
左侧将打印的信息重定向到fifo管道文件中,右侧cat作为进程再把fifo管道数据读了进来,通过这种方式,就完成了命令行式的进程间通信。我们会发现即便向fifo文件中写了相关的内容, 但是它的大小依然是0。因为只是管道文件fifo在这里仅仅是一种符号, 向文件写消息时并不会刷新落实到磁盘上, 只是写入到fifo, 管道文件fifo又是内存级别文件,所以大小没有改变
(2) 程序内创建删除命名管道
命名管道也可以从程序里创建和删除,相关函数:
创建命名管道:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);//路径,权限码
成功返回0,失败返回-1(失败错误码errno被设置)
删除命名管道:
#include <unistd.h>
int unlink(const char *path);//路径
成功返回0,失败返回-1(失败错误码errno被设置)
2.2.3 深入理解命名管道
要想让两个进程之间进行通信,就需要有一份共享的资源,匿名管道以继承的方式拥有共同的文件(文件地址具有唯一性),那么命名管道是如何让不同的进程看到同一份资源的呢?
让不同的进程通过文件路径+文件名看到同一个文件, 并打开就是看到了同一个资源, 即具备了进程间通信的前提。
2.2.4 匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
2.2.5 用命名管道实现server&client通信
common.hpp
#pragma once
#include<cerrno>
#include<cstring>
#include<iostream>
#include<string>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include<stdio.h>
#include<assert.h>
using namespace std;
#define NUM 1024
const string fifoname="./fifo";
uint32_t mode=0666;
server.cc
#include "common.hpp"
int main()
{
umask(0); //这个设置并不影响系统的默认配置, 只会影响当前进程
//1. 创建管道文件, 而我们今天只需创建一次
int n=mkfifo(fifoname.c_str(),mode);
if(n!=0) //创建失败
{
cout<<errno<<" : "<<strerror(errno)<<endl;
return 1;
}
cout<< "create fifo success, begin success" <<endl;
//2. 让服务端直接开启管道文件
int rfd=open(fifoname.c_str(), O_RDONLY); //打开管道文件时一定会存在
if(rfd<0)
{
cout<<errno<<" : "<<strerror(errno)<<endl;
return 2;
}
cout<< "open fifo success, begin ipc" <<endl;
//3. 正常通信 --- 服务端读
char buffer[NUM];
while(true)
{
buffer[0]=0;
ssize_t n=read(rfd,buffer,sizeof(buffer)-1); //当成字符串看
if(n>0) //读成功了
{
buffer[n]=0;
cout<< "client# " << buffer << endl;
}
else if(n==0) //客户端关闭了写端
{
cout<< "client quit, me too " << endl;
break;
}
else
{
cout<<errno<<" : "<<strerror(errno)<<endl;
break;
}
}
//4. 关闭不要的fd
close(rfd);
//5. 删掉刚被创建的管道文件, 不影响下次的正常退出
unlink(fifoname.c_str());
return 0;
}
client.cc
#include "common.hpp"
int main()
{
//需不需要创建管道文件, 不需要, 我只需要打开对应的文件即可! --- 打开同一份文件
//1. 客户端以写方式打开文件
int wfd=open(fifoname.c_str(), O_WRONLY);
if(wfd<0)
{
cerr<<errno<<" : "<<strerror(errno)<<endl;
return 1;
}
//2. 正常通信 --- 客户端写
char buffer[NUM]; //缓冲区
while(true)
{
cout<<"请输入你的消息# ";
char*msg=fgets(buffer,sizeof(buffer),stdin); //不用减1, 不知道是否要减1时统一处理成减1
assert(msg);
(void)msg; //保证以release发布时, msg是被使用的
buffer[strlen(buffer)-1]=0; //过滤'\n'
// abcde\n\0
// 012345
if(strcasecmp(buffer,"quit")==0) break; //比较字符串
ssize_t n=write(wfd,buffer,strlen(buffer));
assert(n>=0);
(void)n;
}
//3. 关闭不要的fd
close(wfd);
return 0;
}
运行测试:
3. System V共享内存
3.1 共享内存的原理
学习进程地址空间后,我们知道进程间是具有独立性的, 每个进程的内核数据结构通过页表映射到物理内存上都是独立的。那么为了让不同的进程进行通信,首先需要两个进程看到同一份资源,站在进程地址空间的角度理解共享内存。
让两个不同进程通信, 进行3个工作:
- 在物理内存上申请一块空间
- 通过返回对应空间的起始地址将申请的空间映射到进程地址空间
- 当不想通信时:
- 取消掉进程和内存之间的映射关系
- 释放申请的内存
我们把申请的这块空间称之为共享内存,将映射关系称之为进程和共享内存进行挂接。将取消进程和内存的映射关系称之为去关联,释放内存释放的就是共享内存。
补充:
- 共享内存和malloc有区别:
malloc是先在堆上申请虚拟空间, 当真正需要使用时建立映射关系, 物理内存分配空间;
共享内存是通过开辟一块物理空间,分别映射至通信进程的虚拟地址空间中。
- 共享内存是一种通信方式,所有想通信的进程都可以用,所以操作系统中可能会同时存在很多个共享内存
3.2 共享内存的相关命令
3.2.1 查看共享内存 ipcs -m
perms代表共享内存的权限,nattch代表共享内存关联进程个数
3.2.2 删除共享内存 ipcrm -m + 共享内存的shmid
3.3 共享内存函数
3.3.0 形成key - ftok
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数
pathname: 路径字符串
proj_id: 项目id
成功时,将返回key。失败返回-1(失败错误码errno被设置为系统调用出错)
深入理解key
针对shmget函数的第一个参数key
OS中可以用shm来进行通信,并不是只能有一对进程使用共享内存 => 所以在任意一个时刻,可能会有多个共享内存被用来进行通信 => 所以,系统中可能会同时存在多个shm,OS需要整体管理共享内存 => OS如何管理呢?
先描述再组织 => 所以共享内存不是我们想的那样,只要在内存中开辟空间即可,系统也要为了管理shm, 构建对应的描述共享内存的结构体对象 => 共享内存 = 共享内存的内核数据结构 + 真正开辟的内存空间
通信的前提是看到相同的资源, 通过传入相同的pathname和proj_id得到相同的key,从而找到同一块共享内存,实现进程间通信。
key vs shmid
类比: 文件的fd和文件的inode编号
key: 本质是在内核中使用的
shmid:对shm的未来所有操作在用户层, 都使用shmid
通过key和shmid的区分,能够面向系统层面和用户层面,这样能够更好的进行解耦,以免内核中的变化影响到用户级。
3.3.1 创建共享内存 - shmget
功能:用来创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
关于shmflg参数:
IPC_CREAT and IPC_EXCL
单独使用 IPC_CREAT: 创建一个共享内存, 如果共享内存不存在, 就创建之; 如果已经存在, 获取已经存在的共享内存并返回
IPC_EXCL不能单独使用, 一般都要配合 IPC_CREAT
IPC_CREAT | IPC_EXCL: 创建一个共享内存, 如果共享内存不存在, 就创建之; 如果已经存在, 则立马出错返回
IPC_CREAT | IPC_EXCL: 如果创建成功, 对应的shm, 一定是最新的!
3.3.2 关联共享内存 - shmat
功能:将共享内存段连接到进程地址空间
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址。 这里可以设为nullptr
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY。这里给0默认可以读写
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
3.3.3 共享内存去关联 - shmdt
功能:将共享内存段与当前进程脱离
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意: 将共享内存段与当前进程脱离不等于删除共享内存段
现象:
创建共享内存的进程已经早就退出了, 但是我们发现共享内存一定还存在
为什么? 共享内存的生命周期随OS,不随进程
3.3.4 控制(主要用于删除)共享内存 - shmctl
功能:用于控制共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
关于cmd参数:
3.4 利用共享内存实现进程间通信
comm.hpp
#pragma once
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<string>
#include<assert.h>
#include<unistd.h>
#include<sys/stat.h>
using namespace std;
//1. 创建key, 保证看到相同的key
#define PATHNAME "."
#define PROJID 0X6666
key_t getKey()
{
key_t k =ftok(PATHNAME,PROJID);
if(k==-1) //创建失败,直接终止
{
cerr<<errno<< " : " << strerror(errno) << endl;
exit(1);
}
return k;
}
string toHex(int x)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%x", x);
return buffer;
}
//2. 创建shm
const int gsize=4096; //共享内存的大小, 以字节为单位
static int createHelper(key_t k,int size, int flag) //保证只在本文件使用
{
int shmid=shmget(k,gsize,flag);
if(shmid==-1) //创建失败,直接终止
{
cerr<<errno<< " : " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int createShm(key_t k,int size)
{
umask(0);
return createHelper(k, size, IPC_CREAT| IPC_EXCL|0666); //增加拥有者的读写权限
}
int getShm(key_t k,int size) //获取共享内存
{
return createHelper(k, size, IPC_CREAT);
}
//3. 关联共享内存
char* attachShm(int shmid)
{
char*start=(char*)shmat(shmid,nullptr,0); //返回申请的起始地址
return start;
}
//4. 共享内存去关联
void deltachShm(char*start)
{
int n=shmdt(start);
assert(n!=-1);
(void)n;
}
//5. 删除共享内存
void delShm(int shmid)
{
int n=shmctl(shmid,IPC_RMID,nullptr);
assert(n!=-1);
(void)n;
}
//封装成一个类
#define SERVER 1
#define CLIENT 0
class Init
{
public:
Init(int t) :type(t)
{
key_t k = getKey();
if(type==SERVER)
{
shmid=createShm(k,gsize);
}
else
{
shmid=getShm(k,gsize);
}
start=attachShm(shmid); //获取到申请共享内存的起始地址
}
char*getStart()
{
return start;
}
~Init()
{
deltachShm(start);
if(type==SERVER)
{
delShm(shmid);
}
}
private:
char*start;
int type; //server or client --- 申请者
int shmid;
};
server.cc
#include"comm.hpp"
int main()
{
Init init(SERVER);
char*start=init.getStart();
int n=0;
while(n<=26)
{
cout<<"client -> server# "<< start << endl;
sleep(1);
n++;
}
return 0;
}
client.cc
#include"comm.hpp"
int main()
{
Init init(CLIENT);
char*start=init.getStart();
char c='A';
while(c<='Z')
{
start[c-'A']=c;
c++;
start[c -'A']='\0';
sleep(1);
}
return 0;
}
3.5 共享内存知识补充
3.5.1 共享内存的优缺点
优点:
- 一旦共享内存映射到进程的地址空间, 该共享内存就被所有的进程直接看到了, 因为共享内存的这种特性, 可以让进程通信的时候, 减少拷贝次数, 所以共享内存是所有进程间通信, 速度最快的。
缺点:
- 如果服务端读取速度较快,用户端发送数据较慢,就会产生同一段消息被服务端读取多遍。共享内存是不进行同步和互斥的,没有对数据进行任何保护。为什么? 因为像管道这种是调用系统接口通信, 而共享内存是直接通信
3.5.2 共享内存的大小
因为系统分配共享内存是以4KB为基本单位,一般建议申请共享内存的大小为4KB的整数倍。
4. System V信号量(了解)
4.1 前提概念
- 公共资源: 大家都能看到的资源
- 互斥: 任何一个时刻, 都只允许一个执行流在进行共享资源的访问
- 临界资源: 任何一个时刻, 都只允许一个执行流在进行访问的共享资源
- 临界区: 临界资源是要通过代码访问的, 凡是访问临界资源的代码
- 原子性: 要么不做, 要么做完, 只有两种确定状态的属性
4.2 感性认识信号量
举一个生活中的例子:
比如看电影, 我们在看电影之前需要先买票
买票的本质功能:
- 对座位资源的预定机制
- 确保不会因为多放出去特定的座位资源, 而导致冲突
如果放映厅是顶级VIP放映厅,只有一个座位, 有一个人买票了后其他人就不能买了, 人与人买票之间存在互斥
信号量(也叫信号灯): 本质是一个计数器(count), 描述资源数量的计数器
任何一个执行流想访问临界资源中的一个子资源的时候也不能直接访问
(P操作, 预定资源) 我们首先要申请信号量资源 — count-- — 只要我申请信号量成功, 我就未来就一定能够拿到一个子资源, 一旦count减为0后, 有进程想要访问, 那么这个进程就会被挂起阻塞
下来我们进入自己的临界区, 访问对应的临界资源
(V操作,释放资源) 访问完后, 释放信号量资源 — count++ — 只要将计数器增加,就表示将我们对应的资源进行了归还
我们想让两个不同的进程看到同一个"计数器(count)"(资源), 所以信号量被归类到了进程间通信
信号量本身也是一个临界资源,它能保护其他共享资源的同时,也需要保护自己的安全,信号量内部的加加减减具有原子性。
二元信号量:信号量为1,说明共享资源时一整个整体,提供互斥功能。
5. IPC资源管理方式
将内核中所有的ipc资源统一用struct ipc_perm* perms[]指针数组进行管理, 模拟了C++中多态的行为