Linux 内存管理
内存映射
linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。
虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个 cpu 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如图:
并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。
内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表(页表实际上存储在 cpu 的内存管理单元 mmu 中),记录虚拟地址与物理地址的映射关系。
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。(内存调用,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。)
多级页表和大页
mmu 规定了一个内存映射的最小单位,也就是页,通常是 4 kb 大小。这样,每一次内存映射,都需要关联 4 kb 或者 4kb 整数倍的内存空间。
多级页表(multilevel page tables)就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。linux 用的正是四级页表来管理内存页。如下图所示,虚拟地址被分为 5 个部分,前 4 个表项用于选择页,而最后一个索引表示页内偏移。
大页(hugepage)就是比普通页更大的内存块,常见的大小有 2mb 和 1gb。大页通常用在使用大量内存的进程上,比如 oracle、dpdk 等。
虚拟内存空间分布
在这五个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 c 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存
内存回收机制
系统不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存:
- 回收缓存,比如使用 lru(least recently used)算法,回收最近使用最少的内存页面;
- 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;
回收不常访问的内存时,会用到交换分区(以下简称 swap)。swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。
- 杀死进程,内存紧张时系统还会通过 oom(out of memory),直接杀掉占用大量内存的进程。
oom它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分。管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。
内存工具
free
[root@k8s ~]# watch -d free every 2.0s: free wed apr 8 15:59:31 2020 total used free shared buff/cache available mem: 8173864 4094104 276572 436676 3803188 3333024 swap: 0 0 0
- 第一列,total 是总内存大小;
- 第二列,used 是已使用内存的大小,包含了共享内存;
- 第三列,free 是未使用内存的大小;
- 第四列,shared 是共享内存的大小;
- 第五列,buff/cache 是缓存和缓冲区的大小;
- 最后一列,available 是新进程可用内存的大小(包括了可回收的缓存,所以一般会比未使用内存更大)。
top
[root@k8s ~]# top ………… kib mem : 8173864 total, 275696 free, 4094212 used, 3803956 buff/cache kib swap: 0 total, 0 free, 0 used. 3332920 avail mem pid user pr ni virt res shr s %cpu %mem time+ command 3482 root 20 0 2430460 1224 760 s 85.1 0.0 3557:06 kswapd0 …………
- virt 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内。
- res 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 swap 和共享内存。
- shr 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等。
- %mem 是进程使用物理内存占系统总内存的百分比。
buffer 和 cache
- 为了协调 cpu 与磁盘间的性能差异,linux 还会使用 cache 和 buffer ,分别把文件和磁盘读写的数据缓存到内存中。
- buffers memory used by kernel buffers (buffers in /proc/meminfo)
- cache memory used by the page cache and slabs (cached and sreclaimable in /proc/meminfo)
- buff/cache sum of buffers and cache
man proc 对 proc 文件系统的说明
- buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20mb 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
- cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
- sreclaimable 是 slab 的一部分。slab 包括两部分,其中的可回收部分,用 sreclaimable 记录;而不可回收部分,用 sunreclaim 记录。
buffer 是对磁盘数据的缓存,而 cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。
总结
对普通进程来说,它能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由系统映射为物理内存。
当进程通过 malloc() 申请内存后,内存并不会立即分配,而是在首次访问时,才通过缺页异常陷入内核中分配内存。
由于进程的虚拟地址空间比物理内存大很多,linux 还提供了一系列的机制,应对内存不足的问题,比如缓存的回收、交换分区 swap 以及 oom 等。
buffer 和 cache 分别缓存磁盘和文件系统的读写数据。
- 从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。
- 从读的角度来说,既可以加速读取那些需要频繁访问的数据,也降低了频繁 i/o 对磁盘的压力。
整理自极客时间:《linux性能优化实战》