第49章 内存映射

49.1 概述

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

  • 文件映射 :文件映射将一个文件的一部分直接映射到==调用进程的虚拟内存中。==一旦一个文件被映射之后就可以通过在相应的区域中操作自己来访问文件内容了。映射的分页会在需要的时候性文件中自动加载。这种映射映射也被称为基于文件的映射或内存映射文件。

  • 匿名映射:一个匿名映射没有对应的文件,相反,这种映射的分页会被初始化为0,
    另一种看待匿名映射 的角度(并且也接近于事实)是把他看成是一个内容总是被初始化为0的虚拟文件的映射。
    一个进程的映射中的内存可以与其他进程的映射共享(即各个进程的页表条目指向RAM中相同分页),这种行为会在两种情况下发生。

  • 当两个进程映射了一个文件的同意个区域时他们会共享物理内存的相同分页。

  • 通过fork()创建的子进程会继承其父进程的映射的副本,并且这些映射所引用的物理内存分页与父进程中相应映射所引用的相同。
    当两个或更多个进程共享相同分页时,每个进程都有可能会看到其他进程对分页内容做处的变更,当然这要取决于映射是私有的还是共享的。

  • 私有映射(MAP_PRIVATE):在映射内容上发生的变更对其他进程不可见,对于文件映射来讲,变更将不会在底层文件上进行。尽管一个私有映射的分页在上面介绍的情况初始时是共享的,但对映射内容所作出的变更来讲则是私有的。内核使用了写时复制技术完成了这个任务。这意味着当一个进程试图修改一个分页内容时,内核会首先为该进程创建一个新分页并将需要修改的分页的内容复制到新分页中(以及调整进程的页表)。正因为这个原因,MAP_PRIATE映射有时候会被称为私有、写时复制映射。

  • 共享映射(MAP_SHARED):在映射内容上发生的变更对所有的共享同一个映射的其他进程可见,对于文件映射来讲,变更将会发生在底层的文件上
    表1 各种内存映射的用途
    这四种不同的内存映射的创建和使用方式如下所述。

  • 私有文件映射:映射的内容被初始化为一个文本区域的内容。多个映射同一文件的进程初始时会共享同样的内存物理分页,但系统使用写时复制技术使得一个进程对映射所作出的变更对其他进程不可见。这种映射的主要用途是使用一个文件的内容来初始化一块内存区域。一些常见的例子包括根据二进制可执行文件或共享库文件的相应部分来初始化一个进程的文本和数据段

  • 私有匿名映射:每次调用mmap()创建一个私有匿名映射时都会产生一个新映射,该映射与同一(或不同)进程创建的其他匿名映射是不同的(即不会共享物理分页)。尽管子进程会继承其父进程的映射,但写时复制语义确保在fork之后父进程和子进程不会看到其他进程所作出的变更。私有匿名映射的主要用途是为一个进程分配新(用0填充)内存(如在分配大内存块时malloc会因此而使用mmap()).

  • 共享文件映射:所有映射一个文件的同一区域的进程会共享同样的物理分页,这些分页的内容将被初始化为该文件区域,对问文件的修改将直接在文件中进行。这种映射主要有两个用途。第一,他允许内存映射I/O,这表示一个文件被加载到进程的虚拟内存的一个区域并且对该块内容的变更会被自动写入到这个文件中。因此内存映射I/O为使用read()和write()来执行问文件/O的这种做法提供了一种替代方案,这种映射的第二种用途是允许无关进程共享一块内容以便以一种类似于System V共享内存段的方式来执行快速IPC

  • 共享匿名映射:与私有匿名映射一样,每次调用mmap()创建一个共享匿名映射时都会产生一个新的,与任何其他映射不共享分页的截然不同的映射。这里的差别在于映射的分页不会被写时复制,这意味着当一个子进程在fork()之后继承映射时,父进程和子进程共享同样的RAM分页,并且一个进程对映射内容所做出的变更会对其他进程可见。共享匿名映射允许以一种类似于System V共享内存段的方式来进行IPC,但是只有相关进程才这么做。

  • 一个进程在执行exec()时映射会消失,但是通过fork()创建子进程会继承映射,映射类型(MAP_PRIVATE或者MAP_SHARED)也会被继承。

mmap()的另一个用途是与POSIX共享内存对象一起使用,它允许无关进程在不创建关联磁盘文件(共享文件映射时需要这样的文件)的情况下共享一块内存区域。

49.2 创建一个映射:mmap()

mmap()系统调用在调用进程的虚拟空间创建一个新映射。

#include<sys/mman.h>
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
	Return staring address of mapping on success,or MAP_FAILED on error

addr 参数映射被放置的虚拟地址。如果将addr指定为NULL,那么内核会为映射选择一个合适的地址。这是创建映射的首选做法。或者在addr中指定一个非NULL值时,内核会选择将映射放置在何处时将这个参数值作为一个提示信息来处理。在实践中,内核至少会将指定的地址舍入到最近的一个分页边界处。不管采用何种方式,内核会选择一个不与任何既有映射冲突的地址。(如果在flags包含了MAP_FIXED,那么addr必须是分页对齐的)。
成功时mmap()会返回新映射的起始地址。发生错误时mmap()会返回MAP_FAILED.
length参数指定了映射的字节数。尽管length无需是一个系统分页大小(sysconf(_SC_PAGESIZE)返回值)的倍数,但内核会议分页大小为单位创建映射,因此实际上length会被向上提升为分页大小的下一个倍数。
prot参数是一个位掩码,它指定了施加于映射之上的保护信息,其取值要么是PROT_NONE,要么是下表中其他三个标记的组合(OR)

描述
PROT_NONE区域无法访问
PROT_WRITE区域内容可读
PROT_WRITE区域内容可修改
PROT_EXEC区域内容可执行

