【Linux】「共享内存揭秘」:高效进程通信的终极指南

举报
Yui_ 发表于 2024/12/06 17:05:56 2024/12/06
【摘要】 本文深入剖析了共享内存的实现机制、关键API的使用以及实际应用中的注意事项,并对比了其与其他IPC方式的优劣。在理解其高性能的同时,也要认识到同步与安全的挑战

本文所讲的共享内存为System V共享内存

1. 什么是共享内存

共享内存(Shared Memory)是一种进程间通信(IPC,Inter-Process Communication)的方式,允许多个进程通过访问同一块内存区域来实现数据共享和快速通信。它是一种效率极高的通信机制,因为数据不需要在进程间进行复制,只需在同一块内存中直接读写即可。

1.2 共享内存的特点

  1. 高效:数据在高效内存区域是直接共享的,不需要在进程之间进行复制,从而减少了CPU和IO的消耗。
  2. 全局性:共享内存是所有附加到该内存的进程都可以访问的,由此它是一种全局资源。
  3. 同步机制依赖:虽然共享内存提供了数据共享的功能,但是不会自动提供对数据的访问同步机制。需要结合其他IPC方法(如信号量、互斥锁等)来避免多个进程同时读写时产生的数据竞争。

1.3 共享内存的工作原理

  1. 操作系统内核会在物理内存中分配一个共享内存段。
  2. 各个进程通过特定的标识符(shm_id)访问同一块共享内存空间。
  3. 共享内存区域是进程的地址空间外的内存,进程需要将其映射到自己的地址空间中才能访问。

还记得在进程地址空间时的内容吗?
共享内存的工作原理可以理解为:操作系统在内存中开辟了一块共享内存段,让两个不同的进程的虚拟地址同时对这块空间建立起映射关系,此时两个独立的进程能看到同一块空间,可以直接对此空间进行写入或者读取操作。
image.png

2.在Linux中使用共享内存

虽然Linux提供了POSIXSystem V两种共享内存接口。但是本文将聚焦于System V

2.1 介绍System V

System V(读作“System Five”)是 UNIX 操作系统家族的一个版本,由美国 AT&T 的贝尔实验室开发。它是早期 UNIX 的一个重要分支,并对后来的 UNIX 系统以及其他现代操作系统产生了深远影响。
虽然 System V 本身已经很少被直接使用,但它的思想和功能在现代操作系统中得到了传承。
比如:POSIX:吸收了 System V 的许多特性,成为跨平台的通用标准。

2.1.1 创建共享内存

创建共享内存时,需要使用到shmget函数
image.png

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

返回值:创建成功返回共享内存的shmid,失败返回-1
参数介绍

  • key:共享内存的标识符。
  • size:共享内存的大小,一般为4096的整数倍。
  • shmflg:设置共享内存的创建方式以及创建权限。
    关于返回值
    在OS中,共享内存也拥有自己的数据结构,所以返回值有点类似于文件系统中的fd,用于对不同的共享内存块进行操作。
    关于参数2
    可能有读者会感到疑惑,为什么共享内存的大小是4096字节的整数倍,这是因为大小刚好与PAGE页大小相同,有利与提高IO效率。但是你可以设置不是4096大小的内存,不过OS在底层依然会分配向上取整的4096整数倍的空间给共享内存,可是你只能使用你设置的空间大小
    关于参数3
    该参数为位图操作,也是就是状态压缩。与open函数的参数2类似,常用的选项有:
  • IPC_CREAT创建共享内存,如果存在则使用已经存在的共享内存。
  • IPC_EXCL避免使用已经存在的共享内存,单独使用没有意义,需要配合IPC_CREAT使用,当使用已经创建的共享内存时,会创建失败。
  • 权限共享内存也是文件,需要权限掩码如0666
    关于参数1
    key是共享内存的标识符,是让不同进程看到同一块空间的关键,虽然可以自己指定一个数字,但是OS提供了一个根据目标路径+项目编号生成独一无二数字的特殊算法有点类似于哈希。key_t类型也就是对int类型的封装,来表示一个数字,用来标识不同的共享内存块,可以理解为inode

2.1.1.1 key值的获取

OS提供了函数ftok来生成key
image.png

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char* pathname,int proj_id);

返回值:

  • 返回生成的标识符。
    参数:
  • pathname:项目路径,绝对/相对都可以。
  • proj_id:项目编号,自定义。
    key是打开共享内存的钥匙,让两个进程看到同一块空间的关键就在于key
    下面我们写一段代码来看看吧,依然是吧代码分为3个部分。
    公共部分common.hpp,服务部分server.cc,客户部分client.cc

