小林图解系统内存管理
三、内存管理
虚拟内存
单片机没有操作系统,每次写完代码都需要烧录进去,程序才能跑起来,单片机的CPU是之间操作内存的物理地址,这种情况下,是不可能同时运行两个程序的,第二个程序在内存某一位置插入的值会擦除掉第一个程序放在相同位置上的所有内容。
操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。如果程序要访问虚拟内存地址的时候,由CPU的内存管理单元MMU转换成不同的物理内存地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了;
内存分段
程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。 不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来;
分段机制下的虚拟内存由段选择子和段内偏移量组成;
分段的方法存在的问题:内存碎片和内存交换效率低
- 外部内存碎片:产生了多个不连续的小物理内存,导致新的程序无法被装载。
- 内部内存碎片:程序所有的内存都被装载到了物理内存,但这个程序有部分的内存可能并不是很常用,这也会导致内存的浪费。
解决外部内存问题的方法就是内存交换。也就是把原来不相邻的内存的内容先写到硬盘上,然后再从硬盘上读回到内存里,不过再读回时应该连续。但是硬盘的IO比较慢,因此效率比较低。如果交换的是一个占空间很大的程序,机器就会卡顿。
内存分页
把整个虚拟和物理内存空间切成一段段固定尺寸的大小,这样的连续且尺寸固定的内存空间,称为页,Linux下,每页的大小为4KB。
虚拟地址和物理地址之间通过页表来映射:
如果内存空间不够,操作系统会把其他正在运行的进程中最近没被使用的内存页面给释放掉,也就是暂时写在磁盘上,当需要使用的时候再加载进来。
此外,分页使得在加载程序时,可以不一次性的把程序都加载到物理内存中,只有在程序运行时,再加载到物理内存中去。
为了解决页表占据内存空间过大的问题,引出了多级页表的概念;但是多级页表也带来的问题,就是虚拟地址到物理地址的转换变复杂了,时间开销增大。
一个解决方案就是引入TLB(Translation Lookaside Buffer),也就是页表缓存,也就是把访问概率高的页表单独存起来,MMU在转换地址时先查TLB,没有再查常规的页表。
段页式内存管理:
- 先将程序划分为多个有逻辑意义的段,也就是分段机制;
- 再把每个段分成多个页
地址由段号,段内页号和页内偏移组成
用户空间分布的情况:
从低到高是7种不同的内存段:
- 程序文件段,包括二进制可执行代码;
- 已初始化数据段,包括静态常量;
- 未初始化数据段,包括未初始化的静态变量;
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关);
- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,⼀般是 8 MB 。当然系统也提供了参数,以便我们自定义大小;
在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用C标准库的malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。
Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史, Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0 ,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。