flags参数是一个控制映射操作各个方面的选项的位掩码。这个掩码必须只包含下列值中一个。
MAP_PRIVATE
创建一个私有映射。区域中内容上所发生的变更对使用同一映射的其他进程是不可见的,对于文件映射来讲,所发生的变更将不会反应在底层文件上。
MAP_SHARED
创建一个共享映射,区域中内容上所发生的变更对使用MAP_SHARED特性映射同一区域的进程是可见的,对于文件映射来讲,所发生的变更直接反应在底层文件上。对于文件的变更将无法确保立即生效,具体参考 msyncx系统调用的介绍
剩余的参数fd和offset是用于文件映射 的(匿名映射将忽略他们)。fd参数是一个标识被映射的文件的文件描述符。offset参数指定了映射在文件中的起点,他必须是系统分页大小的倍数。要映射整个文件就需要将offset指定为0,并且将length指定为文件的大小。

有关内存保护的更多细节

前面提过mmap() prot参数指定了新内存映射上的保护信息。这个参数可以取PROT_NONE或者PROT_READ、PROT_WRITE、以及PROT_EXEC中一个或多个标记的掩码。如果一个进程在访问一个内存区域时,违反了该区域上的保护位,那么内核回想该进程发送一个SIGSEGV信号。
标记为PROT_NONE的分页内存的一个用途是作为一个进程分配的内存区域的起始位置或结束位置的守护分页,如果进程意外的访问了其中一个被标记为PROT_NONE的分页,那么内核会通过生成一个SIGSEGV信号来通知该信号一个事实。
内存保护信息驻留在进程私有的虚拟内存表中。因此不同的进程可能会使用不同的保护位来映射同一个区域。
标准中规定的对offset和addr的对其约束
SUSv3规定mmap()的offset参数必须要与分页对齐,而addr参数指定了MAP_FIXD的清醒也必须要与分页对齐,linux遵循了这些要求,但后面有限SUSv3de 的要求与之前的标准提出的要求是不同的,之前的标准对这些参数的要求低一些。SUSv3中的措辞会(不必要的)导致一些之前符合标准的不符合标准了。SUSv4放宽这方面的要求:

  • 一个实现可能会要求offset为系统分页大小的倍数
  • 如果指定了MAP_FIXED,那么一个实现可能会要求adr是对齐的
  • 如果指定了MAP_FIXED并且addr为非0值,那么addr和offset除以系统分页大小所得的余数应该相等。
//使用mmap()创建一个私有文件映射
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc char *argv[])
{
	char *addr;
	int fd;
	struct stat sb;
	if(argc !=2 || strcmp(argv[1],"--help") ==0)
	{
		printf("%s file\n",argv[0]);
	}
	fd = open(argv[1],O_RDONLY);
	if(-1 ==fd)
	{
		perror("open");
	}
	/*obtain the size of the file and use it to specify the size of the
	mapping and the size of the buffer to be written*/

	if(fstst(fd,&sb)==-1)
	{
		perror("fstat");
	}
	addr = mmap(NULL,sb.size,PROT_READ,MAP_PRIVATE,fd,0);
	if(addr == MAP_FAILED)
	{
		perror("mmap");
	}
	if(write(STDOUT_FILENO,addr,sb.st_size)!=sb.st_size)
	{
		perror("fatal write");
	}
retur 0;
}

49.3接触映射区域

munmap()系统调用执行与mmap相反的操作,即从调用进程的虚拟地址空间删除一个映射。

#include <sys/mman.h>
int munmap(void *addr,size_t length);
			Return 0 on success,or -1 on error

addr必须是搭接处映射的地址范围的起始地址,他必须与一个分页便捷对齐,
legth是一个非负整数,它指定了待解除区域的大小(字节数)。范围为系统分页大小的下一个倍数的地址空间将会被接触映射
一般来讲通常会解除整个映射,因此可以讲addr指定为上一个mmap()调用返回的地址,并且length的值与mmap()调用中使用的lengthy一样。
或者也可以基础一个映射中的部分映射,这样原来的映射要么会收缩,要么会分成两个,这取决于在何处开始接触映射,还可以指定一个跨越多个映射的地址范围,这样的话所有在范围内的映射都会被解除
如果在由addr和length指定的地址范围中不存在映射,那么munmap()将不起任何作用并返回0(表示成功)。
在接触映射期间,内核会删除进程持有的在指定范围内所有的内存锁(通过mlock或mlockall建立的)
当一个进程终止了或者执行了一个exec()之后进程中所有的映射会自动被解除。
为确保一个共享文件映射的内容会被写入到底层文件中,在使用munmap()接触一个映射之前需要调用msync()

49.4文件映射

要创建一个文件映射,需要执行下面的步骤
1,获取文件的描述符,通常通过调用open()来完成
2,将文件描述符fd 传入mmap()调用。
执行上述步骤后mmap()会将打开的文件的内容映射到调用进程的地址空间中。一旦mmap()被调用之后就能够关闭文件描述符了,而不会对映射产生任何影响。但在一些情况下,将这个文件描述符保持在打开状态可能是有用的----参见程序打开一个私有文件映射以及POSIX共享内存一章节

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

在打开描述符fd引用的文件时必须要具备与prot和flags参数值匹配的权限。特别的文件必须总是被打开以允许读取,并且在flags中指定了PROT_WRITE和MAP_SAHRED,那么文件必须总是被打开以允许读取和写入。
offset参数指定了从文件区域中哪个字节开始映射,它必须是系统分页大小的倍数。将offset指定为0回导致从文件的起始位置开始映射。length参数指定了映射的字节数。offset和length参数一起确定了文件的哪个区域会被映射进内存。如下图所示
内存映射文件概览

    在linux 上,一个文件映射的分页会在首次被访问时被映射进内存。这意味着如果在mmap()修改了文件区域,但映射的对应部分(分页)还没有被访问过,那么如果相应的分页还没有被加载进内存的话,便更对这个进程是可见的。这个行为是依赖于实现的,可移植的应用程序应该避免依赖某个特定内核在这种场景中的行为。

49.4.1 私有文件映射

私有文件映射最常见的两个用途如下所述:

  • 允许多个执行同一个程序或使用同一个共享库的进程共享(只读的)文本段,他是从底层可执行文件或库文件的相应部分映射而来的
