1.Perf工具概览

linux中包含了众多性能分析工具,perf(特指linux-tools perf)工具是2009年在linux内核2.6.31中引入的一个工具。它的主要功能是可以跟踪hardware performance counter(PMU)、tracepoints、software performance counter(hrtimer)、dynamic probes等信息。linux内核将这些信息进行封装,通过syscall(perf_event_open等)的形式提供,使之抽象为events的概念,可以供userspace的进程使用。perf作为一个linux下的命令行工具,可以读取这些events,并结合性能分析的场景,提供了诸如stats、top、record、report等子工具命令,适配更细化的分析需求。

Android中一般使用的并不是老牌的linux-tools perf工具,而是使用经过Android客制化的perf工具,用于支持Android中拓展的一些feature。

  • simpleperf:

Android最早于Android 6.0(2015年)中引入,距今(2022年)已经有7年的历史。其主要作用就是实现Linux中perf工具的基本功能。

  • traced_perf:

Google于2019[在2019年开始开发][修改了一下]年开始开发,其作为perfetto的一个consumer而不是单独的一个项目去开发的。其开发目的是能够:

a.利用perfetto的成熟平台,提供profiling、unwinding、UI等各方面的能力

b.伴随着Android权限管控的愈发严格和MAC的要求,原Simpleperf的独立selinux domain完成所有功能的方式已经无法满足sandbox的需求,需要进行严格的domain隔离

本文着重讲一下traced_perf。

2.traced_perf的结构

2.1.代码结构

traced_perf的代码位于AOSP的external/perfetto/src/profiling/perf/ 目录下,可以看出,traced_perf的代码实际上是perfetto项目的一个子目录。

此目录下的代码如下图:

可以看出代码分为三类:

  • 编译脚本相关: BUILD.gn

  • 单元测试相关:X_unittest.cc

  • 主要代码逻辑

除了上述的代码目录外,在perfetto的主目录下还存在文件:external/perfetto/traced_perf.rc

此文件是traced_perf可执行文件的启动脚本。

2.2运行时结构

根据external/perfetto/Android.bp的编译脚本可以看出,traced_perf最终会被编译为一个可执行文件,并且被install到/system/bin/traced_perf。此可执行文件以daemon的形式存在,其启动和结束受它对应的rc启动脚本的控制。

其运行时的生命周期可以通过traced_perf.rc文件去分析:

  • traced_perf的权限配置

▫traced_perf的用户设置为了nobody,可以确保权限不会影响到其他用户,避免被恶意破解后获得提权

▫traced_perf的组包含nobody、readproc、readtracefs三个,readproc是为了使之被赋予可以读取/proc/PID目录的权限,readtracefs是为了赋予其读取tracefs mount的目录的权限,这两个权限是traced_perf能够正常运行所必须的权限。

▫traced_perf赋予了相应的capability,分别为KILL,DAC_READ_SEARCH。KILL是为了使traced_perf能够给其他进程发送信号。DAC_READ_SEARCH是为了使之能够至少能够获取一些文件的权限,而不至于甚至不能够探测某些文件的存在。这两个权限都是traced_perf正常运行所必需的。

▫task_profiles是为了给traced_perf设置为高capacity(unwinding)的一类cgroup,从而使得调度器可以给予其更合理的资源分配。

  • traced_perf的资源

traced_perf申请了一个名为traced_perf的unix_socket,此unix_socket是traced_perf与待profiling的进程间通信的通道,后续章节有涉及。

  • traced_perf的生命周期管控

traced_perf的生命周期管控通过property trigger来完成。当设置persist.traced_perf.enable 为true的时候,会自动启动traced_perf。同时,它还会受到sys.init.perf_lsm_hooks和traced.lazy.traced_perf的管控。

3.traced_perf的架构

3.1perfetto的框架

traced_perf作为perfetto工具集的一个组成部分,其遵循perfetto的service model的。perfetto的service model如下图所示:

3.1.1producer

traced_perf作为Tracing service的producer,其和tracing service的交互由两条通道,分别是IPC channel和shared_memory,其中IPC channel为unix socket,后面有详细描述。

shared_memory是指与tracing service之间建立的共享内存通道,此共享内存通道有两个作用:

1.进行高效的进程间数据传递,这里传递的主要是结构化的采样点数据。

2.与控制流进行隔离,避免被恶意破解后造成安全隐私风险。

traced_perf本身作为producer端,提供了Data Source,每个producer可以提供多种Data Source。traced_perf本身对外提供的Data Source包含linux.perf 和一个metadata的Data Source。后续我们对此Data Souce展开详细的描述。

