Linux操作系统~基于systemV共享内存的进程间通信

目录

一.进程间通信有哪些方式

二.什么是systemV

三.共享内存-双向通信-大致实现思路

四.4个函数about共享内存

1.shmget函数-创建

 ftok函数

​编辑

 e.g.

ipcs/ipcrm指令(ipc资源会被回收吗)

2.shmctl函数-删除/释放

3.shmat函数-挂接

4.shmdt函数-去挂接

5.e.g.

五.总结/共享内存的特性

Q:为什么共享内存的大小建议是4KB整数倍?

六.共享内存的内核数据结构

Q:为什么我们看到的shmid是0,1,2,3,4递增的呢?


一.进程间通信有哪些方式

首先我们先来看一下进程间通信总共有哪些方式?

管道

  1. 匿名管道pipe
  2. 命名管道

System V IPC

  1. System V 消息队列
  2. System V 共享内存
  3. System V 信号量

POSIX IPC

  1. 消息队列
  2. 共享内存
  3. 信号量
  4. 互斥量
  5. 条件变量
  6. 读写锁

这里我讨论的是基于systemV共享内存的进程间通信


二.什么是systemV

        命名管道和匿名管道都是基于文件的通信方式,还有systemV标准的通信方式:

  1. OS层面专门为进程间通信设计的一个方案
  2. OS不相信任何用户,给用户提供功能的时候,采用系统调用
  3. System V进程间通信,一定会存在专门用来通信的接口(system call)

同一主机内的进程间通信方案——system V方案

注意进程间通信的本质:让不同进程看到同一份资源


三.共享内存-双向通信-大致实现思路

1.通过某种调用,在内存中创建一份内存空间

2.通过某种调用,让进程(参与通信的多个进程)“挂接”到这份新开辟的内存空间上(挂接就是通过页表映射到这片物理空间)-让不同的进程看到了同一份资源

3.不用共享内存后,我们需要去关联(去掉页表中的映射)

4.释放共享内存


四.4个函数about共享内存

1.shmget函数-创建

用于创建一个共享内存

  • key:用来唯一表示这个共享内存块(就像路径+文件名),需要用户自己设定。我们可以调用ftok函数,将自定义路径名和项目id,经过算法转换成一个唯一的key,这个key会设置到管理shm共享内存的数据结构中(注意这里的key和shmid是不一样的)。

注:

key_id实际上是一个有符号整数

key只是用来在系统层面进行标识唯一性的,不能用来管理shm

shmid是OS给用户返回的id,用来在用户层面对shm进行管理

        如果硬是要类比的话,shmid就像是文件描述符,key就像是struct_file的地址,OS是使用shmid对struct_file进行管理的

  • size:共享内存大小,建议是4KB的整数倍
  • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
  • 返回值:shmid,管理当前共享内存的id;成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

这里常用两个权限标志:

  1. IPC_CREATE:不存在则创建:如果单独使用IPC_CREAT,或者flg为0:创建一个共享内存,如果创建的共享内存已经存在,则直接返回当前已经存在的共享内存。(基本不会空手而归)
  2. IPC_EXCL(单独使用没有意义)

 ftok函数

pathname:路径名

proj_id:自定义项目名ID

        路径名+项目名ID应该是唯一的,通过这两个参数生成的key也就是唯一的了,用于唯一标识共享内存块


 e.g.

1.生成同一个key

我们在server和client中使用同一个获取key值的方法ftok(并且传入相同的路径和项目ID),这样也就保证了两个不同的进程可以看到同一份资源。

int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if(key < 0){
        perror("ftok");
        return 1;
    }

    printf("%u\n", key);

    return 0;
}

2.创建一个共享内存

int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666); //创建全新的shm,如果和系统已经存在ID冲突,我们出错返回,这个0666表示共享内存的权限

if(shmid < 0){
    perror("shmget");
    return 2;
}

ipcs/ipcrm指令(ipc资源会被回收吗)

ipcs可以看到systemV进程通信设施状态的指令(信号量,共享内存,消息队列都属于ipc资源)

  1. ipcs -s可以显示信号量的状态
  2. ipcs -m显示共享内存的状态
  3. ipcs -q显示消息队列的状态

        systemV的IPC资源,生命周期是随内核的,只能通过,程序员显示的释放(释放IPC资源的指令或者system call系统调用)或者是OS重启。文件,堆空间,进程退出的时候,这些资源都会被操作系统回收,但是IPC资源不会。

