【Linux】system V共享内存

发布于:2025-09-15 ⋅ 阅读:(19) ⋅ 点赞:(0)

共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递彼此的数据。

1.共享内存原理

假设现在有两个进程A和B,他们都有自己的进程地址空间,要访问时都要通过虚拟地址空间找到对应的物理空间。

我们可以在物理内存中申请一块空间,然后在虚拟内存的堆栈之间的共享区也申请一块空间。

进程A可以进程B也可以。

这样进程A和进程B就可以通过自己进程地址空间的虚拟地址,访问同一个物理内存了,这就是共享内存,此时进程A和B就可以通信了。

前面所述的所有操作都由操作系统完成,操作系统会为用户提供相应的系统调用接口,我们用系统调用完成上述工作。想要释放这个共享内存,就只需要取消关联关系,OS就会释放内存

2.shm创建和删除

shm就是share memory,shmget函数,shmget函数成功的话返回这个共享内存的标识符,失败的话返回-1.

这个函数的第二个参数size就是共享内存的大小。

第三个参数shmflg是一个标记位,常见的就两个参数就是IPC_CREAT 和 IPC_EXCL,IPC_CREAT:创建共享内存,如果不存在就创建,否则打开这个已存在的共享内存并返回。

IPC_EXCL(单独使用无意义):正确用法是和IPC_CREAT一起用,IPC_CREAT | IPC_EXCL,意思就是如果要创建的共享内存不存在,就创建,如果已存在,函数就会出错返回,所以证明只要shmget成功返回一定会是一个全新的共享内存。

两个进程用shm通信,我们就需要标识共享内存的唯一性,这个唯一性就用第一个参数key来区分。

key不是内核直接形成的,而是用户层构建并传入的,为了让要通信的进程约定一个key,这个key是什么不重要,只要能进行比较就行。

理论上这个key传什么都可以,但是为了减少key值的冲突,一般会使用算法构建来一个key,就是ftok

这个函数就是一个用户级的算法函数,需要用户提供一个路径pathname和project_id,这个两个参数都可以随便写,成功它的返回值就是构建好的key值,失败返回-1,如果这个key还是冲突的,就更改两个参数值就行。

//Shm.hpp文件
#pragma once

#include <iostream>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>

#define PATHNAME "."
#define PROJ_ID 0x66
#define SIZE 4096

#define ERR_EXIT(m) \
do \
{ \
    perror(m); \
    exit(EXIT_FAILURE); \
}while(0)

class Shm
{
public:
    Shm()
        :_shmid(-1), _size(SIZE)
    {}
    void Creat()
    {
        key_t k = ftok(PATHNAME, PROJ_ID);
        if(k < 0)
            ERR_EXIT("ftok");
        printf("key:0x%x\n", k); //k是十六进制的
        _shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL); //创建全新的共享内存
        if(_shmid < 0) 
            ERR_EXIT("shmget");
        printf("shmid:%d\n", _shmid);
    }

    ~Shm(){} 
private:
    int _shmid;
    size_t _size;
};
//server.cc文件
#include "Shm.hpp"
int main()
{
    Shm shm;
    shm.Creat();
    return 0;
}

第一次创建没问题,第二次创建会出错,因为我们创建时用的选项是IPC_CREAT 和 IPC_EXCL。

  • ipcs -m:查看共享内存
  • ipcrm -m shmid:删除共享内存,删除shm资源要指定用shmid,不是用key,因为key未来是只给内核来进行区分唯一性的,用的是shmid来进行管理共享内存的,指令本质是运行在用户空间的,只能用shmid。

创建了共享内存但是没有显示的删除,这个内存依旧会存在,证明共享内存生命周期随内核,不是随进程。

除了指令级别的删除内存,还可以用函数删除内存,shmctl是对shm进行管理的一个函数,这个函数包括了对shm删除的功能。

第一个参数shmid就是共享内存的id值,第二个参数cmd就包含了很多选项,其中IPC_RMID就是删除,第三个参数暂时不管,直接传null就行。失败返回-1。

//Shm.hpp文件
class Shm
{
public:
    Shm()
        :_shmid(-1), _size(SIZE)
    {}
    void Creat()
    {
        key_t k = ftok(PATHNAME, PROJ_ID);
        if(k < 0)
            ERR_EXIT("ftok");
        printf("key:0x%x\n", k);
        _shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL); //创建全新的共享内存
        if(_shmid < 0) 
            ERR_EXIT("shmget");
        printf("shmid:%d\n", _shmid);
    }

    void Destroy()
    {
        if(_shmid >= 0)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if(n < 0)
                ERR_EXIT("shmctl");
            printf("shm:%d 删除成功\n", _shmid);
        }
    }

    ~Shm(){} 