3.1.2Tracing Service

Tracing Service作为perfetto在手机端的核心服务,其承担了主控的作用。Tracing Service在手机端主要表现为traced进程,它一方面接收consumer的配置文件的控制,另外一方面将配置文件转化为对Producer的控制;同时还承担了producer端与consumer端的桥梁。producer端与consumer的数据通道采用了trace buffer内存,这部分内存是没有进行进程间共享的,从而可以保持数据的隔离。

3.1.3Consumer

consumer端是指对perfetto trace类数据的消耗端,比如perfetto ui、shell command、traceur等,consumer端还可以进行自定义,在Android中添加客制化的consumer,从而对Data Source进行客制化的处理。

consumer端和Tracing Service的IPC通道主要也是通过unix socket进行连接的。

3.2traced_perf与perfetto的交互

3.2.1整体流程

整体流程示意图如下,读者可以根据此流程理解。交互流程涉及到Perfetto内部的众多类的实现,建议读者优先理解涉及到的C++类的声明与函数实现,而不要刚开始就陷入到调用流程的跟踪中,避免陷入到多层嵌套的复杂逻辑中。当把几个关键类的功能和对外关系理清楚之后,再通过调用关系依次跟踪调用流程。

  • PerfProducer:

调用ConnectService建立连接流程

实现OnConnect流程

实现OnTracingSetup、StartDataSource等函数

  • ProducerEndPoint

创建建立进程间通信的必要对象

实现OnConnect

  • ClientImpl

建立Socket连接

实现onConnect、onDataAvaliable等

通过上述流程拆分可以看出,每个类的职责都是非常清晰的。

3.2.2IPC通道建立

对于traced_perf来讲,建立IPC通道由下面几个重要流程:

1.实例化task_runner和AndroidRemoteDescriptorGetter。task_runner是traced_perf中使用的一个Looper工具类实例,AndroidRemoteDescriptorGetter是traced_perf为了获取想要trace的应用的私有进程数据而建立的一个类。后续章节有相关描述。

2.与Tracing Service建立连接

3.启动消息循环

3.2.3IPC通道框架

IPC通道的框架相对来说比较复杂,本小节进行一个原理剖析。

  • TaskRunner: 是一个Looper interface,PerfProducer使用的实例是基于unix domain socket实例化的TaskRunner。此task_runner_在各个结构间传递,承担了各类消息的派发和处理。

  • ProducerEndPoint: 是Tracing service的producer端的接口类,通过ProducerIPCClientImpl得以实例化。

为了能够将PerfProducer类注册为Tracing Service的producer,需要执行如下操作:

其中,ProducerIPCClient::Connect是一个静态方法,其实例化了ProducerIPCClientImpl并将其以unique_ptr的形式返回。

上述流程走完之后,实际上就建立了PerfProducer的事件处理流程。

关注到ConnectService中,ProducerIPCClient::Connect中的第二个参数是this指针,实际上是把PerfProducer的对象指针传递给了ProducerEndpoint对象,它是通过ProducerIPCClientImpl构造函数中的第三个参数producer传递过去的。

3.2.3.1相关概念

  • DataSource

顾名思义,这个是数据源的意思。根据Perfetto的框架图,consumer端需要指明从哪个“数据源”收集数据,而Producer可以提供数据源。数据源在perfetto中的定义以proto的形式进行了规定,在PerfProducer中,它对数据源的定义进行了抽象,通过DataSourceState进行描述。

与DataSource相对应的一个数据结构是traced_perf里面的DataSourceState的结构,可以看到DataSourceState中维护了一个TraceWriter指针,此TraceWriter提供了写入Trace数据的相关方法。

3.2.3.2TraceWriter

TraceWriter类是为了让使用者可以在perfetto的共享内存中,以零拷贝的形式写入Trace数据,方便使用者高效写入Trace数据。

  • NewTracePacket

创建一个TracePacket并返回一个handle

  • FinishTracePacket

完成之前创建的TracePacket

  • Flush

将TracePacket刷入到service端

3.2.3.3IPC消息的接收

ProducerEndPoint对象会通过PerfProducer对象提供的service_sock_name与PerfProducer进行通信,当连接建立之后,就进入了IPC流程,服务端会将消息按照perfetto定义的协议格式发送对应的指令。消息协议如下:

上述消息会被ProducerEndPoint解析,并最终转化为Producer接口类的虚函数调用(注意到ProducerEndPoint维护了一个Producer(PerfProducer)实例的指针)。

