高版本采集系统、进程CPU使用率的方式

背景
cpu 信息作为性能监控四大常用指标 (cpu、内存、网络、磁盘)之一,对衡量设备性能,分析、确定一些线上性能问题有着较为重要的作用。

这里举个应用场景: 在得物APM平台的页面慢启动监控中,样本存在慢启动对应的函数火焰图中并未提取到明显耗时函数的情况,但通过分析其启动阶段的CPU使用率信息、CPU频率信息,发现其系统CPU使用率极高,而进程、主线程CPU使用率并不高,启动阶段主要的资源使用是cpu 及 io, 因为未存在IO阻塞函数,因此可以将此类问题归因为系统CPU负载高、主线程未分配到充足时间片来执行启动阶段的函数,因此导致页面慢启动问题。

 

计算系统CPU使用率
/proc 及 /sys 伪文件系统
在介绍具体实现时,需要先了解 一些关键的伪文件系统(pseudo filesystems),如procfs、sysfs。它们以文件的方式为内核与进程提供通信的接口,但其信息只存在于内存当中,而不占用外存空间。 用户和程序可以通过 读取 这些文件下的 目录及子目录下的相关文件得到系统提供的一些资源信息,我们所知道的 ps、top、free等程序底层实现也是通过读取这些fs来获取系统的相关信息的。

举个例子,比如在/proc/cpuinfo 文件提供了每个cpu的相关信息(型号、缓存大小等)。
 

processor	: 0
BogoMIPS	: 38.40
Features	: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer	: 0x41
CPU architecture: 8
CPU variant	: 0x2
CPU part	: 0xd05
CPU revision	: 0

processor	: 1
BogoMIPS	: 38.40
Features	: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp
CPU implementer	: 0x41
CPU architecture: 8
CPU variant	: 0x2
CPU part	: 0xd05
CPU revision	: 0

 /proc/stat文件提供了所有CPU的活动信息,该文件中的所有数值都是从系统启动开始累计到当前时刻

cpu  60174457 9663009 55832451 71782723 217812 9886952 2586380 0 0 0
cpu0 11196635 2001943 11939773 68088651 212914 2441300 665882 0 0 0
cpu1 11507874 2276717 11213445 436700 1056 2143323 556399 0 0 0
cpu2 11412154 2242954 11019498 440953 1110 2136361 523968 0 0 0
cpu3 4944155 744241 8900551 498496 1205 1987431 635147 0 0 0
cpu4 6428646 540160 3749526 564847 464 368445 63627 0 0 0
cpu5 6457629 570385 3797990 568408 499 369781 58906 0 0 0
cpu6 6400553 560137 3879073 567166 417 370833 57719 0 0 0
cpu7 1826808 726469 1332591 617497 144 69475 24730 0 0 0
intr 5891780796 0 0 0 1237792393 0 0 0 0 0 228957851 5575 4061 1310143 0 77591952 0 1928 1425 164383405 0 0 294267 0 17817217 14494461 90294 0 0 0 0 0 0 0 0 0 180421 41024658 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 0 0 0 0 1522 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 124 800439 0 0 0 0 0 0 0 0 0 0 0 0 47816585 614983 189532 0 0 0 0 0 0 0 0 0 0 0 0 2568146 62612 15144952 148716 211433 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 49109042 0 0 67340327 0 0 0 0 0 0 0 0 264827 32197 0 2 207 214 81540 48 2384 81 2165 19707 716458 0 2 0 2 355498 421 0 0 0 0 1788049 6956 2428 0 0 2007479 0 5997 0 0 0 0 0 0 0 10 0 0 0 0 0 0 0 0 0 3 673471 3 59 0 200861 1 6 11270883 0 0 1 0 1 0 1 0 1 0 1 0 1 42 9 10 0 0 10 1967 152466 3854 67 0 0 2354590 0 0 0 0 0 0 14536775 0 15033 142 395394 338959 60 903 12122483 1289409 0 0 0 0 0 0 32346 4617898 2602221 2081525 2177844 0 0 1140925 0 0 0 0 0 0 0 0 0 0 0 407084 46869118 32724648 785 2 0 0 0 0 0 0 0 0 0 0 0 0 4943944
ctxt 8547174056
btime 1658839741
processes 9876338
procs_running 1
procs_blocked 0
softirq 1499737914 1595873 498961882 9622280 70242366 145843255 0 15928568 411156254 37489 346349947