common.hpp

/**
 * 公共文件,用于key值获取,进制转化,创建共享内存,获取共享内存
 */
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <unistd.h>
#include <string>
#include <cstring>

#define PATH "./"
#define PROJID 0x333
#define SIZE 4096
const mode_t mode = 0666;

//机制转化,转16机制
std::string toHEX(int x)
{
    /**
     * parame: x:一个十进制整数
     * return: string:16进制字符串
     * @return
     */
    char buff[128];
    snprintf(buff,sizeof(buff),"0x%x",x);
    return buff;
}

//获取key
key_t getKey()
{
    /**
     * return : ket_t :返回key值
     * @return 
     */
    key_t key = ftok(PATH,PROJID);
    assert(key!=-1);//失败就终止进程   
    return key;
}

//创建共享内存
int createShm(key_t key,size_t size)
{
    /**
     * parame: key 标识符
     * param: size 创建size大小的共享内存
     * return: 返回共享内存id
     * @return
     */
    int id = shmget(key,size,IPC_CREAT|IPC_EXCL|mode);
    assert(id!=-1);
    return id;
}

//获取共享内存
int getShm(key_t key,size_t size)
{
    /**
     * parame: key 标识符
     * param: size 创建size大小的共享内存
     * return: 返回共享内存id
     * @return
     */
    int id = shmget(key,size,IPC_CREAT);
    assert(id!=-1);
    return id;
}

server.cc

/**
 * 服务端,用于接受客户端发来的信息
 */

#include "common.hpp"

int main()
{
    //创建共享内存
    key_t key = getKey();
    int shmid = getShm(key,SIZE);
    std::cout<<"server key:"<<toHEX(key)<<std::endl;
    std::cout<<"server shmid:"<<shmid<<std::endl;
    return 0;
}

client.cc

/**
 * 客户端,用于向服务端发送信息
 */

#include "common.hpp"

int main()
{
    //创建共享内存
    key_t key = getKey();
    int shmid = createShm(key,SIZE);
    std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
    std::cout<<"cilent shmid:"<<shmid<<std::endl;
    return 0;
}

下面是运行结果:
image.png

在shell命令行,我们可以通过指令ipcs -m来查看创建出来的共享内存
image.png

该共享内存表的标识依次为:

key值、shmid、拥有者、权限、大小、挂载数、状态

2.1.2 通过指令回收共享内存

无论我们使用那种通信方式,都要记得在使用结束后将资源释放。
对于共享内存的释放,我们可以在shell的命令行释放。使用指令ipcrm -m shmid
image.png

当然,在命令行里释放还是太让人不舒适了,我们可以直接在程序中控制共享内存的释放。

2.1.3 通过共享内存控制函数释放

shmctl 是一个用于操作和管理 共享内存段 的 System V IPC 函数。它提供了对共享内存段的多种控制功能,比如删除共享内存段、获取信息、修改权限等。

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

参数:

  • shmid:共享内存标识符。
  • cmd:控制操作的命令,有以下值:
    • IPC_STAT
    • IPC_SET
    • IPC_RMID:标识共享内存段为删除状态,等待所有关联进程分离后释放。
  • buf:指向struct shmid_ds的指针,用于传递或者接收共享内存段的元段数据信息。
    • IPC_RMID模式下:buf可设置为nullptr.
      返回:
      成功返回0,失败返回-1。
      修改以下代码

client.cc

/**
 * 客户端,用于向服务端发送信息
 */

#include "common.hpp"

int main()
{
    //创建共享内存
    key_t key = getKey();
    int shmid = createShm(key,SIZE);
    std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
    std::cout<<"cilent shmid:"<<shmid<<std::endl;
    //释放共享内存
    int cnt = 5;
    while(cnt) 
    {
        std::cout<<cnt<<std::endl;
        sleep(1);
        cnt--;
    }
    shmctl(shmid,IPC_RMID,nullptr);
    return 0;
}

image.png

可以看到5秒后,共享内存被释放了。

2.1.4 进程关联共享内存

共享内存的开辟就是为给不同的进程同一块空间的,那么我们要怎么给不同的进程看到同一块空间呢?就需要用到进程关联函数shmat.

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