尽管可执行文件的文本daunting同城被保护成只允许读取和执行访问(PROT_WRITE|PROT_EXEC),
但是在映射时仍然使用了MAP_PRIVATE而不是MAP_SHARED,这是因为调试器或自修改的程序能够修改
程序文本(在修改了内存上的保护信息之后),而这样的变更是不应该发生在底层文件上或是影响到其他进程的。
  • 映射一个可执行文件或共享库的初始化数据段。。这种映射会被处理成私有使得对映射数据段内容的变更不会发生在底层文件上
    mmap()的这两种用法通常对程序是不可见的,因为这些映射是由程序加载器和动态链接器创建的。

49.4.2 共享文件映射

当多个进程创建了同一个文件区域的共享映射时,他们会共享同样的内存物理分页。此外,对映射内容的变更会反映的文件上。实际上,这个文件被当成了该块内存区域的分页存储,如下图所示(下图经过简化,他并没有支出映射分页在物理内存中通常是不连续的这个事实)
两个进程和一个文件的同一区域的共享映射
共享文件映射存在两个用途:内存映射I/O和IPC,

内存映射I/O

由于共享文件映射中的内容是从文件初始化而来的,并且对映射内容所作出的变更都
会自动反映到文件上,因此可以简单的通过访问内存中的自己来执行文件I/O,而依靠内
核来确保对内存的变更会被传递到映射文件中。(一般来讲,一个程序会定义一个结构化数据类型来与磁盘文件中的内容对应起来,然后使用该数据类型来转换映射的内容)这种技术成为内存映射I/O,它是使用read()和write()来访问文件内容这种方法的替代方案。
内存映射I/O具备两个潜在的优势。

  • 使用内存访问来取代read()和write()系统调用能够简化一些应用程序的逻辑。

  • 在一些情况下,它能够比使用传统的I/O系统调用执行文件I/O这种做法提供更好的性能。
    内存映射I/O之所以能够带来性能优势的原因如下:

  • 正常的read(0或者write()需要两次传输,一次是在文件和内核高速缓冲区之间,另一次是在高速缓冲区和用户空间缓冲区之间,使用mmap()无需第二次传输了。对于输入来讲,一旦内核将相应的文件块映射进内存之后用户进程就能够使用这些数据了。对于输出来讲,用户进程仅仅需要修改内存中的内容,然后依靠内核内存管理器来自动更新底层的文件

  • 除了节省了内核空间和用户空间的一次传输之外,mmap()还能够通过减少所使用的内存来提升性能能,当使用read()和write()时,数据江北保存在两个缓冲区中:一个位于用户空间,另一个位于内核空间。当使用mmap()时,内核空间和用户空间会共享同一个缓冲区,此外如果多个进程正字啊同一个文件上执行I/O,那么他通过使用mmap()就能共享同一个内核缓冲区,从而又能减少内存的消耗。
    内存映射I/O所带来的性能优势在大型文件中执行随机访问是最有可能体现出来。如果顺序的访问一个文件,并假设执行I/O时使用的缓冲技术足够大小以至于能够避免执行大量的I/O系统调用,那么与write()和read()相比,mmap()所带来的性能上的提升就非常有限挥着根本没有带来性能上的提升。性能提升的幅度之所以非常有限的原因是不管使用何种技术,整个文件的内容在磁盘和内存之间只传输一次,效率上的提升主要得益于减少了用户空间和内核空间之间的一次数据传输,并且与磁盘I/O所需的时间相比,内存使用量的降低通常是可以忽略的。

内存映射I/O也有一些缺点。对于小数据量来说,内存映射I/O的开销(映射,分页故障,接触映射以及更新硬件内存管理单元的超前转换缓冲器)
使用共享文件的IPC

由于所有使用同样文件区域的共享映射的进程共享同样的物理分页,因此共享文件映射的第二个用途是作为一种快速的(IPC)方法。==这种共享内存区域与System V共享内存对象之间的区别在于区域中的内容上的变更会反映到底层文件上。==这种特性对那些需要共享内存内容在应用程序或系统重启时能够持久化的应用程序来讲是非常有用的。
下面程序演示了如何使用mmap()创建一个共享文件映射。

#include <sys/mman.h>
#include <fcntl.h>
#define MEM_SIZE 10
int main(int argc,char *argv[])
{
	char * addr;
	int fd;
	if(argc<2 || strcmp(argv[1],"--help") ==0)
	{
		printf("%S, file [new-alue]\n",argv[o]);
	}
	fd = open(argv[1],O_RDWR);
	if(fd == -1)
	{
		perror("open");
	}
	addr = mmap(NULL,MEM_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	if(addr == MAP_FAILED)
	{
		perror(mmap);
	}
	if(close(fd) == -1)
	{
		perror("close");ents of region
	}
	printf("Current string=%.*额cints\n",MEM_SIZE,addr);
	     //Secure practice:output at most MEM_SIZE bytes
	if(argc >2){ //updat
		if(strlen(argv[2])>=MEM_SIZE){
			printf("new-value too large");
		}
		memset(addr,argv[2],MEM_SIZE-1);
		if(msync(addr,MEM_SIZE,MS_SYNC) == -1)
			perror("msync");
		printf("copied \%s\ to shared memory\n",argv[2]);
	}
	exit(0);
}

这个程序首先映射一个名称通过第一个命令行参数指定的文件,然后打印出映射区域的起始位置的字符串值。最后,如果提供了第二个参数,那么该字符串就会被复制进共享内存zhong

使用程序映射文件并将一个字符串赋值进映射区域中
./mmap_s test.txt hello
Current string=;
Copied "hello"to shared memory

这个简单的程序没有使用任何机制来同步多个进程对映射文件的访问,但现实世界中的应用程序通常需要同步对共享映射的访问。

49.4.3 边界情况

在很多情况下,一个映射的大小是系统分页大小的整数倍,并且映射会完全落入映射文件的范围之内。但是这种要求不是必须的,下面看一下当这些条件不满足时会发生什么。
下图描绘了映射完全落入映射文件的范围之内但区域的大小并不是系统分页大小的一个整数的情况(在这个讨论中假设分页大小为4096字节)
length不是系统分页大小的整数倍的内存映射 由于映射的大小不是系统分页大小的整数倍,因此它会被向上舍入到系统分页大小的下一个整数倍。由于文件的大小要大于这个被向上舍入的大小,因此文件中对应字节会像图49-3中那样被映射。
试图访问映射结尾之外的字节将会导致SIGSEGV 信号的产生(假设在该位置处不存在其他映射)。这个信号的默认动作是终止进程并打印出一个core dump。
当映射扩充过了底层文件的结尾处时(参见图 49-4)情况就变得更加复杂了。与之前一样,由于映射的大小不是系统分页大小的整数倍,因此它会被向上舍入。但在这种情况下,虽然在向上舍入区域(即图中2200 字节和4095 字节)中的字节是可访问的,但它们不会被映射到底层文件上(由于在文件中不存在对应的字节),并且它们会被初始化为0(SUSv3 对此进行了规定)。当然,这些字节也不会与映射同一个文件的其他进程共享,即使它们指定了足够大的length 参数。对这些字节做出的变更不会被写入到文件中。
在这里插入图片描述 如果映射中包含了超出向上舍入区域中(即图49-4 中4096 以及之后的字节)的分页,那么试图访问这些分页中的地址将会导致SIGBUS 信号量的产生,即警告进程文件中没有区域与这些地址对应。与之前一样,试图访问超过映射结尾处的地址将会导致SIGSEGV 信号的产生。
从上面的描述中可以看出,创建一个大小超过底层文件大小的映射可能是无意义的。但通过扩展文件的大小(如使用ftruncate()或write()),可以使得这种映射中之前不可访问的部分变得可用。

49.4.4 内存保护和文件访问模式交互

到目前为止还没有详细解释的一点是通过mmap() prot 参数指定的内存保护与映射文件被打开的模式之间的交互。从一般原则来讲,PROT_READ 和PROT_EXEC 保护要求被映射的文件使用O_RDONLY 或O_RDWR 打开,而PROT_WRITE 保护要求被映射的文件使用O_WRONLY 或O_RDWR 打开。
然而,由于一些硬件架构提供的内存保护粒度有限,因此情况会变得复杂起来(参见49.2节)。对于这种架构,下列结论是适用的。

  • 所有内存保护组合与使用 O_RDWR 标记打开文件是兼容的。
  • 没有内存保护组合——哪怕仅仅是PROT_WRITE——与使用O_WRONLY 标记打开的文件是兼容的(导致EACCES 错误的发生)。这与一些硬件架构不允许对一个分页的只写访问这样一个事实是一致的。在49.2 节中指出过在那些架构上PROT_WRITE隐含了PROT_READ,这意味着如果分页可写入,那么它也能被读取。而读取操作与O_WRONLY 是不兼容的,该操作是不能暴露文件的初始内容的。
  • 使用 O_RDONLY 标记打开一个文件的结果依赖于在调用mmap()时是否指定了
    MAP_PRIVATE 或MAP_SHARED。对于一个MAP_PRIVATE 映射来讲,在mmap()中可以指定任意的内存保护组合——因为对MAP_PRIVATE分页内容做出的变更不会被写入到文件中,因此无法写入文件不会成为问题。对于一个MAP_SHARED 映射来讲, 唯一与O_RDONLY 兼容的内存保护是PROT_REA 和(PROT_READ |PROT_EXEC)。这是符合逻辑的,因为一个PROT_WRITE, MAP_SHARED 映射允许更新被映射的文件。

49.5 同步映射区域:msync()

内核会自动将发生在MAP_SHARED 映射内容上的变更写入到底层文件中,但在默认情况下,内核不保证这种同步操作会在何时发生。(SUSv3 要求一个实现提供这种保证。)
msync()系统调用让应用程序能够显式地控制何时完成共享映射与映射文件之间的同步。同步一个映射与底层文件在多种情况下都是非常有用的。如,为确保数据完整性,一个数据库应用程序可能会调用msync()强制将数据写入到磁盘上。调用msync()还允许一个应用程序确保在可写入映射上发生的更新会对在该文件上执行read()的其他进程可见。

	#include <sys/mman.h>
	int msync(void *addr,size_t length,int flags);
			Returns 0 on success,or -2 on error

传给 msync()的addr 和length 参数指定了需同步的内存区域的起始地址和大小。在addr中指定的地址必须是分页对齐的,length 会被向上舍入到系统分页大小的下一个整数倍。(SUSv3 规定addr 必须要分页对齐。SUSv4 表示一个实现可以要求这个参数是分页对齐的。)
flags 参数的可取值为下列值中的一个。
MS_SYNC
执行一个同步的文件写入。这个调用会阻塞直到内存区域中所有被修改过的分页被写入到磁盘为止。
MS_ASYNC
执行一个异步的文件写入。内存区域中被修改过的分页会在后面某个时刻被写入磁盘并立即对在相应文件区域中执行read()的其他进程可见。
另一种区分这两个值的方式可以表述为在MS_SYNC 操作之后,内存区域会与磁盘同步,而在MS_ASYNC 操作之后,内存区域仅仅是与内核高速缓冲区同步。

如果在 MS_ASYNC 操作之后不采取进一步的动作,那么内存区域中被修改过的分页最终会作为由pdflush 内核线程(在Linux 2.4 以及之前的版本上是kupdated)执行的自动缓冲区刷新的一部分被写入到磁盘。在Linux 上存在两种更快的发动输出的(非标准)方法。在msync()调用之后可以在映射对应的文件描述符上调用一个fsync()(或fdatasync())。这个调用会阻塞直到快速缓冲区与磁盘同步为止。或者可以使用posix_fadvise()POSIX_FADV_DONTNEED 操作启动一个异步的分页写入。(Linux 特有的这两个操作并没有在SUSv3 中予以规定。)

在 flags 参数中还可以加上下面这个值。
MS_INVALIDATE
使映射数据的缓存副本失效。当内存区域中所有被修改过的分页被同步到文件中之后,内存区域中所有与底层文件不一致的分页会被标记为无效。当下次引用这些分页时会从文件的相应位置处复制相应的分页内容,其结果是其他进程对文件做出的所有更新将会在内存区域中可见。
与很多其他现代UNIX 实现一样,Linux 提供了一个所谓的同一虚拟内存系统。这表示内存映射和高速缓冲区块会尽可能地共享同样的物理内存分页。因此通过映射获取的文件视图与通过I/O 系统调用(read()、write()等)获得的文件视图总是一致的,而msync()的唯一用途就是强制将一个映射区域中的内容写入到磁盘。
不管怎样,SUSv3 并没有要求实现统一虚拟内存系统,并且并不是所有的UNIX 实现都提供了同一虚拟内存系统。在这类系统上需要调用msync()来使得一个映射上发生的变更对其他read()该文件的进程可见,并且在执行逆操作时需要使用MS_INVALIDATE 标记来使得其他进程对文件所做出的写入对映射区域可见。使用mmap()和I/O 系统调用操作同一个文件的多进程应用程序如果希望可被移植到不具备统一虚拟内存系统的系统之上的话就需要恰当使用msync()。

49.6 其他mmap()标记

除了MAP_PRIVATE 和MAP_SHARED 之外,Linux 允许在mmap() flags 参数中包含其他一些值(取OR)。表49-3 对这些值进行了总结。除了MAP_PRIVATE 和MAP_SHARED 之外,在SUSv3 中仅规定了MAP_FIXED 标记。
在这里插入图片描述 下面提供了与表49-3 中列出的flags 值有关的更多细节信息(不包含MAP_ PRIVATE 和MAP_SHARED,因为之前已经介绍过这两个标记了)。
MAP_ANONYMOUS
创建一个匿名映射,即没有底层文件对应的映射。在49.7 节中将会对这个标记进行深入介绍。
MAP_FIXED
在49.10 节中将会对这个标记进行介绍。
MAP_HUGETLB(自Linux 2.6.32 起)
这个标记在mmap()所起的作用与SHM_HUGETLB 标记在System V 共享内存段中所起的作用一样。参见48.2 节。
MAP_LOCKED(自Linux 2.6 起)
按照mlock()的方式预加载映射分页并将映射分页锁进内存。在50.2 节中将会对使用这个标记所需的特权以及管理其操作的限制进行介绍。
MAP_NORESERVE
这个标记用来控制是否提前为映射的交换空间执行预留操作。细节信息请参见49.9 节。
MAP_POPULATE(自Linux 2.6 起)
填充一个映射的分页。对于文件映射来讲,这将会在文件上执行一个超前读取。这意味着后续对映射内容的访问不会因分页故障而发生阻塞(假设此时不会因内存压力而导致分页被交换出去)。
MAP_UNINITIALIZED(自Linux 2.6.33 起)
指定这个标记会防止一个匿名映射被清零。它能够带来性能上的提升,但同时也带来了安全风险,因为已分配的分页中可能会包含上一个进程留下来的敏感信息。因此这个标记一般只供嵌入式系统使用,因为在这种系统中性能是一个至关重要的因素,并且整个系统都处于嵌入式应用程序的控制之下。这个标记只有在使用CONFIG_MMAP_ALLOW_ UNINITIALIZED 选项配置内核时才会生效。

49.7 匿名映射

匿名映射是没有对应文件的一种映射。本节将介绍如何创建匿名映射以及私有和共享匿名映射的用途。
MAP_ANONYMOUS 和/dev/zero
在Linux 上,使用mmap()创建匿名映射存在两种不同但等价的方法。

  • 在 flags 中指定MAP_ANONYMOUS 并将fd 指定为−1。(在Linux 上,当指定了
    MAP_ANONYMOUS 之后会忽略fd 的值。但一些UNIX 实现要求在使用MAP_
    ANONYMOUS 时将fd 指定为−1,因此可移植的应用程序应该确保它们这样做了。)
  • 打开/dev/zero 设备文件并将得到的文件描述符传递给mmap()。
    不管是使用MAP_ANONYMOUS 还是使用/dev/zero 技术,得到的映射中的字节会被初始化为0。在两种技术中,offset 参数都会被忽略(因为没有底层文件,所以也无从指定偏移量)。稍后将会介绍使用这两种技术的例子。
    MAP_PRIVATE 匿名映射
    MAP_PRIVATE 匿名映射用来分配进程私有的内存块并将其中的内容初始化为0。下面的代码使用/dev/zero 技术创建了一个MAP_PRIVATE 匿名映射。
fd = open("/dev/zero",O_RDWR);
if(fd == -1)
{
    perror("open");
    return -1;
}
addr = mmap(NULL,length,PORT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
if(addr == MAP_FAILED)
{
    perror("open");
    return -1;
}

glibc 中的malloc()实现使用MAP_PRIVATE 匿名映射来分配大小大于MMAP_
THRESHOLD 字节的内存块。这样在后面将这些内存块传递给free()之后就能高效地释放这些块(通过munmap())。(它还降低了重复分配和释放大内存块而导致内存分片的可能性。)MMAP_THRESHOLD 在默认情况下是128 kB,但可以通过mallopt()库函数来调整这个参数。

MAP_SHARED 匿名映射
MAP_SHARED 匿名映射允许相关进程(如父进程和子进程)共享一块内存区域而无需一个对应的映射文件。
下面的代码使用 MAP_ANONYMOUS 技术创建了一个MAP_SHARED 匿名映射。

addr = mmap(NULL,length,PORT_READ|PROT_WRITE,
        MAP_SHARED | MAP_ANONYMOUS,fd,0);
if(addr == MAP_FAILED)
{
    perror("open");
    return -1;
}

如果在上面的代码之后加上一个对fork()的调用,那么由于通过fork()创建的子进程会继承映射,两个进程就会共享内存区域。
示例程序
程序清单49-3 演示了如何使用MAP_ANONYMOUS 或/dev/zero 技术来在父进程和子进程之间共享一个映射区域。至于到底该选择何种技术则由在编译程序时是否定义了USE_MAP_ANON 来确定。父进程在调用fork()之前将共享区域中的一个整数初始化为1。然后子进程递增这个共享整数并退出,而父进程则等待子进程退出,然后打印出该整数的值。运行这个程序之后能看到下面这样的输出。
在这里插入图片描述程序清单49-3:在父进程和子进程之间共享一个匿名映射–anon_mmap.c


#ifdef USE_MAP_ANON
#define _BSD_SOURCE             /* Get MAP_ANONYMOUS definition */
#endif
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
    int *addr;                  /* Pointer to shared memory region */

#ifdef USE_MAP_ANON             /* Use MAP_ANONYMOUS */
    addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
                MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED)
    {
	perror("mmap:");
	return -1;
    }
 

#else                           /* Map /dev/zero */
    int fd;

    fd = open("/dev/zero", O_RDWR);
    if (fd == -1)
    {
	perror("open:");
	return -1;
    }   

    addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED)
    {
	perror("mmap2:");
	return -1;
    } 


    if (close(fd) == -1)        /* No longer needed */
    {
	perror("close:");
	return -1;
    } 
#endif

    *addr = 1;                  /* Initialize integer in mapped region */

    switch (fork()) {           /* Parent and child share mapping */
    case -1:
    {
	perror("fork:");
	return -1;
    } 

    case 0:                     /* Child: increment shared integer and exit */
        printf("Child started, value = %d\n", *addr);
        (*addr)++;
        if (munmap(addr, sizeof(int)) == -1)
        {
	    perror("munmap:");
	    return -1;
        } 
        exit(EXIT_SUCCESS);

    default:                    /* Parent: wait for child to terminate */
        if (wait(NULL) == -1)
        {
	    perror("wait:");
	    return -1;
        } 
        printf("In parent, value = %d\n", *addr);
        if (munmap(addr, sizeof(int)) == -1)
        {
	    perror("munmap:");
	    return -1;
        } 
        exit(EXIT_SUCCESS);
    }
}