Producer的实例需要实现如下接口:

  • OnConnect

当与Tracing Service建立Socket连接后会被调用

  • OnDisconnect

当与Tracing Service断开Socket连接后会被调用。此时可以销毁PerfProducer对象了。

  • OnStartupTracingSetup

当第一个DataSource被创建之前被调用,可以做一些初始化的工作。

  • SetupDataSource

设置DataSource时被调用,其传递的参数包含DataSourceInstanceId以及DataSourceConfig

  • StartDataSource

启动DataSource

  • StopDataSource

停止DataSource

  • Flush

Tracing Service要求Producer将数据写入到共享内存中。

  • ClearIncrementalState

Producer端应该在此调用后,停止引用之前写入到共享内存的数据。

3.2.3.4IPC消息的发送

ProducerEndPoint提供如下接口:

  • Disconnect:

用于与ProducerEndPoint断开连接,此时不再能收到来自于Service端的回调消息。

  • RegisterDataSource

注册DataSource

  • UpdateDataSource

更新DataSource

  • RegisterTraceWriter

注册TraceWriter

  • CommitData

通知Tracing Service shared memory中的数据已经更新。

  • CreateTraceWriter

创建TraceWriter

  • 其他同步方法

4.traced_perf的事件处理

上一章节中,我们讨论了traced_perf与perfetto的框架的关系,本章节中着重阐述traced_perf在perfetto producer框架下,如何实现其作为perfetto的一个producer,达到profiling进线程counter信息、获取调用栈、分析性能问题的目的的。

上一章节中,描述了trace_perf 通过IPC通道从tracing service进行事件接收,这些事件最终转化为了Producer的重写函数,那么traced_perf作为tracing service的producer,需要实现这函数从而完成整个流程。

图中方框里面的是PerfProducer的事件处理状态,连接线上的字是traced_perf中发生的事件或者通过IPC接收到的命令。

4.1onConnect的实现

onConnect的实现非常简单,首先设置连接状态的状态机为“kConnected”状态,其次实例化了两个名字分别为“linux.perf”与“perfetto.metatrace”的DataSourceDescriptor,然后通过endpoint_指针的RegisterDataSource方法进行DataSource注册,其中endpoint_即为上一章节中提到的ProducerEndPoint对象的指针。

4.2StartDataSource的实现

StartDataSource的参数有两个,分别是DataSourceInstanceID和DataSourceConfig,其中DataSourceInstanceID是一个唯一的无符号64位的id,用来标识DataSource的实例;DataSourceConfig是data_source_config.proto生成的protobuf类,其原型可以参考:https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/config/data_source_config.proto;l=1;bpv=1;bpt=0

4.3启动MetaTraceSource

通过endpoint_智能指针,调用CreateTraceWriter方法,创建一个TraceWriter对象。同时将此metatrace进行使能,并保存到metatrace_writers_维护的一个map结构中。

4.4tracepoint与id的mapping的lookup操作

tracepoint一般是以名字的方式提供给配置文件的,但是linux kernel中一般使用其对应的id进行API访问控制,因此这里需要一个映射的提取。一般来说,此id可以通过位于tracefs的events/GROUP/NAME/id文件中可以提取出来。

4.5打开perf event对应的eventfd

首先将pb格式封装的配置文件转化为perf_event_attr数据结构,之后调用linux kernel提供的系统调用向操作系统注册。

打开perf event所必须的linux syscall为perf_event_open,此API的参数比较复杂,详细介绍可以参考官方文档:https://man7.org/linux/man-pages/man2/perf_event_open.2.html

这里着重讲解一下关键的配置信息:

▫perf_event_attr

perf_event_attr是一个比较大的结构体,包含了对perf_event配置的各种属性信息,以比较简单的tracepoint事件为例,一般来说需要设置以下必须的字段:

type: 设置为PERF_TYPE_TRACEPOINT类型

size: 设置为sizeof(perf_event_attr)

config:设置为上一步中获取到的mapping的id信息

sample_type: 设置sample中包含的数据类型

read_format:设置read返回的数值中包含的数据类型

开关bitmask配置:包含是否包含mmap的数据,是否包含comm等近30个配置项

pid

获取哪个pid的perf event事件

cpu

获取哪个cpu的perf event事件

groud_fd

可以将多个events通过同一个event fd进行返回,可以将其中一个事件传入-1作为group leader,后续事件可以将返回的fd传入此参数中。

4.6创建TraceWriter并使能perf event

