1.mmap()
mmap()系统调用在调用进程的虚拟地址空间中创建一个新的内存映射。
映射分2种:
1.文件映射:
文件映射将一个文件的一部分直接映射到调用进程的虚拟内存中。
2.匿名映射:
一个匿名映射没有对应的文件。相反,这种映射的分页会被初始化为0.
一个进程的映射中的内存可以与其他进程中的映射共享(即各个子进程的页表条目指向 RAM 中相同的分页)。这种情况会存在2种情况发生:
1.当两个进程映射了一个文件的同一个区域时,它们会共享物理内存的相同分页
2.通过 fork() 创建的子进程会继承父进程的映射的副本,并且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同。
当2个或者多个进程共享相同分页时,每个进程都有可能看到其他进程对分页内容作出的改变,这当然要取决于映射是私有的还是共享的:
1.私有映射(MAP_PRIVATE):
在映射内容上发生的变更对其他进程是不可见的,对于文件映射来说,变更将不会在底层文件上进行。尽管一个私有映射的分页在上面介绍的情况中,
初始时是共享的,但对映射内容作出的变更对各个进程来讲是私有的。内核使用了写时复制技术完成了这个任务。这意味着,当一个进程试图修改一个分页
的内容时,内核首先会为该进程创建一个新分页,并将需要修改的分页中的内容复制到新分页中(以及调整进程的页表)。正因为这个原因,MAP_PRIVATE
映射会被称为私有,写时复制映射。
2.共享映射(MAP_SHARED):
在映射内容上发生的变更对所有共享同一个映射的其他进程都是可见的,对于文件映射来说,变更将会发生在底层文件上。
私有文件映射:
映射的内容被初始化为一个而文件区域的内容。多个映射同一个文件的进程初始时会共享同样的内存物理分页,但系统使用写时复制技术使得一个进程对映射所做的变更
对其他进程不可见。这种映射的主要用途是使用一个文件的内容来来初始化一块内存区域。一些常见的例子包括根据二进制可执行文件或共享文件的相应部分来初始化一个进程
的文件和数据段。
私有匿名映射:
每次调用 mmap() 创建一个私有映射时都会产生一个新映射,该映射与同一进程创建的其他匿名映射是不同的(即不会共享物理分页)。尽管子进程会继承父进程的映射,但
写时复制语义确保了 fork() 之后父进程和子进程不会看到其他进程对映射所做的变更。匿名映射的主要用途是位一个进程分配新(用0填充)的内存(如在分配大块内存时用 malloc()
会为此使用 mmap())。
共享文件映射:
所有映射一个文件同一区域的进程会共享同样的内存物理分页,这些分页的内容将被初始化为该文件区域。对映射内容的修改将直接在文件中进行。这种映射主要用于2个用途:第一,
它允许内存映射IO, 这表示一个文件会被加载到进程的虚拟内存中的一个区域中并且对该区域的变更会自动被写入到这个文件中。因此,内存映射IO 为使用 read(),write()来执行文件IO
这种做法提供了一种替代方案。这种映射的第二个用途是允许无关进程共享一块内容以便以一种类似于System V 共享内存段的方式来执行(快速)IPC。
共享匿名映射:
与私有匿名映射一样,每次调用 mmap() 创建一个共享匿名映射时都会产生一个新的,与任何其他映射不共享分页的截然不同的映射。这里的区别是映射的分页不会被写时复制。这意味着,
当一个子进程在 fork() 之后继承映射,父进程和子进程共享同一的 RAM分页,并且一个进程对映射内容所作出的变更会对其他进程可见。共享匿名映射允许以一种类似于 System V 共享
内存段的方式来进行 IPC, 但只有相关进程可以这么做。
区别:
私有指同一个进程?
私有匿名映射 和 共享匿名映射区别: 有没有写时复制,写时复制确保 父子进程对映射区域的变更不可见。而共享匿名映射,没有写时复制,父子进程对相关映射是可见的。
私有:只有自己可见
共享:其他进程也可见
私有:每次都是新的映射
共享:共享映射
一个进程在执行 exec() 后映射会丢失,但通过 fork() 创建的子进程会继承映射,映射类型(MAP_PRIVATE 和 MAP_SHARED) 也会被继承。
2.文件映射
创建一个文件映射的步骤:
1.获取一个文件的描述符,通常通过调用 open() 来获取
2.将文件描述符作为 fd 参数,传入 mmap() 中
执行上面操作之后,mmap() 会将打开的文件的内容映射到调用进程的地址空间。一旦 mmap() 被调用之后就能够关闭文件描述符了,而不会对映射产生任何影响。
私有文件映射用途:
1.允许多个执行统一程序或者使用同一个共享库的进程共享同样(只读的)文本段,它是从底层可执行文件或库文件的相应部分映射而来的。 //文本段
2.映射一个可执行文件或共享库的初始化数据段。这种映射会被处理成私有,使得对映射数据段内容的变更不会发生在底层文件上。//全局数据段
mmap() 的这2种用法通常对程序是不可见的,因为这些映射是由程序加载器和动态链接器创建的。
共享文件映射:
当多个进程创建了同一个文件区域的共享映射时,它们会共享同样的内存物理分页。此外,对映射内容的变更将会反应到文件上。
共享文件映射存在2个用途:
1.内存映射IO
由于共享文件映射中的内容是从文件初始化而来,并且对映射内容所做的变更都会自动反应到文件上,因此可以简单的通过访问内存中的字节来执行文件IO,而
依靠内核来确保对内存的变更会被传递到映射文件上。(一般来说,一个程序会定义一个结构化数据类型来与磁盘文件中的内容对应起来,然后使用该数据类型来转换
映射的内容)这项技术被称为内存映射IO,它是使用 read() 和 write() 来访问文件内容这种方法的替代方案。
内存映射IO具备2个潜在的优势:
1.使用内存访问来代替 read() 和 write() 系统调用能够简化一些应用程序的逻辑
2.在一些情况下,它能够比使用传统的 IO 系统调用执行文件IO这种做法提供更好的性能。
内存映射IO之所以能够带来性能优势原因如下:
1.正常的 read,write需要2次传输:一次是在文件和内核高速缓冲区之间,另外一次是在高速缓冲区与用户空间缓冲区之间。使用 mmap() 就无需第二次传输了,
对于输入来讲,一旦内核将相应的文件块映射到内存之后,用户进程就能够使用这些数据了。对于输出来讲,用户进程仅仅需要修改内存中的内容,然后可以依靠
内核管理器来自动更新底层的文件。(少了一次传输)
2.除了节省了内核空间和用户空间之间的一次传输之外,mmap() 还能够通过减少所需使用的内存来提示性能。当使用 read,write时,数据将别保存到2个缓冲区
中:一个位于用户空间,另外一个位于内核空间。当使用 mmap() 时,内核空间和用户空间会共享同一个缓冲区。此外,如果多个进程正在同一个文件上执行IO,
那么它们通过使用 mmap() 就能够共享同一个内核缓冲区,从而能够节省内存的消耗。(减少使用的内存)
内存映射IO所带来的性能优势在大型文件中执行重复随机访问时最有可能体现出来。如果顺序的访问一个文件,并且假设执行IO时使用的缓冲区大小足够大以至于能够
避免执行大量的IO系统调用,那么与 read,write 相比,mmap() 带来的性能上的提升就非常有限或者说根本没有带来性能提升。性能提升之所以有限是因为不管使用何种
技术,整个文件内容在磁盘和内存之间只传输一次,效率的提升主要得益于减少了用户空间和内存空间的一次数据传输,并且与磁盘IO所需的时间相比,内存使用量的降低通常
是可以忽略的。
内存映射IO也有缺点。对于小数据量IO来讲,内存映射IO的开销(即映射,分页故障,接触映射以及更新硬件内存管理单元的超前转换缓冲器)实际上要比简单的read,write大。
此外,有些时候内核难以高效的处理可写入映射的回写。
2.IPC
由于所有使用同样文件区域的共享映射的进程共享同样的内存物理分页,因此共享文件映射的第二个用途就是作为一种IPC方法。这种共享内存区域与 System V 共享内存对象
之间的区别在于,区域上的内容的更变会反应到底层的映射文件上。这种特性对于那些需要共享内容在应用程序或系统重启时能够持久化的应用程序来说是非常有用的。
3.同步映射区域:msync(void *addr, size_t length, int flags)
内核会自动将发生在 MAP_SHARED 映射内容上的变更写入到底层文件中的,但默认情况下,内核不保证这种同步操作会在何时发生。
msync()系统调用能够让应用程序显示的控制何时完成共享映射与映射文件之间的同步。
flags 取值:
MS_SYNC : 执行一个同步的文件写入。这个调用会阻塞直到内存区域中所有被修改过的分页被写入底层磁盘为止。
MS_ASYNC : 执行一个异步写入。内存区域中被修改的分页会在后面某个时刻被写入磁盘并立即在相应文件区域中执行read()的其他进程可见。
另外一种区分这两个值的方式可以表述为,在 MS_SYNC操作之后,内存区域会与磁盘同步,而在 MS_ASYNC之后,内存区域仅仅是与内核高速缓冲区同步。
如果在 MS_ASYNC 操作之后不采取进一步的动作,那么内存区域中被修改过的分页最终会作为由 pdflush 内核线程执行的自动缓冲区刷新的一部分被写入磁盘。
在Linux 上存在2种更快的发动输出的方法,在 msync() 调用之后,可以在映射对应的文件描述符上执行一个 fsync() 或者 fdatasync()。这个调用会阻塞,直到
快速缓冲区与磁盘同步为止。
MS_INVALIDATE : 使映射数据的缓存副本无效。当内存区域中所有被修改过的分页被同步到文件中之后,内存区域中所有与底层不一致的分页会被标记为无效。当下次
引用这些分页时,会从文件中相应的位置处复制相应的分页内容。其结果是,其他进程对文件做出的所有更新将会在内存区域中可见。
4.匿名映射
匿名映射是一种没有对应文件的映射。
在Linux 中,使用 mmap() 创建匿名映射存在2种不同但等级的方法:
1.在 flags 中指定 MS_ANONYMOUS 并将 fd 指定为 -1.
2.打开 /dev/zero 设备文件,并将得到的文件描述符传递给 mmap()
/dev/zero 是一个虚拟的设备,从中读取数据总是返回0,而写到这个设备的数据总是被丢弃。
5.MAP_NORESERVE 和 过度利用交换空间
懒交换预留:
如果内核总是为此类映射分配(或者预留)足够的交换空间,那么很多交换空间可能会被浪费。相反,内核可以只在需要的时候用到映射分页的时候(即当应用程序访问分页时)为
它们预留交换空间,这种方法称为懒交换预留。它的一个优点是,应用程序总共使用的虚拟内存量能够超过RAM+交换空间的总量。
换个角度来看,懒交换空间允许交换空间被过度利用。
OOM 杀手:
上面提到的当使用懒交换空间时,如果应用程序试图使用整个映射的话就会导致内存被耗尽。在这种情况下,内核会通过杀死进程来缓解内存消耗情况。
内核中用来在内存被耗尽时选择杀死哪个进程的代码通常被称为 out-of-memory(OOM)杀手。
6.非线性映射: remap_file_pages
使用 mmap() 创建的文件映射是连续的:映射文件的分页与内存区域的分页存在一个顺序的,一对一的对应关系。
可以使用多个带 MAP_FIXED 标记的 mmap() 调用创建非线性映射。然后这种方法的伸缩性不够好,其问题在于其中的每个 mmap() 调用都会创建一个独立的内核虚拟内存区域(VMA)
数据结构。每个 VMA 的配置都需要花费时间并且会消耗一些不可交换的内核内存。此外,大量的VMA会降低虚拟内存管理器的性能。特别的,当存在数以万计的 VMA 时处理每个分页故障
所花费的时间会大幅提高。
/proc/PID/maps 中的每一行表示一个 VMA
mmap();
munmap();
mprotect();
msync();
fsync();
fdatasync();
mremap();
remap_file_pages();