Linux 内存管理(十一)调试信息
/proc/meminfo
在 Linux 系统中查看内存信息最准确的方法是查看/proc/meminfo 节点信息,他包含当前时刻系统的所有的物理页面信息:
1 | vooxle@liushuai:~/workspace/blog$ cat /proc/meminfo |
meminfo 节点实现在 meminfo_proc_show() 函数里,该函数在 fs/proc/meminfo.c 文件里。
- MemTotal:系统当前可用物理内存总量,通过读取全局变量 _totalram_pages 来获得
- MemFree: 系统当前剩余空闲物理内存,通过读取全局变量 wm_zone_stat[] 数组中的 NR_FREE_PAGES 来获得
- Memavailable: 系统中可使用页面的数量,由 si_mem_available() 函数计算而来,计算公式为 Available= memfree + pagecache + reclaimable - totalreserve_pages。这里包括了空闲页面( memfree)、文件映射页面( pagecache)、可回收的页面( reclaimable),最后减去系统保留的页面
- Buffers:用于块层的缓存,由 nr_blockdev_pages() 函数来计算
- Cached: 用于页面高速缓存的页面。计算公式为 Cached= NR_FILE_PAGES - swap_cache - Buffers
- SwapCached: 这里统计交换缓存的数量,交换缓存类似于内容缓存,只不过它对应的是交换分区,而内容缓存对应的是文件。这里表示匿名页面曾经被交換出去,现在又被交換回来,但是页面内容还在交换缓存中
- Active:活跃的匿名页面( LRU_ACTIVE_ANON)和活跃的文件映射页面( LRU_ACTIVE_FILE)
- Inactive: 不活跃的匿名页面( LRU_INACTIVE_ANON)和不活跃的文件映射页面( LRU_INACTIVE_FILE)
- Active(anon): 活跃的匿名页面( LRU_ACTIVE_ANON)
- Inactive(anon): 不活跃的匿名页面( LRU_INACTIVE_ANON)
- Active(file): 活跃的文件映射页面( LRU_ACTIVE_FILE)
- Inactive(file): 不活跃的文件映射页面( LRU_INACTIVE_FILE)
- Unevictable: 不能回收的页面( LRU_UNEVICTABLE)
- Mlocked: 不会被交换到交换分区的页面,由全局的 vm_zone_stat[] 中的 NR_MLOCK 来统计
- SwapTotal: 交换分区的大小
- Swapfree: 交换分区的空闲空间大小
- Dirty: 脏页的数量,由全局的 vm_node_stat[] 中的 NR_FILE_DIRTY 来统计
- Writeback: 正在回写的页面数量,由全局的 vm_node_stat[] 中的 NR_WRITEBACK 来统计
- Anonpages: 统计有反向映射(RMAP)的页面,通常这些页面都是匿名页面并且都映射到了用户空间,但是并不是所有匿名页面都配置了反向映射,如部分的 shmem 和 tmps 页面就没有设置反向映射。这个计数由全局的 vm_node_stat[] 中的 NR_ANON_MAPPED 来统计
- Mapped: 统计所有映射到用户地址空间的内容级存页面,由全局的 vm_node_stat[] 中的 NR_FILE_MAEED 来统计
- Shmem: 共享内存(基于 tmps 实现的 shmem、 devtmfs 等)页面的数量,由全局的 vm_node_stat[] 中的 NR_SHMEM 来统计
- KReclaimable: 内核可回收的内存,包括可回收的 slab 页面( NR_SLAB_RECLAIMABLE)和其他的可回收的内核页面( NR_KERNEL_MISC_RECLAIMABLE)
- Slab: 所有 slab 页面,包括可回收的 slab 页面( NR_SLAB_RECLAIMABLE)和不可回收的 slab 页面 (NR_SLAB_UNRECLAIMABLE)
- Reclaimable: 可回收的 slab 页面( NR_SLAB_RECLAIMABLE)
- SUnreclaim: 不可回收的 slab 页面(NR_SLAB_UNRECLAIMABLE)
- Kernelstack: 所有进程内核栈的总大小,由全局的 vm_zone_stat[] 中的 NR_KERNEL_STACK_KB 来统计
- Pagetables: 所有用于页表的页面数量,由全局的 vm_zone_stat[] 中的 NR_PAGETABLE 来统计
- NFS_Unstable: 在 NFS 中,发送到服务器端但是还没有写入磁盘的页面(INR_UNSTABLE_NFS)
- WritebackTmp: 回写过程中使用的临时缓存( NR_WRITEBACK_TEMP)
- VmallocTotal: vmalloc 区域的总大小
- VmallocUsed: 已经使用的 vmalloc 区域总大小
- Percpu: percpu 机制使用的页面,由 pcpu_nr_pages() 函数统计
- AnonHugePage: 统计透明巨页的数量
- ShmemHugePages: 统计在 shmem 或者 tmpfs 中使用的透明巨页的数量
- ShmemPmdMapped: 使用透明巨页并且映射到用户空间的 shmem 或者 tmps 的页面数量
- CmaTotal: CMA 机制使用的内存
- CmaFree: CMA 机制中空闲的内存
- HugePages_Total: 普通巨页的数量,普通巨页的页面是预分配的
- Hugepages_Free: 空闲的普通巨页的数量
- Hugepageslze: 普通巨页的大小,通常是 2MB 或者 1GB
- Hugetlb: 普通巨页的总大小,单位是 KB
读者可能会对上述内容感到疑惑,下面归纳常见的问题。
为什么 MemTotal 不等于 QEMU 虚拟机中分配的内存大小?
读者可能会发现 MemTotal 显示的总内存大小并不等于物理系统中真实的内存大小或者 QEMU 虚拟机中分配的内存大小,如在 QEMU 虚拟机启动参数中指定内存大小为 1GB 进入 QEMU 虚拟机后发现 MemTotal 为“99984KB"。这是因为内核静态使用的内存(如内核代码等)在启动阶段需要用到,它没有计入 MemTotal 统计项中,而是统计到 reserved 中。下面是一个计算机的内核启动日志信息:
1 | [0.000000] Memory: 929640K/1048576K available(23228K kernel code, 1090K rwdata, 3872K rodata, 4608K init, 503K bss, 53400K reserved, 65536K cma-reserved) |
从上述内核日志可以看到,在启动初始化时有 53400 大小的内容被保留了,用于内核代码等。在内核初始化完成之后,init 段的内存会被释放,因此被保留的内存大小为 53400KB-4608KB=48792KB,加上 MemTotal 正好是 1GB 内存
MemAvailable 究竟是什么意思?
MemavAilable 表示系统中有多少可以利用的内存,这些内存不包括交换分区。 MemAvailable 的计算和 MemFree、可回收的 slab 页面、内容页面以及每个内存管理区的最低水位等有密切关系。
为什么 sab 分配器要区分 SReclaimable 和 SUnreclaim
一个 slab 分配器由一个或者多个连续的物理页面组成。在为 slab 分配器分配物理页面时根据 slab 描述符( cache->flags)是否设置了 SLAB_RECLAIM_ACCOUNT 标志位来判断这些页面是属于 SReclaimable 还是属于 SUnreclaim.
1 | <mm/slab.c> |
而在创建 sab 描述符时若发现设置了 SLAB_RECLAIM_ACCOUNT,那么分配物理页面的行为就是可回收的,即设置 __GFP_RECLAIMABLE,表示这些页面是可以被 sab 机制的收割机回收的。
1 | <mm/slab.c> |
因此,这些 slb 分配器的页面的迁移类型是 MIGRATE_RECLAIMABLE。在页面回收机中会调用 slab 收割机的回调函数( shrinker->scan_objects)来回收一些 slab 对象,但是在 scan_objects 回调函数的实现中并没有判断哪些 slab 分配器的页面设置了__GFP_RECLAIMABLE,哪些页面没有设置__GFP_RECLAIMABLE。在 slab 机制里,有一个定时器会定时扫描和检查哪些 slab 分配器可以被销毁,如果一个 slab 分配器中都是空闲的 slab 对象那么这个 slab 分配器就可以被回收,并且 slab 分配器占用的页面会被释放,见 cache_reap() 数。因此,统计 SReclaimable 和 SUnreclaim 页面的含义是在于计算系统可用的总内存数 meminfo 中的 MemAvailable,详见 si_mem_available() 函数。
1 | <mm/page_alloc.c> |
为什么 Active(anon)+ nactive(anon)不等于 Anonpages
我们知道 Active(anon)表示 LRU 链表中的活跃匿名页面, Inactive(anon)表示不活跃匿名页面,这两个值相加,表示系统的 LRU 链表中的总匿名页面数量。而 AnonPages 表示和用户态进程地址空间建立映射关系。当一个匿名页面和进程地址空间建立映射关系时会调用 page_add_new_anon_mmap() 函数来新增一个 RMAP。
1 | void page_add_new_anon_rmap () |
但是 shmem(基于 tmpfs 实现)使用的页面会被添加到系统的匿名页面的 LRU 链表中因此它会被计入 Active(anon)或者 Inactive(anon)之中。主要原因是 shmem 使用的页面基于 RAM 内存,它可以被写入交换分区里。在分配 shmem 页面时设置了 PG_SwapBacked 标志位,见 shmem_alloc_and_acct_page() 函数。
1 | static struct page *shmem_alloc_and_acct_page () |
通过判断 PG_SwapBacked 标志位来定将页面加到名页面的 LRU 链表中还是文件映射的 LRU 链表中,见 page_is_file_cache() 函数。若没有设置 PG_SwapBacked 标志位,则页面是文件映射的页面,会被添加到文件映射的 LRU 链表中否则,被添加到名页面的 LRU 链表中。
1 | static inline int page_is_file_cache(struct page * page) |
另外, shmem 页面并没有计入 Anonpages 中,而是计入了 MM_SHMEMPAGES 类型的计数值(即 Shmem)中,见 do_shared_fault()->finish_fault()->alloc_set_pte() 函数。
1 | vm_fault_t alloc_set_pte() |
在 page_add_file_rmap() 中会把这个页面计入 NR_FILE_MAPPED 计数值中。
1 | void page_add_file_rmap() |
总之, shmem 页面一方面被添加到了置名页面的 LRU 链表里,另一方面被统计到文件映射页面的计数中,真是个“另类的”页面。
为什么 Active(file) + Inactive(file) 不等于 Mapped?
Active(file)+ nactive(file) 表示系统 LRU 链表中所有文件映射页面的总和,而 Mapped 表示统计所有映射到用户地址空间的内容缓存页面,由 NR_FILE_MAPPED 来统计。当一个内容缓存映射到用户态的进程地址空间时,会调用 page_add_file_map() 函数来建立 RMAP,并增加 NR_FILE_MAPPED 计数值。
1 | void page_add_file_rmap() |
有一个特殊情况需要考虑,就是 shmem 页面。它会被计入 NR_FILE_MAPPED 计数值中但是它会设置 PG_SwapBacked 标志位,因此它会被计入匿名页面。 当创建一个 shmem 页面时会把它计入 NR_FILE_PAGES 和 NR_SHMEM 计数值中,见 shmem_add_to_page_cache() 函数。
1 | static int shmem_add_to_page_cache() |
为什么 Active(file)+ Inactive(file)不等于 Cached?
Cached 计数值的计算公式是 Cached = NR_FILE_PAGES - swap_cache - Buffers。但是 shmem 页面被计入 NR_FILE_PAGES 里,同时,它也在匿名页面 LRU 链表的计数值里 Active(file) + Inactive(file)表示系统的 LRU 链表中所有文件映射页面的总和,因此 LRU 表所有文件映射页面总和不等于 Cached 计数值。
伙伴系统信息
/proc/buddyinfo 节点包含当前系统的伙件系统简要信息 而 proc/pagetypeinfo 节点则包含当前系统的伙伴系统详细信息,包括每个迁移类型和每个链表的成员数量等。当前系统只有一个 DMA32 的内存管理区,支持的迁移类型有 Unmovable、 Movable、Reclaimable、 HighAtomic、CMA 以及 Isolate 等迁移类型,其中页面数量最多的迁移类型是 Movable 类型。迁移类型的最小的单位是页块,在 ARM64 架构中,页块的大小是 2MB,即 order 为 9 其中一共有 512 个页面:
1 | root@liushuai:/home/vooxle/workspace/blog# cat /proc/pagetypeinfo |
读者需要注意,页块的大小和普通巨页有关。当系统配置了 CONFIG_HUGETLB_PAGE 时,页块的 order 大小等于 HUGETLB_PAGE_ORDER,通常是 9:否则,页块的 order 大小是 10,如下图所示。
1 |
查看内存管理区的信息
/proc/zoneinfo 节点包含当前系统所有内存管理区的信息。/proc/zoneinfo 节点是显示如下几部分信息。
当前内存节点的内存统计信息
下面是/proc/zoneinfo 节点的第一部分信息。
1 | root@liushuai:/home/vooxle/workspace/blog# cat /proc/zoneinfo |
在第 2 行中,表示当前内存节点是第 0 个内存节点,当前内存管理区为 DMA。 在第 3 行中,表示下面是该内存节点的总体信息。如果当前内存管理区是内存节点的第一个内存管理区,那么会显示该内存节点的总信息。它通过 node_page_state() 函数来读取内存节点的数据结构 plist_data 中的 vm_stat 计数值。 上述信息是在 zoneinfo_show_print() 函数中输出的。
1 | <mm/vmstat.c> |
当前内存管理区的总信息下面继续看/proc/zoneinfo 节点的信息
下面继续看/proc/zoneinfo 节点的第二部分信息。
1 | pages free 3971 |
- pages free: 表示这个内存管理区中空闲页面的数量
- min:表示这个内存管理区中处于最低警戒水位的页面数量
- low:表示这个内存管理区中处于低水位的页面数量
- high:表示这个内存管理区中处于高水位的页面数量
- spanned:表示这个内存管理区包含的页面数量
- present:表示这个内存管理区里实际管理页面的数量
- managed:表示这个内存管理区中被伙伴系统管理的页面数量
- protection:表示这个内存管理区预留的内存
内存管理区详细的页面信息
接下来是内存管理区详细的页面信息/proc/zoneinfo 节点的第三部分信息
1 | nr_free_pages 3971 |
上述是这个内存管理区详细的页面信息。它通过 zone_page_state() 函数来读取 zone 数据结构中的 vm_stat 计数值。
每个 CPU 内存分配器的信息
最后是每个 C 内存分配器的/proc/zonelnf。节点的第四部分信息
1 | pagesets |
- pagesets:表示每个 CPU 内存分配器中每个 CPU 缓存的页面信息。
- node_unreclaimable:表示页面回收失败的次数。
- start_pfn:表示内存管理区的起始页帧号
查看与进程相关的内存信息
进程的 mm_struct 数据结构中有一个 rss_stat 成员,它用于记录进程的内存使用情况。
1 | /* |
进程的 mm_struct 数据结构会记录下面 4 种页面的数量
- MM_FILEPAGES:进程使用的文件映射的页面数量。
- MM_ANONPAGES:进程使用的匿名页面数量。
- MM_SWAPENTS:进程使用的交换分区的匿名页面数量
- MM_SHMEMPAGES:进程共享的内存的页面数量
增加和减小进程内存计数的接口函数有如下几个。
1 | #获取 member 计数值 |
proc 文件系统包含每个进程的相关信息,其中 /proc/PID/status 节点有不少和具体进程内存相关的信息。下面是 sshd 线程的状态信息,只截取了和内存相关的信息。
1 | root@liushuai:/proc/2# cat status |
- Name:进程的名称
- Pid:PID。
- VmPeak:进程使用的最大虚拟内存,通常情况下它等于进程的内存描述符 mm 中的 total_vm
- VmSize:进程使用的虚拟内存,它等于 mm->total_vm
- VmLck:进程锁住的内存,它等于 m->locked_vm,这里指使用 mlock() 锁住的内存。
- VmPin:进程固定住的内存,它等于 mm->pinned_vm,这里指使用 get_user_page() 固定住的内存。
- VmHWM:进程使用的最大物理内存,它通常等于进程使用的匿名页面、文件映射页面以及共享内存页面的大小总和
- VmRSS:进程使用的最大物理内存,它常常等于 VmHWM,计算公式为 VmRSS = RssAnon + RssFile + RssShmem
- RssAnon:进程使用的匿名页面,通过 get_mm_counter(mm, MM_ANONPAGES)获取。
- RssFile:进程使用的文件映射页面,通过 get_mm_counter(mm, MM_FILEPAGES)获取
- RssShmem:进程使用的共享内存页面,通过 get_mm_counter(mm, MM_SHMEMPAGES)获取。
- VmData:进程私有数据段的大小,它等于 mm->data_vm
- VmStk:进程用户栈的大小,它等于 mm->stack_vm
- VmExe:进程代码段的大小,通过内存描述符 mm 中的 start_code 和 end_code 两个成员获取
- VmLib:进程共享库的大小,通过内存描述符 mm 中的 exec_vm 和 VmExe 计算。
- VmPTE:进程页表大小,通过内存描述符 mm 中的 pgtables_byes 成员获取。
- VmSwap:进程使用的交换分区的大小,通过 get_mm_counter(mm, MM_SWAPENTS) 获取
- HugetlbPages:进程使用巨页的大小,通过内存描述符 mm 中的 hugetlb_usage 成员获取
为什么 S_swap 与 P_swap 不相等
proc/meminfo 节点中 SwapTotal 减去 SwapFree 等于系统中已经使用的交换内存大小,我们称之为 S_swap。另外,我们写一个小程序来遍历系统中所有的进程,并把进程中 /proc/PID/status 节点的 VmSwap 值都累加起来,我们把它称为 P_swap。为什么这两个值不相等?在 Linux 内核中通过 si_mapinfo() 函数来查看 S_swap 的值,由 nr_swap_pages 和 swap_info_struct 中的 flags 来统计,见 si_swapinfo() 函数。
1 | <mm/swapfile. c> |
当一个页面需要被交换到交换分区时,它需要在 kswapd 内核线程中经历活跃和不活跃 LRU 链表老化过程。一个页面被选为候选交换页面后,它需要调用 try_to_unmap_one() 函数来断开所有和用户进程地址空间映射的 PTE。 try_to_unmap_one() 函数的代码片段如下。
1 | static bool try_to_unmap_one() |
在 try_to_unmap_one() 函数中,对于匿名页面,会减小进程的 MM_ANONPAGES 计数,增加 MM_SWAPENTS 计数,这里通过 PageAnon() 来判断页面是否为匿名页面。 shmem(共享内存)比较特殊,它是基于 tmpfs 来实现的,本质上它是基于 RAM 的一个文件系统,因此它具有文件的属性,如有文件节点、页面高速缓存等。另外,它的内容不能随便丢弃。当系统内存短缺时会把 shmem 暂时写入交换分区以便腾出内存,因此它有部分匿名页面的属性。那它究竟属于匿名页面还是文件映射页面呢?
创建 shmem 页面时,使用 shmem_fault() 函数,它的 page->mmaping 字段指向 inode->i_mapping,因此我们没法通过 PageAnon() 来判定它是否是传统的匿名页面。在 try_to_unmap_one() 函数中, shmem 页面并没有被统计到进程的 MM_SWAPENTS 计数中,/proc/PID/status 节点中的 VmSwap 不包含被写入交换分区的 shmem 页面。
参考文献
《奔跑吧 Linux 内核》