当前位置:   article > 正文

花式读取Android CPU使用率_android 获取cpu使用率

android 获取cpu使用率

本文包含以下内容:

  1. 介绍常见的获取android cpu使用率的方法
  2. 介绍这些常见方法背后的原理
  3. 介绍我自己写的一个脚本,这个脚本可以获取各个线程在cpu各个核上的占用率

一、常见的获取Android CPU使用率方法及其原理

首先说一下如何查看cpu的基本信息,相信很多人也知道,使用下面的命令即可

adb shell cat /proc/cpuinfo

比如我从手边一台电视上获取到的信息如下,可以看到是个4核CPU,还能看到对应的CPU architecture

  1. processor : 0
  2. BogoMIPS : 24.00
  3. Features : fp asimd evtstrm aes pmull sha1 sha2 crc32
  4. CPU implementer : 0x41
  5. CPU architecture: AArch64
  6. CPU variant : 0x0
  7. CPU part : 0xd03
  8. CPU revision : 4
  9. Hardware : Maserati
  10. processor : 1
  11. BogoMIPS : 24.00
  12. Features : fp asimd evtstrm aes pmull sha1 sha2 crc32
  13. CPU implementer : 0x41
  14. CPU architecture: AArch64
  15. CPU variant : 0x0
  16. CPU part : 0xd08
  17. CPU revision : 2
  18. Hardware : Maserati
  19. processor : 2
  20. BogoMIPS : 24.00
  21. Features : fp asimd evtstrm aes pmull sha1 sha2 crc32
  22. CPU implementer : 0x41
  23. CPU architecture: AArch64
  24. CPU variant : 0x0
  25. CPU part : 0xd08
  26. CPU revision : 2
  27. Hardware : Maserati
  28. processor : 3
  29. BogoMIPS : 24.00
  30. Features : fp asimd evtstrm aes pmull sha1 sha2 crc32
  31. CPU implementer : 0x41
  32. CPU architecture: AArch64
  33. CPU variant : 0x0
  34. CPU part : 0xd03
  35. CPU revision : 4
  36. Hardware : Maserati

后面会发现,很多CPU使用率都是从/proc下获取到了,而/proc又是啥呢?可以直接参考Linux man-pages

The proc filesystem is a pseudo-filesystem which provides an interface to kernel data structures. It is commonly mounted at /proc. Most of it is read-only, but some files allow kernel variables to be changed.

以上其实算是Linux基础知识,在这里做备忘用

1.1 /proc/stat

adb shell cat /proc/stat

还是一样,在我的电视上通过上面的命令可以看到如下内容,需要说明的是,下面带#符号的是我加的注释,实际的打印没有这些内容。因为我们关注的是CPU使用率,所以实际上只需要关注前五行的数据。其他几行数据的含义可以查看Linux man-pages了解其含义。

  1. # user nice system idle iowait irq softirq steal guest guest_nice
  2. cpu 9209017 769851 5253355 93211564 47788 0 507580 0 0 0
  3. cpu0 2331920 357020 1753947 22302205 5287 0 499656 0 0 0
  4. cpu1 2280688 26391 794710 24135619 9820 0 2260 0 0 0
  5. cpu2 2289562 26618 782293 24138249 9637 0 3323 0 0 0
  6. cpu3 2306846 359821 1922404 22635490 23043 0 2339 0 0 0
  7. intr 2268122829 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 1514463779 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 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 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 39918 0 0 0 0 0 0 0 0 0 0 0 0 0 0 24 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 8478059 0 1231467 0 63977920 21812722 0 0 0 0 0 0 0 0 22 14476527 0 0 0 0 0 0 0 0 0 0 0 0 0 4489 0 0 3044630 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 30151490 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 0 0 0 3126 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
  8. ctxt 3894466714
  9. btime 1504771975
  10. processes 324478
  11. procs_running 1
  12. procs_blocked 0
  13. softirq 593223335 0 272491432 25632652 41701499 0 0 4153 92974658 20514 160398427

前五行分别打印了总体CPU数据和各个核的数据。每列数据的含义同样可以参考Linux man-pages,如下: 需要注意的是,以下时间都是从系统启动到当前时间内的累计时间

  • user (1) Time spent in user mode. 用户态时间
  • nice (2) Time spent in user mode with low priority (nice).
  • system (3) Time spent in system mode. 系统态时间
  • idle (4) Time spent in the idle task. 除IO等待之外的其他等待时间
  • iowait (since Linux 2.5.41) (5) Time waiting for I/O to complete. This value is not reliable, for the following reasons: 1. The CPU will not wait for I/O to complete; iowait is the time that a task is waiting for I/O to complete. When a CPU goes into idle state for outstanding task I/O, another task will be scheduled on this CPU. 2. On a multi-core CPU, the task waiting for I/O to complete is not running on any CPU, so the iowait of each CPU is difficult to calculate. 3. The value in this field may decrease in certain conditions. IO等待时间
  • irq (since Linux 2.6.0-test4) (6) Time servicing interrupts. 硬中断时间
  • softirq (since Linux 2.6.0-test4) (7) Time servicing softirqs. 软中断时间
  • steal (since Linux 2.6.11) (8) Stolen time, which is the time spent in other operating systems when running in a virtualized environment
  • guest (since Linux 2.6.24) (9) Time spent running a virtual CPU for guest operating systems under the control of the Linux kernel.
  • guest_nice (since Linux 2.6.33) (10) Time spent running a niced guest (virtual CPU for guest operating systems under the control of the Linux kernel).

  • 一般取前七个变量(user, nice, system, idle, iowait, irq, softirq)之和即为总的cpu时间,因为这是一个累计时间,所以我们只需要在两个时间点分别读一下cpu快照,设为total_time_old 和 total_time_new,则两个值相减即为这段时间内的总CPU时间total_time_delta,然后想办法读一个进程或线程在相同时间段内的cpu时间proc_time_delta, 则该进程或线程的cpu使用率即为100% * proc_time_delta / total_time_delta 那么要如何读一个进程或线程的cpu数据呢?请看下文

