Linux 内存管理(十一)调试信息

原图

/proc/meminfo

在 Linux 系统中查看内存信息最准确的方法是查看/proc/meminfo 节点信息,他包含当前时刻系统的所有的物理页面信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
vooxle@liushuai:~/workspace/blog$ cat /proc/meminfo
MemTotal: 32766544 kB
MemFree: 18617392 kB
MemAvailable: 31598252 kB
Buffers: 829808 kB
Cached: 11869936 kB
SwapCached: 0 kB
Active: 7991876 kB
Inactive: 4838568 kB
Active(anon): 125680 kB
Inactive(anon): 628 kB
Active(file): 7866196 kB
Inactive(file): 4837940 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 92274680 kB
SwapFree: 92274680 kB
Dirty: 248 kB
Writeback: 0 kB
AnonPages: 130960 kB
Mapped: 164660 kB
Shmem: 5180 kB
KReclaimable: 746412 kB
Slab: 1116388 kB
SReclaimable: 746412 kB
SUnreclaim: 369976 kB
KernelStack: 11840 kB
PageTables: 3800 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 108657952 kB
Committed_AS: 1769632 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 56828 kB
VmallocChunk: 0 kB
Percpu: 58368 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
CmaTotal: 0 kB
CmaFree: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 722824 kB
DirectMap2M: 16992256 kB
DirectMap1G: 17825792 kB

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
2
[0.000000] Memory: 929640K/1048576K available(23228K kernel code, 1090K rwdata, 3872K rodata, 4608K init, 503K bss, 53400K reserved, 65536K cma-reserved)
[8.910031] Freeing unused kernel memory: 4608K

从上述内核日志可以看到,在启动初始化时有 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
2
3
4
5
6
7
8
9
10
11
<mm/slab.c>

static struct page *kmem_getpages()
{
if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
mod_lruvec_page_state(page, NR_SLAB_RECLAIMABLE, nr_pages);
else
mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE, nr_pages);

...
}

而在创建 sab 描述符时若发现设置了 SLAB_RECLAIM_ACCOUNT,那么分配物理页面的行为就是可回收的,即设置 __GFP_RECLAIMABLE,表示这些页面是可以被 sab 机制的收割机回收的。

1
2
3
4
5
6
7
8
9
<mm/slab.c>

int __kmem_cache_create(struct kmem_cache *cachep, slab_flags_t flags)
{
...
if (flags & SLAB_RECLAIM_ACCOUNT)
cachp->allocflags |= __GFP_RECLAIMABLE;
...
}

因此,这些 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
2
3
4
5
6
7
8
9
10
11
<mm/page_alloc.c>

long si_mem_available(void)
{
...
reclaimable = global_node_page_state(NR_SLAB_RECLAIMABLE) +
global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);
available += reclaimable - min(reclaimable / 2, wmar_low);
...
return available;
}

为什么 Active(anon)+ nactive(anon)不等于 Anonpages

我们知道 Active(anon)表示 LRU 链表中的活跃匿名页面, Inactive(anon)表示不活跃匿名页面,这两个值相加,表示系统的 LRU 链表中的总匿名页面数量。而 AnonPages 表示和用户态进程地址空间建立映射关系。当一个匿名页面和进程地址空间建立映射关系时会调用 page_add_new_anon_mmap() 函数来新增一个 RMAP。

1
2
3
4
5
6
void page_add_new_anon_rmap ()
{
...
__mod_node_page_state(page_pgdat(page), NR_ANON_MAPPED, nr);
...
}

但是 shmem(基于 tmpfs 实现)使用的页面会被添加到系统的匿名页面的 LRU 链表中因此它会被计入 Active(anon)或者 Inactive(anon)之中。主要原因是 shmem 使用的页面基于 RAM 内存,它可以被写入交换分区里。在分配 shmem 页面时设置了 PG_SwapBacked 标志位,见 shmem_alloc_and_acct_page() 函数。

1
2
3
4
5
6
static struct page *shmem_alloc_and_acct_page ()
{
page =shmem_alloc_page(gfp, info, index);
__SetPageLocked(page);
__SetPageSwapbacd(page);
}

通过判断 PG_SwapBacked 标志位来定将页面加到名页面的 LRU 链表中还是文件映射的 LRU 链表中,见 page_is_file_cache() 函数。若没有设置 PG_SwapBacked 标志位,则页面是文件映射的页面,会被添加到文件映射的 LRU 链表中否则,被添加到名页面的 LRU 链表中。

1
2
3
4
static inline int page_is_file_cache(struct page * page)
{
return !PageSwapBacked(page);
}

另外, shmem 页面并没有计入 Anonpages 中,而是计入了 MM_SHMEMPAGES 类型的计数值(即 Shmem)中,见 do_shared_fault()->finish_fault()->alloc_set_pte() 函数。

1
2
3
4
5
6
7
vm_fault_t alloc_set_pte()
{
...
inc_mm_counter_fast(vma->vm_mm, mm_counter_file(page));
page_add_file_rmap(page, false);
...
}

