学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

MMKV分析mmap

基本概念

系统调用

为了保证每一个进程都能安全的执行。现代OS中,CPU运行有两种模式:“用户模式”与“内核模式”。

内核模式下,应用具有对硬件的所有控制权,可以执行所有CPU指令,可以访问任意地址内存,在内核模式下的任何异常都是灾难性的,将会导致整台机器停机。

一些容易发生安全问题的操作都被限制在只有内核模式下才可以执行,例如I/O操作,修改基址寄存器内容等。

用户模式下,应用没有对硬件的直接控制权,也不能直接访问地址的内存,程序是通过调用系统API来达到访问硬件和内存,这种保护模式下,即使应用发生崩溃也是可以恢复的。

应用程序代码运行在用户模式下,当应用程序需要实现内核模式下的指令时,先向系统发送调用请求,操作系统收到请求后,执行系统调用接口,使处理器进入内核模式,当处理器完成系统调用操作后,OS会让处理器返回用户模式,继续执行用户代码。

连接用户模式和内核模式的接口称之为系统调用

应用程序中十大对文件的操作过程就是典型的系统调用过程。

虚拟文件系统 VFS

一个操作系统可以支持多种底层不同的文件系统(比如NTFS, FAT, ext3, ext4),通过使用同一套I/O系统调用即可对Linux中的任意文件进行操作而无需考虑其所在的具体文件系统格式,Linux在用户进程和底层文件系统之间加入了一个抽象层,即虚拟文件系统(Virtual File System, VFS),进程所有的文件操作都通过VFS,由VFS来适配各种底层不同的文件系统,完成实际的文件操作。

Linux进程的虚拟内存

进程的虚拟地址空间可分为两部分,内核空间和用户空间,内核空间中放的是内核代码和数据,而进程的用户空间中存放的是用户代码和程序。

1
2
3
4
5
1.虚拟的意思是进程以为自己有这么一大块内存,实际上物理内存可能还没有分配给它,等到缺页异常是系统才会分配,
通过这种以时间换空间的方式提高了内存利用效率。从虚拟内存到物理内存的映射过程需要一个专门的硬件单元MMU来完
成。
2. 系统调用的代码和数据就在内核虚拟内存中,
3. 因为在保护模式下,用户态进程无法访问到这里,必须要通过系统调用的方式陷入到内核态才行。

MMKV

MMKV是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。

原理:

  1. 内存准备
    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。
  2. 数据组织
    数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。
  3. 写入优化
    考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力。我们考虑将增量 kv 对象序列化后,append 到内存末尾。
  4. 空间增长
    使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。我们需要在性能和空间上做个折中。

mmap

为什么mmap()可以节约IO读写时间?

  • 常规文件读写流程
  1. 读文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1、进程调用库函数向内核发起读文件请求;

2、内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项;

3、调用该文件可用的系统调用函数read()

3、read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode;

4、在inode中,通过文件内容偏移量计算出要读取的页;

5、通过inode找到文件对应的address_space;

6、在address_space中访问该文件的页缓存树,查找对应的页缓存结点:

(1)如果页缓存命中,那么直接返回文件内容;

(2)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;

7、文件内容读取成功。
  1. 写文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
写文件

前5步和读文件一致,在address_space中查询对应页的页缓存是否存在:

6、如果页缓存命中,直接把文件内容修改更新在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。

7、如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,进行第6步。

8、一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:

(1)手动调用sync()或者fsync()系统调用把脏页写回

(2)pdflush进程会定时把脏页写回到磁盘

同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
  • 内存映射读写流程
    具体操作方式是:open一个文件,然后调用mmap系统调用,将文件的内容的全部或一部分直接映射到进程的地址空间,映射完成后,进程可以像访问普通内存一样做其他的操作,比如memcpy等等。mmap并不分配物理地址空间,它只是占有进程的虚拟地址空间。这跟常规文件读写方式不一样的,常规文件读写方式需要预先分配好物理内存,内核才能将页高速缓冲中的文件数据拷贝到用户进程指定的内存空间中。

而内存映射读写方式,当多个进程需要同时访问同一个文件时,每个进程都将文件所存储的内核高速缓冲映射到自己的进程地址空间。当第一个进程访问内核中的缓冲区时候,前面讲过并没有实际拷贝数据,这时MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,就会触发缺页中断。内核将文件的这一页数据读入到内核高速缓冲区中,并更新进程的页表,使页表指向内核缓冲中的这一页。之后有其他的进程再次访问这一页的时候,该页已经在内存中了,内核只需要将进程的页表登记并且指向内核的页高速缓冲区即可。

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
#include <sys/mman.h> /* for mmap and munmap */
#include <sys/types.h> /* for open */
#include <sys/stat.h> /* for open */
#include <fcntl.h> /* for open */
#include <unistd.h> /* for lseek and write */
#include <stdio.h>

int main(int argc, char **argv)
{
int fd;
char *mapped_mem, * p;
int flength = 1024;
void * start_addr = 0;

fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
flength = lseek(fd, 1, SEEK_END);
write(fd, "", 1); /* 在文件最后添加一个空字符,以便下面printf正常工作 */
lseek(fd, 0, SEEK_SET);
mapped_mem = mmap(start_addr, flength, PROT_READ, //允许读
MAP_PRIVATE, //不允许其它进程访问此内存区域
fd, 0);

/* 使用映射区域. */
printf("%s", mapped_mem); /* 为了保证这里工作正常,参数传递的文件名最好是一个文本文件 */
close(fd);
munmap(mapped_mem, flength);
return 0;
}

mmap和常规文件操作的区别

  1. 常规文件读写
  • 两次拷贝 (磁盘->内核,内核->用户态)
    常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制,这是由OS控制的。这样造成读文件时需要先将
    文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数
    据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任
    务。
    写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘
    中(延迟写回),也是需要两次数据拷贝。

  • 当存在多个进程同时读取同一个文件时,每一个进程中的地址空间都会保存一份副本,这样肯定不是最优方式的,造成了物理内存的浪费

  1. mmap文件操作

内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<—->用户空间两者之间需要大量数据传输等操作的话效率是非常高的

1
2
3
使用mmap操作文件中,由于不需要经过内核空间的数据缓存,只使用一次数据拷贝,就从磁盘中将数据传入内存
的用户空间中,供进程使用。
mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。

文章参考

  1. 从内核文件系统看文件读写过程
  2. 认真分析mmap:是什么 为什么 怎么用
  3. linux内存映射mmap原理分析
  4. 理解mmap