49.8 重新映射一个区域:mremap()

在大多数UNIX 实现上一旦映射被创建,其位置和大小就无法改变了。但Linux 提供了(不可移植的)mremap()系统调用来执行此类变更。

#define _GNU_SOURCE
#include <sys/mman.h>
void *mremap(void_address,size_t old_size,size_t new_size,int flags,...);
    Returns string address of remmaped region on success or
        MAP_FAILED on error

old_address 和old_size 参数指定了需扩展或收缩的既有映射的位置和大小。在old_address中指定的地址必须是分页对齐的,并且通常是一个由之前的mmap()调用返回的值。映射预期的新大小会通过new_size 参数指定。在old_size 和new_size 中指定的值都会被向上舍入到系统分页大小的下一个整数倍。
在执行重映射的过程中内核可能会为映射在进程的虚拟地址空间中重新指定一个位置,而是否允许这种行为则是由flags 参数来控制的。它是一个位掩码,其值要么是0,要么包含下列几个值。
MREMAP_MAYMOVE
如果指定了这个标记,那么根据空间要求的指令,内核可能会为映射在进程的虚拟地址空间中重新指定一个位置。如果没有指定这个标记,并且在当前位置处没有足够的空间来扩展这个映射,那么就返回ENOMEM 错误。
MREMAP_FIXED(自Linux 2.4 起)
这个标记只能与MREMAP_MAYMOVE 一起使用。它在mremap()中所起的作用与
MAP_FIXED 在mmap()(49.10 节)中所起的作用类似。如果指定了这个标记,那么mremap()会接收一个额外的参数void *new_address,该参数指定了一个分页对齐的地址,并且映射将会被迁移至该地址处。所有之前在由new_address 和new_size 确定的地址范围之内的映射将会被解除映射。
mremap()在成功时会返回映射的起始地址。由于(如果指定了MREMAP_MAYMOVE 标记)这个地址可能与之前的起始地址不同,从而导致指向这个区域中的指针可能会变得无效,因此使用mremap()的应用程序在引用映射区域中的地址时应该只使用偏移量(不是绝对指针)(参见48.6 节)。

