1.linux内存是怎么工作的
内存映射
物理内存又叫主存,一般是用DRAM,只有内核才可以访问物理内存。
虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个CPU指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,
进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存.
内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系,页表实际上存储在 CPU 的内存管理单元 MMU中.如下图所示:
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行.
TLB:存储在MMU中的高速缓存
MMU不是按照字节来管理内存的,而是按照页来管理的,一页是4KB,那么32位的系统需要页表项4G/4KB=100w个页表项,就会导致页表过长。
页表过长
多级页表
多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。
大页
虚拟内存空间分布
- 只读段,包括代码和常量等。
- 数据段,包括全局变量等。
- 堆,包括动态分配的内存,从低地址开始向上增长。
- 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。
- 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。
内存分配与回收
malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和mmap()。
对小块内存(小于128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。
brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
而 mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是malloc 只对大块内存使用 mmap 的原因。
在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:
1.回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;
2.回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;
3.杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。
如何查看内存使用情况
free、top、ps
总结
对普通进程来说,它能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由系统映射为物理内存。
当进程通过 malloc() 申请内存后,内存并不会立即分配,而是在首次访问时,才通过缺页异常陷入内核中分配内存。
由于进程的虚拟地址空间比物理内存大很多,Linux 还提供了一系列的机制,应对内存不足的问题,比如缓存的回收、交换分区 Swap 以及 OOM 等。
当你需要了解系统或者进程的内存使用情况时,可以用 free 和 top 、ps 等性能工具。它们都是分析性能问题时最常用的性能工具,希望你能熟练使用它们,并真正理解各个指标的含义。
2.内存中的Buffer和Cache
Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。
Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与SReclaimable 之和。
proc 文件系统
Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。
Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。
3.系统缓存优化程序的运行效率
缓存命中率
命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。
cachestat 提供了整个操作系统缓存的读写命中情况。
cachetop 提供了每个进程的缓存命中情况。
pcstat,文件在内存中的缓存大小以及缓存比例。
总结
Buffers 和 Cache 可以极大提升系统的 I/O 性能。通常,我们用缓存命中率,来衡量缓存的使用效率。命中率越高,表示缓存被利用得越充分,应用程序的性能也就越好。
你可以用 cachestat 和 cachetop 这两个工具,观察系统和进程的缓存命中情况。其中,cachestat 提供了整个系统缓存的读写命中情况。cachetop 提供了每个进程的缓存命中情况。
不过要注意,Buffers 和 Cache 都是操作系统来管理的,应用程序并不能直接控制这些缓存的内容和生命周期。所以,在应用程序开发中,一般要用专门的缓存组件,来进一步提升性能。比如,程序内部可以使用堆或者栈明确声明内存空间,来存储需要缓存的数据。再或者,使用 Redis 这类外部缓存服务,优化数据的访问效率。
4.内存泄漏了,我该如何定位和处理
先用vmstat查看系统整体的变化趋势,然后发现可用内存在不断变少,此时可用说明内存也许存在泄漏。进一步查出哪个进程导致可用内存减少,用memleak命令,发现是我们写的那个函数。然后打开这个函数,看看对应的代码有没有释放内存。
总结
应用程序可以访问的用户内存空间,由只读段、数据段、堆、栈以及文件映射段等组成。其中,堆内存和内存映射,需要应用程序来动态管理内存段,所以我们必须小心处理。不仅要会用标准库函数 malloc() 来动态分配内存,还要记得在用完内存后,调用库函数_free() 来 _ 释放它们。
今天的案例比较简单,只用加一个 free() 调用就能修复内存泄漏。不过,实际应用程序就
复杂多了。比如说,
malloc() 和 free() 通常并不是成对出现,而是需要你,在每个异常处理路径和成功路径
上都释放内存 。
在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。
更复杂的是,在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。
所以,为了避免内存泄漏,最重要的一点就是养成良好的编程习惯,比如分配内存后,一定要先写好内存释放的代码,再去开发其他逻辑。还是那句话,有借有还,才能高效运转,再借不难。
当然,如果已经完成了开发任务,你还可以用 memleak 工具,检查应用程序的运行中,内存是否泄漏。如果发现了内存泄漏情况,再根据 memleak 输出的应用程序调用栈,定位内存的分配位置,从而释放不再访问的内存。
5.系统的swap变高
如果系统内存紧张,该怎么做呢?
Swap 把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
Swap 原理
一旦发现内存紧张,系统会通过三种方式回收内存。我们来复习一下,这三种方式分别是 :
- 基于 LRU(Least Recently Used)算法,回收缓存;
- 基于 Swap 机制,回收不常访问的匿名页;
- 基于 OOM(Out of Memory)机制,杀掉占用大量内存的进程。
活跃和非活跃的内存页,按照类型的不同,又分别分为文件页和匿名页,对应着缓存回收和 Swap 回收。
Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。
所谓换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。
既然 Swap 是为了回收内存,那么 Linux 到底在什么时候需要回收内存呢?前面一直在说内存资源紧张,又该怎么来衡量内存是不是紧张呢?
一个最容易想到的场景就是,有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。这个过程通常被称为直接内存回收。
除了直接内存回收,还有一个专门的内核线程用来定期回收内存,也就是kswapd0。为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是
页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages
_free 表示。
kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。
剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。
我们可以看到,一旦剩余内存小于页低阈值,就会触发内存的回收。这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下
pages_low = pages_min5/4
pages_high = pages_min3/2
NUMA 与 Swap
swappiness
对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。
而对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。
Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。
swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。
总结
在内存资源紧张时,Linux 通过直接内存回收和定期扫描的方式,来释放文件页和匿名页,以便把内存分配给更需要的进程使用。
文件页的回收比较容易理解,直接清空,或者把脏数据写回磁盘后再释放。
而对匿名页的回收,需要通过 Swap 换出到磁盘中,下次访问时,再从磁盘换入到内存中。
你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值(也就是页低阈值),还可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。
在 NUMA 架构下,每个 Node 都有自己的本地内存空间,而当本地内存不足时,默认既可以从其他 Node 寻找空闲内存,也可以从本地内存回收。你可以设置 /proc/sys/vm/zone_reclaim_mode ,来调整 NUMA 本地内存的回收策略。
6.找到系统内存的问题
内存系统指标
内存性能工具
性能指标和工具的联系
如何迅速分析内存的性能瓶颈
小结
- 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时Swap 的使用倾向。
- 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。
- 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
- 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。
- 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。
7. 总结
buffer:缓存磁盘数据(读写)
cache:缓存文件数据(读写)
内存怎么工作:每个进程都分配了一个虚拟空间,不同位数的计算机的虚拟空间大小不一样,虚拟空间通过MMU中的页表找到对应的物理空间
并不是立刻给虚拟空间分配对应的物理空间,而是当要用到的时候再去分配,访问到的时候,页表中找不到对应的虚拟空间地址,就会产生一个缺页异常,然后MMU为这个虚拟空间分配一个物流空间,并更新页表,然后返回用户态。
虚拟空间分为用户空间和内核空间,用户空间由五部分组成:只读段 数据段 堆 栈 文件映射段,每个线程的内核空间都指向同一个物理内存
缓存命中率来评价缓存:cachestat(缓存的名字情况)、cachetop(每个进程的缓存命中情况)、pcstat
分配内存:malloc,包括brk和mmap,brk适用于小内存的分配,mmap适用于大内存。brk是不释放内存,把这一段当做缓存使用,mmap是释放内存。brk会有内存碎片,但是可以利用这个缓存提升发生缺页异常时的效率;mmap没有内存碎片,但缺页异常发生的时候,效率更低。
swap:不用的时候把内存中的数据换出到磁盘,用到的时候再从磁盘换入到内存
内存资源紧张的时候,linux通过直接内存回收和定期扫描的方式,来释放文件页和匿名页,以便把内存给需要的进程使用
文件页的回收,直接清空或者把脏数据写回磁盘后释放
匿名页的回收,需要通过swap换出到磁盘,下次访问的时候,再从磁盘换入到内存中
页最小值
页低位
页高位
主缺页异常:分配内存的时候,需要通过IO获取;次缺页异常:分配内存的时候,直接从物理内存处分配
优化:
- 最好禁止swap,如果必须开启swap,降低swappiness的值,减少内存回收时swap的使用倾向
- 减少内存的动态分配,
- 尽量使用缓存和缓冲区来访问数据
- 使用cgroups等方式限制进程的内存使用情况
- 通过/proc/pid/oom_adj,调整核心应用的oom_score
参考
- linux性能优化实践-倪鹏飞