内存映射

通过 mmap 系统调用,可以将文件映射到进程地址空间进行读写。

../../_images/d5ea69f80807e1989e714246c1289a8f.png

这样一来,文件读写就跟内存操作一样直观,无需繁琐的 IO 操作代码。内核负责在内存和磁盘之间同步数据,应用程序开发者完全不用关心其中的细节。此外,IO 系统调用的避免,或多或少提高了程序的执行效率。

接下来,我们以文件存储结构体为例,演示 mmap 系统调用的用法。

结构体定义如下:

student_info

  1. #define NAME_LEN_LIMIT 32
  2. struct student_info
  3. {
  4. char name[NAME_LEN_LIMIT];
  5. int age;
  6. int score;
  7. };

读写映射

例子代码是一个函数,将结构体写入到给定文件中,文件路径以及结构体字段均由函数参数给出:

write_info

  1. /**
  2. * Use system call mmap to write struct to file
  3. *
  4. * Arguments
  5. * path: path of the file for writing
  6. *
  7. * Returns
  8. * 0 if success, -1 if some error happened.
  9. **/
  10. int write_info(char *path, char *name, int age, int score)
  11. {
  12. // open file in read-write mode
  13. // if exists, truncate it; else, create with 644
  14. int fd = open(
  15. path,
  16. O_RDWR|O_CREAT|O_TRUNC,
  17. S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH
  18. );
  19. if (fd == -1) {
  20. perror("fail to open file");
  21. return -1;
  22. }
  23. // if no enough space, expand it
  24. if (expand_file(fd, sizeof(struct student_info)) == -1) {
  25. return -1;
  26. }
  27. // map the opened file to memory area
  28. struct student_info *infop = (struct student_info *)mmap(
  29. NULL,
  30. sizeof(struct student_info),
  31. PROT_WRITE,
  32. MAP_SHARED,
  33. fd,
  34. 0
  35. );
  36. if (infop == MAP_FAILED) {
  37. perror("fail to map file");
  38. return -1;
  39. }
  40. // copy name string
  41. strncpy(infop->name, name, NAME_LEN_LIMIT-1);
  42. infop->name[NAME_LEN_LIMIT-1] = '\0';
  43. // set other fields
  44. infop->age = age;
  45. infop->score = score;
  46. return 0;
  47. }

该函数先调用 open 系统调用以 读写模式 打开文件并清空(第 14 行处),文件不存在则创建,权限位为 644

然后调用 expand_file 函数扩展文件长度,确保至少能容纳一个 student_info 结构体( 第 25 行处 )。expand_file 设计细节不再深入讨论,请自行查看代码:mmio.c

接着调用 mmap 系统调用将文件映射进内存中(第 30 行处)。 mmap 各个参数说明如下:

  • addr ,内存起始地址,指定为 NULL 则由内核自行分配;
  • len ,内存映射区区长度;
  • prot ,内存区访问权限;
  • flags映射类型 标志位;
  • fd ,被映射文件描述符;
  • offset ,被映射文件偏移量;

其中,内存区访问权限可以是以下值的或操作组合:

  • PROT_NONE ,内存页 不可访问
  • PROT_READ ,内存页 可读
  • PROT_WRITE ,内存页 可写
  • PROT_EXEC ,内存页 可执行

映射类型 决定内存区修改是否对其他进程可见,以及是否应用至原文件:

  • MAP_SHARED ,修改对其他进程可见,并同步到文件;
  • MAPPRIVATE ,创建 私有写复制 ( _copy-on-write )映射,因此修改对其他进程不可见,也不同步至文件;

映射完毕后,对结构体的操作将被应用到文件(第 44-49 行)。

只读映射

只读映射与读写映射类似:

mmio.c

  1. /**
  2. * Use mmap to read struct from file
  3. *
  4. * Arguments
  5. * path: path of the file for reading
  6. *
  7. * Returns
  8. * 0 if success, -1 if some error happened.
  9. **/
  10. int read_info(char *path)
  11. {
  12. // open file in read only mode
  13. int fd = open(path, O_RDONLY);
  14. if (fd == -1) {
  15. perror("fail to open file");
  16. return -1;
  17. }
  18. // map the opened file to memory area
  19. struct student_info *infop = (struct student_info *)mmap(
  20. NULL,
  21. sizeof(struct student_info),
  22. PROT_READ,
  23. MAP_SHARED,
  24. fd,
  25. 0
  26. );
  27. if (infop == NULL) {
  28. perror("fail to map file");
  29. return -1;
  30. }
  31. // print info
  32. printf("Name: %s\n", infop->name);
  33. printf("Age: %d\n", infop->age);
  34. printf("Score: %d\n", infop->score);
  35. return 0;
  36. }

不同的地方在于:

  • 文件可以以只读模式打开(第 13 行处);
  • mmap 系统调用 prot 参数指定为 PROT_READ ;文件映射完毕后,即可直接访问结构体字段并输出,非常直观、便捷!

程序演示

下面,运行 mmio 程序,以读写模式映射文件并写入数据记录:

  1. $ ./mmio fasion.data 'Fasion Chan' 28 8

注意到,当前目录确实出现了我们写入的文件:

  1. $ ls
  2. fasion.data mmio mmio.c

运行 hexdump 命令查看文件内容:

  1. $ hexdump -C fasion.data
  2. 00000000 46 61 73 69 6f 6e 20 43 68 61 6e 00 00 00 00 00 |Fasion Chan.....|
  3. 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
  4. 00000020 1c 00 00 00 08 00 00 00 |........|
  5. 00000028

前两行为 name 字段,总共 32 字节,为以 0 结尾的字符串;最后一行为 age 以及 score 字段,均是 4 字节长整型,二进制内容表明这正是我们写入的。

接着,我们运行 mmio 程序,以只读模式映射文件并输出内容:

  1. $ ./mmio fasion.data
  2. Name: Fasion Chan
  3. Age: 28
  4. Score: 8

下一步

订阅更新,获取更多学习资料,请关注我们的 微信公众号

../../_images/wechat-mp-qrcode.png小菜学编程

参考文献