个人技术分享

目录

System V

System V 共享内存

含义

原理

系统接口

代码示例

Comm.hpp

Server.cc(服务端)

Client(客户端)

 结果演示

System V

System V 是一个 UNIX 操作系统的版本,它是 Unix 第 5 版(Version 5)的系统的后续版本。System V 被认为是商业化 Unix 的早期标准之一,由 AT&T(美国电话电报公司)开发,并于 1983 年发布。它在很大程度上影响了后来的 Unix 系统的发展,并成为了许多商业 Unix 变体的基础。

System V 共享内存

含义

System V 共享内存是一种在 UNIX 系统中实现进程间通信(IPC)的方法,它来源于 AT&T Unix System V 操作系统。

这种共享内存机制允许多个进程共享同一块物理内存,从而实现高效的数据交换。与其他 IPC 机制(如管道、消息队列等)相比,共享内存通常更快速,因为它直接操作物理内存而不需要进行数据拷贝。

原理

我们将左边看作服务端,右边看作客户端。首先服务器进程调用shmget()系统调用来请求内核分配一块物理内存,并返回一个唯一的共享内存标识符,然后其他进程就可以通过共享内存标识符来连接到共享内存。

然后连接共享内存时,需要先调用shmat()系统调用,内核会在进程的地址空间中分配一块虚拟地址空间,然后将共享内存中的数据复制到这个虚拟地址空间中。

然后内核会更新进程的页表,将共享内存的物理地址通过页表映射到这个虚拟地址空间的对应位置,这样,当进程访问这些虚拟地址时,CPU会通过页表找到对应的物理地址,从而实现对共享内存的访问。

最后shmat会返回一个指向共享内存区域的void类型的指针,通过这个指针,就可以对共享内存区进行读、写或执行。

OS一定会允许系统中存在多个共享内存块。这些共享内存也要被操作系统管理,系统会在物理内存中创建一个数据结构(比如结构体),用来维护共享内存,它记录着共享内存快的相关信息,比如大小、访问权限、状态等,所以对共享内存的管理就变成了对数据结构的增删查改。

系统接口

一般来说,创建或获取共享内存的步骤如下:

  1. 使用 shmget 函数创建或获取共享内存段,并获得共享内存的标识符。

  2. 使用 shmat 函数将共享内存段连接到进程的地址空间下·中。

  3. 进程可以通过访问共享内存的地址来读写共享内存中的数据。

  4. 当不再需要共享内存时,使用 shmdt 函数将共享内存从进程的地址空间中分离。

  5. 如果不再需要共享内存段,可以使用 shmctl 函数删除它。


#include<sys/ipc.h>
#include<sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);

作用:允许进程获取对现有共享内存段的访问权限,或在需要时创建新的共享内存段。

参数:

  • key:共享内存的键值,用于唯一标识共享内存段。通常是一个整数或者由 ftok 函数生成的键值。

  • size:共享内存段的大小,以字节为单位。

  • shmflg:标志参数,指定共享内存的访问权限和创建方式。比如可以使用 IPC_CREAT 标志来创建新的共享内存段,使用 0666 权限位来设置访问权限等。

返回值: 

shmget 函数的返回值是一个共享内存的标识符(也称为共享内存段的描述符),用于后续的操作,比如连接到共享内存段或删除共享内存段等。


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

作用:用于将共享内存段连接到调用进程的地址空间中,从而使得进程可以访问共享内存中的数据。

参数:

  • shmid:共享内存的标识符,即之前使用 shmget 函数创建或获取的共享内存段的描述符。

  • shmaddr:指定共享内存连接到进程地址空间的地址,通常为 NULL,表示让操作系统自动选择合适的地址。

  • shmflg:标志参数,用于指定连接方式,通常为 0。

返回值:  

shmat 函数返回一个指针,指向共享内存连接后在进程地址空间中的起始地址。通过这个指针,进程可以直接访问共享内存中的数据。

需要注意的是,连接共享内存后,进程需要谨慎管理共享内存的使用,确保在不再需要时及时分离。使用完共享内存后,应该使用 shmdt 函数将共享内存从进程的地址空间中分离,以释放系统资源。


int shmdt(const void *shmaddr);

作用:用于将共享内存从调用进程的地址空间中分离,即断开与共享内存的连接。

参数:

  • shmaddr:共享内存连接后在进程地址空间中的起始地址,即之前使用 shmat 函数返回的指针。

返回值:  

shmdt 函数没有返回值,通常返回 -1 表示失败,或者返回 0 表示成功。

调用 shmdt 函数后,进程将不再能够访问共享内存中的数据。这个函数可以用于释放进程所占用的共享内存资源,从而确保系统资源得到合理利用。


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

作用:用于控制共享内存段的属性。通过 shmctl 函数,可以对共享内存段进行一系列操作,比如获取信息、设置权限、删除共享内存段等。