1.2 /proc/[pid]/stat 和 /proc/[pid]/task/[tid]/stat

 
adb shell cat /proc/[pid]/stat adb shell cat /proc/[pid]/task/[tid]/stat

至于如何获取pid 和 tid, 则可以用ps命令,比如在我手边的电视上用ps命令先查看一个进程的pid,这里以ijkplayer demo为例,如下

 
u0_a69 18446 1758 915464 29648 SyS_epoll_ 0000000000 S tv.danmaku.ijk.media.example

然后用下面的命令看看这个进程都有那些线程

 
adb shell ps -t 18446

结果如下

  1. USER PID PPID VSIZE RSS WCHAN PC NAME
  2. u0_a69 18446 1758 915464 29648 SyS_epoll_ 0000000000 S tv.danmaku.ijk.media.example
  3. u0_a69 18451 18446 915464 29648 do_sigtime 0000000000 S Signal Catcher
  4. u0_a69 18452 18446 915464 29648 poll_sched 0000000000 S JDWP
  5. u0_a69 18453 18446 915464 29648 futex_wait 0000000000 S ReferenceQueueD
  6. u0_a69 18454 18446 915464 29648 futex_wait 0000000000 S FinalizerDaemon
  7. u0_a69 18455 18446 915464 29648 futex_wait 0000000000 S FinalizerWatchd
  8. u0_a69 18456 18446 915464 29648 futex_wait 0000000000 S HeapTaskDaemon
  9. u0_a69 18457 18446 915464 29648 binder_thr 0000000000 S Binder_1
  10. u0_a69 18458 18446 915464 29648 binder_thr 0000000000 S Binder_2
  11. u0_a69 18491 18446 915464 29648 futex_wait 0000000000 S ModernAsyncTask
  12. u0_a69 18495 18446 915464 29648 SyS_epoll_ 0000000000 S RenderThread
  13. u0_a69 18502 18446 915464 29648 futex_wait 0000000000 S mali-mem-purge
  14. u0_a69 18503 18446 915464 29648 futex_wait 0000000000 S mali-utility-wo
  15. u0_a69 18504 18446 915464 29648 futex_wait 0000000000 S mali-utility-wo
  16. u0_a69 18505 18446 915464 29648 futex_wait 0000000000 S mali-utility-wo
  17. u0_a69 18506 18446 915464 29648 futex_wait 0000000000 S mali-utility-wo
  18. u0_a69 18507 18446 915464 29648 poll_sched 0000000000 S mali-cmar-backe
  19. u0_a69 18508 18446 915464 29648 futex_wait 0000000000 S mali-hist-dump
  20. u0_a69 19664 18446 915464 29648 futex_wait 0000000000 S ModernAsyncTask
  21. u0_a69 25018 18446 915464 29648 binder_thr 0000000000 S Binder_3
  22. u0_a69 25026 18446 915464 29648 futex_wait 0000000000 S ModernAsyncTask

分别看看进程和随便一个线程的cpu数据如下

  1. 18446 (k.media.example) S 1758 1757 0 0 -1 1077936448 20639 0 1 0 70 18 0 0 20 0 21 0 8405754 937435136 7412 18446744073709551615 1 1 0 0 0 0 4612 0 38136 18446744073709551615 0 0 17 3 0 0 0 0 0 0 0 0 0 0 0 0 0
  2. 18495 (RenderThread) S 1758 1757 0 0 -1 1077936192 3474 0 0 0 32 4 0 0 16 -4 21 0 8405783 937435136 7412 18446744073709551615 1 1 0 0 0 0 4612 0 38136 18446744073709551615 0 0 -1 3 0 0 0 0 0 0 0 0 0 0 0 0 0

一口气打印了50多个数据,不用怕,接着查看Linux man-page,可以知道各个数据项的含义如下,先说明一下/proc/[pid]/stat的含义,如下,可以看到就是进程的信息

Status information about the process. This is used by ps(1). It is defined in the kernel source file fs/proc/array.c.

而/proc/[pid]/task的含义,如下

This is a directory that contains one subdirectory for each thread in the process. The name of each subdirectory is the numerical thread ID ([tid]) of the thread (see gettid(2)). Within each of these subdirectories, there is a set of files with the same names and contents as under the /proc/[pid] directories.

是线程的信息,而且该目录下的子目录结构和/proc/[pid]/stat下的一致 下面来看看这50多项是什么含义,为了阅读方便,我把上面的获取到的数据也写到各项含义后面

(1) pid %d The process ID. 18446

(2) comm %s 线程名或进程名 (k.media.example)

(3) state %c One of the following characters, indicating process state运行状态,常见值有如下: (这个例子中是S)

  • R Running
  • S Sleeping in an interruptible wait
  • D Waiting in uninterruptible disk sleep
  • Z Zombie

(4) ppid %d 父进程ID The PID of the parent of this process. 1758

(5) pgrp %d The process group ID of the process. 1757

(6) session %d The session ID of the process. 0

(7) tty_nr %d The controlling terminal of the process. 0

(8) tpgid %d The ID of the foreground process group of the controlling terminal of the process. -1

(9) flags %u The kernel flags word of the process. For bit meanings, see the PF_* defines in the Linux kernel source file include/linux/sched.h. Details depend on the kernel version. 1077936448

(10) minflt %lu The number of minor faults the process has made which have not required loading a memory page from disk. 20639

(11) cminflt %lu The number of minor faults that the process's waited-for children have made. 0

(12) majflt %lu The number of major faults the process has made which have required loading a memory page from disk. 1

(13) cmajflt %lu The number of major faults that the process's waited-for children have made. 0