在 Linux 上,realloc()函数使用mremap()来高效地为malloc()之前使用mmap()
MAP_ANONYMOUS 分配的大内存块重新指定位置。(在49.7 节中介绍glibc malloc()实现的时候曾提及过这个特性。)使用mremap()来完成这种任务使得在重新分配空间的过程中避免复制字节成为可能。

49.9 MAP_NORESERVE 和过度利用交换空间

一些应用程序会创建大(通常是私有匿名的)映射,但只使用映射区域中的一小部分。如特定的科学应用程序会分配非常大的数组,但只使用其中一些散落在数组各处的元素(所谓的稀疏数组)。
如果内核总是为此类映射分配(或预留)足够的交换空间,那么很多交换空间可能会被浪费。相反,内核可以只在需要用到映射分页的时候(即当应用程序访问分页时)为它们预留交换空间。这种方法被称为懒交换预留(lazy swap reservation),它的一个优点是应用程序总共使用的虚拟内存量能够超过RAM 加上交换空间的总量。
换个角度来看,懒交换预留允许交换空间被过度利用。这种方式能够很好地工作,只要所有进程都不试图访问整个映射。但如果所有应用程序都试图访问整个映射,那么RAM 和交换空间就被耗尽。在这种情况下,内核会通过杀死系统中的一个或多个进程来降低内存压力。在理想情况下,内核会尝试选择引起内存问题的进程(参见下面对OOM 杀手的讨论),但这是无法保证的。正因为这个原因,有时候可能会选择防止懒交换预留,转而强制系统在映射被创建时分配所有所需的交换空间。
内核如何处理交换空间的预留是由调用 mmap()时是否使用了MAP_NORESERVE 标记以及影响系统层面的交换空间过度利用操作的/proc 接口来控制的。表49-4 对这些因素进行了总结。
在这里插入图片描述 Linux 特有的/proc/sys/vm/overcommit_memory 文件包含了一个整数值,它控制着内核对交换空间过度利用的处理。在2.6 之前的Linux 上这个文件中的整数只能取两个值:0 表示拒绝明显的过度利用(遵从MAP_NORESERVE 标记的使用),大于0 表示在所有情况下都允许过度利用。
拒绝过度利用意味着大小不超过当前可用空闲内存的映射是被允许的。既有的分配可能会被过度利用(因为它们可能不会使用映射的所有分页)。
从 Linux 2.6 起,1 的含义与之前的内核中正数的含义一样,但2(或更大)则会导致使用采用严格的过度利用。在这种情况下,内核会在所有mmap()分配上执行严格的记账并将系统中此类分配的总量控制在小于或等于:

[swap size] +[ARM size] *overcommit_ratio /100

overcommit_ratio 的值是一个整数——用百分比表示——它位于Linux 特有的/proc/sys/vm/overcommit_ratio 文件中。这个文件中包含的默认值是50,表示内核最多可分配的空间为系统RAM 总量的50%,只要所有进程不同时试图全部用完给它们分配的内存,那么这种空间的分配就不会有问题。
注意过度利用监控只适用于下面这些映射。

  1. 私有可写映射(包括文件和匿名映射),这种映射的交换“开销”等于所有使用该映射的进程为该映射所分配的空间总和。

  2. 共享匿名映射,这种映射的交换“开销”等于映射的大小(因为所有进程共享该映射)。
    为只读私有映射预留交换空间是没有必要的,因为映射中的内容是不可变更的,从而无需使用交换空间。共享文件映射也不需要使用交换空间,因为映射文件本身担当了映射的交换空间。
    当一个子进程在fork()调用中继承了一个映射时,它将会继承该映射的MAP_NORESERVE设置。MAP_NORESERVE 标记并没有在SUSv3 中予以规定,它只在其他一些UNIX 实现上得到了支持。
    OOM杀手
    上面提及过当使用懒交换预留时,如果应用程序试图使用整个映射的话就会导致内存被耗尽。在这种情况下,内核会通过杀死进程来缓解内存消耗情况。
    内核中用来在内存被耗尽时选择杀死哪个进程的代码通常被称为out-of-memory(OOM)杀手。OOM 杀手会尝试选择杀死能够缓解内存消耗情况的最佳进程,这里的“最佳”是由一组因素来确定的。如一个进程消耗的内存越多,它就越可能成为OOM 杀手的候选目标。其他能提高一个进程被选中的可能性的因素包括进程是否创建了很多子进程以及进程是否拥有一个较低的nice 值(即大于0 的值)。内核一般不会杀死下列进程。

  3. 特权进程,因为它们可能正在执行重要的任务。

  4. 正在访问裸设备的进程,因为杀死它们可能会导致设备处理一个不可用的状态。

  5. 已经运行了很长时间或已经消耗了大量CPU 的进程,因为杀死它们可能会导致丢失很多“工作”。
    为杀死被选中的进程,OOM 杀手会向其发送一个SIGKILL 信号。
    从 2.6.11 内核开始,Linux 特有的/proc/PID/oom_score 文件给出了在需要调用OOM 杀手时内核赋给每个进程的权重。在这个文件中,进程的权重越大,那么在必要的时候被OOM 杀手选中的可能性就越大。同样也是从2.6.11 内核开始,Linux 特有的/proc/PID/oom_adj 文件能够用来影响一个进程的oom_score 值。这个文件可以被设置成范围在−16 到+15 之间的任意一个值,其中负数会减小oom_score 值,而正数则会增大oom_score 值。特殊值−17 会完全将进程从OOM 杀手的候选目标中删除。有关这一方面的更多细节请参考proc(5)手册。

