共享内存

共享内存的原理

==进程通信的本质就是——让不同的进程看到一份相同的资源!==

image-20230728210819347

==所以想要让两个彼此独立的进程进行通信就必须做三件事情==

  1. 申请一块内存空间
  2. 将创建好内存映射进进程的地址空间里面
  3. 当未来不想通信的时候——取消进程和内存的映射关系然后释放内存!

image-20230728211826439

==将创建好内存映射进进程就是——叫做进程和共享内存进程挂接!==

==取消进程和内存的映射关系又叫做——去关联==

进一步理解共享内存

如果我们使用C语言的malloc能不呢做到这件事情呢?——==不行!==

因为虽然C语言的malloc是可以做到开辟内存空间,同时将内存空间通过页表映射到进程地址空间里面,让进程查看内存的相关资源

那么这样子和上面的有什么区别呢?——==malloc出来的空间是不能让其他进程访问的!==

所以==system V版本的共享内存的进程通信 ,是专门设计出来的进行IPC的(IPC——进程间的通信)和malloc或者new是完全不同的东西!主要是因为这是为了让申请出来的空间被进程共享!——内部为了维护多进程共享做了更多的工作==

==共享内存是一种通信方式!,所有想通信的人都可以用!==

==操作系统中一定可能会同时存在很多的共享内存!——我们可以用共享内存,别人自然也可以用!==

共享内存的概念

==通过让不同的进程,看到同一个内存块方式就是——共享内存!==

创建共享内存的函数接口

image-20230729101839858

image-20230729103310263

分配一个system V的共享内存段——这就是shmget函数的功能

==当函数执行成功——这个函数就会返回一个共享内存标识符(是一个数字下标!),如果失败就会返回-1==

这个函数有三个参数

最好理解的就是size参数——即申请多大的内存空间(但是如果不是PAGE_SIZE的值的整数倍就会向上取整)

shmflg参数——有两种选项——==IPC_CREAT和IPC_EXCL==

IPC_CREAT——如果共享内存不存在就创建它,如果存在——就获取这个共享内存!(一般都是第一个进程创建一个共享内部,后面的进程获取共享内存)——IPC_CREAT的值就是0

IPC_EXCL——这个参数是无法单独使用的!(单独使用无意义!)

==一般IPC_EXCL是与IPC_CREAT一起使用!( 形式:IPC_CREAT|IPC_EXCL)——代表的含义就是如果不存在就创建共享内存,如果存在这个共享内存就出错返回!==(什么情况下会使用这种情况呢?——**最大意义就在于,如果创建成功,这一定是一个==新的共享内存==**)

==最重要的就是key参数==

我们应该如何保证——我们获取的是同一个共享内存!(内存标识符是只有调用了该函数之后我们才能获取得到!是为了方便我们使用它,我们必须在调用函数的时候就知道我们获取的是同一个共享内存!)

==key值是什么不重要!——重要的是能进行唯一性标识很重要!==

因为shmget返回的共享内存会与key值相关!

image-20230729110644880

==那么我们该如何获取一个key值呢==

image-20230729105921053

哦我们可以使用ftok函数来获取——这个函数可以将==一个路径名和一项目标识符(这个值可以自己随便写)==转化为一个唯一的key值!——这个key值是多少是不重要的!重要的是能够帮我们形成一个与其他key值不冲突的唯一值!

image-20230729111613509

如果成功返回 0 ,如果失败返回 -1

请注意,获取key值的路径名必须是一个已经存在的文件,否则ftok函数将会失败。另外,不同的文件路径和proj_id组合可能会产生相同的key值,因此在使用ftok函数时应该选择合适的文件路径和proj_id来确保唯一性。

==我们可以通过相同的路径和id形成相同的key值,这样子,不同的进程就可以在操作系统里面找到同样的共享内存了!==

//comm.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include<cstring>
#include<cerrno>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x12

key_t getKey()
{
    key_t k = ftok(PATHNAME,PROJ_ID);
    if(k < 0 )
    {
        std::cerr<< "errno : "<< errno << ":"<< std::strerror(errno) << std::endl;
        exit(-1);
    }
    return k;
}
#endif
//shm_cliet.cc
#include "comm.hpp"
int main()
{
    key_t k = getKey();
    std::cout << k <<std::endl;
    return 0;
}
//shm_server.cc
#include"comm.hpp"
int main()
{
    key_t k = getKey();
    std::cout << k << std::endl;
    return 0;
}