对于进程来说,通过/proc/${pid}/ 目录下提供的文件,可以获取单个进程的一些统计信息

除了procfs,另一个常用的文件系统是 sysfs,其根目录为/sys。 sysfs 是在procfs之后引入的,它将很多原本存在于procfs的信息迁移到sysfs,sysfs被设计用来导出设备树中呈现的信息,这样就不会使得 /procfs文件显得混乱。se有一个procfs sysfs相关区别问题的讨论,可以了解下: https://unix.stackexchange.com/questions/4884/what-is-the-difference-between-procfs-and-sysfs。

在本文中,主要关注 /sys/devices/system/cpu/ 目录下的信息,它提供了cpu的一些详细配置及活动信息,如 cpu最小、最大频率、cpu各频率活动时间、cpu idle累计时间等。

示例:读取 cpu${index}/cpufreq/scaling_cur_freq 可以获取某个cpu当前的工作频率

mars:/ $ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
1209600


Android 8.0及以下
在Android低版本设备中,可以通过读取 /proc/stat 文件实现, /proc/stat 内容首行有8个数值, 分别提供了所有CPU在 用户态(user)、用户态-低优先级(nice)、内核态(sys)、空闲态(idle)、io等待(iowait)、硬中断(irq)、软中断(softirq) 状态 下的时间总和,将这些值累加作为系统总的CPU时间(cpuTime),计算 iowait/cpuTime 为系统的CPU空闲率,1-cpu空闲率 及为cpu利用率 。注意这里的时间单位为 jiffies,通常一个jiffies 等于10ms。 在Android 系统下也可以通过Os.sysconf(OsConstants._SC_CLK_TCK) 得到每秒的jiffies数。

Android 高版本实现方案
在Android 8.0以上版本,为了防止旁路攻击(Side Channel Attack),相关讨论可见 https://issuetracker.google.com/issues/37140047, 普通应用程序已经无法访问/proc/stat 文件,所以无法通过/proc/stat 的方式计算系统cpu利用率。
另外说明下,部分线下性能监控相关的开源库 如Dokit 会在Android8.0以上的设备 通过执行shell 命令 top -n 1 来直接获取某个进程CPU使用率信息,不过这种方式在 release环境是无法使用的。
 

   private float getCpuDataForO() {
        java.lang.Process process = null;
        try {
  	        //调用shell 执行 top -n 1
            process = Runtime.getRuntime().exec("top -n 1");
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            int cpuIndex = -1;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (TextUtils.isEmpty(line)) {
                    continue;
                }
                int tempIndex = getCPUIndex(line);
                if (tempIndex != -1) {
                    cpuIndex = tempIndex;
                    continue;
                }
                if (line.startsWith(String.valueOf(Process.myPid()))) {
                    if (cpuIndex == -1) {
                        continue;
                    }
                    String[] param = line.split("\s+");
                    if (param.length <= cpuIndex) {
                        continue;
                    }
                    String cpu = param[cpuIndex];
                    if (cpu.endsWith("%")) {
                        cpu = cpu.substring(0, cpu.lastIndexOf("%"));
                    }
                    float rate = Float.parseFloat(cpu) / Runtime.getRuntime().availableProcessors();
                    return rate;
                }
            }
        } catch (IOException e) {
           //...
        } finally {
          	//...
        }
        return 0;
    }

 

计算系统cpu利用率关键是获取cpu时间 及idle 时间。下面介绍另一种获取cputime 及 idletime的方式.

获取cputime
在 /sys/devices/system/cpu/cpu[x]/cpufreq/stats/ 目录下包含一些提供cpu频率相关的统计信息, 关于该目录下文件的具体说明可参考kernel文档:https://www.kernel.org/doc/Documentation/cpu-freq/cpufreq-stats.txt,