(14) utime %lu 用户态时间Amount of time that this process has been scheduled in user mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). This includes guest time, guest_time (time spent running a virtual CPU, see below), so that applications that are not aware of the guest time field do not lose that time from their calculations. 70

(15) stime %lu 系统态时间 Amount of time that this process has been scheduled in kernel mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). 18

(16) cutime %ld Amount of time that this process's waited-for children have been scheduled in user mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). (See also times(2).) This includes guest time, cguest_time (time spent running a virtual CPU, see below). 0

(17) cstime %ld Amount of time that this process's waited-for children have been scheduled in kernel mode, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). 0

(18) priority %ld 优先级,取值在0(high)-39(low)之间,本例中是20

(19) nice %ld The nice value (see setpriority(2)), a value in the range 19 (low priority) to -20 (high priority). 0

(20) num_threads %ld 线程数 ,在本例中是21

(21) itrealvalue %ld hard coded as 0.

(22) starttime %llu 进程启动的时间 The time the process started after system boot. 8405754

(23) vsize %lu Virtual memory size in bytes. 937435136

(24) rss %ld Resident Set Size: number of pages the process has in real memory. This is just the pages which count toward text, data, or stack space. This does not include pages which have not been demand-loaded in, or which are swapped out. 7412

(25) rsslim %lu Current soft limit in bytes on the rss of the process; see the description of RLIMIT_RSS in getrlimit(2). 18446744073709551615

(26) startcode %lu [PT] The address above which program text can run. 1

(27) endcode %lu [PT] The address below which program text can run. 1

(28) startstack %lu [PT] The address of the start (i.e., bottom) of the stack. 0

(29) kstkesp %lu [PT] The current value of ESP (stack pointer), as found in the kernel stack page for the process. 0

(30) kstkeip %lu [PT] The current EIP (instruction pointer). 0

(31) signal %lu The bitmap of pending signals, displayed as a decimal number. Obsolete, because it does not provide information on real-time signals; use /proc/[pid]/status instead. 0

(32) blocked %luThe bitmap of blocked signals, displayed as a decimal number. Obsolete, because it does not provide information on real-time signals; use /proc/[pid]/status instead. 4612

(33) sigignore %lu The bitmap of ignored signals, displayed as a decimal number. Obsolete, because it does not provide information on real-time signals; use /proc/[pid]/status instead. 0

(34) sigcatch %lu The bitmap of caught signals, displayed as a decimal number. Obsolete, because it does not provide information on real-time signals; use /proc/[pid]/status instead. 38136

(35) wchan %lu [PT] This is the "channel" in which the process is waiting. It is the address of a location in the kernel where the process is sleeping. The corresponding symbolic name can be found in /proc/[pid]/wchan. 18446744073709551615

(36) nswap %lu always 0

(37) cnswap %lu always 0

(38) exit_signal %d (since Linux 2.1.22) Signal to be sent to parent when we die. 17

(39) processor %d 上次运行在哪个cpu核上(since Linux 2.2.8) CPU number last executed on. 3

(40) rt_priority %u (since Linux 2.5.19) Real-time scheduling priority, a number in the range1 to 99 for processes scheduled under a real-time policy, or 0, for non-real-time processes (see sched_setscheduler(2)). 0

(41) policy %u (since Linux 2.5.19) Scheduling policy (see sched_setscheduler(2)). Decode using the SCHED_* constants in linux/sched.h. 0

(42) delayacct_blkio_ticks %llu (since Linux 2.6.18) Aggregated block I/O delays, measured in clock ticks (centiseconds). 0

(43) guest_time %lu (since Linux 2.6.24) Guest time of the process (time spent running a virtual CPU for a guest operating system), measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). 0

(44) cguest_time %ld (since Linux 2.6.24) Guest time of the process's children, measured in clock ticks (divide by sysconf(_SC_CLK_TCK)). 0

(45) start_data %lu (since Linux 3.3) [PT] Address above which program initialized and uninitialized (BSS) data are placed. 0

(46) end_data %lu (since Linux 3.3) [PT] Address below which program initialized and uninitialized (BSS) data are placed. 0

(47) start_brk %lu (since Linux 3.3) [PT] Address above which program heap can be expanded with brk(2). 0

(48) arg_start %lu (since Linux 3.5) [PT] Address above which program command-line arguments (argv) are placed. 0

(49) arg_end %lu (since Linux 3.5) [PT] Address below program command-line arguments (argv) are placed. 0

(50) env_start %lu (since Linux 3.5) [PT] Address above which program environment is placed. 0

(51) env_end %lu (since Linux 3.5) [PT] Address below which program environment is placed. 0

(52) exit_code %d (since Linux 3.5) [PT] The thread's exit status in the form reported by waitpid(2). 0

上面这些项在kernel中都能找到对应的代码,在fs/proc/array.c的do_task_stat方法中,如下

  1. seq_printf(m, "%d (%s) %c", pid_nr_ns(pid, ns), tcomm, state);
  2. seq_put_decimal_ll(m, ' ', ppid);
  3. seq_put_decimal_ll(m, ' ', pgid);
  4. seq_put_decimal_ll(m, ' ', sid);
  5. seq_put_decimal_ll(m, ' ', tty_nr);
  6. seq_put_decimal_ll(m, ' ', tty_pgrp);
  7. seq_put_decimal_ull(m, ' ', task->flags);
  8. seq_put_decimal_ull(m, ' ', min_flt);
  9. seq_put_decimal_ull(m, ' ', cmin_flt);
  10. seq_put_decimal_ull(m, ' ', maj_flt);
  11. seq_put_decimal_ull(m, ' ', cmaj_flt);
  12. seq_put_decimal_ull(m, ' ', cputime_to_clock_t(utime));
  13. seq_put_decimal_ull(m, ' ', cputime_to_clock_t(stime));
  14. seq_put_decimal_ll(m, ' ', cputime_to_clock_t(cutime));
  15. seq_put_decimal_ll(m, ' ', cputime_to_clock_t(cstime));
  16. seq_put_decimal_ll(m, ' ', priority);
  17. ....

