Linux实战-内存

1.linux内存是怎么工作的

内存映射

物理内存又叫主存,一般是用DRAM,只有内核才可以访问物理内存。
虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个CPU指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,
屏幕快照 2021-08-22 下午4.09.02.png
进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存.

内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系,页表实际上存储在 CPU 的内存管理单元 MMU中.如下图所示:
屏幕快照 2021-08-22 下午4.10.00.png
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行.
TLB:存储在MMU中的高速缓存
MMU不是按照字节来管理内存的,而是按照页来管理的,一页是4KB,那么32位的系统需要页表项4G/4KB=100w个页表项,就会导致页表过长。

页表过长

多级页表

多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。
屏幕快照 2021-08-22 下午4.11.50.png

大页

虚拟内存空间分布

屏幕快照 2021-08-22 下午4.12.20.png

  1. 只读段,包括代码和常量等。
  2. 数据段,包括全局变量等。
  3. 堆,包括动态分配的内存,从低地址开始向上增长。
  4. 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。
  5. 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 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 原理

一旦发现内存紧张,系统会通过三种方式回收内存。我们来复习一下,这三种方式分别是 :

  1. 基于 LRU(Least Recently Used)算法,回收缓存;
  2. 基于 Swap 机制,回收不常访问的匿名页;
  3. 基于 OOM(Out of Memory)机制,杀掉占用大量内存的进程。

活跃和非活跃的内存页,按照类型的不同,又分别分为文件页和匿名页,对应着缓存回收和 Swap 回收。

Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。
所谓换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。

既然 Swap 是为了回收内存,那么 Linux 到底在什么时候需要回收内存呢?前面一直在说内存资源紧张,又该怎么来衡量内存是不是紧张呢?

一个最容易想到的场景就是,有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。这个过程通常被称为直接内存回收。

除了直接内存回收,还有一个专门的内核线程用来定期回收内存,也就是kswapd0。为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是

页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages
_free 表示。

屏幕快照 2021-08-22 下午7.10.00.png
kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。

剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。

我们可以看到,一旦剩余内存小于页低阈值,就会触发内存的回收。这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下
pages_low = pages_min5/4
pages_high = pages_min
3/2

NUMA 与 Swap

屏幕快照 2021-08-22 下午7.11.53.png

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.找到系统内存的问题

内存系统指标

内存性能指标.xmind

内存性能工具

性能指标和工具的联系

屏幕快照 2021-08-22 下午9.19.40 1.png
屏幕快照 2021-08-22 下午9.19.47.png

如何迅速分析内存的性能瓶颈

屏幕快照 2021-08-22 下午9.20.25.png

小结

  1. 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时Swap 的使用倾向。
  2. 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。
  3. 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
  4. 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。
  5. 通过 /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获取;次缺页异常:分配内存的时候,直接从物理内存处分配

优化:

  1. 最好禁止swap,如果必须开启swap,降低swappiness的值,减少内存回收时swap的使用倾向
  2. 减少内存的动态分配,
  3. 尽量使用缓存和缓冲区来访问数据
  4. 使用cgroups等方式限制进程的内存使用情况
  5. 通过/proc/pid/oom_adj,调整核心应用的oom_score

参考

  1. linux性能优化实践-倪鹏飞
  • 43
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值