在 page_add_file_rmap() 中会把这个页面计入 NR_FILE_MAPPED 计数值中。

1
2
3
4
5
6
void page_add_file_rmap()
{
...
mod_lruvec_page_state(page, NR_FILE_MAPPED, nr);
...
}

总之, 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
2
3
4
5
6
void page_add_file_rmap()
{
...
__mod_lruvec_page_state(page, NR_FILE_MAPPED, nr);
...
}

有一个特殊情况需要考虑,就是 shmem 页面。它会被计入 NR_FILE_MAPPED 计数值中但是它会设置 PG_SwapBacked 标志位,因此它会被计入匿名页面。 当创建一个 shmem 页面时会把它计入 NR_FILE_PAGES 和 NR_SHMEM 计数值中,见 shmem_add_to_page_cache() 函数。

1
2
3
4
5
6
7
static int shmem_add_to_page_cache()
{
...
__mod_node_page_state(page_pgdat(page), NR_FILE_PAGES, nr);
__mod_node_page_state(page_pgdat(page), NR_SHMEM, nr);
...
}

为什么 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
root@liushuai:/home/vooxle/workspace/blog# cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512

Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Node 0, zone DMA, type Unmovable 1 1 0 0 2 1 1 0 1 0 0
Node 0, zone DMA, type Movable 0 0 0 0 0 0 0 0 0 1 3
Node 0, zone DMA, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Unmovable 1 0 0 0 0 0 1 1 1 1 0
Node 0, zone DMA32, type Movable 9 5 5 5 4 6 6 5 5 3 459
Node 0, zone DMA32, type Reclaimable 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone DMA32, type Isolate 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Unmovable 1 499 477 515 204 39 28 5 0 0 0
Node 0, zone Normal, type Movable 8557 24298 10230 7542 2723 231 664 257 371 94 3641
Node 0, zone Normal, type Reclaimable 8 1 62 35 38 12 0 1 0 1 0
Node 0, zone Normal, type HighAtomic 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type CMA 0 0 0 0 0 0 0 0 0 0 0
Node 0, zone Normal, type Isolate 0 0 0 0 0 0 0 0 0 0 0

Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate
Node 0, zone DMA 1 7 0 0 0 0
Node 0, zone DMA32 2 1014 0 0 0 0
Node 0, zone Normal 394 14596 370 0 0 0

读者需要注意,页块的大小和普通巨页有关。当系统配置了 CONFIG_HUGETLB_PAGE 时,页块的 order 大小等于 HUGETLB_PAGE_ORDER,通常是 9:否则,页块的 order 大小是 10,如下图所示。

1
2
3
4
5
#ifdef CONFIG_HUGETLB_PAGE
#define pageblock_order HUGETLB_PAGE_ORDER
#else
#define pageblock_order (MAX_ORDER-1)
#endif

查看内存管理区的信息

/proc/zoneinfo 节点包含当前系统所有内存管理区的信息。/proc/zoneinfo 节点是显示如下几部分信息。

当前内存节点的内存统计信息

下面是/proc/zoneinfo 节点的第一部分信息。

1
2
3
4
5
6
7
8
9
10
root@liushuai:/home/vooxle/workspace/blog# cat /proc/zoneinfo
Node 0, zone DMA
per-node stats
nr_inactive_anon 157
nr_active_anon 31943
nr_inactive_file 1209487
nr_active_file 1968292
nr_unevictable 0
...
nr_kernel_misc_reclaimable 0

在第 2 行中,表示当前内存节点是第 0 个内存节点,当前内存管理区为 DMA。 在第 3 行中,表示下面是该内存节点的总体信息。如果当前内存管理区是内存节点的第一个内存管理区,那么会显示该内存节点的总信息。它通过 node_page_state() 函数来读取内存节点的数据结构 plist_data 中的 vm_stat 计数值。 上述信息是在 zoneinfo_show_print() 函数中输出的。

1
2
3
4
<mm/vmstat.c>

static void zoneinfo_show_print(struct seq_file * m, pg_data_t *pgdat,
struct zone *zone)

当前内存管理区的总信息下面继续看/proc/zoneinfo 节点的信息

下面继续看/proc/zoneinfo 节点的第二部分信息。

1
2
3
4
5
6
7
8
pages free     3971
min 8
low 11
high 14
spanned 4095
present 3999
managed 3971
protection: (0, 1776, 31892, 31892, 31892)
  • pages free: 表示这个内存管理区中空闲页面的数量
  • min:表示这个内存管理区中处于最低警戒水位的页面数量
  • low:表示这个内存管理区中处于低水位的页面数量
  • high:表示这个内存管理区中处于高水位的页面数量
  • spanned:表示这个内存管理区包含的页面数量
  • present:表示这个内存管理区里实际管理页面的数量
  • managed:表示这个内存管理区中被伙伴系统管理的页面数量
  • protection:表示这个内存管理区预留的内存

内存管理区详细的页面信息