ipcrm指令:

用于删除ipc资源参数:

-M   以shmkey删除共享内存

-m   以shmid删除共享内存

-Q   以msgkey删除消息队列

-q    以msgid删除消息队列

-S    以semkey删除信号量

-s    以semid删除信号量


2.shmctl函数-删除/释放

  • shmid:前面shmget函数的返回值,描述共享内存数据结构中的shmid
  • cmd:cmd指令,我们这里只用删除指令IPC_RMID
  • *buf:操作系统用于描述当亲共享内存的数据结构

shmctl(shmid, IPC_RMID, NULL);

printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);

这样就能够删除shmid对应的共享内存(释放空间)


3.shmat函数-挂接

  • shmid:前面shmget函数的返回值,描述共享内存数据结构中的shmid
  • shmaddr:挂接到哪个位置,一般设为NULL(我们自己不太清楚,但是操作系统清楚)
  • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的,我们一般传0
  • 返回值:成功返回创建共享内存的起始地址(虚拟地址),就像是malloc的返回值

char *mem = (char*)shmat(shmid, NULL, 0);

printf("attaches shm success\n");

这样就得到了一个指向共享内存的地址mem


 4.shmdt函数-去挂接

        并不是释放共享内存,而是删除共享内存和进程的关系(删除进程页表上进程虚拟地址和共享内存物理地址映射的页表项)

  • shmid:前面shmget函数的返回值,描述共享内存数据结构中的shmid
  • shmaddr:去挂接哪个位置,一般不设置,用缺省值(我们自己不太清楚,但是操作系统清楚)
  • shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的,我们一般不传

    shmdt(mem);

这样当前进程共享内存的挂接就解除了


 5.e.g.

这里模拟实现一个用共享队列进行通信的服务端和客户端。服务端读数据,客户端写数据

server.c

#include <unistd.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATH_NAME "./"  //用于ftok函数,表示当前的路径名,路径名+项目ID应该是唯一的,随之生成的key也就是唯一的
#define PROJ_ID 0x1234  //用于ftok函数,表示当前的路径名,路径名+项目ID应该是唯一的,随之生成的key也就是唯一的
#define SIZE 4097

int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if(key < 0){
        perror("ftok");
        return 1;
    }

    int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666); 
    //创建全新的shm,加上了IPC_EXCL选项,如果和系统已经存在ID冲突,则出错返回,这个0666表示共享内存的权限
    if(shmid < 0){
        perror("shmget");
        return 2;
    }

    printf("key: %u, shmid: %d\n", key, shmid);
    //sleep(1);

    char *mem = (char*)shmat(shmid, NULL, 0);  //将共享内存挂接到当前进程
    printf("attaches shm success\n");
    //sleep(15);

    //此处进行通信逻辑,直接像数组一样对共享内存进行操作即可,服务端进行读操作
    while(1){
        sleep(1);
        printf("%s\n", mem); //server 认为共享内存里面放的是一个长字符串 
    }

    shmdt(mem);  //去挂接

    printf("detaches shm success\n");
    //sleep(5);

    shmctl(shmid, IPC_RMID, NULL);  //释放共享内存
    printf("key: 0x%x, shmid: %d -> shm delete success\n", key, shmid);

    //sleep(10);

    return 0;
}

client.c

#include <unistd.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATH_NAME "./"
#define PROJ_ID 0x1234
#define SIZE 4097
int main()
{
    key_t key = ftok(PATH_NAME, PROJ_ID);
    if(key < 0){
        perror("ftok");
        return 1;
    }

    printf("%u\n", key);

    //client这里只需要进行获取即可,只需要加上IPC_CREATE选项即可
    int shmid = shmget(key, SIZE, IPC_CREAT);
    if(shmid < 0){
        perror("shmget");
        return 1;
    }
    char *mem = (char*)shmat(shmid, NULL, 0);
    //sleep(5);
    printf("client process attaches success!\n");

    //此处进行通信逻辑,直接像数组一样对共享内存进行操作即可,客户端进行写操作,每隔两秒,多写入一个字母
    char c = 'A';
    while(c <= 'Z'){
        mem[c-'A'] = c;
        c++;
        mem[c-'A'] = 0;
        sleep(2);
    }


    shmdt(mem);
    //sleep(5);
    printf("client process detaches success\n");

    //client不需要释放共享内存的空间,由服务端释放

    return 0;
}

