Linux性能优化策略

在 Linux 系统的世界里,内存就如同人体的血液,为系统的运行提供着源源不断的动力。然而,随着系统的日益复杂和应用程序的不断增多,内存管理变得愈发关键。如果对内存性能不加优化,系统就可能会像一个疲惫不堪的运动员,逐渐变得迟缓、卡顿,甚至出现崩溃的情况。因此,掌握 Linux 内存性能优化技巧,就如同给系统注入了一针强心剂,能让它重新焕发生机,更加高效地运转。今天,就让我们一起深入探索 Linux 内存性能优化的奥秘,让你的系统如虎添翼吧!

一、引言

在工作生活中,我们时常会遇到一些性能问题:比如手机用久了,在滑动窗口或点击 APP 时会出现页面反应慢、卡顿等情况;比如运行在某台服务器上进程的某些性能指标(影响用户体验的 PCT99 指标等)不达预期,产生告警等;造成性能问题的原因多种多样,可能是网络延迟高、磁盘 IO 慢、调度延迟高、内存回收等,这些最终都可能影响到用户态进程,进而被用户感知。

在 Linux 服务器场景中,内存是影响性能的主要因素之一,本文从内存管理的角度,总结归纳了一些常见的影响因素(比如内存回收、Page Fault 增多、跨 NUMA 内存访问等),并介绍其对应的调优方法。

二、内存回收

2.1 什么是内存

内存(Memory)是计算机的重要部件之一,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,与硬盘等外部存储器交换的数据。

它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。

只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来;内存的运行也决定计算机整体运行快慢的程度。

2.2 Linux内存回收机制

为啥要回收:内核需要为任何时刻突发到来的内存申请提供足够的内存,以便cache的使用和其他相关内存的使用不至于让系统的剩余内存长期处于很少的状态。当真的有大于空闲内存的申请到来的时候,会触发强制内存回收。

内存回收针对的目标有两种,一种是针对zone的,另一种是针对一个memcg的,把针对zone的内存回收方式分为三种,分别是快速内存回收、直接内存回收、kswapd内存回收。

在整机层面,设置了三条水线:min、low、high;当系统 free 内存到 low 水线以下时,系统会唤醒kswapd 线程进行异步内存回收,一直回收到 high 水线为止,这种情况不会阻塞正在进行内存分配的进程;但如果 free 内存降到了 min 水线以下,就需要阻塞内存分配进程进行回收,不然就有 OOM(out of memory)的风险,这种情况下被阻塞进程的内存分配延迟就会提高,从而感受到卡顿。

些水线可以通过内核提供的 /proc/sys/vm/watermark_scale_factor 接口来进行调整,该接口 合法取值的范围为 [0, 1000],默认为 10,当该值设置为 1000 时,意味着 low 与 min 水线,以及 high 与 low 水线间的差值都为总内存的 10% (1000/10000)。

针对 page cache 型的业务场景,我们可以通过该接口抬高 low 水线,从而更早的唤醒 kswapd 来进行异步的内存回收,减少 free 内存降到 min 水线以下的概率,从而避免阻塞到业务进程,以保证影响业务的性能指标。
在 memory cgroup 层面,目前内核没有设置水线的概念,当内存使用达到 memory cgroup 的内存限制后,会阻塞当前进程进行内存回收。不过内核在 v5.19内核 中为 memory cgroup提供了 memory.reclaim 接口,用户可以向该接口写入想要回收的内存大小,来提早触发 memory cgroup 进行内存回收,以避免阻塞 memory cgroup 中的进程。

查看Linux内存情况

查看/proc/meminfo


[root@test ~]# cat /proc/meminfo
MemTotal:       16166688 kB
MemFree:        14051412 kB
MemAvailable:   14772588 kB
Buffers:            2116 kB
Cached:          1073260 kB
SwapCached:            0 kB
Active:           770384 kB
Inactive:         698264 kB
Active(anon):     450156 kB
Inactive(anon):    76748 kB
Active(file):     320228 kB
Inactive(file):   621516 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:      33554428 kB
SwapFree:       33554428 kB
Dirty:               476 kB
Writeback:             0 kB
AnonPages:        393328 kB
Mapped:           153828 kB
Shmem:            133628 kB
Slab:             246448 kB
SReclaimable:     133892 kB
SUnreclaim:       112556 kB
KernelStack:       13472 kB
PageTables:        30496 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    41637772 kB
Committed_AS:    4257776 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      320696 kB
VmallocChunk:   34350426108 kB
HardwareCorrupted:     0 kB
AnonHugePages:    155648 kB
CmaTotal:              0 kB
CmaFree:               0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:      279276 kB
DirectMap2M:     6965248 kB
DirectMap1G:    11534336 kB

