页面错误机制(Page Fault)是操作系统中经常出现的一类问题,其含义为由于用户访问了未在物理内存中映射的虚拟内存地址引起的,而操作系统应用页面错误处理机制实现了多种功能,例如懒加载(Lazy Loading)、写时复制(Copy-On-Write,COW)、内存映射文件(Memory-Mapped Files)等,这些功能优化了操作系统的执行效率,本文旨在详解应用页面错误来实现的实际功能:
虚拟内存我有在文章中介绍,不懂的小伙伴可以去看看:https://cloud.tencent.com/developer/article/2455414
虚拟内存简而言之就是操作系统假装分配的内存,它实际上对应的地址是不存在的,在程序执行过程中会根据页表找到虚拟地址对应的实际物理地址,到实际的物理地址中执行程序,它主要由两大好处:
1)隔离性(Isolation):操作系统通过给一个应用进程分配连续的一块虚拟内存,使得每个应用进程只在自己空间中运行,保证了不同进程之间的数据安全性与隔离性,但是这里注意,分配的虚拟内存连续并不代表对应的实际物理内存是连续的,实际物理内存是与虚拟内存有映射关系的
2)间接性(level of indirection):正是因为这种虚拟地址->物理地址的映射,使得操作系统可以实现多种不同的功能来优化性能,比如让计算机运行比实际上大好几倍空间的应用程序,它们使用的其实是计算机的物理地址,但是系统可以通过合理的内存分配策略使得它们能够同时运行
如果想要应用页面错误机制实现功能,我们必须要知道以下具体信息:
页面错误地址:知道了页面错误的虚拟地址才能对地址的映射进行更好的操作,在xv6系统中保存在stval寄存器中
页面错误的种类:页面错误可能由于多种原因,比如写时出错,读时出错等,针对不同的错误有不同的操作,xv6系统中可以通过用户空间的scauce查看
导致页面错误的指令地址:知道这个方便我们定位到指定指令进行具体处理,xv6系统中保存在sepc中
具体空间分配如下图:
从下到上依次为:
text:进程文本区,保存进程的文本信息
data:进程数据区,保存数据模块
guard page:守护进程页,一般它为用户进程分配内存的临界页面,分配的内存地址不能小于它
stack:这里保存了应用进程已经分配的地址空间
heap:堆,表示剩余的地址空间
trapframe:数据栈,保存了当前进程的一些必要信息,如页表地址、进程pid、栈针地址等
trampoline:蹦床页面,保存了用户态与内核态切换的进出代码,对这部分感兴趣的小伙伴可以看我的另一篇文章:https://cloud.tencent.com/developer/article/2457403
当进程分配新的内存空间时,会将stack对应的栈针上移,如果栈针上移后溢出,说明物理地址不足以分配当前内存空间,导致错误
介绍完上述基础概念,下面正式介绍第一种页面错误应用:懒加载,与我们开发过程中接触的懒汉式的单例模式类似,它在执行过程中,如果发现应用程序需要内存,并不立即分配物理内存,而是先分配虚拟地址空间,之后直到使用的时候再将物理地址空间加载出来
操作系统在分配内存时,会按照用户需求分配固定大小内存的页面,但是如果这些页面并没有实际的内容,那么操作系统会将他们映射到0号物理地址:
在这种方法下,操作系统无须对每个虚拟页面映射一个物理地址页面,大大减少了所需的实际物理内存,而当某个虚拟地址页面需要被实际调用时,操作系统会为它分配一个物理地址内存,更改它所对应的地址映射,从而提高内存分配的效率,示例如下图:
关于这个的典型例子是fork()函数,fork函数在执行时会创建一个子进程,而子进程与父进程会共享内存空间,如果操作系统为每个fork进程都复制一份相同大小的内存空间,会很耗费系统空间,对此操作系统让父进程与子进程指向同一片地址空间,如果子进程执行的是读操作,只需要读取数据即可,但是若执行的是写操作,这时操作系统才会为子进程分配一片单独的地址空间
这里需要注意的一个问题是,如果这个父进程对应的其中一个子进程退出,操作系统底层会在进程退出时,释放该子进程映射的物理地址空间,如果此时子进程并没有应用COW,它自己的页表指针指向的还是父进程对应的物理地址空间,那么如果此时释放了这篇物理地址空间可能会导致父进程在运行程序时出现错误,因此在操作系统底层,会为每个物理地址空间建立一个引用计数,表示有多少个应用进程正在使用这片地址区域,如果子进程要退出时,那么就对这个引用计数减一,只有当引用计数减为0的时候才会释放这篇物理地址空间
操作系统在分配页表时并不会随意分配,而是只有在需要的时候才会为应用程序分配页表映射,而这样做带来的后果时操作系统有时甚至会使用一些页面逐出策略来分配页面,最常用的便是LRU(Least Recently Used)算法,该算法检测最长时间没有使用的页面进行驱逐,同时如果有多个页面,操作系统一般会选择没有写过的页面来减少内存复制的开销
操作系统有时会将进程地址空间与内存地址空间直接进行映射,这样用户读或写便可以直接对进程地址空间进行操作,而不是通过从用户态转到内核态来计算地址空间操作,这样做减少了一层中间件的转换,它提高了操作系统处理文件的效率,同时也使得多个进程可以映射同一片地址空间,让进程之间通信更加容易,操作系统中的mmap函数如下:
mmap(va, len, prot, flags, fd, offset)
每个参数介绍:
va:virtual address,虚拟地址
len:要映射的内容长度
prot:对文件的访问权限等,可读(read),可写(write)等
flags:一些内存区域的其他信息,是否共享(shared)等
fd:文件描述符,对应于打开的文件流
offset:相对于虚拟地址的偏移量,复制到哪片地址中
以上就是关于操作系统页面错误机制的全部讲解了,正是因为这些页面分配的实现策略,才使得如今的操作系统有如此迅速的效率,希望对你有所帮助,祝好!!!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。