private:
    int _shmid;
    size_t _size;
};
//server.cc文件
#include "Shm.hpp"
int main()
{
    Shm shm;
    shm.Creat();
    sleep(3);
    shm.Destroy();
    return 0;
}

3.shm与进程关联

进程调用shmat函数,就会让自己堆栈上的虚拟地址与shm的物理地址进行映射,将共享内存挂接到进程的地址空间中,at就是attach。

返回值是映射成功之后起始虚拟地址,这段shm是连续的,虚拟地址也是连续的,只要知道起始虚拟地址以及shm的大小,就可以访问shm的任意字节;失败返回-1。

第一个参数就是shmid;第二个参数是一个虚拟地址,我们可以固定地址进行挂接,我们一般不考虑,设置为null就行;第三个参数是权限相关标志位,我们用的时候设为0,表示使用共享内存的默认设置。

这里还需要多加一个类成员变量,就是这个起始虚拟地址。

//Shm.hpp文件 Shm类

    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if((long long)_start_mem < 0)
            ERR_EXIT("shmat");
        printf("Attach success\n");
    }

    void* VirtualAdd()
    {
        printf("virtual add:%p\n", _start_mem);
        return _start_mem;
    }

private:
    int _shmid;
    size_t _size;
    void* _start_mem; 
#include "Shm.hpp"
int main()
{
    Shm shm;
    shm.Creat();
    sleep(2);
    shm.Attach();
    sleep(2);
    shm.VirtualAdd();
    sleep(2);
    shm.Destroy();
    return 0;
}

运行时会发现权限有问题。

这是因为前面shmget函数的第三个参数我们没有设置权限,我们可以直接在这设置权限,和其他文件设置权限是一样的做法,这里直接与就行。

//创建全新的共享内存并设置权限
_shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); 

此时我们再运行时就没问题了。

这里我们再打开一个shell然后添加一个监控脚本,脚本如下。

while :; do ipcs -m; sleep 1; done

此时权限就是我们设置好的,而且会发现nattach变成了1,nattach就是记录有多少个进程和这个共享内存关联

4.获取共享资源

前面我们一直在server这一个进程里操作,现在我们要一个client进程通过共享内存与server进程产生联系,因为server进程已经创建好了共享内存,client进程直接获取共享内存,并且映射到自己的进程地址空间里就行了,这时,两个进程就能看到同一份资源了。

前面我们提到过一个函数shmget,这个函数的第三个参数如果是IPC_CREAT并且共享内存已经存在的情况下,这个函数会打开这个已存在的共享内存并返回。所以获取共享内存的代码如下。

    void Get()
    {
        key_t k = ftok(PATHNAME, PROJ_ID);
        if(k < 0)
            ERR_EXIT("ftok");
        int _shmid = shmget(k, SIZE, IPC_CREAT); //获取共享内存
        if(_shmid < 0)
            ERR_EXIT("shmget");
        printf("shmid:%d\n", _shmid);
    }

这个代码和创建共享内存的代码Creat相比,只有shmget的第三个参数不同,其他都一样,所以我们可以对这两个函数进行调整,重复部分合并一下,写一个辅助函数,并把这个辅助函数设为私有。

//Shm.hpp文件 Shm类
private:
    void CreatHelper(int Opt)
    {
        key_t k = ftok(PATHNAME, PROJ_ID);
        if(k < 0)
            ERR_EXIT("ftok");
        int _shmid = shmget(k, SIZE, Opt); 
        if(_shmid < 0)
            ERR_EXIT("shmget");
        printf("shmid:%d\n", _shmid);
    }

public:
    void Get() //获取共享内存
    {
        CreatHelper(IPC_CREAT);
    }
    void Creat() //创建全新的共享内存并设置权限
    {
        CreatHelper(IPC_CREAT | IPC_EXCL | 0666);
    }

我们在client进程里就可以调用这个接口了,在client进程里获取共享内存然后映射到自己的进程地址空间里,然后我们把虚拟地址打印出来。

//client.cc文件
#include "Shm.hpp"

int main()
{
    Shm shm;
    shm.Get(); //获取共享内存
    sleep(2);
    shm.Attach(); //映射到自己的进程地址空间里
    sleep(2);
    shm.VirtualAdd(); //打印虚拟地址空间
    sleep(2);
    return 0;
}