使用free命令查看


[root@test ~]# free -h
              total        used        free      shared  buff/cache   available
Mem:            15G        874M         13G        130M        1.2G         14G
Swap:           31G          0B         31G

参数说明:

  • total:总内存大小。
  • used:已经使用的内存大小(这里面包含cached和buffers和shared部分)。
  • free:空闲的内存大小。
  • shared:进程间共享内存(一般不会用,可以忽略)。
  • buffers:内存中写完的东西缓存起来,这样快速响应请求,后面数据再定期刷到磁盘上。
  • cached:内存中读完缓存起来内容占的大小(这部分是为了下次查询时快速返回)。
  • available:还可以被应用程序使用的物理内存大小,和free的区别是,free是真正未被使用的内存,available是包括buffers、cached的。
  • Swap:硬盘上交换分区的使用大小。

Buffer和Cache

通过free或者top命令我们可以发现其将内存分为了buffer和cache等部分。Buffer 和 Cache 的设计目的,是为了提升系统的 I/O 性能。它们利用内存,充当起慢速磁盘与快速 CPU 之间的桥梁,可以加速 I/O 的访问速度。通过man free可以查阅到下面的说明


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)

Buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers 值。

Cache 是内核页缓存和 Slab 用到的内存,对应的是 /proc/meminfo 中的 Cached 与 SReclaimable 之和。

进一步通过man proc,可以看到具体说明:


Buffers %lu
    Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).
Cached %lu
   In-memory cache for files read from the disk (the page cache).  Doesn't include SwapCached.
SReclaimable %lu (since Linux 2.6.19)
    Part of Slab, that might be reclaimed, such as caches.   
SUnreclaim %lu (since Linux 2.6.19)
    Part of Slab, that cannot be reclaimed on memory pressure.

Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。

Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。

SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。

最终总结:Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。

从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。

从读的角度来说,不仅可以提高那些频繁访问数据的读取速度,也降低了频繁 I/O 对磁盘的压力。

三、Huge Page

内存作为宝贵的系统资源,一般都采用延迟分配的方式,应用程序第一次向分配的内存写入数据的时候会触发 Page Fault,此时才会真正的分配物理页,并将物理页帧填入页表,从而与虚拟地址建立映射。

此后,每次 CPU 访问内存,都需要通过 MMU 遍历页表将虚拟地址转换成物理地址。为了加速这一过程,一般都会使用 TLB(Translation-Lookaside Buffer)来缓存虚拟地址到物理地址的映射关系,只有 TLB cache miss 的时候,才会遍历页表进行查找。

页的默认大小一般为 4K, 随着应用程序越来越庞大,使用的内存越来越多,内存的分配与地址翻译对性能的影响越加明显。试想,每次访问新的 4K 页面都会触发 Page Fault,2M 的页面就需要触发 512 次才能完成分配。
另外 TLB cache 的大小有限,过多的映射关系势必会产生 cacheline 的冲刷,被冲刷的虚拟地址下次访问时又会产生 TLB miss,又需要遍历页表才能获取物理地址。

对此,Linux 内核提供了大页机制。上图的 4 级页表中,每个 PTE entry 映射的物理页就是 4K,如果采用 PMD entry 直接映射物理页,则一次 Page Fault 可以直接分配并映射 2M 的大页,并且只需要一个 TLB entry 即可存储这 2M 内存的映射关系,这样可以大幅提升内存分配与地址翻译的速度。


因此,一般推荐占用大内存应用程序使用大页机制分配内存。当然大页也会有弊端:比如初始化耗时高,进程内存占用可能变高等。可以使用 perf 工具对比进程使用大页前后的 PageFault 次数的变化:


perf stat -e page-faults -p  -- sleep 5

目前内核提供了两种大页机制,一种是需要提前预留的静态大页形式,另一种是透明大页(THP, Transparent Huge Page) 形式。

3.1 静态大页

首先来看静态大页,也叫做 HugeTLB。静态大页可以设置 cmdline 参数在系统启动阶段预留,比如指定大页 size 为 2M,一共预留 512 个这样的大页:


hugepagesz=2M hugepages=512

还可以在系统运行时动态预留,但该方式可能因为系统中没有足够的连续内存而预留失败。

预留默认 size(可以通过 cmdline 参数 default_hugepagesz=指定size)的大页:


echo 20 > /proc/sys/vm/nr_hugepages

预留特定 size 的大页:


echo 5 > /sys/kernel/mm/hugepages/hugepages-*/nr_hugepages

预留特定 node 上的大页:


echo 5 > /sys/devices/system/node/node*/hugepages/hugepages-*/nr_hugepages

当预留的大页个数小于已存在的个数,则会释放多余大页(前提是未被使用) 。

编程中可以使用 mmap(MAP_HUGETLB) 申请内存。

这种大页的优点是一旦预留成功,就可以满足进程的分配请求,还避免该部分内存被回收;缺点是:

需要用户显式地指定预留的大小和数量

需要应用程序适配,比如:

  • mmap、shmget 时指定 MAP_HUGETLB;
  • 挂载 hugetlbfs,然后 open 并 mmap

当然也可以使用开源 libhugetlbfs.so,这样无需修改应用程序

预留太多大页内存后,free 内存大幅减少,容易触发系统内存回收甚至 OOM

紧急情况下可以手动减少 nr_hugepages,将未使用的大页释放回系统;也可以使用 v5.7 引入的HugeTLB + CMA 方式,细节读者可以自行查阅。

3.2 透明大页

再来看透明大页,在 THP always 模式下,会在 Page Fault 过程中,为符合要求的 vma 尽量分配大页进行映射;如果此时分配大页失败,比如整机物理内存碎片化严重,无法分配出连续的大页内存,那么就会 fallback 到普通的 4K 进行映射,但会记录下该进程的地址空间 mm_struct;然后 THP 会在后台启动khugepaged 线程,定期扫描这些记录的 mm_struct,并进行合页操作。因为此时可能已经能分配出大页内存了,那么就可以将此前 fallback 的 4K 小页映射转换为大页映射,以提高程序性能。整个过程完全不需要用户进程参与,对用户进程是透明的,因此称为透明大页。

虽然透明大页使用起来非常方便、智能,但也有一定的代价:进程内存占用可能远大所需:因为每次Page Fault 都尽量分配大页,即使此时应用程序只读写几KB

可能造成性能抖动

  • 在第 1 种进程内存占用可能远大所需的情况下,可能造成系统 free 内存更少,更容易触发内存回收;系统内存也更容易碎片化。
  • khugepaged 线程合页时,容易触发页面规整甚至内存回收,该过程费时费力,容易造成 sys cpu 上升。
  • mmap lock 本身是目前内核的一个性能瓶颈,应当尽量避免 write lock 的持有,但 THP 合页等操作都会持有写锁,且耗时较长(数据拷贝等),容易激化 mmap lock 锁竞争,影响性能。

因此 THP 还支持 madvise 模式,该模式需要应用程序指定使用大页的地址范围,内核只对指定的地址范围做 THP 相关的操作。这样可以更加针对性、更加细致地优化特定应用程序的性能,又不至于造成反向的负面影响。

可以通过 cmdline 参数和 sysfs 接口设置 THP 的模式:cmdline 参数:


transparent_hugepage=madvise

sysfs 接口:


echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

四、mmap_lock 锁

锁是内存管理中的一把知名的大锁,保护了诸如mm_struct 结构体成员、 vm_area_struct 结构体成员、页表释放等很多变量与操作。

mmap_lock 的实现是读写信号量, 当写锁被持有时,所有的其他读锁与写锁路径都会被阻塞。Linux 内核已经尽可能减少了写锁的持有场景以及时间,但不少场景还是不可避免的需要持有写锁,比如 mmap 以及 munmap 路径、mremap 路径和 THP 转换大页映射路径等场景。

