共享内存基本概念

共享内存是目前比较主流的进程之间互相通信的方式,这种通信中读取数据的对象和写入数据的对象不一定是同一个,写入的所有数据放在一块共享的内存中,这样方式的通信十分方便快捷。但是这样就会有一些数据的同步问题,因为在这个内存上的读取时没有阻塞的,每个人都不知道自己读取的数据是不是最新的自己需要的数据,这也是我们在使用共享内存进行通信的时候需要注意的问题。

共享内存可以将一个文件映射到一块内存上,这种不同于以往读取文件内容的方式,我们使用管道或者其他方式读写文件内容时,往往需要打开两次文件,一次读取文件,将文件内容读入一个buff中,然后在buff中进行修改,再将buff中的内容重新写回去。而共享内存可以直接在内存上进行修改最终映射回去,它相比而言效率更高。
在这里插入图片描述

内存映射文件

相关函数总结

函数功能函数格式
创建共享内存 int shm_open(const char *name, int oflag, mode_t mode)
取消内存共享int shm_unlink(const char *name)
建立内存映射void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset)
关闭内存映射int munmap(void *start,size_t length)

接下来我们将详细讲解以上这些函数的用法。

创建共享内存

函数定义如下:
int shm_open(const char *name, int oflag, mode_t mode)

其中的参数具体如下:

参数含义
name posixIPC名字,格式为/somename
oflag标志
mode权限

具体的标志有:

标志名含义
O_CREAT没有该对象则创建
O_EXCL如果O_CREAT指定,但name不存在,就返回错误
O_RDONLY只读
O_RDWR读写
O_TRUNC若存在则截断

具体的权限有:

权限含义
S_IWUSR用户/属主写
S_IRUSR用户/属主读
S_IWGRP组成员写
S_IRGRP组成员读
S_IWOTH其他用户写
S_IROTH其他用户读

返回值的含义为:

返回值
-1出错
其他共享内存描述符

取消内存共享

函数定义如下:
int shm_unlink(const char *name)

具体参数如下:

参数含义
nameposix共享内存名字

返回值如下:

返回值含义
-1出错
0成功

建立映射函数

函数定义如下:
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset)

对于映射函数mmap的具体参数意义如下:

参数含义
start映射区的开始地址,通常使用NULL,让系统决定映射区的起始地址
length映射区的长度,单位字节,不足一内存页按一内存页处理
prot内存保护标志
flags映射对象的类型
fd文件描述符,不能是套接字和终端的fd,-1为匿名内存映射
offset被映射对象内容的起点,只能是页大小的整数倍

其中关于内存的保护标志:

参数含义
PROT_EXEC页内容可以被执行
PROT_READ页内容可以被读取
PROT_WRITE页可以被写入
PROT_NONE页不可访问,不能与文件的打开模式冲突

关于映射对象类型:

参数含义
MAP_SHARED变动共享
MAP_PRIVATE变动私有
MAP_ANON匿名内存映射

关于返回值:本质上是映射的内存地址

MAP_FAILED映射失败
非MAP_FAILED映射成功,返回共享内存地址

结束映射函数

函数定义如下:
int munmap(void *start,size_t length)

参数意义:

参数含义
start映射内存起始地址
length内存大小

返回值意义:

返回值含义
0成功
-1失败

利用文件映射的方式读文件内容

基本思路:

  1. 打开一个文件
  2. 进行映射,将文件映射进内存
  3. 对文件内容进行操作
  4. 结束文件映射

因为我们只是读取文件内容,所以在打开文件open时使用只读标志O_RDONLY,且映射mmap时,内存保护标志为PROT_READ(内存是用来读还是用来写的)。

在main的参数中,我们使用了argc和argv这两个参数,具体解释如下:

argvargc指的是终端运行该可执行文件时的参数,argc值的是参数个数,argv指的是终端执行命令时的字符串数组。如./a.out test,此时的argc=2,argv={./a.out, test}

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
using namespace std;

int main(int argc, char** argv){
    if(3!=argc){ // 打开文件时防止出错
        printf("Usage:%s filepath size\n",argv[0]);
        return 1;
    }
    // 打开文件
    int fd = open(argv[1],O_RDONLY);
    // 进行映射  
    void* buff = mmap(NULL,stoi(argv[2]),PROT_READ,MAP_SHARED,fd,0);   // 进行映射
    if(buff == MAP_FAILED){   // 测试文件是否映射失败
        perror("mmap error");
        return 1;
    }
    cout << static_cast<char*>(buff) << endl;  // 显示被映射的文件内容
    munmap(buff,stoi(argv[2]));  // 将文件映射回去
    close(fd);
}

以上代码就是将在终端给出的文件内容映射进buff中

