内存映射
只有内核才可以直接访问物理内存。Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便的访问内存,更确切的说是访问虚拟内存。
虚拟内存地址空间内部又被分为内核空间和用户空间两部分,不同CPU指令可以处理的最大长度的处理器,地址空间范围也不同,32位,最大4G,64位系统都是定义的 128 T,并不是说最大。分别占据内存空间的最高位和最低位,剩下的中间部分是未定义的。
虽然每个进程的地址空间都包含了内核空间,但是这些内核空间,其实关联的都是相同的物理内存。这样,进程从用户态切换到内核态后,就可以很方便的访问内核空间内存。
内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。
页表实际存储在CPU内存管理单元的 MMU 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。
当进程访问的虚拟地址在页表中查不到就会产生缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。(仅仅表示进程将要使用某块虚拟内存,但是此时并没有在真正的物理内存上进行创建)
MMU 并不是以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是4KB大小。这样,每一次内存映射,都需要关联 4KB 或者 4KB 整数倍的内存空间。
页的大小只有4KB,页表就会很大,32位就有100多万页(4GB/4KB),Linux 提供了两种机制解决这个问题,就是多级页和大页。
多级页就是虚拟地址分为5个部分,前4个表项用于选择页,而最后一个索引表示页内偏移。
大页就是比普通页更大的内存块,常见的又2MB和1GB。大页通常用在使用大量内存的进程上,比如 Oracle。
虚拟内存分为: 从高位(0xffffffff)到低位分为 内核空间、栈、文件映射、堆、数据段、只读段
- 只读段: 包括代码和常量等
- 数据段:包括全局变量等
- 堆,动态分配的内存,从低地址开始向上增长
- 文件映射段,动态库、共享内存等,从高地址向低地址增长
- 栈:包括局部变量和函数调用的上下文等,大小一般是固定的 8M。
内存分配:
小内存(小于128KB), C 标准使用 brk() 来分配,通过移动堆顶来份额皮内存。这些内存释放后,不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
大块内存(大约128K),直接使用 mmap() 来分配,也就是在文件映射段找一空空闲内存分配出去。
第一种方式,可以减少缺页异常,提高内存访问效率。由于这些内存没有归还系统,频繁的内存分配和释放会造成内存碎片。
第二种方式:因为释放直接归还系统,所以每次 mmap 都会发生缺页异常,如果频繁大内存分配释放,会增大内核管理内存的负担。
Linux 主要通过 slab 分配器管理小内存。
回收内存:
通过 LRU 算法,回收最近使用最小的内存页面
回收不常访问内存,把不常用的内存通过交换分区直接写入磁盘中(交换分区 Swap 效率很低,会对性能造成严重的影响)
杀死进程,内存紧张时,会报 OOM(Out Of Memory),直接杀掉占用大量内存的进程。
Buffer&Cache
- Buffer: 缓存起来,准备写入硬盘,可以将多次少量写转成一次大量写。另外,读磁盘的时候,磁盘数据会缓存到 Buffer 中。
- Cache:将文件数据缓存到内存,下次访问的时候直接从内存返回,不必再去硬盘加载。实际上 Cache 也会缓存写文件时的数据。
总结:Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。
Buffer 和 Cache 的设计目的,是为了提升系统 I/O 性能。它们利用内存,充当起慢速磁盘与快速 CPU 之间的桥梁,可以加快 I/O 访问速度。
1 | stack -p $(pgrep app) // 跟踪进程 |
如果缓存(Cache)命中率很高,然而缓存读取次数却很少,可以考虑是否使用了直接 IO,即直接从磁盘上读取数据,而不经过操作系统。
磁盘读和文件读:
1 | 提示,如果没有空硬盘不要进行写硬盘操作,否则有可能造成数据丢失 |
工具:
查看哪些进程被 OOM 杀死了
1 | dmesg | grep -i "Out of memory" |