image-20230729114207777

ftok确实可以形成相同的key

//shm_server.cc
#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include<cstring>
#include<cerrno>
#include<stdlib.h>
#include<cstdio>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x66
#define MAX_SIZE 4096

key_t getKey()
{
    key_t k = ftok(PATHNAME,PROJ_ID);
    if(k < 0 )
    {
        std::cerr<< "errno : "<< errno << ":"<< std::strerror(errno) << std::endl;
        exit(-1);
    }
    return k;
}
int getShmhelper(key_t key,int flags)
{
    int shmid = shmget(key,MAX_SIZE,flags);
    if(shmid < 0)
    {
        printf("errer code : %d : %s",errno,std::strerror(errno));
        exit(-1);
    }
    return shmid;
}

int creatShm(key_t k)//给第一个进程使用的!用于创建内存
{
    //创建一定是一个全新的共享内存
    return getShmhelper(k,IPC_CREAT|IPC_EXCL);
}

int getShm(key_t k)//给后续进程使用的!
{
    return getShmhelper(k,IPC_CREAT);//也可以设置为0,默认行为就是不存在帮你创建,存在给你返回
}
#endif
//shm_server.cc
#include"comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = creatShm(k);
    printf("shmid: %d\n",shmid);
    return 0;
}
//shm_cliet.cc
#include "comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = getShm(k);
    printf("shmid: %d\n",shmid);
    return 0;
}

image-20230729171550122

==我们可以看懂key值和id都是一眼一样的==

但是如果我们重复的进行运行就会出现

image-20230729171922335

上面我们说个过OS中一定可能会同时存在很多的共享内存!而共享内存是需要申请的!

我们以前使用c语言的malloc和free,使用malloc的时候我们需要传入要申请的空间的大小!但是free的时候却不用——这是因为虽然我们其实系统不是根据我们申请多少就给我们分配多少!——它一定会多出一些空间用来专门的存储这个内存的属性!而申请内存有时候不止一个,甚至几百上千个!——这是就要用到这个些属性进行先描述后组织的进行管理

==语言级的尚且如此——那么内核级的内存申请要如此!——操作系统替我们申请内存后,同时也要管理内存!==

共享内存 = 物理内存块 + 共享内存的相关属性!

当创建共享内存的时候——如何保证系统是唯一的呢?(这是一定要保证的这样子才能让不同的进程可以找到同一个共享内存)——==就是用key和这个共享内存关联起来保证唯一性!==

那么key在哪里呢?——==在共享内存的相关属性里面!==

在内核里面就有一个struct shm结构体,里面就有一个key

image-20230729173921956

==当我们使用shmget的时候传入key本质就是将我们key值设置进入这个结构体里面的==——换句话就是要设置进入共享内存的属性里面的!用来表示共享内存在内核里面的唯一性!

==那么shmid 和key有什么区别呢?——shmid类似与于d(是一个下标),而key类似于文件管理系统中的inode==——虽然shmid和key都具有标识的作用,但是为了方便上下层解耦——shmid是在用户层标志唯一性,key是在内核层标识唯一性!

共享内存的删除与ipc资源的查看和特征

当我们尝试多次运行进程的时候,我们会发现——除了第一次,后面都因为共享内存已经存在而报错!——==可是我们的进程明明已经退出了!按理来说进程退出后,通信资源不是应该也相对应的被释放吗?==(像是我们打开文件,或者管道,都是在进程退出后会自动释放的!)

==但是共享内存却不一样!==

ipcs -m

该指令是我们用来==查看共享内存的指令==

image-20230729193410678

==我们就可以查看到我们刚刚创建的共享内存段——我们进程退出后,但是共享内存还是存在!==

这就引入了我们ipc资源的==第一个特征==——==共享内存的生命周期是随着OS的,而不是随着进程的!==

如果我们没有显示的去命令或者函数接口去释放共享内存,这个共享内存会一直存在!

除非我们对操作系统进行关机重启!

==这也是所有system V版本的进程间通信的特性!==image-20230729193934840

我们可以使用ipcs指令查看所有的systemV版本的资源!——有例如消息队列,共享内存,信号量!ipc -s(共享内存)/-m(信号量)/-q(消息队列)

