pcstat 如何获取内存页信息

1. 介绍

详见: https://github.com/tobert/pcstat pcstat 是 page cache stats 的缩写, 使用该工具可以帮助我们判断一个文件是否被 Linux cache 缓存, 或获取进程在 cache 中的缓存信息. 这在调优数据库或诊断其他 IO 密集型应用的场景下会有所帮助. 该工具的输出有多种格式, 选项包括:

Usage of ./pcstat:
  -bname=false: convert paths to basename to narrow the output
  -histo=false: print a simple histogram instead of raw data
  -json=false: return data in JSON format
  -nohdr=false: omit the header from terse & text output
  -pid=0: show all open maps for the given pid
  -pps=false: include the per-page status in JSON output
  -terse=false: show terse output

2. 如何判断文件是否被缓存.

我们通过在 mincore.go 源文件中增加打印vec(获取内存数据段中的结构信息)来进行说明, 如下所示为打印的vec信息, 表格中为最终的输出结果:

# ./pcstat /lib64/ld-2.12.so
([]uint8) (len=38 cap=38) {
 00000000  01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01  |................|
 00000010  01 01 01 01 01 01 01 01  01 01 01 01 01 01 01 01  |................|
 00000020  01 01 01 01 00 00                                 |......|
}
|-------------------+----------------+------------+-----------+---------|
| Name              | Size           | Pages      | Cached    | Percent |
|-------------------+----------------+------------+-----------+---------|
| /lib64/ld-2.12.so | 154624         | 38         | 36        | 094.737 |
|-------------------+----------------+------------+-----------+---------|

    */

spew.Dump(vec) 打印出该文件在内存中的页信息, 是一个包含 23 个元素的数组, 对应表格中 23 个页. 表格中的输出列做如下解释:

Name:    程序或文件的名字
Size:    程序或文件的大小, 单位字节
Pages:   Size大小的文件由 23 个页组成
Cached:  程序或文件在缓存中有 23 个页
Percent: 缓存率为 100%

继续追踪 vec 数组的来源信息, 通过 mincore.go 源文件, 可以看到其通过 系统函数 mmap 将文件映射到内存中, 在每次调用结束通过 unmap 函数取消映射. mmap 函数介绍见: http://linux.die.net/man/2/mmap

   mmap, munmap - map or unmap files or devices into memory 

相关代码如下:

   1  mmap, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_NONE, 

syscall.MAP_SHARED)
    ...
    // one byte per page, only LSB is used, remainder is reserved and clear
   2  vecsz := (size + int64(os.Getpagesize()) - 1) / int64(os.Getpagesize())
   3  vec := make([]byte, vecsz)
     ...
   4  mmap_ptr := uintptr(unsafe.Pointer(&mmap[0]))
      size_ptr := uintptr(size)
      vec_ptr := uintptr(unsafe.Pointer(&vec[0]))
     ...
   5  ret, _, err := syscall.Syscall(syscall.SYS_MINCORE, mmap_ptr, size_ptr, vec_ptr)
     ...
   6  defer syscall.Munmap(mmap)

   7     for i, b := range vec {
           if b%2 == 1 { 
               mc[i] = true
           } else {
              mc[i] = false
           }   
         }

      return mc, nil

下面详细说明 1 ~ 6 步骤中的逻辑:

(1) 调用系统函数 Mmap 将文件描述符为 f.Fd() 的且大小为 size 的文件, 以 PROT_NONE 的访问权限和 MAP_SHARED 标志的方式映射到内存中. PROT_NONE 和 MAP_SHARED 对应内存管理的结构体 struct vm_area_struct 的 pgprot_t 和 unsigned long 两项, 下图可以简单说明映射的过程,

详见: https://en.wikipedia.org/wiki/Virtual_memory mmap

Mmap 函数返回字节类型([]byte)的数组 mmap, prot类型和flag类型如下:

PROT_EXEC
    Pages may be executed. 
PROT_READ
    Pages may be read. 
PROT_WRITE
    Pages may be written. 
