Linux IPC之System V共享内存

导言:共享内存允许两个或多个进程共享物理内存的同一块区域(段)。由于一个共享内存段会成为一个进程用户空间内存的一部分,因此,这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。与管道或消息队列要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,共享内存的速度更快(每个进程也存在通过系统调用来执行复制操作的开销)。共享内存这种IPC机制不由内核控制,意味着通常需要通过某些同步方法使得进程不会出现同时访问共享内存的情况。

共享内存IPC机制概述

使用一个共享内存段,通常需要执行如下步骤:
1. 调用shmget()创建一个新的共享内存段,或取得一个既有的共享内存段的标识符(即,由其他进程创建的共享内存段),这个调用将返回后续调用中需要用到的共享内存标识符
2. 使用shmat()来附上(attach)共享内存段,使该段成为调用进程的虚拟内存的一部分。
3. 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat()调用返回的addr值,它是指向进程的虚拟地址空间中该共享内存段的起点的指针。
4. 调用shmdt()来分离共享内存段,在这个调用之后,进程就无法再引用这块共享内存了,这一步是可选的,并且在进程终止时会自动完成这一步。
5. 调用shmctl()来删除共享内存段,只有当,当前所有附加内存段的进程都与之分离(detach)之后,内存段才会被销毁,只有一个进程需要执行这一步。

使用方法

shmget

shmget()在成功时,返回新的既有的共享内存段的标识符

#include <sys/types.h> // for portability
#include <sys/shm.h>

// returns shared memory segment identifier on success, or -1 on error
int shmget(key_t key, size_t size, int shmflg);
  • key参数通常是IPC_PRIVATE值或由ftok()返回的键。
  • size是一个正整数,表示需要分配的段的字节数。内核是以系统分页大小的整数倍来分配共享内存的,因此,实际上size会被提升到最近的系统分页大小的整数倍。如果使用shmget()来获取一个既有段的标识符,那么size对段不会产生任何效果,但它必须要小于或等于段的大小。
  • shmflg参数执行的任务与其在其他IPC get调用中执行的任务一样,指定施加于新共享内存段上的权限,或需检查的既有内存段的权限。

shmat

shmat()系统调用将shmid标识的共享内存段附加到调用进程的虚拟地址空间中。shmat()结果是返回附加共享内存段的地址,我们可以像对待普通的C指针那样对待这个值,段与进程的虚拟内存的其他部分看起来毫无差异。通常会将shmat()的返回值赋给一个指向某个定义的结构的指针,以便在该段上设定该结构。

#include <sys/types.h> // For portability
#include <sys/shm.h>

// returns address at which shared memory is attached on success, or (void *)-1 on error
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 如果shmaddr是NULL,则段会被附加到内核所选择的一个合适的地址处。(这是附加一个段的优选方法)
  • 如果shmaddr不为NULL,且没有设置SHM_RND,则段会被附加到由shmaddr指定的地址处,它必须是系统分页大小的一个倍数(否则会发生EINVAL错误)。
  • 如果shmaddr不为NULL,且设置SHM_RND,则段会被附加到在shmaddr中提供的地址,被舍入到最近的常量SHMLBA(shared memory low boundary address)的倍数。这个常量等于系统分页大小的某个倍数。(将一个段附加到值为SHMLBA的倍数的地址处在一些架构上是有必要的,因为这样才能够提升CPU的快速缓冲性能和防止出现同一个段的不同附加操作在CPU快速缓冲中存在不一致的视图的情况)。在x86架构上,SHMLBA的值与系统分页大小是一样的,这意味着此类缓冲不一致不可能在那些架构上出现。

说明:为shmaddr指定一个非NULL值,不是一种推荐的做法,原因如下:
1. 它降低了一个应用程序的可移植性。在一个UNIX实现上有效的地址在另一个实现上可能是无效的。
2. 试图将一个共享内存段附加到一个正在使用中的特定地址处的操作会失败。

访问权限

要附加一个共享内存段以供只读访问,则需要在shmflg中指定SHM_RDONLY标记。试图更新只读段中的内容会导致段错误(SIGSEGV信号)的发生。如果没有指定SHM_RDONLY,就可以读写该内存。

shmdt

当一个进程不再需要访问一个共享内存段时,可以调用shmdt()来将该段分离出其虚拟地址空间。