knight-zxw:/ $ ls /sys/devices/system/cpu/cpu0/cpufreq/stats/
reset  time_in_state  total_trans  trans_table


其中 time_in_state 提供了cpu 在每个频率下的运行时间 (单位为10ms),该文件内容格式为多行文本,每行左侧为频率值、右侧为在该频率下运行的时间。
 

knight-zxw:/ $ cat /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state
300000 0
403200 0
499200 0
595200 0
691200 55897525
806400 2729597
902400 1315020
998400 1019161
1094400 11892764
1209600 3945629
1305600 6093815
1401600 1252173
1497600 1166578
1612800 1782695
1708800 978913
1804800 20824808

我们将文件内容 每行右边的数值累加 便是 cpuX 当前的运行时间。 因为一个设备可能包含多个cpu 所以需要读取多个文件进行累加。

除了读取 /sys/devices/system/cpu/cpu[X]/cpufreq/stats/time_in_state 文件,还有一个方式是读取
/sys/devices/system/cpu/cpufreq/policy[X]/stats/time_in_state, 其文件内容一致。
关于 sysfs policy 相关内容可阅读linux 文档:https://www.kernel.org/doc/html/v4.14/admin-guide/pm/cpufreq.html。 这里简单描述下 /sys/devices/system/cpu/cpufreq/policy[X]/ 表示一个cpu频率策略,通常每个policy 控制多个cpu,同时进行频率控制(这些cpu硬件配置一般也相同)。以我的8核手机设备为例(目前Android的主流设备一般都包含8个核心),会提供 policy0、policy4、policy7 (通常分别控制 大中小核),每个policy控制的cpu可以通过 /sys/devices/system/cpu/cpufreq/policy[x]/affected_cpus 读取:

knight-zxw:/ $ cat /sys/devices/system/cpu/cpufreq/policy0/affected_cpus
0 1 2 3


该policy的最大频率、最小频率、当前频率 可以分别通过 scaling_max_freq、scaling_min_freq、scaling_cur_freq获取。 因为policy下提供的 time_in_state 对应的是多个cpu的,因此读取 policy的 time_in_state相对 之前的方式 可以读取更少的文件。

获取 idleTime
同频率相关的统计信息类似,在 /sys/devices/system/cpu/cpu[X]/cpuidle 下提供了每个cpu 在idle状态下运行相关的统计信息,具体信息见文档:https://www.kernel.org/doc/Documentation/cpuidle/sysfs.txt。
cpuidle目录下的子目录通常包含 driver 、以及多个 state[x]文件夹。

knight-zxw:/ $ ls /sys/devices/system/cpu/cpu0/cpuidle/
driver  state0  state1


这里的 state[x] 表示idle状态 休眠的深度,x值越大表示休眠状态越深,功耗越小、但进入和退出该状态的成本也越大。 以不同状态的延迟度为例,在我的设备上 state0的延迟度为43, 而state1 的延迟度为531。

knight-zxw:/ $ cat /sys/devices/system/cpu/cpu0/cpuidle/state0/latency
43
knight-zxw:/ $ cat /sys/devices/system/cpu/cpu0/cpuidle/state1/latency
531


回到正文,/sys/devices/system/cpu/cpu7/cpuidle/state[x]/time 提供了 cpu 在 idle state[x] 下停留的时间,因此通过累加 /sys/devices/system/cpu/cpu[x]/cpuidle/state[x]/time 的值即可获取 系统在idle状态的运行时间(注意 这里的时间单位是微秒)

knight-zxw:/ $ cat /sys/devices/system/cpu/cpu0/cpuidle/state0/time
429942749686


而在 /sys/devices/system/cpu/cpu1/cpuidle/state1/usage下 记录了这个状态进入的次数

knight-zxw:/ $ cat /sys/devices/system/cpu/cpu1/cpuidle/state0/usage
555273877


idle 计算调整
上节讲了idle的计算方式,在测试过程中 马上就发现了一些问题:

在采样周期内(如果较短) time并不一定会被更新,有可能会经过几个采样周期才会更新一次。 因此,如果没有注意到这个场景,在采样周期内 因为 cpu的 idle time值不变而认为当前cpu处于100%使用就是错误的,其实 cpu 是 100% 空闲的,因此计算出的cpu使用率可能比实际高。
同样的问题, 因为time 可能在几个采样周期后才更新,计算出的idle值 可能超过 这个采样周期内对应的cputime,因此如果没有正确处理这个场景的,这个采样周期计算出的idle时间过大,cpu使用率比实际低。
上面的问题,如果采样周期足够长(比如10s采样一次)通常不会有问题,有误差也在可接受范围内,如果是1s采样一次就会偏差很大。下面简单描述下这个问题的简单处理方案:

每次采样时,同时采集当前cpu的频率,当发现某个cpu在采样间隔内 idle time时间没有变化的时候,判断当前cpu是否处于最高频率下工作,如果是最高频率则无需调整,如果不是则 调整idletime 为采样周期的时间 (理论上也可以通过读取 state[x]/usage 计算cpu 进入idle状态的次数来判断,个人暂未验证)
针对第二个问题,如果采样时 计算出的 CPU idle time 大于 采样周期的时间,则将idle 调整为 采样周期的时间,即认为在这个周期内该cpu完全处于 idle状态。
 

public fun getSysIdleDeltaTime(
        allCpu: List<Cpu>,
        intervalMills: Long,
    ): Long {
        //采样间隔 微妙
        val realSampleIntervalMicros = intervalMills * 1000L;
        // 返回的 采样周期内的 idle时间
        var totalIdleDeltaTime = 0L
        for (cpu in allCpu) {
            //获取cpu当前最新的idle时间
            val nowIdleTime = cpu.idleTime()
            //获取上一次记录的该cpu idle时间
            val lastIdleTime = lastCpuIdleTimes[cpu.cpuIndex]
            lastCpuIdleTimes[cpu.cpuIndex] = nowIdleTime
            //第一次调用,只更新数据,直接跳过
            if (lastIdleTime == null) {
                continue
            }
            var deltaIdleTime = (nowIdleTime - lastIdleTime)

            if (deltaIdleTime == 0L) { //间隔采样区间内idle时间为0, 判断是CPU 100% use 还是 100% idle
                //判断当前CPU是否处于基本满频运行
                var maxFreq = 0L
                //当前调频频率
                val scalingCurFreq = cpu.cpuFreq.scalingCurFreq()
                if (!allowReadScalingMaxFeqFile) {
                    maxFreq = cpu.cpuFreq.maxFreq()
                } else {
                    try {
                        //读取当前的频率
                        maxFreq = cpu.cpuFreq.scalingMaxFreq()
                    } catch (e: Exception) {
                        //部分机型出现过读取失败的问题,未确认原因
                        allowReadScalingMaxFeqFile = false
                        maxFreq = cpu.cpuFreq.maxFreq()
                    }
                }

                //当前是否运行在最高频
                val isRunningAtMaxFreq =  maxFreq == scalingCurFreq
                if (!isRunningAtMaxFreq) {
                    deltaIdleTime = realSampleIntervalMicros
                }
            } else if ((deltaIdleTime) > realSampleIntervalMicros) {
                //通常idle时间过长 基本是刚从idle状态退出,此时只能容错取采样周期作为idle时长
                //因此本次采样周期的CPU使用率和实际情况有细微差别
                deltaIdleTime = realSampleIntervalMicros
            }
            totalIdleDeltaTime += deltaIdleTime

        }
        return totalIdleDeltaTime / 1000;
    }

计算 CPU SPEED
除了计算CPU利用率,我们也可以采集cpu frequency 统计CPU频率相关的信息。
可以以cpu cluster为单位统计,在 /sys/devices/system/cpu/cpufreq/policy[x]/ 包含以下文件或目录

