深入理解操作系统(27)第十章:虚拟存储器(4)存储器映射(两种映射对象/交换空间/共享和私有对象/写时拷贝COW/fork过程/execve过程/mmap)

1. 存储器映射

1.1 存储器映射

1.1.1 存储器映射-定义

Linux通过将一个虚拟存储器区域与一个磁盘上的对象关联起来,以初始化这个虚拟存储器区域的内容,
这个过程称为存储器映射(memory mapping)
	(磁盘上的对象有2种:普通文件和匿名文件)

1.1.2 映射的两种对象类型

虚拟存储器区域可以映射到两种类型的对象:

1. Unix文件系统中的普通文件:
   一个区域可以映射到一个普通磁盘文件(如一个可执行目标文件)的连续部分。

文件被分成页面大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理存储器,直到CPU第一次引用到页面(也就是,发射一个虚拟地址,落在地址空间这个页面的范围之内)。如果区域比文件的这部分要大一些,那么就用零来填充这个区域的余下部分。

2. 匿名文件:
   一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。

CPU第一次引用这样一个区域内的虚拟页面时,内核就在物理存储器中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面,并更新页表,将这个页面标记为是驻留在存储器中的。注意在磁盘和存储器之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面,有时也叫做请求二进制零的页(demand-zero page)。

1.1.3 交换空间

# 1. 定义:什么是交换空间?
swap space是磁盘上的一块区域,可以是一个分区,也可以是一个文件,或者是他们的组合。
简单点说,当系统物理内存吃紧时,Linux会将内存中不常访问的数据保存到swap上,
这样系统就有更多的物理内存为各个进程服务,
而当系统需要访问swap上存储的内容时,再将swap上的数据加载到内存中,
这就是我们常说的swap out和swap in换入换出。

# 2. 为什么需要交换空间?
原因就是:让系统可以运行大于实际物理内存的程序,或者运行更多更多的程序。

无论在哪种情况中,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。

交换文件也叫做交换空间(swap space)或者交换区域(swap area),

需要意识到的很重要的一点是:

在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。

1.2 再看共享对象

1.2.1 存储器映射的历史

存储器映射的概念来源于一个聪明的发现:

如果虚拟存储器系统可以集成到传统的文件系统中,
那么就能提供一种简单而高效的把程序和数据加载到有储器中的方法。

1.2.2 进程共享对象和私有对象

正如我们已经看到的,进程这一抽象能够为每个进程提供自己私有的虚拟地址空间,可以免受其他进程的错误读写。不过,许多进程有同样的只读文本区域。例如,每个运行Unix shell程序tcsh的进程都有相同的文本区域。而且,许多程序需要访问只读运行时库代码的相同拷贝。例如,每个c程序都要求来自标准C库的诸如printf这样的函数。那么,如果每个进程都在物理存储器中保持这些常用代码的复制拷贝,那就是极端的浪费了。

幸运的是,存储器映射给我们提供了一种清晰的机制,用来控制多个进程如何共享对象
一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象

如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟存储器的其他进程而言,也是可见的。而且,这些变化也会反映在磁盘上的原始对象中。

另一方面,对于一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中。

一个共享对象映射到的虚拟存储器区域叫做共享区域。类似地,也有私有区域。
假设进程1将一个共享对象映射到它的虚存储器的一个区域中,如图10.31(a)所示。
假设进程2将同一个共享对象映射到它的地空间(并不一定要和进程1在相同的虚拟地址处),如图10.31(b)所示。

图10.31
在这里插入图片描述

因为每个对象都有一个唯一的文件名,内核可以迅速地判定进程1已经映射了这个对象,而且可以使进程2中的页表条目指向相应的物理页面。
关键点在于即使对象被映射到了多个共享区域,物理存储器中也只需要存放共享对象的一个拷贝。为了方便,我们将物理页面显示为连续的,但是在一般情况下当然不是这样的。

1.2.3 写时拷贝(copy-on-write)

私有对象是使用一种叫做写时拷贝(copy-on-wnte)的巧妙技术被映射到虚拟有储器中的。

