草庐IT

Linux——详解共享内存shared memory

就要 宅在家 2024-03-13 原文

目录

一.共享内存介绍

(一).什么是共享内存

(二).共享内存优点

(三).共享内存缺点

二.共享内存使用

(一).创建—shmget

①key

②size

③shmflg

④返回值

(二).连接—shmat

(三).分离—shmdt

(四).销毁—shmctl

(五).查看—ipcs

(六).删除—ipcrm

(七).读取与写入 

三.共享内存与访问控制

(一).添加访问控制

(二).可能的陷阱


一.共享内存介绍

(一).什么是共享内存

共享内存本质上就是内存中的一块区域,用于进程间通信使用。该内存空间由操作系统分配与管理。与文件系统类似的是,操作系统在管理共享内存时,不仅仅有内存数据块,同时还会创建相应结构体来记录该共享内存属性,以便于管理。

因此,共享内存不只有一份,可以根据需求申请多个。

进程之间进行通信的时候,会获取 到共享内存的地址,写端进程写入数据,读端进程通过直接访问内存完成数据读取。

(二).共享内存优点

相比于管道而言,共享内存不仅能够用于非父子进程之间的通信,而且访问数据的速度也比管道要快。这得益于通信直接访问内存,而管道则需要先通过操作系统访问文件再获得内存数据。

(三).共享内存缺点

用于进程间通信时,共享内存本身不支持阻塞等待操作。这是因为当读端读取数据后,数据并不会在内存中清空。因此读端和写端可以同时访问内存空间,即全双工。因为共享内存本质是进程直接访问内存,无法主动停止读取,如果读端不加以限制,那么将持续读取数据。同理,写端也会持续写入数据。换句话说,共享内存本身没有访问控制。 

二.共享内存使用


(一).创建—shmget

想要使用共享内存首先要建立共享内存。

①key

shmget会根据key值创建一个共享内存,因此当创建多个共享内存时,每一个key值要独一无二。

获得key值可以使用库函数ftok专门获取一个独一无二的key_t类型值。

参数pathname为路径,必须是真实存在且可以访问的路径。

参数proj_id是int类型数字,且必须传入非零值。

成功返回key_t值,失败返回-1。

ftok函数内部会根据路径和proj_id通过算法生成一个独一无二的key_t返回值。

多进程通信时,需要通信双方使用同一个key值,因此双方使用的ftok参数应该一致。 

②size

该参数用于确定共享内存大小。

一般而言是4096的整数倍,因为内存的块的大小就是4KB即4096B。因此即便我们需要的空间大小不是块大小的整数倍,操作系统实际上也还是分配块的倍数个。但在使用时,那些超过size大小的多余分配空间不能访问。  

③shmflg

 该参数用于确定共享内存属性。

使用上为:标志位 | 内存权限

标志位参数有两种:IPC_CREAT、IPC_EXCL

常用使用方式有两种:

方式含义
shmget(..., IPC_CREAT | 权限)创建失败不报错返回已有shmid
shmget(..., IPC_CREAT | IPC_EXCL | 权限)创建失败报错返回-1

值得注意PC_EXCL无法单独使用。

通常情况下在多进程通信时,创建方使用IPC_CREAT | IPC_EXCL,接收方使用0即可。

④返回值

返回值为int类型,称为shmid。每一个共享内存都会有一个shmid,用于连接与分离时传递参数。 

(二).连接—shmat

创建共享内存后还不能直接使用,需要找到内存地址后才能使用,即连接。 

 shmid即shmget返回值。

shmaddr用于确定将共享内存挂在进程虚拟地址哪个位置,一般填nullptr即可代表让内核自己确定位置。

shmflg用于确定挂接方式,一般填0

连接成功返回共享内存在进程中的起始地址,失败返回-1。 

(三).分离—shmdt

当使用完毕后,需要分离挂接的共享内存。

 shmaddr与shmat的相同,为共享内存在进程中地址位置,一般填nullptr。