虽然数据项很多,但是我们并不是全都关心,其中只有线程名,pid,优先级,运行在哪个核上,以及当前进程或线程占用的cpu时间这几项是我们所关心的。具体来说,process_total_time = utime + stime + cutime + cstime ,即上面数据项中的(14)~(17)项。由此,我们就很清楚要怎么计算某一进程或线程的CPU使用率了: 在两个时间点分别通过/proc/stat和/proc/[pid]/stat抓取总体CPU数据快照和进程(线程)CPU数据快照,从而计算出total_time_delta和process_time_delta,如果要具体到某一个核上的CPU使用率,则利用/proc/stat也可以计算出core_time_delta,随后利用process_time_delta100%/total_time_delta或process_time_delta100%/core_time_delta即可计算进程(线程)的总体CPU使用率或某一个核上的CPU使用率

1.3 top

 
adb shell top

top提供了CPU数据的实时监视

  1. Usage: top [ -m max_procs ] [ -n iterations ] [ -d delay ] [ -s sort_column ] [ -t ] [ -h ]
  2. -m num Maximum number of processes to display. 最多显示几个进程,top会自动进行排序,比如让CPU占用率高的进程在前
  3. -n num Updates to show before exiting. 刷新次数
  4. -d num Seconds to wait between updates. 刷新间隔,可以输入小数即代表毫秒级间隔
  5. -s col Column to sort by (cpu,vss,rss,thr). 选择以哪一项进行排序
  6. -t Show threads instead of processes. 显示线程
  7. -h Display this help screen.

在手边的电视上运行top -m 5命令,结果如下

  1. User 5%, System 5%, IOW 0%, IRQ 0%
  2. User 70 + Nice 0 + Sys 70 + Idle 1069 + IOW 1 + IRQ 0 + SIRQ 3 = 1213
  3. PID PR CPU% S #THR VSS RSS PCY UID Name
  4. 1728 0 2% S 28 648828K 18764K fg system /system/bin/surfaceflinger
  5. 26366 2 2% S 31 1004812K 134940K fg system com.xxxxxxxxxxx
  6. 1792 0 1% S 61 1640236K 16508K fg root /applications/bin/xxxx
  7. 3906 3 0% S 47 935428K 31300K fg system com.xxxxxxxxxxxx
  8. 25192 1 0% S 60 973844K 36872K bg system com.xxxxxxxxxx

相信此时你一定已经明白最开始两行数据的含义了。接下来的表头项含义如下

  1. PID:略
  2. PR:在android N之前代表运行在哪个核上,在android N上代表优先级,当然可能设备厂商会进行自定义
  3. CPU%:略
  4. S:运行状态
  5. #THR:线程数
  6. VSS:Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
  7. RSS:Resident Set Size 实际使用物理内存(包含共享库占用的内存)
  8. PCY:调度策略优先级,SP_BACKGROUND/SP_FOREGROUND
  9. UID:进程所有者的用户id
  10. Name:进程名

加上-t参数,结果如下

  1. User 2%, System 2%, IOW 0%, IRQ 0%
  2. User 30 + Nice 0 + Sys 33 + Idle 1195 + IOW 0 + IRQ 0 + SIRQ 2 = 1260
  3. PID TID PR CPU% S VSS RSS PCY UID Thread Proc
  4. 29402 29402 2 0% R 4204K 1612K fg shell top top
  5. 1792 2099 1 0% S 1640236K 16508K fg root InitHDMIthread /applications/xxxx
  6. 1039 1039 3 0% S 0K 0K fg root irq/202-scaler
  7. 29395 29395 0 0% S 0K 0K fg root kworker/0:2
  8. 1737 2392 3 0% S 826844K 10920K fg media mediaserver /system/bin/mediaserver

