转:https://blog.csdn.net/shijunwang/article/details/80217266
欢迎查看《深入理解计算机系统》系列博客
《深入理解计算机系统》笔记(四)虚拟存储器,malloc,垃圾回收(本篇)
《深入理解计算机系统》笔记(五)并发、多进程和多线程【Final】
——————————————————————————————————————–概述
●我们电脑上运行的程序都是使用虚拟存储,跟物理内存根本不搭边。
●既然虚拟内存是在磁盘上的,为什么它又运行这么好,并没有感觉卡顿?这要感谢程序的局部性!
●虚拟存储器的调度是一个操作系统必须做好的事情!不然程序就会慢的跟蜗牛一样。
●垃圾收集器就是一个动态分配器,它自动释放这些程序不再需要的已分配的块。
●在64处理器的PC上获取一个变量的地址发现是:6个字节?比如i7的核64位,但是只能寻址48位,所以指针是6个字节。指针字节跟操作系统的位没有必然关系。
●由于虚拟存储器的存在使得程序链接和加载都很容易。比如程序的“代码段”,总是从虚拟地址0x08048000开始的。
●“写时拷贝”是一个非常重要的概念。
一个系统中的进程是与其他进程共享CPU和内存资源的,然而,如果太多的进程需要太多的存储器,那么他们中的一些就根本无法运行。存储器还很容易破坏。如果某个进程不小心写了另外一个进程的存储器,它就可能易某种完全跟程序逻辑无关的方式失败。为了更加有效地管理存储器并且少出错,现代系统提供了一种对内存的抽象概念,叫做虚拟存储器(VM).
作为程序员我们为什么要了解虚拟存储器呢。1)虚拟存储器是中心。虚拟存储编辑系统的所有层面。2),虚拟存储器是强大的。虚拟存储器给予应用程序强大的能力,可以创建和销毁存储器片(chunk)、将存储器片映射到磁盘文件的某个部分,以及其他进程共享存储器。3),虚拟存储器是危险的。每次应用程序引用一个变量、间接引用一个指针,或者调用一个类似与malloc的动态分配的函数,就会跟虚拟存储器打交道。
9.1物理和虚拟寻址
早期的计算机采用物理寻址(Physical Addressing),现代的计算机采用虚拟寻址(virtual addressing),将一个虚拟地址转换为物理地址的任务叫做地址翻译。地址翻译需要CPU硬件和操作系统之间的紧密合作。CPU芯片中有一个MMU的存储器管理单元(Memory Management Unit)。如下图所示:
从上面两张图上发现,虚拟寻址只是CPU的一种技术。
9.2地址空间
地址空间是一个非负数地址的有序集合:如果地址空间的整数是连续的,那么我们说它是一个线性地址空间。那么一个拥有虚拟寻址的CPU,也就会有一个虚拟地址空间。内存中的一个字节,就会有一个物理地址和一个虚拟地址。如果CPU有N个虚拟地址空间,那么该字节就有N个虚拟地址。
9.3虚拟存储器作为缓存的工具
概念上而言,虚拟存储器被组织为一个由“存放在磁盘上的N个连续字节大小的单元”组成的数组。磁盘上的内容被缓存到内存中,内存和磁盘之间采用块block格式传输。由于虚拟存储的技术早于高速缓存的技术,所以当时这种块被称之为页(page),也就是现在的说法。如下图:
上图展示了一个8个虚拟页的小型虚拟存储器。绿色的3个页表示已经缓存到内存了(参考内存中绿色的三个页)连个未分配和三个未缓存的。
由于内存的访问速度比磁盘快100000倍,所以如果内存不命中,那么代价将是非常高的(程序运行将非常慢),另外,读取磁盘某一个扇区第一个字节的开销比起读取整个扇区的字节慢100000倍。这两个条件决定了虚拟存储的页的大小最好是一个扇区或几个扇区。典型地是4KB-2MB。
还有,即便是有个很大的页,如果操作系统在替换页的操作处理不当,速度将会很慢。缺页是一种异常,名字就叫“缺页”,发生异常后,虚拟存储器加载磁盘到内存并替换牺牲页。操作系统再次运行该指令,就不再缺页了。
当我们与多人了解了虚拟存储器的概念之后,我们的第一印象是:“它的效率应该很低下吧,因为如果不命中,那么代价将非常高。”我们也担心页面的调度破坏程序的性能。实际上,虚拟存储器运行的相当好,主要归功于程序局部性(locality).
尽管整个运行过程中程序引用的不同页面的总数可能超出物理存储器的总的大小,但是局部性原则保证了在任意时刻,程序将往往在一个较小的活动页面(active page)集合工作,这个集合叫做工作集。
tips 在Unix系统中使用getrusage函数来检测缺页的数量。
9.4虚拟存储器作为存储器管理工具
实际上,操作系统为“每一个进程”提供了一个独立的页表,因而也就是一个独立的虚拟地址空间。展现形式就是:让人感觉这个程序正在完全是使用CPU和内存资源。如下图,多个虚拟叶面同事映射到同一个共享物理页面上(动态链接库或者叫共享库就是这个原理)。
进程独立的地址空间允许每个进程的存储器映像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的地方。比如Linux系统上每个进程文本节总是从虚拟地址0x08048000初开始。总结起来说:虚拟存储器简化了链接、简化了加载、简化共享和简化存储器分配。
9.5虚拟存储器作为存储器保护的工具
现代操作系统都严格限制普通应用程序访问自己的只读数据和别进程的数据,还有操作系统的内核部分。所以虚拟存储器可以在页的开始部分设置几个标志位,用于标明这个进程是系统进程还是用户进程。如果某一天指令违反了这个条件,那么CPU就触发一个异常。Unix外壳一般将这种异常报告为“段错误segmentation fault”
9.6地址翻译,有点儿专业。跟应用程序层面关系不大。
9.7案例研究:Intel Core i7/Linux存储器系统
在64位的mac电脑上打印一个变量的指针,结果指针是6个字节。这是因为i7的CPU只支持48位的虚拟地址,和52位的物理地址空间。虽然我的mac是i5的但是估计也是48位虚拟地址。其他内容是地址翻译,缺页异常。
Linux虚拟存储器区域:Linux将虚拟存储器组织成一些区域的集合。一个区域(area)就是已经存在着(已分配的)的虚拟存储器的连续片(chunk)
9.8存储器映射
有一个疑问:“电脑4G的内存,加载一个程序很慢,但是看看内存剩余量,还有剩余内存,不应该慢啊。如果升级到8G的内存,程序加载的快了。这是因为操作系统有一个叫做交换空间的东西,交换空间可能显示虚拟存储器的页数“。明白了,如果内存大,交换空间就大,程序加载就快。
概念:私有有的写时拷贝,理解这个概念很重要。用图来说明:
上图右部分说明了写时拷贝:进程1和2都共享内存中的一组页,但是进程2需要写最后一个页。那么启动写时拷贝,拷贝最后一页到内存其他地方。(内存中的页,可能不是连续的)。
fork函数直接在虚拟存储器上开辟一段,跟主进程一不一样的拷贝,那么,也就跟主进程指向同一个物理存储器。从上图左边部分中我们可以印证一下进程2可以看做是从进程1调用fork函数创建的。
execve是如何加载应用程序的。execve函数在当前进程中加载并运行a.out,用a.out程序有效的替换了当前程序。加载a.out时候需要以下几个步骤:
1)删除已存在的用户区域。
2)映射私有区域。为新的程序的文本、数据、bss、和栈区域创建新的区域结构。
3)映射共享区域。
4)设置程序技术器PC。使之指向文本区域的入口点。
9.9动态存储器分配malloc
动态分配,我自己的理解就是分配在虚拟存储器中的区域,这个区域可能已经在内存中了,也可能在磁盘上。
下面是真正的定义:动态存储器分配器,维护着一个进程的虚拟存储器区域,成为”堆“,对于每一个进程,操作系统内核维护着一个变量brk(break)指向堆的顶部。分配器有两种基本风格。
1)显式分配器:例如C和C++的malloc 和new 运算法。但是需要程序员收到free和delete处理。
2)隐式分配器:也叫做垃圾收集器,其自动释放未使用的已分配的块的过程叫做垃圾收集。例如:Java的垃圾回收机制。
动态存储分配器的要求和目标
●处理任意请求序列。
●立即响应请求。分配器必须立即响应分配请求。因此不允许分配器提高性能,从新排列或者缓冲请求。
●只是用堆。
●对齐块,比如8个字节的对齐。
●不修改已分配的块。不能压缩已分配的块。
目标
●1最大化吞吐率
●2最大化存储器的利用率。天真的程序员经常不正确的假设虚拟存储器是一个无限的资源,事实上,一个系统中被所有进程分配的虚拟存储器的全部数量是受磁盘上交换空间的数量限制的。
存储器碎片(这是一个很有意思的话题)。首先介绍内部碎片和外部碎片。如图:
外部碎片要比内部碎片处理复杂得多!分配器还要有其他功能:合并空闲的块,也是非常有意思的(原书使用19页的量,来描述分配器的原理和动作),聪明的程序员采用了很多种技巧来实现,参考p568页。
9.10 垃圾收集(回收)
![](https://i-blog.csdnimg.cn/blog_migrate/73d3043fe233834eb650cb8245b9fd85.jpeg)
9.11程序中常见的与存储器有关的错误。
●间接引用坏指针
●读未初始化的存储器
●允许栈缓存去溢出
●假设指针和它们指向的对象是相同大小的
●造成错位
●引用指针,而不是它指向的对象
●误解指针运算
●应用不存在的变量
●应用空闲块中的数据
●引起存储器泄露
总结
</div>
</div>