左边两个是进程server和进程client,server先运行,创建共享空间并且attach成功,右边是监控脚本,可以看到此时这个shm的nattach变成了1,然后运行client进程,此时nattach就变成了2。

client和server的虚拟地址不一样是正常的,一样也是正常的,这个无所谓。

代码整合

我们需要多添加几个类成员变量,pathname、project_id,还有usertype用户类型,然后对代码做调整。

//Shm.hpp文件
#pragma once

#include <iostream>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#define PATHNAME "."
#define PROJ_ID 0x66
#define SIZE 4096
#define CREAT "creat"
#define USER "user"
using namespace std;
#define ERR_EXIT(m) \
do \
{ \
    perror(m); \
    exit(EXIT_FAILURE); \
}while(0)

class Shm
{
private:
    void CreatHelper(int Opt)
    {
        _shmid = shmget(_key, SIZE, Opt); 
        if(_shmid < 0)
            ERR_EXIT("shmget");
        printf("shmid:%d\n", _shmid);
    }

    void Creat() //创建全新的共享内存并设置权限
    {
        CreatHelper(IPC_CREAT | IPC_EXCL | 0666);
    }
    void Get() //获取共享内存
    {
        CreatHelper(IPC_CREAT);
    }
    void Attach()
    {
        _start_mem = shmat(_shmid, NULL, 0);
        if((long long)_start_mem < 0)
            ERR_EXIT("shmat");
        printf("Attach success\n");
    }
    void Destroy()
    {
        if(_shmid >= 0)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if(n < 0)
                ERR_EXIT("shmctl");
            printf("shm:%d 删除成功\n", _shmid);
        }
    }

public:
    Shm(const string &pathname, int projid, const string &usertype)
        :_shmid(-1)
        , _size(SIZE)
        , _start_mem(nullptr)
        ,_pathname(pathname)
        ,_projid(projid)
        ,_usertype(usertype)
    {
        _key = ftok(PATHNAME, PROJ_ID);
        if(_key < 0)
            ERR_EXIT("ftok");
        printf("key:0x%x\n", _key); 
        if(usertype == CREAT) 
            Creat();
        else if(usertype == USER)
            Get();

        Attach(); //挂载
    }

    void* VirtualAdd()
    {
        printf("virtual add:%p\n", _start_mem);
        return _start_mem;
    }

    ~Shm()
    {
        if(_usertype == CREAT) //只有创建者需要销毁
            Destroy();
    } 
private:
    key_t _key;
    int _shmid;
    size_t _size;
    void* _start_mem; 
    string _pathname;
    int _projid;
    string _usertype;
};

 现在在server端和client端就只要只要做初始化就行了,并且调用一下打印虚拟地址的接口。

//server.cc文件
#include "Shm.hpp"
int main()
{
    Shm shm(PATHNAME, PROJ_ID, CREAT);
    shm.VirtualAdd();
    return 0;
}
//client.cc文件
#include "Shm.hpp"
int main()
{
    Shm shm(PATHNAME, PROJ_ID, USER);
    shm.VirtualAdd();
    return 0;
}

5.进程通信

我们可以把整个共享内存看成一块,用char*类型的指针接收VirtualAdd的返回值,server进程直接按字符串打印。

#include "Shm.hpp"
int main()
{
    Shm shm(PATHNAME, PROJ_ID, CREAT);
    char* mem = (char*)shm.VirtualAdd();
    while(true)
    {
        printf("%s\n", mem);
        sleep(1);
    }
    return 0;
}

client进程就发送数据,发送A到Z的数据到共享内存。

#include "Shm.hpp"
int main()
{
    Shm shm(PATHNAME, PROJ_ID, USER);
    char* mem = (char*)shm.VirtualAdd();

    for(char c = 'A'; c <= 'Z'; c++)
    {
        mem[c-'A'] = c;
        sleep(1);
    }
    return 0;
}

先运行server.cc,此时会打印空串,因为shm里还没有东西,nattach也是1。

然后运行client,client就开始往shm里写入数据,server就开始从shm里读数据打印出来,此时nattach为2.

for循环结束时,client进程退出,server进程还在继续,nattach变成1.

我们会发现之前用管道通信的时候,写入或读取要用到系统调用的函数write和read,但是这里我们读写共享内存没有用到系统调用,而是直接读取。

  • 因为我们会把共享内存映射到进程的地址空间中,堆栈之间的共享区里,而这个共享区属于用户,可以让用户直接使用。
  • 管道属于文件,是内核级别的内核文件缓冲区,属于操作系统,所以要用到系统调用。

