【Linux】进程间通信-共享内存

System V标准下的进程间通信方式

System V标准是在同一主机内的进程间通信方案,是站在OS层面专门为进程间通信设计的方案

进程通信的本质是先让相互通信的进程先看到同一份资源, System V提供了三个主流方案

  • system V共享内存
  • system V消息队列(有点落伍)
  • system V信号量

其中:system V共享内存和system V消息队列是以传送数据为目的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴


共享内存的建立与释放

共享内存的建立大致包括以下两个过程:

  1. 在物理内存当中申请共享内存空间 2)将申请到的共享内存挂接到地址空间., 即建立映射关系

共享内存的释放大致包括以下两个过程:

1)将共享内存与地址空间去关联, 即取消映射关系 2)释放共享内存空间, 即将物理内存归还给系统


总结:

共享内存方案的本质是让参与通信的进程关联上同一块内存空间,读写该空间从而实现通信

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

2.通过某种调用让参与通信的进程“挂接”到这份新开辟的内存空间上, 于是我们就让不同的进程看到了同一份资源

3.去关联(去挂接)

4.释放共享内存

image-20220804174942467


1)因为OS中可能存在多个进程在使用不同的共享内存区域进行各自的进程间通信,所以共享内存在系统中可能存在很多,操作系统当然要管理这些共享内存,以实现创建删除挂接去关联一系列复杂的操作 那如何进行管理呢? 先描述再组织!

2)如何保证不同进程看到的是同一共享内存呢

共享内存一定要有唯一标识它的ID,使不同进程识别同一个共享内存资源,那这个“ID”存在于哪里呢,很明显我们可以猜到:这应该在描述共享内存的struct结构体中

image-20220808201339898


共享内存的创建

shmget函数

创建共享内存我们需要用shmget函数:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数说明

第一个参数key: 表示待创建共享内存在系统当中的唯一标识,可用 ftok 函数生成

(内核用 key唯一确定共享内存,用户层使用 返回值shmid 进行管理共享内存)

  • 为了使不同进程看到同一段共享内存,即让不同进程拿到同一个ID,需要由用户自己设定, 这里我们通常使用一个函数来进行获取 ftok函数
key_t ftok(const char *pathname, int proj_id);

其中:pathname为自定义路径名 ,proj_id为整数标识符

其作用就是将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中

注意:

1 .pathname所指定的文件必须存在且可存取

2.使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改

3.需要进行通信的各个进程,在使用ftok函数获取key值时,需要采用同样的路径名和和整数标识符,进而生成同一个key值,然后才能找到同一个共享资源


第二个参数:size : 表示待创建共享内存的大小

  • 建议是 4KB 的整数倍,因为共享内存在内核中申请的基本单位是页(内存页):大小为4KB

注意:如果我们申请的是4097个字节呢?

内核会给你申请4096*2个字节(申请两页),但是看到的是4097个字节

image-20220814155111715


第三个参数shmflg: 表示创建共享内存的方式,本质是标记位->底层是宏,是只有一个比特位是1且相互不重复的数据,这样 | 在一起之后就能传递多个标志位

标志位有: IPC_CREAT和IPC_EXCL,但是IPC_EXCL单独使用没有意义,通常要搭配起来 IPC_CREAT | IPC_EXCL

组合方式作用
IPC_CREAT如果单独使用IPC_CREAT或者flag为0,表示创建一个共享内存,若不存在,则创建,若已存在,则直接返回当前已存在的共享内存,也就是说基本不会空手而归
IPC_CREAT | IPC_EXCL若不存在,则创建,若已存在,则返回出错 这样的意义在于如果调用成功:得到的一定是一个全新的,没被别人使用过的共享内存
  • 使用组合IPC_CREAT,一定会获得一个共享内存,但无法确认该共享内存是否是新建的共享内存
  • 使用组合IPC_CREAT | IPC_EXCL,只有shmget函数调用成功时才会获得共享内存,并且该共享内存一定是新建的共享内存

返回值说明:

