目录
一、通信的相关概念
二、管道(半双工)
1、管道的概念
三、匿名管道(fork实现,用于父子及血缘关系进程间通信)
1、匿名管道的使用
2、匿名管道的读写情况
3、管道的特征
4、基于匿名管道的进程池
四、命名管道(open打开相同文件实现,可用于非血缘关系的进程间通信)
1、命名管道的创建及删除
1.1命令行创建命名管道
1.2程序内创建及删除命名管道
1.3基于命名管道的用户端发送,服务端接收
五、System V共享内存
1、共享内存(物理内存块+相关属性)的原理
2、共享内存相关命令
2.1查看共享内存(ipcs -m/-q/-s)
2.2删除共享内存(ipcrm -m shmnid)
3、创建/查看/删除/控制(删除)/关联共享内存
3.1形成key(ftok)
3.2创建共享内存(shmget)
3.3关联/卸载共享内存(shmat/shmdt)(关联类似malloc)
3.4控制(主要用移除)共享内存(shmctl)
4、利用共享内存进行进程间通信
5、共享内存的优缺点
5.1共享内存的优点
5.2共享内存的缺点
5.3共享内存的特点
5.4共享内存大小的建议
一、通信的相关概念
进程之间具有独立性,进程间如果要发生通信,就需要打破这种独立性。进程间通信必定需要一块公共的区域用来作为信息的存放点,操作系统需要直接的或间接给通信进程双方提供内存空间,例如这块内存空间是文件系统提供的,那么就是管道通信,通信的本质就是让不同的进程看到同一份内存空间。
进程间通信是为了完成:
1、数据传输:一个进程需要将它的数据发送给另一个进程;
2、资源共享:多个进程之间共享相同的资源;
3、事件通知:一个进程需要向另一个或另一组进程发送消息,通知他们发送了某种事件(例如子进程终止时要通知父进程)
4、进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
二、管道(半双工)
1、管道的概念
管道是基于文件系统的通信方式,那么就从底层的角度看一下管道通信的原理:
管道文件是内存级文件,不用访问磁盘进行文件加载,操作系统直接创建结构体对象及内核缓冲区。如上图例子,管道文件不必使用open进行打开,操作系统会创建文件结构体对象及其内核缓冲区,并将其放入父进程的文件描述符表中,父进程创建子进程后,父子进程便能基于管道这个内存级文件进行通信。
管道只能单向通信。
三、匿名管道(fork实现,用于父子及血缘关系进程间通信)
通过上方管道的概念可知,通过父进程fork创建子进程,让子进程拷贝父进程中管道文件的地址,两个进程便能看到同一个管道文件,这个管道文件是一个内存级文件,并没有名字,所以被称为匿名管道。
所以对待管道和对待文件一样,体现Linux一切皆文件的思想。
1、匿名管道的使用
#include <unistd.h>
int pipe(int pipefd[2]);//pipefd[2]是输出型参数
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
成功时返回零,错误时返回 -1,并适当地设置 errno。
pipefd[2]是输出型参数,外边搞个pipefd[2]数组传进去,系统调用pipe结束后这个数组中存放的就是读/写的fd。
子进程写入数据到管道,父进程读取管道数据代码。
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
//父进程读取,子进程写入
int main()
{
//第一步父进程创建管道
int fds[2];
int n=pipe(fds);
assert(n==0);
//第二步父进程fork()创建子进程
pid_t id =fork();//fork之后,父进程返回值>0,子进程返回值==0
assert(id>=0);
const char* s="我是子进程,我的pid是:";
int cnt=0;
if(id==0)
{
close(fds[0]);//子进程关闭读取的fd
//子进程的通信代码
while(true)
{
char buffer[1000];//这个缓冲区只有子进程能看到
snprintf(buffer,sizeof(buffer),"子进程第%d次向父进程发送:%s%d",++cnt,s,getpid());//向缓冲区buffer中打印
write(fds[1],buffer,strlen(buffer));//子进程将缓冲区数据写入管道
sleep(1);//每隔1秒写一次
//break;
}
close(fds[1]);//如果break跳出循环,子进程将关闭写端
exit(0);
}
close(fds[1]);//父进程关闭写入
//父进程的通信代码
while(true)
{
char buffer[1000];//这个缓冲区只有父进程能看到
//如果管道中没有数据,读取端再读,默认会阻塞当前读取的进程
ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);
if(s>0)//s是read读取成功字节数
{
buffer[s]='\0';
cout << "父进程的pid是:"<<getpid()<<" "<<buffer<<endl;
}
else if(s==0)//如果子进程关闭写端,父进程将会输出“读完了”
{
//读到文件结尾
cout<<"读完了"<<endl;
break;
}
}
n=waitpid(id,nullptr,0);
assert(n==id);
close(fds[0]);//父进程读取fd用完记得关一下
return 0;
}
2、匿名管道的读写情况
1、如果管道中没有数据,读取端进程再进行读取,会阻塞当前正在读取的进程;
2、如果写入端写满了,再写就会对该进程进行阻塞,需要等待对方对管道内数据进行读取;
3、如果写入进程关闭了写入fd,读取端将管道内的数据读完后read的返回值为0,和谐退出;
4、如果读关闭,操作系统会给写端发送13号信号SIGPIPE,终止写端。
3、管道的特征
1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道;
2、管道提供流式服务 ;
3、进程退出,管道释放,所以管道的生命周期随进程
4、内核会对管道操作进行同步与互斥
5、管道是半双工。需要双方通信时,需要建立起两个管道
4、基于匿名管道的进程池
#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
#define PROCESS_NUM 5
#define Make_Seed() srand((unsigned long)time(nullptr)^getpid()^0X55^rand()%1234)
typedef void(*func_t)();//函数指针类型
//子进程需要完成的任务
void downLodeTask()
{
cout<<getpid()<<"下载任务"<<endl;
sleep(1);
}
void ioTask()
{
cout<<getpid()<<"IO任务"<<endl;
sleep(1);
}
void flushTask()
{
cout<<getpid()<<"刷新任务"<<endl;
sleep(1);
}
//多进程代码
class sunEndPoint
{
public:
sunEndPoint(pid_t subId,int writeFd)
:_subId(subId)
,_writeFd(writeFd)
{
char namebuffer[1000];
snprintf(namebuffer,sizeof(namebuffer),"process-%d[pid(%d)-fd(%d)]",num++,_subId,_writeFd);
_name=namebuffer;
}
public:
string _name;
pid_t _subId;//pid
int _writeFd;//写fd
static int num;
};
int sunEndPoint::num=0;
void loadTaskFunc(vector<func_t>* out)
{
assert(out);
out->push_back(downLodeTask);
out->push_back(ioTask);
out->push_back(flushTask);
}
int recvTask(int readFd)
{
int code=0;
ssize_t s=read(readFd,&code,sizeof(code));
if(s==sizeof(code))//合法信息
{
return code;
}
else if(s<=0)
{
return -1;
}
else
return 0;
}
void createSubProcess(vector<sunEndPoint>* subs,vector<func_t>& funcMap)
{
vector<int> deleteFd;//解决下一个子进程拷贝父进程读端的问题
for(int i=0;i<PROCESS_NUM;++i)
{
int fds[2];
int n=pipe(fds);
assert(n==0);
(void)n;
pid_t id=fork();
if(id==0)//子进程
{
//关闭上一个文件的写端文件描述符
for(int i=0;i<deleteFd.size();++i)
{
close(deleteFd[i]);
}
//子进程,处理任务
close(fds[1]);
while(true)
{
//1、获取命令码,如果父进程没有发送,子进程被阻塞
int commandCode=recvTask(fds[0]);
//2、完成任务
if(commandCode>=0&&commandCode<funcMap.size())
{
funcMap[commandCode]();
}
else if(commandCode==-1)
{
break;
}
}
exit(0);
}
close(fds[0]);
sunEndPoint sub(id,fds[1]);
subs->push_back(sub);
deleteFd.push_back(fds[1]);
}
}
void sendTask(const sunEndPoint& process,int taskNum)
{
cout<<"send task num"<<taskNum<<"send to->"<<process._name<<endl;
int n=write(process._writeFd,&taskNum,sizeof(taskNum));
assert(n==sizeof(int));//判断是否成功写入4个字节
(void)n;
}
void loadBlanceContrl(const vector<sunEndPoint>& subs,const vector<func_t>& funcMap,int count)
{
int processnum =subs.size();//子进程的个数
int tasknum=funcMap.size();
bool forever=(count==0?true:false);
while(true)
{
//选择一个子进程,从vector<sunEndPoint>选择一个index
int subIdx=rand()%processnum;
//选择一个任务,从vector<func_t>选择一个index
int taskIdx=rand()%tasknum;
//将任务发送给指定的子进程,将一个任务的下标发送给子进程
sendTask(subs[subIdx],taskIdx);//taskIdx作为管道的大小4个字节
sleep(1);
if(!forever)//forever不为0
{
--count;
if(count==0)
break;
}
}
//写端退出,读端将管道内数据读完后read返回0
for(int i=0;i<processnum;++i)
{
close(subs[i]._writeFd);//最晚被创建的子进程拥有早期创建的子进程的父进程的读端,所以这里其实是后创建的进程先关闭
}
}
//回收子进程
void waitProcess(vector<sunEndPoint> processes)
{
int processnum=processes.size();
for(int i=0;i<processnum;++i)
{
waitpid(processes[i]._subId,nullptr,0);
cout<<"wait sub process success"<<processes[i]._subId<<endl;
}
}
//父进程给子进程发布命令,父进程写,子进程读
int main()
{
Make_Seed();//创建随机数
//父进程创建子进程及和子进程通信的管道
vector<func_t> funcMap;//vector<函数指针> funcMap
loadTaskFunc(&funcMap);//加载任务
vector<sunEndPoint> subs;//子进程集合
createSubProcess(&subs,funcMap);//维护父子通信信道
//这里的程序是父进程,用于控制子进程
int taskCnt=9;//让子进程做9个任务
loadBlanceContrl(subs,funcMap,taskCnt);
//回收子进程信息
waitProcess(subs);
return 0;
}
四、命名管道(open打开相同文件实现,可用于非血缘关系的进程间通信)
1、命名管道的创建及删除
1.1命令行创建命名管道
mkfifo filename//filename是以p开头的管道文件
命名管道可用于无血缘关系的进程间通信,两个进程均打开同一路径下的同名文件(看到同一份资源),便能进行通信。命名管道大小为零的原因是数据并不会被刷新到磁盘中,它也是一个内存级文件。
1.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被设置)
1.3基于命名管道的用户端发送,服务端接收
1、comm.hpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAMED_PIPE "/tmp/mypipe"
//创建管道
bool createFifo(const std::string& path)
{
umask(0);
int n=mkfifo(path.c_str(),0600);//创建管道文件
if(n==0)
return true;
else
{
std::cout<<"error:"<<errno<<"error string"<<strerror(errno)<<std::endl;
return false;
}
}
//销毁管道
void destroyFifo(const std::string& path)
{
int n=unlink(NAMED_PIPE);
assert(n==0);
(void)n;
}
2、server.cc(先运行服务端)
#include "comm.hpp"
int main()
{
bool r=createFifo(NAMED_PIPE);
assert(r);
(void)r;
std::cout << "server begin" << std::endl;
int rfd =open(NAMED_PIPE,O_RDONLY);//会等用户端打开管道文件,服务端才进行open服务端以只读的方式打开
if(rfd==-1)//打开失败
exit(1);
//读取
char buffer[1000];
while(true)
{
ssize_t s=read(rfd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;//将最后一个字符设置为'\0'
std::cout<<"client->server:"<<buffer;
}
else if(s==0)//说明客户端一直没发数据
{
std::cout << "client quit, me too!" << std::endl;
break;
}
else//读取出错
{
std::cout<<"err string"<<strerror(errno)<<std::endl;
break;
}
}
close(rfd);
destroyFifo(NAMED_PIPE);
return 0;
}
服务端在以只读的方式open管道文件前,会被阻塞,等待用户端open写入管道文件。(读写端均需要被打开)
3、client.cc
#include "comm.hpp"
int main()
{
int wfd =open(NAMED_PIPE,O_WRONLY);//用户端以写的方式打开
if(wfd==-1)//打开失败
exit(1);
char buffer[1000];
while(true)
{
std::cout<<"you can say:";
fgets(buffer,sizeof(buffer),stdin);//fgets剩一个空间会被系统填充'\0',不用-1
ssize_t s=write(wfd,buffer,strlen(buffer));
assert(s==strlen(buffer));
(void)s;
}
close(wfd);
return 0;
}
五、System V共享内存
1、共享内存(物理内存块+相关属性)的原理
需要让不同进程看到同一块内存资源。用户使用操作系统提供的接口在物理内存中申请一块资源,通过进程的页表将这段物理空间映射至进程地址空间,进程将这段虚拟地址的起始地址返回给用户。通信结束后记得取消物理内存和虚拟内存的映射关系(去关联),并释放共享内存。
1、共享内存和malloc有点像,区别在于malloc出来的内存只能本进程知道这块空间的地址,共享内存是通过开辟一块物理空间,分别映射至通信进程的虚拟地址空间中。
2、共享内存是一种通信方式,所有想通信的进程都可以用,所以操作系统中可能会同时存在很多个共享内存。
2、共享内存相关命令
2.1查看共享内存(ipcs -m/-q/-s)
ipcs -m/-q/-s //共享内存/消息队列/信号量数组
2.2删除共享内存(ipcrm -m shmnid)
ipcrm -m shmnid//使用shmid删除共享内存
3、创建/查看/删除/控制(删除)/关联共享内存
3.1形成key(ftok)
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);//路径/项目id
成功时,将返回key。失败返回-1(失败错误码errno被设置为系统调用出错)
通过传入相同的pathname和proj_id得到相同的key,从而找到同一块共享内存,实现进程间通信。
key通过shmget,设置进共享内存的属性中用来标识该共享内存在内核中的唯一性。key可以理解为一个个房间(内存块)的门牌号(编号)。
shmget的返回值:key类似fd:inode的关系。
3.2创建共享内存(shmget)
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);//标定唯一性(确认是哪块共享内存)/申请多大的内存空间/二进制标志位,见下图
成功时,将返回一个有效的共享内存标识符。(不同操作系统的数字下标不同,和文件的数字下标不兼容)
失败返回-1(失败错误码errno被设置)
3.3关联/卸载共享内存(shmat/shmdt)(关联类似malloc)
1、将共享内存与虚拟内存进行关联
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);//共享内存id,被映射的进程地址空间(给nullptr),给0默认可以读写
成功时,将返回共享内存的虚拟地址。失败返回-1(失败错误码errno被设置)
2、将共享内存与虚拟内存去关联
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);//参数:shmat的返回值
成功时,将返回0。失败返回-1(失败错误码errno被设置)
3.4控制(主要用移除)共享内存(shmctl)
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);//shmid(类似fd),传入系统设定的宏,shmid_ds数据结构
传入IPC_RMID移除共享内存成功时,将返回0。失败返回-1(失败错误码errno被设置)
4、利用共享内存进行进程间通信
1、comm.hpp
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATHNAME "."//当前路径(路径都行)
#define PROJ_ID 0X55//项目id也无要求
#define MAX_SIZE 4096
key_t getKey()
{
key_t k=ftok(PATHNAME, PROJ_ID);
if(k==-1)
{
std::cout<<"ftok"<<errno<<":"<<strerror(errno)<<std::endl;
exit(1);
}
return k;
}
int getShmHelper(key_t key,int flags)
{
int shmid=shmget(key,MAX_SIZE,flags);
if(shmid==-1)//创建共享内存失败
{
std::cerr<<"shmget"<<errno<<":"<<strerror(errno)<<std::endl;
exit(2);
}
return shmid;//返回共享内存标识符
}
int getShm(key_t key)//创建||获取共享内存
{
return getShmHelper(key,IPC_CREAT);//传0也行
}
int createShm(key_t key)//必定创建共享内存
{
return getShmHelper(key,IPC_CREAT|IPC_EXCL|0600);//生成一个全新的共享内存
}
void* attachShm(int shmid)//让共享内存与虚拟内存建立联系
{
void* memstart=shmat(shmid,nullptr,0);
if((long long)memstart==-1L)
{
std::cerr<<"shmat"<<errno<<":"<<strerror<<std::endl;
exit(3);
}
return memstart;
}
void detchShm(void* memStart)//去关联
{
if(shmdt(memStart)==-1)
{
std::cerr<<"shmdt"<<errno<<":"<<strerror<<std::endl;
exit(4);
}
}
void delShm(int shmid)//删除共享内存
{
if(shmctl(shmid,IPC_RMID,nullptr)==-1)
{
std::cerr<<"shmctl"<<errno<<":"<<strerror<<std::endl;
}
}
2、shm_server.cc
#include "comm.hpp"
int main()
{
key_t k=getKey();
printf("0X%x\n",k);
int shmid=createShm(k);
char* memStart=(char*)attachShm(shmid);//让共享内存与虚拟内存建立联系
printf("memStart address:%p\n",memStart);
//通信接收代码
while(true)
{
printf("client say:%s\n",memStart);
sleep(1);
//调用用户级结构体
struct shmid_ds ds;//创建结构体对象ds
shmctl(shmid,IPC_STAT,&ds);//获取ds对象的状态
printf("获取属性:%d,pid:%d,myself:%d,key:%d\n",ds.shm_segsz,getpid(),ds.shm_cpid,ds.shm_perm.__key);
}
detchShm(memStart);//去关联
sleep(10);
delShm(shmid);//删除共享内存,client和server都能删除共享内存,尽量谁创建谁删
return 0;
}
2、shm_client.cc
#include "comm.hpp"
int main()
{
key_t k=getKey();
printf("0X%x\n",k);
int shmid=getShm(k);//获取共享内存
sleep(5);
char* memStart=(char*)attachShm(shmid);//让共享内存与虚拟内存建立联系
printf("memStart address:%p\n",memStart);
//通信传输代码
const char* massage="I am client";
pid_t id=getpid();
int cnt=0;//发送计数
while(true)
{
snprintf(memStart,MAX_SIZE,"%s[%d]:%d\n",massage,getpid,++cnt);
sleep(1);
}
detchShm(memStart);//去关联
return 0;
}
5、共享内存的优缺点
5.1共享内存的优点
共享内存是所有进程间通信中速度最快的。(无需缓冲区,能大大减少通信数据的拷贝次数)
5.2共享内存的缺点
如果服务端读取速度较快,用户端发送数据较慢,就会产生同一段消息被服务端读取多遍。共享内存是不进行同步和互斥的,没有对数据进行任何保护。
5.3共享内存的特点
上图表明共享内存的生命周期是随操作系统的,进程的退出不会销毁共享内存。这是System V资源的特征。
5.4共享内存大小的建议
因为系统分配共享内存是以4KB为基本单位,一般建议申请共享内存的大小为4KB的整数倍。