需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。
目录
一、通信的相关概念
二、管道(半双工)
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、获取消息队列(msgget)
2、控制消息队列(msgctl)
3、其他略
七、信号量(计数器)(了解)
1、信号量的本质
2、信号量的作用
八、IPC资源的管理方式
进程之间具有独立性,进程间如果要发生通信,就需要打破这种独立性。进程间通信必定需要一块公共的区域用来作为信息的存放点,操作系统需要直接的或间接给通信进程双方提供内存空间,例如这块内存空间是文件系统提供的,那么就是管道通信,通信的本质就是让不同的进程看到同一份内存空间。
进程间通信是为了完成:
1、数据传输:一个进程需要将它的数据发送给另一个进程;
2、资源共享:多个进程之间共享相同的资源;
3、事件通知:一个进程需要向另一个或另一组进程发送消息,通知他们发送了某种事件(例如子进程终止时要通知父进程)
4、进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
管道是基于文件系统的通信方式,那么就从底层的角度看一下管道通信的原理:
管道文件是内存级文件,不用访问磁盘进行文件加载,操作系统直接创建结构体对象及内核缓冲区。如上图例子,管道文件不必使用open进行打开,操作系统会创建文件结构体对象及其内核缓冲区,并将其放入父进程的文件描述符表中,父进程创建子进程后,父子进程便能基于管道这个内存级文件进行通信。
管道只能单向通信。
通过上方管道的概念可知,通过父进程fork创建子进程,让子进程拷贝父进程中管道文件的地址,两个进程便能看到同一个管道文件,这个管道文件是一个内存级文件,并没有名字,所以被称为匿名管道。
所以对待管道和对待文件一样,体现Linux一切皆文件的思想。
#include
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
#include
#include
#include
#include
#include
#include
#include
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是:"<
1、如果管道中没有数据,读取端进程再进行读取,会阻塞当前正在读取的进程;
2、如果写入端写满了,再写就会对该进程进行阻塞,需要等待对方对管道内数据进行读取;
3、如果写入进程关闭了写入fd,读取端将管道内的数据读完后read的返回值为0,和谐退出;
4、如果读关闭,操作系统会给写端发送13号信号SIGPIPE,终止写端。
1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道;
2、管道提供流式服务 ;
3、进程退出,管道释放,所以管道的生命周期随进程
4、内核会对管道操作进行同步与互斥
5、管道是半双工。需要双方通信时,需要建立起两个管道
#include
#include
#include
#include
#include
#include
#include
#include
#include
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<* 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* subs,vector& funcMap)
{
vector deleteFd;//解决下一个子进程拷贝父进程读端的问题
for(int i=0;i=0&&commandCodepush_back(sub);
deleteFd.push_back(fds[1]);
}
}
void sendTask(const sunEndPoint& process,int taskNum)
{
cout<<"send task num"<"<& subs,const vector& funcMap,int count)
{
int processnum =subs.size();//子进程的个数
int tasknum=funcMap.size();
bool forever=(count==0?true:false);
while(true)
{
//选择一个子进程,从vector选择一个index
int subIdx=rand()%processnum;
//选择一个任务,从vector选择一个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 processes)
{
int processnum=processes.size();
for(int i=0;i funcMap;//vector<函数指针> funcMap
loadTaskFunc(&funcMap);//加载任务
vector subs;//子进程集合
createSubProcess(&subs,funcMap);//维护父子通信信道
//这里的程序是父进程,用于控制子进程
int taskCnt=9;//让子进程做9个任务
loadBlanceContrl(subs,funcMap,taskCnt);
//回收子进程信息
waitProcess(subs);
return 0;
}
mkfifo filename//filename是以p开头的管道文件
命名管道可用于无血缘关系的进程间通信,两个进程均打开同一路径下的同名文件(看到同一份资源),便能进行通信。命名管道大小为零的原因是数据并不会被刷新到磁盘中,它也是一个内存级文件。
创建命名管道:
#include
#include
int mkfifo(const char *pathname, mode_t mode);//路径,权限码
成功返回0,失败返回-1(失败错误码errno被设置)
删除命名管道:
#include
int unlink(const char *path);//路径
成功返回0,失败返回-1(失败错误码errno被设置)
1、comm.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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:"<
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:"<
服务端在以只读的方式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;
}
需要让不同进程看到同一块内存资源。用户使用操作系统提供的接口在物理内存中申请一块资源,通过进程的页表将这段物理空间映射至进程地址空间,进程将这段虚拟地址的起始地址返回给用户。通信结束后记得取消物理内存和虚拟内存的映射关系(去关联),并释放共享内存。
1、共享内存和malloc有点像,区别在于malloc出来的内存只能本进程知道这块空间的地址,共享内存是通过开辟一块物理空间,分别映射至通信进程的虚拟地址空间中。
2、共享内存是一种通信方式,所有想通信的进程都可以用,所以操作系统中可能会同时存在很多个共享内存。
ipcs -m/-q/-s //共享内存/消息队列/信号量数组
ipcrm -m shmnid//使用shmid删除共享内存
#include
#include
key_t ftok(const char *pathname, int proj_id);//路径/项目id
成功时,将返回key。失败返回-1(失败错误码errno被设置为系统调用出错)
通过传入相同的pathname和proj_id得到相同的key,从而找到同一块共享内存,实现进程间通信。
key通过shmget,设置进共享内存的属性中用来标识该共享内存在内核中的唯一性。key可以理解为一个个房间(内存块)的门牌号(编号)。
shmget的返回值:key类似fd:inode的关系。
#include
#include
int shmget(key_t key, size_t size, int shmflg);//标定唯一性(确认是哪块共享内存)/申请多大的内存空间/二进制标志位,见下图
成功时,将返回一个有效的共享内存标识符。(不同操作系统的数字下标不同,和文件的数字下标不兼容)
失败返回-1(失败错误码errno被设置)
1、将共享内存与虚拟内存进行关联
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);//共享内存id,被映射的进程地址空间(给nullptr),给0默认可以读写
成功时,将返回共享内存的虚拟地址。失败返回-1(失败错误码errno被设置)
2、将共享内存与虚拟内存去关联
#include
#include
int shmdt(const void *shmaddr);//参数:shmat的返回值
成功时,将返回0。失败返回-1(失败错误码errno被设置)
#include
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);//shmid(类似fd),传入系统设定的宏,shmid_ds数据结构
传入IPC_RMID移除共享内存成功时,将返回0。失败返回-1(失败错误码errno被设置)
1、comm.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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"<
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;
}
共享内存是所有进程间通信中速度最快的。(无需缓冲区,能大大减少通信数据的拷贝次数)
如果服务端读取速度较快,用户端发送数据较慢,就会产生同一段消息被服务端读取多遍。共享内存是不进行同步和互斥的,没有对数据进行任何保护。
上图表明共享内存的生命周期是随操作系统的,进程的退出不会销毁共享内存。这是System V资源的特征。
因为系统分配共享内存是以4KB为基本单位,一般建议申请共享内存的大小为4KB的整数倍。
#include
#include
#include
int msgget(key_t key, int msgflg);//key:“门牌号”,msgflg:宏(IPC_CREAT and IPC_EXCL)
如果成功,返回值将是消息队列标识符(非负整数) ,否则为 -1。用 errno 指示错误。
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//msqid(类似fd),传入系统设定的宏,shmid_ds数据结构
信号量的本质是一个计数器,用以表示公共资源中,资源数量多少的问题。
临界资源:未来将被保护的资源;
临界区:进程有对应的代码来访问对应的临界资源;
非临界区:进程中不访问临界资源的代码;
互斥和同步保护公共资源。(互斥:同一时间只有一个进程能进行资源访问)
原子性:要么不做,要么就做完。只有两态。
多个资源块通过程序员的编码实现互斥,防止同一资源块同时被访问。
信号量就是通过计数器对链接资源进行保护的一种方式。(需要让进程间看到同一个信号量资源)
信号量通过计数器sem--(预定资源,P操作)/sem++(释放资源,V操作)
信号量本身也是一个临界资源,它能保护其他共享资源的同时,也需要保护自己的安全,信号量内部的加加减减具有原子性。
二元信号量:信号量为1,说明共享资源时一整个整体,提供互斥功能。
三种ipc资源数据结构的首地址元素相同,用一个struct ipc_perm* perms[]指针数组进行管理。
访问时只需(struct shmid_ds*)perms[0]->共享内存的属性。
这是C语言模拟实现的一种多态行为。