在用系统级性能工具找出是哪个进程降低了系统速度之后,就需要用特定的进程性能工具来发现这个进程的行为,以便确定:
- 确定应用程序的运行时间是花费在内核上还是在应用程序上
- 确定应用程序有哪些库调用和系统调用,以及它们花费的时间
进程性能统计信息
要了解一个应用程序的性能,至关重要的一点就是理解它与操作系统、CPU和存储系统是怎么进行交互的。
内核时间 VS 用户时间
一个应用程序所耗时间最基本的划分是内核时间和用户时间。内核时间是消耗在linux内核上的时间,用户时间是消耗在应用程序或者库代码上的时间。
linux中,有time、ps这样的工具可以大致表明应用程序将其时间是花费在应用程序代码上还是花在了内核代码上。还有比如oprofile和strace这样的命令可以跟踪哪些内核调用是代表该进程发起的,以及每个调用完成需要多少时间。
库时间 VS 应用程序时间
任何应用程序,即使其复杂性非常低,也需要依赖系统库才能执行复杂的操作。这些库可能会导致性能问题。因此,能查看应用程序在某个库中花费了多少时间就很重要了。虽然为了解决一个问题而去修改库的源代码并不总是实用,但是可以改变应用程序代码来调用不同的库函数,或者调用更少的库函数。在库被应用程序使用时,ltrace命令和oprofile工具包提供了分析库性能的途径。linux加载器ld的内置工具帮助你确定使用多个库是否会减慢应用程序的启动时间
细分应用程序时间
当已经知道某应用程序是瓶颈后,linux提供了工具来分析这个应用程序,以找出在这个程序中,时间都花在了哪里。工具gprof和oprofile可以生成应用程序的配置文件,确定是哪些源代码花费了大量时间
工具
time
time(如同秒表一样)可以测量命令执行的时间。它测量的时间有三种类型:
- 第一种是真正的或者经过的时间,即程序开始到结束之间的时间
- 第二种是用户时间,即CPU代表该程序执行应用代码所花费的时间
- 第三种是系统时间,即CPU代表该程序执行系统或内核所花费的时间
语法
与CPU性能相关的选项:
- 作用是对application程序计时,在其完成后,在标准输出中显示它的CPU使用情况
-v
:表示详情
time [-v] application
与CPU相关的user输出:
User time (seconds)
:CPU花费在应用程序上的秒数System time (seconds)
:CPU代表应用程序花费在Linux内核上的秒数Percent of CPU this job got
:进程运行时消耗CPU的百分比Elapsed (wall clock) time (h:mm:ss or m:ss)
:应用程序从启动到完成所经历的时间Major (requiring I/O) page faults
:主缺页故障的数量或者需要从磁盘读取内存页的页故障数量Minor (reclaiming a frame) page faults
:次缺页故障的数量或者不需要访问磁盘就可以解决的页故障Swaps
:进程被交换到磁盘的次数Voluntary context switches
:进程让出CPU(比如,进入睡眠状态)的次数Involuntary context switches
:进程被迫让出CPU的次数Page size (bytes)
:系统的页面大小Exit status
:应用程序的退出状态
实例
$ /usr/bin/time ls
admin corefiles data oceanstar
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 992maxresident)k
0inputs+0outputs (0major+317minor)pagefaults 0swaps
$ /usr/bin/time --verbose ls
admin corefiles data oceanstar
Command being timed: "ls"
User time (seconds): 0.00
System time (seconds): 0.00
Percent of CPU this job got: 100%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.00
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 992
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 317
Voluntary context switches: 1
Involuntary context switches: 1
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
另外,base shell有内置time命令,可以提供进程执行信息的子集
strace
- strace是当程序执行时,追踪其发起的系统调用的工具。
- strace可以展示准备的系统调用,它在确定应用程序是如何使用linux内核的方面是非常有用的
- 在分析大型程序或者你完全不懂的程序时,跟踪系统调用的频率和长度是非常有价值的。通过查看strace的输出,你可以了解应用程序如何使用内核,以及它依赖于什么类型的函数
语法
下面是一些与CPU性能相关的命令行选项:
strace [-c] [-p pid] [-o file] [--help] [commandd [arg ...]]
实例
从下面可以看出read调用时间占用了20%的时间,供消耗了0.44s。它被调用了2427次,平均下来,一次调用的时间为184us。在这些调用中,有26次返回了错误
ltrace
ltrace
与strace的概念相似,但是它跟踪的是应用程序对库的调用而不是对内核的调用。虽然ltrace主要用于提供对库调用的参数和返回值的精确跟踪,但是也可以用它来汇总每个调用所花的时间。从而既可以发现应用程序有哪些库调用,又可以发现每个调用时间是多长。
使用ltrace要小心,因为它会产生误导性结构。如果一个库函数调用了另一个函数,则花费的时间要计算两次。比如,库函数foo()调用了bar(),那么foo()的报告时间 = foo()运行的全部时间 + bar()花费的时间
语法
与cpu性能相关的命令行选项:
-c
:使得ltrace在命令执行完后打印出所有调用的汇总-s
:除了库调用之外,ltrace还跟踪系统调用,该项与strace提供的功能相同-p pid
:跟踪给定PID的进程-o file
:将ltrace的输出保存到file--help
:显示帮助信息
如果ltrace 不带任何选项,那么将在标准错误输出上显示所有的库调用
ltrace [-c] [-p pid] [-o filename] [-S] [--help] command
输出选项:
% time
:相对库调用所花费的总时间,该项是这个库调用所花的百分比second
:调用这个库所用的总秒数users/call
:调用这个库所花的微秒数calls
:调用这个库的总次数function
:这个库的名称
实例
运行ltrace -c xxxx
:
- 库函数XSetVMProtocols、hypot、XQuertPointer分布占用了在库所花时间的19.65%、17.19%、12.06%。
- 消耗第二多的函数hypot,其调用次数为702次,而消耗第一多的XSetVMProtocols,其调用次数仅为1次,除非我们的应用程序能够完全删去XSetVMProtocols,否则不管他需要多少时间,我们都会被这个时间所制约。因此我们最好将注意力转向hypot。
- hypot函数的每次调用都是相对轻量级的,因此,如果我们能减少它的调用次数,就有可能加快应用程序的速度。如果这个应用程序有性能问题,那么第一个要调查的函数就是hypot
怎么调查呢?第一个我们就是要确定hypot是做什么的,方法:
(1)通过man手册
(2)如果不能man,调用就找出这个函数属于哪个库,然后阅读这个库的文档
但是ltrace并不会明显的显示一个函数是来自哪个地方的,因此我们必须借助工具ldd
和objdump
。
- ldd用于显示一个动态连接的应用程序使用了哪些库
- objdump用于在每个库中查找给定的函数
-T
:列出库依赖或者提供的所有符号(主要是函数)
具体操作如下:
(1)第一步ldd显示这个应用程序使用的库
(2)第二步通过objdump去每个库中查找hypot符号
(3)第三步 【查看应用程序源代码】找出hypot在哪里被调用,如果可能的话,减少其调用的次数;或者尝试优化hypot的源代码
ps(进程状态)
- ps可以用来跟踪运行进程
- 它给出正在运行进程的详细的静态和动态统计信息。
- 静态信息包括命令名和PID
- 动态信息包括内存和CPU的当前使用情况
语法
与CPU性能最相关的选项:
ps [-o etime, time, pcpu, command] [-u user] [-u user] [PID]
-o xxx
:该项允许你明确规定想要跟踪的进程通信信息。不同的统计项由一个没有空格的、用逗号分隔的列表指定etime
:统计经过时间,也就是从程序开始执行起耗费的总时间time
:统计CPU时间,也就是进程运行于CPU所花费的系统时间+用户时间pcpu
:统计进程当前消耗的CPU的百分比command
:命令名-A
:显示所有进程的统计信息-u user
:显示指定有效时间ID的所有进程的统计信息-U user
:显示指定用户ID的所有进程的统计信息
实例
下面显示中:
- 使用了88%的CPU
- 运行了6秒,但是消耗的CPU时间只有5秒
下面显示中,我们没有调查具体进行的CPU性能,而是查看了特定用户运行的全部进程。这可以知道特定用户消耗的资源量的信息:netdump用户只运行了bash和top,其中bash不占用任何CPU,而top只占用了0.5%的CPU
与time不同,ps使得我们能监控当前正在运行的进程的信息。对于运行时间较长的工作,我们可以使用ps定期检测进程的状态(而不是在程序已经执行完毕后监测)
ld.so(动态加载器—这个暂时没什么用)
- 执行一个动态连接应用程序时,首先运行的是linux加载器ld.so。
- ld.so加载该应用程序所有的库,并将它使用的符号与库提供的函数关联起来。因为不同的库最初被链接到内存中的不同位置,这些位置还可能是重叠的,链接器需要对所有的符号进行排序,以确保每个符号都位于内存中的不同位置。 一个符号从一个虚拟地址移动到另一个虚拟地址,就被叫做重定位
- 加载器做这项工作是需要时间的,如果它完全不用去做那就更好了
- 预连接应用程序的目标就是通过重排整个系统的系统库来完整这项工作,以保证它们不会相互重叠。需要进行大量重定位的应用程序可能没有被预链接过。
- linux加载器的运行不需要用户进行任何干预,只需要执行一个动态程序即可,它是自动运行的。虽然加载器的执行对用户来说是隐藏的,但是它的执行仍然要花时间,这就有可能会延长应用程序的启动时间。当你要了解加载器的统计信息时,加载器展示的是其工作量,以便你能弄清楚它是否是瓶颈
gprof
剖析linux应用程序的一种强有力的方法是使用gprof分析命令。
- gprof可以展示应用程序的调用图,应该采样该应用程序的时间都花在了哪里。
- gprof的工作方式是,首先编译你的应用程序,然后运行该应用程序生成一个采样文件。
- gprof是非常强大的,但是它需要应用源程序,并且增加了编译开销。尽管gprof可以确定函数被调用的精确次数,以及函数所花的大致时间,但是其编译将有可能改变应用程序的时间特性,延缓程序的执行。
语法
要用gprof剖析一个应用程序,第一步编译源程序:
-pg
:开启剖析功能(注意不要与可执行文件剥离)-g3
:开启符号。符号信息对使用gprof的源注释特性是必须的
g++ -pg -g3 -o app main.c
第二步是运行编译后的程序,会生成一个输出文件gmon.out
./app
第三步是使用gprof命令来显示结果,语法如下:
--brief
:简化gprof的输出。默认情况下,pgrof输出全部的性能信息,并用图例解释每个指标的含义。该选项删除了图例-p
或者--flat-profile
:显示应用程序中每个函数花费的总时间和其调用次数-q
或者--graph
:打印出已剖析的应用程序的调用图。其显示了程序中的函数是如何相互调用的,每个函数所花的时间,以及其子函数所花的时间-A
或者-annotated-source
:在原始源代码的下面显示剖析信息
gprof [-p -flat-profile -q --graph --brief -A -annotated-source] app
对于一个特定的剖析来说,并不是所有的输出统计信息都是可以得到的。哪个输出统计信息是可得的取决于应用程序是如何为了剖徐而被编译的
实例
运行一个gprof --brief -p xxx
的例子:
- 程序中有两个函数a()和b(),每个函数都调用了一次。
- a()完成的时间(91%)是b()完成的时间(8.99%)的10倍。
- 函数a()花费的时间为5.06s,函数b()花费的时间是0.5s
运行一个gprof --brief -q xxx
的例子(展示程序的调用图):
另外,gprof还可以对源代码进行注释,以展示每个函数调用的频率。如下,没有展示函数消耗的时间,但是显示了函数被调用的次数。
$ gprof --brief -A app
*** File /home/oceanstar/CLionProjects/lib_fiber/main.c:
#include <stdio.h>
#include <stdlib.h>
int a(void)
50000 -> {
int i=0,g=0;
while(i++<100000)
{
g+=i;
}
return g;
}
int b(void)
50000 -> {
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 <No of Iterations>\n", argv[0]);
exit(-1);
}
else
iterations = atoi(argv[1]);
printf("No of iterations = %d\n", iterations);
while(iterations--)
{
a();
b();
}
}
Top 10 Lines:
Line Count
5 50000
15 50000
Execution Summary:
3 Executable lines in this file
3 Lines executed
100.00 Percent of the file executed
100000 Total number of line executions
33333.33 Average executions per line
oprofile
【待添加】
小结
本文主要介绍了怎样跟踪单个进程的CPU性能瓶颈。
- 可以确定一个应用程序消耗的时间是如何分配到linux内核、系统库,甚至该应用程序本身的
- 可以知道怎样找出哪些调用是对内核的,哪些是对系统库的,以及完成它们分别花了多少时间
- 可以了解如何剖析应用程序,确定源代码的那个行消耗了大量的时间。
在优化了CPU瓶颈之后,接下来就应该去优化那些不受CPU约束的瓶颈,比如磁盘或者超载网络的IO瓶颈