49.10 MAP_FIXED 标记

在mmap() flags 参数中指定MAP_FIXED 标记会强制内核原样地解释addr 中的地址,而不是只将其作为一种提示信息。如果指定了MAP_FIXED,那么addr 就必须是分页对齐的。
一般来讲,一个可移植的应用程序不应该使用MAP_FIXED,并且需要将addr 指定为NULL,这样就允许系统选择将映射放置在哪个地址处了。之所以需要这样做的原因与48.3节中解释在使用shmat()附加一个System V 共享内存段时通常倾向于将shmaddr 指定为NULL的原因是一样的。
然而,还是存在一种可移植应用程序需要使用MAP_FIXED 的情况。如果在调用mmap()时指定了MAP_FIXED,并且内存区域的起始位置为addr,覆盖的length 字节与之前的映射的分页重叠了,那么重叠的分页会被新映射替代。使用这个特性可以可移植地将一个文件(或多个文件)的多个部分映射进一块连续的内存区域,如下所述。
6. 使用 mmap()创建一个匿名映射(参见49.7 节)。在mmap()调用中将addr 指定为NULL 并且不指定MAP_FIXED 标记。这样就允许内核为映射选择一个地址了。
7. 使用一系列指定了MAP_FIXED 标记的mmap()调用来将文件区域映射(即重叠)进在上一步中创建的映射的不同部分中。
尽管可以忽略第一个步骤而直接使用一系列mmap() MAP_FIXED 操作来在应用程序选中的地址范围内创建一组连续的映射,但这种做法的可移植性与上面这种两步式做法相比就要差一些了。上面提及过,一个可移植的应用程序应该避免在固定的地址处创建新映射。上面的第一步避免了移植性问题的出现,因为这一步让内核选择了一个连续的地址范围,然后在该地址范围中创建新映射。
从 Linux 2.6 开始,使用remap_file_pages()系统调用(下一节介绍)也能够取得同样的效果,但使用MAP_FIXED 的可移植性更强,因为remap_file_pages()是Linux 特有的。

49.11 非线性映射:remap_file_pages()