分离成功返回0,失败返回-1。 

(四).销毁—shmctl

该接口本身用于控制共享内存,可用于销毁。 

shmid不再介绍,cmd传入IPC_RMID,buf传nullptr。 

成功返回0,失败返回-1。 

(五).查看—ipcs

该指令为系统指令。

使用时可以查看当前全部共享内存。

ipcs -m 

(六).删除—ipcrm

通过指定共享内存shmid,进行删除。

ipcrm -m [shmid] 

 

(七).读取与写入 

调用shmat后会返回一个地址,读端直接读取该地址数据,写端直接向该地址写入即可。

//读端, 将共享内存数据读取到文件,此处为显示器文件
char* p = (char*)shmat(...);
write(1, p, sizeof p);
//写端,将文件中数据写入共享内存,此处为键盘文件
char* p = (char*)shmat(...);
read(0, p, 4096);

 

三.共享内存与访问控制

(一).添加访问控制

通过博客第一部分我们知道,共享内存不支持访问控制,那么我们可不可以添加访问控制给共享内存呢——完全可以。

方式是借用命名管道的访问控制,即阻塞。 

首先我们有如下代码,该代码是读端一直读取写端数据,直到写端输入quit为止。

//写端
int main()
{
  key_t key = ftok(".", 131);
  int shmid = shmget(key, 4096, IPC_CREAT|0660);//获取shmid
  char* p = (char*)shmat(shmid, nullptr, 0);//连接
  while(1){
    ssize_t s = read(0, p, 4096);//写入shm
    p[s - 1] = 0;
    assert(s > 0);
    (void)s;
  }
  shmdt(p);//分离
  return 0;
}
//读端
int main()
{
  key_t key = ftok(".", 131);
  int shmid = shmget(key, 4096, IPC_CREAT|IPC_EXCL|0660);//创建
  char* p = (char*)shmat(shmid, nullptr, 0);//连接
  while(1){
    assert(p != nullptr);
    if(strcmp(p, "quit") == 0)break;
    printf("%s\n", p);//读取shm中数据
    sleep(1);
  }
  shmdt(p);//分离
  shmctl(shmid, IPC_RMID, nullptr);//销毁
  return 0;
}

 但是因为共享内存无法访问控制,读端会一直读取数据,即便我们添加sleep函数也不能从根本解决问题。

解决方式是,在读端和写端分别加上管道的读端和写端。因为我们知道管道读端在读取到来自写端的数据前会阻塞,因此,将管道读端放在共享内存读端之前,将管道写端放在共享内存写端之后。

这样一来,当shm写端写入数据后会触发管道写端写数据,当管道写端写入数据后,管道读端才会停止阻塞,进而执行shm读端。

图例如下:


代码如下: 

//写端
int main()
{
  key_t key = ftok(".", 131);
  int shmid = shmget(key, 4096, IPC_CREAT|0660);//获取shmid
  char* p = (char*)shmat(shmid, nullptr, 0);//连接
  int fd = open(..., O_WRONLY);//打开命名管道
  while(1){
    ssize_t s = read(0, p, 4096);//写入shm
    p[s - 1] = 0;
    assert(s > 0);
    (void)s;
    char i[4] = { 0 };
    write(fd, i, sizeof i);//写入管道
  }
  shmdt(p);//分离
  close(fd);
  return 0;
}
//读端
int main()
{
  int i = mkfifo(PATH_FIFO, 0660);//创建管道
  assert(i >= 0);
  key_t key = ftok(".", 131);
  int shmid = shmget(key, 4096, IPC_CREAT|IPC_EXCL|0660);//创建
  char* p = (char*)shmat(shmid, nullptr, 0);//连接shm
  int fd = open(..., O_RDONLY);//连接管道
  while(1){
    char buf[4];
    read(fd, buf, sizeof buf);//管道等待读取,阻塞
    assert(p != nullptr);
    if(strcmp(p, "quit") == 0)break;
    printf("%s\n", p);//读取shm中数据
    sleep(1);
  }
  shmdt(p);//分离
  shmctl(shmid, IPC_RMID, nullptr);//销毁
  close(fd);
  return 0;
}

