共享内存
一、概念
(1)进程间通信方式之一,若干个进程可以共享一段内存,这段内存 称之为"共享内存"。
(2)这种方式,会比其它 IPC 方式(fifo pipe messages)少一次 copy 内存,共享内存的效率会高很多。
(3)共享内存在内核中创建,随内核持续性!
实现方式
(1)在内核中开辟了一块共享内存,其它进程通过 “映射” 方式获取这段内存的首地址(指针)。
(2)当一个进程往这段内存写入数据,实际就是往映射了该共享内存的其它进程中写入数据。

二、共享内存的操作流程
(1)ftok: 用于创建或获取共享内存通信对象的键值 key。
(2)shmget: 创建或打开一个共享内存区域,返回一个共享内存标识符。
(3)shmat: 将共享内存区域映射到进程的地址空间
(4)shmctl: 对共享内存区域进行控制
(5)shmdt: 解除共享内存区域的映射
三、相关API
1.ftok: 创建或获取共享内存通信对象的键值。
函数原型:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数:
@pathname: 路径名,在文件系统中存在即可
@proj_id: 工程代号8bits,通常来说,工程代号可以在 1 到 255 中随意选取(不要重复选取)
返回值:
成功返回 键值key
失败返回 -1 errno被设置
key 特性:唯一性,不同的文件路径名和项目标识符组合会生成不同的键值。不同的进程中调用 ftok 函数,并提供相同的文件路径名和项目标识符,得到的键值也应该是相同的。
pathname:用于提供一个路径名,将其转换为一个整数,作为键值的一部分。实际文件的内容与共享内存中的数据无关,但是 ftok 函数使用 pathname 参数来保证每个不同的文件都会产生一个唯一的键值。
proj_id:提供一个项目标识符,通常是一个介于 1 到 255 之间的非零整数。它是用来进一步确保键值的唯一性的。
2.shmget: 创建或打开一个共享内存区域
函数原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
@key: 共享内存通信对象的键值
@size: 指定共享内存的大小,单位是byte,实际擦偶作创建新的共享内存时,size需要是 4096 的倍数
@shmflg:标志位
①创建 IPC_CREAT | 权限
②打开 0 读写方式打开,不同设备可能不一样
返回值:
成功返回 共享内存的 id ,唯一标识该共享内存
失败返回 -1 ,errno 被设置
示例:
shmget(key,4096,IPC_CREAT|0666);
如果具有给定键值(key)的共享内存存在,则打开它。
如果具有给定键值的共享内存不存在,则创建它,并以指定的大小(4096字节)和权限(0666)进行初始化,并打开它。
shmget(key,0,0);
打开已有的 共享内存,若不存在,则调用失败返回 -1 errno 被设置为 ENOENT(找不到文件或目录)
3.shmat: 将共享内存区域映射到进程的地址空间
函数原型:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
@shmid: 共享内存的 id,也就是 shmget 的返回值
@shmaddr: 共享内存映射到进程地址的哪个地方;一般为NULL 表示由操作系统自行分配
@shmflg: 映射标志
①SHM_RDONLY 只读映射
②0 读写映射 不同机器会有差别
……
返回值:
成功返回 映射到进程空间的地址,该地址就是共享内存的首地址
失败返回 -1,errno 被设置
4.shmctl: 对共享内存区域进行控制
函数原型:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
@shmid: 需要操作的共享内存 id,也就是 shmget 的返回值
@cmd: 控制指令,不同的控制命令对应的 第三个参数 不一样
IPC_STAT 获取属性
IPC_SET 设置属性
IPC_RMID 删除指定的共享内存
...
@buf: 根据第二个参数决定
if cmd == IPC_RMID buf 为 NULL
if cmd == IPC_STAT buf 用来保存获取到的数据
if cmd == IPC_SET buf 用来保存要设置的数据
...
返回值:
成功返回 0
失败返回 -1,errno 被设置
5.shmdt: 解除共享内存区域的映射
函数原型:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:
@shmaddr 需要解映射的进程内存空间地址,shmat 的返回值
返回值:
成功返回 0
失败返回 -1 errno 被设置
四、代码实现
read.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h> // shmget 头文件
#include <unistd.h> // close 头文件
#include <string.h> // string类 头文件
#define N 30
/** 外部传参
* @param argc 参数个数
* @param argv[0] 可执行文件
* @param argv[1] 任意文件路径
* @param argv[2] 工程代号(1-255)
*/
int main(int argc,char *argv[])
{
// 判断参数
if(argc < 3)
{
printf("参数过少!\n可执行文件\t任意文件路径\t工程代号(1-255)\n");
exit(1);
}
// 1.创建一个通信对象的键值 key
key_t key = ftok(argv[1],atoi(argv[2]));
if(key == -1)
{
perror("创建通信对象键值失败!");
exit(2);
}
// 2.借助 key 创建打开一个共享内存
int shmid = shmget(key,4096,IPC_CREAT|0777);
if(shmid == -1)
{
perror("创建/打开一个共享内存失败!\n");
exit(3);
}
// 3.把共享内存映射到进程的地址空间
char* p = shmat(shmid,NULL,0); // 读写映射
if(p == (void*)-1)
{
perror("映射地址空间失败!");
exit(4);
}
// 4.操作
// 存储上一次共享内存空间中的内容
// 存储上一次共享内存空间中的内容
char buf[N] = {0};
while(1)
{
// 当前共享内存空间的数据
char buf_flag[N] = {0};
strcpy(buf_flag, p);
if(strcmp(buf, buf_flag) != 0)
{
// 共享内存中数据更新
printf("读取: %s \n", buf_flag);
// 更新 buf
strcpy(buf, buf_flag);
}
if(strcmp(p, "over") == 0)
{
printf("读取结束,over!\n");
break;
}
}
// 5.解除映射
shmdt(p);
// 6.关闭共享内存
close(shmid);
}
write.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h> // shmget 头文件
#include <unistd.h> // close 头文件
#include <string.h> // string类 头文件
/** 外部传参
* @param argc 参数个数
* @param argv[0] 可执行文件
* @param argv[1] 任意文件路径
* @param argv[2] 工程代号(1-255)
*/
int main(int argc,char *argv[])
{
// 1.判断参数数量
if(argc < 3)
{
printf("参数少于 2 个\n请输入: 可执行文件\t文件路径\n");
exit(0);
}
// 2.创建一个通信对象的键值
key_t key = ftok(argv[1],atoi(argv[2]));
if(key == -1)
{
perror("创建键值失败!");
exit(1);
}
// 3.创建或打开一个共享内存
int shmid = shmget(key,4096,IPC_CREAT|0666);
if(shmid == -1)
{
perror("共享内存创建打开失败!");
exit(2);
}
// 4.将共享内存区域映射到进程的内存空间地址
char *p = shmat(shmid,NULL,0);
if (p == (void *)-1)
{
perror("映射共享内存失败!");
exit(3);
}
// 5.写入数据到共享内存
char buf[30] = {0};
while(1)
{
memset(buf,0,sizeof(buf));
scanf("%s",buf);
strcpy(p,buf);
if(strcmp(buf,"over") == 0)
{
printf("写入结束,over!\n");
memset(p,0,sizeof(buf));
break;
}
}
// 6.解除映射
shmdt(p);
// 7.关闭共享内存
close(shmid);
}
结果