接下来是内存管理区详细的页面信息/proc/zoneinfo 节点的第三部分信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nr_free_pages 3971
nr_zone_inactive_anon 0
nr_zone_active_anon 0
nr_zone_inactive_file 0
nr_zone_active_file 0
nr_zone_unevictable 0
nr_zone_write_pending 0
nr_mlock 0
nr_page_table_pages 0
nr_kernel_stack 0
nr_bounce 0
nr_zspages 0
nr_free_cma 0
numa_hit 0
numa_miss 0
numa_foreign 0
numa_interleave 0
numa_local 0
numa_other 0

上述是这个内存管理区详细的页面信息。它通过 zone_page_state() 函数来读取 zone 数据结构中的 vm_stat 计数值。

每个 CPU 内存分配器的信息

最后是每个 C 内存分配器的/proc/zonelnf。节点的第四部分信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pagesets
cpu: 0
count: 0
high: 0
batch: 1
vm stats threshold: 12
cpu: 1
count: 0
high: 0
batch: 1
...
vm stats threshold: 12
node_unreclaimable: 0
start_pfn: 1
  • pagesets:表示每个 CPU 内存分配器中每个 CPU 缓存的页面信息。
  • node_unreclaimable:表示页面回收失败的次数。
  • start_pfn:表示内存管理区的起始页帧号

查看与进程相关的内存信息

进程的 mm_struct 数据结构中有一个 rss_stat 成员,它用于记录进程的内存使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* When updating this, please also update struct resident_page_types[] in
* kernel/fork.c
*/
enum {
MM_FILEPAGES, /* Resident file mapping pages */
MM_ANONPAGES, /* Resident anonymous pages */
MM_SWAPENTS, /* Anonymous swap entries */
MM_SHMEMPAGES, /* Resident shared memory pages */
NR_MM_COUNTERS
};

#if USE_SPLIT_PTE_PTLOCKS && defined(CONFIG_MMU)
#define SPLIT_RSS_COUNTING
/* per-thread cached information, */
struct task_rss_stat {
int events; /* for synchronization threshold */
int count[NR_MM_COUNTERS];
};
#endif /* USE_SPLIT_PTE_PTLOCKS */

struct mm_rss_stat {
atomic_long_t count[NR_MM_COUNTERS];
};
...
struct mm_struct

进程的 mm_struct 数据结构会记录下面 4 种页面的数量

  • MM_FILEPAGES:进程使用的文件映射的页面数量。
  • MM_ANONPAGES:进程使用的匿名页面数量。
  • MM_SWAPENTS:进程使用的交换分区的匿名页面数量
  • MM_SHMEMPAGES:进程共享的内存的页面数量

增加和减小进程内存计数的接口函数有如下几个。

1
2
3
4
5
6
7
8
9
10
11
12
#获取 member 计数值
unsigned long get_mm_counter(struct mm_struct *mm, int member)
# 使 member 计数值增加 value
void add_mm_counter(struct mm_struct *mm, int member, long value)
#使 member 计数值增加 1
void inc_mm_counter(struct mm_struct *mm, int member)
#使 member 计数值减小 1
roid dec_mm_counter(struct mm_struct *mm, int member)
#当 page 不是匿名页面时,若 page 设置了 PageSwapbacked,那么返回 MM_SHMEMPAGES,否则返回 MM_EILEPAGES
int mm_counter_file(struct page *page)
#返回 page 对应的统计类型
int mm_counter(struct page *page)

proc 文件系统包含每个进程的相关信息,其中 /proc/PID/status 节点有不少和具体进程内存相关的信息。下面是 sshd 线程的状态信息,只截取了和内存相关的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
root@liushuai:/proc/2# cat status
Name: kthreadd
Umask: 0000
State: S (sleeping)
Tgid: 2
Ngid: 0
Pid: 2
PPid: 0
TracerPid: 0
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 64
Groups:
NStgid: 2
NSpid: 2
NSpgid: 0
NSsid: 0
Threads: 1
SigQ: 0/127633
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: ffffffffffffffff
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
Speculation_Store_Bypass: thread vulnerable
Cpus_allowed: ffff,ffffffff
Cpus_allowed_list: 0-47
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 3335
nonvoluntary_ctxt_switches: 0
  • 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
2
3
<mm/swapfile. c>

void si_swapinfo(struct sysinfo *val)

当一个页面需要被交换到交换分区时,它需要在 kswapd 内核线程中经历活跃和不活跃 LRU 链表老化过程。一个页面被选为候选交换页面后,它需要调用 try_to_unmap_one() 函数来断开所有和用户进程地址空间映射的 PTE。 try_to_unmap_one() 函数的代码片段如下。

1
2
3
4
5
6
7
8
9
10
11
static bool try_to_unmap_one()
{
...
if (PageAnon(page)) {
dec_mm_counter(mm, MM_ANONPAGES);
inc_mm_counter(mm, MM_SWAPENTS);
swp_pte = swp_entry_to_pte(entry);
set_pte_at(mm, address, pvmw.pte, swp_pte);
}
...
}

在 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 内核》