knight-zxw:/ $ ls /sys/devices/system/cpu/cpufreq/policy0/
affected_cpus     cpuinfo_min_freq            scaling_available_frequencies  scaling_cur_freq  scaling_max_freq  schedutil
cpuinfo_cur_freq  cpuinfo_transition_latency  scaling_available_governors    scaling_driver    scaling_min_freq  stats
cpuinfo_max_freq  related_cpus                scaling_boost_frequencies      scaling_governor  scaling_setspeed


这里 cpuinfo_xxx 文件表示这些cpu硬件上支持的频率信息( 最小频率、最大频率、当前频率), 而 scaling_xxx 表示当前CPUFreq系统用相应调频驱动及策略进行调节时所支持的频率信息, scaling_driver 表示当前的调频驱动,scaling_governor表示当前调频策略,比如目前常见的 schedutil governor。

系统出于一些性能上考虑,普通应用程序是无法读取 cpuinfo_cur_freq文件 ,在Java层直接读取该文件会抛出FileNotFoundException ,而 cpuinfo_min_freq 和 cpuinfo_max_freq 可以正常读取,毕竟这些值是不变的。
因此计算cpu的当前频率使用情况 可以累加所有cpu的 scaling_cur_freq (单位为kHz)数值得出, cpu利用率百分比 可以通过 scaling_cur_freq/cpuinfo_max_freq 得出。

计算进/线程CPU使用率
通过读取/proc/p i d / s t a t 文件可以获取进程的 C P U 使用信息 , A n d r o i d 应用当前进程的 p i d 可以通过 P r o c e s s . m y P i d ( ) 获取。在我的 A n d r o i d 12 版本设备下 / p r o c / {pid}/stat 文件可以获取进程的CPU使用信息, Android应用当前进程的pid 可以通过Process.myPid()获取。 在我的Android 12版本设备下 /proc/pid/stat文件可以获取进程的CPU使用信息,Android应用当前进程的pid可以通过Process.myPid()获取。在我的Android12版本设备下/proc/{pid}/stat的文件内容如下
 

14330 (uapp.apm.sample) S 845 845 0 0 -1 1077936448 34734 812 29 0 261 55 1 2 10 -10 41 0 181359922 7155191808 36467 18446744073709551615 1 1 0 0 0 0 4608 1 1073775868 0 0 0 17 1 0 0 0 0 0 0 0 0 0 0 0 0 0

因为不同的Android系统 对应linux内核版本不同,因此该文件的内容包含的信息也可能不同(一般是在后面新增一些信息),在linux3.5及以上一共包含52项信息。下面列出前25项信息的含义
按照顺序其每个token表示的信息为:
(1) pid: 进程ID
(2) comm: 以圆括号包裹的程序名, 超过TASK_COMM_LEN(通常为16个字符)的名称会被截断
(3) state: 进程状态, R表示Running、S 表示Sleeping in an interruptible wait ,详见 文档 /proc/[pid]/stat 部分
(4) ppid: 父进程ID
(5) pgrp: 进程组ID
(6) session: 进程会话组ID
(7) tty_nr: The controlling terminal of the process.
(8) tpgid: The ID of the foreground process group of the controlling terminal of the process.
(9) flags: 当前进程内核标识位
(10) minflt: 次要缺页中断次数 (次要缺页表是无需从磁盘加载内存页)
(11) cminflt: 当前进程等待子进程的minflt
(12) majflt: 主要缺页中断次数 (主要缺页表是需要从磁盘加载内存页)
(13) cmajflt: 当前进程等待子进程的majflt
(14) utime: 当前进程处于用户态运行的时间,单位为jiffies
(15) stime: 当前进程处于内核内运行的时间,单位为jiffies
(16) cutime: 当前进程的所有子进程(包括子进程的子进程)在内核态执行的时间
(17) cstime: 当前进程的所有子进程(包括子进程的子进程)在内核态执行的时间
(18) priority: 动态优先级, 值是由系统分析之后动态调整的,用户不能直接修改
(19) nice: 静态优先级,nice值取值范围[19,-20], 值越小表时优先级越高
(20) num_threads: 线程个数
(21) itrealvalue: 内核2.6.17后废弃,值恒为0
(22) starttime: 自系统启动后的进程创建时间
(23) vsize: 进程的虚拟内存大小,单位为bytes
(24) rss: 进程独占+共享库的内存页数
(25) rsslim: rss大小上线