==那么我们该如何删除ipc资源呢?——指令版本==

ipcrm -m xxxxx(shmid)

我们虽然直觉上来说是使用key值来删除——但是实际上我们应该使用的是shmid!因为我们处于用户层!而shmid是用户层的标识符!而key是属于内核层的标识符!

image-20230729194525278

这样子我们成功的的删除了共享内存!

当然我们也是可以选择用key值删除的 ipcrm -M key值这样也行!

除此之外还有很多其他的选项帮组我们删除ipc资源!

image-20230729194735728

==删除共享内存的系统调用接口——shmctl==

image-20230729195054944

但是该接口不单单是用来删除的——==这是一个控制接口!(删除,设置,获取属性)==

shmctl()函数的功能是对标识符为shmid的System V共享内存段执行由cmd参数指定的控制操作。

==shmid参数就是——我们要控制的共享内存是哪一个!==

==cmd有很多的选项——但是我们这边介绍的有三个IPC_STAT,IPC_SET,IPC_RMID==

IPC_STAT:将与shmid相关联的内核数据结构中的信息复制到buf指向的shmid_ds结构中。调用者必须对共享内存段具有读取权限。

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_cpid; /* 创建者的PID */
       pid_t shm_lpid; /* 最后一次执行shmat(2)/shmdt(2)的进程的PID */
       shmatt_t shm_nattch; /* 当前附加的进程数 */
       // 其他成员...
};
struct ipc_perm {
       key_t __key; /* 由shmget(2)提供的键值 */
       uid_t uid; /* 拥有者的有效用户ID */
       gid_t gid; /* 拥有者的有效组ID */
       uid_t cuid; /* 创建者的有效用户ID */
       gid_t cgid; /* 创建者的有效组ID */
       unsigned short mode; /* 权限 + SHM_DEST和SHM_LOCKED标志 */
       unsigned short __seq; /* 序列号 */
};

IPC_SET:将buf指向的shmid_ds结构的一些成员值==写入==与该共享内存段相关联的内核数据结构,并更新其shm_ctime成员。

==IPC_RMID==:标记该段要被销毁。只有在最后一个进程分离它时(即与shm_nattch成员为零时),该段实际上才会被销毁。(remove shmid)——==这个选项我们一般最常用!==

==buf参数——当我们想要获取共享内存的信息的时候我们就可以上传这个参数!——如果不需要,传一个空指针即可==

image-20230729202501252

特别返回值就是SHM_STAT和SHM_INFO两个操作,但是我们一般不怎么使用所以就不细讲了

==主要就是成功返回值是 0 ,失败返回值是 -1==

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include<cstring>
#include<cerrno>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x12
void delShm(int shmid)//这就是删除
{
    int n = shmctl(shmid,IPC_RMID,nullptr);
    if(n == -1)
    {
        std::cerr << "errno:" << errno << " : " << std::strerror(errno) << std::endl;
    }
}
#endif
//shm_server.cc
#include"comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = creatShm(k);
    printf("shmid: %d\n",shmid);

    sleep(2);
    delShm(shmid);//一般是谁创建谁删除
    return 0;
}

image-20230729204426212

==我们可以直观的看出来共享内存传创建后被删除!==

关联共享内存和进程

上面我们都是再说如何创建和删除共享内存!——但是我们还是不能使用这个共享内存!因为我们还没有讲进程和共享内存关联起来!

==所以我们要采用另一个接口关联共享内存和进程==

image-20230729204940974

==shmat(shm attache ))——这个系统接口就是将共享内存和进程管理起来==

==参数shmid——就是要关联的是哪一个共享内存==

==参数shmaddr——将这个共享内存映射到哪一个地址空间里面!==

但是绝大部分情况我也不会设置这参数,主要是我们并不知道我们要将共享内存映射到虚拟地址空间的哪一个区域里面!如果是空指针==操作系统会帮我们选择一个合适的没有被使用果的地址来进行映射==

==参数shmflg——和读写权限有关的参数——我们默认设置为0就可以==

image-20230729210101849

shmat的返回值就是我们——==共享内存映射在进程地址空间的起始地址!==(等价于我们以前使用的malloc)——如果失败就返回 -1

//comm.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include<cstring>
#include<cerrno>
#include<stdlib.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x12
#define MAX_SIZE 4096

