mmap使用 (更新中)

Last updated on 8 months ago

是什么

mmap 是一个符合 POSIX 标准的 系统调用,它允许将一个文件或者其他对象(如设备、匿名内存等)映射到进程的地址空间中,应用程序可以通过内存访问方式来读写文件或者与其他对象进行交互。

能解决什么问题?

  • io 性能优化; 将一个普通文件映射到内存中,需要对文件进行频繁读写时,用内存读写取代I/O读写,以获得较高的性能
  • 提供进程间共享内存及相互通信的方式,当多个进程将同一个对象映射到内存时,数据在所有进程之间共享
  • 进程间通信

怎么用?

函数申明

1
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr:指定映射区域的起始地址。通常设为 NULL,表示让系统自动选择合适的地址。

  • length:指定映射区域的长度,即映射的文件的大小。

  • prot: 指定映射区域的保护方式,即对映射区域的访问权限。可以使用以下标志的组合:

    • PROT_NONE:无权限,不能访问映射区域。
    • PROT_READ:可读权限,可以读取映射区域的内容。
    • PROT_WRITE:可写权限,可以写入映射区域的内容。
    • PROT_EXEC:可执行权限,可以执行映射区域的内容。
  • flags:指定映射区域的其他选项。常用的标志包括:

    • MAP_SHARED:表示映射区域可以被多个进程共享,对映射区域的修改会影响到其他进程。
    • MAP_PRIVATE:表示映射区域只能被当前进程访问,对映射区域的修改不会影响到其他进程。
    • MAP_ANONYMOUS:表示映射的是匿名内存而不是文件,此时 fdoffset 参数会被忽略。
    • MAP_FIXED:表示要求系统将映射区域映射到指定的地址 addr,如果无法满足要求,则 mmap() 函数会失败。
  • fd:文件描述符,用于指定要映射的文件。如果是匿名映射,则可以设为 -1

  • offset:文件偏移量,用于指定文件的起始偏移位置。

mmap() 函数的返回值是映射区域的起始地址,如果映射失败,则返回 MAP_FAILED

简单使用过程

  1. 打开文件: 首先,使用 open() 函数打开要映射的文件,并获取文件描述符。
  2. 获取文件大小: 使用 fstat() 函数获取文件的大小和其他相关信息。
  3. 映射文件到内存: 使用 mmap() 函数将文件映射到进程的地址空间中。在调用 mmap() 函数时,需要指定文件描述符、文件大小、映射权限等参数,并获得映射的指针。
  4. 访问文件内容: 一旦文件映射到内存中,就可以通过访问指针来读取或写入文件内容,就好像访问内存一样。
  5. 解除映射: 在不再需要访问文件内容时,使用 munmap() 函数解除文件的内存映射。
  6. 关闭文件: 最后,使用 close() 函数关闭文件描述符。

代码说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
const char *filename = "example.txt";
int fd;
struct stat sb;
char *file_contents;

// 打开文件
fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 获取文件信息 stat 里面包含了文件的元数据信息
if (fstat(fd, &sb) == -1) {
perror("fstat");
exit(EXIT_FAILURE);
}

// 映射文件到内存
//sb.st_size 为文件长度, 这里是映射整个文件
//PROT_READ 设定映射区域为 读
//MAP_PRIVATE 私有权限,只能被当前进程访问
//文件
file_contents = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (file_contents == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}

// 访问文件内容
printf("File contents:\n%s\n", file_contents);

// 解除映射
if (munmap(file_contents, sb.st_size) == -1) {
perror("munmap");
exit(EXIT_FAILURE);
}
// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}

return 0;
}

编译并执行,符合预期

1
2
3
4
5
root@ubuntu:/home# echo "test ddddddddd"> example.txt 
root@ubuntu:/home# gcc mmap.c -o mmap && ./mmap
File contents:
test ddddddddd
root@ubuntu:/home#

具体场景的使用:

物理设备映射到虚拟内存

物理设备映射到内存,即设备的物理地址映射到磁盘上,这样可以以普通的内存读写操作来访问设备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define DEVICE_FILE "/dev/sda" // 示例设备文件路径
#define BUFFER_SIZE (1024 * 1024) // 缓冲区大小为 1MB

