零
文章概要
壹 前言
贰 分页
叁 虚拟内存
肆 内存空间概览
伍 MMU和段页机制
陆 pageCache
壹
前言
其实内存管理才是最贴近我们编码的底层知识。其他的可以不知道,内存管理还是需要知道一些的。不过也不用过于深究,不理解的就把它抽象成一个大接口就可以了。知道了内存管理的一些知识,对于我们理解IO,理解kafka,理解数据库也有帮助。
贰
分页
大家都知道,任何事物都是从无都有,从小到大,渐渐发展,不断改进成现在这个样子的。一开始的计算机程序,占用空间都比较少,当然计算机内存当时也比较小,不过还是可以放那么几个程序一起运行的。我们先不管这中间到底发生了什么事情,看下图,把一个内核放到内存里面还是戳戳有余的,毕竟内核精巧强悍,同时再放一个qq也没啥问题,再放一个浏览器也可以将就。不过看英雄联盟这款游戏,本身都比内存大,这辈子怕是放不进去的。
![752f1a89b0e08a4af18f150426a264b5.png](https://i-blog.csdnimg.cn/blog_migrate/1ff4f04ba1d506469b3e272bf9317db0.jpeg)
这还只是把它们挨个放到连续内存中,实际上还有一系列内存碎片化问题。本篇不过多触及。所以,linux系统将每个程序大卸八块,变成一个一个4k大小的页(这里我们只谈4k大小,当然也有分成其他大小的选项),每当我们需要用到程序的某一块时,再load到物理内存中,供进程使用。如下图
![2466d51aa431a4f0f58ed92e03f97e25.png](https://i-blog.csdnimg.cn/blog_migrate/9304668fb4c12f7341d6f5a5298d0be4.jpeg)
既然程序被分成了4k,那么物理内存当然需要按照这个规则行事最为方便,linux将物理内存也看做由一堆4k空间组成的大数组。每个4k的小格子被称为页框,很直白就是用来装页的框子。每当内存不足时,就把最不常用的页从页框换出,然后把某个进程新需要的页加载进物理内存。这个就是LRU算法(本篇不深究)。这样一来,就可以可持续发展下去了(sustainable)。不过我们肯定少说了些什么东西,毕竟这么多的页框,我们也不知道哪个在用,哪个不在用,还剩下多少,所以肯定需要有那么一个数据结构去维护这些框子。这个数据结构好像叫做页表,不过他还和另一个东西有关。
叁
虚拟内存
虽然我们解决了程序过大内存不够用的问题,但是目前好像还是程序自己在物理内存中进行数据操作,他可以通过代码找到其他进程的物理内存空间,然后对其他进程内存中的数据进行操作。这就是程序间互相干扰的问题。于是就诞生了虚拟地址。如下图,最麻烦的某种机制我们先直接封装到黑箱里面,我们只需要知道,它可以帮助我们从虚拟内存找到物理内存就可以了。
![15893072b90f9f80f9b61db31070cb7c.png](https://i-blog.csdnimg.cn/blog_migrate/231e73251612bb0d5656891a37f79169.jpeg)
我们看到虚拟内存比物理内存还要长出一大截。进程被创建后,每个进程都独享这么一个虚拟内存,64位操作系统,那就是2^64byte大小。这里小伙伴们千万不要误解,这个虚拟内存看上去大,实际上有多少数据加载到内存中,还得看物理内存中有多少和他相关的页。虚拟内存中好多页实际上目前从物理内存中还找不到与之对应的页。虚拟页与物理页的映射关系,大概可以用下图表示,MMP,哈哈哈!!!
![c57a271db26e4839bb95420f0dbe31e1.png](https://i-blog.csdnimg.cn/blog_migrate/05fec6e0807b5a6c10aac395ebecd9ea.jpeg)
我们来谈谈,为什么大家就互不干扰了呢。第一幅图中物理内存中左侧的蓝色4k页是存在的,映射完成后,我们可以直接操作该区域数据。而右侧的蓝色4k页没有没找到,于是将其他程序最近不使用的4k页换出去了,再把自己需要的4k页换到了该位置(此处其实发生了缺页中断,就不画了)。所以,你永远只能操作自己的数据,操作不到别人的数据,因为被换出去了,哈哈哈。
肆
内存空间概览
上文,我们知道每个进程独享一份虚拟内存空间,那么虚拟内存空间大概长什么样子呢。如下图,大概罗列出来一些段,不知道全不全,不过对我们理解内存管理没有影响。
![97b10f0847093800f52ecc191646143e.png](https://i-blog.csdnimg.cn/blog_migrate/8b8ae8227d0f0d314f8e152bee267cc9.jpeg)
代码段:就是我们平时写的代码数据, text section
数据段:运行代码所需要的已初始化全局变量,data section
堆段:heap
共享段:共享内存段,C库,或一些动态链接库,内存映射文件mmap将内核中的内存映射到用户进程空间,减少了内核态向用户态copy的消耗,记住这个mmap,代码中有用。
用户栈区(stack)
最后边红色的内核虚拟空间,会给每个进程一种独享一个内核的错觉,这部分内存不参与换入换出操作。与物理内存保持映射关系,所有进程其实都在与这个物理内存上的内核进行交互。接下来我们来看下虚拟地址是如何转换为物理地址的。
伍
MMU和段页机制
MMU就是上文中黑箱的一部分,他通过CPU将线性地址转换为物理地址,人称内存管理单元,这玩意是个硬件。不过只靠MMU还无法完成进程操作数据的需求,因为MMU必须配合页表进行线性地址与物理地址的转换操作。但是在MMU将线性地址(即虚拟地址)转换为物理地址之前,我们还需要一步操作,那便是先得出数据的线性地址来。
数据的线性地址则是通过段地址左移一定位数+逻辑地址的方式来实现的,这个段地址放在寄存器中。上文我们知道一个进程被操作系统分成很多段。如下图,每个段(就是我们经常见到得segment这个单词)的开头就是段基址,左移一定位数+程序中用到的偏移地址,就得到线性地址。
![4224e2124ba3747e9f8909510a93762a.png](https://i-blog.csdnimg.cn/blog_migrate/39f01cb23f29c0c3b892c217b413ef91.jpeg)
另外,操作系统每次创建一个进程,并不会马上为该进程开辟内存,根据上文我们也知道为什么了,因为只有实际需要操作内存数据时,才会load相应的页到物理内存中,但是操作系统会为每一个进程维护一张页表。这张页表,可以将虚拟内存中的页与实际内存中的页框对应起来。如何对应呢
根据 物理地址 = f (线性地址)这个映射关系,很显然是线性地址里面有某些信息可以帮助我们从页表中找到具体的物理页。线性地址大概有如下三部分内容:
1 页目录 在上述页表的基础上,引入二级页表机制,即查页表的页表。
2 页表 页目录和页表的包含的字段类似,如该条记录存不存在的标志位,不存在则会引发缺页中断,读写权限,脏页标志(dirty,这个后来以后再说),虚拟页号和物理页框号,这两个才是映射重点,类似于我们平时在数据库里通过一个id查询另一个id。
3 页框偏移 offset
所以最后的结果应该如下图所示:
![ff2232b8021cc532f537fc943e1b1d81.png](https://i-blog.csdnimg.cn/blog_migrate/3ca23dc483491fe8f69913ea6ef49f23.jpeg)
实际中的linux分页可能要比上文中的层次多一些。不过我们大概理解到这里就可以看其他的了。
陆
pageCache
Linux中通过page cache机制来加速对磁盘文件的许多访问,当它首次读取或写入数据介质时,Linux会将数据存储在未使用的内存区中,通过这些区域充当缓存(其实还是4k页,和我们上文讲的4k内存有啥区别吗,这一点我目前没有搞懂),如果再次读取这些数据时,直接从内存中快速获取该数据。当发生写操作时,Linux不会立刻执行磁盘写操作,而是把page cache中的页面标记为脏页(就是上文中页表里有个属性dirty),定期同步到存储设备中,或者手动同步(就是我们代码里经常写的flush()方法)。
对于脏页有个规则,如果内存紧缺,需要淘汰,脏页必须刷新到磁盘后才能淘汰,不过我们不用操心,这一步骤由内核完成。
![0c810756ce4a6ac83f1cb9575c88db18.png](https://i-blog.csdnimg.cn/blog_migrate/fda56f6ef0b02d7d38cd13928b628053.jpeg)