参数:

  • shmid:共享内存的标识符,即之前使用 shmget 函数创建或获取的共享内存段的描述符。

  • cmd:控制命令,用于指定 shmctl 函数要执行的操作,比如获取信息、设置权限、删除共享内存段等。常用的命令包括 IPC_STAT(获取共享内存段信息)、IPC_SET(设置共享内存段权限)、IPC_RMID(删除共享内存段)等。

  • buf:指向一个 shmid_ds 结构体的指针,用于传递或接收共享内存段的属性信息。

返回值:  

shmctl 函数的返回值通常是一个整数,表示执行成功与否。成功执行时返回 0,否则返回 -1,并设置 errno 来指示错误类型。


代码示例

Comm.hpp

首先是comm.hpp,用来存放服务端和客户端都需要的函数、变量。

由于服务端需要创建共享内存,而客户端只需要获取共享内存标识符,所以将CreatShmHelper抽象出来,让服务端调用CreatShm,客户端调用GetShm。GetKey函数利用ftok()系统调用来生成一个唯一的键,以便在不同的进程共享内存时,可以通过这个键来识别和访问共享内存块。

同时我们创建一个命名管道,名称为"my_fifo",这个管道可以使服务器在读取共享内存中的数据时做到与客户端同步,即客户端写入数据,服务端才会打印。在后面,我们可以使用信号量、互斥锁、信号等来代替管道实现同步。

#pragma once

#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <errno.h>
#include <cstdlib>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>


const std::string pathname = "/home/gpc/pipeline/shm/my_fifo";
const int proj_id = 0x11223344;

// 共享内存的大小,强烈建议设置为n*4096
const int size = 4096; // 4096*2

key_t getKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if (key < 0)
    {
        std::cerr << "errno:" << errno << ",errstring:" << strerror(errno) << std::endl;
        exit(1);
    }

    return key;
}

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

int CreatShm(key_t key)
{
    return CreatShmHelper(key, IPC_CREAT | IPC_EXCL | 0644);
}

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

bool Makefifo()
{
    int n = mkfifo(pathname.c_str(), 0666);
    if (n < 0)
    {
        std::cerr << "errno:" << errno << ",errstring:" << strerror(errno) << std::endl;
        return false;
    }
    std::cout << "mkfifo success .....read" << std::endl;
    return true;
}

Server.cc(服务端)

 在服务端,定义Init类,构造函数中直接完成命名管道的创建、共享内存的申请以及服务端连接到共享内存的操作,析构函数中将共享内存与进程的地址空间分离,然后删除共享空间,并关闭命名管道。

#include "comm.hpp"

class Init
{
public:
    Init()
    {
        bool r = Makefifo();
        if (!r)
            return;

        shmid = CreatShm(key);
        std::cout << "shmid:" << shmid << std::endl;

        sleep(1);
        std::cout << "开始将shm映射到进程的地址空间中" << std::endl;

        s = (char *)shmat(shmid, nullptr, 0);
        fd = open(pathname.c_str(), O_RDONLY);
    }
    ~Init()
    {
        shmdt(s);
        std::cout << "开始将shm从进程的地址空间中移除" << std::endl;

        sleep(1);
    
        std::cout << "开始将shm从操作系统中删除" << std::endl;
        shmctl(shmid, IPC_RMID, nullptr);
       
        close(fd);
    }

public:
    int shmid;
    int fd;
    char *s;
};
int main()
{
    Init init;
    while (true)
    {
        int code = 0;
        ssize_t n = read(init.fd, &code, sizeof(code));
        if (n > 0)
        {
            // 直接读取
            std::cout << "共享内存的内容是:" << init.s << std::endl;
            sleep(1);
        }
        else if (n == 0)
        {
            break;
        }
    }
    return 0;
}
Client(客户端)

在客户端中,我们需要获得跟服务端相同的键,通过这个键,我们可以利用GetShm函数获取跟这个键值对应的共享内存标识符,然后向共享内存写入,每次写入完毕,向管道中随便写入一个code,用来通知服务端:写入完毕,请读取数据。

#include "comm.hpp"

int main()
{
    key_t key = getKey();
    int shmid = GetShm(key);
    char *s = (char *)shmat(shmid, nullptr, 0);
    std::cout << "attach shm done" << std::endl;
    int fd = open(pathname.c_str(), O_WRONLY);

    sleep(10);
    // TODO
    // 共享内存的通信方式,不会提供同步机制,共享内存是直接裸露给所有使用者的,一定要注意安全问题
    char c = 'a';
    for (; c <= 'z'; c++)
    {
        s[c - 'a'] = c;
        std::cout << "write:" << c << " done " << std::endl;
        sleep(1);

        // 通知对方
        int code = 1;
        write(fd, &code, sizeof(4));
    }

    shmdt(s);
    std::cout << "detach shm done" << std::endl;
    
    close(fd);
    return 0;
}
 结果演示

共享内存效果视频