进程间通信方式--共享内存

本文详细介绍了共享内存的概念、实现方式,包括ftok、shmget、shmat、shmctl和shmdt等API的用法,以及在并发编程中如何避免竞态条件。通过实例演示了如何在父子进程中使用共享内存进行数据同步。
摘要由CSDN通过智能技术生成

共享内存

一、概念

(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 进行计算的时刻,另一个进程可能在这个事件里被执行了多次!所以,在父子进程同时访问共享资源时,一次累加,可能会导致多次累加的结果被覆盖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值