void * attachShm(int shmid)
{
    void* mem = shmat(shmid,nullptr,0);    
    if((long long)mem == -1L)//如果是32位系统请修改为(int) 100这种值我们称之为字面值  只是编译器一般将其认作是整数,为了让数据类型更清楚我们可以在后面加个后缀 1L(就是一个long long类型的整形) 10u(就是一个无符号类型整数)
    {
        std::cerr << "shmat:"<< "errno:" << errno << " : " << std::strerror(errno) << std::endl;
        exit(-4);
    }
    return mem;
}
#endif

//shm_server.cc
#include"comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = creatShm(k);
    printf("shmid: %d\n",shmid);
    sleep(2);
    char* mem = (char*)attachShm(shmid);
    printf("attach success!address start :%p\n",mem);
    sleep(2);
    delShm(shmid);
    return 0;
}

image-20230729212817074

==我们发现执行的时候出问题了?——显示权限不足!==

我们本来是想看到有多少个内存和进程是关联的

image-20230729212929909

就是这个nattch的属性!——显示0 就是说明没有和任何进程关联

出现我们上面的问题主要是和另一个属性有关==perm(permission)==)这个说明了当前的共享内存权限是全0,意味着该共享内存不能读也不能写,不能被执行!

//所以在创建共享内存的时候我们必须要被权限加上!
int creatShm(key_t k)
{
    return getShmhelper(k,IPC_CREAT|IPC_EXCL|0666);//表示该内存可读可写!
    //0666 表示拥有者 所属组 其他人有读写执行的权限或者我们可以0600 只让拥有者有全权限!
}

image-20230729213506417

==我们可以看到shmflg参数中选项也是有着一项的!==

image-20230729213941973

==我们再次运行——我们就可以发现了perm参数和nattch参数都发生了变化!==

当然——我们删除的时候最好不要直接删除——我们要进行去关联!(去关联不是删除共享内存,只是将进程和共享内存的映射关系去掉!)

==使用去关联的接口==

image-20230730111057536

shmdt(shm detach(拆卸))

==这个函数的作用就是将共享内存从调用这个函数的进程的进程空间中进行卸载==

shmaddr——就是我们共享内存在映射在进程地址空间的地址——就是shmat的返回值!

image-20230730111617559

==成功就是0,失败就返回 -1==

//comm.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include<cstring>
#include<cerrno>
#include<stdlib.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x12
#define MAX_SIZE 4096

void (void* start)
{
    if(shmdt(start) == -1)
    {
        std::cerr << "shmdt:"<< "errno:" << errno << " : " << std::strerror(errno) << std::endl;
    }
}
#endif

//shm_server.cc
#include"comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = creatShm(k);
    printf("shmid: %d\n",shmid);
    sleep(2);
    char* mem = (char*)attachShm(shmid);
    printf("attach success!address start :%p\n",mem);
    sleep(2);
    detachshm(mem);
    sleep(2);
    delShm(shmid);
    return 0;
}

image-20230730112631241

//client.cc
//对于clinet端我们也是一样进行挂接
#include "comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = getShm(k);
    printf("shmid: %d\n",shmid);

    char* mem = (char*)shmat(shmid,nullptr,0);
    printf("attach success!address start :%p\n",mem);

    detachshm(mem);//client只需要去关联就可以了!不用删除
    return 0;
}

进行通信

我们刚刚进行的所有的操作都是为了让两个进程看到同一份的资源!还没有进行通信!

//server.cc
#include"comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = creatShm(k);
    printf("shmid: %d\n",shmid);
    sleep(2);
    char* mem = (char*)attachShm(shmid);
    printf("attach success!address start :%p\n",mem);
    while(true)
    {
        //我们将mem看做是字符串!
        printf("client : say# %s\n",mem); 
        sleep(1);
    }
    detachshm((void*)mem);
    sleep(2);
    delShm(shmid);
    return 0;
}

//client.cc
#include "comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = getShm(k);
    printf("shmid: %d\n",shmid);

    char* mem = (char*)shmat(shmid,nullptr,0);
    printf("attach success!address start :%p\n",mem);

    const char* messege  = "hello i am client, i am sending messege to you:";
    pid_t id = getpid();
    int cnt = 1;
    while(true)
    {
        snprintf(mem,MAX_SIZE,"%s[pid:%d][count:%d]",messege,id,cnt);
        cnt++;
        //我们可以直接将数据写入就可以!
    }
    detachshm(mem);//client只需要去关联就可以了!不用删除

    return 0;
}