使用mmap()创建的文件映射是连续的:映射文件的分页与内存区域的分页存在一个顺序的、一对一的对应关系。对于大多数应用程序来讲,线性映射已经够用了。然而一些应用程序需要创建大量的非线性映射——文件分页的顺序与它们在连续内存中出现的顺序不同的映射。图49-5 给出了一种非线性映射。
在这里插入图片描述 在上一节中介绍了一种创建非线性映射的方法:使用多个带MAP_FIXED 标记的mmap()调用。然而这种方法的伸缩性不够好,其问题在于其中每个mmap()调用都会创建一个独立的内核虚拟内存区域(VMA)数据结构。每个VMA 的配置需要花费时间并且会消耗一些不可交换的内核内存。此外,大量的VMA 会降低虚拟内存管理器的性能。特别地,当存在数以万计的VMA 时处理每个分页故障所花费的时间会大幅度提高。(这对于一些在一个数据库文件中维护多个不同视图的大型数据库管理系统来讲是一个问题。)
从内核 2.6 开始,Linux 提供了remap_file_pages()系统调用来在无需创建多个VMA 的情况下创建非线性映射,具体如下。

  1. 使用 mmap()创建一个映射。
  2. 使用一个或多个remap_file_pages()调用来调整内存分页和文件分页之间的对应关系。(remap_file_pages()所做的工作是操作进程的页表。)
#define _GNU_SOURCE
#include <sys/mman.h>
int remap_file_pages(void *addr,size_t size,int prot,size_t pgoff,int flags);
    Return 0 on success,or -1 on error

pgoff 和size 参数标识了一个在内存中的位置待改变的文件区域,pgoff 参数指定了文件区域的起始位置,其单位是系统分页代销(sysconf(_SC_PAGESIZE)的返回值)。size 参数指定了文件区域的长度,其单位为字节。addr 参数起两个作用。

  • 它标识了分页需调整的既有映射。换句话说,addr 必须是一个位于之前通过mmap()映射的区域中的地址。
  • 它指定了通过 pgoff 和size 标识出的文件分页所处的内存地址。
    addr 和size 都应该是系统分页大小的整数倍。如果不是,那么它们会被向下舍入到最近的分页大小的整数倍。
    假设使用了下面的mmap()调用来映射通过描述符fd 引用的打开着的文件的三个分页,并且该调用将返回地址0x4001a000 赋给了addr。
ps = sysconf(_SC_PAGESIZE);  /*Obtain system page size*/
addr = mmap(0,3*ps,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);

下面的调用将会创建一个非线性映射,如图49-5所示。

 remap_file_pages(addr,ps,0,2,0);
        /*Maps page 0 of file into page 2 of region*/
 remap_file_pages(addr+2,ps,0,0,0);
        /*Maps page 2 of file into page 0 of region*/

到现在为止还没有对 remap_file_pages()中的其他两个参数进行介绍。

  • prot 参数会被忽略,其值必须是0。在将来可能能够使用这个参数来修改受
    remap_file_pages()影响的内存区域的保护信息。在当前实现中,保护信息保持与整个VMA 上的保护信息一致。

虚拟机和垃圾收集器是其他一些使用多个VMA 的应用程序,其中一些应用程序需要能够写保护单个分页。因此人们预期remap_file_pages()将会还允许修改一个VMA 中单个分页上的权限,但到目前为止这种特性还没有被实现。

  • flags 参数当前未被使用
    在当前的实现上,remap_file_pages()仅适用于共享(MAP_SHARED)映射。
    remap_file_pages()系统调用是Linux 特有的,SUSv3 并没有对这个函数进行规定,并且其他UNIX 实现也没有提供这个函数。

49.12 总结

mmap()系统调用在调用进程的虚拟地址空间中创建一个新内存映射。munmap()系统调用执行逆操作,即从进程的地址空间中删除一个映射。
映射可以分为两种:基于文件的映射和匿名映射。文件映射将一个文件区域中的内容映射到进程的虚拟地址空间中。匿名映射(通过使用MAP_ANONYMOUS 标记或映射/dev/zero来创建)并没有对应的文件区域,该映射中的字节会被初始化为0。
映射既可以是私有的(MAP_PRIVATE),也可以是共享的(MAP_SHARED)。这种差别确定了在共享内存上发生的变更的可见性,对于文件映射来讲,这种差别还确定了内核是否会将映射内容上发生的变更传递到底层文件上。当一个进程使用MAP_PRIVATE 映射了一个文件之后,在映射内容上发生的变更对其他进程是不可见的,并且也不会反应到映射文件上。MAP_SHARED 文件映射的做法则相反——在映射上发生的变更对其他进程可见并且会反应到映射文件上。
尽管内核会自动将发生在一个MAP_SHARED 映射内容上的变更反应到底层文件上,但它不保证何时会完成这个操作。应用程序可以使用msync()系统调用来显式地控制一个映射的内容何时与映射文件进行同步。
内存映射有很多用途,包括:

  • 分配进程私有的内存(私有匿名映射);
  • 对一个进程的文本段和初始化数据段中的内容进行初始化(私有文件映射);
  • 在通过 fork()关联起来的进程之间共享内存(共享匿名映射);
  • 执行内存映射I/O,还可以将其与无关进程之间的内存共享结合起来(共享文件
    映射)

在访问一个映射的内容时可能会遇到两个信号。如果在访问映射时违反了映射之上的保护规则(或访问一个当前未被映射的地址),那么就会产生一个SIGSEGV 信号。对于基于文件的映射来讲,如果访问的映射部分在文件中没有相关区域与之对应(即映射大于底层文件),那么就会产生一个SIGBUS 信号。
交换空间过度利用允许系统给进程分配比实际可用的RAM 与交换空间之和更多的内存。过度利用之所以可能是因为所有进程都不会全部用完为其分配的内存。使用
MAP_NORESERVE 标记可以控制每个mmap()调用的过度利用情况,而使用/proc 文件则可以控制整个系统的过度利用情况。
mremap()系统调用允许调整一个既有映射的大小。remap_file_pages()系统调用允许创建非线性文件映射。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值