综述

  • gprof用于分析函数调用耗时,可用之抓出最耗时的函数,以便优化程序。
  • gcc链接时也一定要加-pg参数,以使程序运行结束后生成gmon.out文件,供gprof分析。
  • gprof默认不支持多线程程序,默认不支持共享库程序。
  1. gcc -pg 编译程序
  2. 运行程序,程序退出时生成 gmon.out
  3. gprof ./prog gmon.out -b 查看输出

注意事项

  • 程序如果不是从main return或exit()退出,则可能不生成gmon.out。
  • 程序如果崩溃,可能不生成gmon.out。
  • 测试发现在虚拟机上运行,可能不生成gmon.out。
  • 一定不能捕获、忽略SIGPROF信号。man手册对SIGPROF的解释是:profiling timer expired. 如果忽略这个信号,gprof的输出则是:Each sample counts as 0.01 seconds. no time accumulated.
  • 如果程序运行时间非常短,则gprof可能无效。因为受到启动、初始化、退出等函数运行时间的影响。
  • 程序忽略SIGPROF信号!

原理

gcc -pg 在应用程序的每个函数中添加了名为 mcount/mcount/_mcount的函数。 应用程序每个函数执行时都会执行mcount,而mcount则会在内存中保存一张函数调用图, 通过函数调用堆栈的形式,查找子函数、父函数的地址,也保存了与函数相关的调用时间、次数等信息。

多进程

如果用gprof分析多进程程序,则可能一个进程的gmon.out覆盖另一个进程的gmon.out, 解决方法是在执行程序之前执行:export GMON_OUT_PREFIX=x.out 则之后生成的文件名就如x.out.pid,多进程的gmon.out就不会相互覆盖。

多线程

gprof无法分析多线程程序。缘故是gprof使用ITIMER_PROF定时器, 当超时时由内核向应用程序发送信号。但多线程程序只有主线程接收ITIMER_PROF。 这里有一个简单的实现方法: 对pthread_create进行包装,并以动态库的形式在程序运行前加载。 我通过上文的描述,整理了一个gprof分析多线程程序的程序,可供参考。

$ gcc -shared -fPIC gprof_helper.c -o ghelper2.so -lpthread -ldl  # create ghelp2.so
$ gcc test.c -lpthread
$ ./a.out hello gprof     # output
$ LD_PRELOAD=./ghelper2.so ./a.out pthread: using profiling hooks for gprof    # outputhello gprof                                 # output# no time accumulated
Each sample counts as 0.01 seconds.
no time accumulated

gprof的输出表明没有时间被统计到。虽然函数调用次数是统计正确的,但没有时间(调用次数多未必就最耗时)。 gprof显示每0.01秒采样一次,如果函数执行的时间都非常短,例如低于0.01秒,则统计不到任何时间。

目录
1.    GPROF介绍    4
2.    使用步骤    4
3.    使用举例    4
3.1    测试环境    4
3.2    测试代码    4
3.3    数据分析    5
3.3.1    flat profile模式    6
3.3.2    call graph模式    7
4.    链接库中的函数    7
5.    使用局限    8
6.    分析示例    12
 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1.    gprof介绍
gprof是GNU profile工具,可以运行于linux、AIX、Sun等操作系统进行C、C++、Pascal、Fortran程序的性能分析,用于程序的性能优化以及程序瓶颈问题的查找和解决。通过分

析应用程序运行时产生的“flat profile”,可以得到每个函数的调用次数,每个函数消耗的处理器时间,也可以得到函数的“调用关系图”,包括函数调用的层次关系,每个函

数调用花费了多少时间。

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2.    使用步骤
1)    用gcc、g++、xlC编译程序时,使用-pg参数,如: g++ -pg -o test.exe test.cpp
编译器会自动在目标代码中插入用于性能测试的代码片断,这些代码在程序运行时采集并记录函数的调用关系和调用次数,并记录函数自身执行时间和被调用函数的执行时间。
2)    执行编译后的可执行程序,如: ./test.exe。该步骤运行程序的时间会稍慢于正常编译的可执行程序的运行时间。程序运行结束后,会在程序所在路径下生成一个缺省文