多了TID和Thread表头项,顾名思义。 那么top命令又是如何计算出cpu占用率的呢?想必你已经猜到了,也是通过读取上面的/proc/stat,/proc/[pid]/stat,/proc/[pid]/task/[tid]/stat。查看top的源码,在system/core/toolbox/top.c中可以看到 读取CPU数据部分的代码如下,可以说是非常浅显易懂了

  1. static void read_procs(void) {
  2. DIR *proc_dir, *task_dir;
  3. struct dirent *pid_dir, *tid_dir;
  4. char filename[64];
  5. FILE *file;
  6. int proc_num;
  7. struct proc_info *proc;
  8. pid_t pid, tid;
  9. int i;
  10. proc_dir = opendir("/proc");
  11. if (!proc_dir) die("Could not open /proc.\n");
  12. new_procs = calloc(INIT_PROCS * (threads ? THREAD_MULT : 1), sizeof(struct proc_info *));
  13. num_new_procs = INIT_PROCS * (threads ? THREAD_MULT : 1);
  14. file = fopen("/proc/stat", "r");
  15. if (!file) die("Could not open /proc/stat.\n");
  16. fscanf(file, "cpu %lu %lu %lu %lu %lu %lu %lu", &new_cpu.utime, &new_cpu.ntime, &new_cpu.stime,
  17. &new_cpu.itime, &new_cpu.iowtime, &new_cpu.irqtime, &new_cpu.sirqtime);
  18. fclose(file);
  19. proc_num = 0;
  20. while ((pid_dir = readdir(proc_dir))) {
  21. if (!isdigit(pid_dir->d_name[0]))
  22. continue;
  23. pid = atoi(pid_dir->d_name);
  24. struct proc_info cur_proc;
  25. if (!threads) {
  26. proc = alloc_proc();
  27. proc->pid = proc->tid = pid;
  28. sprintf(filename, "/proc/%d/stat", pid);
  29. read_stat(filename, proc);
  30. sprintf(filename, "/proc/%d/cmdline", pid);
  31. read_cmdline(filename, proc);
  32. sprintf(filename, "/proc/%d/status", pid);
  33. read_status(filename, proc);
  34. read_policy(pid, proc);
  35. proc->num_threads = 0;
  36. } else {
  37. sprintf(filename, "/proc/%d/cmdline", pid);
  38. read_cmdline(filename, &cur_proc);
  39. sprintf(filename, "/proc/%d/status", pid);
  40. read_status(filename, &cur_proc);
  41. proc = NULL;
  42. }
  43. sprintf(filename, "/proc/%d/task", pid);
  44. task_dir = opendir(filename);
  45. if (!task_dir) continue;
  46. while ((tid_dir = readdir(task_dir))) {
  47. if (!isdigit(tid_dir->d_name[0]))
  48. continue;
  49. if (threads) {
  50. tid = atoi(tid_dir->d_name);
  51. proc = alloc_proc();
  52. proc->pid = pid; proc->tid = tid;
  53. sprintf(filename, "/proc/%d/task/%d/stat", pid, tid);
  54. read_stat(filename, proc);
  55. read_policy(tid, proc);
  56. strcpy(proc->name, cur_proc.name);
  57. proc->uid = cur_proc.uid;
  58. proc->gid = cur_proc.gid;
  59. add_proc(proc_num++, proc);
  60. } else {
  61. proc->num_threads++;
  62. }
  63. }
  64. closedir(task_dir);
  65. if (!threads)
  66. add_proc(proc_num++, proc);
  67. }
  68. for (i = proc_num; i < num_new_procs; i++)
  69. new_procs[i] = NULL;
  70. closedir(proc_dir);
  71. }
  72. static int read_stat(char *filename, struct proc_info *proc) {
  73. FILE *file;
  74. char buf[MAX_LINE], *open_paren, *close_paren;
  75. file = fopen(filename, "r");
  76. if (!file) return 1;
  77. fgets(buf, MAX_LINE, file);
  78. fclose(file);
  79. /* Split at first '(' and last ')' to get process name. */
  80. open_paren = strchr(buf, '(');
  81. close_paren = strrchr(buf, ')');
  82. if (!open_paren || !close_paren) return 1;
  83. *open_paren = *close_paren = '\0';
  84. strncpy(proc->tname, open_paren + 1, THREAD_NAME_LEN);
  85. proc->tname[THREAD_NAME_LEN-1] = 0;
  86. /* Scan rest of string. */
  87. sscanf(close_paren + 1,
  88. " %c " "%*d %*d %*d %*d %*d %*d %*d %*d %*d %*d "
  89. "%" SCNu64
  90. "%" SCNu64 "%*d %*d %*d %*d %*d %*d %*d "
  91. "%" SCNu64
  92. "%" SCNu64 "%*d %*d %*d %*d %*d %*d %*d %*d %*d %*d %*d %*d %*d %*d "
  93. "%d",
  94. &proc->state,
  95. &proc->utime,
  96. &proc->stime,
  97. &proc->vss,
  98. &proc->rss,
  99. &proc->prs);
  100. return 0;
  101. }

而计算CPU占用率的方法也和我们前面说的一致,同样在top.c中可以看到,也很浅显易懂

  1. static void print_procs(void) {
  2. int i;
  3. struct proc_info *old_proc, *proc;
  4. long unsigned total_delta_time;
  5. struct passwd *user;
  6. char *user_str, user_buf[20];
  7. for (i = 0; i < num_new_procs; i++) {
  8. if (new_procs[i]) {
  9. old_proc = find_old_proc(new_procs[i]->pid, new_procs[i]->tid);
  10. if (old_proc) {
  11. new_procs[i]->delta_utime = new_procs[i]->utime - old_proc->utime;
  12. new_procs[i]->delta_stime = new_procs[i]->stime - old_proc->stime;
  13. } else {
  14. new_procs[i]->delta_utime = 0;
  15. new_procs[i]->delta_stime = 0;
  16. }
  17. new_procs[i]->delta_time = new_procs[i]->delta_utime + new_procs[i]->delta_stime;
  18. }
  19. }
  20. total_delta_time = (new_cpu.utime + new_cpu.ntime + new_cpu.stime + new_cpu.itime
  21. + new_cpu.iowtime + new_cpu.irqtime + new_cpu.sirqtime)
  22. - (old_cpu.utime + old_cpu.ntime + old_cpu.stime + old_cpu.itime
  23. + old_cpu.iowtime + old_cpu.irqtime + old_cpu.sirqtime);
  24. qsort(new_procs, num_new_procs, sizeof(struct proc_info *), proc_cmp);
  25. printf("\n\n\n");
  26. printf("User %ld%%, System %ld%%, IOW %ld%%, IRQ %ld%%\n",
  27. ((new_cpu.utime + new_cpu.ntime) - (old_cpu.utime + old_cpu.ntime)) * 100 / total_delta_time,
  28. ((new_cpu.stime ) - (old_cpu.stime)) * 100 / total_delta_time,
  29. ((new_cpu.iowtime) - (old_cpu.iowtime)) * 100 / total_delta_time,
  30. ((new_cpu.irqtime + new_cpu.sirqtime)
  31. - (old_cpu.irqtime + old_cpu.sirqtime)) * 100 / total_delta_time);
  32. printf("User %ld + Nice %ld + Sys %ld + Idle %ld + IOW %ld + IRQ %ld + SIRQ %ld = %ld\n",
  33. new_cpu.utime - old_cpu.utime,
  34. new_cpu.ntime - old_cpu.ntime,
  35. new_cpu.stime - old_cpu.stime,
  36. new_cpu.itime - old_cpu.itime,
  37. new_cpu.iowtime - old_cpu.iowtime,
  38. new_cpu.irqtime - old_cpu.irqtime,
  39. new_cpu.sirqtime - old_cpu.sirqtime,
  40. total_delta_time);
  41. printf("\n");
  42. if (!threads)
  43. printf("%5s %2s %4s %1s %5s %7s %7s %3s %-8s %s\n", "PID", "PR", "CPU%", "S", "#THR", "VSS", "RSS", "PCY", "UID", "Name");
  44. else
  45. printf("%5s %5s %2s %4s %1s %7s %7s %3s %-8s %-15s %s\n", "PID", "TID", "PR", "CPU%", "S", "VSS", "RSS", "PCY", "UID", "Thread", "Proc");
  46. for (i = 0; i < num_new_procs; i++) {
  47. proc = new_procs[i];
  48. if (!proc || (max_procs && (i >= max_procs)))
  49. break;
  50. user = getpwuid(proc->uid);
  51. if (user && user->pw_name) {
  52. user_str = user->pw_name;
  53. } else {
  54. snprintf(user_buf, 20, "%d", proc->uid);
  55. user_str = user_buf;
  56. }
  57. if (!threads) {
  58. printf("%5d %2d %3" PRIu64 "%% %c %5d %6" PRIu64 "K %6" PRIu64 "K %3s %-8.8s %s\n",
  59. proc->pid, proc->prs, proc->delta_time * 100 / total_delta_time, proc->state, proc->num_threads,
  60. proc->vss / 1024, proc->rss * getpagesize() / 1024, proc->policy, user_str, proc->name[0] != 0 ? proc->name : proc->tname);
  61. } else {
  62. printf("%5d %5d %2d %3" PRIu64 "%% %c %6" PRIu64 "K %6" PRIu64 "K %3s %-8.8s %-15s %s\n",
  63. proc->pid, proc->tid, proc->prs, proc->delta_time * 100 / total_delta_time, proc->state,
  64. proc->vss / 1024, proc->rss * getpagesize() / 1024, proc->policy, user_str, proc->tname, proc->name);
  65. }
  66. }
  67. }