image-20230730205911964

==两个进程就完成了通信!==

共享内存的优缺点

共享内存是所有进程通信方式中速度最快的!——因为是直接通过内存进行通信的!可以大大减少的减少数据的拷贝的速度!

例如同样的代码管道和共享内存的拷贝次数

管道的拷贝次数:

image-20230730211427698

一共6次

共享内存的拷贝次数:

image-20230730211558258

一共4次

不要看只减少了两轮拷贝!——如果一次的拷贝是十几二十T的大小,那么差距就很大了!

==共享内存的缺点==

//server.cc
#include"comm.hpp"
int main()
{
    //...
    while(true)
    {
        printf("client : say# %s\n",mem); 
        sleep(1);//每隔1s读一次!
    }
    //...
}

//client.cc
#include "comm.hpp"
int main()
{
    //...
    pid_t id = getpid();
    int cnt = 1;
    while(true)
    {
        sleep(5);//每隔5s写一次
        snprintf(mem,MAX_SIZE,"%s[pid:%d][count:%d]",messege,id,cnt);
        //我们可以直接将数据写入就可以!
    }
    //...
    return 0;
}

image-20230730221306991

我们可以看到当读的速度快,而写的速度慢的时候——会一直重复的读取老的数据!

==这也是是共享内存的缺点!——不会进程同步和互斥的操作,不会对数据进行任何的保护!==

像是管道——如果没有写入,就会阻塞,如果写入太快,读取太慢,就会写满后就不在进行写入

==如果我们想要实现写完通知读端去读取,没有同时读端的时候让读端去阻塞等待,应该怎么实现呢?==

其实也很简单——可以通过管道的特性来解决!

image-20230730222505405

共享内存的特点总结

  1. 生命周期随着内核
  2. 通讯速度最快!
  3. 没有数据保护!

共享内存的内核结构

我们在使用shmctl这个函数的第三个参数类型就是shmds_t(shm data struct),这个类型就是共享内存的数据结构(不止是怎么简单,在内核里面更加的复杂,只是暴露给我们简化了)

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_cpid; /* 创建者的PID */
    pid_t shm_lpid; /* 最后一次执行shmat(2)/shmdt(2)的进程的PID */
    shmatt_t shm_nattch; /* 当前附加的进程数 */
    // 其他成员...
};
struct ipc_perm {
    key_t __key; /* 由shmget(2)提供的键值 */
    uid_t uid; /* 拥有者的有效用户ID */
    gid_t gid; /* 拥有者的有效组ID */
    uid_t cuid; /* 创建者的有效用户ID */
    gid_t cgid; /* 创建者的有效组ID */
    unsigned short mode; /* 权限 + SHM_DEST和SHM_LOCKED标志 */
    unsigned short __seq; /* 序列号 */
};
#include"comm.hpp"
int main()
{
    key_t k = getKey();
    printf("key : %d\n",k);
    int shmid = creatShm(k);
    printf("shmid: %d\n",shmid);
    char* mem = (char*)attachShm(shmid);
    printf("attach success!address start :%p\n",mem);
    while(true)
    {
        shmid_ds shmds;
        shmctl(shmid,IPC_STAT,&shmds); 
        printf("size : %d,creatpid: %d :mypid: %d,key:0x%x \n",shmds.shm_segsz,shmds.shm_cpid,getpid(),shmds.shm_perm.__key);
        sleep(1);
    }
    detachshm((void*)mem);
    sleep(2);
    delShm(shmid);
    return 0;
}

image-20230730224854748

我们发现我们确实可以获取到属性!

还有关于共享内存的大小

建议是是4KB的整数倍!——因为系统分配共享内存的大小是以4KB为基本单位的!——是内存划分数据块的基本单位!

==例如:我们申请的是4097大小!——那么在内核里面会向上取整,也就是4096*2 = 8192的大小空间即8KB的空间!==

image-20230730225710961

虽然我们看上去是4097——但是==内核给我们的内存大小和我们能用的内存大小是两个不同概念!==4097字节只是说明内核给我们的8KB的空间里面我们只能使用4097个字节!