件名为gmon.out的文件,这个文件就是记录程序运行的性能、调用关系、调用次数等信息的数据文件。
3)    使用gprof命令来分析记录程序运行信息的gmon.out文件,如:gprof test.exe gmon.out则可以在显示器上看到函数调用相关的统计、分析信息。上述信息也可以采用

gprof test.exe gmon.out> gprofresult.txt重定向到文本文件以便于后续分析。

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3.    使用举例
3.1    测试环境
本文提供的样例的测试环境如下:
?    Linux server164 2.6.9-22.ELsmp #1 SMP Mon Sep 19 18:32:14 EDT 2005 i686 i686 i386 GNU/Linux
?    gcc version 3.2.3 20030502 (Red Hat Linux 3.2.3-47.3)
?    GNU gprof 2.15.92.0.2
3.2    测试代码
清单 1. 耗时测试应用程序示例
example1.c

#include 
int a(void) 
{
  int i=0,g=0;
  while(i++<100000)
  {
     g+=i;
  }
  return g;
}

int b(void)
{
  int i=0,g=0;
  while(i++<400000)
  {
    g +=i;
  }
  return g;
}
int main(int argc, char** argv)
{
   int iterations;
   if(argc != 2)
   {
      printf("Usage %s \n", argv[0]);
      exit(-1);
   }
   else
      iterations = atoi(argv[1]);
   printf("No of iterations = %d\n", iterations);
   while(iterations--)
   {
      a();
      b();
   }
}
这个应用程序包括两个函数:a 和 b,它们通过运行不同次数的循环来消耗不同的CPU时间。main 函数中采用了一个循环来反复调用这两个函数。函数b中循环的次数是 a 函数的

4 倍,因此我们期望通过gprof的分析结果可以观察到大概有 20% 的时间花在了 a 函数中,而 80% 的时间花在了 b 函数中。
3.3    数据分析
在 gcc 编译命令中加上 –pg参数即可。编译方法如下:
gcc example1.c -pg -o example1 -O2 -lc
在编译好这个应用程序之后,按照普通方式运行这个程序:
./example1 50000
程序运行完之后,应该会看到在当前目录中新创建了一个文件 gmon.out。

3.3.1    flat profile模式
使用 gprof 命令分析gmon.out 文件,如下所示:

gprof example1 gmon.out -p

-p参数标识“flat profile”模式,在分析结果中不显示函数的调用关系,AIX平台默认此参数有效。
输出以下内容:
清单 2. flat profile 的结果
Flat profile:
Each sample counts as 0.01 seconds.
  %    cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 80.38    203.27   203.27    50000     4.07     4.07    b
 19.61    252.87    49.60    50000     0.99     0.99    a
  0.00    252.88     0.01                                  main

上面结果中各个列的含义如下:
%time       函数以及衍生函数(函数内部再次调用的子函数)所占的总运行时间的百分比 
cumulative seconds 函数累计执行的时间
self seconds  函数执行占用的时间
calls          函数的调用次数
self   ms/call   每一次调用函数花费的时间microseconds,不包括衍生函数的运行时间
total  ms/call    每一次调用函数花费的时间microseconds,包括衍生函数的运行时间
name           函数名称
列的含义,在gprof的输出结果中都有详细的说明。
从输出结果中可以看到,正如我们期望的一样,b 函数所花费的时间大概是 a 函数所花费的时间的 4倍。
很多函数调用(例如 printf)在上面的输出结果中都没有出现。这是因为大部分函数都是在C链接库(libc.so)中,而链接库并没有使用 -pg 进行编译,因此就没有对链接库中

的函数收集调度信息。

3.3.2    call graph模式
如果希望反映函数之间的调用关系,需要采用如下命令:

gprof example1 gmon.out –q

-q参数标识“call graph”模式,在分析结果中显示函数的调用关系。
输出以下内容:
清单 3. Call graph
granularity: each sample hit covers 4 byte(s) for 0.00% of 252.72 seconds

index % time    self  children    called     name
                                                 
[1]    100.0    0.00  252.72                 main [1]
               201.41    0.00   50000/50000       b [2]
                51.31    0.00   50000/50000       a [3]
-----------------------------------------------
               201.41    0.00   50000/50000       main [1]