参数:

  • shmid:共享内存的标识符。
  • shmaddr:用于指定共享内存要附加的地址,如果提供nullptr,操作系统会自动选择一个合适的地址。
  • shmflg:附加时的默认标志:
    • 0:默认行为,读写属性。
    • SHM_RDONLY:只读模式
      返回值:
      成功返回共享内存的首地址,失败返回(void*)-1
      关于参数2
      默认情况我们就传nullptr,让OS自己去找。
      关于参数3
      默认情况也直接传0,默认为读写属性。
      关于返回值
      一般情况下,我们的通信都是通过字符来进行通信的,所有我们可以将其强转为char*.
      然后我们再来改一改,client.cc和server.cc的代码

client.cc

/**
 * 客户端,用于向服务端发送信息
 */

#include "common.hpp"

int main()
{
    //创建共享内存
    key_t key = getKey();
    int shmid = createShm(key,SIZE);
    std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
    std::cout<<"cilent shmid:"<<shmid<<std::endl;
    //开始进行进程关联
    char* start = (char*)shmat(shmid,nullptr,0);
    if((void*)start == (void*)-1)
    {
        perror("shmat");
        shmctl(shmid,IPC_RMID,nullptr);
        exit(1);
    }
    //成功
    printf("client start:%p\n",start);//打印地址看看
    //释放共享内存
    sleep(5);
    shmctl(shmid,IPC_RMID,nullptr);
    return 0;
}

server.cc

/**
 * 服务端,用于接受客户端发来的信息
 */

#include "common.hpp"

int main()
{
    //创建共享内存
    key_t key = getKey();
    int shmid = getShm(key,SIZE);
    std::cout<<"server key:"<<toHEX(key)<<std::endl;
    std::cout<<"server shmid:"<<shmid<<std::endl;
    //开始关联
    char* start = (char*)shmat(shmid,nullptr,0);
    if((void*)start==(void*)-1)
    {
        perror("shmat");
        exit(1);
    }
    //成功
    printf("server start:%p\n",start);
    sleep(3);
    return 0;
}

image.png

从这个图中我们可以发现,关联数的变化。
最重要的是,我们的地址居然是不一样的,这也就更加印证来虚拟地址的存在,两个进程通过页表映射指向同一块空间。
程序结束后,会自动取消关联状态
当然我们也可以手动去关联。

2.1.5 进程去关联

进程去关联需要用到函数shmdt.
shmdt 是用于分离共享内存段的函数,它将之前通过 shmat 附加到进程地址空间的共享内存段移除。调用此函数后,进程将无法通过原地址访问该共享内存段。

int shmdt(const void *shmaddr);

参数:

  • shmaddr:共享内存段的首地址,必须是之前通过shmat返回的地址。
    返回值
    成功返回1,失败返回-1
    注意:共享内存被删除后,已成功挂接的进程仍然可以继续进行正常的通信,不过此时无法再关联其他进程。

2.1.6 再讲shmctl

在上文,笔者已经使用了它的释放共享内存的功能了。除此之外,它还具有其他的功能。
当我们给参数2传递以下的参数时:

  • IPC_STAT:获取或设置控制共享内存的数据结构。
  • IPC_SET:在进程有足够权限的情况下,将共享内存的当前关联值设置为buf数据结构中的值。
    buf就是共享内存的数据结构,可以使用IPC_STAT获取,也可以使用IPC_SET设置。
    除去释放共享内存的IPC_RMID不需要传递参数3,这两种情况都是需要传递参数3的。

通过shmctl获取共享内存的数据结构,并获取pidkey.

/**
 * 客户端,用于向服务端发送信息
 */

#include "common.hpp"

int main()
{
    //创建共享内存
    key_t key = getKey();
    int shmid = createShm(key,SIZE);
    std::cout<<"cilent key:"<<toHEX(key)<<std::endl;
    std::cout<<"cilent shmid:"<<shmid<<std::endl;
    //开始进行进程关联
    char* start = (char*)shmat(shmid,nullptr,0);
    if((void*)start == (void*)-1)
    {
        perror("shmat");
        shmctl(shmid,IPC_RMID,nullptr);
        exit(1);
    }
    //成功
    printf("client start:%p\n",start);//打印地址看看
    std::cout<<"============="<<std::endl;
    struct shmid_ds buf;
    int n = shmctl(shmid,IPC_STAT,&buf);
    if(n == -1)
    {
        perror("shmctl");
        shmctl(shmid,IPC_RMID,nullptr);
        exit(1);
    }
    std::cout<<"buf.shm_cpid:"<<buf.shm_cpid<<std::endl;
    std::cout<<"buf.shm_perm.__key:"<<toHEX(buf.shm_perm.__key)<<std::endl;
    //释放共享内存
    shmdt(start);//去关联
    shmctl(shmid,IPC_RMID,nullptr);
    return 0;
}