由于两个进程都把共享内存映射到自己的地址空间里,所以一个进程一写,另一个进程就能直接获取到,所以共享内存通信速度是最快的

从上面的运行结果来看,我们会发现共享内存通信其实存在一个缺点,就是没有像管道通信那样的“同步机制”,server运行起来后直接从共享内存里读,不管client有没有写入,甚至没有client进程都可以。

这种同步机制也是一种保护机制,shm通信就没有保护机制(这里的保护不是对共享内存的保护,而是对共享内存里的数据的保护),这也是他通信速度快的原因。

比如说我们让client写成对的字母AA、BB、CC...到共享内存里,读取的时候就不一定是成对的读,有可能读成AAB、AABBC等等。

//client.cc文件
#include "Shm.hpp"
int main()
{
    Shm shm(PATHNAME, PROJ_ID, USER);
    char* mem = (char*)shm.VirtualAdd();
    int i = 0;
    for(char c = 'A'; c <= 'G'; c++, i += 2)
    {
        mem[i] = c;
        sleep(1);
        mem[i+1] = c;
        sleep(1);
    }
    return 0;
}

模拟保护机制

我们可以让这两个进程再建立一个管道,这个管道是命名管道,而且建立这个管道的目的不是为了通信,而是为了保护共享内存里的数据。

client进程只写了一个数据的时候,不做处理,写两个A的时候,向管道里发送数据,通知server进程可以读取数据了,相当于唤醒server进程,而进程server也不能直接从共享内存里读取数据,而是先从管道里读取数据,如果管道里没有数据,证明client进程还没写完,server进程不做处理,如果管道里有数据(相当于通知server进程可以读取了),server进程才从共享内存里读数据。

此时就可以让server进程读数据的节奏跟着client写的节奏进行了,就能实现同步。

命名管道我们已经实现过了,直接拿来用就行了:【Linux】命名管道 ​​​​​​,对里面的Read和Wirte做一下修改。

//Fifo.hpp文件
#pragma once
#include <iostream>
#include <unistd.h>
#include <string>
#include <sys/types.h>
 #include <fcntl.h>
 #include <sys/stat.h>
 #include <stdio.h>
 #include "comm.hpp"

using namespace std;

#define FIFO_FILE "fifo"

class NamedPipe
{
public:
    NamedPipe(const string& path, const string& name)
        :_path(path), _name(name)
    {
        _fifo_name = _path + "/" + _name;
        umask(0);
        int n = mkfifo(_fifo_name.c_str(), 0666);
        if(n < 0) 
        {
            ERR_EXIT("mkfifo");
        }
        cout << "fifo创建成功" << endl;
    }

    ~NamedPipe()
    {
        int n = unlink(_fifo_name.c_str());
        if(n < 0) 
        {
            ERR_EXIT("unlink");
        }
        cout << "fifo删除成功" << endl;
    }
private:
   string _path;
   string _name;
   string _fifo_name;
};

class FileOper
{
public:
    FileOper(const string& path, const string& name)
        :_path(path), _name(name), _fd(-1)
    {
        _fifo_name = _path + "/" + _name;
    }

    void OpenForRead()
    {
        _fd = open(_fifo_name.c_str(), O_RDONLY);
        if(_fd < 0) 
        {
            ERR_EXIT("open");
        }
        cout << "fifo打开成功" << endl;
    }

    bool Read()
    {
        char c;
        int n = read(_fd, &c, 1); //只要读一个字节就行
        if(n > 0) return true; 
        else return false;
    }

    void OpenForWrite()
    {
        _fd = open(_fifo_name.c_str(), O_WRONLY); //写方式打开
        if(_fd < 0)
        {
            ERR_EXIT("open");
        }
        cout << "client打开fifo成功" << endl;
    }

    void Write()
    {
        char c = 'a'; //这个字符是什么不重要,只要是一个字符就行
        int n = write(_fd, &c, 1);
    }

    void Close()
    {
        if(_fd > 0) close(_fd);
    }

    ~FileOper()
    {}
private:
   string _path;
   string _name;
   string _fifo_name;
   int _fd;
};

先创建管道,然后server端从管道里读,再从共享内存里读,client进程写完两个字符再唤醒server进程。

