嵌入式Hacker (es-hacker)

Embedded bsp developer enjoys thinking and hacking opensource and develop boards(NanoPi, LicheePi, RPi...)

0%

Linux系统编程_用mmap+数组的方式修改数据文件

正文开始前,先聊点非技术的东西,推荐2本生动有趣的书:

  • 《经济学原理 宏观经济学》,曼昆,豆瓣评分,9.3 4945人评价
  • 《经济学原理 微观经济学》,曼昆,豆瓣评分9.6,1879人评价

我想看这两本书原因是:Joel on Software 在一篇 给计算机系学生 的忠告文章里说,毕业之前你一定要修一门经济学课程,Joel( stackoverflow 的创始人 ) 的话在我心里有很重的分量,我愿意接受他的建议,甚至认为自己 应该在高中时 就阅读上面2本书。

这两本书在豆瓣上的评价极高:

一看到《经济学原理》这样的书名,我会立即把它归为异次元一类,碰都不会去碰,别说读它了,自从大学毕业后,对于任何此类教科书,我会毫不客气地说,滚!

这种情绪的对立既包含了对中国教育制度的控诉,又体现了对死读书无法改变命运的残酷现实的无奈。

工作约7到8年后,自我体会到和社会的逐渐脱节,这里不止是因为工作环境的隔离性和社交圈的狭窄,还有自身知识量的乏馈,光是义务教育和本科教育所建立的知识结构显然已经无法支撑起我的求知欲所对应的飞速发展的社会万物,可是学习从何开始呢?

话说到这里,志同道合的人应该知道怎么做了,我就不再讲废话了,下面回归到技术上。

以下是正文:

背景知识

mmap()系统调用在调用进程的虚拟地址空间中创建一个新内存映射( memory mappings ),映射分为两种:

  1. 文件映射( File mapping ):文件映射将一个文件的一部分直接映射到调用进程的虚拟内存中。一旦一个文件被映射之后就可以通过在相应的内存区域中操作字节来访问文件内容了。映射的分页会在需要的时候从文件中(自动)加载。这种映射也被称为基于文件的映射( file-based mapping )或内存映射文件( memory-mapped file )。

  2. 匿名映射( Anonymous mapping ):匿名映射没有对应的文件,这种映射的分页会被初始化为0。

我们可以使某个磁盘文件的内容看起来就像是内存中的一个数组。如果文件的内容是由多条 C 语言结构体描述的记录构成的话,你就可以通过访问结构体数组来更新文件的内容了。

创建映射: mmap()

mmap()系统调用在调用进程的虚拟地址空间中创建一个新映射,如果是文件映射的话,是通过文件描述符和文件关联在一起:

1
#include <sys/mman.h>
2
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

成功时 mmap() 会返回新映射的起始地址。发生错误时 mmap( )会返回 MAP_FAILED。

参数:

  • addr,用去指定内存区域的起始地址,为了可移植性,一般设为 NULL, 表示由系统自动决定内存区域的起始地址。不管采用何种方式,内核会选择一个不与任何既有映射冲突的地址。

  • len, 映射区域的长度。

  • prot,映射区域的访问权限,可选项包括 READ/WRITE/EXEC/NONE。

  • flags, 映射区域的可见性:

    • 私有映射(MAP_PRIVATE):在映射内容上发生的变更对其他进程不可见,对于文件映射来讲,变更将不会在底层文件上发生。
    • 共享映射(MAP_SHARED):在映射内容上发生的变更对所有共享同一个映射的其他进程都可见,对于文件映射来讲,变更将会发生在底层的文件上。
  • fd, 被映射的文件的文件描述符。

  • offset, 从该偏移位置开始映射。

更多知识点:

  • 在打开描述符fd引用的文件时必须要具备与prot和flags参数值匹配的权限。

  • offset参数指定了从文件区域中的哪个字节开始映射,它必须是系统分页大小的倍数。

  • 一旦mmap()被调用之后就能够关闭文件描述符了,而不会对映射产生任何影响。

  • 在Linux上,一个文件映射的分页会在首次被访问时被映射进内存。

  • 除了普通的磁盘文件,使用 mmap() 还能够映射各种真实和虚拟设备的内容,如硬盘、光盘以及/dev/mem。

同步映射区域:msync()

msync() 系统调用让应用程序能够显式地 在某种程度上 控制何时完成共享映射与映射文件之间的同步。

1
$ man msync
2
SYNOPSIS
3
    #include <sys/mman.h>
4
       int msync(void *addr, size_t length, int flags);
5
DESCRIPTION
6
       msync()  flushes  changes made to the in-core copy of a file that was mapped into memory using mmap(2) back to the filesystem.

参数:

  • addr和length参数指定了需同步的内存区域的起始地址和大小。

  • flags 的可取值:

    • MS_SYNC,执行一个同步的文件写入。会阻塞直到内存区域中所有被修改过的分页被写入到磁盘为止。
    • MS_ASYNC,执行一个异步的文件写入,会立即返回。内存区域中被修改过的分页会在后面某个时刻被写入磁盘并立即对在相应文件区域中执行 read() 的其他进程可见。
    • MS_INVALIDATE,使映射数据的缓存副本失效。其结果是其他进程对文件做出的所有更新将会在内存区域中可见。

更多知识点:

  • 如果映射是私有的,那么不修改被映射的文件。

  • 与其他内存映射函数一样,地址必须与页边界对齐。

  • MS_ASYNC 不保证何时写入,因为在后台运行的 IO elevator 算法试图通过合并和排序写入来最大程度地提高效率,以最大程度地提高 IO 的吞吐量。

演示 demo

分解代码

定义数据类型:

1
typedef struct {
2
    int integer;
3
    char string[24];
4
} RECORD;

构造数据文件 records.dat:

1
fp = fopen("records.dat","w+");
2
for(i=0; i<NRECORDS; i++) {
3
    record.integer = i;
4
    sprintf(record.string,"RECORD-%d",i);
5
    fwrite(&record,sizeof(record),1,fp);
6
}

映射文件 records.dat:

1
f = open("records.dat",O_RDWR);
2
mapped = (RECORD *)mmap(0, NRECORDS*sizeof(record), 
3
                        PROT_READ|PROT_WRITE, MAP_SHARED, f, 0);

通过结构体数组修改数据( RECORD-43->RECORD-243 )

1
mapped[43].integer = 243;
2
sprintf(mapped[43].string,"RECORD-%d",mapped[43].integer);
3
msync((void *)mapped, NRECORDS*sizeof(record), MS_ASYNC);

清理现场

1
munmap((void *)mapped, NRECORDS*sizeof(record));
2
close(f);

运行效果

1
$ gcc mmap.c -o mmap
2
$ ./mmap
3
$ ls
4
mmap  mmap.c  records.dat
5
$ strings records.dat
6
RECORD-0
7
RECORD-1
8
RECORD-2
9
...
10
RECORD-42
11
RECORD-243
12
RECORD-44
13
...

可以看到:RECORD-43 被成功修改为 RECORD-243。

演示的 demo 就这么简单,却需要如此多的背景知识,甚至还有大量的细节没在文中披露,以后再深入讲解吧。

相关参考

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对 嵌入式系统和开源软件 感兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:嵌入式系统砖家,一起来学习吧,关注或转发 都是对作者莫大的支持,谢谢大家,祝工作顺利~

这是一篇有趣的文章吗?

欢迎关注我的其它发布渠道