Linux命令之谁在大量使用系统资源(内存篇)

一、系统内存使用情况 -系统宏观

1. free、sar统计

#Mem: buff/cache --> Buffers + Cached + Slab
# free -g
              total        used        free      shared  buff/cache   available
Mem:            251          24           1           4         225         222
Swap:             0           0           0

# free -m
              total        used        free      shared  buff/cache   available
Mem:         257437       24703        1379        4099      231354      227769
Swap:             0           0           0

# sar -r 1

02:43:35 PM kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
02:43:36 PM   1387084 262228620     99.47      1148 230242600  32319288     12.26 105564996 147804576      3980
02:43:37 PM   1386580 262229124     99.47      1148 230243532  32319288     12.26 105565364 147805308       216
02:43:38 PM   1386580 262229124     99.47      1148 230243532  32319288     12.26 105565364 147805308       216
02:43:39 PM   1381852 262233852     99.48      1148 230246148  32319288     12.26 105565560 147806944      2312
02:43:40 PM   1367028 262248676     99.48      1148 230261344  32319288     12.26 105566696 147821916     18876
02:43:41 PM   1366880 262248824     99.48      1148 230261344  32317020     12.26 105566192 147821916     19628
02:43:42 PM   1367408 262248296     99.48      1148 230262572  32317020     12.26 105565996 147822864       332
02:43:43 PM   1367408 262248296     99.48      1148 230262572  32317020     12.26 105565996 147822864       332

02:43:36 PM数据为例:

kbmemfree 可用的可用内存量(以kbs为单位):1.38G

kbmemused 已用内存量(以kbs为单位):262.2G

%memused 已用内存的百分比 : 99.47%   ( kbmemfree/kbmemused )

kbbuffers 内核用作写缓冲区的内存量(以kbs为单位):  1.14M

kbcached 内核用于缓存数据的内存量(以kbs为单位):230G

kbcommit 当前工作负载所需的内存量(以kbs为单位)。这是对需要多少 RAM/交换区才能保证永远不会出现内存不足的估计 :32.3G

%commit 当前工作负载所需的内存相对于内存总量(RAM+交换)的百分比。该数字可能大于 100%,因为内核通常会过度使用内存 : 12.26%  (%commit / (kbmemfree + kbmemused))

kbactive 当前活跃内存量。除非万不得已,这部分内存才会被回收 : 105.5G

kbinact 当前非活跃内存总量,当内存不足时,这部分内存最容易被内核回收 : 147.8G

kbdirty 脏页大小,脏页指的是暂存于内存还没来得及持久化到硬盘的数据。可以使用sync刷入硬盘: 3.9M

分析为:系统有大量内存用于kbcached,即内核中缓存大量数据。其中kbactive活跃内存量 105.5G,kbinact非活跃内存总量 147.8G。

2. /proc/meminfo统计

# cat /proc/meminfo | more
MemTotal:       263615704 kB  #总可用内存
MemFree:          999420 kB   #表示系统尚未使用的物理内存。
MemAvailable:   233195576 kB  #在不交换的情况下,估计有多少内存可用于启动新的应用程序。
Buffers:            1148 kB   #块设备所占用的缓存页 Memory in buffer cache, so relatively temporary storage for raw disk blocks. This shouldn't get very large. 包括:直接读写块设备,以及文件系统元数据(metadata)比如superblock使用的缓存页。
Cached:         230607916 kB  #Page cache中的内存(磁盘文件缓存和共享内存)。Memory in the pagecache (Diskcache and Shared Memory)  
#用户进程的内存页分为两种:file-backed pages(与文件对应的内存页),和anonymous pages(匿名页),比如进程的代码、映射的文件都是file-backed,而进程的堆、栈都是不与文件相对应的、就属于匿名页。  
SwapCached:            0 kB   #内存与交换区设备的"中间层",用于匿名页换入换出时的缓存。匿名页在放入到交换区之前,会在SwapCached中建立一个文件页,但该缓存很快就被刷入到交换区设备,然后从SwapCached中删除;另外,当匿名页从交换区设备换入内存时,会放入到SwapCached中。
Active:         106144160 kB  #最近使用较多且通常不会换出或回收的内存,包含active anon和active file
Inactive:       147591380 kB  #拥有的记忆不最近使用过,可以换出或回收,包含inactive anon和inactive file。
Active(anon):   26732612 kB   #最近使用较多且通常不会被换出的匿名内存。mapped pages,也称为匿名页,为与文件无关的内存页(它们用于临时存储进程的运行时数据,如动态分配的内存、函数调用栈等。通常通过系统调用(如mmap()或sbrk())或C库函数(如malloc())进行动态分配。如果系统内存不足,匿名页可能会被交换(换出)到交换分区(Swap)中,以腾出物理内存供其他进程使用。当进程再次访问被交换的匿名页时,它将被交换回物理内存。当进程不再需要匿名页时,它可以通过相应的系统调用(如munmap()或free())释放这些页。内核将回收这些页的物理内存,并将其标记为可再分配。)
Inactive(anon):   592480 kB   #匿名页不最近使用过,可以换出
Active(file):   79411548 kB   #最近使用的页面缓存内存,通常在需要时才回收。file pages为与文件关联的内存页(比如程序文件,数据文件对应的内存页)。
Inactive(file): 146998900 kB  #可以回收而不会对性能产生巨大影响的页面缓存内存
Unevictable:           0 kB   #由于各种原因,无法检索的页面不能被换出
Mlocked:               0 kB   #使用锁定到内存的页面mlock()系统调用。Mlocked页面也是不可检索的
SwapTotal:             0 kB   #可用的总交换空间
SwapFree:              0 kB   #剩余的可用交换空间
Dirty:              1188 kB   #等待写回磁盘的内存(脏文件页),这些页在回收前,需要先刷入到磁盘中。需要注意的是,匿名页在swap-out前也会被置为脏页。
Writeback:             0 kB   #正在执行回写操作的页。
AnonPages:      23126864 kB   # Non-file backed pages mapped into userspace page tables,用户进程正在使用的匿名页
Mapped:           171868 kB   # Files which have been mmaped, such as libraries 用户进程正在使用的文件页,如对应的链接库so、可执行程序、mmap的文件等。
Shmem:           4198356 kB   #使用的共享内存总量
Slab:            7014628 kB   #内核数据结构缓存
SReclaimable:    6669888 kB   #slab可以被回收的内核内存,包括目录项(dentry) 和索引节点( inode )的缓存等,用 SReclaimable 记录
SUnreclaim:       344740 kB  #slab不可回收部分,用 SUnreclaim 记录。
KernelStack:       19184 kB  #内核堆栈使用的内存,这是不可回收的。
PageTables:        61624 kB  #专用于最低级别页表的内存量。如果许多进程都连接到同一个共享内存段,这个值可能会增加到很高。
NFS_Unstable:          0 kB  #NFS页面已发送到服务器,但尚未提交到存储
Bounce:                0 kB  #用于块设备的存储器bounce buffers
WritebackTmp:          0 kB  #FUSE用于临时写回缓冲区的内存
CommitLimit:    131807852 kB #基于过量使用比率(vm.overcommit_ratio),这是系统上当前可分配的内存总量。
Committed_AS:   32319376 kB #系统当前分配的内存量。提交的内存是由进程分配的所有内存的总和,即使它还没有被它们“使用”。
VmallocTotal:   34359738367 kB #vmalloc存储区的总大小
VmallocUsed:      744736 kB    #使用的vmalloc区域的数量
VmallocChunk:   34224736252 kB #vmalloc区域中空闲的最大连续块
HardwareCorrupted:     0 kB  #内核识别为损坏/不工作的RAM数量
AnonHugePages:  21659648 kB  #映射到用户空间页表的非文件备份的大页面
CmaTotal:              0 kB  #为当前内核保留的连续内存区域的总量。
CmaFree:               0 kB  #当前内核可以自由使用的连续内存区域。
HugePages_Total:       0   #内核分配的大页面数量(用vm.nr_hugepages)
HugePages_Free:        0   #进程未分配的大页面数
HugePages_Rsvd:        0   #已承诺从池中进行分配但尚未进行分配的大型页面的数量。
HugePages_Surp:        0   #内存中大于中的值的大页数vm.nr_hugepages剩余页面的最大数量由以下因素控制vm.nr_overcommit_hugepages.
Hugepagesize:       2048 kB #巨页的大小hugepage(在基于Intel的系统上通常为2MB)
DirectMap4k:      356416 kB #映射到4k大小页面的内核空间的内存量。
DirectMap2M:    15015936 kB #用2MB大小的页面映射到内核空间的内存量。
DirectMap1G:    254803968 kB #用1GB大小的页面映射到内核空间的内存量。

