【Linux篇章】进程通信黑科技:System V 共享内存,开启进程间通信的星际数据通道!

在计算机系统里,进程就像一个个独立工作的小员工,各自完成自己的任务。但有时候,这些小员工之间需要交流信息、共享数据,这就涉及到进程间通信(IPC)。System V 共享内存就是进程间通信的一种强大方式,下面就带大家了解一下它的奥秘。

一·什么是共享内存:

本质:也是让不同进程看到同一份资源。

1.1共享内存概念及外观:

想象有一个公共的大黑板,不同的进程都可以在这个黑板上写东西和看东西,这个大黑板就类似于 System V 共享内存。它允许不同的进程访问同一块物理内存区域,进程可以直接在这块内存中读写数据,而不需要进行繁琐的数据复制操作,这使得数据的传输速度非常快。(shm -->system shared memory)

那么此时就需要“先描述,后组织”;把这些shm用各自的结构体给组织起来(这里我们可以认为是链表然后有多个进程用它;那么就会存在引用计数(nattach)来明确这块共享内存当没有进程使用的时候就销毁它;后面我们会讲如何操作)。

这里我们的动态库加载也是用的共享内存但是是mmap映射方式罢了(了解即可)。

1.2共享内存数据结构:

这里我们可以用man shmctl 这个指令来查看:

下面我们就来看一下上面说的描述共享内存的结构体吧:

代码语言:javascript代码运行次数:0运行复制
struct shmid_ds {
               struct ipc_perm shm_perm;    /* Ownership and permissions */
               size_t          shm_segsz;   /* Size of segment (bytes) */
               time_t          shm_atime;   /* Last attach time */
               time_t          shm_dtime;   /* Last detach time */
               time_t          shm_ctime;   /* Creation time/time of last
                                               modification via shmctl() */
               pid_t           shm_cpid;    /* PID of creator */
               pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
               shmatt_t        shm_nattch;  /* No. of current attaches */
               ...
           };
代码语言:javascript代码运行次数:0运行复制
struct ipc_perm {
               key_t          __key;    /* Key supplied to shmget(2) */
               uid_t          uid;      /* Effective UID of owner */
               gid_t          gid;      /* Effective GID of owner */
               uid_t          cuid;     /* Effective UID of creator */
               gid_t          cgid;     /* Effective GID of creator */
               unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
               unsigned short __seq;    /* Sequence number */
           };

也就是说当我们创建好共享内存的话;以及然后与想要通信的进程关联就需要填充这些数据等。

二·为什么需要共享内存:

传统的进程间通信方式,比如管道、消息队列等,在数据传输时需要将数据从一个进程的内存复制到内核空间,再从内核空间复制到另一个进程的内存,这个过程比较耗时。而共享内存直接让多个进程访问同一块内存,避免了多次数据复制,大大提高了数据传输的效率。就好比大家都围在一个大黑板前交流,比一个人把信息写在纸条上,再传递给另一个人要快得多。

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

三·共享内存相关使用的指令及函数:

3.1相关指令:

3.1.1查看共享内存:

代码语言:javascript代码运行次数:0运行复制
ipcs -m + shmid(共享内存编号)

3.1.2删除共享内存:

代码语言:javascript代码运行次数:0运行复制
ipcrm -m + shmid(共享内存编号)

下面我们删除shmid为36的这个共享内存:

这样就被销毁了。

这里我们只能用shmid来操控共享内存而不能是key(后面会说);而且进程退出!=shm回收;还是需要手动指令回收或者函数来回收。

3.1.3 删除全部共享内存:

代码语言:javascript代码运行次数:0运行复制
ipcrm -a 

使用 ipcrm -a 可以一次性删除系统中所有类型的 IPC 资源。

不过需要注意的是,执行该命令需要有足够的权限。通常情况下,普通用户只能删除自己创建的 IPC 资源,而要删除系统中所有的 IPC 资源,需要以超级用户(root)的身份执行该命令,所以实际操作中一般会这样使用:

代码语言:javascript代码运行次数:0运行复制
sudo ipcrm -a

ipc指令拓展:

