硬件结构
CPU缓存一致性
软中断
操作系统结构
Linux内核 vs Windows内核
内存管理
为啥有虚拟内存?
虚拟内存
若两个程序都引用绝对物理地址,不可行。
操作系统会通过一种机制把不同进程的虚拟地址和不同内存的物理地址映射起来,这样不同进程运行的时候,写入的是不同的物理地址,就不会冲突了,程序使用的内存地址是 虚拟内存地址,实际硬件里面的空间地址是 物理内存地址。CPU芯片中的内存管理单元(MMU)的映射关系将虚拟地址转变成物理地址,然后通过物理地址访问内存。
内存分段
管理虚拟地址和物理地址之间的关系 方式1
程序由各种段(代码段、数据分段、栈段、堆段)组成,不同的段有不同的属性,所以就用分段形式分离段。
虚拟地址组成:段选择因子和段内偏移量
虚拟地址是通过段表与物理地址进行映射
缺陷:
- 内存碎片问题:主要出现外部内存碎片问题(解决问题:内存交换)
- 内存交换效率低问题 : 内存交换时,若交换的是一个占内存空间很大的程序,整个机器就会显得卡顿。
为解决上面的问题出现了内存分页!
内存分页
管理虚拟地址和物理地址之间的关系 方式2
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为4KB。虚拟地址与物理地址之间通过页表来映射。
进程访问的虚拟地址再也表中查不到时,系统产生一个缺页异常。
分页:页与页之间紧密排列,所以不会有外部碎片。内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,从而出现内部内存碎片。
若内存空间不够,把最近没使用的内存页面释放掉(换出),需要再加载进来(换入),所以内存交换效率高
缺陷: 空间上缺陷,页表可能会很大
多级页表和TCB解决上述问题:多级页表解决了空间上的问题,但会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项。
段页式内存管理
内存分段和内存分页组合起来
实现方式:
- 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
- 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
虚拟内存有啥作用?
- 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
malloc如何分配内存?
虚拟地址空间内部分为内核空间和用户空间,32位系统内核空间占用1G,位于高处,剩下3G是用户空间。64位系统的内核空间和用户空间都是128T,分别占最高和最低处,中间部分是未定义的。
用户空间分布情况,从低到高分别是6种不同的内存段:
代码段:
数据段:
BSS段:未初始化的静态变量和全局变量
堆段:(内存动态分配)
文件映射段:(内存动态分配)
栈段:
malloc申请内存时有两种方式向操作系统申请堆内存
- brk()系统调用从堆分配。将「堆顶」指针向高地址移动,获得新的内存空间。(用户分配的内存小于 128 KB)
- mmap()系统调用从文件映射区域分配。调用中{ 私有匿名映射}的方式在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。(用户分配的内存大于 128 KB)
malloc() 分配的是虚拟内存。
malloc(1)分配多大内存?
malloc() 分配内存时,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。
free释放内存后,会归还给操作系统吗?
brk()方式下,先把这点内存缓存着放进malloc的内存池里,进程再次申请同样大小的内存时就可以直接复用。
mmap()方式下,是直接归还给操作系统的。
只用mmap : 频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
只用brk() : 随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露"。
内存满了,发生什么?
内存分配过程
应用程序申请内存时,实际上是在申请虚拟内存,当应用程序读写了这块虚拟内存,CPU就会去访问这个虚拟内存,若发现这个虚拟内存没有映射到物理内存就产生缺页中断,进程从用户态切换到内核态,将缺页中断交给缺页中断函数处理,函数看有没有空闲物理内存,有就直接分配物理内存并建立与虚拟内存之间的映射关系。若没有空闲的物理内存,内核就开始进行回收内存的工作:直接内存回收(不行的话内核就放大招——OOM)、后台内存回收。
OOM——OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置 。