五、练习
在内核中创建一个 System V 共享内存,将这块内存的前四个字节当作是一个 int 类型。
利用父子进程 并发执行自增 +1,总共两百万次
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h> // shmget 头文件
#include <unistd.h> // close 头文件
#include <string.h> // string类 头文件
#include <sys/wait.h>
/** 外部传参
* @param argc 参数个数
* @param argv[0] 可执行文件
* @param argv[1] 任意文件路径
* @param argv[2] 工程代号(1-255)
*/
int main(int argc,char* argv[])
{
// 判断参数
// 判断参数
if(argc < 3)
{
printf("参数过少!\n可执行文件\t任意文件路径\t工程代号(1-255)\n");
exit(1);
}
// 1.创建 进程通信对象键值 key
key_t key = ftok(argv[1],atoi(argv[2]));
if(key == -1)
{
perror("创建 key 失败!");
exit(1);
}
// 2.通过 key 创建/打开 一个共享内存
int shmid = shmget(key,4096,IPC_CREAT|0777);
if(shmid == -1)
{
perror("创建共享内存失败!");
exit(2);
}
// 3.将共享内存空间映射到进程空间
int *p = shmat(shmid,NULL,0); // 读写映射
if(p == (void*)-1)
{
perror("映射失败!");
close(shmid);
exit(3);
}
// 4.操作
*p = 0; // 初值为 0
int i = 0;
int pid = fork();
if(pid == -1)
{
perror("创建子进程失败!");
// 关闭资源
shmdt(p);
close(shmid);
exit(4);
}
else if(pid == 0)
{
// 子进程 +1万次
while(i < 1000000)
{
(*p)++;
i++;
}
}
else
{
// 父进程 +1万次
while(i < 1000000)
{
(*p)++;
i++;
}
wait(NULL);
printf("最终结果为: %d\n",*p);
}
// 5.解除映射
shmdt(p);
// 6.关闭共享内存
close(shmid);
return 0;
}
运行结果

结果分析:
在父子进程并发执行的情况下,同时访问共享资源可能会导致竞态条件。假设在某个时间点,共享内存中的整型数据为100000,父子进程同时访问共享内存并对其进行递增操作:
time时刻- 共享内存中前四个字节表示的整型数据值为:100000,此时父子进程同时拿去共享资源
time1时刻- 父进程:对共享内存中的值执行递增操作,将其从100000增加到100001。
time2时刻- 子进程:与父进程同时进行,对共享内存中的值执行递增操作,也将其从100000增加到100001。
在某一时刻,父进程或子进程完成了递增操作,将共享内存中的值设置为100001。
在另一个时刻,另一个进程也完成了递增操作,将共享内存中的值再次设置为100001。
这种情况下,可能发生了覆盖问题:
在time1时刻,共享内存中的值应该是100001,但是由于父子进程在time时刻同时获取了值为100000的初始值,
到了time2时刻,另一个进程仍使用100000作为初始值进行递增操作,导致之前的递增操作被覆盖,从而使得最终的结果不是期望的100002,而是100001。
在time2 进行计算的时刻,另一个进程可能在这个事件里被执行了多次!所以,在父子进程同时访问共享资源时,一次累加,可能会导致多次累加的结果被覆盖。
本文详细介绍了共享内存的概念、实现方式,包括ftok、shmget、shmat、shmctl和shmdt等API的用法,以及在并发编程中如何避免竞态条件。通过实例演示了如何在父子进程中使用共享内存进行数据同步。
6430

被折叠的 条评论
为什么被折叠?