运行结果:我们2s写一次,1s读一次,我们读的时候并没有把数据拿走,所以会读两次

        这里server会一直读取共享内存中的内容,即使共享内存中没有内容可以读,所以不会退出while循环,也不会执行去挂接和释放共享内存的操作。(需要后期自己用ipcrm指令删除,也可以考虑在循环中定义一个计数器,几s后自动退出循环,进行去挂接,释放共享内存的操作)

        这里我们读的时候直接读的是mem这个连续的地址空间,和前面管道中的不同,管道是基于文件的,在一次打开中,读过的内容不会再读了。管道中的内容都不会像普通文件一样保存在磁盘中,读取过的数据就会失效,不能重复读取(包括匿名管道和命名管道)


五.总结/共享内存的特性

1.我们在使用共享内存的时候,没有调用类似pipe or fifo中的read过样的接口,因为匿名管道和命名管道都是基于文件的,read和write也是基于文件的,本质是将数据从内核拷贝到用户,或者从用户拷贝到内核。共享内存并不是基于文件的。

2.共享内存一旦建立好并映射进自己进程的地址空间,进行通信的进程就可以直接看到该共享内存,就如同自己malloc的空间一般,不需要任何系统调用接口,直接当数组用都行。所以共享内存是所有的进程间通信中速度最快的!(共享内存最多拷贝一次,管道可能要4次或者更多)

3.当client没有写入,甚至没有启动的时候,server端也会直接读取shm,不会像管道那样等待client写入。共享内存不提供任何司步或者互斥儿制,需要程序员自行保证数据的安全!

Q:为什么共享内存的大小建议是4KB整数倍?

        共享内存在内核中申请的基本单位是页,内存页,大小为4KB,内核给你的大小是4KB的整数倍。比如我申请4097个字节->内核会给你4096byte*2的空间


六.共享内存的内核数据结构

用户层:是操作系统层的子集

ipc_perm中存放的uid是用户id,gid是组id,key就是我们传入的用于唯一标识共享内存块的id。

除此之外还有当前共享内存的一些基本信息

在内核中,所有的ipc资源都是通过数组组织起来的,这三种资源结构体都不一样,怎么放在同一个数组中呢?原因就是下面的ipc_perm

SystemV三种通信方式的ipc资源,描述ipc资源的数据结构很相似,而且第一个成员都是ipc_perm,这个ipc_perm是完全相同的。

这个数组实际上是一个ipc_perm*类型的数组,也就是一个指针数组,然后根据对应的类型强制类型转换。(用C语言实现的切片技术

Q:为什么我们看到的shmid是0,1,2,3,4递增的呢?

        shmid为什么是0,1,2,3,4递增的呢?因为说白了这个shmid就是ipc_perm*数组的下标,共享内存是ipc资源,所有ipc资源都是用一个ipc_perm*数组管理的,shmid就是直接取了这个数组的下标。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C# 提供了多种进程间通信的方式,其中包括基于共享内存的通信方式。下面介绍一下使用共享内存实现进程间通信的方法。 首先需要在 C# 中引入 System.IO.MemoryMappedFiles 命名空间。然后创建一个 MemoryMappedFile 对象,并指定共享内存的名称和大小。可以通过 MemoryMappedFile.CreateNew 方法创建一个新的共享内存对象,也可以通过 MemoryMappedFile.OpenExisting 方法打开已经存在的共享内存对象。 创建共享内存对象: ``` MemoryMappedFile mmf = MemoryMappedFile.CreateNew("mySharedMemory", 1024); ``` 打开共享内存对象: ``` MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("mySharedMemory"); ``` 在获取到共享内存对象之后,可以通过 MemoryMappedFile.CreateViewAccessor 方法创建一个 MemoryMappedViewAccessor 对象,用于读写共享内存中的数据。此时需要指定访问的起始位置和长度。如果需要对共享内存进行读操作,则需要使用 Read 方法,如果需要进行写操作,则需要使用 Write 方法。 读取共享内存中的数据: ``` using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor()) { byte[] buffer = new byte[1024]; accessor.ReadArray(0, buffer, 0, buffer.Length); } ``` 向共享内存中写入数据: ``` using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor()) { byte[] buffer = new byte[1024]; accessor.WriteArray(0, buffer, 0, buffer.Length); } ``` 需要注意的是,共享内存是一种高效的进程间通信方式,但同时也需要开发者自行处理同步和互斥问题,以确保共享内存中的数据不会被多个进程同时修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值