精华内容
下载资源
问答
  • 易语言读写共享内存例程源码,读写共享内存例程,CreateFileMappingA,MapViewOfFile,UnmapViewOfFile,CloseHandle,OpenFileMappingA
  • C语言编写程序,用信号量和共享内存实现读写同步,程序中父进程为读进程,子进程为写进程。开启共享内存空间和二值信号量,子进程占用信号量,父进程无法执行读,等待子进程写完释放信号量后父进程进行读操作。依次...
  • ShareMemLib将共享内存代码封装成lib,定义了发送者和监听者 两个进程在实例化ShareMemory时指定相同的map,并指定当前为发送者或监听者 发送者负责发送消息,监听者负责接受消息(监听者务必实现接受消息处理方法) ...
  • Android调用C++实现共享内存,利用Binder进行跨进程通信。博客地址:https://blog.csdn.net/liny000/article/details/83717727
  • 【需求描述】 1、共享内存保存信息 2、提供接口写入共享内存 3、提供接口获取共享内存 【编写语言:C】 【环境:linux】 1、写入内存一千万条数据 耗时:5.356秒 2、读取内存一千万条数据 耗时:1.449秒
  • C++操作共享内存读写工程、C++操作共享内存读写工程、C++操作共享内存读写工程
  • linux多线程间使用共享内存例程,C++版本。可用于大量数据的多线程共享。
  • 网络上QT的共享内存都是写到一个程序的,我自己写了个 共享程序写图片程序A,共享内存B 按按钮,显示图片的小例子。给大家一点参考。
  • 易语言共享内存检测重复运行模块源码 系统结构:L_重复运行,L_写共享内存,L_读共享内存,检测内存,Z_共享内存_CreateFileMappingA,Z_共享内存_OpenFileMappingA,Z_共享内存_MapViewOfFile,Z_共享内存_C
  • C++共享内存进程间通信 FileMapping用于进程间快速通信
  • 共享内存

    千次阅读 2018-10-09 17:34:43
    共享内存是常用的进程之间的通信,两个进程可以直接共享访问同一块内存区域 一:共享内存的实现步骤如下: (1)创建共享内存区 进程1通过操作系统提供的API从内存中申请一块共享区域,Linux系统中可以通过...

    共享内存是常用的进程之间的通信,两个进程可以直接共享访问同一块内存区域

    一:共享内存的实现步骤如下:

    (1)创建共享内存区

    进程1通过操作系统提供的API从内存中申请一块共享区域,Linux系统中可以通过shmget,函数实现,生成的共享内存块与某个特定的key进行绑定。

    (2)映射共享内存到进程1中

    在Linux环境中,可以通过shmat实现

    (3)映射共享内存到进程2中

    进程2通过进程1的shmat函数和同一个key值,然后执行shmat,将这个内存映射到进程2中。

    (4)进程1和进程2中相互通信

    共享内存实现两个映射后,可以利用该区域进行信息交换,由于没有同步机制,需要参与通信的进程自己协商处理

    (5)撤销内存映射关系

    完成通信之后,需要撤销之前的映射操作,通过shmdt实现

    (6)删除共享内存区

    在Linux中通过shctl函数来实现

    二:相关函数

    shmget函数:

    该函数用来创建共享内存,它的原型为:

     

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

    该函数用来创建共享内存,它的原型为:

    第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存命名

    shmget函数成功时返回一个与key相关的内存标识符(非负整数),用于后续的共享内存函数,调用失败返回-1.不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数直接使用信号量键,所有其他的信号量函数再由semget函数返回的信号量表示符。

    第二个参数,size以字节为单位指定需要共享的内存容量

    第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作,共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

    2、shamt函数

    第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:

     

    void *shamt(int shm_id,const void *shm_addr,int shmflg);

    第一个参数:shm_id是由shmget函数返回的共享内存标识

    第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

    第三个参数,shm_flg是一组标志位,通常为0。

    调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

    3、shmdt函数

    该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

     

    int shmdt(const void *shmaddr);

    参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.

    4、shmctl函数

    与信号量的semctl函数一样,用来控制共享内存,它的原型如下:

     

    int shmctl(int shm_id,int command,struct shmin_ds *buf);

    第一个参数,shm_id是shmget函数返回的共享内存标识符

    第二个参数,command是要采取的操作,它可以取下面的三个值 :

        IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。

        IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值

        IPC_RMID:删除共享内存段

    第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。

    shmid_ds结构至少包括以下成员:

     

    struct shmid_ds
    {
        uid_t shm_perm.uid;
        uid_t shm_perm.gid;
        mode_t shm_perm.mode;
    }

     

    展开全文
  • 之前在网上找的都是只能传递字符串的共享内存,转换成结构体格式经常出现无法读取的问题
  • 进程间通信之共享内存 shared memory ) 1 效率最高 2 存在竞态 七种进程间通信方式: 一 无名管道( pipe ) 二 有名管道( fifo ) 三 共享内存 shared memory 四 信号 sinal 五 消息队列 message queue ) ...
  • 利用共享内存实现进程间的通信,可用于操作系统的教学。(原创)
  • codesys PLC的共享内存方式实现变量通讯,版本为3.5,下位机程序包括C++和C#
  • VC++ 共享内存读写操作

    热门讨论 2014-01-02 13:58:34
    此解决方案含两个工程文件,一个是写操作工程文件,即把任意字符串写入创建的共享内存里,另外一个读操作工程文件,则是读取共享内存里的数据,从而实现了进程之间的共享内存读写操作。
  • QT间进程通信之共享内存

    热门讨论 2013-12-09 15:42:50
    Qt提供了一种安全的共享内存的实现QSharedMemory,以便在多线程和多进程编程中安全的使用。比如说QQ的聊天的客户端,这里有个个性头象,当点击QQ音乐播放器的时候,启动QQ音乐播放(启动一QQ音乐播放器的进程)这时...
  • 本文首先介绍了众所周知的共享内存用户态API,然后介绍了相关的内核主要数据结构,并逐一分析了shmget、shmat、数据访问、shmdt的内核实现及数据结构之间的动态关系,从数据的关联图即可一窥共享内存的实现机制。
  • MFC共享内存例子

    热门讨论 2012-11-21 15:14:00
    使用MFC开发的共享内存测试程序,用于完成内存镜像文件的创建与读写功能,实现不同进程之间进行数据交换
  • 共享单车、共享充电宝、共享雨伞,世间的共享有千万种,而我独爱共享内存。 早期的共享内存,着重于强调把同一片内存,map到多个进程的虚拟地址空间(在相应进程找到一个VMA区域),以便于CPU可以在各个进程访问到...

    共享单车、共享充电宝、共享雨伞,世间的共享有千万种,而我独爱共享内存。

    早期的共享内存,着重于强调把同一片内存,map到多个进程的虚拟地址空间(在相应进程找到一个VMA区域),以便于CPU可以在各个进程访问到这片内存。

    现阶段广泛应用于多媒体、Graphics领域的共享内存方式,某种意义上不再强调映射到进程虚拟地址空间的概念(那无非是为了让CPU访问),而更强调以某种“句柄”的形式,让大家知道某一片视频、图形图像数据的存在并可以借助此“句柄”来跨进程引用这片内存,让视频encoder、decoder、GPU等可以跨进程访问内存。所以不同进程用的加速硬件其实是不同的,他们更在乎的是可以通过一个handle拿到这片内存,而不再特别在乎CPU访问它的虚拟地址(当然仍然可以映射到进程的虚拟地址空间供CPU访问)。

    只要内存的拷贝(memcpy)仍然是一个占据内存带宽、CPU利用率的消耗大户存在,共享内存作为Linux进程间通信、计算机系统里各个不同硬件组件通信的最高效方法,都将持续繁荣。关于内存拷贝会大多程度地占据CPU利用率,这个可以最简单地尝试拷贝1080P,帧率每秒60的电影画面,我保证你的系统的CPU,蛋会疼地不行。

    我早就想系统地写一篇综述Linux里面各种共享内存方式的文章了,但是一直被带娃这个事业牵绊,今日我决定顶着娃娃们的山呼海啸,也要写一篇文章不吐不快。

    共享内存的方式有很多种,目前主流的方式仍然有:

    共享内存的方式

    1.基于传统SYS V的共享内存;

    2.基于POSIX mmap文件映射实现共享内存;

    3.通过memfd_create()和fd跨进程共享实现共享内存;

    4.多媒体、图形领域广泛使用的基于dma-buf的共享内存。

    SYS V共享内存

    历史悠久、年代久远、API怪异,对应内核代码linux/ipc/shm.c,当你编译内核的时候不选择CONFIG_SYSVIPC,则不再具备此能力。

    你在Linux敲ipcs命令看到的share memory就是这种共享内存:

    下面写一个最简单的程序来看共享内存的写端sw.c:

    以及共享内存的读端sr.c:

    编译和准备运行:

    在此之前我们看一下系统的free:

    下面运行sw和sr:

    我们发现sr打印出来的和sw写进去的是一致的。这个时候我们再看下free:

    可以看到used显著增大了(711632 -> 715908), shared显著地增大了(2264 -> 6360),而cached这一列也显著地增大326604->330716。

    我们都知道cached这一列统计的是file-backed的文件的page cache的大小。理论上,共享内存属于匿名页,但是由于这里面有个非常特殊的tmpfs(/dev/shm指向/run/shm,/run/shm则mount为tmpfs):

    所以可以看出tmpfs的东西其实真的是有点含混:我们可以理解它为file-backed的匿名页(anonymous page),有点类似女声中的周深。前面我们反复强调,匿名页是没有文件背景的,这样当进行内存交换的时候,是与swap分区交换。磁盘文件系统里面的东西在内存的副本是file-backed的页面,所以不存在与swap分区交换的问题。但是tmpfs里面的东西,真的是在统计意义上统计到page cache了,但是它并没有真实的磁盘背景,这又和你访问磁盘文件系统里面的文件产生的page cache有本质的区别。所以,它是真地有那么一点misc的感觉,凡事都没有绝对,唯有变化本身是不变的。

    也可以通过ipcs找到新创建的SYS V共享内存:

    POSIX共享内存

    我对POSIX shm_open()、mmap () API系列的共享内存的喜爱,远远超过SYS V 100倍。原谅我就是一个懒惰的人,我就是讨厌ftok、shmget、shmat、shmdt这样的API。

    上面的程序如果用POSIX的写法,可以简化成写端psw.c:

    读端:

    编译和执行:

    这样我们会在/dev/shm/、/run/shm下面看到一个文件:

    坦白讲,mmap、munmap这样的API让我找到了回家的感觉,刚入行做Linux的时候,写好framebuffer驱动后,就是把/dev/fb0 mmap到用户空间来操作,所以mmap这样的 API,真的是特别亲切,像亲人一样。

    当然,如果你不喜欢shm_open()这个API,你也可以用常规的open来打开文件,然后进行mmap。关键的是mmap,wikipedia如是说:

    mmap

    In computing, mmap(2) is a POSIX-compliant Unix system call that maps files or devices into memory. It is a method of memory-mapped file I/O. It implements demand paging, because file contents are not read from disk directly and initially do not use physical RAM at all. The actual reads from disk are performed in a "lazy" manner, after a specific location is accessed. After the memory is no longer needed, it is important to munmap(2) the pointers to it. Protection information can be managed using mprotect(2), and special treatment can be enforced using madvise(2).

    POSIX的共享内存,仍然符合我们前面说的tmpfs的特点,在运行了sw,sr后,再运行psw和psr,我们发现free命令再次戏剧性变化:

    请将这个free命令的结果与前2次的free结果的各个字段进行对照:

    第3次比第2次的cached大了这么多?是因为我编写这篇文章边在访问磁盘里面的文件,当然POSIX的这个共享内存本身也导致cached增大了。

    memfd_create

    如果说POSIX的mmap让我找到回家的感觉,那么memfd_create()则是万般惊艳。见过这种API,才知道什么叫天生尤物——而且是尤物中的尤物,它完全属于那种让码农第一眼看到就会两眼充血,恨不得眼珠子夺眶而出贴到它身上去的那种API;一般人见到它第一次,都会忽略了它的长相,因为它的身材实在太火辣太抢眼了。

    先不要浮想联翩,在所有的所有开始之前,我们要先提一下跨进程分享fd(文件描述符,对应我们很多时候说的“句柄”)这个重要的概念。

    众所周知,Linux的fd属于一个进程级别的东西。进入每个进程的/proc/pid/fd可以看到它的fd的列表:

    这个进程的0,1,2和那个进程的0,1,2不是一回事。

    某年某月的某一天,人们发现,一个进程其实想访问另外一个进程的fd。当然,这只是目的不是手段。比如进程A有2个fd指向2片内存,如果进程B可以拿到这2个fd,其实就可以透过这2个fd访问到这2片内存。这个fd某种意义上充当了一个中间媒介的作用。有人说,那还不简单吗,如果进程A:

    fd = open();

    open()如果返回100,把这个100告诉进程B不就可以了吗,进程B访问这个100就可以了。这说明你还是没搞明白fd是一个进程内部的东西,是不能跨进程的概念。你的100和我的100,不是一个东西。这些基本的东西你搞不明白,你搞别的都是白搭。

    Linux提供一个特殊的方法,可以把一个进程的fd甩锅、踢皮球给另外一个进程(其实“甩锅”这个词用在这里不合适,因为“甩锅”是一种推卸,而fd的传递是一种分享)。我特码一直想把我的bug甩(分)锅(享)出去,却发现总是被人把bug甩锅过来。

    那么如何甩(分)锅(享)fd呢?

    Linux里面的甩锅需要借助cmsg,用于在socket上传递控制消息(也称Ancillary data),使用SCM_RIGHTS,进程可以透过UNIX Socket把一个或者多个fd(file descriptor)传递给另外一个进程。

    比如下面的这个函数,可以透过socket把fds指向的n个fd发送给另外一个进程:

    而另外一个进程,则可以透过如下函数接受这个fd:

    那么问题来了,如果在进程A中有一个文件的fd是100,发送给进程B后,它还是100吗?不能这么简单地理解,fd本身是一个进程级别的概念,每个进程有自己的fd的列表,比如进程B收到进程A的fd的时候,进程B自身fd空间里面自己的前面200个fd都已经被占用了,那么进程B接受到的fd就可能是201。数字本身在Linux的fd里面真地是一点都不重要,除了几个特殊的0,1,2这样的数字外。同样的,如果你把 cat /proc/interrupts 显示出的中断号就看成是硬件里面的中断偏移号码(比如ARM GIC里某号硬件中断),你会发现,这个关系整个是一个瞎扯。

    知道了甩锅API,那么重要的是,当它与memfd_create()结合的时候,我们准备甩出去的fd是怎么来?它是memfd_create()的返回值。

    memfd_create()这个函数的玄妙之处在于它会返回一个“匿名”内存“文件”的fd,而它本身并没有实体的文件系统路径,其典型用法如下:

    我们透过memfd_create()创建了一个“文件”,但是它实际映射到一片内存,而且在/xxx/yyy/zzz这样的文件系统下没有路径!没有路径!没有路径!

    所以,当你在Linux里面编程的时候,碰到这样的场景:需要一个fd,当成文件一样操作,但是又不需要真实地位于文件系统,那么,就请立即使用memfd_create()吧,它的manual page是这样描述的:

    memfd_create

    memfd_create() creates an anonymous file and returns a file descriptor that refers to it. The file behaves like a regular file, and so can be modified, truncated, memory-mapped, and so on. However, unlike a regular file, it lives in RAM and has a volatile backing storage.

     

    重点理解其中的regular这个单词。它的行动像一个regular的文件,但是它的背景却不regular。

    那么,它和前面我们说的透过UNIX Socket甩锅fd又有什么关系呢?memfd_create()得到了fd,它在行为上类似规则的fd,所以也可以透过socket来进行甩锅,这样A进程相当于把一片与fd对应的内存,分享给了进程B。

    下面的代码进程A通过memfd_create()创建了2片4MB的内存,并且透过socket(路径/tmp/fd-pass.socket)发送给进程B这2片内存对应的fd:

    下面的代码进程B透过相同的socket接受这2片内存对应的fd,之后通过read()读取每个文件的前256个字节并打印:

    上述代码参考了:

    https://openforums.wordpress.com/2016/08/07/open-file-descriptor-passing-over-unix-domain-sockets/

    上述的代码中,进程B是在进行read(fds[i], buffer, sizeof(buffer)),这体现了基于fd进行操作的regular特点。当然,如果是共享内存,现实的代码肯定还是多半会是mmap:

    mmap(NULL, SIZE, PROT_READ, MAP_SHARED, fd, 0);

    那么,透过socket发送memfd_create() fd来进行进程间共享内存这种方法,它究竟惊艳在哪里?

    我认为首要的惊艳之处在于编程模型的惊艳。API简单、灵活、通用。进程之间想共享几片内存共享几片内存,想怎么共享怎么共享,想共享给谁共享给谁,无非是多了几个fd和socket的传递过程。比如,我从互联网上面收到了jpeg的视频码流,一帧帧的画面,进程A可以创建多片buffer来缓存画面,然后就可以透过把每片buffer对应的fd,递交给另外的进程去解码等。Avenue to Jane(大道至简),简单的才是最好的!

    memfd_create()的另外一个惊艳之处在于支持“封印”(sealing,就是你玩游戏的时候的封印),sealing这个单词本身的意思是封条,在这个场景下,我更愿意把它翻译为“封印”。中国传说中的封印,多是采用如五行、太极、八卦等手段,并可有例如符咒、法器等物品的辅助。现指对某个单位施加一种力量,使其无法正常使用某些能力的本领(常出现于玄幻及神魔类作品,游戏中)。我这一生,最喜欢玩的游戏就是《仙剑奇侠传》和《轩辕剑——天之痕》,不知道是否暴露年龄了。

    采用memfd_create()的场景下,我们同样可以用某种法器,来控制共享内存的shrink、grow和write。最初的设想可以详见File Sealing & memfd_create()这篇文章:

    https://lwn.net/Articles/591108/

    我们如果在共享内存上施加了这样的封印,则可以限制对此片区域的ftruncate、write等动作,并建立某种意义上进程之间的相互信任,这是不是很拉风?

    还记得镇压孙悟空的五行山顶的封印吗?还记得孙悟空的紧箍咒吗?还记得悟空每次离开师傅的时候在师傅周围画的一个圈吗?

    封印

    * SEAL_SHRINK: If set, the inode size cannot be reduced * SEAL_GROW: If set, the inode size cannot be increased * SEAL_WRITE: If set, the file content cannot be modified

    File Sealing & memfd_create()文中举到的一个典型使用场景是,如果graphics client把它与graphics compoistor共享的内存交给compoistor去render,compoistor必须保证可以拿到这片内存。这里面的风险是client可能透过ftruncate()把这个memory shrink小,这样compositor就拿不到完整的buffer,会造成crash。所以compositor只愿意接受含有SEAL_SHRINK封印的fd,如果没有,对不起,我们不能一起去西天取经。

    在支持memfd_create()后,我们应尽可能地使用这种方式来替代传统的POSIX和SYS V,基本它也是一个趋势,比如我们在wayland相关项目中能看到这样的patch:

    dma_buf

    dma_buf定义

    The DMABUF framework provides a generic method for sharing buffers between multiple devices. Device drivers that support DMABUF can export a DMA buffer to userspace as a file descriptor (known as the exporter role), import a DMA buffer from userspace using a file descriptor previously exported for a different or the same device (known as the importer role), or both.

     

    简单地来说,dma_buf可以实现buffer在多个设备的共享,应用可以把一片底层驱动A的buffer导出到用户空间成为一个fd,也可以把fd导入到底层驱动 B。当然,如果进行mmap()得到虚拟地址,CPU也是可以在用户空间访问到已经获得用户空间虚拟地址的底层buffer的。

    上图中,进程A访问设备A并获得其使用的buffer的fd,之后通过socket把fd发送给进程B,而后进程B导入fd到设备B,B获得对设备A中的buffer的共享访问。如果CPU也需要在用户态访问这片buffer,则进行了mmap()动作。

    为什么我们要共享DMA buffer?想象一个场景:你要把你的屏幕framebuffer的内容透过gstreamer多媒体组件的服务,变成h264的视频码流,广播到网络上面,变成流媒体播放。在这个场景中,我们就想尽一切可能的避免内存拷贝

    技术上,管理framebuffer的驱动可以把这片buffer在底层实现为dma_buf,然后graphics compositor给这片buffer映射出来一个fd,之后透过socket发送fd 把这篇内存交给gstreamer相关的进程,如果gstreamer相关的“color space硬件转换”组件、“H264编码硬件组件”可以透过收到的fd还原出这些dma_buf的地址,则可以进行直接的加速操作了。比如color space透过接收到的fd1还原出framebuffer的地址,然后把转化的结果放到另外一片dma_buf,之后fd2对应这片YUV buffer被共享给h264编码器,h264编码器又透过fd2还原出YUV buffer的地址。

    这里面的核心点就是fd只是充当了一个“句柄”,用户进程和设备驱动透过fd最终寻找到底层的dma_buf,实现buffer在进程和硬件加速组件之间的zero-copy,这里面唯一进行了exchange的就是fd。

    再比如,如果把方向反过来,gstreamer从网络上收到了视频流,把它透过一系列动作转换为一片RGB的buffer,那么这片RGB的buffer最终还要在graphics compositor里面渲染到屏幕上,我们也需要透过dma_buf实现内存在video的decoder相关组件与GPU组件的共享。

    Linux内核的V4L2驱动(encoder、decoder多采用此种驱动)、DRM(Direct Rendering Manager,framebuffer/GPU相关)等都支持dma_buf。比如在DRM之上,进程可以透过

    int drmPrimeHandleToFD(int fd,
    uint32_t handle,
    uint32_t flags,
    int * prime_fd 
    )

    获得底层framebuffer对应的fd。如果这个fd被分享给gstreamer相关进程的video的color space转换,而color space转换硬件组件又被实现为一个V4L2驱动,则我们可以透过V4L2提供的如下接口,将这片buffer提供给V4L2驱动供其导入:

    如果是multi plane的话,则需要导入多个fd:

    相关细节可以参考这个文档:

    https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/dmabuf.html

    一切都是文件不是文件创造条件也要把它变成文件!这就是Linux的世界观。是不是文件不重要,关键是你得觉得它是个文件。在dma_buf的场景下,fd这个东西,纯粹就是个"句柄",方便大家通过这么一个fd能够对应到最终硬件需要访问的buffer。所以,透过fd的分享和传递,实际实现跨进程、跨设备(包括CPU)的内存共享。

    如果说前面的SYS V、POSIX、memfd_create()更加强调内存在进程间的共享,那么dma_buf则更加强调内存在设备间的共享,它未必需要跨进程。比如:

    有的童鞋说,为嘛在一个进程里面设备A和B共享内存还需要fd来倒腾一遍呢?我直接设备A驱动弄个全局变量存buffer的物理地址,设备B的驱动访问这个全局变量不就好了吗?我只能说,你对Linux内核的只提供机制不提供策略,以及软件工程每个模块各司其责,高内聚和低耦合的理解,还停留在裸奔的阶段。在没有dma_buf等类似机制的情况下,如果用户空间仍然负责构建策略并连接设备A和B,人们为了追求代码的干净,往往要进行这样的内存拷贝:

    dma_buf的支持依赖于驱动层是否实现了相关的callbacks。比如在v4l2驱动中,v4l2驱动支持把dma_buf导出(前面讲了v4l2也支持dma_buf的导入,关键看数据方向),它的代码体现在:

    drivers/media/common/videobuf2/videobuf2-dma-contig.c中的:

    其中的vb2_dc_dmabuf_ops是一个struct dma_buf_ops,它含有多个成员函数:

    当用户call VIDIOC_EXPBUF这个IOCTL的时候,可以把dma_buf转化为fd:

    int ioctl(int fd, VIDIOC_EXPBUF, struct v4l2_exportbuffer *argp);

    对应着驱动层的代码则会调用dma_buf_fd():

    应用程序可以通过如下方式拿到底层的dma_buf的fd:

    dma_buf的导入侧设备驱动,则会用到如下这些API:

    dma_buf_attach()
    dma_buf_map_attachment()
    dma_buf_unmap_attachment()
    dma_buf_detach()

    下面这张表,是笔者对这几种共享内存方式总的归纳:

    落花满天蔽月光,借一杯附荐凤台上。

    全剧终

     

    本文分享自微信公众号 - Linux阅码场(LinuxDev),作者:宋宝华

    原始发表时间:2019-12-09

     

    https://cloud.tencent.com/developer/article/1551288

     

    https://segmentfault.com/a/1190000021126368?utm_source=tag-newest

    展开全文
  • 共享单车、共享充电宝、共享雨伞,世间的共享有千万种,而我独爱共享内存。早期的共享内存,着重于强调把同一片内存,map到多个进程的虚拟地址空间(在相应进程找到一个VMA区域),以便于CPU...

    共享单车、共享充电宝、共享雨伞,世间的共享有千万种,而我独爱共享内存。

    早期的共享内存,着重于强调把同一片内存,map到多个进程的虚拟地址空间(在相应进程找到一个VMA区域),以便于CPU可以在各个进程访问到这片内存。

    现阶段广泛应用于多媒体、Graphics领域的共享内存方式,某种意义上不再强调映射到进程虚拟地址空间的概念(那无非是为了让CPU访问),而更强调以某种“句柄”的形式,让大家知道某一片视频、图形图像数据的存在并可以借助此“句柄”来跨进程引用这片内存,让视频encoder、decoder、GPU等可以跨进程访问内存。所以不同进程用的加速硬件其实是不同的,他们更在乎的是可以通过一个handle拿到这片内存,而不再特别在乎CPU访问它的虚拟地址(当然仍然可以映射到进程的虚拟地址空间供CPU访问)。

    只要内存的拷贝(memcpy)仍然是一个占据内存带宽、CPU利用率的消耗大户存在,共享内存作为Linux进程间通信、计算机系统里各个不同硬件组件通信的最高效方法,都将持续繁荣。关于内存拷贝会大多程度地占据CPU利用率,这个可以最简单地尝试拷贝1080P,帧率每秒60的电影画面,我保证你的系统的CPU,蛋会疼地不行。

    我早就想系统地写一篇综述Linux里面各种共享内存方式的文章了,但是一直被带娃这个事业牵绊,今日我决定顶着娃娃们的山呼海啸,也要写一篇文章不吐不快。

    共享内存的方式有很多种,目前主流的方式仍然有:

    共享内存的方式

    1.基于传统SYS V的共享内存;

    2.基于POSIX mmap文件映射实现共享内存;

    3.通过memfd_create()和fd跨进程共享实现共享内存;

    4.多媒体、图形领域广泛使用的基于dma-buf的共享内存。

    共享内存

    SYS V共享内存

    历史悠久、年代久远、API怪异,对应内核代码linux/ipc/shm.c,当你编译内核的时候不选择CONFIG_SYSVIPC,则不再具备此能力。

    你在Linux敲ipcs命令看到的share memory就是这种共享内存:

    下面写一个最简单的程序来看共享内存的写端sw.c:

    以及共享内存的读端sr.c:

    编译和准备运行:

    在此之前我们看一下系统的free:

    下面运行sw和sr:

    我们发现sr打印出来的和sw写进去的是一致的。这个时候我们再看下free:

    可以看到used显著增大了(711632 -> 715908), shared显著地增大了(2264 -> 6360),而cached这一列也显著地增大326604->330716。

    我们都知道cached这一列统计的是file-backed的文件的page cache的大小。理论上,共享内存属于匿名页,但是由于这里面有个非常特殊的tmpfs(/dev/shm指向/run/shm,/run/shm则mount为tmpfs):

    所以可以看出tmpfs的东西其实真的是有点含混:我们可以理解它为file-backed的匿名页(anonymous page),有点类似女声中的周深。前面我们反复强调,匿名页是没有文件背景的,这样当进行内存交换的时候,是与swap分区交换。磁盘文件系统里面的东西在内存的副本是file-backed的页面,所以不存在与swap分区交换的问题。但是tmpfs里面的东西,真的是在统计意义上统计到page cache了,但是它并没有真实的磁盘背景,这又和你访问磁盘文件系统里面的文件产生的page cache有本质的区别。所以,它是真地有那么一点misc的感觉,凡事都没有绝对,唯有变化本身是不变的。

    也可以通过ipcs找到新创建的SYS V共享内存:

    POSIX共享内存

    我对POSIX shm_open()、mmap () API系列的共享内存的喜爱,远远超过SYS V 100倍。原谅我就是一个懒惰的人,我就是讨厌ftok、shmget、shmat、shmdt这样的API。

    上面的程序如果用POSIX的写法,可以简化成写端psw.c:

    读端:

    编译和执行:

    这样我们会在/dev/shm/、/run/shm下面看到一个文件:

    坦白讲,mmap、munmap这样的API让我找到了回家的感觉,刚入行做Linux的时候,写好framebuffer驱动后,就是把/dev/fb0 mmap到用户空间来操作,所以mmap这样的 API,真的是特别亲切,像亲人一样。

    当然,如果你不喜欢shm_open()这个API,你也可以用常规的open来打开文件,然后进行mmap。关键的是mmap,wikipedia如是说:

    mmap

    In computing, mmap(2) is a POSIX-compliant Unix system call that maps files or devices into memory. It is a method of memory-mapped file I/O. It implements demand paging, because file contents are not read from disk directly and initially do not use physical RAM at all. The actual reads from disk are performed in a "lazy" manner, after a specific location is accessed. After the memory is no longer needed, it is important to munmap(2) the pointers to it. Protection information can be managed using mprotect(2), and special treatment can be enforced using madvise(2).

    POSIX的共享内存,仍然符合我们前面说的tmpfs的特点,在运行了sw,sr后,再运行psw和psr,我们发现free命令再次戏剧性变化:

    请将这个free命令的结果与前2次的free结果的各个字段进行对照:

    第3次比第2次的cached大了这么多?是因为我编写这篇文章边在访问磁盘里面的文件,当然POSIX的这个共享内存本身也导致cached增大了。

    memfd_create

    如果说POSIX的mmap让我找到回家的感觉,那么memfd_create()则是万般惊艳。见过这种API,才知道什么叫天生尤物——而且是尤物中的尤物,它完全属于那种让码农第一眼看到就会两眼充血,恨不得眼珠子夺眶而出贴到它身上去的那种API;一般人见到它第一次,都会忽略了它的长相,因为它的身材实在太火辣太抢眼了。

    先不要浮想联翩,在所有的所有开始之前,我们要先提一下跨进程分享fd(文件描述符,对应我们很多时候说的“句柄”)这个重要的概念。

    众所周知,Linux的fd属于一个进程级别的东西。进入每个进程的/proc/pid/fd可以看到它的fd的列表:

    这个进程的0,1,2和那个进程的0,1,2不是一回事。

    某年某月的某一天,人们发现,一个进程其实想访问另外一个进程的fd。当然,这只是目的不是手段。比如进程A有2个fd指向2片内存,如果进程B可以拿到这2个fd,其实就可以透过这2个fd访问到这2片内存。这个fd某种意义上充当了一个中间媒介的作用。有人说,那还不简单吗,如果进程A:

    fd = open();

    open()如果返回100,把这个100告诉进程B不就可以了吗,进程B访问这个100就可以了。这说明你还是没搞明白fd是一个进程内部的东西,是不能跨进程的概念。你的100和我的100,不是一个东西。这些基本的东西你搞不明白,你搞别的都是白搭。

    Linux提供一个特殊的方法,可以把一个进程的fd甩锅、踢皮球给另外一个进程(其实“甩锅”这个词用在这里不合适,因为“甩锅”是一种推卸,而fd的传递是一种分享)。我特码一直想把我的bug甩(分)锅(享)出去,却发现总是被人把bug甩锅过来。

    那么如何甩(分)锅(享)fd呢?

    Linux里面的甩锅需要借助cmsg,用于在socket上传递控制消息(也称Ancillary data),使用SCM_RIGHTS,进程可以透过UNIX Socket把一个或者多个fd(file descriptor)传递给另外一个进程。

    比如下面的这个函数,可以透过socket把fds指向的n个fd发送给另外一个进程:

    而另外一个进程,则可以透过如下函数接受这个fd:

    那么问题来了,如果在进程A中有一个文件的fd是100,发送给进程B后,它还是100吗?不能这么简单地理解,fd本身是一个进程级别的概念,每个进程有自己的fd的列表,比如进程B收到进程A的fd的时候,进程B自身fd空间里面自己的前面200个fd都已经被占用了,那么进程B接受到的fd就可能是201。数字本身在Linux的fd里面真地是一点都不重要,除了几个特殊的0,1,2这样的数字外。同样的,如果你把 cat /proc/interrupts 显示出的中断号就看成是硬件里面的中断偏移号码(比如ARM GIC里某号硬件中断),你会发现,这个关系整个是一个瞎扯。

    知道了甩锅API,那么重要的是,当它与memfd_create()结合的时候,我们准备甩出去的fd是怎么来?它是memfd_create()的返回值。

    memfd_create()这个函数的玄妙之处在于它会返回一个“匿名”内存“文件”的fd,而它本身并没有实体的文件系统路径,其典型用法如下:

    我们透过memfd_create()创建了一个“文件”,但是它实际映射到一片内存,而且在/xxx/yyy/zzz这样的文件系统下没有路径!没有路径!没有路径!

    所以,当你在Linux里面编程的时候,碰到这样的场景:需要一个fd,当成文件一样操作,但是又不需要真实地位于文件系统,那么,就请立即使用memfd_create()吧,它的manual page是这样描述的:

    memfd_create

    memfd_create() creates an anonymous file and returns a file descriptor that refers to it.  The file behaves like a regular file, and so can be modified, truncated, memory-mapped, and so on.
    However, unlike a regular file, it lives in RAM and has a volatile backing storage.

    重点理解其中的regular这个单词。它的行动像一个regular的文件,但是它的背景却不regular。

    那么,它和前面我们说的透过UNIX Socket甩锅fd又有什么关系呢?memfd_create()得到了fd,它在行为上类似规则的fd,所以也可以透过socket来进行甩锅,这样A进程相当于把一片与fd对应的内存,分享给了进程B。

    下面的代码进程A通过memfd_create()创建了2片4MB的内存,并且透过socket(路径/tmp/fd-pass.socket)发送给进程B这2片内存对应的fd:

    下面的代码进程B透过相同的socket接受这2片内存对应的fd,之后通过read()读取每个文件的前256个字节并打印:

    上述代码参考了: 

    https://openforums.wordpress.com/2016/08/07/open-file-descriptor-passing-over-unix-domain-sockets/

    上述的代码中,进程B是在进行read(fds[i], buffer, sizeof(buffer)),这体现了基于fd进行操作的regular特点。当然,如果是共享内存,现实的代码肯定还是多半会是mmap:

    mmap(NULL, SIZE, PROT_READ, MAP_SHARED, fd, 0);

    那么,透过socket发送memfd_create() fd来进行进程间共享内存这种方法,它究竟惊艳在哪里?

    我认为首要的惊艳之处在于编程模型的惊艳。API简单、灵活、通用。进程之间想共享几片内存共享几片内存,想怎么共享怎么共享,想共享给谁共享给谁,无非是多了几个fd和socket的传递过程。比如,我从互联网上面收到了jpeg的视频码流,一帧帧的画面,进程A可以创建多片buffer来缓存画面,然后就可以透过把每片buffer对应的fd,递交给另外的进程去解码等。Avenue to Jane(大道至简),简单的才是最好的!

    memfd_create()的另外一个惊艳之处在于支持“封印”(sealing,就是你玩游戏的时候的封印),sealing这个单词本身的意思是封条,在这个场景下,我更愿意把它翻译为“封印”。中国传说中的封印,多是采用如五行、太极、八卦等手段,并可有例如符咒、法器等物品的辅助。现指对某个单位施加一种力量,使其无法正常使用某些能力的本领(常出现于玄幻及神魔类作品,游戏中)。我这一生,最喜欢玩的游戏就是《仙剑奇侠传》和《轩辕剑——天之痕》,不知道是否暴露年龄了。

    采用memfd_create()的场景下,我们同样可以用某种法器,来控制共享内存的shrink、grow和write。最初的设想可以详见File Sealing & memfd_create()这篇文章:

    https://lwn.net/Articles/591108/

    我们如果在共享内存上施加了这样的封印,则可以限制对此片区域的ftruncate、write等动作,并建立某种意义上进程之间的相互信任,这是不是很拉风?

    还记得镇压孙悟空的五行山顶的封印吗?还记得孙悟空的紧箍咒吗?还记得悟空每次离开师傅的时候在师傅周围画的一个圈吗?

    封印

    * SEAL_SHRINK: If set, the inode size cannot be reduced
    * SEAL_GROW: If set, the inode size cannot be increased
    * SEAL_WRITE: If set, the file content cannot be modified

    File Sealing & memfd_create()文中举到的一个典型使用场景是,如果graphics client把它与graphics compoistor共享的内存交给compoistor去render,compoistor必须保证可以拿到这片内存。这里面的风险是client可能透过ftruncate()把这个memory shrink小,这样compositor就拿不到完整的buffer,会造成crash。所以compositor只愿意接受含有SEAL_SHRINK封印的fd,如果没有,对不起,我们不能一起去西天取经。

    在支持memfd_create()后,我们应尽可能地使用这种方式来替代传统的POSIX和SYS V,基本它也是一个趋势,比如我们在wayland相关项目中能看到这样的patch:

    dma_buf

    dma_buf定义

    The DMABUF framework provides a generic method for sharing buffers between multiple devices. Device drivers that support DMABUF can export a DMA buffer to userspace as a file descriptor (known as the exporter role), import a DMA buffer from userspace using a file descriptor previously exported for a different or the same device (known as the importer role), or both. 

    简单地来说,dma_buf可以实现buffer在多个设备的共享,应用可以把一片底层驱动A的buffer导出到用户空间成为一个fd,也可以把fd导入到底层驱动 B。当然,如果进行mmap()得到虚拟地址,CPU也是可以在用户空间访问到已经获得用户空间虚拟地址的底层buffer的。

    上图中,进程A访问设备A并获得其使用的buffer的fd,之后通过socket把fd发送给进程B,而后进程B导入fd到设备B,B获得对设备A中的buffer的共享访问。如果CPU也需要在用户态访问这片buffer,则进行了mmap()动作。

    为什么我们要共享DMA buffer?想象一个场景:你要把你的屏幕framebuffer的内容透过gstreamer多媒体组件的服务,变成h264的视频码流,广播到网络上面,变成流媒体播放。在这个场景中,我们就想尽一切可能的避免内存拷贝

    技术上,管理framebuffer的驱动可以把这片buffer在底层实现为dma_buf,然后graphics compositor给这片buffer映射出来一个fd,之后透过socket发送fd 把这篇内存交给gstreamer相关的进程,如果gstreamer相关的“color space硬件转换”组件、“H264编码硬件组件”可以透过收到的fd还原出这些dma_buf的地址,则可以进行直接的加速操作了。比如color space透过接收到的fd1还原出framebuffer的地址,然后把转化的结果放到另外一片dma_buf,之后fd2对应这片YUV buffer被共享给h264编码器,h264编码器又透过fd2还原出YUV buffer的地址。

    这里面的核心点就是fd只是充当了一个“句柄”,用户进程和设备驱动透过fd最终寻找到底层的dma_buf,实现buffer在进程和硬件加速组件之间的zero-copy,这里面唯一进行了exchange的就是fd。

    再比如,如果把方向反过来,gstreamer从网络上收到了视频流,把它透过一系列动作转换为一片RGB的buffer,那么这片RGB的buffer最终还要在graphics compositor里面渲染到屏幕上,我们也需要透过dma_buf实现内存在video的decoder相关组件与GPU组件的共享。

    Linux内核的V4L2驱动(encoder、decoder多采用此种驱动)、DRM(Direct Rendering Manager,framebuffer/GPU相关)等都支持dma_buf。比如在DRM之上,进程可以透过

    int drmPrimeHandleToFD(int fd,

    uint32_t handle,

    uint32_t flags,

    int * prime_fd 

    )

    获得底层framebuffer对应的fd。如果这个fd被分享给gstreamer相关进程的video的color space转换,而color space转换硬件组件又被实现为一个V4L2驱动,则我们可以透过V4L2提供的如下接口,将这片buffer提供给V4L2驱动供其导入:

    如果是multi plane的话,则需要导入多个fd:

    相关细节可以参考这个文档:

    https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/dmabuf.html

    一切都是文件不是文件创造条件也要把它变成文件!这就是Linux的世界观。是不是文件不重要,关键是你得觉得它是个文件。在dma_buf的场景下,fd这个东西,纯粹就是个"句柄",方便大家通过这么一个fd能够对应到最终硬件需要访问的buffer。所以,透过fd的分享和传递,实际实现跨进程、跨设备(包括CPU)的内存共享。

    如果说前面的SYS V、POSIX、memfd_create()更加强调内存在进程间的共享,那么dma_buf则更加强调内存在设备间的共享,它未必需要跨进程。比如:

    有的童鞋说,为嘛在一个进程里面设备A和B共享内存还需要fd来倒腾一遍呢?我直接设备A驱动弄个全局变量存buffer的物理地址,设备B的驱动访问这个全局变量不就好了吗?我只能说,你对Linux内核的只提供机制不提供策略,以及软件工程每个模块各司其责,高内聚和低耦合的理解,还停留在裸奔的阶段。在没有dma_buf等类似机制的情况下,如果用户空间仍然负责构建策略并连接设备A和B,人们为了追求代码的干净,往往要进行这样的内存拷贝:

    dma_buf的支持依赖于驱动层是否实现了相关的callbacks。比如在v4l2驱动中,v4l2驱动支持把dma_buf导出(前面讲了v4l2也支持dma_buf的导入,关键看数据方向),它的代码体现在:

    drivers/media/common/videobuf2/videobuf2-dma-contig.c中的:

    其中的vb2_dc_dmabuf_ops是一个struct dma_buf_ops,它含有多个成员函数:

    当用户call VIDIOC_EXPBUF这个IOCTL的时候,可以把dma_buf转化为fd:

    int ioctl(int fd, VIDIOC_EXPBUF, struct v4l2_exportbuffer *argp);

    对应着驱动层的代码则会调用dma_buf_fd():

    应用程序可以通过如下方式拿到底层的dma_buf的fd:

    dma_buf的导入侧设备驱动,则会用到如下这些API:

    dma_buf_attach()

    dma_buf_map_attachment()

    dma_buf_unmap_attachment()

    dma_buf_detach()

    下面这张表,是笔者对这几种共享内存方式总的归纳:

    落花满天蔽月光,借一杯附荐凤台上。

    全剧终

    Linux阅码场原创精华文章汇总

    更多精彩,尽在"Linux阅码场",扫描下方二维码关注


    你的随手转发或点个在看是对我们最大的支持!

    展开全文
  • 共享内存实现原理

    万次阅读 2018-09-28 18:02:39
    共享内存的使用实现原理(必考必问,然后共享内存段被映射进进程空间之后,存在于进程空间的什么位置?共享内存段最大限制是多少?) nmap函数要求内核创建一个新额虚拟存储器区域,最好是从地质start开始的一个...

    共享内存的使用实现原理(必考必问,然后共享内存段被映射进进程空间之后,存在于进程空间的什么位置?共享内存段最大限制是多少?)

    nmap函数要求内核创建一个新额虚拟存储器区域,最好是从地质start开始的一个区域,并将文件描述符fd指定对象的一个连续的片(chunk)映射到这个新的区域。

     SHMMNI为128,表示系统中最多可以有128个共享内存对象。

     

    共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

    采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

    Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,本文将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

    一、内核怎样保证各个进程寻址到同一个共享内存区域的内存页面

    1、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

    2、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

    3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

    4、对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
    注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

    5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
    注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

    上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。

    二、mmap()及其相关系统调用

    mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

    注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

    1、mmap()系统调用形式如下:

    void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
    参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

    2、系统调用mmap()用于共享内存的两种方式:

    (1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下: 

            fd=open(name, flag, mode);
    if(fd<0)
            ...
            
    ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。 

    (2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
    对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。

    3、系统调用munmap()

    int munmap( void * addr, size_t len )
    该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

    4、系统调用msync()

    int msync ( void * addr , size_t len, int flags)
    一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

    三、mmap()范例

    下面将给出使用mmap()的两个范例:范例1给出两个进程通过映射普通文件实现共享内存通信;范例2给出父子进程通过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。

    范例1:两个进程通过映射普通文件实现共享内存通信

    范例1包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。

    下面是两个程序代码:

    /*-------------map_normalfile1.c-----------*/
    #include <sys/mman.h>;
    #include <sys/types.h>;
    #include <fcntl.h>;
    #include <unistd.h>;
    typedef struct{
            char name[4];
            int  age;
    }people;

    main(int argc, char** argv) // map a normal file as shared mem:
    {
            int fd,i;
            people *p_map;
            char temp;
            
            fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
            lseek(fd,sizeof(people)*5-1,SEEK_SET);
            write(fd,"",1);
            
            p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 );
            close( fd );
            temp = 'a';
            for(i=0; i<10; i++)
            {
                    temp += 1;
                    memcpy( ( *(p_map+i) ).name, &temp,2 );
                    ( *(p_map+i) ).age = 20+i;
            }
            printf(" initialize over /n ");
            sleep(10);

            munmap( p_map, sizeof(people)*10 );
            printf( "umap ok /n" );
    }

    /*-------------map_normalfile2.c-----------*/
    #include <sys/mman.h>;
    #include <sys/types.h>;
    #include <fcntl.h>;
    #include <unistd.h>;
    typedef struct{
            char name[4];
            int  age;
    }people;

    main(int argc, char** argv)        // map a normal file as shared mem:
    {
            int fd,i;
            people *p_map;
            fd=open( argv[1],O_CREAT|O_RDWR,00777 );
            p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
            for(i = 0;i<10;i++)
            {
            printf( "name: %s age %d;/n",(*(p_map+i)).name, (*(p_map+i)).age );

            }
            munmap( p_map,sizeof(people)*10 );
    }

    map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。map_normfile1首先打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。

    map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。

    分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下:

    initialize over
    umap ok

    在map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果):

    name: b        age 20;        name: c        age 21;        name: d        age 22;        name: e        age 23;        name: f        age 24;
    name: g        age 25;        name: h        age 26;        name: I        age 27;        name: j        age 28;        name: k        age 29;

    在map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:

    name: b        age 20;        name: c        age 21;        name: d        age 22;        name: e        age 23;        name: f        age 24;
    name:        age 0;        name:        age 0;        name:        age 0;        name:        age 0;        name:        age 0;

    从程序的运行结果中可以得出的结论

    1、 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;

    2、 可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截短为5个people结构大小,而在map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10个people结构的值,后面将给出详细讨论。
    注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。

    3、 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

    范例2:父子进程通过匿名映射实现共享内存

    #include <sys/mman.h>;
    #include <sys/types.h>;
    #include <fcntl.h>;
    #include <unistd.h>;
    typedef struct{
            char name[4];
            int  age;
    }people;
    main(int argc, char** argv)
    {
            int i;
            people *p_map;
            char temp;
            p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
            if(fork() == 0)
            {
                    sleep(2);
                    for(i = 0;i<5;i++)
                            printf("child read: the %d people's age is %d/n",i+1,(*(p_map+i)).age);
                    (*p_map).age = 100;
                    munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。
                    exit();
            }
            temp = 'a';
            for(i = 0;i<5;i++)
            {
                    temp += 1;
                    memcpy((*(p_map+i)).name, &temp,2);
                    (*(p_map+i)).age=20+i;
            }

            sleep(5);
            printf( "parent read: the first people,s age is %d/n",(*p_map).age );
            printf("umap/n");
            munmap( p_map,sizeof(people)*10 );
            printf( "umap ok/n" );
    }

    考察程序的输出结果,体会父子进程匿名共享内存:

    child read: the 1 people's age is 20
    child read: the 2 people's age is 21
    child read: the 3 people's age is 22
    child read: the 4 people's age is 23
    child read: the 5 people's age is 24

    parent read: the first people,s age is 100
    umap
    umap ok

    四、对mmap()返回地址的访问

    前面对范例运行结构的讨论中已经提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:





    注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:

    #include <sys/mman.h>;
    #include <sys/types.h>;
    #include <fcntl.h>;
    #include <unistd.h>;
    typedef struct{
            char name[4];
            int  age;
    }people;

    main(int argc, char** argv)
    {
            int fd,i;
            int pagesize,offset;
            people *p_map;
            
            pagesize = sysconf(_SC_PAGESIZE);
            printf("pagesize is %d/n",pagesize);
            fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
            lseek(fd,pagesize*2-100,SEEK_SET);
            write(fd,"",1);
            offset = 0;        //此处offset = 0编译成版本1;offset = pagesize编译成版本2
            p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);
            close(fd);
            
            for(i = 1; i<10; i++)
            {
                    (*(p_map+pagesize/sizeof(people)*i-2)).age = 100;
                    printf("access page %d over/n",i);
                    (*(p_map+pagesize/sizeof(people)*i-1)).age = 100;
                    printf("access page %d edge over, now begin to access page %d/n",i, i+1);
                    (*(p_map+pagesize/sizeof(people)*i)).age = 100;
                    printf("access page %d over/n",i+1);
            }
            munmap(p_map,sizeof(people)*10);
    }

    如程序中所注释的那样,把程序编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize*2-99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(大小为:pagesize-99)。程序中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize*3的字节数。

    版本1的输出结果如下:

    pagesize is 4096
    access page 1 over
    access page 1 edge over, now begin to access page 2
    access page 2 over
    access page 2 over
    access page 2 edge over, now begin to access page 3
    Bus error                //被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个页面

    版本2的输出结果如下:

    pagesize is 4096
    access page 1 over
    access page 1 edge over, now begin to access page 2
    Bus error                //被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个页面

    结论:采用系统调用mmap()实现进程间通信是很方便的,在应用层上接口非常简洁。内部实现机制区涉及到了linux存储管理以及文件系统等方面的内容,可以参考一下相关重要数据结构来加深理解。在本专题的后面部分,将介绍系统v共享内存的实现。

    在共享内存(上)中,主要围绕着系统调用mmap()进行讨论的,本部分将讨论系统V共享内存,并通过实验结果对比来阐述两者的异同。系统V共享内存指的是把所有共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。

    系统调用mmap()通过映射一个普通文件实现共享内存。系统V则是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的),后面还将阐述。

    1、系统V共享内存原理

    进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构注同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。

    注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下:

    struct shmid_kernel /* private to the kernel */
    {        
            struct kern_ipc_perm        shm_perm;
            struct file *                shm_file;
            int                        id;
            unsigned long                shm_nattch;
            unsigned long                shm_segsz;
            time_t                        shm_atim;
            time_t                        shm_dtim;
            time_t                        shm_ctim;
            pid_t                        shm_cprid;
            pid_t                        shm_lprid;
    };

    该结构中最重要的一个域应该是shm_file,它存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件,一般情况下,特殊文件系统shm中的文件是不能用read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。

    这里我们采用[1]中的图表给出与系统V共享内存相关数据结构:





    正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。

    在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。

    2、系统V共享内存API

    对于系统V共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。

    #include <sys/ipc.h>;
    #include <sys/shm.h>;

    shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。这里我们不对这些系统调用作具体的介绍,读者可参考相应的手册页面,后面的范例中将给出它们的调用方法。

    注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。

    3、系统V共享内存限制

    在/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

    在[2]中,给出了这些限制的测试方法,不再赘述。

    4、系统V共享内存范例

    本部分将给出系统V共享内存API的使用方法,并对比分析系统V共享内存机制与mmap()映射普通文件实现共享内存之间的差异,首先给出两个进程通过系统V共享内存通信的范例:

    /***** testwrite.c *******/
    #include <sys/ipc.h>;
    #include <sys/shm.h>;
    #include <sys/types.h>;
    #include <unistd.h>;
    typedef struct{
            char name[4];
            int age;
    } people;
    main(int argc, char** argv)
    {
            int shm_id,i;
            key_t key;
            char temp;
            people *p_map;
            char* name = "/dev/shm/myshm2";
            key = ftok(name,0);
            if(key==-1)
                    perror("ftok error");
            shm_id=shmget(key,4096,IPC_CREAT);        
            if(shm_id==-1)
            {
                    perror("shmget error");
                    return;
            }
            p_map=(people*)shmat(shm_id,NULL,0);
            temp='a';
            for(i = 0;i<10;i++)
            {
                    temp+=1;
                    memcpy((*(p_map+i)).name,&temp,1);
                    (*(p_map+i)).age=20+i;
            }
            if(shmdt(p_map)==-1)
                    perror(" detach error ");
    }
    /********** testread.c ************/
    #include <sys/ipc.h>;
    #include <sys/shm.h>;
    #include <sys/types.h>;
    #include <unistd.h>;
    typedef struct{
            char name[4];
            int age;
    } people;
    main(int argc, char** argv)
    {
            int shm_id,i;
            key_t key;
            people *p_map;
            char* name = "/dev/shm/myshm2";
            key = ftok(name,0);
            if(key == -1)
                    perror("ftok error");
            shm_id = shmget(key,4096,IPC_CREAT);        
            if(shm_id == -1)
            {
                    perror("shmget error");
                    return;
            }
            p_map = (people*)shmat(shm_id,NULL,0);
            for(i = 0;i<10;i++)
            {
            printf( "name:%s/n",(*(p_map+i)).name );
            printf( "age %d/n",(*(p_map+i)).age );
            }
            if(shmdt(p_map) == -1)
                    perror(" detach error ");
    }

    testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwrite及testread,先后执行./testwrite及./testread 则./testread输出结果如下:

    name: b        age 20;        name: c        age 21;        name: d        age 22;        name: e        age 23;        name: f        age 24;
    name: g        age 25;        name: h        age 26;        name: I        age 27;        name: j        age 28;        name: k        age 29;

    通过对试验结果分析,对比系统V与mmap()映射普通文件实现共享内存通信,可以得出如下结论:

    1、 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。注:前面讲到,系统V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。

    2、 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

    3、 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。

    结论:

    共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

    共享内存涉及到了存储管理以及文件系统等方面的知识,深入理解其内部机制有一定的难度,关键还要紧紧抓住内核使用的重要数据结构。系统V共享内存是以文件的形式组织在特殊文件系统shm中的。通过shmget可以创建或获得共享内存的标识符。取得共享内存标识符后,要通过shmat将这个内存区映射到本进程的虚拟地址空间。

    --------------------- 本文来自 hai0808 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/liu0808/article/details/52967167?utm_source=copy

    展开全文
  • 进程间通信:共享内存概念及代码

    千次阅读 2020-03-10 15:46:15
    前言 接下讨论的IPC机制,它们最初由System V版本的Unix引入。由于这些机制都出现在同一个版本中并且有着相似的编程接口,所以它们被称为System V IPC机制。接下来的内容包括: ...共享内存是在两个正在运行...
  • qt写的共享内存简单例子

    热门讨论 2013-03-22 14:19:47
    qt写的共享内存简单例子,希望对你起到抛砖引玉的作用
  • 【Linux】Linux的共享内存

    万次阅读 多人点赞 2018-08-10 19:17:45
    实现进程间通信最简单也是最直接的方法就是共享内存——为参与通信的多个进程在内存中开辟一个共享区。由于进程可以直接对共享内存进行读写操作,因此这种通信方式效率特别高,但其弱点是,它没有互斥机制,需要信号...
  • VB程序间共享内存

    热门讨论 2012-03-20 20:44:00
    程序间共享内存,用于数据交换,非常强大的示例,解决你进程间数据共享的问题
  • Windows进程间通信之共享内存

    热门讨论 2013-05-13 21:02:09
    共享内存方式实现进程间通信。详细看我的博客:http://blog.csdn.net/pengguokan/article/details/8921346
  • C#进程间通信-共享内存代码实例

    热门讨论 2013-12-04 08:03:58
    C#进程间通信-共享内存代码实例。用实现了2个进程,他们之间使用共享内存方式进行通信。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 942,693
精华内容 377,077
关键字:

共享内存