共享内存是所有IPC方式中最快的一种,原因在于共享内存一旦映射到进程地址空间,进程间数据的传递就不需要涉及内核。
对于管道、FIFO和消息队列,两个进程之间通过这三种方式进行通信,则内核就扮演着“中转站”的角色。
——发送消息一方,通过系统调用(write或msgsnd)将消息从用户层拷贝到内核层,由内核暂时保存这份信息;
——接受消息的一方,通过系统调用(read或msgrcv)将消息从内核层提取到用户层;
注意:在使用read/write时,操作系统还会将数据缓存到临时缓冲区内。
我们在来看共享内存:
内核负责构建出一片内存区域,两个或多个进程可以将这块内存区域映射到自己的虚拟地址空间,从此之后内核不再参与双方通信。
注意:建立共享内存之后,内核并不是完全不参与进程间的通信,因为当进程使用共享内存时,可能会发生缺页,引发缺页中断,这种情况下,内核还是会参与进来的。
一般情况下,允许多个进程同时操作共享内存,就不得不防范竞争条件的出现,比如有两个进程同时执行写操作将会导致数据的不确定性,或者一个进程在执行读取操作时,另外一个进程正在执行更新操作。因此,共享内存这种进程间通信的手段通常不会单独出现,总是和信号量、文件锁等同步的手段配合使用。
共享内存
共享内存空间有自己特定的数据结构,包括访问权限、大小以及最近访问时间等;
它的结构体定义在:/usr /src/kernels/2.6.32-431.el6.i686/include/Linux/shm.h中。如下:
操作系统提供给用户看的结构体:
内核中的:
——shm_perm表示kern_ipc_perm数据结构
——shm_file共享段特殊文件
——shm_nattch当前附加的内存区数
——shm_segsz内存区字节数
——shm_atim最后访问时间
——shm_dtim最后分离时间
——shm_ctim最后修改时间
——shm_cprid创建者pid
——shm_lprid最后访问进程的pid
——mlock_user锁定在共享内存RAM中的用户的user_struct描述符指针
由于共享内存会占用大量的内存空间,因此,操作系统对共享内存做了限制:
在/usr/include/linux/shm.h可以看到:
——SHMMAX表示一个共享内存段的最大字节数 为32MB;
——SHMMIN表示一个共享内存段的最小字节数 为1B;
——SHMMNI表示系统所能创建的共享内存的最大个数 为4096;
——SHMALL表示系统中共享内存的分页总数 为2M个页即可表示所有共享段的总字节数为2M*4KB=8GB;
——SHMSEG表示一个进程允许attach的共享内存段的最大个数,可以看到系统默认和SHMMNI一样;
我们可以通过/proc/sys/kernel/shmmni、cat /proc/sys/kernel/shmmax和/proc/sys/kernel/shmall查看和修改他们:
共享内存的操作
创建或打开——shmget函数
功能:创建或打开共享内存
原型:int shmget(key_t key, size_t size, int shmflg);
参数:key表示共享内存段的名字;
size表示共享内存大小;
shmflg标识共享内存段的创建标识;
——一般常用两个:①创建:IPC_CREAT|0644②打开:0
返回值:成功返回一个非负整数,即该共享内存段的标识符;失败返回-1;
共享内存挂载—— shmat函数
功能:将共享内存挂载到进程自己的地址空间下
原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:shmid表示共享内存的标识即shmget得到的id;
shmaddr表示挂载到虚拟地址空间的位置,一般置NULL,内核会帮我们找到位置;
shmflg用来指定共享内存段的访问权限和映射条件;
——flag有以下几个值:
但是我们一般将flag设置为0表示具有读写权限;
返回值:成功返回一个指针,指向共享内存段的第一个节; 失败返回-1;
共享内存卸载——shmdt函数
功能:卸载但并不删除共享内存段即只是将共享内存段与当前进程脱离;
原型:int shmdt(const void *shmaddr);
参数:shmadder由shmat返回的指针;
返回值:成功返回0; 失败返回-1;
注意:shmdt 函数仅仅是使进程和共享内存脱离关系,并未删除共享内存。 shmdt 函数的作用是将共享内存的引用计数减 1 。只有共享内存的引用计数为 0 时,调用 shmctl 函数的 IPC_RMID 命令才会真正地删除共享内存。
进程执行 exec 之后,所有 attach 的共享内存都会被分离。当进程终止之后,共享内存也会自动被分离。
共享内存控制——shmctl函数
功能:用于控制共享内存包括读取状态、设置状态和删除操作;
原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:shmid表示共享内存的标识即shmget得到的id;
cmd表示将要执行的操作,包括IPC_RMID、IPC_SET、IPC_STAT和IPC_INFO;
这些操作可以在/usr/include/linux/ipc.h中看到:
对于IPC_STAT,用于获取 shmid 对应的共享内存的信息。所谓信息,就是下面结构体的内容:
可以看到,在该结构体中有一个shm_perm,我们可以看看该结构体内容:
在shm_perm中有一个mode字段,该有两个比较特殊的标志位,即 SHM_DEST 和 SHM_LOCKED :
删除共享内存时,可能由于 attach 它的进程个数不为 0 ,因此只能打上一个标记,表示标记删除,待到所有 attach 该共享内存的进程都执行过分离( detach )操作,共享内存的引用计数变成 0 之后,才执行真正的删除操作。所谓的标记指的就是 SHM_DEST 标志位。
对于已经标记删除的共享内存,可以通过 ipcs-m 命令的 status 栏来查看,其 dest 含义是已经标记删除的意思。
可以通过 shmctl 的 SHM_LOCK 操作将一个共享内存段锁入内存,这样它就不会被置换出去。这样做的好处是访问共享内存的时候,不会产生缺页中断( page fault )。
通过 ipcs-m 的输出可以查看共享内存是否被锁入内存,注意下面状态中的 locked 字段,该字段表明对应的共享内存已被锁入内存。
对于IPC_SET:IPC_SET 也只能修改 shm_perm 中的 uid 、 gid 及 mode 。
进行删除操作的话,选用IPC_RMID;但是需要注意的是:如果共享内存的引用计数 shm_nattch 等于 0 ,则可以立即删除共享内存。但是如果仍然存在进程attach 该共享内存,则并不执行真正的删除操作,而仅仅是设置 SHM_DEST 标记。待所有进程都执行过分离操作之后,再执行真正的删除操作。共享内存处于 SHM_DEST 状态的情况下,依然允许新的进程调用 shmat 函数来 attach该共享内存。
当然,如果是超级用户,还可以有两个操作:SHM_LOCK和SHM_UNLOCK
buf表示指向一个保存着共享内存的模式状态和访问权限的数据结构,结构体如下:
返回值:成功返回0;失败返回-1;
我们可以在命令行中使用:
ipcs -m查看共享内存
ipcrm -M key删除共享内存段
测试案例:(没有使用信号量或文件锁等手段进行同步使用,后面的文章中会写一个测试用例,本章着重讲解它的一些基础概念)
①先写一个create.c用来创建共享内存;
②再写一个at.c旨在打开创建的共享内存并向其中写入数据;
③最后写一个read.c用来读取共享内存中的数据;
creat.c:
at.c:
read.c:
测试结果:
当我们执行create.c后,使用ipcs -m可以看到
bytes表示创建的大小为36字节;
nattch表示连接到共享内存的进程数;
执行read.ch
本篇文章只是大体上的简单介绍了共享内存的些许基础概念,并没有深入探讨,文章中如有错误的地方,欢迎大家指正,不胜感激!