#include <sys/types.h> // For portability
#include <sys/shm.h>

// returns 0 on success, or -1 on error
int shmdt(const void *shmaddr);

注意:分离一个共享内存段,与删除它是不同的。删除是通过shmctl()IPC_RMID操作来完成的。

通过共享内存传输数据(例子)

例子:使用System V共享内存和信号量。这个程序由两个程序构成,写者和读者。写者从标准输入中读取数据块,并将数据复制到一个共享内存段中。读者将共享内存段中的数据块复制到标准输出中。(类似,程序将共享内存当成了管道来处理)

TODO

共享内存在虚拟内存中的位置

在x86-32架构上,一个进程的各个部分在虚拟内存中的布局如下:其中,共享内存段被附加在向上增长的堆和向下增长的栈之间未被分配的空间中,为给堆和栈的增长腾出空间,附加共享内存段的虚拟地址从0x40000000开始,内存映射和共享库也是被放置在这个区域中。

pic

地址0x40000000被定义成了内核常量TASK_UNMAPPED_BASE。通过将这个常量定义成一个不同的值,并且重建内核可以修改这个地址的值。如果在调用shmat()mmap()时,显示地指定了一个地址,那么一个共享内存段或内存映射可以被放置在低于TASK_UNMAPPED_BASE的地址处。通过Linux特有的/proc/PID/maps文件能够看到一个程序映射的共享内存段和共享库的位置。从内核2.6.14开始,Linux还提供了/proc/PID/smaps文件,它给出了有关一个进程中各个映射的内存消耗方面的更多信息,更多细节可参考proc(5)手册

/proc/PID/maps文件中每行包含的列,从左至右含义如下:

  1. 一对用连字符隔开的数字,分别表示内存段被映射到的虚拟地址范围(以十六进制表示)。
  2. 内存段的保护位和标记位。前三个字母表示段的保护位(r读/w写/x执行),-表示禁用相应的保护位,最后一个字母表示内存段的映射标记(p私有/s共享),例如,System V的共享内存段总是被标记为共享。
  3. 段在对应的映射文件中的十六进制偏移量(以字节计数)。
  4. 相应的映射文件所位于的设备的设备号(主要和次要ID)。
  5. 映射文件的i-node号,或者System V共享内存段的标识符。
  6. 与这个共享内存段相关联的文件名,或者其他标识标签。