PROT_NONE
    Pages may not be accessed.

MAP_SHARED
    Share this mapping. Updates to the mapping are visible to other processes that map this 
file, and are carried through to the underlying file. The file may not actually be updated 
until msync(2) or munmap() is called. 

MAP_PRIVATE
    Create a private copy-on-write mapping. Updates to the mapping are not visible to 
other processes mapping the same file, and are not carried through to the underlying file. 
It is unspecified whether changes made to the file after the mmap() call are visible in 
the mapped region.

(2) 这里的 - 1 保证了 size 大小的文件由 0 个或多个页组成, 想想如果没有 - 1, 在 size 小于系统页大小的时候 vecsz 的值将为 0, 实际上在 0 < size < page_size 的时候至少需要 1 个页, vecsz 即是文件的页数.

(3) 创建的类型是 byte, 个数为 vecsz 的数组, 用来保存内存页中的信息.

(4) uintptr 是整数类型用来保存指针中足够大的数, unsafe.Pointer 可以将任何类型的值转为指针类型, mmap_ptr 即是 mmap数组第一个元素在内存中的指针地址, size_ptr 在文件全部缓存的情况下值为 size.对应内存中该文件的页信息就保存在 mmap_ptr ~ mmap_ptr + size 之间. vec_ptr 指向 vec 数组的开头的指针.(注: golang 中的 make 只适用于 map, slice 和 channel, 并不返回指针, 只返回初始化的值, 该值关联到声明的类型).

(5) 先来看看linux 中 mincore(2) 函数原型相关的信息:

mincore(2): int mincore(void *addr, size_t length, unsigned char *vec);
0 on success, takes the pointer to the mmap, a size, which is the size
that came from f.Stat().

mincore() returns a vector that indicates whether pages of the
calling process's virtual memory are resident in core (RAM), one character
per page.

详见: http://man7.org/linux/man-pages/man2/mincore.2.html http://www.freebsd.org/cgi/man.cgi?query=mincore

SYS_MINCORE 将 vec_ptr 指针指向 mmap(mmap为字节数组 []byte)的区域, 即 mmap_ptr ~ mmap_ptr + size_ptr 的内存区域, 而且 vec_ptr 是关联 vec数组(vec_ptr指针指向 vec数组的首元素地址), 数组 vec 中的元素是一个 8 位标识信息, 如上述的 spew.Dump输出,每个元素为1字节. 如果为 0 表示该页不在内存中, 如果是下面的一个或多个标识组成则表示该页存在于内存中:

 MINCORE_INCORE	               Page is in core (resident).

 MINCORE_REFERENCED	       Page has	been referenced	by us.

 MINCORE_MODIFIED	       Page has	been modified by us.

 MINCORE_REFERENCED_OTHER      Page has	been referenced.

 MINCORE_MODIFIED_OTHER        Page has	been modified.

 MINCORE_SUPER	               Page is part of a "super" page. (only i386 & amd64)

(6) 在函数结束前取消mmap映射.

(7) 遍历 vec 数组, b%2 为 1 表示至少有一个表示存在,则该页在内存中.文件映射到内存中调用了系统相关的函数进行处理, 由于笔者系统知识有限, 没有清晰的阐明进程和内存管理相关的内容.

3. 如何获取进程 id 的信息.

pcstat工具支持读取 pid 进程来获取相关的内存页信息, 从 main.go 源文件的 getPidMaps 函数可知, pcstat 通过统计 /proc/(pid)/maps 中的信息来实现获取进程 id 内存页信息. 值得一提的是, 在源文件 mnt_ns_linux.go 中, 如果进程和父进程不在同一个命名空间(mount namespace)中(比如系统中运行的一个 docker 进程), 则需要通过 syscall.Syscall(SYS_SETNS, uintptr(uint(fd)), uintptr(CLONE_NEWNS), 0) 设置标志 CLONE_NEWNS 标志位, 使进程创建一个新的 mount namespace, 每个进程都存在于一个 mount Namespace里面,mount Namespace为进程提供了一个文件层次视图, 如果不设定这个flag, 子进程和父进程将共享一个mount Namespace,其后子进程调用mount或umount将会影响到所有该Namespace内的进程。如果子进程在一个独立的mount Namespace里面,就可以调用mount或umount建立一份新的文件层次视图。该flag配合pivot_root系统调用,可以为进程创建一个独立的目录空间.