那些内存被回收?
文件页:回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
匿名页:回收的方式是通过 Linux 的 Swap 机制
LRU 回收算法,实际上维护着 active(存放最近被访问的内存页) 和 inactive(最近很少被访问的页面) 两个双向链表
啥条件下触发kswapd内核线程回收内存呢?
内核定义了三个内存阈值,用来衡量当前剩余内存是否充裕或者紧张:1.页最小阈值,2. 页低阈值,3. 页高阈值;当剩余内存页(pages_free)小于页低阈值(pages_low),就会触发 kswapd 进行后台回收,然后 kswapd 会一直回收到剩余内存页(pages_free)大于页高阈值(pages_high)
NUMA架构
NUMA和SMP架构都是针对CPU的。
SMP:一种多个CPU处理器共享资源的电脑硬件架构。存在问题:随着 CPU 处理器核数的增多,多个 CPU 都通过一个总线访问内存,这样总线的带宽压力会越来越大,同时每个 CPU 可用带宽会减少。
为解决上述问题,提出NUMA(非一致存储访问结构):NUMA 架构将每个 CPU 进行了分组,每一组 CPU 用 Node 来表示,一个 Node 可能包含多个 CPU 。每个 Node 有自己独立的资源,包括内存、IO 等。
如何保护一个进程不被 OOM 杀掉呢?
在系统空闲内存不足的情况,进程申请了一个很大的内存,如果直接内存回收都无法回收出足够大的空闲内存,那么就会触发 OOM 机制,内核就会根据算法选择一个进程杀掉。
怎么避免预读失效和缓存污染?
传统的 LRU 算法并没有被 Linux 和 MySQL 使用,因为传统LRU存在两个问题:
- 预读失效,导致缓存命中率下降
- 缓存污染,导致缓存命中率下降
预读
读取 4KB 数据,实际上内核使用预读机制(ReadaHead)读取了 16KB 数据,通过一次磁盘顺序读将多个 Page 数据装入 Page Cache。这样下次读取 4KB 数据后面的数据时就不用从磁盘读取了,直接在 Page Cache 即可命中数据。因此,预读机制带来的好处就是减少了磁盘 I/O 次数,提高系统磁盘 I/O 吞吐量。
若这些被提前加载进来的页,并没有被访问,相当于这个预读工作是白做了,这个就是预读失效。不会被访问的预读页却占用了 LRU 链表前排的位置,而末尾淘汰的页,可能是热点数据,这样就大大降低了缓存命中率。
避免预读失效
让预读页停留在内存里的时间要尽可能的短,让真正被访问的页才移动到 LRU 链表的头部,从而保证真正被读取的热数据留在内存里的时间尽可能长。
- Linux 操作系统实现两个了 LRU 链表:活跃 LRU 链表(active_list)和非活跃 LRU 链表(inactive_list);
- MySQL 的 Innodb 存储引擎是在一个 LRU 链表上划分来 2 个区域:young 区域 和 old 区域。
这俩设计思想都是类似的,都是将数据分为了冷数据和热数据,然后分别进行 LRU 算法。
有了这两个 LRU 链表后,预读页就只需要加入到 inactive list 区域的头部,当页被真正访问的时候,才将页插入 active list 的头部。如果预读的页一直没有被访问,就会从 inactive list 移除,不会影响active list中的热点数据。
Mysql:划分为young和old区域,划分这两个区域后,预读的页就只需要加入到old区域的头部,当页被真正访问的时候,才将页插入young区域的头部。
缓存污染
如果使用「只要数据被访问一次,就将数据加入到活跃 LRU 链表头部(或者 young 区域)」这种方式的话,那么还存在缓存污染的问题。
当批量读取数据的时候,由于数据被访问了一次,这些大量数据都会被加入到「活跃 LRU 链表」里,让之前缓存在活跃 LRU 链表(或者 young 区域)里的热点数据全部都被淘汰了,如果这些大量的数据在很长一段时间都不会被访问的话,那么整个活跃 LRU 链表(或者 young 区域)就被污染了。
缓存污染的时候,这些热点数据再次访问的时候,由于缓存未命(之前大量热点数据已经被淘汰了)中,就会产生大量的磁盘I/O,系统性能下降。
避免缓存污染造成的影响
LRU 算法只要数据被访问一次,就将数据加入活跃 LRU 链表(或者 young 区域),这种 LRU 算法进入活跃 LRU 链表的门槛太低了。只要

最低0.47元/天 解锁文章
1377





