前言
本篇是APM系列文章的第三篇,主要介绍如何通过一个第三方应用,去监控整个系统中所有应用进程的CPU占用,线程数量等信息,从而辅助排查问题。
文章开始之前,抛出2个问题:
1.为什么要进行采集CPU峰值?
2.直接使用TOP命令采集不可以吗?
第一个问题,采集应用的CPU峰值,可以辅助我们排查ANR问题,以及检测各个应用之间资源抢占的问题。
第二个问题,为什么不用TOP?为什么TOP采集的是瞬时值,比如0.01S的时间内突然CPU占用率很高,恰好又被TOP命令采集到,这种其实是不影响使用的,也不是问题,而使用TOP其实就存在这样的问题(TOP采集的是0.005S范围内的)。
其实这里还有第三个优势,那就是在APM中进行CPU采集,不依赖外部自动化框架,甚至可以应用于生产环境,要远比使用自动化测试框架执行脚本方便的多。
一.如何采集系统的CPU占用
了解如何采集系统CPU负载之前,我们需要先了解一下Linux的一个知识点。Linux中,内核层的进程相关的一些参数,会映射到外部文件上,其实就包括CPU。
CPU的信息就在stat中,相关的信息如下:
cpu 68724093 7457530 63397397 925179093 133980 12530536 5032898 0 0 0
cpu0 10112649 219978 5476279 162917877 56663 902499 71777 0 0 0
cpu1 6708277 290737 10433269 152733544 20066 5946336 4106893 0 0 0
cpu2 7910158 363569 7767095 162047420 21765 1395025 165457 0 0 0
cpu3 7764109 362644 7439487 161488289 24479 2588136 188684 0 0 0
cpu4 18176591 3311604 16252285 142544691 5175 853398 313223 0 0 0
cpu5 18052306 2908995 16028979 143447269 5829 845139 186861 0 0 0
intr 9609584710 0 0 0 2763090998 0 240905789 7 113975428 88 321676 0 0 0 266 18095257 0 128 170362452 0 0 0 585219272 434 0 0 33 0 0 100 0 0 0 0 0 40580 11321182 32310 30406 45 7 1 291167 30377 77009 30399 2 2511542 31631 0 0 0 0 0 0 0 0 0 0 0 0 113969235 104206676 6 99 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 13 2 0 0 0 0 0 0 0 26050299 42922 0 0 0 0 2 0 0 0 20403273 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 12661077
ctxt 18927154346
btime 1697697895
processes 6541854
procs_running 1
procs_blocked 0
softirq 2632078945 756013040 411005607 55 251933491 0 0 20403721 422860516 0 769862515
第一行cpu自然自然指的是CPU的总体负载;
第2到7行指的是每个CPU的负载。
第10行btime指的是系统启动时间。
我们的关注点,自然是cpu的相关内容。
列 | 值 | 解释 |
0 | cpu | cpu名 |
1 | 68724093 | 用户态占用的CPU时间片数量 |
2 | 7457530 | |
3 | 63397397 | 内核态占用的CPU时间片数量 |
4 | 925179093 | 空闲的cpu时间片 |
5 | 133980 | IO等待占用的CPU时间片数量 |
对于CPU来说,一秒有10个时间片,占用10个时间片就意味着占用一个CPU核1秒时间。
采集一段时间的CPU占用率,使用结束时的值减去初始值即可。
二.如何采集应用的CPU占用
同第一章一样,每个进程的相关信息,其实也被映射到proc文件夹下,如果想采集CPU的信息,则其文件位置为:proc/进程ID/stat
其内容如下:
27005 (chs.scenemanage) S 364 364 0 0 -1 1077952832 904400 31987 243 1 6046993 2110950 404 90 20 0 113 0 18317266 43353554944 87520 18446744073709551615 369542340608 369542358060 549678426064 0 0 0 4612 1 1073775864 0 0 0 17 5 0 0 0 0 0 369542470240 369542471680 370186252288 549678426720 549678426819 549678426819 549678428126 0
每个字段的含义:
第几列 | 名称 | 英文名 | 实例值 | 解释 |
0 | 进程PID | pid | 27005 | 进程ID (pid) |
1 | 进程名称 | comm | chs.scenemanage | 进程名,这里的进程名被截取了并不是完整的。 |
2 | 状态 | state | S | R:运行, S:休眠, D:等待IO, T: 暂停, T:停止,Z:僵尸进程, X:死亡 |
3 | 父进程ID | ppid | 364 | |
4 | 进程组ID | pgrp | 364 | 该任务所在的会话组ID |
5 | 会话ID | session | 0 | |
6 | 控制终端 | tty_nr | 0 | 该任务的tty终端的设备号,INT(0/256)=主设备号,(0-主设备号)=次设备号 |
7 | 控制进程组 | tpgid | -1 | 终端的进程组号,当前运行在该任务所在终端的前台任务(包括shell 应用程序)的PID。 |
8 | 内核调度优先级 | flags | 1077952832 | 进程标志位,查看该任务的特性。 |
9 | 拷贝次数 | min_flt | 904400 | 该任务不需要从硬盘拷数据而发生的缺页(次缺页)的次数。 |
10 | 缺页的次数目 | cmin_flt | 31987 | 累计的该任务的所有的waited-for进程曾经发生的次缺页的次数目。 |
11 | 主缺页次数 | maj_flt | 243 | 该任务需要从硬盘拷数据而发生的缺页(主缺页)的次数。 |
12 | 缺页次数 | cmaj_flt | 1 | 累计的该任务的所有的waited-for进程曾经发生的主缺页的次数目。 |
13 | 用户模式时间 | utime | 6046993 | |
14 | 内核模式时间 | stime | 2110950 | |
15 | 等待子进程的用户模式时间 | cutime | 404 | |
16 | 等待子进程的内核模式时间 | cstime | 90 | |
17 | 优先级 | priority | 20 | |
18 | nice 值 | nice | 0 | |
19 | 线程数 | num_threads | 113 | |
20 | 无用字段 | itrealvalue | 0 | |
21 | 启动时间 | starttime | 18317266 | 单位毫秒 |
22 | 虚拟内存大小 | vsize | 43353554944 | |
23 | 物理内存大小 | rrs | 87520 | |
后面的简略 |
所以求一个进程在一个时间段的CPU占用率,只要拿结束时的CPU时间片数量,减掉开始时的时间片数量,即可得到这段时间内占用的CPU时间片数量。除以总的时间片数量,得到的就是CPU占用率。
三.存在的问题
按照第一章和第二章的结论,我们可以确实可以计算出一段时间内安卓系统和某个应用的CPU占用率,甚至还可以得到进程中的线程数量。
但是实际操作的过程中,也遇到一些问题。
问题1:proc/进程ID/stat文件无权限阅读
虽然其权限值为-r--r--r--。关掉SELinux就正常的,所以最后通过配置SELinux的白名单解决。
问题2:报错FileNotFoundException
报错内容如下:
11-02 13:48:04.658 27807 27879 W System.err: java.io.FileNotFoundException: proc/10796/stat (No such file or directory)
11-02 13:48:04.659 27807 27879 W System.err: at java.io.FileInputStream.open0(Native Method)
11-02 13:48:04.659 27807 27879 W System.err: at java.io.FileInputStream.open(FileInputStream.java:231)
11-02 13:48:04.659 27807 27879 W System.err: at java.io.FileInputStream.<i nit>(FileInputStream.java:165)
11-02 13:48:04.659 27807 27879 W System.err: at com.xxxx.apmcommon.IOHelper.fromFileToIputStream(IOHelper.java:210)
11-02 13:48:04.659 27807 27879 W System.err: at com.xxxx.apmcommon.IOHelper.readFile(IOHelper.java:36)
11-02 13:48:04.659 27807 27879 W System.err: at com.xxxx.util.WatchUtils$Companion.readStatByPid2(WatchUtils.kt:90)
11-02 13:48:04.659 27807 27879 W System.err: at com.xxxx.watch.watchcpu.CpuPresenter.startReadAppCpu(CpuPresenter.kt:30)
11-02 13:48:04.659 27807 27879 W System.err: at com.xxxx.watch.watchcpu.GetTopBroadcast$GetTopRunnable.run(GetTopBroadcast.kt:87)
11-02 13:48:04.659 27807 27879 W System.err: at android.os.Handler.handleCallback(Handler.java:873)
11-02 13:48:04.659 27807 27879 W System.err: at android.os.Handler.dispatchMessage(Handler.java:99)
11-02 13:48:04.659 27807 27879 W System.err: at android.os.Looper.loop(Looper.java:193)
11-02 13:48:04.659 27807 27879 W System.err: at android.os.HandlerThread.run(HandlerThread.java:65)
即使关掉了SELinux,某些进程仍然会提示这样的错误,说明和SELinux无关。而实际上,这个文件是存在的。
这个问题到目前为止仍然没有解决,由于是某些极个别的设备上的问题,不影响使用,所以就暂且搁置这个问题了。