image.png

共享内存 = 共享内存的内核数据结构(struct shmid_ds)+真正开辟的空间

2.2 共享内存的简单使用

创建、关联共享内存
服务端向客户端写入数据
客户端端每个一秒读取一次。

common.cc


#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <unistd.h>
#include <string>
#include <cstring>

#define PATH "./"
#define PROJID 0x333
#define SIZE 4096

class Shm{
private:
    key_t key;
    int shmid;
    mode_t mode = 0666;
public:
    Shm(){
        key = getKey();
    }
    key_t getKey()
    {
        /**
         * function:返回并设置key值
         * return: 返回key值
         * @return 
         */
        key_t k = ftok(PATH,PROJID);
        assert(k!=-1);
        return key = k;
    }
    int createShm()
    {
        /**
         * function:创建共享内存
         * return:返回共享内存的标识符
         * @return
         */
        int sid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|mode);
        assert(sid!=-1);
        return shmid = sid;
    }
    int getShm()
    {
        /**
         * function:获取共享内存
         * return: 返回共享内存的标识符
         * @return
         */
        int sid = shmget(key,SIZE,IPC_CREAT);
        assert(sid!=-1);
        return shmid = sid;
    }
    char* processAssociation()
    {
        /**
         * function:进行进程于共享内存间的关联
         * return:返回共享内存的是起始地址
         * @return
         */
        char* start = (char*)shmat(shmid,nullptr,0);
        assert((void*)start!=(void*)-1);
        return start;
    }
    void deAssociation(void* start)
    {
        /**
         * function:进行去关联
         * :parame start 共享内存的起始地址
         */
        shmdt(start);
    }
    void release()
    {
        /**
         * function:释放共享内存
         */
        int n = shmctl(shmid,IPC_RMID,nullptr);
        assert(n!=-1);
    }
    int getShmid()
    {
        /**
         * @return 
         */
        return shmid;
    }
};

server.cc

/**
 * 向cilent端发送信息
 */
#include "common.hpp"

int main()
{
    Shm shm;
    shm.createShm();//创建
    char* start = shm.processAssociation();//关联
    printf("server:");
    for(int i = 0;i<26;++i)
    {
        start[i] = ('a'+i);
        printf("%c",start[i]);
        fflush(stdout);
        start[i+1] = 0;
        sleep(1);
    }
    shm.deAssociation(start);
    shm.release();
    return 0;
}

cilent.cc

/**
 * 接受server的消息
 */
#include "common.hpp"

int main()
{
    Shm shm;
    shm.getShm();
    char* start = shm.processAssociation();
    for(int i = 0;i<26;++i)
    {
        printf("cilent :%s\n",start);
        sleep(1);
    }
    shm.deAssociation(start);
    shm.release();
    return 0;
}

运行结果:
image.png

3. 总结

什么共享内存比管道快。
共享内存快的原因就在于比管道少了两次IO(输入输出)操作。IO是很慢的,怎么证明呢?

#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <stdio.h>
int cnt = 0;
void alarm_handler(int signum) {
    printf("cnt: %d\n", cnt);
    exit(1);
}
int main() {
    signal(SIGALRM, alarm_handler); // 注册信号处理器
    alarm(1); // 设置一个1秒的闹钟
    while(true)
    {
        printf("%d\n",cnt++);
    }
    return 0;
}

image.png

频繁的IO操作,导致cnt最终才累加到135073。
现在我们换个写法

#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <stdio.h>
int cnt = 0;
void alarm_handler(int signum) {
    printf("cnt: %d\n", cnt);
    exit(1);
}
int main() {
    signal(SIGALRM, alarm_handler); // 注册信号处理器
    alarm(1); // 设置一个1秒的闹钟
    while(true)
    {
        //printf("%d\n",cnt++);
        cnt++;
    }    
    return 0;
}

image.png

效率差距非常之大。
管道:
image.png

共享内存:
image.png

共享内存的缺点

多个进程无限制地访问同一块区域,导致共享内存的数据无法确保安全。
共享内存没有同步和互斥机制,某个进程可能数据还没有写完,就被别人读走了,或者被覆盖了。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。