测试程序cyclictest的源码分析
背景知识
numa
Non-Uniform Memory Access,非一致性内存访问。
numa出现之前,所有CPU共用一根总线访问内存(UMA,统一内存访问)。UMA的问题是随着CPU核数增加,访存效率下降。
在numa架构里,有结点的概念。一个结点里包含了若干CPU和内存。结点内部访存快,结点外部访存慢,访存存在着本地和远程的区别。可以使用numactl --hardware命令查看系统中的numa结点。
smi
System Management Interrupt,系统管理中断。
这是硬件系统的中断,内核是看不到它的。
外部设备通过SMI引脚来触发smi,软件也可以通过端口0xB2触发smi。
代码导读
选项相关变量
是用户选项与源码之间的接口。大致相当于process_options()函数的解读。
1 | /* -a选项, --affinity选项 */ |
关键数据结构
1 | /* Struct to transfer parameters to the thread */ |
main函数流程
process_options()处理参数。check_privs()检查当前进程调度算法是否是实时的,或可切换为实时的。如设置了
trigger,则调用trigger_init()初始化单向链表head。此链表的元素为
struct thread_trigger。1
2
3
4
5
6
7
8/* Info to store when the diff is greater than the trigger */
struct thread_trigger {
int cpu;
int tnum; /* 线程的编号 */
int64_t ts; /* time-stamp */
int diff;
struct thread_trigger *next; // 下一个元素的指针
};此链表第一个元素的指针为head,最后一个元素的指针为tail。
current用来指向链表上需要被更新的元素。
此链表增加元素采用后插法。
如设置了
lockall,则调用mlockall(MCL_CURRENT|MCL_FUTURE)。调用
set_latency_target()设置电源管理系统,这是为了降低时延。/dev/cpu_dma_latency要存在。- 把0写入文件
/dev/cpu_dma_latency。效果为告诉电源管理系统不要切换到高cstate。当文件关闭后,电源管理器的行为将切换回系统默认的状态。目的是为了阻止CPU进入低功耗状态。详见Documentation/power/pm_qos_interface.txt。
调用
check_kernel()检查内核信息。- 使用
uname()获取内核信息,保存在struct utsname kname。 - 把
kname.release的信息分别保存在maj,min,sub里。 - 使用上步三个变量控制
kv、functiontracer、traceroptions的值。 - 返回
kv的值。
- 使用
调用
setup_tracer()来设置tracer。- 执行
debugfs_prepare()设置fileprefix,即debugfs的目录名。 - 由于现代内核都大于2.6.28,故只看if分支的代码。
- 如果debugfs里tracing_enabled文件存在且tracing_on文件不存在,则向tracing_enabled文件里写入1。
- 如果设置了tracetype,则向ftrace_enabled写入1,否则写入0。
- 使用
settracer("nop")把”nop”写入current_tracer文件。 - 在一个switch分支里依据tracetype的值,把相应的tracer写入current_tracer文件。
- 如设置了enable_events,则调用
event_enable_all()把1写入debugfs里的events/enable文件。 - 向traceroptions文件里写入一些tracer。
- 如设置了traceopt_count,则向traceroptions文件写入traceptr数组里的指针元素。
- 向tracing_max_latency文件里写入0。
- 如latency_hist目录存在,则向latency_hist/wakeup/reset文件写入1。
- 如trace_fd==-1,则给trace_fd赋值,如tracing_on存在它对应的文件就是tracing_on,否则它对应的文件是tracing_enable。
- 调用
opentracemark_fd()给tracemark_fd赋值。 - 跳过else分支不进行分析,因为现代内核一定大于2.6.28。
- 调用
tracing(1)向trace_fd里写入1。
- 执行
调用
enable_trace_mark()打开debugfs下的文件trace_marker。- 执行
debugfs_prepare()挂载debugfs并把它的路径名赋值给fileprefix。 - 执行
open_tracemark_fd()打开debugfs下的文件trace_marker。
- 执行
执行
check_timer()检查时钟的精度。如果设置了check_clock_resolution则开始检查时钟的精度。目的是看报告时钟精度不能高于测量时钟精度。
- 获取时钟精度,将reported_resolution设置为ns表示的精度。
- 计算1ms可以调用clock_gettime()多少次,将这个结果保存在times里,如少于1000次按1000次算,如大于100000次按100000次算。
- time是一个以
struct timespec为元素的数组,数组大小为times,调用clock_gettime()在一个for循环里把time数组填满。 - 在一个for循环里,用min_non_zero_diff记录下数组time里相邻元素之间的最小差值(这个最小差值不能为0)。这个值即为测量到的时钟精度。
- 测量时钟精度不能高于报告时钟精度,否则需输出warn信息。
设置mode,如选项里有-n则use_nanosleep=0B01,如选项里有-s则use_system=0B10。
把信号SIGALRM加到信号屏蔽字上。
调用signal()让sighand()处理信号SIGINT, SIGTERM, SIGUSR1。
- 如信号是SIGUSR1,则向stderr输出错误信号,然后返回。
- 如是其它两个信号,则shutdown置1。
- 如定义了refresh_on_max,则唤醒一个等待在PTHREAD_COND_INITIALIZER上的线程。
- 如定义了tracelimit,则向trace_fd文件写入”0”。
为两个关键的数组分配内存。数组parameters包含了每个线程的参数,数组statistics包含了每个线程的统计信息。
在一个for循环里创建子线程:
- 使用pthread_attr_init()初始化attr。
- 依据setaffinity的值设置cpu的值。
- 一般不会编译numa参数,忽略if分支。
- 为parameters[i]分配内存并将内存初始化为0。
- 为statistics[i]分配内存并将内存初始化为0。
- 如设置了histogram,则为statistics[i]里关于直方图的字段分配内存并初始化为0。
- 如设置了verbose,则为
statistics[i]->value分配内存,并设置好它的掩码par->bufmsk。 - 初始化parameters[i]和statistics[i]的相关字段。一般来说,都是根据用户的输入直接赋值。但要注意的是,如果没有设置histogram,每个线程par->interval的值会相差distance。
- 使用pthread_create()创建线程,线程代码为timerthread,线程参数为parameters[i]。
如定义了use_fifo,则创建子线程,子线程的代码为fifothread。
如没有设置shutdown,则进入无限循环中:
- 执行policyname(),读取整型的policy,返回对应的字符串到policystr。
- 如设置了force_sched_other,则slash=”/“、policystr2=”other”,否则slash和policystr2都是空串
- 如果verbose和quiet都没设置,则打印policy和loadavg的值。
- 在一个for循环里打印每个线程的状态,如线程的循环次数达到了用户设定的次数,则allstopped++
- 调用usleep()睡眠10000ms,即10s。
- 如设置了shutdown或allstopped,则退出此循环。
- 如设置了refresh_on_max,则执行pthread_cond_wait()睡眠,等待子线程通过
pthread_cond_signal()唤醒自己。
设置ret为EXIT_SUCCESS。
outall标志。除了顺序执行可到此处外,在创建线程的for循环里,如果设置了verbose,则会给
statistic[i]->values和statistic[i]->smis分配内存,如果内存分配失败也会跳转到outall标志。- 将shutdown置位。
- 调用usleep()睡眠50000ms,即50s。
- 如设置了quiet,则将quiet置2。
- 在一个for循环里确保所有的线程都已退出。
- 如设置了trigger,则调用trigger_print()打印出结构体
thread_trigger存储的信息。 - 如设置了histogram,则打印相关数据并释放
statistics[i]->hist_arry和statistics[i]->outliers指向的内存。 - 如果设置了tracelimit。则打印出所有线程的tid。
- 在一个for循环里释放所有
statistics[i]占用的空间。
outpar标志。除了顺序执行可到此处外,在主线程里如果给statistics分配内存的时候失败,也会跳转到outpar标志。本标志的代码是用for循环释放所有的
parameters[i]的内存。out标志。除了顺序执行可到此处外,如果设置了lockall但
mlockall()执行失败的时候,或如果给parameters分配内存失败的时候,也会跳转到此处。- 如设置了tracelimit,则调用tracing(0)给文件
trace_fd写入0,以确保触发器已关闭。 - 如tracemark_fd和trace_fd还没有关闭,则调用
close()关闭之。 - 如设置了enable_events,则调用
event_disable_all()向文件events/enable写入0。 - 如果设置了tracetype但没有设置notrace,则调用
setkernvar向文件ftrace_enabled写入0。 - 如果设置了lockall则调用
munlockall()解除内存的锁定。 - 现代内核版本都高于2.6.28了,不考虑此if分支。
- 如果/dev/cpu_dma_latency还是打开状态,则关闭之。
- 如果affinity_mask非NULL,则调用
rt_bitmask_free()释放它指向的内存。 - 调用
exit()返回ret。
- 如设置了tracelimit,则调用tracing(0)给文件
timerthread线程
如果par->node==-1,则不进行numa相关的设置。
如par->cpu==-1,则用户没有设置亲和性,直接跳过if分支;否则,通过
pthread_setaffinity_np()设置当前线程的亲和性。把
par->interval写入到interval。通过
gettid()设置stat->tid。把
par->signal添加进当前线程的信号屏蔽字。如果
par->mode==MODE_CYCLIC,则创建一个计时器,当计时器超时的时候会向当前线程发信号par->signal。并将interval记录在tspec.it_interval。1
2
3
4
5/* POSIX.1b structure for timer start values and intervals. */
struct itimerspec {
struct timespec it_interval;
struct timespec it_value;
} tspec;使用setscheduler()设置当前线程的调度算法和优先级。
暂忽略设置smi后要执行的if分支。
如果设置了aligned或secaligned,即线程唤醒的时候在时间上对齐,则执行if分支。
- 调用
pthread_barrier_wait()让所有线程都到达globalt_barr再执行。 - 在
globalt_barr和align_barr两个内存屏障之间:如果线程编号为0,即par->tnum==0,则调用clock_gettime()把par->clock的时间记录在globalt里。更进一步地,如果设置的是secaligned==1,则意味着要按秒对齐,则需进一步调整blobalt。 - 调用
pthread_barrier_wait()让所有线程都到达align_barr再执行。 - 所有线程都把0号线程记录下的
globalt时间记录在变量now中。 - 如果设置了offset,则须把offset的时间加到now上。
- 调用
如果即没有设置aligned,也没有设置secaligned,则走else分支,调用
clock_gettime()把par->clock的时间记录到now里。用
next记录now + interval的时间。它代表了线程下次过期的绝对时间。如果设置了
duration,则用stop记录now + duration的时间。如果
par->mode == MODE_CYCLIC,则用timer_settime()启动进程的计时器。如果
par->mode == MODE_SYS_ITIMER,则用setitimer()启动系统的计时器。执行
stat->threadstarted++表示线程已启动。当
shutdown还没有置1的时候,不停进行while循环:- 依据par->mode的值,设置等待的方式。MODE_CYCLIC和MODE_SYS_ITIMER是通过信号等;MODE_CLOCK_NANOSLEEP是进程计时器,通过睡眠等;MODE_SYS_NANOSLEEP是系统计时器,通过睡眠等。
- 调用
clock_gettime()让now记录时间。 - 如设置了
smi,则用stat->smi_count记录系统管理中断的次数。 - 用
diff记录now - next的值。 - 分别用
stat->min和stat->max记录diff的最小值和最大值。如产生了新的最大值且设置了refresh_on_max,则执行pthread_cond_signal()唤醒睡眠中的主线程。 - 用
stat->avg记录diff的累加和。 - 如设置了trigger且
diff > trigger,则执行trigger_update()给结构体current的相关字段赋值,current会自己转移到链表里的下个元素。 - 如果用户设置了程序运行时间duration,且已经达到了用户指定的时间now - stop >= 0,则shutdown++,意为可以关闭程序了。
stopped变量是线程是否停止的标志,0表示没有停止,1表示停止。当线程没有停止的时候,如果用户设置了追踪的限制时间tracelimit且diff > tracelimit,则表示进程应在当前线程停止,则会标记相关的变量和文件。- 用
stat->act记录当前diff的值。 - 如用户分配了缓冲区
stat->values或stat->smis,则它们的掩码par->bufmsk非0,此会把每个diff都记录到缓冲区里。如果diff的数量超过缓冲区的大小,则会覆盖之前的记录。 - 如设置了
histogram,则向代表直方图的数组里记录相应的数据。 stat->cycles++;- next += interval;如果是在MODE_CYCLIC模式,可能因为信号或线程而溢出了多次,这些也都需要一并计算进去。
- 如果now > next,则next += interval。
- 如果用户设置了循环次数par->max_cycles,且当前线程执行了这么多次的循环,则退出当前的while循环。
out标志,用于释放相关的资源。