如果将计算机的存储媒介中的处理性能与容量做一个对比.则会出现如图所示的金字塔模型.
从上图可以看出处理速度和存储容量是成反比.也就是说.性能越强的计算机.其硬件资源越是稀缺.所以合理的利用和分配就越重要.由于读写速度相差甚大.所以将大部分程序逻辑临时用的数据.全部存在内存之中.例如变量 全局变量 函数跳转地址 静态库 执行代码 临时开辟的内存结构体(对象)等.
内存为什么需要管理:
当存储的东西越来越多.也就发现物理内存的容量依然不够用.提高对内存的利用率和合理的分配内存.管理就变的非常重要了.
1).操作系统会对内存进行非常详细的管理.
2).基于操作系统的基础上.不同语言的内存管理机制也应运而生.有一些语言并没有提供自动的内存管理模式.有的语言却提供了自身程序的内存管理模式.如下图.
操作系统如何管理内存:
对计算机来讲内存真正的载体是物理内存条.这个是实打实的物理硬件容量.所以在操作系统中定义的这部分容量叫物理内存.
物理内存的布局实际上就是一个内存大数组.如图所示.
每个元素都会对应一个地址.称为物理内存地址.CPU在运算的过程中.如果需要从内存中取一个字节的数据.就需要基于这个数据的物理内存地址去运算.而且物理内存的地址是连续的.可以根据一个基准地址进行偏移来取得相应的连续内存数据.
一个操作系统是不可能运行一个程序的.这个大数组物理内存势必要被多个程序分成多份.供每个程序使用.但程序是活的.一个程序可能一会需要1MB内存.一会又需要1GB的内存.操作系统只能取这个程序允许的最大内存极限分配给这个进程.但这样会导致每个进程都会多要去一部分内存.而这些多要的内存却大概率不会被使用.如图所示.
当N个程序同时使用同一块内存时.产生读写的冲突也在所难免.这些就会导致这些昂贵的物理内存条.几乎运行不了几个程序.内存的利用率也就提高不上来.
虚拟内存:
所谓虚拟.类似假 凭空而造的意思.如图所示.
虚拟内存地址是基于物理内存地址之上凭空而造的一个新的逻辑地址.而操作系统暴露给用户进程的只是虚拟内存地址.操作系统内部会对虚拟内存地址和真实的物理内存地址建立映射关系.来管理地址的分配.从而使物理内存的利用率提高.
这样用户程序(进程)只能使用虚拟的内存地址获取数据.系统会将这个虚拟地址翻译成实际的物理地址.这里每个程序统一使用一套连续的虚拟地址.例如0x 0000 0000 ~0x ffff ffff.从程序的角度来看.它觉得自己独享了一整块内存.并且不用考虑访问冲突的问题.系统会将虚拟地址翻译成物理地址.从内存上加载数据.
虚拟内存目的是解决以下几件事.
1).物理内存无法被最大化利用.
2).程序逻辑内存空间使用独立.
3).内存不够.继续虚拟磁盘空间.
对于1和2两点.上述已经有一定的描述了.其中针对1的最大化.虚拟内存还实现了读时共享写时复制的机制.可以在物理层同一字节的内存地址被多个虚拟内存空间映射.如图所示.
如果一个进程需要进行写操作.则这个内存将会被复制一份.成为当前进程的独享内存.如果是读操作.则可能多个进程访问的物理空间是相同的空间.
如果一个内存几乎都是被读取的.则可能多个进程共享同一块物理内存.但是它们各自的虚拟内存是不同的.当然这个共享不是永久的.当其中有一个进程对这个内存发生写操作时.就会复制一份.执行写操作的进程就会将虚拟内存地址映射到新的物理内存地址上.
对于第三点.是虚拟内存为了最大化利用物理内存.如果进程使用的内存足够大.则会导致物理内存短暂的供不应求.此时虚拟内存也会"开疆拓土".从磁盘(硬盘)上虚拟出一定量的空间.挂在虚拟地址上.而且这个动作对于进程来讲是不知道的.因为进程只能够看见自己的虚拟内存空间.如图所示.
MMU内存管理单元:
假设使用固定匹配地址逻辑做映射.可能会出现很多虚拟内存映射到同一个物理内存上.如果发现被占用.则会在重新映射.这样对映射地址的寻址代价极大.所以操作系统又加了一层专门用来管理虚拟内存和物理内存映射关系的东西.即MMU(内存管理单元).如图所示.
MMU是在CPU里的.或者说是CPU具有一个MMU.
虚拟内存怎么存放:
虚拟内存本身是通过一个叫页表的东西实现的.
1).页
页是操作系统用来描述内存大小的一个单位名称.一个页的含义是大小为4KB(1024 * 4 = 4096字节)的内存空间.操作系统对虚拟内存空间是按照这个单位来管理的.
2).页表:
页表实际上就是页的集合.即基于页的一个数组.页只表示内存的大小.而页表条目(PTE)才是页表数组中的一个元素.
用一个抽象的图来表示页 页表 页表元素PTE的概念和关系.
虚拟内存的实现方式.大多数是通过页表实现的.操作系统虚拟内存空间被分成一页一页来管理.每页的大小为4KB(这是可以配置的.不同操作系统不一样).磁盘和主内存之间的置换也是以页为单位来操作的.4KB算是通过实践折中出来的通用值.太小了会出现频繁置换.太大了又会浪费内存.
虚拟内存到物理内存的映射关系的存储结构类似上图中的页表记录.实则是一个数组.这里需要注意的是.页是一次读取的内存单元.但是真正到虚拟内存寻址的是PTE.也就是页表中的一个元素.PTE的大致内部结构如图所示.
可以看出每个PTE是由一个有效位和一个物理页号或磁盘地址组成.有效位表示当前虚拟页是否已经被缓存在主内存中(或CPU的高速缓存Cache中)
虚拟页表(简称页表)虽然作为虚拟内存与物理内存的映射关系.但是本身也需要存放在某个位置上.所以自身也占用一定的内存.所以页表本身也被操作系统放在物理内存的指定位置.CPU把虚拟地址给MMU.MMU去物理内存中查询页表.得到实际的物理地址.当然MMU不会每次都去查询.它自己也有一份缓存.叫作Translation Lookaside Buffer(TLB).是为了加速地址翻译.CPU MMU TLB的相互关系如下图.
从图中看出.TLB是虚拟内存页.即虚拟地址和物理地址映射关系的缓存层.MMU当收到地址查询指令.第一时间请求TLB.如果没有机会才会进行从内存中的虚拟页进行查找.这样才能会触发多次内存读取.而读取TLB则不需要内存读取.进程读取的步骤如下.
1).CPU进行虚拟地址请求MMU.
2).MMU优先从TLB中的到虚拟页.
3).如果的到则返回上层.
4).如果没有.则从主存的虚拟页表中查询关系.
PTE内部构造有效位特征含义:
1).有效位为1.表示虚拟页已经被缓存在内存(或者CPU高速缓存TLB-Cache)中.
2).有效位位0.表示虚拟页未被创建且没有占用内存(或者CPU高速缓存了TLB-Cache).或者表示已经创建虚拟页.但是没有存到内存(或者CPU高速缓存TLB-Cache)中.
通过上述标识位.可以将虚拟页集集合分成三个子集.如图.
CPU内存访问过程:
1).进程将内存相关的寄存器指令请求运算发送给CPU.CPU得到具体的指令请求.
2).计算指令被CPU加载到寄存器中.准备执行相关指令逻辑.
3).CPU对相关的可能请求的内存生成虚拟内存地址.一个虚拟内存地址包括虚拟页号VPN和虚拟页偏移量VPO.
4).从虚拟地址中得到虚拟页号VPN.
5).通过虚拟页号VPN请求MMU内存管理单元.
6).MMU通过虚拟页号查找对应的PTE条目(优先TLB缓存查询).
7).通过得到对应的PTE上的有效位来判断当前虚拟页是否在主存上.
8).如果索引到的PTE条目的有效位为1.则表示命中.将对应的PTE上的物理页号PPN和虚拟地址中的虚拟页偏移量VPO进行串联从而构造出主存中的物理地址PA.进入步骤9.
9).通过物理内存地址访问物理内存.当前的寻址流程结束.
10).如果有效位为0.则表示未命中.一般称这种情况为缺页.此时MMU将产生一个缺页异常.抛给操作系统.
11).操作系统捕获到缺页异常.开始执行异常处理程序.
12).选择一个牺牲页并将对应的所缺虚拟页调入并更正新页表上的PTE.如果当前牺牲页有数据.则写入磁盘.得到物理内存页号PPN.
13).缺页异常处理程序更新之前索引到PTE.并且写入物理内存页号PPN.有效位设置为1.
14).缺页处理程序再次返回原来的进程.并且再次执行缺页指令.CPU重新将虚拟地址发给MMU.此时虚拟页已经存在物理内存中.本次一定会命中.通过1-9的步骤.最终将请求的物理内存返给处理器.
内存的局部性:
内存的命中率实际上是衡量每次内存访问均能被页直接寻址到而不是产生缺页的指标.所以如果经常在一定范围内.则出现缺页异常的情况就会降低.这就是程序的一种局部性特性的体现.
局部性就是多次内存引用的时候.会出现有的内存被引用多次.而且在该位置附近的其他位置.也有可能接下来被引用.大多数程序会具备局部性的特点.
示例:
func Loop(nums []int, step int) {
length := len(nums)
for i := 0; i < step; i++ {
for j := i; j < length; j += step {
//访问内存.并写入值.
nums[j] = 4
}
}
}
Loop函数的功能是遍历数组nums.并且将nums中的每个元素均设置为4.但是这里用了一个step规定遍历的跨度.上面的程序表示访问数组的局部性.step跨度越小.则表示访问nums相邻内存的局部性约好.step越大则相反.
func CreateSource(len int) []int {
nums := make([]int, 0, len)
for i := 0; i < len; i++ {
nums = append(nums, i)
}
return nums
}
func Loop(nums []int, step int) {
length := len(nums)
for i := 0; i < step; i++ {
for j := i; j < length; j += step {
//访问内存.并写入值.
nums[j] = 4
}
}
}
func BenchmarkLoopStep1(b *testing.B) {
src := CreateSource(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Loop(src, 1)
}
}
func BenchmarkLoopStep2(b *testing.B) {
src := CreateSource(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Loop(src, 2)
}
}
func BenchmarkLoopStep3(b *testing.B) {
src := CreateSource(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Loop(src, 3)
}
}
func BenchmarkLoopStep4(b *testing.B) {
src := CreateSource(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Loop(src, 4)
}
}
func BenchmarkLoopStep5(b *testing.B) {
src := CreateSource(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Loop(src, 5)
}
}
一种蛾眉,下弦不似初弦好。庾郎未老,何事伤心早?
素壁斜辉,竹影横窗扫。空房悄,乌啼欲晓,又下西楼了。 纳兰
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路