1.4 dumpsys cpuinfo

 
adb shell dumpsys cpuinfo

在手边的电视上运行的结果如下

  1. Load: 3.18 / 3.42 / 3.49
  2. CPU usage from 1053590ms to 153542ms ago:
  3. 7% 1792/xxx: 2.7% user + 4.3% kernel
  4. 3.3% 1728/surfaceflinger: 2.3% user + 0.9% kernel
  5. 3.1% 26366/com.xxxx: 2.4% user + 0.7% kernel / faults: 197480 minor
  6. 2.1% 25192/com.xxxx: 1.7% user + 0.4% kernel / faults: 28686 minor
  7. 1.7% 2204/system_server: 1.2% user + 0.4% kernel / faults: 4071 minor
  8. ....

dumpsys的原理是利用Binder的dump,如源码所示,在/frameworks/native/cmds/dumpsys/dumpsys.cpp中

  1. sp<IBinder> service = sm->checkService(services[i]);
  2. if (service != NULL) {
  3. if (N > 1) {
  4. aout << "------------------------------------------------------------"
  5. "-------------------" << endl;
  6. aout << "DUMP OF SERVICE " << services[i] << ":" << endl;
  7. }
  8. int err = service->dump(STDOUT_FILENO, args);
  9. if (err != 0) {
  10. aerr << "Error dumping service info: (" << strerror(err)
  11. << ") " << services[i] << endl;
  12. }
  13. } else {
  14. aerr << "Can't find service: " << services[i] << endl;
  15. }

对应到/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

 
  1. if (MONITOR_CPU_USAGE) {
  2. ServiceManager.addService("cpuinfo", new CpuBinder(this));
  3. }

对应的CPUBinder内容如下

  1. static class CpuBinder extends Binder {
  2. ActivityManagerService mActivityManagerService;
  3. CpuBinder(ActivityManagerService activityManagerService) {
  4. mActivityManagerService = activityManagerService;
  5. }
  6. @Override
  7. protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
  8. if (mActivityManagerService.checkCallingPermission(android.Manifest.permission.DUMP)
  9. != PackageManager.PERMISSION_GRANTED) {
  10. pw.println("Permission Denial: can't dump cpuinfo from from pid="
  11. + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
  12. + " without permission " + android.Manifest.permission.DUMP);
  13. return;
  14. }
  15. synchronized (mActivityManagerService.mProcessCpuTracker) {
  16. pw.print(mActivityManagerService.mProcessCpuTracker.printCurrentLoad());
  17. pw.print(mActivityManagerService.mProcessCpuTracker.printCurrentState(
  18. SystemClock.uptimeMillis()));
  19. }
  20. }
  21. }

printCurrenLoad和printCurrentState方法在/frameworks/base/core/java/com/android/internal/os/ProcessCpuTracker.java中可以看到,一样是读取/proc下的内容,比如第一行三个Load值就是读取的/proc/loadavg的前三项,查看man-page即可知其含义,如下

The first three fields in this file are load average figures giving the number of jobs in the run queue (state R) or waiting for disk I/O (state D) averaged over 1, 5, and 15 minutes.

二、写个脚本获取各个线程在各个CPU核上的占用率

