共享内存是效率最高的 IPC ,因为他抛弃了内核这个“代理人”,直截了当地将一块裸 露的内存放在需要数据传输的进程面前,让他们自己搞,这样的代价是:这些进程必须小心 谨慎地操作这块裸露的共享内存,做好诸如同步、互斥等工作,毕竟现在没有人帮他们来管 理了,一切都要自己动手。也因为这个原因,共享内存一般不能单独使用,而要配合信号量、 互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外。
共享内存的思想很朴素,进程与进程之间虚拟内存空间本来相互独立,不能互相访问的,但是可以通过某些方式,使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这 样的效果就相当于多个进程的虚拟内存空间部分重叠在一起,看示意图:
编写程序时的思维导图如下
主函数
├── 创建共享内存 shmget()
├── 连接共享内存 shmat()
└── 创建信号量 semget()信号量操作函数
├── 初始化信号量 sem_init()
├── P操作 sem_p()
└── V操作 sem_v()循环写入共享内存数据
├── P操作(空间)sem_p(SPACE)
│ └── 信号量操作函数 sem_p()
├── 写入数据到共享内存 memcpy()
│ └── 内存拷贝函数 memcpy()
└── V操作(数据量)sem_v(DATA)
└── 信号量操作函数 sem_v()断开共享内存连接 shmdt()
上述思维导图解释说明:
思维导图将代码分成了几个模块,主要模块包括主函数和信号量操作函数。在主函数中,通过调用相关函数创建共享内存和信号量,然后进入循环写入共享内存数据的过程。循环中先进行 P操作(空间),即空间信号量减1,然后将数据写入共享内存,并进行 V操作(数据量),即数据量信号量加1。循环不断地向共享内存中写入数据,直到程序结束。最后,在退出程序之前需要断开共享内存的连接。 这个思维导图反映了代码的主要逻辑和关键函数调用,对于理解代码的执行流程和实现功能是很有帮助的。
创建一个共享内存需要使用的函数以及使用规范如下:
功能
获取共享内存的 ID
头文件
#include <sys/ipc.h>
#include <sys/shm.h>
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key
共享内存的键值
size
共享内存的尺寸 (PAGE_SIZE 的整数倍)
shmflg
IPC CREAT
_
如果 key 对应的共享内存不存在,则创建之
IPC EXCL
_
如果该 key 对应的共享内存已存在,则报错
SHM HUGETLB
_
使用“大页面”来分配共享内存
SHM NORESERVE
_
不在交换分区中为这块共享内存保留空间
mode
共享内存的访问权限 (八进制,如 0644)
返回值
成功
该共享内存的 ID
失败
- 1
备注
如果 key 指定为为 IPC_PRIVATE,则会自动产生一个随机未用的新键值
上表 函数 shmget( )的接口规范
功能
对共享内存进行映射,或者解除映射
头文件
#include
ys/types.h>#include <sys/shm.h>
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
参数
shmid
共享内存 ID
shmaddr
shmat( )
1 ,如果为 NULL,则系统会自动选择一个合适的虚拟 内存空间地址去映射共享内存。
2,如果不为 NULL,则系统会根据 shmaddr 来选择一 个合适的内存区域。
shmdt( )
共享内存的首地址
shmflg
SHM RDONLY
_
以只读方式映射共享内存
SHM REMAP
_
重新映射,此时 shmaddr 不能为 NULL
SHM RND
_
自动选择比 shmaddr 小的最大页对齐地址
返回值
成功
共享内存的首地址
失败
- 1
备注
无
共享内存的映射和解除映射函数接口规范
1,共享内存只能以只读或者可读写方式映射,无法以只写方式映射
2 ,shmat( )第二个参数 shmaddr 一般都设为 NULL,让系统自动找寻合适的地址。但 当其确实不为空时,那么要求 SHM_RND 在 shmflg必须被设置,这样的话系统将会选择比 shmaddr 小而又最大的页对齐地址 (即为 SHMLBA 的整数倍) 作为共享内存区域的起始地 址。如果没有设置 SHM_RND,那么 shmaddr 必须是严格的页对齐地址。
总之,映射时将 shmaddr 设置为 NULL 是更明智的做法,因为这样更简单,也更具移 植性。
3,解除映射之后,进程不能再允许访问 SHM。
功能
获取或者设置共享内存的相关属性
头文件
#include
s/ipc.h> #include
s/shm.h> 原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid
共享内存 ID
cmd
IPC STAT
_
获取属性信息,放置到buf 中
IPC SET
_
设置属性信息为 buf 指向的内容
IPC RMID
_
将共享内存标记为“即将被删除”状态
IPC INFO
_
获得关于共享内存的系统限制值信息
SHM INFO
_
获得系统为共享内存消耗的资源信息
SHM STAT
_
同 IPC_STAT,但 shmid 为该 SHM 在内核中记录所 有 SHM 信息的数组的下标,因此通过迭代所有的 下标可以获得系统中所有 SHM 的相关信息
SHM LOCK
_
禁止系统将该 SHM 交换至 swap 分区
SHM UNLOCK
_
允许系统将该 SHM 交换至 swap 分区
buf
属性信息结构体指针
返回值
成功
IPC INFO
_
内核中记录所有 SHM 信息的数组的下标最大值
SHM INFO
_
SHM STAT
_
下标值为 shmid 的 SHM 的 ID
失败
- 1
备注
无
上表函数 shmctl( )的接口规范
创建一个共享内存写入数据
#include
#include #include #include #include // 定义信号量编号 #define SPACE 0 #define DATA 1 //定义一个联合体用于semctl()函数的参数 union semun { int val; struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; }; // 初始化信号量 void sem_init(int id, int num,int start_val) { union semun a; // 定义一个联合体变量 a.val = start_val; // 设置联合体变量的值为start_val semctl(id, num, SETVAL, a); // 将联合体变量的值设置给指定的信号量 num } // P操作 void sem_p(int id, int sem_num) { struct sembuf a[1]; // 定义一个sembuf结构体数组 // 设置sembuf结构体的成员变量 a[0].sem_num = sem_num; a[0].sem_op = -1; // 将空间信号量的值减 1 a[0].sem_flg = 0; semop(id, a, 1); // 执行信号量操作 } // V操作 void sem_v(int id, int sem_num) { struct sembuf a[1]; // 定义一个sembuf结构体数组 // 设置sembuf结构体的成员变量 a[0].sem_num = sem_num; a[0].sem_op = 1; // 将数据量信号量的值加 1 a[0].sem_flg = 0; semop(id, a, 1); // 执行信号量操作 } int main() { // 创建共享内存 int shm_id = shmget(ftok("./",1), 2, IPC_CREAT | 0777); //第一个IPC对象,必须是偶数 if(-1 == shm_id) { perror("creat shm failed"); return -1; } char *shm_p = shmat(shm_id, NULL, 0); // 连接共享内存,获取共享内存的指针,shm_p指向共享内存的首地址 // 创建信号量的IPC对象 int sem_id = semget(ftok("./",2), 2, IPC_CREAT | 0777); //第二个IPC对象,创建两个信号量 if(sem_id == -1) { perror("creat sem failed"); return -1; } sem_init(sem_id, SPACE, 1); // 设置空间初始值为1 sem_init(sem_id, DATA, 0); // 设置数据量初始值为0 char *msg = "0123456789"; int i = 0; while(1) { // 空间-1,如果空间信号量的值大于0,则减1;否则阻塞等待 sem_p(sem_id, SPACE); // 将字符写入共享内存 memcpy(shm_p, msg+i, 1); // 数据量+1,将数据量信号量的值加1 sem_v(sem_id, DATA); i = (i+1)%10; } shmdt(shm_p); // 断开共享内存连接 return 0; } 上述代码说明:
这段代码主要是通过共享内存和信号量实现了一个简单的生产者-消费者模型。生产者先申请空间信号量,然后将数据写入共享内存,最后释放数据信号量。消费者则相反,先申请数据信号量,然后读取数据,最后释放空间信号量。生产者和消费者通过信号量的加减操作来实现互斥以及控制共享内存的访问。
读取数据
#include
#include #include #include #include #define SPACE 0 #define DATA 1 union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ }; void sem_init(int id, int num,int start_val) { union semun a; a.val = start_val; semctl(id, num, SETVAL, a);//semctl(sem_id, SPACE, SETVAL, a); //semctl(sem_id, DATA, SETVAL, a); } void sem_p(int id, int sem_num) { struct sembuf a[1]; // unsigned short sem_num; /* semaphore number */ // short sem_op; /* semaphore operation */ // short sem_flg; /* operation flags */ a[0].sem_num = sem_num; a[0].sem_op = -1; a[0].sem_flg = 0; semop(id, a, 1); } void sem_v(int id, int sem_num) { struct sembuf a[1]; // unsigned short sem_num; /* semaphore number */ // short sem_op; /* semaphore operation */ // short sem_flg; /* operation flags */ a[0].sem_num = sem_num; a[0].sem_op = 1; a[0].sem_flg = 0; semop(id, a, 1); } int main() { //创建共享内存 int shm_id = shmget(ftok("./",1), 2, IPC_CREAT | 0777);//第一个IPC对象,必须是偶数 if(-1 == shm_id) { perror("creat shm failed"); return -1; } char *shm_p = shmat(shm_id, NULL, 0); //创建信号量的IPC对象 int sem_id = semget(ftok("./",2), 2, IPC_CREAT | 0777);//第二个IPC对象 if(sem_id == -1) { perror("creat sem failed"); return -1; } sem_init(sem_id, SPACE, 1);//设置空间初始值 sem_init(sem_id, DATA, 0);//设置数据量初始值 char *msg = "0123456789"; int i = 0; while(1) { //fgets(shm_p,msg+i,stdin); //空间+1 p操作(空间) sem_p(SPACE) sem_v(sem_id, SPACE); fprintf(stderr,"%c",*shm_p); //数据量-1 v操作(数据量) sem_V(DATA) sem_p(sem_id,DATA); } shmdt(shm_p); return 0; } 上述代码的实现过程:
上述代码的作用是实现了一个简单的生产者-消费者模型中消费者的功能。代码中使用共享内存和信号量来实现进程间的数据共享和同步。
具体作用如下:
1. 创建一个共享内存区域,大小为2字节。
2. 创建两个信号量,分别用来表示空间和数据量。初始时,空间信号量为1(代表有可用的空间),数据量信号量为0(代表没有数据可用)。
3. 通过循环不断地进行消费操作: a. 通过P操作(sem_p)获取空间信号量,如果空间信号量的值大于0,则表示有空间可用,进入下一步。如果空间信号量的值为0,则会阻塞等待,直到有空间可用。 b. 从共享内存中获取数据,并进行相应的处理/输出。 c. 通过V操作(sem_v)释放数据量信号量,将数据量信号量的值加1,表示一个数据被消费掉。
4. 循环不会结束,消费者会一直进行消费操作,直到程序被终止或手动停止。 总的来说,该代码实现了对共享内存中数据的消费,通过信号量来控制共享内存的读取和空闲空间的管理,实现了进程间的同步和互斥,避免了数据的竞争和不一致性。
使用以上接口需要知道的几点及注意事项:
1,IPC_STAT 获得的属性信息被存放在以下结构体中:
struct shmid_ds
{
struct ipc_perm shm_perm; /* 权限相关信息 */
size_t
time_t
time_t
time_t
shm_segsz; /* 共享内存尺寸 (字节) */
shm_atime; /* 最后一次映射时间 */
shm_dtime; /* 最后一个解除映射时间 */
shm_ctime; /* 最后一次状态修改时间 */
pid_t shm_cpid; /* 创建者 PID *
pid_t shm_lpid; /* 最后一次映射或解除映射者 PID */
shmatt_t shm_nattch;/* 映射该 SHM 的进程个数 */
};
其中权限信息结构体如下:
struct ipc_perm
{
__key; uid; gid; cuid; cgid;
unsigned short __seq; };
/* 该 SHM 的键值 key */
/* 所有者的有效 UID */
/* 所有者的有效 GID */
/* 创建者的有效 UID */
/* 创建者的有效 GID */
/* 读写权限 +
SHM DEST +
SHM LOCKED
/* 序列号 */
2 ,当使用 IPC_RMID 后,上述结构体 struct ipc_perm 中的成员 mode 将可以检测出 SHM_DEST,但 SHM 并不会被真正删除,要等到 shm_nattch 等于 0 时才会被真正删除。 IPC_RMID 只是为删除做准备,而不是立即删除。
3 ,当使用 IPC_INFO 时,需要定义一个如下结构体来获取系统关于共享内存的限制值 信息,并且将这个结构体指针强制类型转化为第三个参数的类型。
struct shminfo
{
unsigned long shmmax; /* 一块 SHM 的尺寸最大值 */
unsigned long shmmin; /* 一块 SHM 的尺寸最小值 (永远为 1) */ unsigned long shmmni; /* 系统中 SHM 对象个数最大值 */ unsigned long shmseg; /* 一个进程能映射的 SHM 个数最大值 */
unsigned long shmall; /* 系统中 SHM 使用的内存页数最大值 */
};
4,使用选项 SHM_INFO 时,必须保证宏_GNU_SOURCE 有效。获得的相关信息被存放 在如下结构体当中:
struct shm_info
{
int used_ids; /* 当前存在的 SHM 个数 */
unsigned long shm_tot; /* 所有 SHM 占用的内存页总数 */ unsigned long shm_rss; /* 当前正在使用的 SHM 内存页个数 */ unsigned long shm_swp; /* 被置入交换分区的 SHM 个数 */ unsigned long swap_attempts; /* 已废弃 */
unsigned long swap_successes; /* 已废弃 */
};
5 ,注意:选项 SHM_LOCK 不是锁定读写权限,而是锁定 SHM 能否与 swap 分区发生 交换。一个 SHM 被交换至 swap 分区后如果被设置了 SHM_LOCK,那么任何访问这个 SHM 的进程都将会遇到页错误。进程可以通过 IPC_STAT 后得到的 mode 来检测 SHM_LOCKED 信息。
使用共享内存的主要优缺点包括:
优点:
高效性:相较于其他进程间通信方式(如管道、消息队列等),共享内存是最高效的通信机制之一。由于数据直接存在于内存中,进程可以直接读取和写入共享内存区域,无需复制数据,从而提高了数据传输的效率。
低延迟:由于共享内存是直接访问内存区域,因此在通信过程中几乎没有额外的开销和延迟。这使得共享内存非常适用于需要实时性和低延迟的应用程序。
简洁性:相对于其他通信机制,共享内存的编程接口比较简单明了。只需将数据放置在共享内存中,并通过内存地址进行访问即可。
数据共享:共享内存允许不同的进程之间共享数据,这对于需要在多个进程之间共享大量数据的应用非常有用。这样可以避免数据复制和传输的开销,提高系统的整体性能。
缺点:
同步问题:由于共享内存可以被多个进程同时访问,因此需要合理地进行同步操作来避免竞态条件和数据一致性问题。需要使用互斥锁、信号量等同步机制来确保正确的访问和修改。
安全性问题:共享内存可能导致安全性问题,因为多个进程可以直接访问和修改共享内存。必须采取适当的安全措施来防止恶意访问和数据损坏。
调试困难:由于共享内存的特性,当多个进程共享同一段内存区域时,调试变得更加困难。并发问题可能会导致难以重现的 bug 和难以跟踪的问题。
局限性:共享内存通信方式通常只适用于运行在同一台计算机上的进程之间,对于分布式系统来说并不适用。此外,共享内存的大小受到系统限制,对于大型数据结构或需要大量内存的应用可能会受到限制。
事实上共享内存虽然效率很高,但是使用过程很繁琐,那么我们为什么不取消使用他,而换一种简单并且高效的方式尼,对于这样的工作方式而言,虽然繁琐,但是他能高效并且准确无误的实现数据共享,并且到目前来说没有一个简单的方式全面的取代他的功能, 没有十全十美的东西,有得必有失去,SHM 的多进 程或者多线程同步和互斥的工作,一般并不是用信号来协调,我们有更好用的工具,比如下 一节马上要介绍的——信号量。