1、oprofile概念
前面我们通过tick timer增加“/proc/stat”计数的方式来计算cpu占用率,更精确的计算方法是采用performance counter。cpu硬件提供了一系列的performance counter用于分析程序性能。
performance counter的原理很简单就是采样。
比如把performance counter配置成“采样cpu周期”型的,采样count为1000。那么在cpu运行1000个时钟周期后发生counter中断,在中断中记录当前的pc、task、is kernel、backtrace等信息,在采样多次以后,用户根据采样的数据来统计cpu的占用率。
也可以把performance counter配置成“采样cache miss”型的,采样count为10.那么cpu在发生10次cache 未命中事件后发生counter中断,在中断中记录当前的pc、task、is kernel、backtrace等信息,在采样多次以后,用户根据采样的数据来分析代码中哪些地方容易发生cache miss,以此来优化代码。
Oprofile就是利用performance counter机制来实现程序性能分析的软件,他的实现包括两部分:内核模块和用户态程序。内核负责使能硬件performance counter采样数据记录到buffer中,用户态的守护进程定时刷新内核buffer到用户采样文件中,用户态还提供采样文件分析工具用来把原始采样数据转化成用户容易分析的数据。
2、oprofile内核态实现
oprofile在内核态以驱动的形式存在,代码位于drivers/oprofile路径。
2.1、oprofile fs
可以看到profile驱动注册文件系统oprofilefs_type,在mount的时候会调用get_sb函数oprofilefs_get_sb()。
2.1.1、“/dev/oprofile/enable”
“/dev/oprofile/enable”启动/停止内核的性能采样,写入0停止采样,写入非0启动采样。
2.1.1.1、fops->read
2.1.1.2、fops->write
2.1.2、“/dev/oprofile/dump”
“/dev/oprofile/dump”dump event buffer中的内容,唤醒阻塞在读“/dev/oprofile/buffer”上的用户进程。
2.1.2.1、fops->write
2.1.3、“/dev/oprofile/buffer”
“/dev/oprofile/buffer”用来提供接口给用户态读取event buffer。对文件的第一次open操作会触发分配event buffer、cpu buffer,并启动buffer sync工作队列;对文件的read操作读出event buffer中的内容。
2.1.3.1、fops->open
2.1.3.2、fops->read
2.1.3.3、fops->release
2.1.4、“/dev/oprofile/buffer_size”
“/dev/oprofile/buffer_size”用来修改fs_buffer_size变量,即event buffer的大小,默认值为131072。
2.1.5、“/dev/oprofile/buffer_watershed”
“/dev/oprofile/buffer_watershed”用来修改fs_buffer_watershed变量,即event buffer的水位大小,默认值为32768。
2.1.6、“/dev/oprofile/cpu_buffer_size”
“/dev/oprofile/cpu_buffer_size”用来修改fs_cpu_buffer_size变量,即cpu buffer的大小,默认值为8192。
2.1.7、“/dev/oprofile/cpu_type”
“/dev/oprofile/cpu_type”用来读取cpu类型。
2.1.8、“/dev/oprofile/backtrace_depth”
“/dev/oprofile/backtrace_depth”用来设置堆栈回溯的深度。
2.1.9、“/dev/oprofile/pointer_size”
“/dev/oprofile/pointer_size”用来读取系统指针的长度。
2.1.10、“/dev/oprofile/stats/”
“/dev/oprofile/stats/cpu%d/”是一个子文件夹,其中包含了3个文件”sample_received”、”sample_lost_overflow”、”backtrace_aborted”。
“/dev/oprofile/stats/文件夹中还包含”sample_lost_no_mm” 、”sample_lost_no_mapping”、”event_lost_overflow”、”bt_lost_no_mapping”几个计数文件。
2.1.11、oprofile_ops.create_files
性能计数器会在“/dev/oprofile/”文件夹下面创建自己的文件,具体的可以见各种采样模式。
2.2、performance counter采样(CONFIG_X86_LOCAL_APIC模式)
2.2.1、oprofile_ops.create_files
性能计数器会在“/dev/oprofile/”文件夹下面创建自己的文件。每个performance counter都创建一个子文件夹“/dev/oprofile/%d/”,每个子文件夹下再创建计数器对应的属性文件。
- event The event type e.g. DATA_MEM_REFS
- unit mask The sub-events to count (more detailed specification)
- counter The hardware counter(s) that can count this event
- count The reset value (how many events before an interrupt)
- kernel Whether the counter should increment when in kernel space
- user Whether the counter should increment when in user space
2.2.2、oprofile_ops. setup
2.2.2.1、model->fill_in_addresses
2.2.2.2、model->setup_ctrs
2.2.2.3、model->check_ctrs
2.2.3、oprofile_add_sample
在上一节的oprofile_ops. Setup()函数中配置完performance counter以后,在发生counter的nmi中断以后会调用oprofile_add_sample()函数向cpu buffer中增加一条记录。记录当时的pc指针、当前任务、当前是内核还是用户态等信息。
2.2.3.1、log_sample
2.2.4、oprofile_ops. backtrace
在oprofile_add_sample()中如果启用了堆栈回溯会调用oprofile_ops. backtrace。
2.2.5、oprofile_ops. shutdown
2.2.6、oprofile_ops. start
2.2.6.1、model-> start
2.2.7、oprofile_ops. stop
2.2.7.1、model-> stop
2.3、nmi timer采样(CONFIG_X86_IO_APIC模式)
在注册performance counter失败的情况下,注册nmi timer来采样,相当于只有一个 “cpu运行“类型的counter。
2.3.1、oprofile_ops. start
2.3.2、oprofile_ops. stop
2.4、tick timer采样
在注册performance counter和nmi timer都失败的情况下,注册tick timer来采样,相当于只有一个 “cpu运行“类型的counter。
2.4.1、oprofile_ops. start
系统的tick timer中断中最终会调用我们注册的钩子函数timer_notify()。
2.4.2、oprofile_ops. stop
2.5、buffer sync
performance counter在采样条件满足时发生中断,采样记录当时的pc、task、is kernel、backtrace等信息到cpu buffer中,系统会起一个独立的内核任务来定时刷新cpu buffer到event buffer,同时系统还会注册事件的钩子来异步刷新。
Event buffer通过“/dev/oprofile/buffer”把采样记录传递给用户态。
2.5.1、wq_sync_buffer
这里要描述两个极其重的概念,app_cookie和cookie。
Cookie这个东西是可以在文件系统中用来惟一标识一个文件,也就是说每一个文件都有一个独一无二的cookie,通过cookie可以找到这个文件。
我们cpu_buffer得到的只是task_struct,我们不能把task_struct传给oprofiled,因为它解析不了;我们不能把task的名字传给oprofiled,因为这样太占空间。所以我们最好传cookie,oprofiled通过系统调用可以通过cookie得到这个文件所有的东西。
那app_cookie和cookie是啥东西呢?
通过get_exec_dcookie和lookup_dcookie,我们可以知道两者的区别。
通过get_exec_dcookie可以找到app_cookie,它是找EXEC的VMA,所以我们可以认为它是这个可执行文件。
通过lookup_dcookie可以找到cookie,它是找所有的vma,看EIP落在哪个区间,然后找出该vma, 然后找出cookie。
我们知道一个程序,它的动态库是通过mmap映射到某个库文件,但虚存vma还是在这个程序的地址空间里。
所以app_cookie只是一个大概的,说明了这个可执行文件。
而cookie却是精确的,它找到具体是哪个映射,可能cookie等于app_cookie,那这个就直接是可执行文件的映射,有可能它是哪个库。
我们为什么需要app_cookie呢?这是为了separate_kernel, separate_lib, separate_thread做考虑。
一般的,我们profile都是以某个cookie为单位,也就是以某个可执行文件,某个库,kernel为单位,采样属于它们的,则归到一类,这样最后opreport时,我们得到的结果是某个库的符号的比例,某个可执行文件的符号比例。
但如果使用separate_kernel,这就要求,对kernel和lib的采样要归到程序上来。也就是说执行程序a, kernel的某个符号在a执行中占了多少比例,某个lib在a执行中占用了多少比例。
这两者是显然有区别的,前者库和kernel的符号都在整个kernel和库中做对比,而后者库和kernel的符号需要归到程序中。
要达到这样的效果,我们必须加入app_cookie。
- 1、通过app_cookie,我们可以知道某个库符号它当前属于哪个进程
- 2、通过app_cookie, 我们可以知道kernel当前是属于哪个进程上下文,因为在kernel中cookie为NO_COOKIE。
static void add_kernel_ctx_switch(unsigned int in_kernel)
{
add_event_entry(ESCAPE_CODE);
if (in_kernel)
add_event_entry(KERNEL_ENTER_SWITCH_CODE);
else
add_event_entry(KERNEL_EXIT_SWITCH_CODE);
}
对于kernel user的互换,我们没有做比较多的处理
static void
add_user_ctx_switch(struct task_struct const * task, unsigned long cookie)
{
add_event_entry(ESCAPE_CODE);
add_event_entry(CTX_SWITCH_CODE);
add_event_entry(task->pid);
add_event_entry(cookie);
/* Another code for daemon back-compat */
add_event_entry(ESCAPE_CODE);
add_event_entry(CTX_TGID_CODE);
add_event_entry(task->tgid);
}
这个处理就比较大了,因为从cpu_buffer传过来的是task_struct的变化,因此有可能是线程的变化,有可能是子进程的变化。所以我们需要加上TGID来区分这样的情况。
对于线程,tgid是一样的,都是父进程的,但pid各不相同。
对于子进程,tgid,pid都不一样了。
对于context switch来说,这里的cookie就是app_cookie.
static int
add_sample(struct mm_struct * mm, struct op_sample * s, int in_kernel)
{
if (in_kernel) {
add_sample_entry(s->eip, s->event);
return 1;
} else if (mm) {
return add_us_sample(mm, s);
} else {
atomic_inc(&oprofile_stats.sample_lost_no_mm);
}
return 0;
}
static int add_us_sample(struct mm_struct * mm, struct op_sample * s)
{
unsigned long cookie;
off_t offset;
cookie = lookup_dcookie(mm, s->eip, &offset);
if (cookie == INVALID_COOKIE) {
atomic_inc(&oprofile_stats.sample_lost_no_mapping);
return 0;
}
if (cookie != last_cookie) {
add_cookie_switch(cookie);
last_cookie = cookie;
}
add_sample_entry(offset, s->event);
return 1;
}
如果cpu_buffer传过来的是“数据”信息,
如果是在kernel里面,则我们直接写到event_buffer里面。
如果是user的,我们使用add_us_sample,这个函数又做了比较多的事,它先是去查找真正的cookie, 找到后看cookie是否变化了,如果变化了,还要通知oprofiled,cookie变化了。
可以想像,在内核和oprofiled里面,都一直维护着kernel, cookie, app_cookie这三个量,所有的“数据”采样,都是在这三个范围内。因为一个数据采样,它是没有意义的,它必须附在这三个信息里面,才能找到相应的sample。
所以,一旦这三个量变化,kernel和oprofiled都会同时做出修改,以表示后面的采样都是在新的环境中。
举几个例子
- 1、系统调用从user—kernel
假设啥都不变,则kernel=1, cookie变成了NO_COOKEI和app_cookie还是原来的task,这就很自然了,因为在kernel中cookie是没有意义的,但app_cookie还是有意义的,因为系统调用还是在这个task的上下文中。
- 2、从用户进程调度到内核线程
首先这会有一个context switch的切换,app_cookie会变成NO_COOKIE。
然后cookie也会变成NO_COOKIE。
- 3、从用户程序到动态库
首先app_cookie不会变,但cooike会变成动态库的
- 4、进程到进程的调度
Context switch当然会有,app_cookie会变,然后cookie也会变。
- 5、进程中的线程的调度
Context switch会有,因为task_struct变化了,但app_cookie是一样的,不过针对这点,我们会把pid, tgpid记录下来,以便上层区分。
理解上面很重要,因为oprofiled中把这些采样归档,写文件,正是需要用到上面的信息。
2.5.2、异步事件同步
除了起内核任务来同步意外,系统还注册了一些钩子函数在一些异步事件时进行同步。
3、oprofile用户态实现
oprofile的用户态代码可以下载oprofile-0.9.8.tar.gz软件包。用户态的代码就是提供了一个守护进程来接收内核态的采样数据,并提供了一些列工具来把采样数据转换成用户可用的数据。
4、Oprofile的使用
4.1、oprofile使用步骤
首先要把内核态的oprofile代码编译成模块,在用户态安装oprofile软件包。现在就可以利用oprofile提供的工作来开始进行性能分析了。
- Step 1、加载oprofile驱动:
“opcontrol –init”加载oprofile驱动并挂载oprofile文件系统。
可以看到我们的cpu支持0-3一共4个performance counter。
- Step 2、配置oprofile的内核文件:
如果不分析内核配置“opcontrol –no-vmlinux”,如果分析内核使用“opcontrol –vmlinux=/boot/vmlinux-uname -r
”制定oprofile使用的内核文件。
- Step 3、配置采样的counter:
首先使用“opcontrol -l”查询本cpu支持哪些类型的performance counter。
可以看到我们的cpu支持“CPU_CLK_UNHALTED”、“ INST_RETIRED”、“ LLC_MISSES”等一系列performance counter。
我们配置实例采样使用“CPU_CLK_UNHALTED”和“ LLC_MISSES”两个counter,使用“opcontrol –event=”命令来配置。
- Step 4、得到采样数据:
我们分析一个实例简单程序,首先编译一个带调试信息的应用程序:
启动程序运行:
“opcontrol –start”启动oprofile采样,采样一段时间后,“opcontrol –stop”oprofile采样。
可以看到“/var/lib/oprofile/samples/current/”路径下已经有采样生成的数据。“opcontrol –reset”清除掉上一次的采样数据。如果使用“opcontrol –event=”命令重新配置过counter,则需使用“opcontrol –shutdown”先kill掉采样进程,再使用“opcontrol –start”重新启动采样。
- Step 5、使用opreport来分析采样数据:
“opreport –l ./hello”来分析hello程序内部的性能:
也可以不指定程序看整个系统的性能情况:
- Step6、使用opannotate来分析采样数据:
使用opannotate来查看采样数据和源码的对应关系:
- Step7、使用opgprof来分析采样数据:
opgprof可以把oprofile生成的数据转换成gprof使用的数据,可以使用gprof工具来分析。
4.2、gprof工具
在 编译或链接源程序的时候在编译器的命令行参数中加入“-pg”选项,编译时编译器会自动在目标代码中插入用于性能测试的代码片断,这些代码在程序在运行时 采集并记录函数的调用关系和调用次数,以及采集并记录函数自身执行时间和子函数的调用时间,程序运行结束后,会在程序退出的路径下生成一个 gmon.out文件。这个文件就是记录并保存下来的监控数据。
可以通过命令行方式的gprof或图形化的Kprof来解读这些数据并对程序的性能进行分 析。另外,如果想查看库函数的profiling,需要在编译是再加入“-lc_p”编译参数代替“-lc”编译参数,这样程序会链接libc_p.a 库,才可以产生库函数的profiling信息。如果想执行一行一行的profiling,还需要加入“-g”编译参数。