int main() {
int fd;
struct stat sb;
char *buffer;

// 打开设备文件
fd = open(DEVICE_FILE, O_RDWR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 获取设备文件大小
if (fstat(fd, &sb) == -1) {
perror("fstat");
exit(EXIT_FAILURE);
}
// 映射设备文件到内存
buffer = mmap(NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (buffer == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}

// 从内存读取设备数据(示例读取前 1MB 数据)
printf("Reading data from device...\n");
for (int i = 0; i < BUFFER_SIZE; ++i) {
printf("%c", buffer[i]);
}
// 解除内存映射
if (munmap(buffer, BUFFER_SIZE) == -1) {
perror("munmap");
exit(EXIT_FAILURE);
}

// 关闭设备文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}

return 0;
}

频繁的文件 io访问

有个些场景 需要对文件进行频繁读写时使用,使用内存读写取代I/O读写,以获得较高的性能,用两个daemon 测试下,一个是用mmap 映射,指针操作,一个是直接操作fd;

mmap_example.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>

#define FILENAME "example.txt"
#define FILE_SIZE 1024 // 假设文件大小为 1KB
#define NUM_ITERATIONS 500 // 反复读写的次数

int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s <num1> <num2>\n", argv[0]);
return 1;
}

int fd;
char *file_memory;
clock_t start_total, end_total;
double total_time;
int size = atoi(argv[1]);
int count = atoi(argv[2]);
// 记录开始时间
start_total = clock();

// 打开文件
fd = open(FILENAME, O_RDWR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 映射文件到内存
file_memory = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_memory == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}

// 记录开始时间(mmap版本)
clock_t start_mmap = clock();

// 反复读写文件内容(mmap版本)
for (int i = 1; i <= count; ++i) {
// 读取文件内容
// printf("Iteration %d (mmap): File content before modification:\n%s\n", i, file_memory);

// 修改文件内容
sprintf(file_memory, "This is modified content for iteration %d (mmap).", i);

// sleep(1);
// printf("Iteration %d (mmap): Modified file content:\n%s\n", i, file_memory);
}

// 记录结束时间(mmap版本)
clock_t end_mmap = clock();

// 计算总的时间(mmap版本)
total_time = ((double) (end_mmap - start_mmap)) / CLOCKS_PER_SEC;

printf("Total time taken (mmap): %f seconds\n", total_time);

// 解除内存映射
if (munmap(file_memory,size) == -1) {
perror("munmap");
close(fd);
exit(EXIT_FAILURE);
}

// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}

return 0;
}

io_example.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>

#define FILENAME "example.txt"
#define FILE_SIZE 1024 // 假设文件大小为 1KB
#define NUM_ITERATIONS 500 // 反复读写的次数

int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s <num1> <num2>\n", argv[0]);
return 1;
}
int fd;
clock_t start_total, end_total;
double total_time;
int size = atoi(argv[1]);
int count = atoi(argv[2]);

// 记录开始时间
start_total = clock();

// 打开文件
fd = open(FILENAME, O_RDWR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

// 记录开始时间(传统IO版本)
clock_t start_io = clock();

// 反复读写文件内容(传统IO版本)
for (int i = 1; i <= count; ++i) {
char buffer[size];

// 读取文件内容
if (read(fd, buffer, size) == -1) {
perror("read");
close(fd);
exit(EXIT_FAILURE);
}

// 修改文件内容
sprintf(buffer, "This is modified content for iteration %d (IO).", i);

// 写入文件内容
if (write(fd, buffer, size) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}

}

// 记录结束时间(传统IO版本)
clock_t end_io = clock();

// 计算总的时间(传统IO版本)
total_time = ((double) (end_io - start_io)) / CLOCKS_PER_SEC;

printf("Total time taken (IO): %f seconds\n", total_time);

// 关闭文件(传统IO版本)
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}

return 0;
}


两者比对下来(反复读写1M 10000次)

  • 有使用 mmap的程序,大多数时间花费再 mmap映射上,以及进本打开关闭操作
  • 没有使用 mmap ,则时间花费再 read 、write系统调用(用户态和内核态之间的切换)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
root@ubuntu:/home# strace -c ./mmap_example 10240 100000
Total time taken (mmap): 0.006987 seconds
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
25.75 0.000120 20 6 mmap
13.52 0.000063 16 4 mprotect
11.59 0.000054 18 3 openat
10.30 0.000048 16 3 3 access
9.87 0.000046 15 3 close
5.36 0.000025 8 3 fstat
5.15 0.000024 12 2 munmap
5.15 0.000024 8 3 clock_gettime
3.65 0.000017 6 3 brk
3.22 0.000015 15 1 execve
2.79 0.000013 13 1 read
2.58 0.000012 12 1 arch_prctl
1.07 0.000005 5 1 write
------ ----------- ----------- --------- --------- ----------------
100.00 0.000466 34 3 total

root@ubuntu:/home# strace -c ./io_example 10240 100000
Total time taken (IO): 9.971689 seconds
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
71.36 5.038501 50 100001 read
28.64 2.021914 20 100001 write
0.00 0.000041 14 3 close
0.00 0.000035 12 3 brk
0.00 0.000018 6 3 fstat
0.00 0.000018 6 3 clock_gettime
0.00 0.000000 0 5 mmap
0.00 0.000000 0 4 mprotect
0.00 0.000000 0 1 munmap
0.00 0.000000 0 3 3 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 3 openat
------ ----------- ----------- --------- --------- ----------------
100.00 7.060527 200032 3 total
root@ubuntu:/home#

虽然不用比较也很明确知道…. 一个是映射到内存读写,而 read 通常将读取的数据读到buffer中,先是系统调用,陷入内核(每次使用read都要进入内核态,进行上下文切换),内核首先将文件数据从磁盘读入page cache缓存,再将数据从page cache拷贝到buffer中,相比下来,用mmap的方式肯定更占优势。

待更新………………

共享内存

原理是?


参考文章

https://eric-lo.gitbook.io/memory-mapped-io/shared-memory

https://zhuanlan.zhihu.com/p/640169233