[2]     79.7 201.41    0.00   50000         b [2]
-----------------------------------------------
                51.31    0.00   50000/50000       main [1]
[3]     20.3   51.31    0.00   50000         a [3]
-----------------------------------------------
上面结果中各个列的含义如下:
index   每个函数第一次出现时都分配了一个编号,根据编号可以方便的查找函数的具体分析数据
%time  函数以及衍生函数(函数内部再次调用的子函数)所占的总运行时间的百分比 
self    函数的总运行时间
children 衍生函数执行的总时间
called   函数被调用的次数,不包括递归调用
name   函数名称
在name列中,可以看出函数之间的调用关系,main函数调用a、b函数,b函数被main函数调用,a函数被main函数调用。通过函数名称后面的数字来标识这个文件中的函数,从而可

以快速定位函数的分析数据的位置,经过一层层的逐步深入的分析就得到各个函数的调用关系以及各个函数的性能数据。

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4.    链接库中的函数
正如在前面曾经介绍的,对于代码剖析的支持是由编译器增加的,因此如果希望从链接库(比如libc.a)中获得剖析信息,就需要使用 -pg 来编译这些库。很多操作系统默认提供

了已经启用代码剖析支持而编译的链接库版本,例如:libc.a对应的采用-pg编译的文件为libc_p.a。对于没有按照标准提供类似libc_p.a链接库的操作系统版本来说,就需要安装

已经编译好的启用代码剖析的链接库版本或者自己下载链接库的源代码进行编译。
使用“启用代码剖析的链接库版本”的应用场景,举例如下:
gcc example1.c -g -pg -o example1 -O2 -lc_p
然后,像普通情况下一样运行gprof对gmon.out进行分析,可以获得 flat profile 或 call graph,而此时的分析结果会非常丰富,包含很多C的标准库函数的调用信息,例如:

printf、write等。
注意:上例的libc_p.a是静态链接库。gprof目前还不支持对动态链接库中的函数进行性能分析。

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
5.    使用局限
gprof的特点是它只能分析应用程序在运行过程中所消耗掉的CPU时间,只有当应用程序的函数消耗CPU的时候,gprof才能够获取函数的性能数据。如果应用程序在运行过程中暂时

挂起,并在系统内核唤醒应用程序后进一步执行,那么在应用程序中间暂停的时间性能数据是无法统计的;而且在应用程序等待I/O操作返回的时间,性能数据也是无法统计的。
如果对清单 1 稍加修改,就可以清楚地看出这个问题:
清单 5. 为清单 1 添加sleep()函数调用
example2.c:

#include 
int a(void)
{
  sleep(1);               /*调用系统函数进行sleep*/
  return 0;
}
int b(void)
{
  sleep(4);               /*调用系统函数进行sleep*/
  return 0;
}
int main(int argc, char** argv)
{
   int iterations;
   if(argc != 2)
   {
      printf("Usage %s \n", argv[0]);
      exit(-1);
   }
   else
      iterations = atoi(argv[1]);
   printf("No of iterations = %d\n", iterations);
   while(iterations--)
   {
      a();
      b();
   }
}

现在 a 函数和 b 函数不再处理繁忙的循环了,而是分别调用sleep()来挂起1秒和4秒。
使用“启用代码剖析的链接库版本”,编译这个应用程序:
gcc example2.c -g -pg -o example2 -O2 -lc_p
并让这个应用程序循环 30 次:
./example2 30
执行 gprof example2 gmon.out –p所生成的结果如下:
清单 6. flat profile 显示了系统调用的结果
Flat profile:

Each sample counts as 0.01 seconds.
 no time accumulated