删除所有消息队列:使用 ipcrm -q 选项,命令格式为 sudo ipcrm -q (需 root 权限)。 删除所有共享内存段:使用 ipcrm -m 选项,命令格式为 sudo ipcrm -m (需 root 权限)。 删除所有信号量集:使用 ipcrm -s 选项,命令格式为 sudo ipcrm -s (需 root 权限)。

3.2使用共享内存函数:

3.2.1ftok:

即file to key :

生成特定的key作为shmget创建的共享内存的唯一性标识(无其他作用是标识唯一性)。

代码语言:javascript代码运行次数:0运行复制
 #include <sys/types.h>
 #include <sys/ipc.h>

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

pathname∶这是一个指向现有文件或目录的路径名的指针。该文件或目录必须存在且调用进程对其有访问权限,因为ftok会使用该文件的索引节点号(inode number)来生成键值。

proj_id:这是一个用户指定的项目ID,它是一个非零的8位整数(取值范围是1-255)。ftok会将该值的低8位与pathname对应的文件的索引节点号组合起来生成最终的键值。

我们需要第一个数据需要的是存在的路径(因为会借助inode等来通过一定方式生成key);第二个是一个八位的数字。

成功就返回32位的键值;失败就返回-1。

3.2.2shmget:

即shm get(内存中创建共享内存):

代码语言:javascript代码运行次数:0运行复制
 #include <sys/ipc.h>
 #include <sys/shm.h>

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

key:这个共享内存段名字。 size:共享内存⼤⼩。 shmflg:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的取值为IPC_CREAT: 共享内存不存在,创建并返回;共享内存已存在,获取并返回。 取值为IPC_CREAT | IPC_EXCL: 共享内存不存在,创建并返回;共享内存已存在,出 错返回。 返回值:成功返回⼀个⾮负整数(shmid),即该共享内存段的标识码;失败返回-1。

对于size:以4kb倍数开辟:但是如果传入的不是;它也会开辟4kb倍数;只是给用户的还是他输入的值;其实开的多只是不允许访问 。

当然了shmflg也可以配合权限掩码使用;就是这块共享内存允许谁操作:

3.2.3shmat:

即shm attach(共享内存关联):

代码语言:javascript代码运行次数:0运行复制
#include <sys/types.h>
  #include <sys/shm.h>

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

shmid: 共享内存标识 shmaddr:指定连接的地址(默认传nullptr) shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY(默认0:可读可写) 返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1(void*强转)

对于传入的shmflg是SHM_RDONLY就是只读了(映射到的虚拟内存空间);不过;我们这个参数一般都是默认0。

3.2.4shmdt:

即shm detach(取消关联):

代码语言:javascript代码运行次数:0运行复制
 #include <sys/types.h>
 #include <sys/shm.h>

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

shmaddr: 由shmat所返回的指针返回值:成功返回0;失败返回-1 注意:将共享内存段与当前进程脱离不等于删除共享内存段(还是需要用shmctl或者ipcrm -m来删除的(删除前必须确保已经detach了))

进程直接异常退出等它不会减少nattch;只有手动去shmdt才行否则后序的shmctl的rmid会出问题 。

3.2.5shmctl:

即shm control (控制共享内存:分为删除,获取信息,改变权限):

代码语言:javascript代码运行次数:0运行复制
 #include <sys/ipc.h>
  #include <sys/shm.h>

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

shmid:由shmget返回的共享内存标识码。 cmd:将要采取的动作(有三个可取值) buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构(默认传nullptr;不关心这个输出型参数) 返回值:成功返回0;失败返回-1。

下面我们基于写好的共享内存创建的代码来验证一下(利用上面所展示的共享内存的数据结构的结构体):

IPC_RMID这里就不需要了(每次建立共享内存后就需要删除;我们看后面实现代码效果就好了)

来演示下剩下的两个:

IPC_STAT(status):

首先在我们的shm类中添加成员函数:

下面我们从外部查看一下是否能把这个共享内存的信息放到传入的ds中:

也是显示出来的。

IPC_SET:

这里注意共享内存权限是只能修改:

我们的目的是通过传入更改共享内存的权限:mode。

这里我们默认创建的权限掩码是0666;我们打算把它修改成0006:

运行一下:

也是可以修改的。

四·共享内存优缺点:

4.1优点:

速度快:避免了数据的多次复制,数据传输效率高。

使用方便:进程可以像操作普通内存一样操作共享内存。

4.2缺点:

同步问题:由于多个进程可以同时访问共享内存,可能会出现数据竞争的问题。比如,一个进程正在写入数据,另一个进程同时读取数据,可能会导致数据不一致。这就需要使用其他的同步机制,如信号量,来保证数据的一致性。

管理复杂:需要手动管理共享内存的创建、连接、分离和删除,容易出现内存泄漏等问题。System V 共享内存是一种强大的进程间通信方式,它为进程之间的高效数据共享提供了可能。但在使用时,需要注意同步和管理问题,以确保系统的稳定性和数据的一致性。通过合理使用 System V 共享内存,我们可以让不同的进程更好地协作,共同完成复杂任务。

不同步的例子:

也就是说shm不会阻塞住它是一直读的;读取的内容可能不是我们期望的;比如:我们想让它读一句话;你好;我走了。 然而如果没控制好让它什么时候读它就有可能只读你好;之后才会说我走了;不连贯。

简单来说就是:速度快;不需要等待;写到虚拟内存空间就马上读;但是正是因为这一点;导致读写可能会出现一些错误(不是我们所需的)--->无读写的同步性;只能自己控制;以及管理起来还是比管道要困难;即对共享内存里的数据无保护机制。

那么我们如何用最简单的方法增强这种同步性;让它读取我们需要的内容呢?

那就是我们之前学的命名管道了(传送门:【Linux篇章】进程通信黑科技:匿名管道的暗码传递与命名管道的实名使命-CSDN博客 )

可以使用命名管道:当我们写进程想让它读的时候就从管道写入;当读进程发现管道有数据时候才会读shm内容(但是有很多细节注意先后顺序; sleep时候的时间等;一定控制好让它读完一条再写{而不是读这一条呢;然后shm内又被写端写入了})

五·共享内存重要小tip:

①shm不随进程属于内核;也就是进程终止它仍旧存在;受内核管控必须指令清除或者代码清除。 (这也就是共享内存的特性了)

②与文件相关但又不强相关: shmid fd但是文件fd关了可以重新再开始而shm清除后下一个后从它此时的mid累加。

下面我们演示一下:

我们知道如果是文件:比如创建一个fd就是3的话删除它再传进还是3;但是共享内存的shmid就不一样了;没有这种特性。

此时我们创建了一个共享内存shmid为37;下面我们把它删除再创建一个。

发现变成了38而不是像我们文件一样是37。

③其次和管道不同的是:管道需要掉系统open等接口进行操作;而shm直接映射到进程的虚拟地址空间这样用户就可以直接使用(语言封装的函数等)

对应管道操作;我们不难发现它使用了open;write等系统调用:

但是对应共享内存的话我们直接就用的是封装好的用户层参数直接向着虚拟内存空间操作:

因为我们如果共享内存完成映射后;就到了进程的虚拟内存空间;这不就类似我们以前语言函数操作创建变量的形式了;但是如果是管道的话它是一个特殊文件(我们需要用系统的接口去执行)。

④key在用户层只是区分不同shm的;用户还是用shmid来操作shm。

我们用ftok特殊方式生成的key只是共享内存的唯一标识;方便找到同一块内共享内存以及Os管理共享内存;无其他的用处;使用共享内存给用户层的还是shmid(类似文件的fd)。

⑤这里说一下;如果是进程异常退出导致的共享内存没有删除或者有nattch的时候把它删除(ipcrm或者shmctl)那么之后还是会有个dest标记如:

(ipcrm -m 或者shmctl -->如果有关联它们都会标记dest(待删除状态)【表示这块内存不能再被使用了】) 但是先前关联过的进程还是可以用;只是后面无法再与它关联;当与它关联的都shmdt掉;就会被立即释放。

共享内存底层大致是怎么样实现的呢?

下面我们粗略的说一下:

首先我们调用的是shmget;它会在共享内存的那个结构体有个文件指针;开辟一个struct_file以及对应的缓冲区(只不过连接到了物理内存而不是磁盘);然后我们调用了shmat完成虚拟映射:其实就是把进程中mm结构体里的vm结构体的vm*file指向这个struct_file;此时就相当于把vm中虚拟地址的start和and指向了这个块缓冲区(这里可以理解成完成了我们对应页表的映射;也就是我们只需要用语言封装的函数向虚拟地址指向的位置进行操作;那么它就会自己转化到缓冲区写入然后到物理内存--->其实就是省去了对文件的系统性操作)

