Linux上下文切换性能测试的一种新方法
2018-07-28孙菲艳李彦峰王娜汪辰
孙菲艳 李彦峰 王娜 汪辰
摘要:上下文切换是Linux操作系统内核优化的一个关键参数指标,如何精确方便地测量上下文切换开销显得至关重要。本文说明了使用nanosleep()函数测试方法的不合理性,提出了一种在用户态编写应用程序并且调用schedu_yield()系统调用主动放弃处理器实现任务切换的测试方法,并且基于ARM Linux-3.2.0实验平台,与传统的使用管道读写切换、在内核态测试context_switch()函数的开销等方法进行了对比分析,结果表明,使用该方法测试上下文切换的准确性和便捷性均有所提高。
关键词:Linux;上下文切换;系统调用;用户态;内核态
中图分类号:TP316 文献标志码:A 文章编号:1009-3044(2018)15-0047-04
A New Method for Testing the Performance of Linux Context Switch
SUN Fei-yan, LI Yan-feng, WANG Na, WANG Chen
(Nanjing Software Institute, Jinling Institute of Technology, Nanjing 211100, China)
Abstract: Context switch is a key parameter of the kernel optimization of Linux operating system. It is very important to measure the cost of context switch accurately and conveniently. This paper illustrates the irrationality of the test method using the nanosleep () function and presents a method, which is to write an user mode application program and use schedu_yield () system call to take the initiative to give up processor, and also compared with other methods like using traditional read / write with pipes to switch, using nanosleep() and testing the context_switch() function cost in the kernel mode directly based on the experimental platform of arm Linux - 3.2.0. And results show that this new method can improve the accuracy and convenience of the context switch testing.
Key words: Linux-3.2.0; context switch; system call; user mode; kernel mode
隨着信息技术、嵌入式技术的快速发展,Linux作为一种可裁剪、广泛支持、易开发的通用操作系统,也得到了越来越广泛的应用[1][2]。上下文切换延时作为linux操作系统内核的任务调度子系统的主要性能指标,测试上下文切换延时已是一项重要的工作。上下文切换是保存上一个任务的执行环境,准备下一个将要运行的任务的环境的必要操作。研究表明,上下文切换操作在操作系统中,每秒会发生几十至几百次,其带来的时间开销不可忽略[3]。有的测试程序采用nanosleep等睡眠函数来测试上下文切换[4][5],但是由于内核中进入睡眠状态的实时任务是在软中断中被唤醒,在软中断没有到来前实际上并不能保证两个实时任务交替切换;也有很多测试程序采用创建管道读写的方式来测试上下文切换[6][7],该方法程序编写相对复杂,所得到的测试结果是上下文切换延时和管道读写延时的总和,而管道读写延时的开销相对较大,会使得测试结果不够精确。本文提出了一种在用户态编写应用程序调用schedu_yield()系统调用主动放弃处理器实现任务切换的测试方法来提高上下文切换测试的精确度,同时也采用了在内核态插桩的方法来进行对比实验,结果表明该方法相比在内核态插桩测试操作更方便。
1 上下文切换
1.1 上下文切换
上下文切换又称为任务上下文切换(包括进程或者线程),是指CPU从一个任务切换到另一个任务的过程,在这个过程中,需要保存当前任务的状态和恢复另一个任务的状态,即当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为运行状态。
1.2上下文切换开销
上下文切换是操作系统内核优化的一个关键参数指标。在任务间发生切换需要花费大量的时间用于处理诸如:保存和恢复寄存器和内存页表、更新内核相关数据结构等操作[8][9]。上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间。
从Linux内核内部实现来看,如图1所示,上下文切换所花费的延迟时间是从调度器选好要调度的任务(任务1)后到把任务上下文切换到另一个任务(任务2)所花费的时间。即context_switch()函数的开销[3]。
2 三种传统的上下文切换测试机制
2.1 采用睡眠函数切换
有的学者采用睡眠函数实现切换的主要机制是:创建两个实时任务TASK1和TASK2,在每个实时任务中插入代码探测段,通过频繁切换两个实时任务来测试上下文切换时间,调度策略设置为SHCED_RR,优先级设置为最高,主要伪代码实现如下:
实时TASK1:
while(loops){
t1 = clock_gettime();
nanosleep(0);
t2 = clock_gettime();
switch_time = (t2 – t1)/2;
loops--;
}
实时TASK2:
while(loops){
nanosleep(0);
loops--;
}
通过在ARM Linux-3.2.0平台上测试发现,该方法并不能保证上下文切换在两个实时任务之间交替发生,CPU实际上是在内核线程ksoftirqd、TASK1、TASK2三个任务之间交替切换,主要原因是当实时任务调用nanosleep()睡眠时,会激活一个高精度定时器,同时在hrtimer_enqueue_reprogram()函数中会调用raise_softirq_irqoff(HRTIMER_SOFTIRQ)来唤醒hrtimer的软中断,而睡眠函数中的定时器到期检查是在HRTIMER_SOFTIRQ软中断中,当其中一个实时任务睡眠的时候,恰好另外一个实时任务已经处于睡眠状态且还没有被HRTIMER_SOFTIRQ软中断唤醒,因此状态仍然是TASK_INTERRUPTIBLE,而此时由于CPU中没有其他的任务可以运行,调度器会选择内核线程ksoftirqd来执行HRTIMER_SOFTIRQ软中断,在ksoftirqd被执行的时候会检查实时任务是否到期进而唤醒两个实时任务,则两个实时任务接下来会轮流抢到CPU,因此是三个任务之间的切换,主要原因是实时任务被唤醒是在HRTIMER_SOFTIRQ软中断中,只有在软中断被执行过之后才能唤醒两个实时任务使其得到运行,用该方法测试的时候内核中的切换顺序如下:
TASK1
TASK2
ksoftirqd
TASK1
TASK2
ksoftirqd
……
因此此時测试出来的上下文切换实际上也包括了内核线程ksoftirqd的运行时间,是不精确的。因此用该方法测试上下文切换是不合理的。
2.2 采用管道读写切换
管道测试是当前被广泛使用的上下文切换延时测试方法。主要实现机制也是创建两个实时任务,将实时任务调度策略设置为SCHED_FIFO,实时优先级设置为最高,两个任务利用管道循环读写n次,读写一个int,以此来进行上下文切换,这样测试出的上下文切换时间包括了管道读写的时间,同时编程相对复杂,实验发现,该方法测试得到的上下文切换时间相对较大(见下述),精确度有待提高。
2.3 在内核代码中插桩测试
内核中上下文切换主要是通过context_switch()函数来实现的,该函数位于kernel/sched.c中的schedule()函数中,该方法的主要机制是:运行用户态的应用程序来实现两个实时任务之间的相互切换,在内核源码的context_switch()函数前后插桩来获取上下文切换前后的时间戳进而得到上下文切换的时间,这里使用了ftrace提供的一个向ftrace跟踪缓冲区输出跟踪信息的工具函数trace_printk(),ftrace是Linux内核中提供的一种调试工具,使用ftrace可以对内核中发生的事情进行跟踪,这在调试bug或者分析内核时非常有用。trace_printk()的函数原型定义在内核头文件include/linux/kernel.h中,在激活配置CONFIG_TRACING后可以使用[10],如下:
schedule() {
……
trace_printk(“t1\n”);
context_switch;
trace_printk(“t2\n”);
……
}
运行应用程序来使两个实时任务交替切换,得到的打印结果如下:
TASK TIMESTAMP
TASK1 t1
TASK2 t2
TASK2 t1
TASK1 t2
……
在上下文切换过程中,TASK1经过context_switch()函数之后,会切换至TASK2,并且直接跳转至TASK2被切换之前的代码运行处即trace_printk(“t2\n”)处,TASK2->t2 – TASK1->t1这个时间段内即上下文切换一次的时间。
使用该方法测试虽然结果会相对更精确,但是需要修改内核源码,重新编译内核,相比用户态测试操作更复杂更耗时。
3 采用sched_yield()实现上下文切换延时测试
3.1 测试原理分析
编写用户态应用来实现内核上下文切换的测试。应用的主体是创建了两个实时任务(进程),调度策略设置为SCHED_FIFO,实时优先级设置为最高(99)。这两个任务都会调用sched_yield()系统调用函数,按照man手册说明,该函数主要是让调用者放弃CPU,将其移动到运行队列的尾部,调用其他任务来运行。
子进程起辅助作用,唯一的作用就是配合父进程完成任务切换,主要代码如下:
while (TRUE) {
......
sched_yield();
......
}
主要的延时计算在父进程中实现,主体代码如下:
for (count = 0; count < MAX_ITERATIONS; ++count) {
......
clock_gettime(CLOCK_MONOTONIC, &before;); // (1)
sched_yield(); // (2)
clock_gettime(CLOCK_MONOTONIC, &after;); // (3)
diff = calcdiff_ns(after, before) / 2000.0; // (4)
/* cost = diff – overhead; */
......
}
父进程控制整个测量的运行周期,通过一个循环运行MAX_ITERATIONS 次,每次执行四步操作:
第(1)步:父进程调用系统调用函数clock_gettime()以纳秒级别的精度获取调用sched_yield()前的绝对时间并存入before变量。
第(2)步:父进程调用sched_yield() 让出处理器,此时其他任务,也就是子进程的任务会被调度(其他任务不会被调度器选中,因为本测试环境中创建的父子进程是两个实时任务,其他非实时任务的调度优先级都低于实时任务)。此时内核发生一次任务调度和上下文切换,执行流切换到子进程的任务。参考前面子进程的执行逻辑,当子进程获得处理器后马上又调用一次sched_yield() 让出处理器,内核再次发生任务切换,即第二次任务切换后父进程再次获得处理器执行第三步。
第(3)步:父进程再次调用系统调用函数clock_gettime()以纳秒级别的精度获取调用此时的绝对时间并存入after变量。
第(4)步:calcdiff_ns()是自己编写的一个函数可以用来计算after和before两个绝对时间之间的时间间隔diff,之所以要除以2000,原因有二,一是最终的结果单位笔者希望采用微秒,其次是根据前面第二步的描述,before和after之间实際执行了两次上下文切换。最后要注意的是,由于此方法是在用户态编写程序计算内核上下文切换的时间,所以为了精确计算,原理上获得的diff值还包含了系统调用等其他指令的开销overhead,所以最终的一次上下文切换的开销cost应该还应该从diff值中减去overhead的值,但经过实际计算,由于diff的值本身已经很低,而overhead的值几乎可以忽略不计,所以用diff值代替cost的值也是合理的。
一次完整的在两个进程之间两次上下文切换的流程如图2所示:
3.2 测试环境
测试的开发板使用的处理器型号为ARM架构的 Cortex-A8,软件环境为linux-3.2.0的内核版本及测试需要的应用程序,具体如表1所示[11]:
3.3 测试结果及分析
在Linux-3.2.0内核下进行了2000000次的测试,测试结果如图2所示,图的横坐标代表测试序列次,纵坐标代表上下文切换延时,单位是微秒(us),且图中给出了延时的最小值(Min),平均值(Avg),最大值(Max),抖动值(Jitter)。
从图3,图4可以看出,使用sched_yield()系统调用来进行上下文切换测试得到的上下文切换Avg为2.8us,而使用管道读写的方式来测试得到的上下文切换Avg为8.6us,即采用sched_yield()系统调用测试使得上下文切换的开销相比使用管道读写的方式减小很多;同时,为了进一步对比,本文也采用在上述2.3中介绍的在内核态中插桩的方法来测试context_switch()函数的开销,经实验得到,内核中插桩跟踪得到的上下文切换的平均延时为2.5us(其中也包含跟踪工具带来的延时),可见,使用sched_yield()系统调用来切换的方法测试结果更接近内核态中测试的context_switch()函数的开销,结果更精确。
图3,图4中所给出的上下文测试结果中也包括了sched_yield()系统调用的开销和管道读写的开销。为了进一步追求结果的精度,表2给出了通过编写程序测量得到的sched_yield()和read()/write()的开销,由表2可以看出,sched_yield()系统调用的开销为1.5us,而使用管道读写的read()和write()的系统调用的开销为3.8us,sched_yield()系统调用的开销更小,可以近似忽略不计,因此在不去掉系统调用开销的情况下,采用sched_yield()测试上下文切换的结果相对更精确。
4 总结
本文利用sched_yield()系统调用函数可以主动放弃CPU的特点,提出了一种在用户态测试上下文切换的新方法。将该方法与在用户态使用管道读写切换的方法在ARM Linux-3.2.0的平台上进行了对比实验并且分析了使用nanosleep()睡眠函数测试不合理的原因,发现使用sched_yield()系统调用来进行切换可以显著提高测试结果的精度,而且sched_yield()系统调用的开销与read()/write()系统调用的开销相比要小很多,甚至可以忽略不计。同时也采用了在内核态插桩的方法来测试上下文切换时间,发现使用sched_yield()函数的测试结果更接近于context_switch()函数的开销,同时采用编写用户态应用程序的方法操作更方便更快捷。本文提到的采用sched_yield()系统调用函数来编写用户态应用程序的方法来测试上下文切换延时也可以应用到操作系统的实时性等方面的测试。
参考文献:
[1] Cede?o W, Laplante P A. An Overview of Real-time Operating Systems[J]. Journal of the Association for Laboratory Automation, 2007, 12(12):40-45.
[2] Jinhui Q, Hui L D, Junchao Y. The Application of Qt/Embedded on Embedded Linux[C]// International Conference on Industrial Control and Electronics Engineering. IEEE Computer Society, 2012:1304-1307.
[3] Context switch, https://en.wikipedia.org/wiki/Context_switch
[4] 吴章金. Linux实时抢占补丁的研究与实践[D]. 兰州大学, 2010.
[5] 张晓龙, 郭锐锋, 陶耀东,等. Linux实时抢占补丁研究及实时性能测试[J]. 计算机工程, 2014, 40(10):304-307.
[6] 胡志刚, MAHAMMED M E, 余正军,等. 嵌入式Linux实时性方法[J]. 中南大学学报(自然科学版), 2004, 35(4):638-642.
[7] 阳国贵, 姜波. 线程切换开销分析工具的设计与实现[J]. 计算机应用, 2010, 30(8):2052-2055.
[8] RobertLove. Linux内核设计与实现[M]. 机械工业出版社, 2011.
[9] DANIEL P.BOVET \& MARCO CESATI.深入理解LINUX內核:第3版[M].中国电力出版社,2006.
[10] Debugging the kernel using Ftrace - part 1. (2009-12-09) https://lwn.net/Articles/365835/
[11] MYC-AM335X 产品数据手册(V2.0)光盘路径MYD-AM335X_V14_2\01-Document\UserManual\Chinese