%   cumulative   self              self     total           
 time   seconds   seconds    calls  Ts/call  Ts/call  name    
  0.00      0.00     0.00       21     0.00     0.00  _IO_file_overflow
  0.00      0.00     0.00       13     0.00     0.00  strncmp
  0.00      0.00     0.00        8     0.00     0.00  memset
  0.00      0.00     0.00        8     0.00     0.00  sigprocmask
  0.00      0.00     0.00        7     0.00     0.00  getenv
  0.00      0.00     0.00        6     0.00     0.00  memcpy
  0.00      0.00     0.00        5     0.00     0.00  _int_malloc
  0.00      0.00     0.00        5     0.00     0.00  malloc
  0.00      0.00     0.00        5     0.00     0.00  sigaction
  0.00      0.00     0.00        5     0.00     0.00  strsep
  0.00      0.00     0.00        4     0.00     0.00  nanosleep
  0.00      0.00     0.00        4     0.00     0.00  sleep
  0.00      0.00     0.00        4     0.00     0.00  strpbrk
  0.00      0.00     0.00        3     0.00     0.00  _IO_new_file_xsputn
  0.00      0.00     0.00        3     0.00     0.00  ____strtoul_l_internal
  0.00      0.00     0.00        3     0.00     0.00  __cxa_atexit
  0.00      0.00     0.00        3     0.00     0.00  __strtoul_internal
  0.00      0.00     0.00        2     0.00     0.00  __errno_location
  0.00      0.00     0.00        2     0.00     0.00  __find_specmb
  0.00      0.00     0.00        2     0.00     0.00  a
  0.00      0.00     0.00        2     0.00     0.00  b
  0.00      0.00     0.00        2     0.00     0.00  mempcpy
  0.00      0.00     0.00        1     0.00     0.00  _IO_default_xsputn
  0.00      0.00     0.00        1     0.00     0.00  _IO_doallocbuf
  0.00      0.00     0.00        1     0.00     0.00  _IO_file_doallocate
  0.00      0.00     0.00        1     0.00     0.00  _IO_file_stat
  0.00      0.00     0.00        1     0.00     0.00  _IO_file_write
  0.00      0.00     0.00        1     0.00     0.00  _IO_setb
  0.00      0.00     0.00        1     0.00     0.00  ____strtol_l_internal
  0.00      0.00     0.00        1     0.00     0.00  ___fxstat64
  0.00      0.00     0.00        1     0.00     0.00  __init_misc
  0.00      0.00     0.00        1     0.00     0.00  __libc_csu_fini
  0.00      0.00     0.00        1     0.00     0.00  __libc_csu_init
  0.00      0.00     0.00        1     0.00     0.00  __libc_init_first
  0.00      0.00     0.00        1     0.00     0.00  __libc_init_secure
  0.00      0.00     0.00        1     0.00     0.00  __libc_setup_tls
  0.00      0.00     0.00        1     0.00     0.00  __libc_start_main
  0.00      0.00     0.00        1     0.00     0.00  __pthread_initialize_minimal
  0.00      0.00     0.00        1     0.00     0.00  __setfpucw
  0.00      0.00     0.00        1     0.00     0.00  __strtol_internal
  0.00      0.00     0.00        1     0.00     0.00  _dl_aux_init
  0.00      0.00     0.00        1     0.00     0.00  _dl_important_hwcaps
  0.00      0.00     0.00        1     0.00     0.00  _dl_init_paths
  0.00      0.00     0.00        1     0.00     0.00  _dl_non_dynamic_init
  0.00      0.00     0.00        1     0.00     0.00  _itoa_word
  0.00      0.00     0.00        1     0.00     0.00  _mcleanup
  0.00      0.00     0.00        1     0.00     0.00  atexit
  0.00      0.00     0.00        1     0.00     0.00  atoi
  0.00      0.00     0.00        1     0.00     0.00  exit
  0.00      0.00     0.00        1     0.00     0.00  fillin_rpath
  0.00      0.00     0.00        1     0.00     0.00  fini
  0.00      0.00     0.00        1     0.00     0.00  flockfile
  0.00      0.00     0.00        1     0.00     0.00  funlockfile
  0.00      0.00     0.00        1     0.00     0.00  main
  0.00      0.00     0.00        1     0.00     0.00  mmap
  0.00      0.00     0.00        1     0.00     0.00  printf
  0.00      0.00     0.00        1     0.00     0.00  setitimer
  0.00      0.00     0.00        1     0.00     0.00  strrchr
  0.00      0.00     0.00        1     0.00     0.00  uname
  0.00      0.00     0.00        1     0.00     0.00  vfprintf
  0.00      0.00     0.00        1     0.00     0.00  write

对以上输出结果进行分析可见,尽管 profile 已经记录了每个函数被调用的确切次数,但是为这些函数记录的时间(实际上是所有函数)都是 0.00。这是因为 sleep 函数实际上

