目录
一,system V共享内存
除了管道通信方式,对于操作系统自己本身,操作系统为了内核中的通信单独设计了通信模块。
进程通信的前提:让不同进程看到同一份资源
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
首先我们从shared memory,共享内存开始,所谓的共享内存,本质上就是将同一份资源,通过加载到物理内存,同时映射到两个进程的内存空间,实现资源共享。
此外,在操作系统中一定会允许多个共享内存,此时就需要操作系统来管理这些共享内存。
快速认识接口
shmget(shared memory get)
创建共享内存 既能创建,又能获取
参数一为创建的key,参数二为空间大小,
参数三为标记位:
IPC_CREAT to create a new segment. If this flag is not used, then shmget() will find the segment associated with key and check to see . if the user has permission to access the segment.(没有就创建shm(共享内存))
PC_EXCL used with IPC_CREAT to ensure failure if the segment already exists.(通常与IPC_CREAT结合使用,不存在就创建,存在就出错返回不创建)。
通过这两个标志位使得我们创建的内存是全新的。
shmat(shared memory attach)
将共享内存挂接到地址空间当中
参数一位shmid,参数2一般设置位nullptr,参数三为标志位表示已什么样的方式挂在,一般也用0。
返回值位起始地址。通过引用计数来计算有多少进程挂在到当前共享内存中。
shmdt(shared memory delete)
去掉内存关联,将共享内存从进程地址空间中移除。
shmctl (shared memory control)
控制共享内存,第一个参数位shmid,第二个位操作指令,IPC_STAT,IPC_SET, IPC_RMID(删除) ,IPC_INFO
编写代码
和命名管道通信的方式一样,我们创建了三个文件server,client,commant.cpp,用来观察进程通信:
首先对于读端还是写端都会有同一个key,我们创建同一个key,此时进程就会把key写到地址空间当中,通过Key值操作系统可以找到两个进程的同一块地址空间,即key值指向的内存就是共享内存。
comman.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstdlib>
#include<unistd.h>
const std::string pathname="/home/danchengwei/myfile/file7";
const int proj_id=0x11223344;
const int size=4096;
//管道文件
const std::string filename = "fifo";
key_t getkey()
{
//转换pathname为key
key_t key=ftok(pathname.c_str(),proj_id);//convert a pathname and a project identifier to a System V IPC key
if(key<0)
{
std::cerr<<"errno:"<<errno<<",strerror"<<strerror(errno)<<std::endl;
return 1;
}
std::cout<<"key:"<<key<<std::endl;
return key;
}
std::string tohex(int id)
{
char buffer[1024];
snprintf(buffer,sizeof(buffer),"0x%x",id);
return buffer;
}
int creatshmHLPER(key_t key,int flag)
{
int shmid=shmget(key,size,flag);
//成光返回id,错误返回-1
if(shmid<0)
{
std::cerr<<"errno:"<<errno<<",strerror"<<strerror(errno)<<std::endl;
exit(2);
}
std::cout<<"shmid:"<<shmid<<std::endl;
return shmid;
}
int getshm(key_t key)
{
return creatshmHLPER(key,IPC_CREAT | IPC_EXCL | 0644);
}
int creatshm(key_t key)
{
return creatshmHLPER(key,IPC_CREAT);
}
bool MakeFifo()
{
int n = mkfifo(filename.c_str(), 0666);
if(n < 0)
{
std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
return false;
}
std::cout << "mkfifo success... read" << std::endl;
return true;
}
server
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include"comman.hpp"
using namespace std;
int main()
{
//创建共享内存
key_t key=getkey();
//key在应用层我们一般不去使用,我们使用的是shmid
int shmid=creatshm(key);
sleep(10);
//挂载 ,即将共享内存映射到进程的地址空间当中。
char*s=(char*)shmat(shmid,nullptr,0);
//打开管道
Start:
int rfd=open(filename.c_str(),O_RDONLY);
if(rfd<0)
{
//cerr<<"errno:"<<errno<<",errstring:"<<strerror(errno)<<endl;
if(MakeFifo())
{
goto Start;
}else{
return 1 ;
}
//return 2;
}
cout<<"打开管道成功..."<<endl;
char buffer[1024];
while(true)
{
ssize_t s=read(rfd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;//表示以/0结尾
cout<<"cilent reply: "<<buffer<<endl;
}
if(s<=0)
{
cout<<"no data had reciveed"<<endl;
//没有数据可读了,退出进程
break;
}
}
//关闭管道
close(rfd);
cout<<"关闭管道成功..."<<endl;
sleep(5);
//在地址空间当中移除共享内存
shmdt(s);
sleep(5);
//按照id删除
shmctl(shmid,IPC_RMID,nullptr);
sleep(5);
return 0;
}
创建完成可以通过指令查看共享内存:
ipcs -m //查看共享内存
ipcrm -m shmid //删除共享内存
其中natch表示有几个进程挂接到该共享内存。
程序结束,共享内存被创建之后并没有释放。它的生命周期时内核决定的。
之后我们再完成与server一样的client的编写:
#include<iostream>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include"comman.hpp"
using namespace std;
int main()
{
//与server一样
//获取key值
key_t key=getkey();
//创建共享内存
int shmid=creatshm(key);
sleep(10);
//把共享内存挂载到地址空间当中
char *s=(char*)shmat(shmid,nullptr,0);
sleep(5);
//打开管道
int wfd = open(filename.c_str(), O_WRONLY);
//数据写入
string message;
while(true)
{
cout<<"请输入你要发送的数据:"<<endl;
getline(cin,message);
//这里再写入管道时,我们使用c++string再转化为c字符串
ssize_t t=write(wfd,message.c_str(),message.size());
if(t<0)
{
cerr<<"错误码:"<<errno<<",错误原因:"<<strerror(errno)<<endl;
break;
}
}
//关闭管道
close(wfd);
cout<<"关闭管道成功..."<<endl;
//取消共享内存的挂载
shmdt(s);
return 0;
}
之后我们就可以同时执行两个进程并观察会发现 ,刚开始创建共享内存,server与client挂载到地址空间,natch增加,之后移除,之后删除.
有了共享内存,那么我们现在通过共享内存就可以实现进程之间的通信,和管道类似,我们可以增加一个管道文件;之后用一端作为读端,一端作为写端。
综上那么共享内存与管道通信有什么区别?
其实共享内存是比管道通信更加快速的,对于共享内存,考虑硬件:
第一次拷贝是从外设到共享内存,第二次是从共享内存写到显示器上,只需要拷贝两次,对于管道.
而对于管道,先是用户到键盘(外设)把数据读到缓冲区中,再把数据拷贝到管道当中(写入),之后再将管道的数据写道别的用户的缓冲区(读),最后再将数据给到显示器(外设)。对于管道考虑硬件需要拷贝四次数据。
system v消息队列
所谓的消息队列是提供让一个进程给另一个进程发送数据块的能力,使用的接口msgctl,使用方法基本和共享内存大致相同。
参数一为msqid即这里的key,参数二为如何去操作消息队列的指令,参数三一般为nullptr.
创建完成之后可以用指令ipcs -q来查看消息对列。根据接口的第二参数我们可以还选择删除消息队列。可以看到使用与共享内存非常的相似。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<iostream>
#include"comman.hpp"
//消息队列
int main()
{
//获取key值
key_t key=getkey();
//创建消息队列
int msgid=msgget(key,IPC_CREAT | IPC_EXCL);
std::cout<<"msgid:"<<msgid<<std::endl;
sleep(10);
//删除消息队列
msgctl(msgid,IPC_RMID,nullptr);
rteurn 0;
}
其次消息队列也可以存在多个,两个进程用多个消息队列进行数据交互,因此在内核当中,操作系统需要管理这些消息队列,先描述在组织,因此消息队列的管理就是消息队列加自身的属性。
关于消息队列大家感兴趣可以查看手册的使用。
system v信号量
信号量主要用于同步和互斥的,当进程们面对同一份资源时,我们的资源就需要被保护起来,而信号量就是为了保护我们的共享资源。
因此我么先来了解同步与互斥:
进程互斥程互斥则是一种防止多个进程同时访问同一资源的机制。在进程互斥的机制下,当一个进程正在访问某个共享资源时,其他进程需要等待该进程释放该资源之后才能访问。实现进程互斥的主要方法有互斥锁和信号量。
进程同步
指的是多个进程在共享资源的过程中保持一致性的机制。其主要目的是避免进程间的竞争和冲突,确保多个进程对共享资源的访问按照一定的顺序和规则进行,从而避免资源的竞争和冲突。
进程同步的实现方法包括信号量、互斥锁、条件变量等。
对于linux系统就是用洗脑两盒互斥锁来解决同步与互斥的问题。
信号量(semaphore),我们在linux中也有对应的函数接口:
semget 获取创建信号量
semctl 控制信号量
、
数1为semid,参数二为编号(起始从0开始),参数三为指令。
semop 操作信号量
对信号量做加加减减操作
参
可以看到无论是共享内存,还是消息对列或者信号量,他们都是有统一的标准的接口,因此这里的函数我们也能使用了。
返回值一般我们叫做semid,参数一为key值,参数二为创建的个数,参数三为标志.
利用指令ipcs -s 可以查看创建的信号量,可以看到创建完进程结束,但信号量还在,因此生命周期还是随内核。
当然对于信号量,也是需要被操作系统管理,管理信号量及其信号量属性。
其次内核是怎样看待IPC资源(共享资源):
首先肯定有两部分:1.会有单独设计的一个模块。2.会有特定的保护机制。
所谓的信号量本身就是一个计数器,为了让进程通信,多个执行流共享同一份资源,而公共资源被并发访问就会产生数据不一致的问题,因此我们就需要保护我们的共享资源来面对同步与互斥的两个情况。
解决同步与互斥简单来说就是,互斥:任何一个时刻只允许一个执行流来使用该资源。同步:多个执行流都来使用该资源时,让他按照一定的顺序来执行。
信号量通俗点说,就是资源数目的计数器,每一个执行流想要访问资源内的某一份资源,不应该执行流直接访问,而是先申请信号量资源,(其实就是对计数器信号量--操作),申请成功后,就完成了对资源的预定机制,如果申请不成功,则执行流就进行阻塞。
我们在编写代码时如果想要访问这一份共享资源,先要申请信号量资源,如果成功,则返回之后的信号量,失败就阻塞。而阻塞的这一部分资源就是临界资源。
但也有特殊情况,只有一个信号量即只允许一个人访问的资源,这种信号量被叫做二元信号量,即就是一个锁的功能---互斥锁。
原子性:只有两种状态,要么不做,要么做完。
本次我们主要先 认识一下。