Buffers 表示块设备(block device)所占用的缓存页,包括:直接读写块设备、以及文件系统元数据(metadata)比如SuperBlock所使用的缓存页。

它与Cached 的区别在于,Cached表示普通文件所占用的缓存页。

为了提升对文件的读写效率,Linux 内核会以页大小(4KB)为单位,将文件划分为多数据块。当用户对文件中的某个数据块进行读写操作时,内核首先会申请一个内存页(称为page cache页缓存)与文件中的数据块进行绑定。

当用户对文件进行读写时,实际上是对文件的页缓存进行读写。所以对文件进行读写操作时,会分以下两种情况进行处理:

        1.当从文件中读取数据时,如果要读取的数据所在的页缓存已经存在,那么就直接把页缓存的数据拷贝给用户即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把页缓存的数据拷贝给用户。
        2. 当向文件中写入数据时,如果要写入的数据所在的页缓存已经存在,那么直接把新数据写入到页缓存即可。否则,内核首先会申请一个空闲的内存页(页缓存),然后从文件中读取数据到页缓存,并且把新数据写入到页缓存中。对于被修改的页缓存,内核会定时把这些页缓存刷新到文件中。

为什么Active(file)+Inactive(file)不等于Mapped
因为LRU Active(file)和Inactive(file)中不仅包含mapped页面,还包含unmapped页面;
Mapped中包含”Shmem”(即shared memory & tmpfs),这部分内存被计入了LRU Active(anon)或Inactive(anon)、而不在Active(file)和Inactive(file)中。
为什么【Active(file)+Inactive(file)】不等于 Cached?
因为”Shmem”(即shared memory & tmpfs)包含在Cached中,而不在Active(file)和Inactive(file)中;
Active(file)和Inactive(file)还包含Buffers。
如果不考虑mlock的话,一个更符合逻辑的等式是:
【Active(file) + Inactive(file) + Shmem】== 【Cached + Buffers】
如果有mlock的话,等式应该如下(mlock包括file和anon两部分,/proc/meminfo中并未分开统计,下面的mlock_file只是用来表意,实际并没有这个统计值):
Active(file) + Inactive(file) + Shmem + mlock_file】== 【Cached + Buffers】

3. smem统计

# smem -h
Usage: smem [options]

Options:
  -h, --help            show this help message and exit
  -H, --no-header       disable header line
  -c COLUMNS, --columns=COLUMNS
                        columns to show
  -t, --totals          show totals
  -R REALMEM, --realmem=REALMEM
                        amount of physical RAM
  -K KERNEL, --kernel=KERNEL
                        path to kernel image
  -m, --mappings        show mappings 
  -u, --users           show users
  -w, --system          show whole system
  -P PROCESSFILTER, --processfilter=PROCESSFILTER
                        process filter regex
  -M MAPFILTER, --mapfilter=MAPFILTER
                        map filter regex
  -U USERFILTER, --userfilter=USERFILTER
                        user filter regex
  -n, --numeric         numeric output
  -s SORT, --sort=SORT  field to sort on
  -r, --reverse         reverse sort
  -p, --percent         show percentage
  -k, --abbreviate      show unit suffixes
  --pie=PIE             show pie graph
  --bar=BAR             show bar graph
  -S SOURCE, --source=SOURCE
                        /proc data source

在Linux系统中,进程的内存使用情况通常可以通过以下几个指标来衡量:VSS、RSS、PSS、USS。它们分别表示不同的内存使用概念:

    VSS(Virtual Set Size):表示进程的虚拟内存大小,包括代码段、数据段、堆、共享库、映射文件等。VSS的值可能会很大,因为它包含了进程能够访问的所有虚拟地址空间。
    RSS(Resident Set Size):表示进程的常驻内存大小,即实际驻留在物理内存中的部分。RSS是进程当前使用的物理内存量,不包括共享库和映射文件等
    PSS(Proportional Set Size):表示进程的比例内存大小,是一个比较复杂的概念。当多个进程共享内存时,PSS会按比例分配共享内存的大小给各个进程,计算得出每个进程的PSS值
    USS(Unique Set Size):表示进程的独立内存大小,即仅属于该进程独自使用的内存部分不包括共享的内存

#查看系统mappings文件信息
# smem -m 
Map                                       PIDs   AVGPSS      PSS
... ...
/dev/shm/MCS-shm-00010002                    1     4696     4696
/usr/lib64/libgio-2.0.so.0.5600.1          110       43     4736
/dev/shm/MCS-shm-00010003                    2     2442     4884
[stack]                                    166       29     4928
/usr/lib64/libc-2.17.so                    166       30     5042
/usr/lib64/dri/swrast_dri.so                 4     1359     5436
/usr/lib64/libgtk-3.so.0.2200.30            40      156     6245
/SYSV00000000                                6     1824    10944
/usr/lib64/libwebkit2gtk-4.0.so.37.44.4      2     5486    10972
/usr/sbin/mariadbd                           1    15740    15740
/usr/lib64/libLLVM-7-rhel.so                 4     6251    25004
[heap]                                     163     2477   403828
<anonymous>                                166     3650   606032


## smem -u | more
User     Count     Swap      USS      PSS      RSS
... ...
rpc          1        0      556      561     1084
mysql        8        0   323868   326582   348360
root       152        0   956544  1012349  1624604

#  smem -w
Area                           Used      Cache   Noncache
firmware/hardware                 0          0          0
kernel image                      0          0          0
kernel dynamic memory       2376148    2212396     163752
userspace memory            1353380     173452    1179928
free memory                  150268     150268          0

#按rss排序进程的占用情况
# smem -tnkp -s rss -r  
  PID User     Command                         Swap      USS      PSS      RSS
 4609 27       /usr/sbin/mariadbd                 0   229.6M   230.2M   235.5M
29623 0        /usr/bin/gnome-shell               0   142.9M   152.5M   174.4M
 2872 0        /usr/bin/gnome-shell               0   111.7M   119.8M   134.8M
... ...

支持-s排序参数 (-s为升序 -r为降序):
known fields:
command  process command line
maps     total number of mappings
name     name of process
pid      process ID
pss      proportional set size (including sharing)
rss      resident set size (ignoring sharing)
swap     amount of swap space consumed (ignoring sharing)
user     owner of process
uss      unique set size
vss      virtual set size (total virtual memory mapped)

二、系统内存分配情况-系统宏观