shmget调用成功: 返回一个有效的共享内存标识符(用户层标识符) ->本质是句柄 shmget调用失败: 返回-1

注意:我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作


牛刀小试:

接下来,我们就可以使用ftok和shmget函数创建一块共享内存了,创建后我们可以将共享内存的key值和句柄进行打印

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x1234       //整数标识符
#define SIZE 4096           //共享内存的大小

int main()
{
    key_t key = ftok(PATHNAME, PROJ_ID); //通过ftok获取key值作为shmget的第一个参数
    if (key < 0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
    if (shm < 0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n", key); //打印key值
    printf("shm: %d\n", shm); //打印句柄
    return 0;
}

我们还可以使用ipcs命令查看有关进程间通信设施的信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-boWB353n-1677667168169)(https://mangoimage.oss-cn-guangzhou.aliyuncs.com/202208041959570.png)]

ipcs命令

单独使用 ipcs 命令时,会默认列出消息队列, 共享内存以及信号量相关的信息

选项:

  • -q:列出消息队列相关信息
  • -m:列出共享内存相关信息
  • -s:列出信号量相关信息

根据 ipcs 命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了

image-20220804200041806

ipcs 命令输出的每列信息的含义是什么呢?

  • key: 系统区别各个共享内存的唯一标识
  • shmid:共享内存的用户层id(句柄)
  • owner:共享内存的拥有者
  • perms:共享内存的权限
  • bytes:共享内存的大小
  • nattch:关联共享内存的进程数
  • status:共享内存的状态

注意:key是在内核层面上保证共享内存唯一性的方式 ,shmid是在用户层面上保证共享内存的唯一性

key和shmid之间的关系类似于fd和FILE*之间的的关系


共享内存的释放

当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放

管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放

所以说,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启,同时也说明了IPC资源是由内核提供并维护的


此时我们若是要将创建的共享内存释放,有两个方法

方法1:使用命令释放共享内存

ipcrm命令

使用 ipcrm -m 共享内存对应的shmid 命令 释放指定id的共享内存资源

image-20220804211054521

注意: 指定删除时使用的是共享内存的用户层id ->即shmid, 而不是key


方法2:在进程通信完毕后调用释放共享内存的函数进行释放

控制共享内存我们需要用shmctl函数

shmctl函数
// man shmctl
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

第一个参数shmid : 表示所控制共享内存的用户级标识符

第二个参数cmd:表示具体的控制动作

  • 常用的选项有以下三个

    选项作用
    IPC_STAT获取共享内存的当前关联值,此时参数buf作为输出型参数
    IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值
    IPC_RMID删除共享内存段

因为我们只考虑删除,所以第二个参数设置为:IPC_RMID就行啦

第三个参数buf:用于获取或设置所控制共享内存的数据结构

  • buf 参数的类型是 struct shmid_ds 也就是共享内存的数据结构s

image-20220804211640163


返回值说明

shmctl调用成功,返回0 , 调用失败,返回-1


牛刀小试:共享内存被创建,5s后程序自动移除共享内存,再过2s程序就会自动退出

这里我们可以写一个脚本监视共享内存的状态

while :; do ipcs -m;echo "###################################";sleep 1;done

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x1234       //整数标识符
#define SIZE 4096           //共享内存的大小

int main()
{
    key_t key = ftok(PATHNAME, PROJ_ID); //通过ftok获取key值作为shmget的第一个参数
    if (key < 0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
    if (shm < 0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n", key); //打印key值
    printf("shm: %d\n", shm); //打印句柄
    
    sleep(2);
    shmctl(shm, IPC_RMID, NULL); //IPC_RMID: 释放共享内存
    sleep(2);
    return 0;
}

image-20220804214007287

我们可以发现共享内存确实创建并且成功释放了


共享内存的关联

shmat函数

将共享内存链接到进程地址空间我们需要用shmat函数

#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数解析:

第一个参数shmid : 表示待关联共享内存的用户级标识符

第二个参数shmaddr :指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置

第三个参数shmflg :表示关联共享内存时设置的某些选项

选项对应作用
SHM_RDONLY关联共享内存后只进行读取操作
SHM_RND若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍 公式: shmaddr-(shmaddr%SHMLBA)
0默认为读写权限

返回值解析:

shmat调用成功:返回共享内存映射到进程地址空间中的起始地址 调用失败:返回(void*)-1

注意:这个地址一定是虚拟地址,类似于malloc返回申请到的空间的起始地址


牛刀小试:使用shmat函数对共享内存进行关联

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x1234       //整数标识符
#define SIZE 4096           //共享内存的大小

int main()
{
    key_t key = ftok(PATHNAME, PROJ_ID); //通过ftok获取key值作为shmget的第一个参数
    if (key < 0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL); //创建新的共享内存
    if (shm < 0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n", key); //打印key值
    printf("shm: %d\n", shm); //打印句柄
    

    printf("attach begin!\n");
    sleep(2);

    char* mem = shmat(shm, NULL, 0); //使用shmat函数使该进程和共享内存关联
    if (mem == (void*)-1)//关联失败
    {
        perror("shmat");
        return 1;
    }
    
    printf("attach end!\n");
    shmctl(shm, IPC_RMID, NULL); //IPC_RMID: 释放共享内存
    sleep(2);
    return 0;
}

同样我们可以用上面的脚本监视我们的共享内存情况:

while :; do ipcs -m;echo "###################################";sleep 1;done

image-20220808104419695

此时我们发现共享内存使关联失败的,为什么呢?

因为我们使用shmget函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存


所以我们使用shmget函数创建共享内存时,要在第三个参数处设置共享内存创建后的权限

int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存

再次运行后:

image-20220808104809827


共享内存的去关联

shmdt函数

取消共享内存与进程地址空间之间的关联我们需要用shmdt函数

#include <sys/shm.h>
int shmdt(const void *shmaddr);

参数解析:

第一个参数shmaddr:传入是调用shmat函数时得到的起始地址,待去关联共享内存的起始地址

返回值解析

调用成功返回0,调用失败返回-1


牛刀小试:去掉共享内存与进程之间的关联

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x1234       //整数标识符
#define SIZE 4096           //共享内存的大小

int main()
{
    //1.创建共享内存
    key_t key = ftok(PATHNAME, PROJ_ID); //通过ftok获取key值作为shmget的第一个参数
    if (key < 0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL|0666); //创建新的共享内存
    if (shm < 0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n", key); //打印key值
    printf("shm: %d\n", shm); //打印句柄
    
    //2.关联共享内存
    printf("attach begin!\n");
    sleep(2);

    char* mem = shmat(shm, NULL, 0); //使用shmat函数使该进程和共享内存关联
    if (mem == (void*)-1)//关联失败
    {
        perror("shmat");
        return 1;
    }
    
    printf("attach end!\n");

    //3.去关联
    printf("detach begin!\n");
    shmdt(mem);
    sleep(2);
    printf("detach end!\n");

    //4.释放共享内存
    shmctl(shm, IPC_RMID, NULL); //IPC_RMID: 
    sleep(2);
    return 0;
}

通过监控即可发现该共享内存的关联数由1变为0的过程,即取消了共享内存与该进程之间的关联

image-20220808105723771


问:共享内存去关联是不是删除这个共享内存?

将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系,去关联的本质是把进程和物理内存之间的构建映射关系的页表项去除


共享内存实现serve&client通信

此时我们需要同时产生两个进程,而Makefile默认只会生成第一个遇到的目标文件,所以我们定义一个伪目标all

Makefile

.PHONY:all
all:server client

server:server.c
	gcc -o $@ $^

client:client.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm server client

同样,这里我们也把两个进程都用到的东西放到一个文件中:

comm.h

为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值, 那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接,这里我们可以将这些需要共用的信息放入一个头文件当中

#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "./" //路径名
#define PROJ_ID 0x1234       //整数标识符
#define SIZE 4096           //共享内存的大小

我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上

server.c

1)服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环, 观察服务端是否挂接成功

#include"comm.h"
int main()
{
    //1.创建共享内存
    key_t key = ftok(PATHNAME, PROJ_ID); //通过ftok获取key值作为shmget的第一个参数
    if (key < 0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL|0666); //创建新的共享内存
    if (shm < 0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n", key); //打印key值
    printf("shm: %d\n", shm); //打印共享内存用户层id
    
    //2.关联共享内存
    sleep(2);

    char* mem = shmat(shm, NULL, 0); //使用shmat函数使该进程和共享内存关联
    if (mem == (void*)-1)//关联失败
    {
        perror("shmat");
        return 1;
    }
    while (1)
    {
        //不进行操作,观察是否关联成功
    }
    //3.去关联
    shmdt(mem);

    //4.释放共享内存
    shmctl(shm, IPC_RMID, NULL); //IPC_RMID: 
    return 0;
}

client.c

2)客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环.观察客户端是否挂接成功

#include"comm.h"
int main()
{
    key_t key = ftok(PATHNAME, PROJ_ID);//与server进程用相同的方式构成的key值
    if (key < 0)
    {
        perror("ftok");
        return 1;
    }
    int shm = shmget(key,SIZE,IPC_CREAT);//获取server进程创建的共享内存的用户层id
    if(shm<0)
    {
        perror("shmget");
        return 2;
    }
    printf("key: %x\n", key); //打印key值
    printf("shm: %d\n", shm); //打印共享内存用户层id

    char* mem = shmat(shm, NULL, 0); //使用shmat函数使该进程和共享内存关联
    if (mem == (void*)-1)//关联失败
    {
        perror("shmat");
        return 1;
    }
    while (1)
    {
        //不进行操作,观察是否关联成功
    }
    //去关联
    shmdt(mem);
    return 0;
}

问:client进程中是否需要释放共享内存?

不需要,因为共享内存是server进程创建的, client只需要获取即可然后操作即可


先后运行服务端和客户端后,通过监控脚本可以看到服务端和客户端所关联的是同一个共享内存,其中共享内存关联的进程数也是2,表示服务端和客户端挂接共享内存成功

image-20220808111602150


通信

此时我们就可以让两个进程进行通信了: 客户端不断向共享内存写入数据:

//server.c
int i = 0;
while (1)
{
    mem[i] = 'A' + i;
    i++;
    mem[i] = '\0';
    sleep(1);
}

服务端不断读取共享内存当中的数据并输出

//client
while (1)
{
    printf("client# %s\n", mem);
    sleep(1);
}

image-20220808112002861


共享内存和管道的对比

当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信

我们先来看看管道是如何实现通信的:

image-20220808195431701

使用管道通信的方式, 将一个文件从一个进程传输到另一个进程需要进行四次拷贝

对应的执行动作:

1)服务端将信息从输入文件复制到服务端的临时缓冲区中 2)将服务端临时缓冲区的信息复制到管道中

3)客户端将信息从管道复制到客户端的缓冲区中 4)将客户端临时缓冲区的信息复制到输出文件中


read或者write的本质:将数据从内核拷贝到用户 / 用户拷贝到内核

之前的管道就是: 把是数据从进程拷贝到内核文件,然后再由内核文件拷贝到另一个进程的空间里,所以需要调用read/write


我们再来看看共享内存是如何实现通信的:

1

使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝

对应的执行动作: 1)把输入文件中的内容拷贝到共享内存 2)把内容从共享内存拷贝到输出文件中


所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少,

共享内存是不需要调用read/write等系统调用接口,共享内存一旦建立好并映射进自己进程的地址空间,该进程就可直接看到该共享内容,就如同malloc的空间一般,不需要任何系统调用接口!


共享内存的特征

1)共享内存的生命周期随内核

2)共享内存是所有进程中速度最快的, 只需要经过页表映射, 不需来回拷贝

3)管道是自带同步与互斥机制的,但是共享内存没有任何同步或互斥机制 (但这并不代表它不需要), 需要程序员自行保证数据安全

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芒果再努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值