(二).可能的陷阱

在添加访问控制时,会有一个可能的陷阱,就是命名管道可不可以在创建shm之前打开(open)呢?

不可以,因为打开管道要求读端和写端同时打开才能继续,否则就会阻塞。

如果阻塞的是写端还好,当读端创建完shm后写端创建失败返回shmid,但是如果阻塞的是读端,那么写端创建shm后,读端创建时因为加上IPC_EXCL的缘故,失败返回-1,之后shmat也失败返回nullptr,进而读端获取到的地址是空。

简单模块注意封装,复杂模块注意分层——未名


如有错误,敬请斧正

有关Linux——详解共享内存shared memory的更多相关文章

  1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  2. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  3. ruby-on-rails - Ruby 中的内存模型 - 2

    ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序

  4. 键删除后 ruby​​ 哈希内存泄漏 - 2

    你好,我无法成功如何在散列中删除key后释放内存。当我从哈希中删除键时,内存不会释放,也不会在手动调用GC.start后释放。当从Hash中删除键并且这些对象在某处泄漏时,这是预期的行为还是GC不释放内存?如何在Ruby中删除Hash中的键并在内存中取消分配它?例子:irb(main):001:0>`ps-orss=-p#{Process.pid}`.to_i=>4748irb(main):002:0>a={}=>{}irb(main):003:0>1000000.times{|i|a[i]="test#{i}"}=>1000000irb(main):004:0>`ps-orss=-p

  5. ruby - 在模块/类之间共享全局记录器 - 2

    在许多ruby​​类之间共享记录器实例的最佳(正确)方法是什么?现在我只是将记录器创建为全局$logger=Logger.new变量,但我觉得有更好的方法可以在不使用全局变量的情况下执行此操作。如果我有以下内容:moduleFooclassAclassBclassC...classZend在所有类之间共享记录器实例的最佳方式是什么?我是以某种方式在Foo模块中声明/创建记录器还是只是使用全局$logger没问题? 最佳答案 在模块中添加常量:moduleFooLogger=Logger.newclassAclassBclassC..

  6. ruby-on-rails - HTTParty 的内存问题和下载大文件 - 2

    这会导致Ruby出现内存问题吗?我知道如果大小超过10KB,Open-URI会写入TempFile。但是HTTParty会在写入TempFile之前尝试将整个PDF保存到内存吗?src=Tempfile.new("file.pdf")src.binmodesrc.writeHTTParty.get("large_file.pdf").parsed_response 最佳答案 您可以使用Net::HTTP。参见thedocumentation(特别是标题为“流媒体响应机构”的部分)。这是文档中的示例:uri=URI('http://e

  7. ruby - 如何使用 cucumber 在场景之间共享状态 - 2

    我有一个功能“从外部网站导入文章”。在我的第一个场景中,我测试从外部网站导入链接列表。Feature:ImportingarticlesfromexternalwebsiteScenario:Searchingarticlesonexample.comandreturnthelinksGiventhereisanImporterAnditsURLis"http://example.com"Whenwesearchfor"demo"ThentheImportershouldreturn25linksAndoneofthelinksshouldbe"http://example.com/d

  8. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  9. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  10. 【Linux操作系统】——网络配置与SSH远程 - 2

    Linux操作系统——网络配置与SSH远程安装完VMware与系统后,需要进行网络配置。第一个目标为进行SSH连接,可以从本机到VMware进行文件传送,首先需要进行网络配置。1.下载远程软件首先需要先下载安装一款远程软件:FinalShell或者xhell7FinalShellxhell7FinalShell下载:Windows下载http://www.hostbuf.com/downloads/finalshell_install.exemacOS下载http://www.hostbuf.com/downloads/finalshell_install.pkg2.配置CentOS网络安装好

随机推荐