通过前面的介绍,我们发现这些方法都只是抓取瞬时CPU占用率数据,而难以方便的持续输出某一个进程或线程在各个CPU核上的占用率数据。此前我们介绍过DS5 StreamLine工具可以完成这一工具,但是有两个问题:一是必须要拿到设备的源码编译出指定的库才能使用StreamLine工具,二是要付费...... 基于这样的背景,我们可以自己写个python脚本,读取/proc下的内容,输出一段时间内某一个进程或线程在各个CPU核上的占用率数据。话不多说,直接上代码,还是比较简单的。在脚本开头简单描述了设计思路

  1. #!/usr/bin/python
  2. #coding:utf-8
  3. ###
  4. # This script can calculate cpu usage percentage on each core of a process's threads
  5. # Author:
  6. # zhang hui <zhanghuicuc@gmail.com>
  7. # Communication University of China
  8. ###Basic Design Idea is as follows:
  9. '''
  10. input pid
  11. list threads by ls /proc/pid/task
  12. struct thread_info{
  13. thread.name
  14. thread.priority
  15. thread.cpudata
  16. }
  17. struct thread.cpudata{
  18. core0_percentage[]
  19. core1_percentage[]
  20. core2_percentage[]
  21. core3_percentage[]
  22. }
  23. for i in threads{
  24. calculate_core_percentage(){
  25. running core has data, other core's data is 0
  26. }
  27. }
  28. for i in threads{
  29. write plot data
  30. }
  31. '''
  32. import os
  33. import sys
  34. import subprocess
  35. import time
  36. import commands
  37. from optparse import OptionParser
  38. from time import sleep
  39. from subprocess import check_output, CalledProcessError
  40. global Options
  41. global Pid
  42. global Interval
  43. Threads = []
  44. class Cpudata:
  45. def __init__(self):
  46. self.percents = []
  47. self.core0_percent = []
  48. self.core1_percent = []
  49. self.core2_percent = []
  50. self.core3_percent = []
  51. self.percents.append(self.core0_percent)
  52. self.percents.append(self.core1_percent)
  53. self.percents.append(self.core2_percent)
  54. self.percents.append(self.core3_percent)
  55. self.proc_utime_old = 0
  56. self.proc_stime_old = 0
  57. self.proc_utime_new = 0
  58. self.proc_stime_new = 0
  59. self.proc_time_delta = 0
  60. self.cpu_times_old = []
  61. self.cpu_times_new = []
  62. def addData(self, coreID, percent):
  63. self.percents[coreID].append(percent)
  64. def getData(self, coreID):
  65. return self.percents[coreID]
  66. def getProcUtimeOld(self):
  67. return self.proc_utime_old
  68. def getProcStimeOld(self):
  69. return self.proc_stime_old
  70. def getProcUtimeNew(self):
  71. return self.proc_utime_new
  72. def getProcStimeNew(self):
  73. return self.proc_stime_new
  74. def getCpuTimesOld(self):
  75. return self.cpu_times_old
  76. def getCpuTimesNew(self):
  77. return self.cpu_times_new
  78. def getProcTimeDelta(self):
  79. return self.proc_time_delta
  80. def getPercents(self):
  81. return self.percents
  82. def setProcUtimeOld(self, utime):
  83. self.proc_utime_old = utime
  84. def setProcStimeOld(self, stime):
  85. self.proc_stime_old = stime
  86. def setProcUtimeNew(self, utime):
  87. self.proc_utime_new = utime
  88. def setProcStimeNew(self, stime):
  89. self.proc_stime_new = stime
  90. def setCpuTimesOld(self, cputimes):
  91. self.cpu_times_old = cputimes
  92. def setCpuTimesNew(self, cputimes):
  93. self.cpu_times_new = cputimes
  94. def calProcTimeDelta(self):
  95. new = (int)(self.proc_utime_new) + (int)(self.proc_stime_new)
  96. old = (int)(self.proc_utime_old) + (int)(self.proc_stime_old)
  97. self.proc_time_delta = new - old
  98. def calPercentage(self, which_cpu):
  99. cpu_time_delta = self.cpu_times_new[which_cpu] - self.cpu_times_old[which_cpu]
  100. if cpu_time_delta == 0:
  101. percentage = 0
  102. else:
  103. percentage = self.proc_time_delta * 100 / cpu_time_delta
  104. for i in range(0,4):
  105. if i == which_cpu:
  106. self.percents[i].append(percentage)
  107. else:
  108. self.percents[i].append(0)
  109. class Thread:
  110. def __init__(self, tid):
  111. self.tid = tid
  112. self.name = ''
  113. self.priority = 0
  114. self.cpudata = Cpudata()
  115. def getName(self):
  116. return self.name
  117. def getTid(self):
  118. return (int)(self.tid)
  119. def getPrio(self):
  120. return self.priority
  121. def getCpudata(self):
  122. return self.cpudata
  123. def setName(self, name):
  124. self.name = name
  125. def run_command(options, cmd):
  126. if options.debug:
  127. print 'COMMAND: ', cmd
  128. try:
  129. out_bytes = subprocess.check_output(cmd, shell=True)
  130. out_text = out_bytes.decode('utf-8')
  131. return out_text
  132. except CalledProcessError, e:
  133. message = "binary tool failed with error %d" % e.returncode
  134. if options.verbose:
  135. message += " - " + str(cmd)
  136. raise Exception(message)
  137. def list_threads(options, pid):
  138. cmd = 'adb shell ls /proc/' + pid + '/task'
  139. result = run_command(options, cmd)
  140. result = result.rstrip()
  141. tids = result.split('\n')
  142. for i in range(len(tids)):
  143. thread = Thread((int)(tids[i]))
  144. Threads.append(thread)
  145. def cal_percent(options, pid, thread, cputimes):
  146. cmd = 'adb shell cat /proc/%d/task/%d/stat' % ((int)(pid), thread.getTid())
  147. #something like:
  148. #18368 (Loader:HlsSampl) S 1761 1760 0 0 -1 1077936192 5301 5158 0 0 936 475 4 0 39 19 44 0 1562463 1030127616 23832 18446744073709551615 2871582720 2871600504 4292793808 3748864648 4141267120 0 4612 0 38136 18446743798832713004 0 0 -1 1 0 0 0 0 0 2871606488 2871607296 2875428864 4292795327 4292795414 4292795414 4292796382 0
  149. result = run_command(options, cmd)
  150. datas = result.split(' ')
  151. thread.setName(datas[1])
  152. which_cpu = (int)(datas[38])
  153. if thread.getCpudata().getProcUtimeOld() == 0 or thread.getCpudata().getProcStimeOld() == 0:
  154. thread.getCpudata().setProcUtimeOld((int)(datas[13]))
  155. thread.getCpudata().setProcStimeOld((int)(datas[14]))
  156. thread.getCpudata().setCpuTimesOld(cputimes)
  157. else:
  158. thread.getCpudata().setProcUtimeNew((int)(datas[13]))
  159. thread.getCpudata().setProcStimeNew((int)(datas[14]))
  160. thread.getCpudata().calProcTimeDelta()
  161. thread.getCpudata().setCpuTimesNew(cputimes)
  162. thread.getCpudata().calPercentage(which_cpu)
  163. thread.getCpudata().setProcUtimeOld(thread.getCpudata().getProcUtimeNew())
  164. thread.getCpudata().setProcStimeOld(thread.getCpudata().getProcStimeNew())
  165. thread.getCpudata().setCpuTimesOld(thread.getCpudata().getCpuTimesNew())
  166. def get_cputime(options):
  167. cmd = 'adb shell cat /proc/stat'
  168. #something like:
  169. #cpu1 9167 717 5062 11946 132 0 12 0 0 0
  170. result = run_command(options, cmd)#subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout
  171. cpu_raw_datas = result.split('\n')
  172. cpu_times = []
  173. for i in range (1,5):
  174. cpu_info = cpu_raw_datas[i].split(' ')
  175. cpu_one_core_total_time = 0
  176. for j in range (1,8):
  177. cpu_one_core_total_time += (int)(cpu_info[j])
  178. cpu_times.append(cpu_one_core_total_time)
  179. return cpu_times
  180. def draw_plot(options):
  181. for i in range(len(Threads)):
  182. print 'ThreadName: ', Threads[i].getName()
  183. print 'Cpu#0: ',
  184. print Threads[i].getCpudata().getPercents()[0]
  185. print 'Cpu#1: ',
  186. print Threads[i].getCpudata().getPercents()[1]
  187. print 'Cpu#2: ',
  188. print Threads[i].getCpudata().getPercents()[2]
  189. print 'Cpu#3: ',
  190. print Threads[i].getCpudata().getPercents()[3]
  191. if __name__=='__main__':
  192. parser = OptionParser(usage="%prog -d -p pid -t interval")
  193. parser.add_option('-d', '--debug', dest="debug", action='store_true', default=False,
  194. help="Print out debugging information")
  195. parser.add_option('-p', '--pid', dest="process_id",
  196. help="Process id")
  197. parser.add_option('-t', '--interval', dest="time_interval",
  198. help="Time interval for data collecting, in seconds ex.(0.1 means 100ms)")
  199. (options, args) = parser.parse_args()
  200. if options.process_id:
  201. Pid = options.process_id
  202. if options.time_interval:
  203. Interval = (float)(options.time_interval)
  204. list_threads(options, Pid)
  205. print 'start collecting data...'
  206. while True:
  207. try:
  208. CpuTimes = get_cputime(options)
  209. for i in range(len(Threads)):
  210. cal_percent(options, Pid, Threads[i], CpuTimes)
  211. sleep(Interval)
  212. except KeyboardInterrupt:
  213. print 'stop collecting data...'
  214. print 'start generating report...'
  215. draw_plot(options)
  216. print 'report exported'
  217. sys.exit("Finished")