应用程序应该避免频繁的调用会持有 mmap_lock 写锁的系统调用 (syscall), 比如有时可以使用 madvise(MADV_DONTNEED)释放物理内存,该参数下,madvise 相比 munmap 只持有 mmap_lock 的读锁,并且只释放物理内存,不会释放 VMA 区域,因此可以再次访问对应的虚拟地址范围,而不需要重新调用 mmap 函数。

另外对于 MADV_DONTNEED,再次访问还是会触发 Page Fault 分配物理内存并填充页表,该操作也有一定的性能损耗。如果想进一步减少这部分损耗,可以改为 MADV_FREE 参数,该参数也只会持有 mmap_lock 的读锁,区别在于不会立刻释放物理内存,会等到内存紧张时才进行释放,如果在释放之前再次被访问则无需再次分配内存,进而提高内存访问速度。

一般 mmap_lock 锁竞争激烈会导致很多 D 状态进程(TASK_UNINTERRUPTIBLE),这些 D 进程都是进程组的其他线程在等待写锁释放。因此可以打印出所有 D 进程的调用栈,看是否有大量 mmap_lock 的等待。


for i in `ps -aux | grep " D" | awk '{ print $2}'`; do echo $i; cat /proc/$i/stack; done

内核社区专门封装了 mmap_lock 相关函数,并在其中增加了 tracepoint,这样可以使用 bpftrace 等工具统计持有写锁的进程、调用栈等,方便排查问题,确定优化方向。


bpftrace -e 'tracepoint:mmap_lock:mmap_lock_start_locking /args->write == true/{ @[comm, kstack] = count();}'

五、跨numa内存访问

在 NUMA 架构下,CPU 访问本地 node 内存的速度要大于远端 node,因此应用程序应尽可能访问本地 node 上的内存。可以通过 numastat 工具查看 node 间的内存分配情况:

观察整机是否有很多 other_node 指标(远端内存访问)上涨:


watch -n 1 numastat -s

查看单个进程在各个node上的内存分配情况:


numastat -p 

5.1 绑 node

可以通过 numactl 等工具把进程绑定在某个 node 以及对应的 CPU 上,这样该进程只会从该本地 node 上分配内存。但这样做也有相应的弊端, 比如:该 node 剩余内存不够时,进程也无法从其他 node 上分配内存,只能期待内存回收后释放足够的内存,而如果进入直接内存回收会阻塞内存分配,就会有一定的性能损耗。

此外,进程组的线程数较多时,如果都绑定在一个 node 的 CPU 上,可能会造成 CPU 瓶颈,该损耗可能比远端 node 内存访问还大,比如 ngnix 进程与网卡就推荐绑定在不同的 node 上,这样虽然网卡收包时分配的内存在远端 node 上,但减少了本地 node 的 CPU 上的网卡中断,反而可以获得更好的性能提升。

5.2 numa balancing

内核还提供了 numa balancing 机制,可以通过 /proc/sys/kernel/numa_balancing 文件或者cmdline参数 numa_balancing=进行开启。

该机制可以动态的将进程访问的 page 从远端 node 迁移到本地 node 上,从而使进程可以尽可能的访问本地内存。但该机制实现也有相应的代价, 在 page 的迁移是通过 Page Fault 机制实现的,会有相应的性能损耗;另外如果迁移时找不到合适的目标 node,可能还会把进程迁移到正在访问的 page 的 node 的 CPU 上,这可能还会导致 cpu cache miss,从而对性能造成更大的影响。因此需要根据业务进程的具体行为,来决定是否开启 numa balancing 功能。

六、内存映射mmap

mmap内存映射的实现过程,总的来说可以分为三个阶段:

(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址

3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化

4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中

(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。

6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。

7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。

8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。

(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。

11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。

12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

6.1 mmap优点总结

由上文讨论可知,mmap优点共有一下几点:

1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。

同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

6.2 mmap相关函数

函数原型


void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

返回说明

成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1], error被设为以下的某个值:


返回错误类型

参数

start:映射区的开始地址

length:映射区的长度

prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起


prot

flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体


flag

fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1

offset:被映射对象内容的起点

相关函数

int munmap( void * addr, size_t len )

成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;

该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;

当映射关系解除后,对原来映射地址的访问将导致段错误发生。


int msync( void *addr, size_t len, int flags )

一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

七、虚拟内存空间

7.1 线性空间

线性地址空间:是指Linux系统中从0x00000000到0xFFFFFFFF整个4GB虚拟存储空间。线性空间又分为用户空间和内核空间。

用户空间(进程地址空间)

用户空间是指从0x00000000到0xBFFFFFFF共3GB的线性地址空间,每个进程都有一个独立的3GB用户空间,所以用户空间由每个进程独有,但是内核线程没有用户空间,因为它不产生用户空间地址。另外子进程共享(继承)父进程的用户空间只是使用与父进程相同的用户线性地址到物理内存地址的映射关系,而不是共享父进程用户空间。运行在用户态和内核态的进程都可以访问用户空间。

linux采用虚拟内存管理技术,每一个进程都有一个3G大小的独立的进程地址空间,这个地址空间就是用户空间。每个进程的用户空间都是完全独立、互不相干的。进程访问内核空间的方式:系统调用和中断。
创建进程等进程相关操作都需要分配内存给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址。实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际页框的程序。该异常是虚拟内存机制赖以存在的基本保证,它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在的映射到了物理地址上。

内核空间

内核空间表示运行在处理器最高级别的超级用户模式(supervisor mode)下的代码或数据,内核空间占用从0xC0000000到0xFFFFFFFF的1GB线性地址空间,内核线性地址空间由所有进程共享,但只有运行在内核态的进程才能访问,用户进程可以通过系统调用切换到内核态访问内核空间,进程运行在内核态时所产生的地址都属于内核空间。

内核空间又可分为以下几个线性空间:

1. 内核逻辑地址空间:是指从PAGE_OFFSET(3G)3G+896)之间的线性地址空间,是系统物理内存映射区,它映射了全部或部分(如果系统包含高端内存)物理内存。内核逻辑地址空间中的地址与RAM内存物理地址空间中对应的地址只差一个固定偏移量(3G),如果RAM内存物理地址空间从0x00000000地址编址,那么这个偏移量就是PAGE_OFFSET。

2. 高端线性地址空间:从high_memory(3G+896M)到0xFFFFFFFF之间的线性地址空间属于高端线性地址空间,其中VMALLOC_START~VMALLOC_END之间线性地址:(1)被vmalloc()函数用来分配物理上不连续但线性地址空间连续的高端物理内存,或者(2)被vmap()函数用来映射高端或低端物理内存,或者(3)由ioremap()函数来重新映射I/O物理空间。其中PKMAP_BASE开始的LAST_PKMAP(一般等于1024)页线性地址空间:被kmap()函数用来永久映射高端物理内存。FIXADDR_START开始的KM_TYPE_NR*NR_CPUS页线性地址空间:被kmap_atomic()函数用来临时映射高端物理内存,其他未用高端线性地址空间可以用来在系统初始化期间永久映射I/O地址空间。

7.2 物理空间

低端内存(物理内存)

内核逻辑地址空间所映射物理内存就是低端内存(实际物理内存的大小,但是小于896),低端内存在Linux线性地址空间中始终有永久的一一对应的内核逻辑地址,系统初始化过程中将低端内存永久映射到了内核逻辑地址空间,为低端内存建立了虚拟映射页表。低端内存内物理内存的物理地址与线性地址之间的转换可以通过__pa(x)和__va(x)两个宏来进行,#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) __pa(x)将内核逻辑地址空间的地址x转换成对应的物理地址,相当于__virt_to_phys((unsigned long)(x)),__va(x)则相反,把低端物理内存空间的地址转换成对应的内核逻辑地址,相当于((void *)__phys_to_virt((unsigned long)(x)))。

高端内存(物理内存)

低端内存地址之上的物理内存是高端内存(物理内存896之上),高端内存在Linux线性地址空间中没有没有固定的一一对应的内核逻辑地址,系统初始化过程中不会为这些内存建立映射页表将其固定映射到Linux线性地址空间,而是需要使用高端内存的时候才为分配的高端物理内存建立映射页表,使其能够被内核使用,否则不能被使用。高端内存的物理地址于线性地址之间的转换不能使用上面的__pa(x)和__va(x)宏。

高端内存概念的由来:如上所述,Linux将4GB的线性地址空间划分成两部分,从0x00000000到0xBFFFFFFF共3GB空间作为用户空间由用户进程独占,这部分线性地址空间并没有固定映射到物理内存空间上;从0xC0000000到0xFFFFFFFF的第4GB线性地址空间作为内核空间,在嵌入式系统中,这部分线性地址空间除了映射物理内存空间之外还要映射处理器内部外设寄存器空间等I/O空间。0xC0000000~high_memory之间的内核逻辑地址空间专用来固定映射系统中的物理内存,也就是说0xC0000000~high_memory之间空间大小与系统的物理内存空间大小是相同的(当然在配置了CONFIG_DISCONTIGMEMD选项的非连续内存系统中,内核逻辑地址空间和物理内存空间一样可能存在内存孔洞),如果系统中的物理内存容量远小于1GB,那么内核线性地址空间中内核逻辑地址空间之上的high_memory~0xFFFFFFFF之间还有足够的空间来固定映射一些I/O空间。

可是,如果系统中的物理内存容量(包括内存孔洞)大于1GB,那么就没有足够的内核线性地址空间来固定映射系统全部物理内存以及一些I/O空间了,为了解决这个问题,在x86处理器平台设置了一个经验值:896MB,就是说,如果系统中的物理内存(包括内存孔洞)大于896MB,那么将前896MB物理内存固定映射到内核逻辑地址空间0xC0000000~0xC0000000+896MB(=high_memory)上,而896MB之后的物理内存则不建立到内核线性地址空间的固定映射,这部分内存就叫高端物理内存。此时内核线性地址空间high_memory~0xFFFFFFFF之间的128MB空间就称为高端内存线性地址空间,用来映射高端物理内存和I/O空间。

896MB是x86处理器平台的经验值,留了128MB线性地址空间来映射高端内存以及I/O地址空间,在嵌入式系统中可以根据具体情况修改这个阈值,比如,MIPS中将这个值设置为0x20000000B(512MB),那么只有当系统中的物理内存空间容量大于0x20000000B时,内核才需要配置CONFIG_HIGHMEM选项,使能内核对高端内存的分配和映射功能。什么情况需要划分出高端物理内存以及高端物理内存阈值的设置原则见上面的内存页区(zone)概念说明。

八、内存泄漏和内存溢出

内存溢出(OOM,out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

8.1 如何判断内存泄露

用 jstat -gcutil PID,观察Old这个参数,如果每次执行完FULLGC之后Old区的值一次比一次升高,就可以判断为发生了内存泄漏。

8.2 如何判断内存溢出

Heap Dump(堆转储文件)它是一个Java进程在某个时间点上的内存快照。Heap Dump是有着多种类型的。不过总体上heap dump在触发快照的时候都保存了java对象和类的信息。通常在写heap dump文件前会触发一次FullGC,所以heap dump文件中保存的是FullGC后留下的对象信息。

通过设置如下的JVM参数,可以在发生OutOfMemoryError后获取到一份HPROF二进制Heap Dump文件:


-XX:+HeapDumpOnOutOfMemoryError

生成的文件会直接写入到工作目录。

注意:该方法需要JDK5以上版本。

转存堆内存信息后,需要对文件进行分析,从而找到OOM的原因。可以使用以下方式:

mat:eclipse memory analyzer, 基于eclipse RCP的内存分析工具。

jhat:JDK自带的java heap analyze tool,可以将堆中的对象以html的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言OQL,分析相关的应用后,可以通过http://localhost:7000来访问分析结果。不推荐使用。

8.3 释放内存

在Linux系统下,我们一般不需要去释放内存,因为系统已经将内存管理的很好。但是凡事也有例外,有的时候内存会被缓存占用掉,导致系统使用SWAP空间影响性能,例如当你在linux下频繁存取文件后,物理内存会很快被用光,当程序结束后,内存不会被正常释放,而是一直作为caching。此时就需 要执行释放内存(清理缓存)的操作了。

释放内存操作:


sync  # 强制将内存中的缓存写入磁盘

echo 数字 > /proc/sys/vm/drop_caches #数字可以是0-3的整数

数字含义:

  • 0:不释放(系统默认值)
  • 1:释放页缓存
  • 2:释放dentries和inodes
  • 3:释放所有缓存

九、内存性能参数

9.1 缓存命中率

缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比。命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。实际上,缓存是现在所有高并发系统必需的核心模块,主要作用就是把经常访问的数据(也就是热点数据),提前读入到内存中。这样,下次访问时就可以直接从内存读取数据,而不需要经过硬盘,从而加快应用程序的响应速度。

我们可以通过cachestat和cachetop命令进行查看:


$ cachestat 1 3
   TOTAL   MISSES     HITS  DIRTIES   BUFFERS_MB  CACHED_MB
       2        0        2        1           17        279
       2        0        2        1           17        279
       2        0        2        1           17        279

这些指标从左到右依次表示:

  • TOTAL ,表示总的 I/O 次数;
  • MISSES ,表示缓存未命中的次数;
  • HITS ,表示缓存命中的次数;
  • DIRTIES, 表示新增到缓存中的脏页数;
  • BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位;
  • CACHED_MB 表示 Cache 的大小,以 MB 为单位。

$ cachetop
11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending
PID      UID      CMD       HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
13029    root     python    1        0        0        100.0%     0.0%

READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率。除此之外,还可以使用pcstat来查看文件的缓存情况,如查看/bin/ls


$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 0         | 000.000 |
+---------+----------------+------------+-----------+---------+

9.2 Swap和NUMA

当物理内存页不足的时候,kswapd0会触发页面交换,但是在一些时候我们会发现Swap在剩余内存很多的情况下也异常升高,这就是和NUMA有关系了。numactl命令可以查看处理器在Node的分布情况。


$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1
node 0 size: 7977 MB
node 0 free: 4416 MB
...

内存阈值(页最小阈值、页低阈值和页高阈值)可以通过内存域在 proc 文件系统中的接口/proc/zoneinfo来查看


$ cat /proc/zoneinfo
...
Node 0, zone   Normal
 pages free     227894
       min      14896
       low      18620
       high     22344
...
     nr_free_pages 227894
     nr_zone_inactive_anon 11082
     nr_zone_active_anon 14024
     nr_zone_inactive_file 539024
     nr_zone_active_file 923986
...

使用free命令可以查看当前Swap的使用情况


$ free
             total        used        free      shared  buff/cache   available
Mem:        8169348      331668     6715972         696     1121708     7522896
Swap:             0           0           0

Linux 本身支持两种类型的 Swap,即 Swap 分区和 Swap 文件。以 Swap 文件为例,开启方式如下


# 创建Swap文件
$ fallocate -l 8G /mnt/swapfile
# 修改权限只有根用户可以访问
$ chmod 600 /mnt/swapfile
# 配置Swap文件
$ mkswap /mnt/swapfile
# 开启Swap
$ swapon /mnt/swapfile

9.3 测试工具dd

dd命令可以用于生成临时文件以及测试读取速度、


# 生成一个512MB的临时文件
$ dd if=/dev/sda1 of=file bs=1M count=512
# 清理缓存
$ echo 3 > /proc/sys/vm/drop_caches

#测试读取
$ dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 16.0509 s, 33.4 MB/s

十、性能工具总结

具体的分析思路主要有这几步:

  • 先用 free 和 top,查看系统整体的内存使用情况。
  • 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
  • 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。

10.1 定位内存问题高效方法

  • 1. 先用 free 和 top,查看系统整体的内存使用情况。
  • 2. 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型。
  • 3. 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等。

10.2 优化内存方法

  • 1. 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。
  • 2.减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。
  • 3.尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问。
  • 4.使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。
  • 5.通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死。

十一、实战记录

11.1 free 命令

  • total 总内存大小;
  • used 已使用内存的大小,包含了共享内存;
  • free 未使用内存的大小;
  • shared 共享内存的大小;
  • buff/cache 缓存和缓冲区的大小;
  • available 新进程可用内存的大小;

11.2 top 命令

  • VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内;
  • RES 是常驻内存的大小,即进程实际使用的物理内存大小,但不包括 Swap 和共享内存;
  • SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等;
  • %MEM 是进程使用物理内存占系统总内存的百分比;

注意

  • 1. 虚拟内存通常并不会全部分配物理内存;
  • 2. 共享内存 SHR 并不一定是共享的,如程序的代码段、非共享的动态链接库,也都算在 SHR 里;

11.3 案例一,写文件(磁盘和文件写案例)


测试命令
$ dd if=/dev/urandom of=/tmp/file bs=1M count=500
 
监控命令
vmstat 2
  • 1. 在 Cache 刚开始增长时,块设备 I/O 很少,而过一段时间后,才会出现大量的块设备写;
  • 2. 当 dd 命令结束后,Cache 不再增长,但块设备写还会持续一段时间,并且多次 I/O 写的结果加起来是 dd 要写的 500M 的数据;

11.4 案例二,写磁盘(磁盘和文件写案例)


# 运行dd命令向磁盘分区/dev/sdb1写入2G数据
$ dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
 
监控命令
vmstat 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
1  0      0 7584780 153592  97436    0    0   684     0   31  423  1 48 50  2  0
 1  0      0 7418580 315384 101668    0    0     0     0   32  144  0 50 50  0  0
 1  0      0 7253664 475844 106208    0    0     0     0   20  137  0 50 50  0  0
 1  0      0 7093352 631800 110520    0    0     0     0   23  223  0 50 50  0  0
 1  1      0 6930056 790520 114980    0    0     0 12804   23  168  0 50 42  9  0
 1  0      0 6757204 949240 119396    0    0     0 183804   24  191  0 53 26 21  0
 1  1      0 6591516 1107960 123840    0    0     0 77316   22  232  0 52 16 33  0

写磁盘时 (即 bo 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快得多;

11.5 案例一、读文件(磁盘和文件读案例)


测试命令
# 运行dd命令读取文件数据
$ dd if=/tmp/file of=/dev/null
 
监控命令
vmstat 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  1      0 7724164   2380 110844    0    0 16576     0   62  360  2  2 76 21  0
 0  1      0 7691544   2380 143472    0    0 32640     0   46  439  1  3 50 46  0
 0  1      0 7658736   2380 176204    0    0 32640     0   54  407  1  4 50 46  0
 0  1      0 7626052   2380 208908    0    0 32640    40   44  422  2  2 50 46  0

读取文件时 (即 bi 大于 0 时),Buffer 保持不变,而 Cache 则在不停增长;

11.6 案例二、读磁盘(磁盘和文件读案例)


测试命令
# 运行dd命令读取文件
$ dd if=/dev/sda1 of=/dev/null bs=1M count=1024
 
监控命令
vmstat 2
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 7225880   2716 608184    0    0     0     0   48  159  0  0 100  0  0
 0  1      0 7199420  28644 608228    0    0 25928     0   60  252  0  1 65 35  0
 0  1      0 7167092  60900 608312    0    0 32256     0   54  269  0  1 50 49  0
 0  1      0 7134416  93572 608376    0    0 32672     0   53  253  0  0 51 49  0
 0  1      0 7101484 126320 608480    0    0 32748     0   80  414  0  1 50 49  0

读磁盘时 (也就是 bi 大于 0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快很多;

文件系统与磁盘的区别

    磁盘是一个存储设备(块设备),可以被划分为不同的磁盘分区,而在磁盘或者磁盘分区上,还可以再创建文件系统,并挂载到系统的某个目录中,这样,系统就可以通过这个挂载目录,来读写文件;即磁盘是存储数据的块设备,也是文件系统的载体;文件系统需要通过磁盘,来保证数据的持久化存储;
    在读写普通文件时,I/O 请求会首先经过文件系统,然后由文件系统负责,来与磁盘进行交互;在读写块设备文件时,会跳过文件系统,直接与磁盘交互,也就是所谓的 “裸 I/O”;

    统计所有进程的物理内存使用量

    每个进程的 PSS ,是指把共享内存平分到各个进程后,再加上进程本身的非共享内存大小的和;

    
    # 使用grep查找Pss指标后,再用awk计算累加值
    $ grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB\n", total }'
    391266 kB
    

    留下回复

    您的邮箱地址不会被公开。 必填项已用 * 标注