root@gerryyang:/home/gerryyang/go# uname -a
Linux gerryyang 3.13.0-37-generic #64-Ubuntu SMP Mon Sep 22 21:28:38 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
root@gerryyang:/home/gerryyang/go# cat /proc/15937/maps 
00400000-00406000 r-xp 00000000 fd:01 1312807                            /home/gerryyang/go/http_svr
00406000-00407000 rw-p 00005000 fd:01 1312807                            /home/gerryyang/go/http_svr
00734000-00755000 rw-p 00000000 00:00 0                                  [heap]
c000000000-c000001000 rw-p 00000000 00:00 0 
c20fff0000-c210100000 rw-p 00000000 00:00 0 
7ff6c8a99000-7ff6c8e6d000 rw-p 00000000 00:00 0 
7ff6c8e6d000-7ff6c8f42000 r--p 0009a000 fd:01 942954                     /usr/lib/x86_64-linux-gnu/libgo.so.5.0.0
7ff6c8f42000-7ff6c9047000 r-xp 00000000 fd:01 404939                     /lib/x86_64-linux-gnu/libm-2.19.so
7ff6c9047000-7ff6c9246000 ---p 00105000 fd:01 404939                     /lib/x86_64-linux-gnu/libm-2.19.so
7ff6c9246000-7ff6c9247000 r--p 00104000 fd:01 404939                     /lib/x86_64-linux-gnu/libm-2.19.so
7ff6c9247000-7ff6c9248000 rw-p 00105000 fd:01 404939                     /lib/x86_64-linux-gnu/libm-2.19.so
7ff6c9248000-7ff6c9261000 r-xp 00000000 fd:01 404944                     /lib/x86_64-linux-gnu/libpthread-2.19.so
7ff6c9261000-7ff6c9460000 ---p 00019000 fd:01 404944                     /lib/x86_64-linux-gnu/libpthread-2.19.so
7ff6c9460000-7ff6c9461000 r--p 00018000 fd:01 404944                     /lib/x86_64-linux-gnu/libpthread-2.19.so
7ff6c9461000-7ff6c9462000 rw-p 00019000 fd:01 404944                     /lib/x86_64-linux-gnu/libpthread-2.19.so
7ff6c9462000-7ff6c9466000 rw-p 00000000 00:00 0 
7ff6c9466000-7ff6c9621000 r-xp 00000000 fd:01 404955                     /lib/x86_64-linux-gnu/libc-2.19.so
7ff6c9621000-7ff6c9821000 ---p 001bb000 fd:01 404955                     /lib/x86_64-linux-gnu/libc-2.19.so
7ff6c9821000-7ff6c9825000 r--p 001bb000 fd:01 404955                     /lib/x86_64-linux-gnu/libc-2.19.so
7ff6c9825000-7ff6c9827000 rw-p 001bf000 fd:01 404955                     /lib/x86_64-linux-gnu/libc-2.19.so
7ff6c9827000-7ff6c982c000 rw-p 00000000 00:00 0 
7ff6c982c000-7ff6c9842000 r-xp 00000000 fd:01 393365                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7ff6c9842000-7ff6c9a41000 ---p 00016000 fd:01 393365                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7ff6c9a41000-7ff6c9a42000 rw-p 00015000 fd:01 393365                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7ff6c9a42000-7ff6ca390000 r-xp 00000000 fd:01 942954                     /usr/lib/x86_64-linux-gnu/libgo.so.5.0.0
7ff6ca390000-7ff6ca590000 ---p 0094e000 fd:01 942954                     /usr/lib/x86_64-linux-gnu/libgo.so.5.0.0
7ff6ca590000-7ff6ca84b000 rw-p 0094e000 fd:01 942954                     /usr/lib/x86_64-linux-gnu/libgo.so.5.0.0
7ff6ca84b000-7ff6ca898000 rw-p 00000000 00:00 0 
7ff6ca898000-7ff6ca8bb000 r-xp 00000000 fd:01 404952                     /lib/x86_64-linux-gnu/ld-2.19.so
7ff6ca8e5000-7ff6ca8e6000 ---p 00000000 00:00 0 
7ff6ca8e6000-7ff6ca8fb000 rw-p 00000000 00:00 0                          [stack:15939]
7ff6ca8ff000-7ff6ca904000 rw-p 00000000 00:00 0                          [stack:15937]
7ff6ca904000-7ff6ca905000 ---p 00000000 00:00 0 
7ff6ca905000-7ff6ca932000 rw-p 00000000 00:00 0                          [stack:15938]
7ff6ca932000-7ff6ca939000 r--p 00010000 fd:01 404955                     /lib/x86_64-linux-gnu/libc-2.19.so
7ff6ca93b000-7ff6ca940000 r--p 0001e000 fd:01 404944                     /lib/x86_64-linux-gnu/libpthread-2.19.so
7ff6ca940000-7ff6ca943000 rw-p 00000000 00:00 0 
7ff6ca946000-7ff6ca947000 r--p 00000000 fd:01 404952                     /lib/x86_64-linux-gnu/ld-2.19.so
7ff6ca947000-7ff6ca948000 rw-p 00000000 00:00 0 
7ff6ca948000-7ff6ca949000 r--p 00001000 fd:01 393365                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7ff6ca949000-7ff6ca94b000 r--p 00003000 fd:01 404939                     /lib/x86_64-linux-gnu/libm-2.19.so
7ff6ca94b000-7ff6ca94d000 r--p 00008000 fd:01 1312807                    /home/gerryyang/go/http_svr
7ff6ca94d000-7ff6caab2000 rw-p 00000000 00:00 0 
7ff6caab4000-7ff6caab7000 r--p 00005000 fd:01 1312807                    /home/gerryyang/go/http_svr
7ff6caab7000-7ff6caaba000 rw-p 00000000 00:00 0 
7ff6caaba000-7ff6caabb000 r--p 00022000 fd:01 404952                     /lib/x86_64-linux-gnu/ld-2.19.so
7ff6caabb000-7ff6caabc000 rw-p 00023000 fd:01 404952                     /lib/x86_64-linux-gnu/ld-2.19.so
7ff6caabc000-7ff6caabd000 rw-p 00000000 00:00 0 
7fff29080000-7fff290a1000 rw-p 00000000 00:00 0 
7fff290ad000-7fff290af000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

