Linux共享内存

共享内存原理

简而言之,就是两个进程指向了同一块物理空间。(它们都能看到同一块内存
在这里插入图片描述
共享内存在内核中同时可以存在很多个,OS要管理所有的共享内存。
如何保证两个不同进程看到的是同一个共享内存呢???要给共享内存提供唯一性标识(后文提到的key)!!!
使用共享内存通信,一定是一个进程创建新的shm,另一个直接获取共享内存即可。

类比:共享内存 vs 文件操作
共享内存,如果进程结束,我们没有主动释放它,则共享内存一直存在。——共享内存的生命周期随内核。(除非重启系统,否则共享内存一直存在)。
文件操作,一个进程打开一个文件,进程退出时,这个被打开的文件就会被系统自动释放掉。——文件的生命周期随进程。

shmget 系统调用函数

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

参数说明

第一个参数 key

1.这个key意义是什么?怎么形成的?
意义:标识共享内存的唯一性。
如何形成:由用户随意指定key值。

2.为什么要让用户传入?

#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

参数由用户指定,由ftok函数的一套算法生成一个key,只要两个进程约定使用同样的字符串,同样的数字,就可以生成同样的key,从而标识同一块内存。

第二个参数 size

指定共享内存段的大小(以字节为单位)。如果是在获取一个已经存在的共享内存段,这个参数可以设置为 0。

注意在内核中共享内存的大小时以4kb为基本单位的。
例子:使用ipcs -m指令,查看共享内存。(这里申请了4096b大小)
在这里插入图片描述
开辟共享内存是向上取整的,例如申请1b,实际上申请了4096b,还有4095b不能使用被浪费了,申请4097b,实际上申请了2×4096b,还有4095b不能使用被浪费了。
所以建议申请大小为n×4 kb。

第三个参数 shmflg

IPC_CREAT:如果共享内存不存在就创建,如果共享内存已经存在,就直接获取它
IPC_EXCL:不能单独使用,没意义。
IPC_CREAT | IPC_EXCL:如果共享内存不存在就创建,如果共享内存已经存在,出错返回!(如果创建成功,一定是全新的共享内存)。
IPC_CREAT | 八进制权限:赋予权限。

关于共享内存的权限

在这里插入图片描述
使用shmget(key, size, IPC_CREAT | IPC_EXCL);
在这里插入图片描述
使用shmget(key, size, IPC_CREAT | IPC_EXCL| 0666);通过按位或的方式给共享内存加权限。
在这里插入图片描述
实际上第三个标志位的标志定义如下图,最右边三位始终是0,这就给权限位留出了空间。
在这里插入图片描述

删除共享内存

指令删除

ipc指令

ipcs 查看系统中指定用户创建的共享内存,消息队列,信号量。
在这里插入图片描述
ipcs -m 查看系统中指定用户创建的共享内存
在这里插入图片描述
使用 ipcrm -m shmid 删除共享内存

key vs shmid
key:在内核的角度,区分shm的唯一性。(类似于struct file*)
shmid指令级,代码级,最后对共享内存进行控制,用的都是shmid(类似于文件描述符fd)
shmdt

代码删除

shmctl系统调用函数

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

第一个参数 int shmid:
共享内存的标识符

第二个参数 int cmd:
控制命令。常用的命令包括:

IPC_STAT: 获取共享内存段的状态信息,并将其存储在 buf 指向的 shmid_ds 结构中。
IPC_SET:
设置共享内存段的权限和其他属性(如 UID、GID、模式等),使用 buf 中的数据。
IPC_RMID:(用于删除共享内存)
标记共享内存段为已删除。当最后一个进程分离该共享内存段时,它将被真正删除。
IPC_INFO (Linux 特有):
获取系统级别的共享内存信息。 SHM_INFO (Linux 特有): 获取共享内存资源的使用情况。
SHM_STAT (Linux 特有): 类似于 IPC_STAT,但通过索引访问共享内存段。

第三个参数buf:获取共享内存的相关属性

  1. 指向 shmid_ds 结构的指针,用于存储或提供共享内存段的状态信息。
  2. 如果 cmdIPC_RMID,则可以设置为 NULL。

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;  // 最后操作进程的 PID
    shmatt_t        shm_nattch; // 当前附加的进程数
}

返回值
成功时,返回值取决于 cmd:

  • 对于 IPC_STAT、IPC_SET 和 IPC_RMID,成功时返回 0。
  • 对于 IPC_INFO 和 SHM_INFO,返回内核中共享内存段的最大索引值。
  • 对于 SHM_STAT,返回共享内存段的标识符。

失败时,返回 -1,并设置 errno 来指示错误类型。

例子:
代码删除共享内存

void DeleteShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, NULL);
    if (n < 0)
    {
        cerr << "shmctl error" << endl;
    }
    else
    {
        cout << "shmctl delete shm success ,shmid: " << shmid << std::endl;
    }
}

挂接共享内存

shmat函数

shmat (shm sttach)函数是 Unix/Linux 系统中用于将 System V 共享内存段附加到进程地址空间的系统调用。

#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

第一个参数 int shmid:
共享内存的标识符

第二个参数 const void *shmaddr
指明用户将 shm 挂接到哪里。
指定共享内存挂接到进程地址空间的具体地址
如果为 NULL,系统会自动选择一个合适的地址。

第三个参数 int shmflg
控制共享内存段附加行为的标志位。常用标志包括:
SHM_RDONLY:以只读方式挂接。
0:默认行为,读写方式挂接。