//server.cc文件
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    NamedPipe fifo(".", FIFO_FILE); //创建命名管道
    FileOper readfile(".", FIFO_FILE);
    readfile.OpenForRead(); //读方式打开

    Shm shm(PATHNAME, PROJ_ID, CREAT); //创建共享内存并挂载
    char* mem = (char*)shm.VirtualAdd();
    while(true)
    {
        //先从管道里读,默认会阻塞在这里
        if( readfile.Read() ) //管道里读成功了才从共享内存里读
        {
            printf("%s\n", mem);
        }
        else 
            break;
    }

    readfile.Close(); //结束时关闭文件
    return 0;
}
//client.cc文件
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    FileOper writefile(".", FIFO_FILE);
    writefile.OpenForWrite(); //打开管道文件

    Shm shm(PATHNAME, PROJ_ID, USER); //打开共享内存并挂载
    char* mem = (char*)shm.VirtualAdd();
    int i = 0;
    for(char c = 'A'; c <= 'G'; c++, i += 2)
    {
        sleep(1);
        mem[i] = c; //先向管道里写成对的字母
        mem[i+1] = c;
        sleep(1);
        
        writefile.Write(); //往管道里写(唤醒server进程)
    }

    writefile.Close(); //关闭文件
    return 0;
}

上图是只运行server没运行client,下图是运行了client。

可以看到此时就是按照一对一对的字母读取的。

因为client进程和server进程并发运行时,可能会导致client进程比server进程更先创建出共享内存,但是我们client进程里Shm shm(PATHNAME, PROJ_ID, USER); 只是获取共享内存,client进程先创建共享内存就会出现如下报错。

所以最好是把server进程创建共享内存的步骤放在最前面

//server.cc文件
int main()
{
    Shm shm(PATHNAME, PROJ_ID, CREAT); //先创建共享内存并挂载
    char* mem = (char*)shm.VirtualAdd();
    
    NamedPipe fifo(".", FIFO_FILE); //创建命名管道
    FileOper readfile(".", FIFO_FILE);
    readfile.OpenForRead(); //读方式打开

    while(true)
    {
        //先从管道里读,默认会阻塞在这里
        if( readfile.Read() ) //管道里读成功了才从共享内存里读
        {
            printf("%s\n", mem);
        }
        else 
            break;
    }

    readfile.Close(); //结束时关闭文件
    return 0;
}

下面是其他文件源码。

//Shm.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
 #include "comm.hpp"

#define PATHNAME "."
#define PROJ_ID 0x67
#define SIZE 4096
#define CREAT "creat"
#define USER "user"

using namespace std;

class Shm
{
private:
    void CreatHelper(int Opt)
    {
        _shmid = shmget(_key, SIZE, Opt); 
        if(_shmid < 0)
            ERR_EXIT("shmget");
        printf("shmid:%d\n", _shmid);
    }

    void Creat() //创建全新的共享内存并设置权限
    {
        CreatHelper(IPC_CREAT | IPC_EXCL | 0666);
    }

    void Get() //获取共享内存
    {
        CreatHelper(IPC_CREAT);
    }

    void Attach()
    {
        _start_mem = shmat(_shmid, NULL, 0);
        if((long long)_start_mem < 0)
            ERR_EXIT("shmat");
        printf("Attach success\n");
    }

    void Destroy()
    {
        if(_shmid >= 0)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if(n < 0)
                ERR_EXIT("shmctl");
            printf("shm:%d 删除成功\n", _shmid);
        }
    }

public:
    Shm(const string &pathname, int projid, const string &usertype)
        :_shmid(-1)
        , _size(SIZE)
        , _start_mem(nullptr)
        ,_pathname(pathname)
        ,_projid(projid)
        ,_usertype(usertype)
    {
        _key = ftok(PATHNAME, PROJ_ID);
        if(_key < 0)
            ERR_EXIT("ftok");
        printf("key:0x%x\n", _key); 
        if(usertype == CREAT) 
            Creat();
        else if(usertype == USER)
            Get();
        Attach(); //挂载
    }

    void* VirtualAdd()
    {
        printf("virtual add:%p\n", _start_mem);
        return _start_mem;
    }

    ~Shm()
    {
        if(_usertype == CREAT) //只有创建者需要销毁
            Destroy();
    }

private:
    key_t _key;
    int _shmid;
    size_t _size;
    void* _start_mem; 
    string _pathname;
    int _projid;
    string _usertype;
};
//comm.hpp文件
#include <stdio.h>
#include <stdlib.h>

#define ERR_EXIT(m) \
do \
{ \
    perror(m); \
    exit(EXIT_FAILURE); \
} while(0)

本次分享就到这里了,我们下篇见~