mmap基本用法和共享内存

用mmap写文件及共享内存

Posted by bbkgl on March 23, 2020

莫道男儿心如铁

君不见满川红叶

尽是离人眼中血

前面写了一篇文章关于luajit2.05中的内存限制,有讲到mmap分配内存中的一些问题,今天就自己动手过一下mmap文件映射内存以及mmap如何共享内存。

文件映射内存

mmap简单例子

/dev/zero里写是不用考虑文件的大小的,所以很方便,如果是父子进程之间的共享内存的话,使用/dev/zero映射是非常好的选择。

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

int main() {
    int fd = open("/dev/zero", O_RDWR, 0);
    void *addr = mmap(nullptr, 4, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
    printf("%d\n", *(int *)addr);
    munmap(addr, 4);
    close(fd);
    return 0;
}

但如果是普通文件,用mmap进行映射的话,就是要考虑文件的大小的,实际操作的内存长度不能大于文件的长度,可以使用stat函数获取文件的信息,包括文件大小等。

首先把文件准备如下:

这里是6个字符的长度,我们用mmap把文件修改成同等长度的“2333”,通过映射到内存的方式进行修改。

20200324232127.png

然后可以看到,文件大小为6:

20200324232212.png

所以首先获取到文件的长度,然后映射到内存中,然后在该范围内操作内存。

下面代码给出了例子:

#include <cstdio>
#include <sys/mman.h>
#include <cstring>
#include <iostream>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int get_file_size(const char *file) {
    struct stat statbuf;
    if (stat(file, &statbuf) == 0)
        return statbuf.st_size;
    return -1;
}

int main() {
    std::string str = "2333333333333333333333333333";
    const char filename[] = "2333.txt";
    int fd = open(filename, O_RDWR, 0);
    int len = get_file_size(filename);
    void *addr = mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
    strncpy((char *)addr, str.c_str(), len);
    munmap(addr, 4);
    close(fd);
    return 0;
}

执行后,看到文件发生了变化:

20200324232640.png

内容由原来的“000000”变成了“233333”,且等长。

其实,mmap分配的内存是存在页对齐,比如我的机器上是4K对齐,那无论申请的内存大小是4k还是1字节,实际上分配出去的都是4K

修改文件大小

很多时候,我们希望能够直接修改文件的大小,以便于能够当内存访问文件时,不会“越界”。。

lseek能够帮忙做到这件事,其实lseek只是移动文件的指针,我们这里可以当做修改文件大小来用。

还是之前的例子,文件大小初始只有6字节,然后用lseek扩展到要操作的内存等长。当然,lseek是不会真的修改文件大小的,只是移动了文件指针,到了真正写入的时候才会改变大小,所以配合write来用。。

示例代码如下:

#include <cstdio>
#include <sys/mman.h>
#include <cstring>
#include <iostream>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int get_file_size(const char *file) {
    struct stat statbuf;
    if (stat(file, &statbuf) == 0)
        return statbuf.st_size;
    return -1;
}

int main() {
    std::string str = "2333333333333333333333333333";
    const char filename[] = "2333.txt";
    int fd = open(filename, O_RDWR, 0);
    int len = get_file_size(filename);
    void *addr = mmap(nullptr, str.length(), PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
    lseek(fd, str.length(), SEEK_SET);
    write(fd, "", 1);
    strcpy((char *)addr, str.c_str());
    munmap(addr, str.length());
    close(fd);
    return 0;
}

然后文件就被修改了:

20200324234540.png

共享内存

我感觉共享内存和之前的文件映射是有一定的联系的。

共享内存的原理大概可以如下图:

20200325225928.png

简单来说就是不同的虚拟空间,映射到了同一个物理空间,中间经过各自的用户级页表。

父子进程

比较简单的就是在父子进程间使用共享内存,这样的话其实两个进程其实操作的虚拟地址值都是一样的。

下面代码简单演示了:

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

int main() {
    int fd = open("/dev/zero", O_RDWR, 0);
    void *addr = mmap(nullptr, 4, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
    if (fork() == 0) {
        *(int *)addr = 2333;
        printf("val in process %d is %d.\n", getpid(), *(int *)addr);
        munmap(addr, 4);
        close(fd);
        exit(0);
    }
    wait(nullptr);
    printf("val in process %d is %d.\n", getpid(), *(int *)addr);
    munmap(addr, 4);
    close(fd);
    return 0;
}

然后输出:

20200325230808.png

很明显共享内存通信成功了。

非亲缘关系进程

简单来说就是不同的进程打开同一个文件,然后分别用mmap进行映射,这样操作的就是同一块内存了。

进程1,先执行,等addr存储的值被改成2333:

#include <cstdio>
#include <sys/mman.h>
#include <cstring>
#include <iostream>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    int fd = open("2333.txt", O_RDWR, 0);
    void *addr = mmap(nullptr, 4, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
    while ((*(int *)addr) != 2333) {
        printf("Wait for other process(1 second)!\n");
        fflush(stdout);
        sleep(1);
    }
    printf("from other process: %d\n", *(int *)addr);
    munmap(addr, 4);
    close(fd);
    return 0;
}

进程2,将addr存储的值改成2333:

#include <cstdio>
#include <sys/mman.h>
#include <cstring>
#include <iostream>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    int fd = open("2333.txt", O_RDWR, 0);
    void *addr = mmap(nullptr, 4, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
    (*(int *)addr) = 2333;
    munmap(addr, 4);
    close(fd);
    return 0;
}

输出如下:

20200325233828.png

可以看到进程1存储在addr被进程2修改了。。。