六·基于共享内存实现的进程通信:

下面我们基于使用上面的共享内存来完成对进程间交流代码的实现:

6.1无手动输入的通信:

下面我们基于命名管道改进它的非同步性的通信方式的代码(这里我们要求client进行写入aa cc...)然后让server读取;而不是当client写了一个server就读(a aa aaac这样的)。然后等client端写完就给server一个命令告诉它写完了;server就可以关闭命名管道以及回收共享内存了(这里一定要注意的是server和client谁先时候执行代码的时间问题;否则将会出现bug

下面我们运行一下:

下面有一些细节问题就是client和server这两个端怎么设计交流以及命名管道和共享内存如何创建的细节问题(多个初始化及析构顺序问题)请看代码:

define.hpp:

这里因为fifo.hpp以及shm.hpp都会用到这个宏;不让它重复使用直接搞成头文让它只包含一次就好。

代码语言:javascript代码运行次数:0运行复制
#pragma once
#define EXIT(e) do { perror(e);exit(EXIT_FAILURE);}while(0)

fifo.hpp:

代码语言:javascript代码运行次数:0运行复制
#pragma once  
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include<string>
using namespace std; 


class cnpipe{
   public:
    cnpipe(const string & path,const string & name):_path(path),_name(name)
     {   _pfname=_path+"/"+_name;
        umask(0);
        int n=mkfifo(_pfname.c_str(),0666);
          if (n != 0)
        {
          EXIT("mkdir");
           
        }
        
     }
     ~cnpipe(){
        int u=unlink(_pfname.c_str());
        if(u==0) std::cout<<"成功删除管道"<<std::endl;
         else EXIT("unlink");
       
     }
    private:
     
   string _path;
    string _name;
    string _pfname;
};
class oppipe{
   

    public:
    oppipe(const string & path,const string & name):_path(path),_name(name){
        _pfname=_path+"/"+_name;
    }

       void openw(){
        _fd=open(_pfname.c_str(),O_WRONLY);
        std::cout<<"openw success"<<std::endl;//服务端也就是管道读端没有进入则一直堵塞
        if (_fd < 0)
     {
         EXIT("openw");
     }
       }
      void openr(){
        _fd=open(_pfname.c_str(),O_RDONLY);
        std::cout<<"openr success"<<std::endl;//客户端也就是管道写端没有进入则一直堵塞
         if (_fd < 0)
      {
          EXIT("openr");
      }
      }
     void wakeup_server(){
          // while(1){
          //   std::string mess;
          //  std:: getline(std::cin,mess);
          // int n=write(_fd,mess.c_str(),mess.size());
          // if(n<0) EXIT("pwrite");
          // }
         char ch ='a';
         int n=write(_fd,&ch,1);
       
     }
     void exit_server(){
        char ch ='e';
         int n=write(_fd,&ch,1);
     }
     bool wait_client(){
        // while(1){
        //     char buff[1024]={0};
        //    int n= read(_fd,buff,sizeof(buff)-1);
        //  if(n>0) std:: cout<<"客户说:"<<buff<<std::endl;
        //  else if(n==0) {cout<<"client exit"<<endl; break;}
        // else  EXIT("pread");
        // }
         while(1){
         char buff[1024]={0};
           char ret;
        int n= read(_fd,&ret,1);
         if(ret=='a') return 1;
         else if(ret=='e') return 0;
         else{}
         }
         
        
    }
  void  Close(){
    if(_fd>0) close(_fd);
    }

    private:
    string _path;
    string _name;
    string _pfname;
    int _fd;

};

shm.hpp:

代码语言:javascript代码运行次数:0运行复制
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define projid 0x66
#define pathname "."
#define USER "user"
#define CREATER "creater"
using namespace std;


class shm{
public:
     shm(const string &pn, int pd,const string &name,int sz=4097):_name(name),_size(sz)
           {
            _key=ftok(pn.c_str(),pd);
            if(_name==USER) {  get(); }
            else if(_name==CREATER) {  create();}
            else{}
            attach();
           }

         void get(){
            corgshm(IPC_CREAT|0666);
         }

         void create(){
            corgshm(IPC_CREAT|IPC_EXCL|0666);
         }

         int sz(){
            return _size;
         }

         void * address(){
            return _mem_address;
         }
       void  getdata
       (struct shmid_ds *pds){
             shmctl(_shmid,IPC_STAT,pds);
         }
         void modifyshm(struct shmid_ds *pds){
             shmctl(_shmid,IPC_SET,pds);
               
         }
         

    ~shm(){
        int n=shmdt(_mem_address);
        if(n==-1) EXIT("shmdt");
       if(_name==CREATER) {
        destroy();
        cout<<"~shm success"<<endl;
    }
    }

private:

     void attach(){
         _mem_address=shmat(_shmid,nullptr,0);
         if((long long)_mem_address==-1) EXIT("shmat");
         cout<<"shmat success"<<endl;
     }

     void  corgshm(int k){
       int n= shmget(_key,_size,k);
       if(n==-1) EXIT("shmget");
       _shmid=n;
        cout<<"shmget sucess"<<endl;
      }

      void destroy(){
         int n=shmctl(_shmid,IPC_RMID,nullptr);
         if(n==-1) EXIT("shmctl"); 
         cout<<"shmctl success"<<endl;
      }
     int _key;
     int _shmid;
     string _name;
     int _size;
     void * _mem_address;

};

client:

代码语言:javascript代码运行次数:0运行复制
#include"define.hpp"
#include"shm.hpp"
#include"fifo.hpp"

int main(){
    shm sm(pathname,projid,USER);
   oppipe op(".","ff");
   op.openw();
    char*as=(char *)sm.address();
    for(char ch='a';ch<='z';ch+=2){
        sleep(2);
        as[ch-'a']=ch;
        sleep(1);
        as[ch-'a'+1]=ch;
        as[ch+2]=0;
        op.wakeup_server();
    }
    op.exit_server();
    return 0;
    

}

server:

代码语言:javascript代码运行次数:0运行复制
#include"define.hpp"
#include"shm.hpp"
#include"fifo.hpp"
int main(){
    shm sm(pathname,projid,CREATER);
    struct shmid_ds ds;
    //测试IPC_STAT:
    // sm.getdata(&ds);
    // cout<<ds.shm_segsz<<endl;
    // cout<<ds.shm_atime<<endl;
    // 测试IPC_SET(只能修改perm):
    // ds.shm_perm.mode=0006;
    // sm.modifyshm(&ds);
    // sleep(4);




    cnpipe cp(".","ff");
    oppipe op(".","ff");
    op.openr();
    char*as=(char *)sm.address();
    while(1){
        if(op.wait_client()){
            printf("%s\n",as);
        }
        else break;
    }
    op.Close();

    return 0;
}

Makefile:

代码语言:javascript代码运行次数:0运行复制
.PHONY:all
all:client server
client:client
	g++ -o $@ $^ -std=c++11
server:server
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client server

6.2访问控制板的通信:

下面我们只需要修改一下client端就可以了;让它直接访问控制板;我们约定当client端输入exit就代表退出(此时server端收到命令直接销毁管道和共享内存即可)其他代码不变就不展示了。

运行一下:

更改后的client:

代码语言:javascript代码运行次数:0运行复制
#include"define.hpp"
#include"shm.hpp"
#include"fifo.hpp"

int main(){
    shm sm(pathname,projid,USER);
   oppipe op(".","ff");
   op.openw();
    char*as=(char *)sm.address();
    // for(char ch='a';ch<='z';ch+=2){
    //     sleep(2);
    //     as[ch-'a']=ch;
    //     sleep(1);
    //     as[ch-'a'+1]=ch;
    //     as[ch+2]=0;
    //     op.wakeup_server();
    // }
    while(1){
         string s;
         getline(cin,s);
         if(s=="exit") break;
        char buff[4097]={0};
        int i=0;
        for(auto a:s) buff[i++]=a;
      snprintf(as,sm.sz(),"%s\n",buff);
      op.wakeup_server();  
    }
    op.exit_server();
    return 0;
    

}

七.小结:

本次System V 共享内存解说就分享到此;期待我们在下一次Linux篇章再次相遇;感谢阅读。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-04-29,如有侵权请联系 cloudcommunity@tencent 删除通信linuxsystem进程科技