页次: 1
共享内存允许两个或多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间内存的一部分,因此这种IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。。速度比起管道或者消息队列更快
另一方面,共享内存这种IPC机制不由内核控制意味着需要通过某些同步方法使得不会出现同时访问共享内存的情况。System V信号量就是天生用来实现这种同步的方法。当然,还可以使用其他方法,比如POSIX信号量和文件锁
在 mmap()术语中,一块内存区域会被映射到一个地址,而在 System V 术语中,一个共享内存段是被附加到一个地址上的。这些术语是等价的,它们在术语上之所以存在差异是因为这两组 API 的起源不同
概述
为使用一个共享内存段通常需要执行下面的步骤:
调用shmget()创建一个新共享内存段或者取得一个既有共享内存段的标识符。
使用shmat()来附上共享内存段,即令该段成为调用进程的虚拟内存的一部分
此时在程序中就可以向对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat()调用返回的addr值。它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针
调用shmdt()来分离这个共享段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步
调用shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会被销毁。只有一个进程需要执行这一步。
shmget:创建和打开一个共享内存段
shmget()系统调用创建一个新共享内存或获取一个既有段的标识符。新创建的内存段中的内容会被初始化为0:
NAME
shmget - allocates a System V shared memory segment
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg); shmflg:
包含9个比特的权限标志,它们的作用与创建文件时使用的mode标志是一样。
权限标志对共享内存非常有用,因为它允许一个进程创建的共享内存可以被共享内存的创建者所拥有的进程写入,同时其它用户创建的进程只能读取共享内存。我们可以利用这个功能来提供一种有效的对数据进行只读访问的方法,通过将数据放共享内存并设置它的权限,就可以避免数据被其他用户修改。
可以对下列标记中的零个或多个取 OR 来控制 shmget()的操作
IPC_CREAT :如果不存在与指定的 key 对应的段,那么就创建一个新的。
IPC_EXCL :如果同时指定了IPC_CREAT 并且与指定的key 对应的段已经存在,那么返回EEXIST 错误
SHM_HUGETLB(自 Linux 2.6 起):
特权(CAP_IPC_LOCK)进程能够使用这个标记创建一个使用巨页(huge page)的共享内存段。
巨页是很多现代硬件架构提供的一项特性用来管理使用超大分页尺寸的内存。(如 x86-32 允许使用 4MB 的分页大小来替代 4KB 的分页大小。)
在那些拥有大量内存的系统上并且应用程序需要大量内存块时,使用巨页可以降低硬件内存管理单元的超前转换缓冲区器(translation look-aside buffer,TLB)的数量
SHM_NORESERVE(自 Linux 2.6.15 起):这个标记在 shmget()中所起的作用与MAP_NORESERVE 标记在 mmap()中所起的作用一样
返回值:
创建成功,则返回一个非负整数,即共享内存标识;
共享内存标识,它唯一的标识了一个IPC对象,这个IPC对象可以是消息队列或信号量或共享内存中的任意一种类型(IPC对象是活动在内核级别的一种进程间通信的工具)
这个标识符是一个非负整数,在Linux系统中标识符被声明成整数,所以可能存在的最大标识符为65535。
这里标识符与文件描述符有所不同,使用open函数打开一个文件时,返回的文件描述符的值为当前进程最小可用的文件描述符数组的下标。IPC对象删除或创建时相应的标识符的值会不断增加到最大的值,归零循环分配使用。
如果失败,则返回-1.
创建完毕之后,我们可以通过 ipcs 命令查看这个共享内存。
#ipcs --shmems
------ Shared Memory Segments ------
key shmid owner perms bytes nattch status
0x00000000 19398656 marc 600 1048576 2 dest
shmat:加载共享内存
NAME
shmat, shmdt - System V shared memory operations
SYNOPSIS
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg); 如果一个进程想要访问这一段共享内存,需要将这个内存加载到自己的虚拟地址空间的某个位置,通过 shmat 函数,就是 attach 的意思
将共享内存端挂载到自己地址空间
第一次创建共享内存段时,它不能被任何进程访问。要想启动对该内存的访问,必须将其连接到一个进程的地址空间
参数:
shmid : 是由shmget函数返回的共享内存标识。
shmaddr :
shmaddr 就是要指定 attach 到这个地方。
但是这个地址的设定难度比较大,除非对于内存布局非常熟悉,否则可能会 attach 到一个非法地址。
所以,通常的做法是将 shmaddr 设为 NULL,让内核选一个合适的地址。
shmflg : 是一组标志位,通常为0。它还可取:
SHM_RND,用以决定是否将当前共享内存段连接到指定的shmaddr上。该参数和shm_addr联合使用,用来控制共享内存连接的地址,除非只计划在一种硬件上运行应用程序,否则不要这样指定。填0让操作系统自己选择是更好的方式。
SHM_RDONLY:
如果设置了,那么标识共享内存只读访问。试图更新只读段中的内容会导致段错误(SIGSEGV 信号)的发生。
否则以读写方式连接此内存段
SHM_REMAP。在指定了这个标记之后shmaddr的值必须为非NULL。
这个标记要求 shmat()调用替换起点在 shmaddr 处长度为共享内存段的长度的任何既有共享内存段或内存映射。
一般来讲,如果试图将一个共享内存段附加到一个已经在用的地址范围时将会导致 EINVAL 错误的发生。
SHM_REMAP 是一个非标准的 Linux 扩展
返回值:
成功:真正被 attach 的地方。
开发人员可以像对待普通的 C 指针那样对待这个值,段与进程的虚拟内存的其他部分看起来毫无差异。
通常会将 shmat()的返回值赋给一个指向某个由程序员定义的结构的指针以便在该段上设定该结构。
失败返回NULL
下表对 shmat()的 shmflg 参数中能取 OR 的常量进行了总结
值 描 述
SHM_RDONLY 附加只读段
SHM_REMAP 替换位于 shmaddr 处的任意既有映射
SHM_RND 将 shmaddr 四舍五入为 SHMLBA 字节的倍数
shmdt:解除绑定共享内存
NAME
shmat, shmdt - System V shared memory operations
SYNOPSIS
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr); 如果共享内存使用完毕,可以通过 shmdt 解除绑定
当一个进程不再需要访问一个共享内存段时就可以调用shmdt()来讲该段分离出其虚拟地址空间了。
注意:仅仅是共享内存分离但并未删除它,其标识符及其相关数据结构都在;直到某个进程的IPC_RMID命令的调用shmctl特地删除它为止,
shmaddr参数标识出了待分离的段,它应该是之前由shmat()调用返回的一个值。
返回:
成功时,返回0
失败时,返回-1.
通过fork()创建的子进程会继承其父进程附加的共享内存段。因此,共享内存为父进程和子进程之间的通信提供了一种简单的IPC方法。
在一个exec()中,所有附加的共享内存段都会被分离。在进程终止后共享内存段也会自动被分离
共享内存在虚拟内存中的位置
段被附加在向上增长的堆和向下增长的栈之间未被分配的空间中。为给栈和堆的增长腾出空间,附加共享内存段的虚拟地址从0x4000000开始。内存映射和共享库也是被放置在这个区域中的(共享内存映射和内存段默认被放置的位置可能会有些不同,这依赖于内核版本和进程的 RLIMIT_STACK 资源限制的设置。)
通过 Linux 特有的/proc/PID/maps 文件能够看到一个程序映射的共享内存段和共享库的位置
从内核 2.6.14 开始,Linux 还提供了/proc/PID/smaps 文件,它给出了有关一个进程中各个映射的内存消耗方面的更多信息。更多细节可参考 proc(5)手册
在共享内存中存储指针
每个进程可能会用到不同的共享库和内存映射,并且可能附加不同的共享内存段集。因此如果遵循推荐的做法,让内核来选择将共享内存段附加到何处,那么一个段在各个进程中可能会被附加到不同的地址上。正因为这个原因,在共享内存段中存储指向段中其他地址的引用时应该使用(相对)偏移量,而不是(绝对)指针
例如,,假设一个共享内存段的起始地址为 baseaddr(即baseaddr 的值为 shmat()的返回值)。再假设需要在 p 指向的位置处存储一个指针,该指针指向的位置与 target 指向的位置相同。如下图所示:
那么设置*p:
*p = target; //error
这段代码存在的问题是当共享内存段被附加到另一个进程中时target指向的位置可能会位于一个不同的虚拟地址处,这意味着在那个进程中那个策划中存储在p 中的值是是无意义的。正确的做法是在p 中存储一个偏移量,如下所示。
*p = (target - baseaddr);
在解引用这种指针时需要颠倒上面的步骤
target = baseaddr + *p;
shmctl:共享内存控制操作
shmctl()系统调用在shmid标识的共享内存段上执行一组控制操作:
NAME
shmctl - System V shared memory control
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf); cmd参数规定了待执行的控制操作:
(1)常规控制操作
IPC_RMID:删除共享内存(如果共享内存不需要用了,那么就一定要去主动去删除它)
标记这个共享内存段以及其关联的shmid_ds数据结构以便删除。
如果当前没有进程附加该段,那么就会执行删除操作,否则就在所有进程都已经与该段分离(即当 shmid_ds 数据结构中 shm_nattch字段的值为 0 时)之后再执行删除操作
在 Linux 上,如果已经使用 IPC_RMID 将一个共享段标记为删除,但因为还存在一些进程仍然附加了该段而没有删除该段,那么其他进程还能够附加该段。但这种行为是不可移植的:
大多数 UNIX 实现会阻止进程将被标记为删除的段附加到自己的地址空间中。(SUSv3 并没有对这种情况的处理方式进行规定。)一些 Linux 应用程序已经依赖了这种行为,这也是 Linux 为何不改变这种行为以与其他 UNIX 实现匹配的原因
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
IPC_SET :如果进程有足够的权限就把共享内存的当前关联值设置为shmid_ds结构中给出的值
(2)加锁和解锁共享内存
一个共享内存段可以被锁进RAM中,这样它永远不会被交换出去了。这种做法能带来性能上的提升,因为一旦段中的所有分页都驻留在内存中,就能确保一个应用程序在访问分页时永远不会因发生分页故障而被延迟。通过shmctl()可以完成两种锁操作
SHM_LOCK 操作将一个共享内存段锁进内存
SHM_UNLOCK 操作为共享内存段解锁以允许它被交换出去。
在版本号小于 2.6.10 的 Linux 上只有特权(CAP_IPC_LOCK)进程才能够将一个共享内存段锁进内存。自 Linux 2.6.10 开始,非特权进程能够在一个共享内存段上执行加锁和解锁操作,其前提是进程的有效用户 ID 与段的所有者或创建者的用户 ID 匹配并且(在执行 SHM_LOCK 操作的情况下)进程具备足够高的 RLIMIT_MEMLOCK 资源限制
作为给内存加锁的一种替代方法,可以使用 mlock()
buf:
是一个指针,包含共享内存模式和访问权限的结构。
buf 参数是 IPC_STAT 和 IPC_SET 操作会用到的,并且在执行其他操作时需要将这个参数的值指定为 NULL
共享内存关联数据结构
每个共享内存段都有一个关联的 shmid_ds 数据结构,其形式如下。
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation permission struct */
size_t shm_segsz; /* size of segment in bytes */
__time_t shm_atime; /* time of last shmat() */
#ifndef __x86_64__
unsigned long int __unused1;
#endif
__time_t shm_dtime; /* time of last shmdt() */
#ifndef __x86_64__
unsigned long int __unused2;
#endif
__time_t shm_ctime; /* time of last change by shmctl() */
#ifndef __x86_64__
unsigned long int __unused3;
#endif
__pid_t shm_cpid; /* pid of creator */
__pid_t shm_lpid; /* pid of last shmop */
shmatt_t shm_nattch; /* number of current attaches */
__syscall_ulong_t __unused4;
__syscall_ulong_t __unused5;
};SUSv3 要求实现提供上面给出的所有字段。其他一些 UNIX 实现在 shmid_ds 结构中包含了额外的非标准字段。
各种共享内存系统调用会隐式地更新 shmid_ds 结构中的字段,使用 shmctl() IPC_SET 操作可以显式地更新 shm_perm 字段中的特定子字段
共享内存的限制
大多数 UNIX 实现会对 System V 共享内存施加各种各样的限制。下面是一份 Linux 共享内存的限制列表。括号中列出了当限制达到时受影响的系统调用及其返回的错误。
SHMMNI:这是一个系统级别的限制,它限制了所能创建的共享内存标识符(换句话说是共享内存段)的数量。(shmget(), ENOSPC)
SHMMIN :这个一个共享内存段的最小大小(字节数)。这个限制的值被定义成了 1(无法修改这个值),但实际的限制是系统分页大小(shmget(), EINVAL)。
SHMMAX :这个是一个共享内存段的最大大小(字节数)。SHMMAX 的实际上限依赖于可用的 RAM和交换空间。(shmget(), EINVAL)
SHMALL
这是一个系统级别的限制,它限制了共享内存中的分页总数。其他大多数 UNIX 实现并没有提供这个限制
SHMALL 的实际上限依赖于可用的 RAM 和交换空间。(shmget(), ENOSPC)
其他一些 UNIX 实现还施加了下列限制(Linux 并没有实现这些限制)。
SHMSEG :这个是进程级别的限制,它限制了所能附加的共享内存段数量。
实验
shmget怎么使用?
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
#include <string.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <mqueue.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
static void usageError(const char *progName, const char *msg)
{
if (msg != NULL)
fprintf(stderr, "%s", msg);
fprintf(stderr, "Usage: %s [-cx] {-f pathname | -k key | -p} "
"seg-size [octal-perms]\n", progName);
fprintf(stderr, " -c Use IPC_CREAT flag\n");
fprintf(stderr, " -x Use IPC_EXCL flag\n");
fprintf(stderr, " -f pathname Generate key using ftok()\n");
fprintf(stderr, " -k key Use 'key' as key\n");
fprintf(stderr, " -p Use IPC_PRIVATE key\n");
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[])
{
/* Parse command-line options and arguments */
int numKeyFlags = 0; /* Counts -f, -k, and -p options */
int flags = 0;
long lkey;
key_t key;
int opt; /* Option character from getopt() */
while ((opt = getopt(argc, argv, "cf:k:px")) != -1) {
switch (opt) {
case 'c':
flags |= IPC_CREAT;
break;
case 'f': /* -f pathname */
key = ftok(optarg, 1);
if (key == -1){
printf("ftok");
exit(EXIT_FAILURE);
}
numKeyFlags++;
break;
case 'k': /* -k key (octal, decimal or hexadecimal) */
if (sscanf(optarg, "%li", &lkey) != 1){
printf("-k option requires a numeric argument\n");
exit(EXIT_FAILURE);
}
key = lkey;
numKeyFlags++;
break;
case 'p':
key = IPC_PRIVATE;
numKeyFlags++;
break;
case 'x':
flags |= IPC_EXCL;
break;
default:
usageError(argv[0], NULL);
}
}
if (numKeyFlags != 1)
usageError(argv[0], "Exactly one of the options -f, -k, "
"or -p must be supplied\n");
if (optind >= argc)
usageError(argv[0], "Size of segment must be specified\n");
int segSize = atoi(argv[optind]);
unsigned int perms = (argc <= optind + 1) ? (S_IRUSR | S_IWUSR) :
atoi(argv[optind + 1]);
int shmid = shmget(key, segSize, flags | perms);
if (shmid == -1){
perror("shmget");
exit(EXIT_FAILURE);
}
printf("%d\n", shmid); /* On success, display shared mem. id */
exit(EXIT_SUCCESS);
}shmat
// svshm_attach.c
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
#include <string.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <mqueue.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
static void
usageError(char *progName)
{
fprintf(stderr, "Usage: %s [shmid:address[rR]]...\n", progName);
fprintf(stderr, " r=SHM_RND; R=SHM_RDONLY\n");
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[])
{
printf("SHMLBA = %ld (%#lx), PID = %ld\n",
(long) SHMLBA, (unsigned long) SHMLBA, (long) getpid());
for (int j = 1; j < argc; j++) {
char *p;
int shmid = strtol(argv[j], &p, 0);
if (*p != ':')
usageError(argv[0]);
void *addr = (void *) strtol(p + 1, NULL, 0);
int flags = (strchr(p + 1, 'r') != NULL) ? SHM_RND : 0;
if (strchr(p + 1, 'R') != NULL)
flags |= SHM_RDONLY;
char *retAddr = (char *)shmat(shmid, addr, flags);
if (retAddr == (void *) -1){
printf("shmat: %s", argv[j]);
exit(EXIT_FAILURE);
}
printf("%d: %s ==> %p\n", j, argv[j], retAddr);
}
printf("Sleeping 5 seconds\n");
sleep(5);
exit(EXIT_SUCCESS);
}
shmctl
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
#include <string.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <mqueue.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <time.h>
static void
printShmDS(const struct shmid_ds *ds)
{
printf("Size: %ld\n", (long) ds->shm_segsz);
printf("# of attached processes: %ld\n", (long) ds->shm_nattch);
printf("Mode: %lo",
(unsigned long) ds->shm_perm.mode);
#ifdef SHM_DEST
printf("%s", (ds->shm_perm.mode & SHM_DEST) ? " [DEST]" : "");
#endif
#ifdef SHM_LOCKED
printf("%s", (ds->shm_perm.mode & SHM_LOCKED) ? " [LOCKED]" : "");
#endif
printf("\n");
printf("Last shmat(): %s", ctime(&ds->shm_atime));
printf("Last shmdt(): %s", ctime(&ds->shm_dtime));
printf("Last change: %s", ctime(&ds->shm_ctime));
printf("Creator PID: %ld\n", (long) ds->shm_cpid);
printf("PID of last attach/detach: %ld\n", (long) ds->shm_lpid);
}
int main(int argc, char *argv[])
{
if (argc != 2 || strcmp(argv[1], "--help") == 0){
printf("%s shmid\n", argv[0]);
exit(EXIT_FAILURE);
}
struct shmid_ds ds;
if (shmctl(atoi(argv[1]), IPC_STAT, &ds) == -1){
printf("shmctl");
exit(EXIT_FAILURE);
}
printShmDS(&ds);
exit(EXIT_SUCCESS);
}// svshm_info.c
#define _GNU_SOURCE
#include <sys/shm.h>
int
main(int argc, char *argv[])
{
struct shm_info info;
int s = shmctl(0, SHM_INFO, (struct shmid_ds *) &info);
if (s == -1){
perror("shmctl");
exit(EXIT_FAILURE);
}
printf("Maximum ID index = %d\n", s);
printf("shm_tot = %ld\n", (long) info.shm_tot);
printf("shm_rss = %ld\n", (long) info.shm_rss);
printf("shm_swp = %ld\n", (long) info.shm_swp);
printf("swap_attempts = %ld\n", (long) info.swap_attempts);
printf("swap_successes = %ld\n", (long) info.swap_successes);
exit(EXIT_SUCCESS);
} // svshm_lock
int
main(int argc, char *argv[])
{
for (int j = 1; j < argc; j++)
if (shmctl(atoi(argv[j]), SHM_LOCK, NULL) == -1){
perror("shmctl");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}// svshm_rm.c
int
main(int argc, char *argv[])
{
for (int j = 1; j < argc; j++)
if (shmctl(atoi(argv[j]), IPC_RMID, NULL) == -1){
perror("shmctl");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
// svshm_unlock 解锁
int
main(int argc, char *argv[])
{
for (int j = 1; j < argc; j++)
if (shmctl(atoi(argv[j]), SHM_UNLOCK, NULL) == -1){
perror("shmctl");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}两进程间交换数据
用共享内存来两进程间交换数据,比如交换一个结构体
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "memory.h"
typedef struct Stu{
int age;
char name[10];
}Stu;
int main(int argc,char *argv[]){
Stu s;
strcpy(s.name, "jack");
//创建共享内存段
int id = shmget(1234, 8, IPC_CREAT | 0644);
if (id == -1){
perror("shmget");
exit(1);
}
//挂载到进程的地址空间
Stu *p = (Stu *)shmat(id, nullptr, 0);
int i = 0;
while (1){
s.age = i++;
memcpy(p, &s, sizeof(Stu)); //写到共享段中
sleep(2);
}
return 0;
} #include <iostream>
#include <string>
#include "unistd.h"
#include <sys/ipc.h>
#include <sys/shm.h>
typedef struct Stu{
int age;
char name[10];
}Stu;
int main() {
int id = shmget(1234, 8, 0);
if (id == -1){
perror("shmget");
exit(1);
}
//挂载到进程的地址空间
Stu *p = (Stu *)shmat(id, nullptr, 0);
while (1){
std::cout << "age = " << p->age << ", name = " << p->name << "\n";
sleep(2);
}
return 0;
}如果进程中的数据改变了,另一个进程只要主动去读了会马上知道。
总结
共享内存允许两个或者多个进程共享内存的同一个分页。通过共享内存交换数据无需内核干涉。一旦一个进程将数据复制进一个共享内存段之后,数据将会立即对其他进程可见。
共享内存是一种快速的 IPC 机制,尽管这种速度上的提升通常会因必须要使用某种同步技术而被抵消掉一部分,如使用一个 System V 信号量来同步对共享内存的访问。
在附加一个共享内存段时推荐的做法是允许内核选择将段附加在进程的虚拟地址空间的何处。这意味着段在不同进程中虚拟地址可能是不同的。正因为这个原因,所有对段中地址的引用都应该表示成为相对偏移量,而不是一个绝对指针
离线
页次: 1