是执行了一次对内核空间的调用,从而将应用程序的执行挂起了,然后有效地暂停执行,并等待内核再次将其唤醒。由于花在用户空间执行的时间与花在内核中睡眠的时间相比非

常小,因此就被取整成零了。其原因是 gprof 以固定的周期对程序运行时间进行采样测量,当程序挂起时,gprof就不会对程序进行采样测量。
Gprof的特性使得有些程序非常难以进行优化,例如花费大部分时间睡眠等待内核唤醒的程序,或者由于外部因素(例如操作系统的 I/O 子系统过载)而运行得非常慢的程序。
通常,有一个很好的基准测试可以用来查看 gprof 对于应用程序的优化能起多大作用,方法是在 time 命令下面执行应用程序。此命令会显示一个应用程序运行完成需要多少时间

,并且在用户空间和内核空间各花费了多少时间。
例如:time ./example2 30
输出结果如下所示:
清单 7. time 命令的输出结果
No of iterations = 30
real    2m30.295s
user    0m0.000s
sys     0m0.004s
我们可以看出应用程序整体运行150秒左右,但大部分时间在睡眠状态,几乎没有多少时间被花费在执行用户空间和内核空间的代码上,此时gprof的分析结果无法体现函数的实际

运行时间。

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
6.    分析示例
在新疆联通进行综合入库程序上线前的测试过程中,测试人员发现程序运行效率低下。下面以整个调优的过程来举例说明基于gprof的性能分析方法。
性能分析前的准备阶段,性能分析数据的生成步骤如下:
1)    首先更改makefile文件中的CC=g++ -g –pg 行,添加-pg参数。
2)    执行make clean删除所有的目标文件、可执行文件
3)    重新编译
4)    在命令行执行可执行程序,并通过控制台发送启动命令
5)    程序退出后在fileindb执行程序目录下生成gmon.out(不是控制台目录)
6)    执行gprof fileindb > gsmindb_mon_result.txt把分析结果重定向到gsmindb_mon_result.txt文件
下面就是从gsmindb_mon_result.txt文件中截取的部分内容:
                                     
打开gsmindb_mon_result.txt文件后,可以看到127行:
[3]     37.2    0.00       41.90       1         .main [3]
                  0.00       41.88       1/1       .CMainManager::Run(int&) [4] 
可见main函数中的CMainManager::Run运行时间(41.88)几乎占据了所有的运行时间(41.90),再查CMainManager::Run的分析数据在148行:
[4]     37.2    0.00       41.88       1         .CMainManager::Run(int&) [4]
               0.00       41.81       1/1       .CBaseBillFile::DoEveryBill() [5] 
可见CMainManager::Run 函数中CBaseBillFile::DoEveryBill执行占了几乎所有的时间,对于占用时间比重很小的函数可以忽略。采用上面的方法一步一步向下查找,最终确定289

行:
           0.56       11.98  849922/849922      .CMobileFavour::CheckAllCondition(int,int) [11]
[12] 11.1    0.56       11.98  849922             .CGsmFavourCheck::CheckAllCondition() [12]
           0.06        5.82  849922/849922      .CGsmFavourCheck::CheckCallKind() [14]
在CGsmFavourCheck::CheckAllCondition()所调用的函数中CGsmFavourCheck::CheckCallKind()占用时间的比重过大,几乎达到50%;CGsmFavourCheck::CheckAllCondition总共用

时11.98,而CGsmFavourCheck::CheckCallKind()函数就占用了5.82,从而说明CGsmFavourCheck::CheckCallKind()函数需要作进一步优化,以提高整体的运行效率。