详见: http://www.man7.org/linux/man-pages/man2/clone.2.html

/*such as docker process:
  [root@cz ~]# ls /proc/2300/ns/
  ipc  mnt  net  pid  uts
*/

func SwitchMountNs(pid int) {
    myns := getMountNs(os.Getpid())
    pidns := getMountNs(pid)

    if myns != pidns {
        setns(pidns)
    }   
}

...

func setns(fd int) error {
    ret, _, err := syscall.Syscall(SYS_SETNS, uintptr(uint(fd)), uintptr(CLONE_NEWNS), 0)
    if ret != 0 {
        return fmt.Errorf("syscall SYS_SETNS failed: %v", err)
    }

    return nil
}

4. 示例

获取 MySQL 进程信息, 以默认格式输出, 读者也可以指定其它格式输出

[root@cz ~]# pcstat -pid `pidof mysqld`
|-----------------------------------------------------------------------------------------+----------------+------------+-----------+---------|
| Name                                                                                    | Size           | Pages      | Cached    | Percent |
|-----------------------------------------------------------------------------------------+----------------+------------+-----------+---------|
| /opt/Percona-Server-5.5.23-rel25.3-240.Linux.x86_64/bin/mysqld                          | 48323893       | 11798      | 370       | 003.136 |
| /lib64/libnss_files-2.12.so                                                             | 65928          | 17         | 17        | 100.000 |
| /lib64/libdl-2.12.so                                                                    | 19536          | 5          | 5         | 100.000 |
| /lib64/libcrypt-2.12.so                                                                 | 40400          | 10         | 10        | 100.000 |
| /lib64/libpthread-2.12.so                                                               | 142640         | 35         | 32        | 091.429 |
| /opt/Percona-Server-5.5.23-rel25.3-240.Linux.x86_64/lib/mysql/plugin/libaudit_plugin.so | 1027315        | 251        | 0         | 000.000 |
| /lib64/libgcc_s-4.4.7-20120601.so.1                                                     | 90880          | 23         | 23        | 100.000 |
| /usr/lib64/libstdc++.so.6.0.13                                                          | 987096         | 241        | 136       | 056.432 |
| /usr/lib64/libunwind.so.8.0.1                                                           | 41744          | 11         | 0         | 000.000 |
| /lib64/libc-2.12.so                                                                     | 1921176        | 470        | 393       | 083.617 |
| /usr/lib64/libtcmalloc.so.4.1.0                                                         | 296880         | 73         | 0         | 000.000 |
| /lib64/ld-2.12.so                                                                       | 154624         | 38         | 36        | 094.737 |
| /lib64/librt-2.12.so                                                                    | 43880          | 11         | 11        | 100.000 |
| /opt/Percona-Server-5.5.23-rel25.3-240.Linux.x86_64/lib/mysql/plugin/auth_socket.so     | 12468          | 4          | 0         | 000.000 |
| /lib64/libfreebl3.so                                                                    | 383504         | 94         | 76        | 080.851 |
| /lib64/libm-2.12.so                                                                     | 596272         | 146        | 113       | 077.397 |
| /lib64/libaio.so.1.0.1                                                                  | 3944           | 1          | 1         | 100.000 |
|-----------------------------------------------------------------------------------------+----------------+------------+-----------+---------|

查看一个 InnoDB log 文件的缓存情况:

|-------------+----------------+------------+-----------+---------|
| Name        | Size           | Pages      | Cached    | Percent |
|-------------+----------------+------------+-----------+---------|
| ib_logfile0 | 5242880        | 1280       | 2         | 000.156 |
| ib_logfile1 | 5242880        | 1280       | 0         | 000.000 |
|-------------+----------------+------------+-----------+---------|