通过解析该文本 获得 utime stime 计算 (utime+stime)/cputime ,即可得出进程的CPU使用率。

// 解析 procstat文件
fun readProcStatSummary(statFile: File): ProcStatSummary {
            val procStatSummary = ProcStatSummary()
            val statInfo = statFile.readText()
            val segments = StringUtil.splitWorker(statInfo, ' ', false)
            procStatSummary.pid = segments[0]
            if (segments[1].endsWith(")")) {
                procStatSummary.name = segments[1].substring(1, segments[1].length - 1)
            }
            procStatSummary.state = segments[2]
            procStatSummary.utime = segments[13].toLong()
            procStatSummary.stime = segments[14].toLong()
            procStatSummary.cutime = segments[15].toLong()
            procStatSummary.cstime = segments[16].toLong()
            procStatSummary.nice = segments[18]
            procStatSummary.numThreads = segments[19].toInt()
            procStatSummary.vsize = segments[22].toLong()
            return procStatSummary
}

// 通过 两次采样 计算采样间隔内的 进程cpu使用率
fun calculateProcCpuUsage(prevProcStateSummary: ProcStatSummary, nowProcStateSummary: ProcStatSummary) {
    procUsedCpuTimeMs = nowProcStateSummary.totalUsedCpuTimeMs - prevProcStateSummary.totalUsedCpuTimeMs
    if (cpuTime > 0) {
        procCpuUsage = procUsedCpuTimeMs.toFloat() / cpuTime
    }
}

在/proc/[processId]/task 包含子线程相关的信息,在Java平台每个Java Thread都对应一个真实的系统线程,遍历 task目录,内容如下:

knight-zxw:/proc/4348/task $ ls
4348  4357  4359  4361  4363  4365  4368  4383  4406  4444  4465  4477  4480  4482  4487  4490  4495  4501
4356  4358  4360  4362  4364  4367  4378  4386  4411  4463  4476  4478  4481  4484  4489  4491  4498

这里每个文件夹都对应进程4348创建的一个子线程,其名称为线程的 系统thread id, 在 task/[tid]/目录下同样也包含 stat文件 记录该线程资源使用相关的统计信息

2|mars:/proc/4348/task $ cat /proc/4348/task/4357/stat
4357 (perfetto_hprof_) S 845 845 0 0 -1 4194368 9 2670 0 1 0 0 5 21 0 -20 30 0 198399608 7024758784 35104 18446744073709551615 1 1 0 0 0 0 20996 1 1073775868 0 0 0 -1 3 0 0 0 0 0 0 0 0 0 0 0 0 0

一些注意事项
兼容性问题
不同的厂商机型可能存在一些权限问题,因此在进行 cpu使用率监控模块采样线程运行前,应先测试相应文件是否能正常读取,如果不能正常读取则判断为当前设备不支持。 在APM系统中,通常也是抽样进行监控,因此部分设备不支持不影响整体度量指标的有效度。

性能优化
在对文件文本内容进行解析时取值时,尽量不要采用正则匹配的方式 (比如调用 String.split(" ")), 比如使用StringTokenier 比 Stirng.split 分词性能会好几倍, 如果你需要采集所有的线程使用率信息,这个成本就会被放大了,因为大型APP 运行时可能包含 几百个线程 (各种三方库内部会创建线程、线程池)。
在android 系统源码中也存在 解析 procstat等文件的代码,我们可以参考其实现 如: ProcStatsUtil、ProcTimeInStateReader

参考资料
kernel doc :https://www.kernel.org/doc/html/v4.14/admin-guide/pm/cpufreq.html
CPU调速器schedutil原理分析:https://deepinout.com/android-system-analysis/android-cpu-related/principle-analysis-of-cpu-governor-schedutil.html
 


————————————————
版权声明:本文为CSDN博主「卓修武」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhuoxiuwu/article/details/126507865

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值