以我手边的电视为例,adb connect之后,看到电视上面有一个pid=22868的进程,则输入下面的命令

 
./CpuWatcher.py -p 22868 -t 0.01

即可以10ms为间隔记录该进程下所有线程在各个cpu核上的占用率结果,记录一段时间后按Ctrl+c退出记录,则自动打印出记录结果,如下

  1. start collecting data...
  2. ^Cstop collecting data...
  3. start generating report...
  4. ThreadName: (ExoPlayerImplIn)
  5. Cpu#0: [0, 0, 0, 0, 0, 0, 26, 0, 27, 23, 0, 0, 0, 0, 0, 0, 0, 23, 26, 0, 0, 0, 0, 20, 0, 17, 0, 0, 0, 0, 20, 0, 0, 0, 0]
  6. Cpu#1: [22, 23, 29, 25, 0, 0, 0, 0, 0, 0, 26, 0, 23, 0, 23, 24, 0, 0, 0, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 25, 27]
  7. Cpu#2: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 22, 0, 0, 0, 0, 0, 0, 0, 24, 0, 0]
  8. Cpu#3: [0, 0, 0, 0, 24, 24, 0, 28, 0, 0, 0, 25, 0, 25, 0, 0, 26, 0, 0, 21, 0, 0, 0, 0, 0, 0, 21, 21, 22, 20, 0, 0, 0, 0, 0]
  9. ThreadName: (Loader:HlsSampl)
  10. Cpu#0: [0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 13, 0, 0, 0, 0, 0]
  11. Cpu#1: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 17, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 0]
  12. Cpu#2: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  13. Cpu#3: [13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 13, 14, 0, 0, 0, 16, 0, 14, 0, 0, 0, 0]
  14. ThreadName: (MediaCodec_loop)
  15. Cpu#0: [4, 4, 0, 5, 2, 0, 0, 3, 0, 0, 0, 0, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0, 3, 0, 4, 3, 0, 4, 3, 0, 4, 0, 0, 0, 3]
  16. Cpu#1: [0, 0, 0, 0, 0, 4, 5, 0, 3, 0, 3, 4, 0, 4, 4, 4, 0, 0, 0, 3, 3, 4, 0, 0, 0, 0, 0, 0, 0, 3, 0, 4, 0, 4, 0]
  17. Cpu#2: [0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  18. Cpu#3: [0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0]
  19. ....
  20. report exported
  21. Finished

这个脚本因为使用adb shell与设备通信,所以可以不用像StreamLine那样必须拿到设备源代码才能使用,但也因此效率偏低,记录的数据的时间间隔做不到很小,是未来会进行改进的点。

作者:zhanghui_cuc
链接:https://juejin.cn/post/7061788020134920206
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/773183
推荐阅读
相关标签
  

闽ICP备14008679号