4.7通知Unwinder启动了DataSource

4.8启动周期性读取任务

周期性的读取任务,主要是从内核的共享内存中,获取perf event的数据。在后续的章节中我们会着重讲述获取的数据。

在TickDataSourceRead函数中的ReadAndParsePerCpuBuffer中,会将从内核的共享内存中读取的sample数据,推送到unwinding_worker的queue中。当调用PostProcessQueue时,会唤醒unwinding_worker对应的线程,并执行unwind操作,直到所有sample都unwind完毕。

若DataSource的状态未停止,则需要继续抓取更多的samples,因此在这个task中,又继续调用了延迟任务,继续让task_runner调度本任务。

5.Sample的获取

Sample事件的获取是从Linux内核中提供的ring buffer共享内存中获取的,这部分操作位于PerfProducer::ReadAndParsePerCpuBuffer中进行的,这部分操作相对来说比较繁琐,下图中截取了一部分。其基本的流程是:

循环通过EventReader的ReadUntilSample获取解析好的Sample,如果DataSource的config中有配置一些filter项,则筛选掉不感兴趣的Sample,直到没有Sample产生了或者已经获取到足够的Sample了。

5.1PerfRingBuffer之环形缓冲区数据的获取

回顾一下perf_event_open函数的原型:

int syscall(SYS_perf_event_open, struct perf_event_attr *attr,                   pid_t pid, int cpu, int group_fd, unsigned long flags);

其中perf_event_attr结构中包含了众多的配置参数。跟通过ring buffer获取sample相关的参数有以下几个配置:

  • sample_period/sample_freq: 指明多久获取一次sample。

  • sample_type: 指明什么类型的数据会包含在sample中,比如Instruction pointer、TID、Sample的时间、地址信息等

通过perf_event_open返回的文件描述符,可以进而通过mmap系统调用,返回一个Kernel与Userspace共享的内存地址空间,此内存地址中的数据一般由Kernel写入,Userspace的程序负责对其进行解析。mmap的共享内存地址的分布如下:

metadata页对应的数据结构如下:

  • data_head: 指向数据区的首地址,这个地址是持续自增加的,在使用它的时候,需要将其地址与mmap buffer的大小进行一个wrap操作。

  • data_tail: 此数据是需要由userspace写入,指明userspace最后读取的数据的位置,从而使得内核不会降未读取的数据覆盖。

  • data_offset: perf_sample的起始位置由此述职来确定。

  • data_size: 包含了perf_sample区域大小信息

由Linux内核提供的perf sample也包含固定的格式,每个perf sample的数据原型如下:

注意到上述结构右边的if注释,假如对应的选项没有在sample_type中配置,则不包含对应的字段,在解析sample的时候,值的注意。

perf_event_header是每个sample的头信息,它的定义如下:

  • size: 本perf sample的大小

  • misc:包含本sample的一些额外的信息

  • type: 不同的sample类型,只有类型为PERF_RECORD_SAMPLE时,才有上面的perf sample的数据结构。比如当其类型为PERF_RECORD_LOST时,对应的perf event的数据结构为

5.2EventReader之Sample的解析

5.2.1perf sample的读取

perf sample的获取实际上是在对环形缓冲区的读取,环形缓冲区的包含一个读取偏移量以及一个写入偏移量。其中写入偏移量是由内核负责写入的,只要读取偏移量小于写入偏移量,则说明环形缓冲区中仍有数据未读取。

这里注意到环形缓冲区实际上是可以出现回卷操作的,假如出现了回卷操作,需要将数据进行重组。

5.2.2Perf sample的解析

perf sample本身的解析工作是通过EventReader::ParseSampleRecord进行的。解析后的数据结构为ParsedSample,其定义为:

可以看出,traced_perf关注的信息包含:

  • CommonSampleData: cpu_mode, cpu, pid, tid, timestamp, timebase_count等信息

  • regs: 用作unwinding的userspace寄存器信息

  • stack: userspace栈信息

  • kernel_ips: 内核instruction pointer信息

下面是Sample解析的具体流程

上述函数会返回解析好的Perf sample,即ParsedSample。进行一系列的筛选逻辑后,此sample会被发送到unwindwing_worker提供的queue中,以便于进行后续的unwinding操作。

至此,所有从内核中所需要的perf event信息已经收集并解析完毕,下一步是将之转化为可读的callstack信息的流程,这离不开unwinding 操作。

6.Unwinding操作

unwinding操作发生在解析完perf event sample之后,其发起动作的调用为:

其主要处理逻辑位于Unwinder::ConsumeAndUnwindReadySamples函数中。

当unwind成功后,调用到PerfProducer中的PostEmitSample中,将unwinding之后的数据写入TraceWriter。

6.1内核栈的解析

内核栈的解析相对简单,其主要操作函数再Unwinder::SynbolizeKernelCallchain中。其主要原理是解析"/proc/kallsyms" 中的内核地址与符号之间的对应关系。根据对应关系,将sample中的kernel态的instruction pointer翻译成地址信息。kernel 态的地址信息介绍见之前章节。

6.2用户栈的解析

用户栈的解析相对复杂,用户栈的解析首先要获取几个必要的信息:

  1. Userspace寄存器信息

  2. Userspace栈信息

  3. /proc//mem信息

  4. /proc//maps信息

其中前两个信息已经通过之前的Sample解析操作成功获取了,那么第3、4个信息怎么获取呢?

在之前的Android版本中,特权进程是可以直接访问到对应pid的这两个信息的,随着Android对安全隐私的越来越重视,对不同进程的敏感信息进行了比较强的隔离。因此traced_perf为了获取此信息,必须按照符合Android安全设计的机制,以相对复杂的方式进行实现。

6.2.1traced_perf如何请求目标进程的maps和mem信息

在AndroidRemoteDescriptorGetter类中,实现了获取/proc//mem以及/proc//maps操作的动作,获取操作是通过发起signal操作来完成的,signal的目标是目标进程:

而信息的接收是通过socket来完成的,即traced_perf进程刚启动时创建的socket:

在此socket的数据收取操作中,获取到上述两个文件的文件描述符(文件描述符已经经过内核态转换,可以在traced_perf进程中正常使用)。

上述代码中的delegate实际上指向的是PerfProducer对象,因此delegate_->onProcDescriptors会将两个文件描述符发送给PerfProducer对象。而PerfProducer进而将此文件描述符发送给了UnwinderHandle对象。

6.2.6目标进程如何将maps和mem信息发送给traced_perf

如前面所述,traced perf通过signal通知的目标进程,让目标进程将文件描述符进行了发送,那么目标进程为什么都会响应这类信息呢?(目标进程可能非常多样,包括daemon、系统apk、三方apk等),答案在C库中。

当目标进程接收到信号后,通过unix socket与traced_perf进程建立连接,然后打开两个文件maps和mem,通过unix socket的sendmsg进行发送。关于通过unix socket发送文件描述符,可以参考文档:https://man7.org/linux/man-pages/man7/unix.7.html,这里不做详细描述。

上述代码在android_profiling_dynamic.cpp中,会编译成C库的一部分,并被大多数进程所加载。

到此为止,用户栈的所有所需信息均已准备完毕。

6.2.3用户栈的解析

将所有信息准备好后,真正解析用户栈可以直接通过libunwindstack提供的方法Unwinder::Unwind即可了,反而流程显得很直接。

7.Sample的写入

Sample数据写入到trace中的操作也是比较直接的,将前面流程中获取到的信息,通过TraceWriter返回的TrackPacket protobuf结构进行写入即可。

至此,整个traced_perf的获取sample的执行流程大概完成了。

8.总结

traced_perf的工作流程主要部分包括:

  • perfetto流程的嵌入:traced_perf是perfetto的producer

  • Sample的获取

  • Unwinding操作

这三项主要内容看似复杂,实际上整体结构也是比较清晰的。Perfetto已经将tracing、profiling的框架打通,Tracing producer要接入perfetto,也是一件按部就班的事情而已。

9.参考链接

TracedPerf源码: https://cs.android.com/

traced_perf相关文档: https://perfetto.dev/docs

perf历史: https://en.wikipedia.org/wiki/Perf_(Linux)

simpleperf相关文档: https://android.googlesource.com/platform/prebuilts/simpleperf/+/782cdf2ea6e33f2414b53884742d59fe11f01ebe/README.md

perf_event_open: https://man7.org/linux/man-pages/man2

长按关注内核工匠微信

Linux内核黑科技| 技术文章 | 精选教程

