0 引言
由于Linux具有功能强大、源代码开放、支持多种硬件平台、模块化的设计方案以及丰富的开发工具支持等特点,在实际系统中,得到了广泛的应用。但由于其最初的设计目标为通用分时操作系统,对于实时系统而言,Linux仍然存在核心不可抢占、关中断、时钟粒度粗糙等缺陷。为了使其应用于嵌入式系统,实时控制等领域,越来越多的厂家和研究机构热衷于改善其实时性,构建基于Linux的实时操作系统。
在Linux 2.4和以前的版本,内核是不可抢占的,也就是说,如果当前任务运行在内核态,即使当前有更紧急的任务需要运行,当前任务也不能被抢占。因此那个紧急任务必须等到当前任务执行完内核态的操作返回用户态后或当前任务因需要等待某些条件满足而主动让出CPU才能被考虑执行,这很明显严重影响抢占延迟。在 Linux 2.6中,内核已经可以抢占,因而实时性得到了加强。但是内核中仍有大量的不可抢占区域, 如由自旋锁spinlock保护的临界区,以及一些失效抢占的临界区。另一个影响Linux实时性的因素就是关中断,同步操作中使用的关中断指令增大了中断延迟,这样很多由中断驱动的实时任务得不到及时的执行,系统的实时性能得不到保障。因此提高Linux的可抢占性,改进其中断机制有利于改善内核的实时性。
本文详细阐述了这些措施的原理并基于标准Linux2.6内核加以实现,最后通过测试,验证了此改进方法的效果。
1 中断线程化
1.1 Linux中断对内核实时性的影响
中断处理是由内核执行的最敏感的任务之一,当内核正打算去做一些别的事情时,中断随时会到来,中断当前的任务进而执行中断处理程序。因此内核的目标就是让中断尽可能快的处理完,尽其所能把更多的处理工作向后推迟。为此Linux把中断的处理分成上半部分和下半部分。上半部分立即执行,下半部分将唤醒相应的和中断处理相关的进程稍后执行。虽然这种机制使得中断处理变得更加高效和易于维护,但是对于系统如果有严重的网络负载或其他I/O负载时,中断将非常频繁,内核当前的实时任务会被不停中断,这对于Linux的实时应用来说是不可接受的。
另外,Linux为了使内核同步而采用了关中断,在内核的关中断区域,中断是被屏蔽的。即使此时有通过中断驱动的实时任务也得不到响应,增加了实时任务的中断延迟。实时化后的Linux中,自旋锁被互斥锁取代,而中断处理代码中大量运用了自旋锁,中断处理代码就有可能因为得不到锁而需要被挂到该锁的等待队列上去。但是只有可调度的进程才可这么做,如果中断处理代码仍然使用原来的自旋锁,那么互斥锁取代自旋锁改进内核实时性的努力将大打折扣。
线程化的中断管理可以有效的解决这些问题。中断线程化后,中断将作为内核线程运行而且赋予不同的实时优先级,实时任务可以有比中断线程更高的优先级,这样,实时任务就可以作为最高优先级的执行单元来运行,即使在严重负载下仍有实时性保证。另一方面中断处理线程也可以因为在内核同步中得不到锁而挂载到锁的等待队列中。很多关中断就不必真正的禁止硬件中断了,而是禁止内核进程抢占,这样就可以减小中断延迟。
1.2 设计与实现
Linux提供了kthread_create创建内核线程,该内核线程在内核空间执行,因此在调度时没有用户空间和内核空间切换,使得其运行更为高效。中断线程化要做的工作是创建中断线程以及中断的处理。中断线程是在系统初始化或者调用requestirq函数时通过 kthread_create函数创建的。其过程等同于如下功能代码:
对于非紧急中断,kthread_create为其创建一个内核线程,并且根据中断号为其赋予一定的静态实时优先级和设置其调度策略。中断到来后,内核并不是直接进入中断服务函数,而是通过设置调度标志告知内核,内核调度程序比较该中断线程的优先级和当前运行任务的优先级,作出调度决策。因此当前正在运行的高优先级的实时任务不会受中断的太大的影响,保证了实时任务运行的可靠性和准确性,中断线程将会其他合适的时刻被调度执行,而且 Linux2.6内核的O(1)调度机制也不会因为内核线程数的增加在调度时间上额外增加调度开销。对于紧急的中断(比如时钟中断),内核保持原来的中断处理方式,而不为其创建中断线程,这样保证了紧急中断的快速响应。
2 自旋锁改互斥锁
2.1 新的自旋锁设计
为了同步不同内核控制路径对共享资源的访问,标准Linux内核提供了很多内核同步原语,其中自旋锁spinlock是一种广泛应用于可抢占内核,SMP环境下的内核同步机制。在spinlock的保持期间,内核是抢占失效的。被自旋锁保护的区域称为临界区,内核中大量使用了spinlock,存在大量的内核不可抢占的临界区,这将严重影响系统的实时性,我们使用新的实时互斥锁rt_mutex来替换spinlock,即让临界区内内核可抢占。其结构体如下:
类型raw_spinlock_t就是原来的spinlock_t。新的自旋锁还是使用spinlock_t来标记。在结构struct rt_mutex中的wait_list字段就是优先级化的等待队列。Owner字段为拥有该锁的进程的环境信息。mutex比spinlock优越的地方有这么几点:(1)当遇到一个锁住的临界区时,任务被挂起到锁的优先级等待队列wait_list中,临界区解锁时等待任务被激活。(2)内核将一个锁住的临界区和一个任务关联,当获得互斥锁时将任务的标识存入锁中。(3)临界区可以在被保护的同时不禁止抢占。(4)在被锁住的临界区中可以实现优先级继承。
2.2 实时互斥锁的操作
并不是所有内核中的自旋锁都可以转换为互斥锁的,一些底层的临界区必须是不可抢占的,所以必须由不可抢占的自旋锁保护,比如:保护硬件寄存器的锁、调度器的运行队列锁、和其它不可抢占自旋锁嵌套的锁。实时内核中,不可抢占的自旋锁与RT互斥锁共存,不可抢占的自旋锁被重命为 raw_spinlock_t。spin_lock函数利用gcc的内嵌技术根据锁的类型通过预处理选择具体的锁执行函数。预处理器使用 __builtin_types_compatible函数,由宏TYPE_EQUAL调用:
#define TYPE_EQUAL(lock, type)
__builtin_types_compatible_p(typeof(lock), type *)
函数映射结构PICK_OP是允许两种锁共存的机制,PICK_OP在编译阶段将锁操作转化为合适的种类(mutex或者spinlock):
根据宏处理的结果,如果类型为spinlock_t,将运行函数_spin_lock,而如果类型为raw_spinlock_t,将运行函数 _raw_spin_lock。对于实时rt_mutex获取,当前任务由于优先级高如果可以抢占该锁,则把锁的先前拥有者添加到该锁的等待队列中并且在新的锁拥有者的task_struct中标记等待该锁的所有任务;而如果不能获得锁,则把当前任务放到锁的优先级等待队列中,直到锁的拥有者释放该锁,唤醒等待队列中最高优先级的任务进入临界区。为了防止优先级反转,可以改变锁的当前拥有者的优先级为锁的等待队列中任务的最高优先级。
实时互斥锁的应用使得高优先级的任务通过抢占锁可以进入临界区,这样内核的不可抢占区的数量和范围都大大缩小,内核可抢占性有了很大的提高,充分减小了实时高优先级任务的抢占延迟。
3 改进后的内核实时性能测试
对Linux进行了实时化改造后,需要对其进行测试来确定系统是否符合实际应用的需要。实时性能测试主要包括测试内核的最大中断延迟和任务调度时间延迟两个重要的指标。
中断延迟反映的是中断信号发生到内核对中断做出响应所需要的时间,通过利用并口的7号中断,短路并口的9-10脚,其中10脚为中断引脚,9口为并口数据口的高位,往IO口0X378写8就可以引发中断,然后在中断服务程序里往0x378写0就会重新拉低中断引脚电平。用高精度示波器测量这个脉冲的宽度来测得中断延迟时间。对于赛扬650MHz,内存128M的工控平台,负载为拷贝CF卡情况下,标准Linux2.6内核最长中断延迟为 240us,而改进后的实时内核最长中断延迟为16us。
采用开源软件LMbench3.0测试系统调度延迟时间。测试机器配置为:CPU ,Intel P4 2.8GHz,内存512M,负载情况为拷贝硬盘、访问网络。标准Linux2.6内核调度延迟为1038us,而改进后的内核的调度延迟为140us。
可见改进后的内核的最长中断延迟和调度延迟比标准内核要短很多,内核的实时性能有了很大的提高。
4 结束语
针对标准Linux2.6内核由于中断关闭时间过长、内核临界不可抢占区过大而多,明显存在实时性能方面的不足,提出了改进其实时性的方法,实现了实时Linux内核的中断线程化以及新的内核同步自旋锁,使其具有更短的中断延迟和调度延迟。
本文作者的创新点:在标准的Linux2.6可抢占内核上对其中断处理机制进行线程化改造,大大的减小了中断响应时间。另外对内核同步机制采用新的自旋锁使得内核可抢占性能更加优异,改进后的内核可以适用于对Linux内核实时性要求更高的场合。