前言
本文介绍了另一种进程间通信——system V,主要介绍了共享内存,消息队列、信号量,当然消息队列了信号量并非重点,简单了解即可。
一、共享内存
1.共享内存的基本原理
共享内存:不同的进程为了进行通信看到的同一个内存块,该内存块被称为共享内存。
进程具有独立性,它的内核数据结构包括对应的代码,数据与页表都是独立的。
OS系统为了让进程间可以实现通信:1.在物理内存上申请一块内存空间 2.将申请好的内存分别与各个进程的页表之间建立映射,然后在各个进程的虚拟地址空间中将虚拟地址与页表建立映射,从而建立起物理地址与虚拟地址的联系。
如果不想继续通信,就取消进程与内存间的映射关系,释放内存。
- 我们将创建好的内存称为共享内存;
- 将进程与共享内存建立映射的操作称为挂接;
- 把取消进程与内存的映射关系这一操作称为关联;
- 把释放内存称为释放共享内存。
共享内存的建立:在物理内存当中申请共享内存空间;将申请到的共享内存嘎姐到地址空间(建立映射关系)。
共享内存的释放:共享内存与地址空间去关联(取消映射关系),释放共享内存空间(将物理内存归还系统)。
对于共享内存的理解:
对比C语言中的malloc可以在物理内存中申请空间,并将开辟好的空间通过页表映射到进程地址空间当中。system V进程间通信,是专门设计的,用于IPC;共享内存是一种通信方式,所有想进行通信的进程都可以使用(OS一定可能会同时存在很多的共享内存)
2.共享内存的创建
-
shmget
:用来创建共享内存
- 参数认识:
shmflg
:通常有两个选项:IPC_CREAT
、IPC_EXCL
。
-
IPC_CREAT
:共享内存不存在,则创建,如果存在,则获取。 -
IPC_EXCL
:无法单独使用,只能配合IPC_CREAT
使用。IPC_CREAT | IPC_EXCL
:如果不存在就创建,如果存在就报错。
size
:共享内存的大小key
:共享内存的唯一性标识,保证进程看到同一份共享内存。如何形成key?用ftok
。ftok
:形成keyftok
是通过存在的路径名pathname
以及设置的标识符proj_id
来形成一个key
值,通过shmget
创建共享内存时,key
值会被填充到维护共享内存的数据结构当中。
key_t getkey()
{
key_t = ftok(PATHNAME, PROJ_JD);
if(k < 0)
{
cout<<"error:"<<errno<<":"<<strerror(errno)<<endl;
exit(1);
}
return k;
}
为什么要存在key?
OS中一定存在很多的共享内存,而共享内存本质就是在内存中申请一块空间,这个key就是用来唯一标识共享内存的。
OS申请的共享内存,那么它一定会对共享内存进行管理(先描述,再组织),共享内存 = 物理内存块 + 共享内存的相关属性。
如果两个进程为了进行通信使用共享内存,那么一定要让两干进程看到同一个key的共享内存,那么key值存在哪里呢?key作为共享内存的唯一标识,应该存在共享内存的相关属性集合。描述共享内存的数据结构的字段struct shm中存着key。
共享内存数据结构的第一个成员是shm_perm
,shm_perm
是一个ipc_perm
类型的结构体变量,每个共享内存的key值都存储在shm_perm
中。
// ipc_perm结构体如下
struct ipc_perm{
key_t __key;
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
unsigned short mode;
unsigned short __seq;
};
3.共享内存的控制
shmctl:控制共享内存
参数
shmid控制共享内存的标识符;
cmd控制种类;
buf控制共享内存的数据结构(一般设置为NULL)。
返回值
返回0表示成功,返回-1表示失败。
共享内存的内核数据结构
struct shmid_ds{
struct ipc_perm shm_perm;
size_t shm_segsz;
time_t shm_atime;
time_t shm_dtime;
time_t shm_ctime;
pid_t shm_cpod;
pid_t shm_lpod;
shmatt_t shm_nattch;
//…
4.共享内存的关联
shmat
:关联共享内存
参数
shmaddr:指定虚拟地址(一般设置为nullptr);
shmflg:读取权限(一般设置为0)。
5.共享内存的去关联
shmdt:去关联
6.查看IPC资源
对于管道,进程退出,文件描述符会自动被释放(文件描述符的生命周期是随进程的),但是对于共享内存来说不是这样的,共享内存的生命周期是随OS的,而不是随进程,这是所有system V进程间的共性。
7.查看共享内存
ipcs -m
8.删除共享内存
ipcsrm -m (shmid)
二、实现进程间通信(代码)
文件comm.hpp
#ifndef __COMM_HPP_
#define __COMM_HPP_
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
#define PATHNAME "."
#define PROJ_JD 0x66
#define MAX_SIZE 4096
key_t getkey()
{
key_t k = ftok(PATHNAME,PROJ_JD);
if(k <0)
{
cerr<<errno<<":"<<strerror(errno)<<endl;
exit(1);
}
return k;
}
int getShmHelper(key_t k,int flags)
{
//k是要shmget,设置进入共享内存属性中的,用来标识
//该共享难内存在内核中的唯一性
//shmid与key:
//fd inode
int shmid = shmget(k,MAX_SIZE,flags);
if(shmid<0)
{
cerr<<errno<<":"<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
//获取
int getShm(key_t k)
{
return getShmHelper(k,IPC_CREAT);
}
//创建
int createShm(key_t k)
{
return getShmHelper(k,IPC_CREAT | IPC_EXCL|0600);
}
void delShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,nullptr)==-1)
{
cerr<<errno<<":"<<strerror(errno)<<endl;
}
}
void * attachShm(int shmid)
{
void*mem = shmat(shmid,nullptr,0);
if((long long)mem==-1L)//64位系统,8个字节,L表示数字类型
{
cerr<<errno<<"shmat:"<<strerror(errno)<<endl;
exit(3);
}
return mem;
}
void detachShm(void * start)
{
if(shmdt(start)==-1)
{
cerr<<"shmdt:"<<errno<<":"<<strerror(errno)<<endl;
}
}
#endif
文件server.cc
#include "comm.hpp"
#include <unistd.h>
using namespace std;
int main()
{
key_t k = getkey();
printf("key:%0x%x\n",k);
int shmid = createShm(k);
printf("shmid:%d\n",shmid);
//sleep(5);
char*start = (char*)attachShm(shmid);
printf("attach success,address start:%p\n",start);
//使用
while(true)
{
printf("client say:%s\n",start);
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);
printf("获取属性:size:%d,pid:%d,myself:%d",ds.shm_segsz,ds.shm_cpid);
sleep(1);
}
//去关联
detachShm(start);
sleep(10);
//删除共享内存
delShm(shmid);
return 0;
}
文件client.cc
#include "comm.hpp"
#include <unistd.h>
using namespace std;
int main()
{
key_t k = getkey();
printf("key:%0x%x\n",k);
int shmid = getShm(k);
printf("shmid:%d\n",shmid);
char*start = (char*)attachShm(shmid);
printf("attach success,address start:%p\n",start);
const char*message = "hello server,我是另一个进程,正在和你通信";
pid_t id = getpid();
int count = 1;
//char buffer[1024];
while(true)
{
sleep(5);
snprintf(start,MAX_SIZE,"%s[pid:%d][消息编号:%d]",message,id,count++);
// snprintf(buffer,sizeof(buffer),"%s[pid:%d][消息编号:%d]",message,id,count++);
// memcpy(start,buffer,strlen(buffer)+1);
}
detachShm(start);
return 0;
}
三、共享内存的特点
-
共享内存的生命周期是随OS的,而不是随进程,这是所有system V进程间通信的共性。
-
共性内存是所有进程间通信速度最快的,因为共享内存是被双方所共享,只要一方有写入,另一方就会立即看到,这样可以大大减少数据的拷贝次数。(优点)
-
综合考虑管道和共享内存:
管道:
写入端进程:需要通过键盘输入到自己定义的缓冲区char buffer[],将数据拷贝到buffer中,再调用write接口将buffer中的数据拷贝到管道中。
读取端进程:也定义了buffer缓冲区,调用read接口将数据从管道拷贝到buffer中,再将数据显示到显示屏上。
共享内存:
通过映射,直接从输入到共享内存,从共享内存到输出。 -
共享内存不给我们提供同步和互斥的操作,无法对数据进行保护。客户端和服务端没有做保护,如果想要保护数据,需要用到信号量,对共享内存进行保护,写完通过读端读取。(缺点)
四、消息队列(了解)
1.概念
消息队列是OS提供的内核级队列,消息队列提供了推广从一个进程想另一个进程发送一块数据的方法。每个数据块都被认为是有一个类型,而接收者进程接收的数据块可以是不同的类型值。
2.消息队列数据结构
struct msqid_ds{
struct ipc_perm msg_perm;
time_t msg_stime;
time_t msg_rtime;
time_t msg_ctime;
unsigned long __msg_cbytes;
msgqnum_t msg_qnum;
msglen_t msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
消息队列数据结构的第一个成员是msg_perm
,它和shm_perm
是同一个类型的结构体变量——ipc_perm
结构体。
3.消息队列的相关函数
msgget:获取消息队列
参数
key
:ftok
函数生成的一个key值,它作为msgget
的第一个参数;msgflg
:与创建共享内存用的函数shmget
的第三个参数相同;
返回值
返回一个有效的消息队列标识符。
msgctl:控制消息队列
它的参数与之前类似接口的参数相同。
msgsnd:发数据
参数
msqid:表示消息队列的用户级标识符;
msgp:表示待发送的数据块;
msgsz:表示待发送的数据块的大小;
msgflg:表示发送数据块的方式,一般默认为0。
返回值
发送成功返回0,发送失败返回-1.
msgrcv:读取消息队列
参数
mspid:表示消息队列的用户级标识符;
msgp:表示获取到的数据块(它是一个输出型参数);
msgsz:表示要获取的数据块的大小;
msgtyp:表示要接收的数据块的类型;
msgflg:表示发送数据块的方式,一般默认为0。
返回值
成功返回实际获取到的mtext数组中的字节数,失败返回-1。
五、信号量
1.概念
信号量的本质是一个计数器,通常用来表示公共资源中资源数多少的问题。信号量主要是用于同步和互斥操作的。
公共资源:能被多个进程同时访问的资源。
- 访问没有被保护的公共资源,会存在数据不一致的问题。(要让不同的进程看到同一份资源是为了进程间通信,通信是为了实现进程的协同,但是,不同进程访问同一份资源会导致该资源数据被修改,进而导致数据不一致的问题)
- 被保护的公共资源称为临界资源,进程要访问资源,一定是进程中有对应的代码来访问这份资源,访问临界资源的代码就称为临界区。有临界区,自然就有非临界区。多进程访问一份临界资源的情况属于少数情况,大部分情况下进程都是申请自己独立的资源,不访问公共资源的代码就是非临界区。
为了避免数据不一致的问题,我们需要对公共资源进行保护,那么该如何保护呢?
答:互斥和同步。
- 互斥:由于有各个进程要求共享资源的情况,并且有些资源需要互斥使用,因此各进程间需要竞争使用这些资源。进程的这种关系称为互斥。
- 原子性:做一件事,只有做完和不做两态(即,要么做要么不做,不能做一半)。
为什么不用全局的整数来作为信号量?
因为全局的整数,有血缘关系的父子进程都不能同时看到(一旦一方修改,就会进行写时拷贝),而不同的进程更加不能看到。因此进程间想看到同一个计数器(可能会发生修改),就不能用全局的整数。
为什么需要信号量?
当我们想要申请某项共享资源时,我们需要通过信号量来预测该共享资源是否被使用。共享资源的使用方式:1.作为一个整体被整个使用(一个打印机,信号量是打印顺序,同一时间只能打印一份文件);2.被划分为一个一个小的资源部分(电影院的座位,信号量是电影票,凭电影票进去看电影,同一场电影可以被多个人同时观看)。进程要访问某些共享资源时,要先申请信号量,申请成功就相当于预定了共享资源,即允许访问该共享资源。
2.信号量数据结构
3.信号量的原子操作(P/V操作)
所有进程在访问公共资源之前,都需要申请sem信号量,而申请信号量的前提是进程必须先看到同一个信号量,所以信号量本身就是一个公共资源。同时信号量的操作必须保证自身是安全的,因此++/–是原子性的。
特殊的:如果信号量的初始值为1,则表示该公共资源是作为一个整体来进行申请、使用、释放的。这种二院信号量是具有互斥功能的。
4.信号量的相关函数
semget:申请信号量
参数
key:使用ftok函数生成的key值,可以唯一表示共享内存;
nsems:表示创建信号量的个数;
semflg:与穿个件共享内存时使用的shmget函数的第三个参数相同。
返回值
信号量集创建成功时返回一个有效到的信号量集标识符。
semctl:信号量的删除
semop:信号量的操作
六、总结
我们发现:共享内存、消息队列、信号量的接口相似度都很高(参数很多都是相同的),获取和删除都是system V标准的进程间通信的操作。
OS的管理本质都是 先描述,再组织,对于共享内存、消息队列、信号量等的第一个成员都是结构体ipc_perm的变量。
虽然它们内部的属性差别很大,但是维护它们的结构的第一个成员是一样的,都可以用key值来标识唯一性。这样设计的好处:在操作系统中可以只定义一个struct ipc_perm结构体类型的数组,每当申请一个IPC资源就在该数组中多开辟一个这样的结构体变量的空间((struct shmid_ds*)perms[0],像这样进行强转强转,此时就能访问其他的属性)
文章来源:https://www.toymoban.com/news/detail-478206.html
总结
以上就是今天要讲的内容,本文介绍了进程间通信的system V的相关概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!文章来源地址https://www.toymoban.com/news/detail-478206.html
到了这里,关于Linux之进程间通信——system V(共享内存、消息队列、信号量等)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!