返回值
成功:返回共享内存在进程地址空间中的起始地址。所以我们可以使用返回值,直接访问共享内存。
失败:返回 (void *) -1,并设置 errno 以指示错误。

例子:

void* ShmAttack(int shmid)
{
    void* addr = shmat(shmid,nullptr,0);
    if((int)addr == -1)
    {
        std::cerr << "shmat error" << std::endl;
        return nullptr;
    }
    
    return addr;
}

使用ipcs -m查看
在这里插入图片描述
nattch指示的是该共享内存,当前有多少个内存挂接。

shmdt系统调用函数

shmdt(shm detach) 函数是 Unix/Linux 系统中用于将共享内存从进程地址空间分离系统调用。当一个进程不再需要访问共享内存段时,应该调用 shmdt 将其分离,以避免资源泄漏。

#include <sys/shm.h>

int shmdt(const void *shmaddr);

参数说明
const void *shmaddr: 指定共享内存挂接到进程地址空间的具体地址

返回值
成功:返回 0。
失败:返回 -1,并设置 errno 以指示错误。

// 客户端
#include "Comm.hpp"
int main()
{
    // 1.获取key
    key_t key = GetShmKeyOrDie(); // 与服务器以同样的方式获取key
    cout << "key: " << ToHex(key) << std::endl;

    // 2.获取共享内存
    int shmid = GetShm(key, defaultsize);
    std::cout << "shmid: " << shmid << std::endl;
    sleep(5);

    // 3.挂接共享内存
    char *addr = (char *)ShmAttach(shmid);
    cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
    sleep(10);

    // 4.将共享内存与进程分离
    ShmDetach(addr);
    cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
    sleep(5);

    return 0;
}
// 服务端
#include "Comm.hpp"
int main()
{
    // 1.获取key
    key_t key = GetShmKeyOrDie();
    std::cout << "key: " << ToHex(key) << std::endl;
    // 2.创建共享内存
    int shmid = CreateShm(key, defaultsize);
    std::cout << "shmid: " << shmid << std::endl;
    sleep(3);

    // 3.将共享内存和进程进行挂接(关联)
    char *addr = (char *)ShmAttach(shmid);
    cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
    sleep(7);

    // 4.将共享内存与进程分离
    ShmDetach(addr);
    cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
    sleep(10);

    // 5.删除共享内存
    DeleteShm(shmid);
    return 0;
}
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <unistd.h>

using namespace std;

const char *pathname = "/home";
const int proj_id = 0x66;
const int defaultsize = 4096; // 单位是字节

std::string ToHex(key_t k) // 转16进制
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "%x", k);
    return buffer;
}

key_t GetShmKeyOrDie()
{
    key_t k = ftok(pathname, proj_id);
    if (k < 0)
    {
        std::cerr << "ftok error,errno:" << errno << ", errno string:" << std::endl;
        exit(1);
    }
    return k;
}

int CreateShmOrDie(key_t key, int size, int flag)
{
    int shmid = shmget(key, size, flag);
    if (shmid < 0)
    {
        std::cerr << "shmget error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int CreateShm(key_t key, int size)
{
    // IPC_CREAT: 不存在就创建,存在就获取
    // IPC_EXCL: 没有意义
    // IPC_CREAT | IPC_EXCL: 不存在就创建,存在就出错返回

    return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t key, int size)
{
    return CreateShmOrDie(key, size, IPC_CREAT);
}

void DeleteShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if (n < 0)
    {
        cerr << "shmctl error" << endl;
    }
    else
    {
        cout << "shmctl delete shm success ,shmid: " << shmid << std::endl;
    }
}

void ShmDebug(int shmid)
{
    struct shmid_ds shmds;
    int n = shmctl(shmid, IPC_STAT, &shmds);
    if (n < 0)
    {
        std::cerr << "shmctl error" << std::endl;
        return;
    }
    std::cout << "shmds.shm_segsz: " << shmds.shm_segsz << std::endl;
    std::cout << "shmds.shm_nattch:" << shmds.shm_nattch << std::endl;
    std::cout << "shmds.shm_ctime:" << shmds.shm_ctime << std::endl;
    std::cout << "shmds.shm_perm.__key:" << ToHex(shmds.shm_perm.__key) << std::endl;
}

void *ShmAttach(int shmid)
{
    void *addr = shmat(shmid, nullptr, 0);
    if ((long long)addr == -1) // 64位机器下指针大小为8b,而int是4b,强转成int会有精度损失
    {
        std::cerr << "shmat error" << std::endl;
        return nullptr;
    }

    return addr;
}

void ShmDetach(void *addr)
{
    int n = shmdt(addr);
    if (n < 0)
    {
        cout << "shmdt error" << endl;
    }
}

缺点:共享内存不提供进程中协同的任何机制。会引起数据不一致。(进程1向内存写了数据,而进程2只读了一部分数据,数据没有读完整)如何解决?使用信号量,或者使用管道来完成同步。
优点:共享内存是所有进程间通信速度最快的
共享内存只要进程1把数据拷贝到内存,进程2直接可以对该共享内存进行访问。
在这里插入图片描述
对于管道,需要进程1,现将数据通过系统调用write拷贝到管道里,然后进程2,再将数据通过系统调用read拷贝出来,需要2次拷贝。(read,write函数本质上是拷贝函数)
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘子13

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值