# sar -R 1
03:01:26 PM   frmpg/s   bufpg/s   campg/s
03:01:27 PM   -310.00      0.00    195.00
03:01:28 PM    169.31      0.00    119.80
03:01:29 PM   -276.00      0.00    260.00
03:01:30 PM   -167.00      0.00    175.00
03:01:31 PM      2.00      0.00     45.00
03:01:32 PM   -295.00      0.00    130.00
03:01:33 PM     96.00      0.00    106.00
03:01:34 PM     -2.00      0.00      0.00
03:01:35 PM   -152.48      0.00    128.71
03:01:36 PM    -20.00      0.00     34.00
03:01:37 PM   2857.00      0.00  -4093.00
03:01:38 PM    287.00      0.00    210.00
03:01:39 PM   -118.00      0.00      0.00
03:01:40 PM    -44.00      0.00    172.00
03:01:41 PM   -420.79      0.00     68.32
03:01:42 PM    441.00      0.00     94.00
03:01:43 PM    -49.00      0.00      0.00
03:01:44 PM     73.00      0.00    128.00
03:01:45 PM      0.00      0.00      0.00
03:01:46 PM     96.00      0.00     43.00
03:01:47 PM   -343.56      0.00     64.36
03:01:48 PM    -48.00      0.00    168.00
03:01:49 PM   -156.00      0.00    130.00
03:01:50 PM    150.00      0.00     86.00
03:01:51 PM  -4673.00      0.00   4377.00
03:01:52 PM    499.01      0.00   -315.84
03:01:53 PM   -149.00      0.00    194.00
03:01:54 PM    150.00      0.00    108.00
03:01:55 PM    -15.00      0.00      0.00
03:01:56 PM   -177.00      0.00    125.00

frmpg/s:系统每秒释放的内存页数。负值表示系统分配的页数。根据机器架构,页面的大小为 4 kB 或 8 kB。

bufpg/s:系统每秒用作缓冲区的增加内存页数。负值意味着系统用作缓冲区的页面减少。

campg/s:系统每秒缓存的增加内存页数。负值意味着缓存中的页面减少。

分析:系统在持续分配少量内存页,用于缓存页。同时也会出现间隔一段时间的集中缓存页面回收。

三、系统中进程占用物理内存情况 -系统微观

查看进程cache占用:
#cd /proc
# find -maxdepth 2 -iname status -exec grep -i -e name  -e VmRSS {} \; -exec echo "---" \;| grep -B 1 VmRSS
## find -maxdepth 2 -iname status -exec grep -i -e name -e VmRSS -e Pid {} \; -exec echo "---" \;| grep -B 4 'VmRSS'


#可根据实际需要去进行匹配:
----------------------------------------------------
VmPeak: 表示进程所占用最大虚拟内存大小
VmSize: 表示进程当前虚拟内存大小
VmLck: 表示被锁定的内存大小
VmHWM: 表示进程所占用物理内存的峰值
VmRSS: 表示进程当前占用物理内存的大小(与procrank中的RSS)
VmData: 表示进程数据段的大小
VmStk: 表示进程堆栈段的大小
VmExe: 表示进程代码的大小
VmLib: 表示进程所使用共享库的大小
----------------------------------------------------
输出:
Name:   systemd
Pid:    1
PPid:   0
TracerPid:      0
VmPeak:   256444 kB
--
Name:   systemd-journal
Pid:    1282
PPid:   1
TracerPid:      0
VmPeak:   162556 kB

....

VmRSS:      3176 kB
VmRSS:      1764 kB
VmRSS:      1516 kB
VmRSS:      2900 kB
VmRSS:     11972 kB
VmRSS:      2500 kB
VmRSS:      2576 kB
VmRSS:      1592 kB
VmRSS:      1284 kB
VmRSS:       792 kB
VmRSS:     19376 kB
VmRSS:      4600 kB
VmRSS:    142284 kB
VmRSS:      1884 kB
VmRSS:      2152 kB
VmRSS:      4244 kB
VmRSS:      4292 kB
VmRSS:      5468 kB
VmRSS:      2256 kB
VmRSS:      2884 kB
VmRSS:      2520 kB
VmRSS:      1588 kB
VmRSS:      1272 kB
VmRSS:       856 kB
VmRSS:      1452 kB
VmRSS:       960 kB
VmRSS:     82872 kB
VmRSS:    836748 kB
VmRSS:  21845784 kB
VmRSS:     41488 kB
VmRSS:      5984 kB
VmRSS:      2452 kB
VmRSS:      2124 kB
VmRSS:      4624 kB
VmRSS:      2340 kB
VmRSS:      2240 kB
VmRSS:     16308 kB
VmRSS:      4572 kB
VmRSS:      5468 kB
VmRSS:      2256 kB
VmRSS:      2888 kB
VmRSS:    262116 kB

分析:可以根据pid查询到占用内存较大的进程,再进行进一步分析。

四、系统中单个进程内存分析-进程宏观