Perfetto工具集之traced_perf相关推荐

  1. blktrace 工具集使用 及其实现原理

    文章目录 工具使用 原理分析 内核I/O栈 blktrace 代码做的事情 内核调用 ioctl 做的事情 BLKTRACESETUP BLKTRACESTOP BLKTRACETEARDOWN 内核 ...

  2. binutils工具集用法

    addr2line用于得到程序指令地址所对应的函数,以及函数所在的源文件名和行号. 在不少嵌入式开发环境中,编译器的名称往往不是gcc,而是想arm-rtems-gcc这样的,对于这种命名形式的编译器 ...

  3. 【linux】Valgrind工具集详解(八):Memcheck命令行参数详解

    [linux]Valgrind工具集详解(五):命令行详解中不够全,在此专门针对Memcheck工具中的命令行参数做一次详细的解释. Memcheck命令行选项 –leak-check=<no| ...

  4. 【linux】Valgrind工具集详解(五):命令行详解

    一.使用方法 usage: valgrind [options] prog-and-args 使用方法:valgrind [参数选项] 程序和参数 二.选择工具 tool-selection opti ...

  5. 【linux】Valgrind工具集详解(一):简介

    一.Valgrind概述 Valgrind是用于构建动态分析工具的仪器框架.它附带了一组工具,每个工具都执行某种调试,分析或类似任务,可帮助您改进程序.Valgrind的架构采用模块化设计,因此可以轻 ...

  6. (翻译)LearnVSXNow! #6 - 创建我们第一个工具集 - 序幕

    在前面的文章中,我们在向导的帮助下创建了一些小的VSPackages.在第五讲中我们整理了VSX的一些思路和概念,深入VSPackages 了解了packages如何工作以及服务的机制.在这篇文章中我 ...

  7. 亚马逊云科技在中国区域上线机器学习新服务,打造广泛而深入的人工智能与机器学习工具集

    2021年5月11日,在完全托管的机器学习服务Amazon SageMaker落地中国区域一周年之际,亚马逊云科技宣布通过与光环新网和西云数据的紧密合作在中国区域进一步落地多项人工智能与机器学习的新服 ...

  8. 暗渡陈仓:用低消耗设备进行破解和渗透测试1.2.2 渗透测试工具集

    1.2.2 渗透测试工具集 Deck包含大量的渗透测试工具.设计理念是每个可能会用到的工具都应该包含进来,以确保在使用时无须下载额外的软件包.在渗透测试行动中给攻击机安装新的软件包很困难,轻则要费很大 ...

  9. error MSB8008: 指定的平台工具集(v110)未安装或无效

    转自VC错误:http://www.vcerror.com/?p=318 问题描述: 平台工具集(v110)是vs2012下用的,你是用vs2010打开工程,它默认是用v100, 所以这个工程可能用v ...

最新文章

  1. 软工实践第三次作业(结对第一次作业)
  2. 用数据挖掘来支持音乐创作
  3. (三十三)设计模式之混合模式
  4. 《vSphere性能设计:性能密集场景下CPU、内存、存储及网络的最佳设计实践》一1.1.1 确定参数...
  5. 正方体最快最简单画_素描新手入门第一幅画可不只是“正方体”
  6. PhotoKit 照片库的管理-获取图像
  7. abp(net core)+easyui+efcore实现仓储管理系统——ABP WebAPI与EasyUI结合增删改查之六(三十二)
  8. 怎么把geany变成英文_细数Word中英文排版6大坑,我不相信你一个也没有遇到过...
  9. oppo手机删了android怎么办,OPPO手机越用越卡?1删除这4个僵尸文件夹,流畅如新机...
  10. vue 第四天 (计算属性的使用)
  11. build lavas 失败_vue lavas的项目在IE下显示空白
  12. JarvisOJ Basic 握手包
  13. 第13周 《C语言及程序设计》实践参考——定期存款利息计算器
  14. CoAP协议之初探(一)
  15. 【前端开发遇到到的问题2】文字下方加下划线
  16. 计算机视觉-刷题笔记day1
  17. Linux查看隐藏文件和文件夹
  18. 第9节 路由器简单原理
  19. 中国移动支付行业投资机会分析与发展战略建议报告2022-2028年
  20. 制作 Linux mint 20.2 随身系统

热门文章

  1. PAT乙级题库踩坑实录
  2. 前端必备,5大mock省时提效小tips,用了提前下班一小时
  3. MATLAB代码:基于雨流计数法的源-荷-储双层协同优化配置
  4. C++题解:【NOIP2006PJ】Jam的计数法(count)
  5. c语言中ioc有什么作用,IOC简介
  6. 明天是程序员节,程序员的过节姿势大全抢先看
  7. 0427-android-距离感应了解一下
  8. 运动员和教练案例分析
  9. Spark 内存管理内存空间分配_大数据培训
  10. iOS 10 is the maximum deployment target for 32-bit targets