在共享内存中存储指针

每个进程都可能会用到不同的共享库和内存映射,并且可能会附加不同的共享内存段集。因此,如果遵循推荐的做法,让内核来选择将共享内存段附加到何处,那么一个段在各个进程中可能会被附加到不同的地址上。因为这个原因,在共享内存段中存储指向段中其他地址的引用时应该使用(相对)偏移量,而不是(绝对)指针。

pic

例如,假设一个共享内存段的起始地址为baseaddr(shmat()的返回值),再假设需要在p指向的位置处存储一个指针,该指针指向的位置与target指向的位置相同:

*p = target;    // Place pointer in *p, Wrong !

原因解释:
这段代码存在问题,因为当共享内存段被附加到另一个进程中时,target指向的位置可能会位于一个不同的虚拟地址处,这意味着在那个进程中,在*p中的值是无意义的。

正确的做法:

// 在`*p`中存储一个偏移量
*p = (target - baseaddr);   // Place offset in *p, OK
// 在解引用这种指针时需要颠倒上面的步骤
target = baseaddr + *p      // Interpret offset

共享内存关联的数据结构

每个共享内存段都有一个关联的shmid_ds数据结构:

struct shmid_ds {
    struct ipc_perm shm_perm;  // ownership and permissions
    size_t shm_segsz;          // size of segment in bytes
    time_t shm_atime;          // time of last shmat()
    time_t shm_dtime;          // time of last shmdt()
    time_t shm_ctime;          // time of last change
    pid_t shm_cpid;            // pid of creator
    pid_t shm_lpid;            // pid of last shmat()/shmdt()
    shmatt_t shm_nattch;       // number of currently attached processes
}

SUSv3要求实现提供上面给出的所有字段,其他一些UNIX实现在shmid_ds结构中包含了额外的非标准字段。各种共享内存系统调用会隐士地更新shmid_ds结构中的字段,使用shmctl() IPC_SET操作可以显示地更新shm_perm字段中的特定子字段。

共享内存的限制

大多数UNIX实现会对System V共享内存施加各种各样的限制,下面是Linux共享内存的限制列表:

root@gerryyang:/proc/sys/kernel# cat shmmni
4096
root@gerryyang:/proc/sys/kernel# cat shmmin
cat: shmmin: No such file or directory
root@gerryyang:/proc/sys/kernel# cat shmmax
33554432
root@gerryyang:/proc/sys/kernel# cat shmall
2097152

SHMMNI

这个是一个系统级别的限制,它限制了所能创建的共享内存标识符(共享内存段)的数量。

SHMMIN

这是一个共享内存段的最小大小(字节数),这个限制的值被定义成了1(无法修改这个值),但实际的限制是系统分页大小(shmget(),EINVAL)

SHMMAX

这是一个共享内存段的最大大小(字节数),SHMMAX的实际上限依赖于可用的RAM和交换空间(shmget(), EINVAL)

SHMALL

这是一个系统级别的限制,它限制了共享内存中的分页总数,其他大多数UNIX实现并没有提供这个限制,SHMALL的实际上限依赖于可用的RAM和交换空间(shmget(), ENOSPC)

Linux特有的SHM_INFO操作返回一个类型为shm_info的结构,它包含了共享内存对象所消耗的实际资源相关的信息。

http://man7.org/tlpi/code/online/dist/svshm/svshm_info.c

总结

共享内存允许两个或多个进程共享内存的同一个分页。通过共享内存交换数据无需内核干涉,一旦一个进程将数据复制进一个共享内存段中之后,数据将会立即对其他进程可见。共享内存是一种快速的IPC机制,尽管这种速度上的提升通常会因为必须要使用某种同步技术(比如,信号量)而被抵消掉一部分。在附加一个共享内存段时推荐的做法是允许内核选择将段附加在进程的虚拟地址空间的某处,这意味着段在不同的进程中的虚拟地址可能是不同的,因为这个原因,所有对段中地址的引用都应该表示成相对偏移量,而不是一个绝对指针。

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值