0.linux进程内存分布

    BSS段:在采用段式内存管理的架构中,BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
    DATA数据段:在采用段式内存管理的架构中,数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
    TEXT代码段:在采用段式内存管理的架构中,代码段(text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

    其中.text即为代码段,为只读。.bss段包含程序中未初始化的全局变量和static变量。
    堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
    栈 (stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“()”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区

    当程序在执行时动态分配空间(C中的malloc函数),所分配的空间就属于heap。
    stack段存放函数内部的变量、参数和返回地址,其在函数被调用时自动分配,访问方式就是标准栈中的LIFO方式。(因为函数的局部变量存放在此,因此其访问方式应该是栈指针加偏移的方式,否则若通过push、pop操作来访问相当麻烦)。

    在采用段式内存管理的架构中(比如intel的80x86系统),BSS 段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时 BSS 段部分将会清零。BSS 段属于静态内存分配,即程序一开始就将其清零了。

    比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。

    text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而BSS段不在可执行文件中,由系统初始化。

栈内存和堆内存在不同方面的主要区别:

方面对比

栈内存

堆内存

尺寸管理

固定大小,在程序开始时确定

灵活的大小,可以在程序的生命周期中改变

速度

更快,只需要调整一个参考

速度较慢,涉及定位合适的块和管理碎片

储存目的

控制信息、局部变量、函数参数

具有动态生命周期的对象和数据结构

数据可访问性

仅在活动函数调用期间可访问

在手动释放或程序结束之前均可访问

内存管理

由系统自动管理

由程序员手动管理

1.smaps统计

        /proc/PID/smaps 文件是基于 /proc/PID/maps 的扩展,展示了一个进程的内存消耗,比同一目录下的maps文件更为详细。

每一个VMA(虚拟内存区域,即一个 vm_area_struct 结构指向的内存区域)都有如下的一系列数据

00400000-00401000 r-xp 00000000 08:11 2080375453         /opt/jdk1.8.0_191/bin/java
Size:                  4 kB
Rss:                   4 kB
Pss:                   2 kB
Shared_Clean:          4 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            4 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
ProtectionKey:         0
VmFlags: rd ex mr mp me dw sd

第一行基础信息

在讲解字段含义之前,我们必须知道什么匿名映射
在linux内存管理中进程用户态内存分布中提到过,映射分为文件映射匿名映射

文件映射file-backed pages 就是磁盘中的数据通过文件系统映射到内存再通过文件映射映射到虚拟空间,这样,用户就可以在用户空间通过 open ,read, write 等函数区操作文件内容。至于实际的代码,open,read,write,close,mmap... 操作的虚拟地址都属于文件映射。

所有page cache里的页面(Cached)都是file-backed pages,不是Anonymous Pages。

shared memory 不属于 AnonPages,而是属于Cached,因为shared memory基于tmpfs,所以被视为file-backed、在page cache里。

匿名映射 Anonymous Pages就是用户空间需要分配一定的物理内存来存储数据,这部分内存不属于任何文件,内核就使用匿名映射将内存中的某段物理地址与用户空间一一映射,这样用户就可用直接操作虚拟地址来范围这段物理内存。比如使用malloc申请内存。

mmap private anonymous pages属于AnonPages(Anonymous Pages),而mmap shared anonymous pages属于Cached(file-backed pages),因为shared anonymous mmap也是基于tmpfs的。
Anonymous Pages是与用户进程共存的,一旦进程退出,则Anonymous pages也释放,不像page cache即使文件与进程不关联了还可以缓存。

AnonPages统计值中包含了Transparent HugePages (THP)对应的 AnonHugePages 。

00400000-00401000 00000000 是该虚拟内存段的开始和结束位置

r-xp 内存段的权限,分别是可读、可写、可运行、私有或共享,最后一位p代表私有,s代表共享

00000000 该虚拟内存段起始地址在对应的映射文件中以页为单位的偏移量,对匿名映射,它等于0或者vm_start/PAGE_SIZE

08:11 文件的主设备号和次设备号。对匿名映射来说,因为没有文件在磁盘上,所以没有设备号,始终为00:00。对有名映射来说,是映射的文件所在设备的设备号。

2080375453 被映射到虚拟内存的文件的索引节点号,通过该节点可以找到对应的文件,对匿名映射来说,因为没有文件在磁盘上,所以没有节点号,始终为00:00。

/opt/jdk1.8.0_191/bin/java  被映射到虚拟内存的文件名称。后面带(deleted)的是内存数据,可以被销毁。对有名来说,是映射的文件名。对匿名映射来说,是此段虚拟内存在进程中的角色。[stack]表示在进程中作为栈使用,[heap]表示堆。其余情况则无显示。

第二行至末尾详细信息

Size虚拟内存空间大小。但是这个内存值不一定是物理内存实际分配的大小,因为在用户态上,虚拟内存总是延迟分配的。这个值计算也非常简单,就是该VMA的开始位置减结束位置延迟分配就是当进程申请内存的时候,Linux会给他先分配页,但是并不会区建立页与页框的映射关系,意思就是说并不会分配物理内存,而当真正使用的时候,就会产生一个缺页异常,硬件跳转page fault处理程序执行,在其中分配物理内存,然后修改页表(创建页表项)。异常处理完毕,返回程序用户态,继续执行。

Rss:是实际分配的内存,这部分物理内存已经分配,不需要缺页中断就可以使用的。
这里有一个公式计算Rss:
Rss=Shared_Clean+Shared_Dirty+Private_Clean+Private_Dirty

share/private:该页面是共享还是私有。
dirty/clean:该页面是否被修改过,如果修改过(dirty),在页面被淘汰的时候,就会把该脏页面回写到交换分区(换出,swap out)。有一个标志位用于表示页面是否dirty。

share/private_dirty/clean 计算逻辑:
查看该page的引用数,如果引用>1,则归为shared,如果是1,则归为private,同时也查看该page的flag,是否标记为_PAGE_DIRTY,如果不是,则认为干净的。

Pss(proportional set size):是平摊计算后的实际物理使用内存(有些内存会和其他进程共享,例如mmap进来的)。实际上包含下面private_clean+private_dirty,和按比例均分的shared_clean、shared_dirty。

举个计算Pss的例子:
如果进程A有x个private_clean页面,有y个private_dirty页面,有z个shared_clean仅和进程B共享,有h个shared_dirty页面和进程B、C共享。那么进程A的Pss为:
x + y + z/2 + h/3

Referenced:当前页面被标记为已引用或者包含匿名映射(The amount of memory currently marked as referenced or a mapping associated with a file may contain anonymous pages)。

在页面替换算法里,当某个页面被访问后,Referenced标志被设置,如果该标志设置了,就 不能将该页移出。

Anonymous:匿名映射的物理内存,这部分内存不来自于文件的内存大小。

ShmemPmdMapped:PMD页面已经被映射的共享(shmem / tmpfs)内存量。在官方文档中,这样解释:"ShmemPmdMapped" shows the ammount of shared (shmem/tmpfs) memory backed by huge pages.

Shared/Private_Hugetlb:由hugetlbfs页面支持的内存使用量,由于历史原因,该页面未计入“ RSS”或“ PSS”字段中。 并且这些没有包含在Shared/Private_Clean/Dirty 字段中。

Swap:存在于交换分区的数据大小(如果物理内存有限,可能存在一部分在主存一部分在交换分区)

SwapPss:这个我并没有找到对应解释,但从源码可以得知,计算逻辑就跟pss一样,只不过针对的是交换分区的内存。

static void smaps_pte_entry(pte_t *pte, unsigned long addr,
        struct mm_walk *walk)
{
    struct mem_size_stats *mss = walk->private;
    struct vm_area_struct *vma = walk->vma;
    struct page *page = NULL;
 
    if (pte_present(*pte)) {//----------------------------------页面在内存中
        page = vm_normal_page(vma, addr, *pte);
    } else if (is_swap_pte(*pte)) {//---------------------------页面被swap出
        swp_entry_t swpent = pte_to_swp_entry(*pte);
 
        if (!non_swap_entry(swpent)) {
            int mapcount;
 
            mss->swap += PAGE_SIZE;
            mapcount = swp_swapcount(swpent);
            if (mapcount >= 2) {
                u64 pss_delta = (u64)PAGE_SIZE << PSS_SHIFT;
               do_div(pss_delta, mapcount);
                mss->swap_pss += pss_delta; // --------- 如果引用超过1,就将均值加入swap_pss中
            } else {
                mss->swap_pss += (u64)PAGE_SIZE << PSS_SHIFT;// ------------ 直接加一个页大小
            }
       } else if (is_migration_entry(swpent))
            page = migration_entry_to_page(swpent);
    }
 
    if (!page)//----------------------------------------------如果页面不存在,就不用更新mss其他信息了;如果存在,调用smaps_account()更新mss。
        return;
smaps_account(mss, page, PAGE_SIZE, pte_young(*pte), pte_dirty(*pte));
}

KernelPageSize:内核一页的大小
MMUPageSize:MMU页大小,大多数情况下,和KernelPageSize大小一样。

Locked:常驻物理内存的大小,这些页不会被换出。

THPeligible:映射是否符合分配THP的条件。如果为true,则为1,否则为0。 它仅显示当前状态。

THP,透明大页(Transparent Huge Pages),RHEL 6 开始引入,目的是使用更大的内存页面(memory page size) 以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。与标准大页的区别在于分配机制,标准大页管理是预分配的方式,而透明大页管理则是动态分配的方式。

VmFlags:表示与特定虚拟内存区域关联的内核标志。标志如下:

rd  - readable
wr  - writeable
ex  - executable
sh  - shared
mr  - may read
mw  - may write
me  - may execute
ms  - may share
gd  - stack segment growns down
pf  - pure PFN range
dw  - disabled write to the mapped file
lo  - pages are locked in memory
io  - memory mapped I/O area
sr  - sequential read advise provided
rr  - random read advise provided
dc  - do not copy area on fork
de  - do not expand area on remapping
ac  - area is accountable
nr  - swap space is not reserved for the area
ht  - area uses huge tlb pages
ar  - architecture specific flag
dd  - do not include area into core dump
sd  - soft-dirty flag
mm  - mixed map area
hg  - huge page advise flag
nh  - no-huge page advise flag
mg  - mergable advise flag

以java进程smaps为例:

# cat JAVA_PID/smaps |grep -A 50 heap
0074b000-0076c000 rw-p 00000000 00:00 0                                  [heap]
Size:                132 kB
Rss:                  16 kB
Pss:                  16 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:        16 kB
Referenced:           16 kB
Anonymous:            16 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
ProtectionKey:         0
VmFlags: rd wr mr mp me ac sd
2c0000000-7c0420000 rw-p 00000000 00:00 0
Size:           20975744 kB
Rss:            20213240 kB
Pss:            20213240 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:  20213240 kB
Referenced:     20195060 kB
Anonymous:      20213240 kB
AnonHugePages:  19556352 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
ProtectionKey:         0
VmFlags: rd wr mr mp me ac sd
7c0420000-800000000 ---p 00000000 00:00 0
Size:            1044352 kB
Rss:                   0 kB
Pss:                   0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
ProtectionKey:         0
VmFlags: mr mp me nr sd

        分析:java进程通过堆分配大量的AnonHugePages匿名映射,同时这些匿名映射中大量被修改过,是个内存密集型应用,同时大部分匿名映射页为AnonHugePages 匿名大页。

2. pmap统计

        pmap(process memory map)命令用于查看进程的内存映射,即进程的内存地址空间。

        pmap 从文件 /proc/<pid>/maps 中获得相关数据,用来观察系统中的指定进程的地址空间分布和内存状态信息,包括进程各个段的大小。对查看完整的进程地址空间很有帮助。

-x, --extended
    显示扩展格式。
-d, --device
    显示设备格式。
-q, --quiet
    不显示某些页眉或页脚行。
-A, --range <low>,<high>
    将结果限制在给定范围内的低地址和高地址。
-X
    显示比 -x 选项更多的细节。 警告:根据 /proc/PID/smaps 改变格式。
-XX
    显示内核提供的所有内容。
-p, --show-path
    在映射列中显示文件的完整路径。
-c, --read-rc
    读取默认配置。
-C, --read-rc-from <file>
    从指定文件读取配置。
-n, --create-rc
    创建新的默认配置。
-N, --create-rc-to file
    创建新配置到指定文件。
-h, --help
    现实帮助信息并退出。
-V, --version
    显示版本信息并退出。

关于扩展格式和设备格式域的说明如下:

Address: start address of map 映射起始地址。
Kbytes: size of map in kilobytes 映射大小(KB)。
RSS: resident set size in kilobytes 驻留集大小。
Dirty: dirty pages (both shared and private) in kilobytes 脏页大小。
Mode: permissions on map 映像权限: r=read, w=write, x=execute, s=shared, p=private (copy on write)。
Mapping: file backing the map , or '[ anon ]' for allocated memory, or '[ stack ]' for the program stack. 占用内存的文件,[anon] 为已分配内存,[stack] 为程序栈。
Offset: offset into the file 文件偏移。
Device: device name (major:minor) 设备名。

# pmap -X 17349 | more
17349:   /opt/jdk/jdk1.8.0_191/bin/java ....
         Address Perm   Offset Device      Inode     Size    Rss    Pss Referenced Anonymous   Swap Locked ProtectionKey Mapping
        00400000 r-xp 00000000  08:11  134218287        4      0      0          0         0      0      0             0 java
        00600000 r--p 00000000  08:11  134218287        4      4      4          0         4      0      0             0 java
        00601000 rw-p 00001000  08:11  134218287        4      0      0          0         0      4      0             0 java
        01040000 rw-p 00000000  00:00          0      132      8      8          8         8      8      0             0 [heap]
        80200000 rw-p 00000000  00:00          0  1398272  12224  12224      11708     12224   3040      0             0
        d5780000 ---p 00000000  00:00          0 19572224      0      0          0         0      0      0             0
       580100000 rw-p 00000000  00:00          0   545280 336980 336980     308308    336980 192512      0             0
       5a1580000 ---p 00000000  00:00          0  9939456      0      0          0         0      0      0             0
       800000000 rw-p 00000000  00:00          0     1408   1256   1256       1256      1256      0      0             0
       800160000 ---p 00000000  00:00          0  1047168      0      0          0         0      0      0             0
    7f3a6c000000 rw-p 00000000  00:00          0      132     16     16         16        16      0      0             0
    7f3a6c021000 ---p 00000000  00:00          0    65404      0      0          0         0      0      0             0
... ...
   7f3bfcbcd000 rw-p 00022000  fd:00  536871970        4      4      4          4         4      0      0             0 ld-2.17.so
    7f3bfcbce000 rw-p 00000000  00:00          0        4      4      4          0         4      0      0             0
    7fff8477c000 rw-p 00000000  00:00          0      132      4      4          4         4     32      0             0 [stack]
    7fff847f4000 r-xp 00000000  00:00          0        8      4      0          4         0      0      0             0 [vdso]
ffffffffff600000 r-xp 00000000  00:00          0        4      0      0          0         0      0      0             0 [vsyscall]
                                                 ======== ====== ====== ========== ========= ====== ====== =============
                                                 38553764 487220 484204     444848    482132 274948      0             0 KB

#查看进程anon占用
#pmap -d 18489 -p | grep anon | awk '{sum+=$2} END {print "anon size (Kbytes):", sum}'
anon size (Kbytes): 32822544


#循环显示进程内存情况
#while true; do pmap -d 进程PID | tail -1; sleep 2; done

# while true; do pmap -d 31193 | tail -1; sleep 2; done
mapped: 112988K    writeable/private: 1060K    shared: 0K
mapped: 112988K    writeable/private: 1060K    shared: 0K
mapped: 112988K    writeable/private: 1060K    shared: 0K
mapped: 112988K    writeable/private: 1060K    shared: 0K
mapped: 112988K    writeable/private: 1060K    shared: 0K
mapped: 112988K    writeable/private: 1060K    shared: 0K

#while true; do pmap -d 31193 -p | grep anon | awk '{sum+=$2} END {print "anon size (Kbytes):", sum}'; sleep 2; done
anon size (Kbytes): 632
anon size (Kbytes): 632
anon size (Kbytes): 632
anon size (Kbytes): 632
.... .... 

五:针对单个进程的ebpf跟踪分析-进程微观

        eBPF 提供了一种高效的机制来监控和追踪系统级别的事件,包括内存的分配和释放。通过 eBPF,我们可以跟踪内存分配和释放的请求,并收集每次分配的调用堆栈。然后,我们可以分析这些信息,找出执行了内存分配但未执行释放操作的调用堆栈,这有助于我们找出导致内存问题的源头。这种方式的优点在于,它可以实时地在运行的应用程序中进行,而无需暂停应用程序或进行复杂的前后处理。

ebpf安装: 

ebpf内核态开发环境搭建

bcc安装:
yum -y install python3-bcc

(1/10): bcc-0.19.0-4.el8.x86_64.rpm                                                  
(2/10): clang-libs-12.0.1-4.module_el8.5.0+1025+93159d6c.x86_64.rpm                  
(3/10): clang-resource-filesystem-12.0.1-4.module_el8.5.0+1025+93159d6c.x86_64.rpm   
(4/10): bcc-tools-0.19.0-4.el8.x86_64.rpm                                            
(5/10): libomp-devel-12.0.1-1.module_el8.5.0+892+54d791e1.x86_64.rpm                 
(6/10): libomp-12.0.1-1.module_el8.5.0+892+54d791e1.x86_64.rpm                       
(7/10): python3-bcc-0.19.0-4.el8.x86_64.rpm                                          
(8/10): compiler-rt-12.0.1-1.module_el8.5.0+892+54d791e1.x86_64.rpm                  
(9/10): python3-netaddr-0.7.19-8.el8.noarch.rpm                                      
(10/10): llvm-libs-12.0.1-2.module_el8.5.0+918+ed335b90.x86_64.rpm                  

# bpftrace -h
USAGE:
    bpftrace [options] filename
    bpftrace [options] - <stdin input>
    bpftrace [options] -e 'program'

OPTIONS:
    -B MODE        output buffering mode ('full', 'none')
    -f FORMAT      output format ('text', 'json')
    -o file        redirect bpftrace output to file
    -d             debug info dry run
    -dd            verbose debug info dry run
    -e 'program'   execute this program
    -h, --help     show this help message
    -I DIR         add the directory to the include search path
    --include FILE add an #include file before preprocessing
    -l [search]    list probes
    -p PID         enable USDT probes on PID
    -c 'CMD'       run CMD and enable USDT probes on resulting process
    --usdt-file-activation
                   activate usdt semaphores based on file path
    --unsafe       allow unsafe builtin functions
    -q             keep messages quiet
    -v             verbose messages
    --info         Print information about kernel BPF support
    -k             emit a warning when a bpf helper returns an error (except read functions)
    -kk            check all bpf helper functions
    -V, --version  bpftrace version
    --no-warnings  disable all warning messages

#bpftrace程序语法规则

#bpftrace语法由以下一个或多个action block结构组成,且语法关键字与c语言类似

#probe[,probe]  /predicate/ { action }
    probes :事件,tracepoint, kprobe, kretprobe, uprobe。两个特殊事件BEGIN/END,用于脚本开始和结束处执行
    filter :predicate过滤条件,事件触发时,判断条件,例如:/pid == 3245/,表示 pid为 3245 的进程执行。
    action :具体执行的操作,例如:{ printf(“close\n”);} 打印 close

    • probe:探针,可以使用bpftrace -l 来查看支持的所有tracepoint和kprobe探针
    • Predicate(可选):在 / / 中指定 action 执行的条件。如果为True,就执行 action
    • action:在事件触发时运行的程序,每行语句必须以 ; 结尾,并且用{}包起来
    • //:单行注释
    • /**/:多行注释
    • ->:访问c结构体成员,例如:bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
    • struct:结构声明,在bpftrace脚本中可以定义自己的结构。

#bpftrace支持以下类型的探针:

    kprobe- 内核函数启动

    kretprobe- 内核函数返回

    uprobe- 用户级功能启动

    uretprobe- 用户级函数返回

    tracepoint- 内核静态跟踪点

    usdt- 用户级静态跟踪点

    profile- 定时采样

    interval- 定时输出

    software- 内核软件事件

    hardware- 处理器级事件


#bpftrace 脚本常用内置变量:
uid:用户 id。
tid:线程 id
pid:进程 id。
cpu:cpu id。
cgroup:cgroup id.
probe:当前的 trace 点。
comm:进程名字。
nsecs:纳秒级别的时间戳。
kstack:内核栈描述
curtask:当前进程的 task_struct 地址。
args:获取该 kprobe 或者 tracepoint 的参数列表
arg0:获取该 kprobe 的第一个变量,tracepoint 不可用
arg1:获取该 kprobe 的第二个变量,tracepoint 不可用
arg2:获取该 kprobe 的第三个变量,tracepoint 不可用
retval: kretprobe 中获取函数返回值
args->ret: kretprobe 中获取函数返回值

#内置函数
exit():退出 bpftrace 程序
str(char *):转换一个指针到 string 类型
system(format[, arguments ...]):运行一个 shell 命令
join(char *str[]):打印一个字符串列表并在每个前面加上空格,比如可以用
来输出 args->argv
ksym(addr):用于转换一个地址到内核 symbol
kaddr(char *name):通过 symbol 转换为内核地址
print(@m [, top [, div]]):可选择参数打印 map 中的 top n 个数据,数
据可选择除以一个 div 值

#bpftrace 内置了 map 对象操作函数,用于传递数据给 map 变量
count():用于计算次数
sum(int n):用于累加计算
avg(int n):用于计算平均值
min(int n):用于计算最小值
max(int n):用于计算最大值
hist(int n):数据分布直方图(范围为 2 的幂次增长)
lhist(int n):数据线性直方图
delete(@m[key]):删除 map 中的对应的 key 数据
clear(@m):删除 map 中的所有数据
zero(@m):map 中的所有值设置为 0




#示例:bpftrace -e 选项可以指定运行一个单行程序 
1、追踪openat系统调用

    bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'

2、系统调用计数

    bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

3、计算每秒发生的系统调用数量

    bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @ = count(); } interval:s:1 { print(@); clear(@); }'

kprobe的基本工作原理其实也很简单。当kprobe函数注册的时候,其实就是把目标地址上内核代码的指令码,替换成了“cc”,也就是int3指令。这样一来,当内核代码执行到这条指令的时候,就会触发一个异常而进入到Linux int3异常处理函数do_int3()里。

在do_int3()这个函数里,如果发现有对应的kprobe注册了probe,就会依次执行注册的pre_handler(),原来的指令,最后是post_handler()。

理论上kprobe其实只要知道内核代码中任意一条指令的地址,就可以为这个地址注册probe函数,kprobe结构中的“addr”成员就可以接受内核中的指令地址。

static int __init kprobe_init(void)
{
        int ret;
        kp.addr = (kprobe_opcode_t *)0xffffffffb1e8ca02; /* 把一条指令的地址赋值给 kprobe.addr */
        kp.pre_handler = handler_pre;
        kp.post_handler = handler_post;
        kp.fault_handler = handler_fault;
 
        ret = register_kprobe(&kp);
        if (ret < 0) {
                pr_err("register_kprobe failed, returned %d\n", ret);
                return ret;
        }
        pr_info("Planted kprobe at %p\n", kp.addr);
        return 0;
}

还要说明的是,如果内核可以使用我们上一讲ftrace对函数的trace方式,也就是函数头上预留了“callq <fentry>”的5个字节(在启动的时候被替换成了nop)。Kprobe对于函数头指令的trace方式,也会用“ftrace_caller”指令替换的方式,而不再使用int3指令替换。

不论是哪种替换方式,kprobe的基本实现原理都是一样的,那就是把目标指令替换,替换的指令可以使程序跑到一个特定的handler里,去执行probe的函数。

1. bpftrace 单行指令追踪

#获取系统支持的tracepoint

# bpftrace  -l | grep :kmem
kprobe:kmem_alloc
kprobe:kmem_alloc_large
kprobe:kmem_cache_alloc
kprobe:kmem_cache_alloc_bulk
kprobe:kmem_cache_alloc_node
kprobe:kmem_cache_alloc_node_trace
kprobe:kmem_cache_alloc_trace
kprobe:kmem_cache_create
kprobe:kmem_cache_create_usercopy
kprobe:kmem_cache_destroy
kprobe:kmem_cache_flags
kprobe:kmem_cache_free
kprobe:kmem_cache_free_bulk
kprobe:kmem_cache_open
kprobe:kmem_cache_release
kprobe:kmem_cache_shrink
kprobe:kmem_cache_size
kprobe:kmem_realloc
kprobe:kmem_zone_alloc
kprobe:kmemcg_cache_deact_after_rcu
kprobe:kmemcg_deactivate_rcufn
kprobe:kmemcg_deactivate_workfn
kprobe:kmemdup_nul
tracepoint:kmem:kfree
tracepoint:kmem:kmalloc
tracepoint:kmem:kmalloc_node
tracepoint:kmem:kmem_cache_alloc
tracepoint:kmem:kmem_cache_alloc_node
tracepoint:kmem:kmem_cache_free
tracepoint:kmem:mm_page_alloc
tracepoint:kmem:mm_page_alloc_extfrag
tracepoint:kmem:mm_page_alloc_zone_locked
tracepoint:kmem:mm_page_free
tracepoint:kmem:mm_page_free_batched
tracepoint:kmem:mm_page_pcpu_drain

#查看tracepoint目录,确定tracepoint struct

/sys/kernel/debug/tracing/events/

# bpftrace -vl tracepoint:kmem:kmem_cache_alloc
tracepoint:kmem:kmem_cache_alloc
    unsigned long call_site
    const void * ptr
    size_t bytes_req
    size_t bytes_alloc
    gfp_t gfp_flags

# cat /sys/kernel/debug/tracing/events/kmem/kmem_cache_alloc/format
name: kmem_cache_alloc
ID: 516
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:unsigned long call_site;  offset:8;       size:8; signed:0;
        field:const void * ptr; offset:16;      size:8; signed:0;
        field:size_t bytes_req; offset:24;      size:8; signed:0;
        field:size_t bytes_alloc;       offset:32;      size:8; signed:0;
        field:gfp_t gfp_flags;  offset:40;      size:4; signed:0;

print fmt: "call_site=%lx ptr=%p bytes_req=%zu bytes_alloc=%zu gfp_flags=%s", REC->call_site, REC->ptr, REC->bytes_req, REC->bytes_alloc, (REC->gfp_flags) ? __print_flags(REC->gfp_flags, "|", {(unsigned long)(((((((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u) | (( gfp_t)0x20000u)) | (( gfp_t)0x02u)) | (( gfp_t)0x08u)) | (( gfp_t)0x4000u) | (( gfp_t)0x10000u) | (( gfp_t)0x200u)) & ~(( gfp_t)(0x200000u|0x400000u))) | (( gfp_t)0x200000u)), "GFP_TRANSHUGE"}, {(unsigned long)((((((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u) | (( gfp_t)0x20000u)) | (( gfp_t)0x02u)) | (( gfp_t)0x08u)) | (( gfp_t)0x4000u) | (( gfp_t)0x10000u) | (( gfp_t)0x200u)) & ~(( gfp_t)(0x200000u|0x400000u))), "GFP_TRANSHUGE_LIGHT"}, {(unsigned long)((((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u) | (( gfp_t)0x20000u)) | (( gfp_t)0x02u)) | (( gfp_t)0x08u)), "GFP_HIGHUSER_MOVABLE"}, {(unsigned long)(((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u) | (( gfp_t)0x20000u)) | (( gfp_t)0x02u)), "GFP_HIGHUSER"}, {(unsigned long)((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u) | (( gfp_t)0x20000u)), "GFP_USER"}, {(unsigned long)(((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u)) | (( gfp_t)0x100000u)), "GFP_KERNEL_ACCOUNT"}, {(unsigned long)((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u) | (( gfp_t)0x80u)), "GFP_KERNEL"}, {(unsigned long)((( gfp_t)(0x200000u|0x400000u)) | (( gfp_t)0x40u)), "GFP_NOFS"}, {(unsigned long)((( gfp_t)0x20u)|(( gfp_t)0x80000u)|(( gfp_t)0x400000u)), "GFP_ATOMIC"}, {(unsigned long)((( gfp_t)(0x200000u|0x400000u))), "GFP_NOIO"}, {(unsigned long)((( gfp_t)0x400000u)), "GFP_NOWAIT"}, {(unsigned long)(( gfp_t)0x01u), "GFP_DMA"}, {(unsigned long)(( gfp_t)0x02u), "__GFP_HIGHMEM"}, {(unsigned long)(( gfp_t)0x04u), "GFP_DMA32"}, {(unsigned long)(( gfp_t)0x20u), "__GFP_HIGH"}, {(unsigned long)(( gfp_t)0x80000u), "__GFP_ATOMIC"}, {(unsigned long)(( gfp_t)0x40u), "__GFP_IO"}, {(unsigned long)(( gfp_t)0x80u), "__GFP_FS"}, {(unsigned long)(( gfp_t)0x200u), "__GFP_NOWARN"}, {(unsigned long)(( gfp_t)0x400u), "__GFP_RETRY_MAYFAIL"}, {(unsigned long)(( gfp_t)0x800u), "__GFP_NOFAIL"}, {(unsigned long)(( gfp_t)0x1000u), "__GFP_NORETRY"}, {(unsigned long)(( gfp_t)0x4000u), "__GFP_COMP"}, {(unsigned long)(( gfp_t)0x8000u), "__GFP_ZERO"}, {(unsigned long)(( gfp_t)0x10000u), "__GFP_NOMEMALLOC"}, {(unsigned long)(( gfp_t)0x2000u), "__GFP_MEMALLOC"}, {(unsigned long)(( gfp_t)0x20000u), "__GFP_HARDWALL"}, {(unsigned long)(( gfp_t)0x40000u), "__GFP_THISNODE"}, {(unsigned long)(( gfp_t)0x10u), "__GFP_RECLAIMABLE"}, {(unsigned long)(( gfp_t)0x08u), "__GFP_MOVABLE"}, {(unsigned long)(( gfp_t)0x100000u), "__GFP_ACCOUNT"}, {(unsigned long)(( gfp_t)0x100u), "__GFP_WRITE"}, {(unsigned long)(( gfp_t)(0x200000u|0x400000u)), "__GFP_RECLAIM"}, {(unsigned long)(( gfp_t)0x200000u), "__GFP_DIRECT_RECLAIM"}, {(unsigned long)(( gfp_t)0x400000u), "__GFP_KSWAPD_RECLAIM"} ) : "none"

#bpftrace指令跟踪 -e   pid 1386

bpftrace -e 'tracepoint:kmem:kmem_cache_alloc / pid == 1386 / { printf("comm=%s call_site=%lx bytes_req=%zu bytes_alloc=%zu \n", comm,args->call_site,args->bytes_req,args->bytes_alloc)}'

# bpftrace -e 'tracepoint:kmem:kmem_cache_alloc / pid == 1386 / { printf("comm=%s call_site=%lx bytes_req=%zu bytes_alloc=%zu \n", comm,args->call_site,args->bytes_req,args->bytes_alloc)}'
Attaching 1 probe...
comm=sshd call_site=ffffffff8b2aa3d8 bytes_req=696 bytes_alloc=704
comm=sshd call_site=ffffffff8aebef94 bytes_req=208 bytes_alloc=208
comm=sshd call_site=ffffffff8aea4e6a bytes_req=256 bytes_alloc=256
comm=sshd call_site=ffffffff8aec04d9 bytes_req=648 bytes_alloc=656
comm=sshd call_site=ffffffff8aebef94 bytes_req=208 bytes_alloc=208
comm=sshd call_site=ffffffff8aea4e6a bytes_req=256 bytes_alloc=256
comm=sshd call_site=ffffffff8aea4e6a bytes_req=256 bytes_alloc=256
comm=sshd call_site=ffffffff8b2aa3d8 bytes_req=696 bytes_alloc=704
comm=sshd call_site=ffffffff8b2ae6ff bytes_req=1152 bytes_alloc=1152
comm=sshd call_site=ffffffff8b2aa3d8 bytes_req=696 bytes_alloc=704
comm=sshd call_site=ffffffff8b2ae6ff bytes_req=1152 bytes_alloc=1152
comm=sshd call_site=ffffffff8aebef94 bytes_req=208 bytes_alloc=208
comm=sshd call_site=ffffffff8aea4e6a bytes_req=256 bytes_alloc=256
comm=sshd call_site=ffffffff8aebef94 bytes_req=208 bytes_alloc=208


## bpftrace -e 'tracepoint:kmem:kmem_cache_alloc / pid == 1386 / { @bytes_alloc = hist(args->bytes_alloc); } '
Attaching 1 probe...

@bytes_alloc:
[64, 128)            426 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[128, 256)           312 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@              |
[256, 512)            10 |@                                                   |
[512, 1K)             10 |@                                                   |
[1K, 2K)               8 |                                                    |
[2K, 4K)               2 |                                                    |

^C

2.bpftrace脚本追踪(python + bcc)

   跟踪-p指定进程的function方法堆栈调用,检查内存的使用情况。

  参考:/usr/share/bcc/tools

from __future__ import print_function
from bcc import BPF
import argparse
import time

#初始化命令项选项与参数解析
parser = argparse.ArgumentParser(
        description="Trace and print kernel stack traces for a kernel function",
        formatter_class=argparse.RawDescriptionHelpFormatter)
#跟踪函数参数
parser.add_argument("function", help="kernel function name")
#指定pid跟踪
parser.add_argument("-p", "--pid",help="trace this PID only")
args = parser.parse_args()
function = parser.parse_args().function
offset = False

# define BPF program
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

//定义了内核Probe程序与用户空间程序通信的结构体data_t
struct data_t {
        u64 stack_id;
        u32 pid;
        char comm[TASK_COMM_LEN];
};
// 定义跟踪栈 BPF_STACK_TRACE(name, max_entries),创建名称为name的 stack trace map,并设置最大的entry条目。map用于存储stack栈的调用记录信息。
BPF_STACK_TRACE(stack_traces, 128);

//通过BBC宏定义内核中events变量
BPF_PERF_OUTPUT(events);

void trace_stack(struct pt_regs *ctx) {
        u32 pid = bpf_get_current_pid_tgid() >> 32;
        FILTER
        struct data_t data = {};
        //int map.get_stackid(void *ctx, u64 flags)     //在内核端,通过stack_traces.get_stackid(ctx,0)通过当前进程ctx上下文保存栈,获取当前进程的内核栈的栈编号,其中ctx是上下文信息,0的意义暂时未知;该工具将每个进程的stackid都传送到了用户端。
        data.stack_id = stack_traces.get_stackid(ctx, 0);
        data.pid = pid;
        //bpf_get_current_comm(char *buf, int size_of_buf) 用当前进程名字填充buf参数地址。
        bpf_get_current_comm(&data.comm, sizeof(data.comm));
        //通过perf_submit方法将event数据发送至用户空间
        events.perf_submit(ctx, &data, sizeof(data));
}
"""
#增加bpf pid过滤条件
if args.pid:
    bpf_text = bpf_text.replace('FILTER',
        'if (pid != %s) { return; }' % args.pid)
else:
    bpf_text = bpf_text.replace('FILTER', '')

# initialize BPF
b = BPF(text=bpf_text)
#在eBPF用户态程序中,可以通过attach_kprobe函数将内核态eBPF程序通过kprobes机制附加到某个内核函数中。attach_kprobe 函数会创建一个 perf event,再将 eBPF 内核态程序附加到 perf event。每个 perf event 的 kprobe probe handler 都是 kprobe_dispatch 函数,他会去 perf event 中获取注册在当前 perf event 的回调函数列表并依次执行,同时将指向 perf ringbuffer 的指针的传递给 eBPF 程序,eBPF 程序可以通过 libbpf 封装好的 PT_REGS_PARAMx 宏定义来获取缓冲区中的数据。 (tracepoint 类型的 eBPF 程序需要定义好 tracepoint 关联的函数的参数的数据结构 /sys/kernel/tracing/events)
#该kprobe会执行自定义的trace_stack()函数。可以通过多次执行attach_kprobe() ,将自定义的函数附加到多个内核函数上。
b.attach_kprobe(event=function, fn_name="trace_stack")

# linux/sched.h
TASK_COMM_LEN = 16

# 判断输入的 function 是否合法
matched = b.num_open_kprobes()
if matched == 0:
        print("Function \"%s\" not found. Exiting." % function)
        exit()

# 获取跟踪栈,返回表对象。此函数已经淘汰,现在BFP可以将表作为items来读取,例如BFP[name].
stack_traces = b.get_table("stack_traces")

start_ts = time.time()

# header
print("%-18s %-12s %-6s %-3s %s" % ("TIME(s)", "COMM", "PID", "CPU", "FUNCTION"))

#用户空间Python程序则需要定义 print_event事件处理函数,并使用 perf_buffer_poll 函数轮训消费。
def print_event(cpu, data, size):
                #读取出对应的数据并生成结构数据
        event = b["events"].event(data)
        ts = time.time() - start_ts
        print("%-18.9f %-12.12s %-6d %-3d %s" % (ts, event.comm.decode('utf-8', 'replace'), event.pid, cpu, function))
        #在用户端,通过stack_traces.walk(event.stack_id)获取了stackid对应的栈列表。
        #根据 stack.id 遍历堆栈
        for addr in stack_traces.walk(event.stack_id):
            # BPF.ksym(addr) 将一个内核内存地址转成一个内核函数名字
            sym = b.ksym(addr, show_offset=offset).decode('utf-8', 'replace')
            print("\t%s" % sym)
        print()

#从perf ring buffers等待数据,有数据会调用open_perf_buffer指定的回调函数。
b["events"].open_perf_buffer(print_event)

while 1:
        try:
                b.perf_buffer_poll()
        except KeyboardInterrupt:
                exit()
# python  stacksnoop.py kmem_cache_free -p 792
TIME(s)            COMM         PID    CPU FUNCTION
3.868038654        sshd         792    1   kmem_cache_free
        kmem_cache_free
        __dequeue_signal
        dequeue_signal
        get_signal
        do_signal
        exit_to_usermode_loop
        do_syscall_64
        entry_SYSCALL_64_after_hwframe

3.938354254        sshd         792    1   kmem_cache_free
        kmem_cache_free
        release_task.part.19
        wait_consider_task
        do_wait
        kernel_wait4
        __do_sys_wait4
        do_syscall_64
        entry_SYSCALL_64_after_hwframe

4.699573755        sshd         792    1   kmem_cache_free
        kmem_cache_free
        finish_task_switch
        __sched_text_start
        schedule
        schedule_hrtimeout_range_clock
        poll_schedule_timeout.constprop.14
        do_select
        core_sys_select
        kern_select
        __x64_sys_select
        do_syscall_64
        entry_SYSCALL_64_after_hwframe

4.699874878        sshd         792    1   kmem_cache_free
        kmem_cache_free
        __dequeue_signal
        dequeue_signal
        get_signal
        do_signal
        exit_to_usermode_loop
        do_syscall_64
        entry_SYSCALL_64_after_hwframe

4.699988604        sshd         792    1   kmem_cache_free
        kmem_cache_free
        release_task.part.19
        wait_consider_task
        do_wait
        kernel_wait4
        __do_sys_wait4
        do_syscall_64
        entry_SYSCALL_64_after_hwframe

  • 11
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值