利用文件映射的方式写文件内容

这里需要注意的是,我们的时候,同时还需要读文件,所以在打开文件的时候,open函数中的参数为O_RDWR,在mmap函数中的保护参数为PROT_READ|PROT_WRITE

在打开文件之后,我们需要对文件映射的内存进行扩展,防止在文件大小过小,无法成功修改。

我们将文件映射后将会得到一个内存地址buff,所以我们想要更改文件内容,就是在buff上直接对数据进行修改即可,在给出的代码中我们直接使用strcpy对buff所指向的内存内容进行修改。

修改结束后就将内存映射回文件中,结束映射。

我们总结一下基本思路:

  1. 打开文件
  2. 对文件内容进行扩展
  3. 映射文件,得到映射的内存地址buff
  4. 对buff指定的文件内容进行修改
  5. 将文件映射回内存,结束映射。
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
using namespace std;

// ./a.out filepath size
int main(int argc, char** argv){
    if(3!=argc){
        printf("Usage:%s filepath size\n",argv[0]);
        return 1;
    }
    int fd = open(argv[1],O_RDWR);
    ftruncate(fd,stoi(argv[2]));
    void* buff = mmap(NULL,stoi(argv[2]),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(buff == NULL){
        perror("mmap error");
        return 1;
    }
    cout << static_cast<char*>(buff) << endl;
    int pos;
    string s;
    cin >> pos >> s;
    strcpy(static_cast<char*>(buff)+pos,s.c_str());   // 对映射的内存内容的pos位置增添内容s

    munmap(buff,stoi(argv[2]));
    close(fd);
}

我们主要是想向读者表达共享内存可以直接在映射的内存上对文件进行修改。该代码就是在映射文件内容中增添pos的内容。

利用共享内存直接创建文件

创建文件和以上的写文件最大的不同在于打开文件时的标志为O_RDWR|O_CREAT,除此以外基本思路是相同的,所以直接来看代码。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>
using namespace std;

// ./a.out filepath size
int main(int argc, char** argv){
    if(3!=argc){
        printf("Usage:%s filepath size\n",argv[0]);
        return 1;
    }
    int fd = open(argv[1],O_RDWR|O_CREAT,0666);  // 如果文件存在就打开该文件,如果文件不存在就创建一个权限为0666的文件
    ftruncate(fd,stoi(argv[2]));   // 因为我们直接使用open函数创建的文件大小为0,所以在对该文件进行映射前需要使用该函数对文件大小进行扩充。
    void* buff = mmap(NULL,stoi(argv[2]),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(buff == NULL){
        perror("mmap error");
        return 1;
    }
   
    string s;
    cin >> s;
    strcpy(static_cast<char*>(buff),s.c_str());   // 对映射的内存内容增添内容s
    munmap(buff,stoi(argv[2]));
    close(fd);
}

使用共享内存创建文件一定要注意在对文件内容进行映射之前,需要扩充文件大小。

使用共享内存进行进程间通信

以上我们了解了共享内存的基本的函数用法,接下来我们来学习如何使用共享内存进行进程间通信。

我们首先创建两个进程,工作代码如下:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace std;

// 共享内存:让父子进程共享内存
int main(){
    int n = 0;
    if(fork() == 0){
        // 子进程
        for(int i=0; i<10; i++)
        cout << getpid() << ":" << ++n << endl;
    }else{
        // 父进程
        for(int i=0; i<10; i++)
        cout << getpid() << ":" << --n << endl;
    }
}

运行结果如下:
在这里插入图片描述

我们可以看出来这两个进程独自运行,互不干扰。

接下来,我们创建一个共享内容,让该父子进程同时访问该内存上的内容。

创建步骤:

  1. 打开/dev/zero文件,这是一个用作共享内存的特殊文件;
  2. 使用mmap函数进行映射,创建共享内存,在这个实验中我们只需要一个初值为0的整数,所以我们申请了一个int型整数的内存大小;
  3. 申请好了内存,接下来我们如何将其作为一个变量来使用呢?借助&来为这块内存上的内容起别名就如下文代码中一样,或者直接对buff进行类型转化为int*型的内存地址,对其进行修改;
  4. 让两个进程在同一个内存上进行操作,输出操作结果;
  5. 结束映射;

代码如下:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace std;

// 共享内存:让父子进程共享内存
int main(){

    int fd = open("/dev/zero",O_RDWR);  // /dev/zero创建共享内存的一个特
殊文件
    if(-1 == fd){  // 判断文件是否打开成功
        perror("open error");
        return 1;
    }
    void* buff = mmap(NULL,sizeof(int),PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
    if(MAP_FAILED == buff){  // 判断文件是否映射成功
        perror("mmap error");
        return 1;
    }
    int &n = *static_cast<int*>(buff);
    if(fork() == 0){
        for(int i=0; i<10; i++)
        cout << getpid() << ":" << ++n << endl;
    }else{
        for(int i=0; i<10; i++)
        cout << getpid() << ":" << --n << endl;
    }

    munmap(buff, sizeof(int));
    buff = NULL;
    close(fd);
}

运行结果如下:

在这里插入图片描述

从运行结果我们就可以看出该父子进程是在同一个内存上进行的。

除了以上这种创建共享内存之外,我们还可以使用另外一种风格,这两种方法效果相同,主要区别是不需要打开文件,直接创建一个匿名共享内存,创建方法相对简单,但是这种匿名共享内存只能在亲缘进程中使用。

申请匿名的共享内存和普通的创建共享内存的一个很大的区别在于不需要打开一个文件进行映射获取内存地址,匿名的共享内存直接在映射时获取,和普通mmap的参数不同的是映射对象类型的不同,它的类型为MAP_SHARED|MAP_ANON

具体代码如下:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace std;

// 共享内存:让父子进程共享内存
int main(){
    void* buff = mmap(NULL,sizeof(int),PROT_WRITE|PROT_READ,MAP_SHARED|MAP_ANON,-1,0);   // 申请匿名共享内存
    if(MAP_FAILED == buff){  // 判断文件是否映射成功
        perror("mmap error");
        return 1;
    }
    int &n = *static_cast<int*>(buff);
    if(fork() == 0){
        for(int i=0; i<10; i++)
        cout << getpid() << ":" << ++n << endl;
    }else{
        for(int i=0; i<10; i++)
        cout << getpid() << ":" << --n << endl;
    }

    munmap(buff, sizeof(int));
    buff = NULL;
}

执行结果如下:

在这里插入图片描述

创建可命名的共享内存

我们知道匿名的共享内存只能父子进程才能使用,那么非亲缘进程该怎么使用共享内存呢?这一部分我们就需要写一个又名的共享内存,使用这个命名的共享内存进行通信。

这就用到了我们开头提及的shm_open函数,在这一节我们会写三个文档,分别用于创建共享内存、向共享内存中写数据以及读出共享内存中的数据。具体如下:

create_shared_memory.cpp:创建共享内存

这一部分的主要函数是shm_open,可以使用它打开一个共享内存。

代码内容如下:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace std;

int main(int argc, char* argv[]){
    if(3 != argc){
        printf("Usage:%s name size\n",argv[0]);
        return 1;
    }
    int fd = shm_open(argv[1],O_CREAT|O_RDWR,0666);  // 打开一个共享内存
 
    if(-1 == fd){
        perror("shm_open error");
        return 1;
    }
    ftruncate(fd,stoi(argv[2]));  // 一定要扩充内存大小
    close(fd);
}

然后我们在终端链接库执行g++ create_shared_memory.cpp -lrt运行程序,然后再做以下命令:

在这里插入图片描述
我们发现该文件创建后默认在dev/shm文件夹下。

shm_write.cpp:使用共享内存写数据

这里我们还是直接使用shm_open来获取共享内存的文件,但是需要注意的是,在创建的时候我们使用的是标志参数为创建O_CREAT和读写O_RDWR,而在读取的时候使用的标志为O_RDWR

文件内容如下:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace std;

int main(int argc, char* argv[]){
    int fd = shm_open(argv[1], O_RDWR, 0);

    if(-1 == fd){
        perror("shm_open error");
        return 1;
    }
    void* buff = mmap(NULL, sizeof(int), PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
    if(MAP_FAILED == buff){
        perror("mmap error");
        return 1;
    }
    cin >> *(int*)buff;
    munmap(buff, sizeof(int));
    close(fd);
}

我们在终端执行如下命令:

在这里插入图片描述需要注意的是,执行命令./a.out /abc中的/abc并不是指根目录下的abc文件,而是指使用shm_open创建的文件的默认路径下。

shm_read.cpp:使用共享内存读数据

在读数据时,我们使用shm_open函数打开共享内存的时候,其中的标志参数为O_RDONLY

文件内容如下:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace std;

int main(int argc, char* argv[]){
    int fd = shm_open(argv[1], O_RDONLY, 0);

    if(-1 == fd){
        perror("shm_open error");
        return 1;
    }
    void* buff = mmap(NULL, sizeof(int), PROT_READ, MAP_SHARED, fd, 0);
    if(MAP_FAILED == buff){
        perror("mmap error");
        return 1;
    }
    cout << *(int*)buff << endl;
    munmap(buff, sizeof(int));
    close(fd);
}

终端执行命令如下:

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