使用gprof对程序的性能分析(集合贴)相关推荐

  1. 软件性能测试分析与调优实践之路-Java应用程序的性能分析与调优-手稿节选

    Java编程语言自从诞生起,就成为了一门非常流行的编程语言,覆盖了互联网.安卓应用.后端应用.大数据等很多技术领域,因此Java应用程序的性能分析和调优也是一门非常重要的课题.Java应用程序的性能直 ...

  2. 《代码随想录(Carl)》 数据结构与算法 程序的性能分析知识点总结

    <代码随想录(Carl)> 数据结构与算法 程序的性能分析 2.1时间复杂度分析 2.1.1时间复杂度 时间复杂度是一个函数,它定性描述该算法的运行时间. 假设算法的数据规模为n,操作单元 ...

  3. 使用JDK自带的VisualVM进行Java程序的性能分析

    VisualVM是什么? VisualVM是JDK自带的一个用于Java程序性能分析的工具,JDK安装完毕后就有啦,在JDK安装目录的bin文件夹下能找到名称为jvisualvm.exe. 要使用Vi ...

  4. 利用WPT(Windows Performance Toolkit)进行程序的性能分析

    Windows Performance Toolkit WPT(Windows Performance Toolkit)是微软提供的一套用于分析程序性能的工具包,它其中包含两个主要的工具 WPR(Wi ...

  5. python性能解决的事_Python程序的性能分析方法

    有时候需要去优化Python端代码的性能,所以需要先找到瓶颈在哪里.好在这个事做起来特别方便. 一般先找出哪些函数是瓶颈,然后再看看那些函数里面时间都具体花在哪些行上了.Python里面提供的cPro ...

  6. 性能分析工具gprof介绍

    Ver:1.0 目录 1. GPROF介绍 4 2. 使用步骤 4 3. 使用举例 4 3.1 测试环境 4 3.2 测试代码 4 3.3 数据分析 5 3.3.1 flat profile模式 6 ...

  7. java dump分析工具_java性能分析与常用工具

    本次源码已放在Github:https://github.com/nateshao/jvm-tuning 个人博客 https://nateshao.gitee.io http://www.nates ...

  8. 系统级性能分析工具perf的介绍与使用

    测试环境:Ubuntu16.04 + Kernel:4.4.0-31 apt-get install linux-source cd /usr/src/tools/perf make &&am ...

  9. linux 系统级性能分析工具 perf 的介绍与使用

    目录 1. 背景知识 1.1 tracepoints 1.2 硬件特性之cache 2. 主要关注点 3. perf的使用 3.0 perf引入的overhead 3.1 perf list 3.2 ...

最新文章

  1. jpa jsf_完整的Web应用程序Tomcat JSF Primefaces JPA Hibernate –第3部分
  2. jquery name选择器_jquery笔记
  3. django-模板渲染上下文context-0223
  4. CentOS 7.6+mysql5.7 设置mysql开机自启动。
  5. AppBoxPro - 细粒度通用权限管理框架(可控制表格行内按钮)源码提供下载
  6. WCF入门(七)——异常处理1
  7. 超级全的一篇前端性能优化清单
  8. java的常量池的大小,Java 常量池注意点
  9. mac android usb驱动 win10,macbook安装Win10后无法创建引导的USB驱动器解决方法
  10. 金盾视频加密器V2014视频加密原理分析
  11. 2021-2027全球与中国抽屉拉手市场情况与未来趋势研究报告
  12. https端口必须443吗
  13. 情人节程序员用HTML网页表白【满天星星音乐背景动画特效】 HTML5七夕情人节表白网页源码 HTML+CSS+JavaScript
  14. Python opencv 在图片上写字
  15. 理解表现和结构相分离
  16. JavaAPI操作Hive
  17. .mht文件转换为html
  18. 自然语言处理NLP星空智能对话机器人系列:深入理解Transformer自然语言处理 BertViz
  19. 2023年最新qq空间说说怎么全部删除_QQ空间说说如何批量删除
  20. cad批量交点打断 lisp_晓东CAD家园-论坛-A/VLISP-批量交点打断-各位有批量交点打断!!各自塞下 总有个你觉得很好 - Powered by Discuz!...

热门文章

  1. 入门项目·电动车报警器
  2. 基于udp通信的在线多人聊天室
  3. 拿捏 javascript 中的节点操作
  4. 关于win7使用微软MediaCreationdTool平滑升级到win10出现0x80072f8f-0x20000的解决方案
  5. 第五届浙江省网络安全竞赛预赛部分wp
  6. 65款天文摄影宇宙星空清晰效果Lightroom预设
  7. 如何下载、升级 iPadOS 15 Beta 测试版?
  8. word模板插入文字、图片
  9. Ajax基础知识 · 上
  10. [运维]cAdvisor 使用教程