一个私有对象开始生命周期的方式基本上与共享对象的一样,在物理存储器中只保有有私有对象的一份拷贝。
比如,图10.32(a)展示了一种情况。其中两个进程将一个私有对象映射到它们虚拟存储器的不同区域。但是共享这个对象同一个物理拷贝。对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时拷贝。只要没有进程试图写它自己的私有区域,它们就可以继续共享物理存储器中对象的一个单独拷贝。然而,只要有一个进程试图写私有区域内的某个页面,那么这个写繰作就会触发一个保护故障。

图10.32
在这里插入图片描述

当故障处理程序注意到保护异常是由于进程试图写私有的写时拷贝区域中的一个页面而引起的,它就会在物理存储器中创建这个页面的一个新拷贝,更新页表条目指向这个新的拷贝,然后恢复这个页面的可写权限,如图10.32(b)所示。当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写作就可以正常执行了。

通过延迟私有对象中的拷贝直到最后可能的时刻,写时拷贝最充分地使用了稀有的物理存储器。

参考:
Linux:COW 写时拷贝技术
https://blog.csdn.net/lqy971966/article/details/118784913

1.3 再看fork函数

既然我们理解了虚拟存储器和存储器映射,那么我们可以清晰地知道fork函数是如何创建一个带有自己独立虚拟地址空间的新进程的。

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构(vm_area_struct)和页表的原样拷贝。它标记两个进程中的每个页面为只读的,并标记两个进程中的每个区域结构为私有的写时拷贝的。

当fork在新进程中返回时,新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。
当这两个进程中的任一个后来进行写操作时,写时拷贝机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

Linux:COW 写时拷贝技术 背景及详细参考:
https://blog.csdn.net/lqy971966/article/details/118784913

1.4 再看execve函数

虚拟存储器和存储器映射在将程序加载到存储器的过程中也扮演着关键的角色。既然我们己经理解了这些概念,我们就能够理解execve函数实际中是如何加载和执行程序的。假设运行在当前进程中的程序执行了如下的execve调用:

execve("a.out“,argv,environ)

execve函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。
加载并运行a.out需要以下几个步骤:

1. 删除已存在的用户区域
   删除当前进程虚拟地址的用户部分中的己行在的区域结构
   
2. 映射私有区域
   为新程序的文本、数据、bss和栈区域创建新的区域结构。
   所有这些新的区域都是私有的写时拷贝的。
		文本和数据区域被映射为a.out文件中的文本和数据区。
		bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。
		栈和堆区域也是请求二进制零的,初始长度为零。
   图10.33概括了私有区域的不同映射
   
3. 映射共享区域
   如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,
   那么这些对像都是动态链接到这个程序的,并且映射到用户虚拟地址空间中的共享区域内。
   
4. 设置程序计数器(pc)
   execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口点。

下一次调度这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

图10.33

在这里插入图片描述

Linux exec 系列函数:execl execv等
https://blog.csdn.net/lqy971966/article/details/110532621
linux system 和 execl 函数对比
https://blog.csdn.net/lqy971966/article/details/110532718

1.5 使用mmap函数的用户级内存映射

Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

mmap函数要求内核创建一个新的虚拟存储器区域,最好是从地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的组块(chu)射到这个新的区域。连续的对象组块(chunk)大小为length字节,从距文件开始处偏移量为offset字节的地方开始。地址仅仅是一个暗示,通常被定义为NULL。为了我们的目的,我们总是假设起始地址为NULL。图10.34描述了这些参数的意义。

图10.34
在这里插入图片描述

返回值:

成功执行时,mmap()返回被映射区的指针。
失败时,mmap()返回MAP_FAILED[其值为-1]

参数:

start:映射区的开始地址,一般为NULL
length:映射区的长度,一般是文件的长度
	mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
	PROT_EXEC:页内容可以被执行
	PROT_READ:页内容可以被读取
	PROT_WRITE:页可以被写入
	PROT_NONE:页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。
	MAP_SHARED //与其它所有映射这个对象的进程共享映射空间
	MAP_PRIVATE //建立一个写入时拷贝的私有映射。
fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
	一般为文件描述符fd
offset:被映射对象内容的起点,一般为0

如:
mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)

参考:mmap
https://blog.csdn.net/lqy971966/article/details/120705836

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值