RTAI分析 RTAI(Real-Time Application Interface)是对Linux内核的硬实时扩展,它遵循自由软件规范;它可以提供工业级的RTOS功能,而且其所有的功能都可无缝的通过GNU/Linux环境访问。RTAI项目是由意大利米兰理工学院航天工程系(DIAPM)发起开发的遵循GPL的开源项目。 RTAI目前的稳定版本是3.3(以下的分析基于3.2),支持的CPU类型包括I386,PPC和ARM,在ARM类型处理器上的移植官方软件包仅包含对EP9301和PXA255的支持; 目前,基于RTAI已经有了很多的应用,如RTnet,USB4RT,RTCAN等。 RTnet是基于Xenomai和RTAI的开源的硬实时网络协议栈;它使用标准的以太网硬件,并且已经支持流行的网络芯片;它以确定性的方式实现了UDP/IP,ICMP和ARP,并且给内核模块和实时用户进程提供了POSIX套接字接口。 USB4RT的目标是在Linux/RTAI上实现一个具有硬实时能力的USB协议栈;它当前已经实现了核心栈和一个UHCI主机控制器驱动的功能,其将来的工作包括实现各种高层功能和支持EHCI(2.0)。 RTCAN为基于Linux的CAN节点提供了一个实时通讯的框架,通过RTCAN提供的一组功能,CAN消息可以通过RTAI进行发送和接收;RTCAN的重点在于平台的可移植性,它为控制系统的开发者提供了一个强有力的工具。 RTAI为开发硬实时系统提供了丰富的功能,其主要功能如下: 提供多种对实时任务的调度方式,它的调度器分为UP(Uniprocessor),SMP(Symmetric Multiple Processor和MUP(Multi-Uniprocessor)三类;其中,UP调度器用于单处理器的系统;SMP调度器用于对称的多处理器的系统,所有的CPU使用相同的时钟中断源,也就是说,任务在所有的CPU上都是以相同的模式运行;而MUP调度器也是用于多处理器系统,只不过每个CPU有自己独立的时钟中断源,所以,在不同的CPU上运行的任务可以以不同的时钟模式运行。在这三类调度方式中,时钟运行的模式都有两种,单触发模式(one-shot mode)和周期模式(periodic mode)。 提供了丰富的任务间的通信方法,主要包括邮箱机制(Mailboxe),消息和过程调用机制(Message and RPC),信号量机制(Semaphore),管道机制(FIFO),共享内存机制(Shared memory);其中的管道机制和共享内存机制可用于RTAI的实时任务和Linux进程之间的通信。 此外,RTAI还提供了LXRT机制,使得可以在Linux的用户空间创建软实时进程,满足软实时应用的要求。 RTAI还在不断的发展之中,而且它在实时环境中的应用也越来越多,它将来的主要发展方向包括实现更多硬件平台上的移植、为RTAI提供C++开发环境、实时文件系统、对基于Flash的文件系统的支持、高级的实时内存管理机制等。
RTAI在ARM平台上的移植,从3.2版本开始,是基于Adeos实现的,它的硬件抽象层HAL使用Adeos提供的服务。也就是说,RTAI实现了Adeos中的一个域,通过Adeos实现RTAI域的初始化、中断的申请、中断服务程序的注册等。 RTAI在Adoes系统中的域优先级高于Linux域(也就是根域),每当中断到来之后,Adeos先调度RTAI对该中断进行处理、执行中断相关的实时任务,只有当RTAI没有实时任务和中断需要处理的时候,Adeos才会调度Linux运行,这就保证了RTAI的中断响应速度和实时任务不受Linux的影响,从而提供了实时系统的可确定性。图4-1表示了RTAI,Linux和Adeos这三个软件实体之间的相互关系。
如图4-1所示,我们可以把整个实时Linux系统划分成硬件平台、Adeos,RTAI,Linux kernel和Linux应用程序五大部分,可以把各个部分之间的关系归为9类,下面对这9类关系分别作简短的描述,有助于理解整个系统的架构和相互关系。 关系①表示实时任务如何与RTAI提供的实时任务接口相互作用。实时任务(本文所指的实时任务,如果没有特别说明都是硬实时的)都是以Linux内核模块方式实现的,要实现一个实时任务,在模块初始化的时候要调用RTAI的任务创建函数初始化实时任务相关的数据和环境,指定定时器的运行模式(单触发模式或周期模式),初始化定时器,然后开始执行任务;需要注意的是,当没有加载任何RTAI的实时任务模块的时候,RTAI的任务调度和时钟中断都没有启动。 关系②表示实时任务通过RTAI提供的管道(FIFO)和共享内存与Linux用户空间中的进程进行通信,通过这种方式,实时任务获取的实时数据就可以传递到用户空间让非实时进程对数据进行后续的处理。 关系③表示RTAI本身的实现需要用到Linux内核提供的某些功能;例如,RTAI本身(包括各种提供给实时任务的服务模块)是以Linux内核驱动模块的形式存在的,这就需要用到Linux内核的动态内核模块加载功能;另外,RTAI目前的内存管理模块在初始化时是使用Linux的内存分配接口分配足够的内存。 关系④表示传统的Linux内核和Linux用户程序的关系。 关系⑤表示RTAI和Adeos之间的交互。最新的RTAI版本3.2是基于Adeos实现的,RTAI实现了Adeos内的一个域,这个域的优先级高于Linux内核所在的根域,可以保证所有的RTAI中断和实时任务都不会受Linux本身的影响,从而确保快速的中断响应和实时任务的按时完成。 关系⑥表示RTAI和底层硬件之间的交互,当外部事件触发了实时任务之后,实时任务在处理的过程中一般要对外部设备执行某些操作,例如控制采集卡进行数据采集、控制步进电机等。 关系⑦⑧⑨表示Linux、Adoes和硬件平台之间的关系,详细分析将在后面关于Adeos分析的讨论。 RTAI初始化 RTAI的初始化是由用户动态加载RTAI的硬件抽象层模块rtai_hal.o开始的,涉及到Linux用户空间、Linux域、RTAI域和Adeos,其初始化流程如图4-2。
从图4-2可以看出,RTAI的初始化通过函数__rtai_hal_init完成,这个函数位于rtai-3.2/base/arch/arm/hal/hal.c中,它完成的主要功能包括(注意:RTAI初始化是在根域中完成的,也就是说当前域为Linux): 1.通过调用adeos_alloc_irq分配一个虚拟中断rtai_sysreq_virq,在前面分析Adeos的时候,我们知道虚拟中断可用于域之间的交互,rtai_sysreq_virq就是用于RTAI请求Linux执行某些必须在Linux域内执行的操作。Linux域内处理这个虚拟中断的入口点是rtai_ssrq_trampoline,这是在初始化函数中通过调用adeos_virtualize_irq实现的。 2.用rtai_syscall_trampoline替换Adeos默认的系统调用入口函数adeos_syscall_entry,这是用来处理特殊的RTAI系统调用(与一般的Linux系统调用相比)和实现LXRT(Linux RealTime,一种通过RTAI在Linux用户空间实现软实时的机制)。 3.调用rtai_proc_register注册RTAI相关的proc文件系统项,通过标准Linux下的proc文件系统提供相关的RTAI系统信息。 4.调用rtai_archdep_init进行一些与硬件体系结构相关的初始化操作。 5.初始化RTAI的属性结构变量,然后调用Adeos的域注册接口adeos_register_domain进行RTAI域的注册,RTAI域实体变量名为rtai_domain。 从前面章节对Adeos域注册的分析,我们知道在域注册的最后,如果新注册的域指定了自己的域初始化函数,Adeos会切换到新注册的域,新域开始运行的时候会调用域本身的初始化函数,RTAI域本身的初始化函数是rtai_domain_entry,位于文件rtai-3.2/base/arch/arm/hal/hal.c中,它完成的主要功能如下(注意,此时的初始化操作都是在RTAI域中完成的): 1.通过adeos_virtualize_irq向Adeos注册RTAI的中断处理函数rtai_irq_trampoline,注册时的处理中断模式掩码为IPIPE_DYNAMIC_MASK,这也就是告诉Adeos不要将中断直接传递给中断管道的下一个低优先级域(在这里就是Linux域),而是由RTAI的中断处理程序来决定是否将这个中断传给下一个低优先级的域(可以通过调用adeos_propagate_irq将中断沿着中断管道向下传播)。 2.通过adeos_catch_event向Adeos注册RTAI的事件和异常处理函数rtai_trap_fault。 3.调用adeos_suspend_domain挂起RTAI域。 --------------- RTAI中断处理 RTAI的中断处理入口是rtai_irq_trampoline,这是在RTAI的初始化过程中向Adeos注册的。它完成的功能很简单,就是根据中断号调用相应的RTAI中断处理程序,当RTAI没有注册某个中断的中断服务程序时,它会通过调用Adeos接口函数adeos_propagate将这个中断沿着Adeos中断管道传递到下一个低优先级的域。 RTAI的中断管理信息保存在结构变量rtai_realtime_irq中,其定义如下:
struct { rt_irq_handler_t handler; void *cookie; int retmode; } rtai_realtime_irq[NR_IRQS]
每一个中断对应这数组rtai_realtime_irq中的一个元素,其中,handler用于保存中断处理函数,cookie用于保存传递给中断处理函数的特殊信息;retmode在基于ARM的实现中没有用到。 另外,RTAI提供三个函数用于中断请求、中断释放和cookie的设置,它们分别是rt_request_irq,rt_release_irq,rt_set_irq_cookie。
-------------------- 时钟中断服务程序 RTAI的时钟中断服务程序rt_timer_handler位于文件rtai-3.2/base/sched/sched.c中,每当系统时钟中断发生的时候,它就处理RTAI中与时钟相关的信息,并有可能进行任务的调度,它类似于Linux中的do_timer函数。它完成的主要功能如下: 1.更新系统的时间信息,RTAI将系统的时间信息保存在结构变量rt_times中,它的结构定义如下:
struct rt_times { int linux_tick; int periodic_tick; RTIME tick_time; RTIME linux_time; RTIME intr_time; };
其中,linux_tick表示Linux的一个系统周期(Linux每秒的系统周期数为HZ)所需的时钟的嘀嗒数;periodic_tick表示RTAI所设置的时钟(定时器)的的运行周期(也就是定时器每计数periodic_tick次就产生一次时钟中断);tick_time表示上一次时钟中断产生时系统所经过的时钟嘀嗒数;linux_time表示下次Linux时钟中断应该产生的时间(以时钟嘀嗒数为单位);intr_time表示下次RTAI时钟中断应该产生的时间。 2.调用wake_up_timed_tasks唤醒RTAI中的超时任务,并调用宏RR_YIELD和TASK_TO_SCHEDULE找到下一个应该被调度运行的任务。 3.根据时钟的运行模式(单触发模式或周期模式)更新系统时间信息,并对定时器进行相应的设置。 4.根据需要进行任务的重新调度。
----------------- 实时任务的管理 在RTAI中,一个实时任务的生成一般包括任务创建、设置定时器、启动时实任务几个阶段,下面就这几个方面进行分析。 1 实时任务创建 实时任务的创建主要完成对代表实时任务实体的任务结构变量的初始化操作,包括分配任务栈、初始化任务栈、初始化链表指针等。任务的初始化操作最终由函数int rt_task_init_cpuid(RT_TASK *task, void (*rt_thread)(int), int data, int stack_size, int priority, int uses_fpu, void(*signal)(void), unsigned int cpuid)来完成。参数列表中的task是指向任务结构变量的指针,这个结构变量应该在初始化任务之前由任务的创建者分配;rt_thread是一个函数指针,指向代表任务的函数,参数列表中的data则是传递给这个函数的参数;stack_size指明任务所需的任务栈大小,priority指明任务的优先级,uses_fpu指明任务是否使用FPU;signal是一个函数指针,它所指向的函数在任务切换的时候被调度,它的调用方式有点类似于Linux中的信号;而cpuid则指明在多处理器系统中优先考虑将任务放在某个处理器上运行。 函数rt_task_init_cpuid的定义在文件rtai-3.2/base/sched/sched.c中,它完成的主要功能如下: 1.调用sched_malloc分配任务栈; 2.初始化任务结构变量(task所指向的RT_TASK结构)内的字段; 3.调用init_arch_stack为任务的首次运行准备任务栈内容; 4.将任务加入实时任务列表; 一个实时任务的绝大部分属性都保存在实时任务的RT_TASK结构变量中,下面对这个结构进行简要的说明。
typedef struct rt_task_struct {
int *stack __attribute__ ((__aligned__ (L1_CACHE_BYTES))); int uses_fpu; int magic; volatile int state, running; unsigned long runnable_on_cpus; int *stack_bottom; volatile int priority; int base_priority; int policy; int sched_lock_priority; struct rt_task_struct *prio_passed_to; RTIME period; RTIME resume_time; RTIME yield_time; int rr_quantum; int rr_remaining; int suspdepth; struct rt_queue queue; int owndres; struct rt_queue *blocked_on; struct rt_queue msg_queue; int tid; /* trace ID */ unsigned msg; struct rt_queue ret_queue; void (*signal)(void); FPU_ENV fpu_reg __attribute__ ((__aligned__ (L1_CACHE_BYTES))); struct rt_task_struct *prev; struct rt_task_struct *next; struct rt_task_struct *tprev; struct rt_task_struct *tnext; struct rt_task_struct *rprev; struct rt_task_struct *rnext;
/* Appended for calls from LINUX. */ int *fun_args, *bstack; struct task_struct *lnxtsk; long long retval; char *msg_buf[2]; int max_msg_size[2]; char task_name[16]; void *system_data_ptr; struct rt_task_struct *nextp; struct rt_task_struct *prevp;
/* Added to support user specific trap handlers. */ RT_TRAP_HANDLER task_trap_handler[RTAI_NR_TRAPS];
/* Added from rtai-22. */ void (*usp_signal)(void); volatile unsigned long pstate; unsigned long usp_flags; unsigned long usp_flags_mask; unsigned long force_soft; volatile int is_hard;
void *trap_handler_data; struct rt_task_struct *linux_syscall_server;
/* For use by watchdog. */ int resync_frame;
/* For use by exit handler functions. */ XHDL *ExitHook;
RTIME exectime[2]; struct mcb_t mcb;
/* Real time heaps. */ struct rt_heap_t heap[2];
} RT_TASK __attribute__ ((__aligned__ (L1_CACHE_BYTES)));
其中,stack用于保存实时任务的当前堆栈指针;uses_fpu表示任务是否使用FPU;magic用于标识这个结构是否是RT_TASK数据结构;字段state,running用于表示任务所处的状态;字段runnable_on_cpus用于表示任务当前运行的CPU;stack_bottom指向任务对栈的栈底;priority表示任务的当前优先级,由于优先级继承的机制存在,这个字段的指有可能会和base_prority的值不同;base_prority表示任务本身的优先级;字段prio_passed_to表示这个任务在优先级继承机制中将优先级传给了那个任务;字段period,resume_time,yield_time用于任务的调度;rr_quantum,rr_remaining用于同等优先级任务的时间片轮转调度;字段suspdepth用于记录任务被挂起的次数;字段signal保存任务的一个类似于Linux下的信号处理函数,这个函数在任务调度的时候会被调用;字段prev,next,tprev,tnext,rprev,rnext用于任务的队列管理; 2 实时任务定时器设置 在创建实时任务的过程中,主要通过3个函数对定时器进行设置,它们分别是:rt_set_oneshot_mode,rt_set_periodic_mode和start_rt_timer,下面分别对这3个函数进行分析。 rt_set_oneshot_mode用于将定时器设置为单触发模式,所谓单触发模式,就是说,每当定时器产生一次中断后,系统都要根据目前系统任务对时间精度的要求情况对定时器重新进行编程,设定下一次触发的时间。rt_set_oneshot_mode完成的功能如下: 1.调用stop_rt_timer停止定时器的运行; 2.设置全局变量oneshot_timer的值为1;在时钟中断函数中,系统会检查oneshot_timer的值,判断定时器的工作模式,然后根据定时器的模式对定时器进行相应的操作。 rt_set_periodic_mode的操作与rt_set_oneshot_mode的类似,只不过它将oneshot_timer的值设为0,表示定时器应工作在周期模式;当定时器工作在周期模式下时,系统只要对定时器进行一次初始化,指定定时器产生中断的周期,以后就不再需要对定时器进行编程了。 start_rt_timer根据设定的定时器运行模式对定时器进行初始化,它完成的主要功能如下: 1.调用函数rt_request_timer注册时钟中断服务程序rt_timer_handler; 2.初始化系统用于保存时间信息的结构rt_smp_times(就是前面所说的rt_times); 3.调用rt_request_linux_irq注册一个Linux下的中断服务程序recover_jiffies,这个中断程序和Linux的时钟中断服务程序共享时钟中断,recover_jiffies用于补偿Linux所丢失的时钟中断(因为可能由于实时任务的运行,使Linux很长一段时间得不到运行的机会,无法响应时钟中断)。 3 实时任务调度 实时任务的调度是通过函数rt_schedule完成的,这个函数完成的功能与rt_timer_handler完成的功能类似,它的定义在文件rtai-3.2/base/sched/sched.c中,它的主要功能如下: 1.通过调用函数wake_up_timed_tasks唤醒超时的任务; 2.调用宏RR_YIELD()和TASK_TO_SCHEDULE()找到下一个优先级最高的任务; 3.更新系统的时间相关信息rt_times; 4.根据需要,如果定时器工作在单触发模式,则根据系统时间要求对定时器进行编程; 5.如果下一个最高优先级的任务不是当前运行的任务,则进行任务切换; Adeos分析 1 简介 Adeos的全称是Adaptive Domain Environment for Operating System,它的目标是为操作系统提供了一个灵活的、可扩展的自适应环境,在这个环境下,多个相同或不同的操作系统可以共存,共享硬件资源; 随着历史的发展,目前已经存在了不少优秀的操作系统,正是因为这些操作系统的共同存在发展,而产生了以下的问题: 一方面,对于那些面向相同的用户,具有相似设计理念和功能的操作系统来说,它们之间是不兼容或不完全兼容的,例如,在Windows操作系统上的应用程序是不能直接在Linux上运行的;这就将用户(包括程序设计者和系统管理员)限制在了一个固定的软件环境下,用户在应用的选择上缺少灵活性。 另一方面,由于最初的应用环境和面向的用户的不同,有些操作系统在设计理念和功能上完全不同;但是,随着计算机技术的发展,现在的一个计算机系统往往具有以前多个系统的功能,各种计算机系统之间的界限越来越模糊;例如,现在的终端电子设备对实时性的要求越来越高(如智能手机),而某些实时系统也对人机界面的交互有了更高的要求,这就会促使这两种应用环境下的操作系统相互融合。 目前,主要存在两类方法使多个操作系统运行在同一个系统上;第一类方法是模拟,例如,VMWare,Plex86和VirtualPC等;它们都是在已有的操作系统上提供一个虚拟的硬件环境,在这个虚拟硬件环境下可以运行另外操作系统。这样,用户就可以充分的利用两个或多个系统所提供的功能和软件;但这种方法最大的缺点就是会极大的降低系统的性能,因为它包含三个软件层次:主机操作系统(Host Operating System)->虚拟硬件环境->客户操作系统(Guest Operating System)。第二类方法,是在硬件上实现一个所谓的超微内核(nano-kernel),通过这个超微内核实现硬件的共享,然后再在这个内核上构建实用的操作系统,例如,SPACE,Cache kernel和Exokernel等,但这些方法都没有考虑当前已经存在的操作系统和用户。 而Adeos是在已有的操作系统下插入一个软件层,通过向上层多个操作系统提供某些原语和机制而实现硬件共享。但是Adeos并不对硬件的使用强加任何的限制,上层的操作系统仍然可以自由的操作硬件,而不会因为Adeos的存在而有任何的约束(实际上,上层的操作系统可以完全不知道有Adeos的存在)。Adeos除了可以实现操作系统对系统资源的共享之外,还可以用于新的操作系统的开发、操作系统内核的调试、跟踪等。 目前,Adeos是基于Linux内核实现的,主要的应用是在Linux的实时化方面,使基于Linux的系统能满足硬实时的要求(Linux+RTAI)。
--------- 2 基本原理 在基于Adeos的系统中,每个操作系统都在独立的域内运行(但不一定所有的域内实现的都是操作系统,也可以是完成其它功能的软件实体),每个域可以有独立的地址空间和类似于进程、虚拟内存等的软件抽象层,而且这些资源也可以由不同的域共享。 在基于Adeos的系统中,存在着四种类型的交互,如图3-1所示;
A类交互是各个域对硬件的直接操作,这些操作包括访存和和对硬件的设置等,在这种情况下,就和Adeos不存在一样;B类交互是双向的,一方面Adeos接收硬件产生的中断和异常,另一方面,Adeos也直接控制硬件;C类交互指当Adeos接收到硬件中断后,会执行相应域的中断服务程序;D类交互指当域内的操作系统知道有Adeos存在的时候,它可以主动向Adeos请求某些服务,例如,请求共享其它域中的资源、请求授权域优先级等。通过D类交互,可以实现各个域之间的通讯。 对于一个计算机系统来说,系统的运行是由内部和外部的中断和异常所触发的,例如系统时钟中断对操作系统来说就是最重要的,操作系统没有了系统时钟中断,就像人没有了心跳一样,所以说,如果想要控制操作系统的运行,最直接的方法就是接管操作系统的中断处理机制。所以,Adeos的主要工作就是管理硬件的中断,根据域的优先级依次执行相应域的中断服务程序,从而驱动域内的系统运行;同时,Adeos还提供域之间的通信机制、实现域的调度等。 为了实现对中断的管理和域之间的优先级控制,Adeos使用了中断管道(Interrupt Pipe)的概念,如图3-2所示;
Adeos通过中断管道在不同的域之间传播中断,而且提供了相应的机制可以让域改变自己在中断管道中的优先级,在图3-2中,各个域的优先级为:域1>域2>……>域N>Idle域; 通常,在操作系统中,对中断的处理方式有两种:允许中断和禁止中断;但在基于Adeos的系统中,由于存在着中断管道,域内的操作系统对中断的处理方式还有另外两种:抛弃中断和终止中断。如果某个域允许中断,中断产生后,Adeos会调用相应域的中断处理程序,这和不存在Adeos的情况是类似的,只不过在这种情况下,中断服务程序由Adeos负责调用;如果某个域禁止中断(实际上并没有真正禁止硬件中断,而只是设置了一个软件标志),当硬件中断沿着中断管道传播到这个域的时候,Adeos既不调用相应域的中断处理程序,也不会将此中断沿着中断管道进一步向下传播,而只是将这个硬件中断的中断类型和环境参数保存起来,并更新这个中断的中断次数。当域允许中断后,Adeos再根据中断类型、环境参数和中断次数调用相应的中断处理程序,并将此硬件中断沿着中断管道进一步向下传播。如果某个域抛弃某个硬件中断,当中断传播到这个域的时候,Adeos不做任何的处理,直接将这个中断沿着中断管道向后传播。如果某个域终止某个中断,当中断传播到这个域的时候,Adeos根据这个域的设置处理完这个中断之后,不再将这个中断沿着中断管道向后传播,也就是说,后面低优先级的域将不知道有这个硬件中断的产生。 所以,Adeos就是通过控制系统的中断来实现对各个域内操作系统的控制。从图3-3可以对基于Adeos的系统的运行模型有一个整体的概念;其中,D1、D2分别代表两个域,且优先级为D1>D2,为了使描述更加清晰明了,对系统作如下的假设: 系统有两个域D1和D2,两个域完全一样,除了优先级D1>D2,且两个域都允许中断; 整个系统只有两个硬件中断INT1和INT2; 每个域有两个中断服务程序ISR1和ISR2,分别对应于INT1和INT2; 每个域有两个任务TASK1和TASK2(不包括IDLE任务),分别由ISR1和ISR2触发运行;且TASK1的优先级高于TASK2(只有当TASK1任务完成后,TASK2才能开始运行);
从图3-3可以看出,在T1时刻,Adeos接收到了硬件的中断信号,然后就开始遍历中断管道,找到最高优先级的域D1,然后执行域D1的中断服务程序ISR1,ISR1执行完后,就切换到域D1,D1内的任务TASK1开始运行;TASK1运行完成后,域D1就被挂起(Suspended),Adeos然后执行D2的中断服务程序ISR1,ISR1执行完后,就切换到域D2,开始D2内TASK1的运行;当D2的TASK1运行到T2时刻时,硬件产生了中断信号INT2,域D2被中断,Adeos接收到INT2后,又再一次开始从头遍历中断管道,找到了最高优先级的域D1,然后执行D2的中断服务程序ISR2,ISR2执行完后,就切换到域D1,开始D1内任务TASK2的运行;TASK2运行完成后,域D1被挂起,Adeos然后执行D2的中断服务程序ISR2,ISR2执行完后,就切换到域D2,并开始域D2内被中断的任务TASK1和新触发的任务TASK2的执行;当域D2的任务都执行完成后,域D2被挂起,系统进入IDLE状态。 3 基于Linux的实现 考虑到从硬件层开始构建一个操作系统的难度,Adeos并没有并没有从零开始构建一个硬件抽象层;目前,Adeos是基于Linux内核实现的,这样的话,就可以将系统的启动和初始化工作都由Linux来完成,在系统完成初始化后,再进行Adeos的初始化工作(包括接管Linux的中断管理机制),Adeos功能既可以直接编译进内核,也可以作为一个内核模块在系统运行时动态加载,就和内核的驱动程序模块一样。 在这种实现方法下,Linux作为Adeos的一个特殊的域存在,我们称之为根域(Root Domain)。Adeos的很多功能都是依靠根域(也就是Linux内核)来实现的,例如,动态注册其它的域模块是通过Linux的动态模块加载功能实现的,为其它域的任务分配任务堆栈是通过Linux内存分配接口实现的等。根域的初始化是在Adeos的初始化过程中完成的;根域对于Adeos来说,有一点类似于Linux初始化过程中创建的INIT进程; ------------------ 3.1 基本架构 Adeos在Linux配置中增加了三个配置开关来配置Adeos的代码:CONFIG_ADEOS_CORE,CONFIG_ADEOS和CONFIG_ADEOS_MODULE。如果定义了CONFIG_ADEOS_CORE,Adeos的核心支持就被编译进了Linux内核,不论Adeos功能最终编译进内核还是编译成可动态加载的模块,这个编译选项都必须被定义;如果定义了CONFIG_ADEOS,则也隐含着对CONFIG_ADEOS_CORE的定义,Adeos功能就被编译进了内核,那么,从Linux启动以后,Adeos功能就被使能了;如果定义了CONFIG_ADEOS_MODULE,则也隐含着对CONFIG_ADEOS_CORE的定义,Adeos功能被编译成可动态加载的模块,只有当这个模块被加载后,Adeos的功能才会起作用。 Adeos对Linux源代码树的修改涉及到30多个文件(包括新增加的文件),如图3-4所示。
其中的Makefie和Config.in文件,是为Adeos代码添加内核编译选项和内核配置选项; adeos目录下的generic.c包含了与平台无关的通用的Adeos代码,而armv.c则包含了与ARM平台相关的通用于ARM平台的Adeos代码; 在arch/arm/kernel目录下,adeos.c中包含了Adeos关于中断处理的代码;armksyms.c中增加了代码,导出了ARM体系相关的Adeos接口;entry-armv.S和entry-common.S中增加了截获Linux中断和系统事件的代码,域的切换代码也在此处实现;同时,修改了irq.c以适应Adeos的中断处理机制;在process.c中修改了Linux的idle进程,当Linux进入idle状态时,将通知Adeos;time.c中增加了Adeos中修改定时器频率的接口; Documentation/adeos.txt对Adeos进行了简单的介绍; 在init/main.c中增加了对Adeos进行初始化的代码; kernel/adeos.c中包含的是Adeos对根域也就是Linux操作的代码;对kernel目录下的其它文件的修改,主要是为了增加Adeos对Linux系统事件进行捕获所需的代码; 3.2 源代码分析 ------------------------- 3.2.1 Adeos相关实体 这里根据Adeos内全局量的重要性,对所有组成Adeos实体的全局量进行分析,包括其初始化、取值范围、功能等。
struct list_head __adeos_pipeline; spinlock_t __adeos_pipelock = SPIN_LOCK_UNLOCKED;
在文件kernel/adeos.c中定义;Adeos的中断管道是用Linux的标准链表来实现的,而这个链表的头就是__adeos_pipeline,它实际上就是中断管道的访问入口,每个域都通过一个链表项按优先级连接在这个链表上。而__adeos_pipelock则是用来保护中断管道的互斥访问。
static adomain_t adeos_root_domain; adomain_t *adp_root = &adeos_root_domain; adomain_t *adp_cpu_current[ADEOS_NR_CPUS] = { [ 0 ... ADEOS_NR_CPUS - 1] = &adeos_root_domain };
在文件kernel/adeos.c中定义(本分析中的文件路径都是相对于Linux内核代码树的根目录而言的);adeos_root_domain是由Adeos静态定义的根域(也就是Linux);adp_root是指向根域的指针;而数组adp_cpu_current[]则是保存着指向CPU上当前运行的域的指针,ADEOS_NR_CPUS是系统中CPU的个数。
int __adeos_event_monitors[ADEOS_NR_EVENTS] = { [ 0 ... ADEOS_NR_EVENTS - 1] = 0 };
在文件kernel/adeos.c中定义;此数组中的每个元素对应着Linux中的一个系统事件(例如,进入系统调用事件、退出系统调用事件等),当某个域想要跟踪某个系统事件是,它就调用adeos_catch_event,告诉Adeos它想要跟踪这个系统事件,并将事件的处理函数传递给Adeos。Adeos根据事件的类型将__adeos_event_monitors数组中的某个元素的值加一或减一(当事件处理函数为空时减一,否则加一)。当某个系统事件发生时,Adeos根据事件类型,检查__adeos_event_monitors数组中的相应元素的值是否大于零,如果大于零,则说明某个域想跟踪这个系统事件,那么就将这个事件传递给中断管道处理,处理过程中会调用域的事件处理程序。
static struct irqdesc __adeos_std_irq_desc[NR_IRQS];
在文件adeos/armv.c中定义的,它的赋值是在__adeos_enable_pipeline中进行的,代码段如下:
for (irq = 0; irq < NR_IRQS; irq++) __adeos_std_irq_desc[irq] = irq_desc[irq];
其中的数组irq_desc[]是标准Linux中用来保存中断描述符的;所以,从这里就可以看出,__adeos_std_irq_desc[]数组保存的是标准的Linux中断描述符;由于Adeos需要接管Linux的中断管理,就要修改irq_desc[]数组的内容,因此,就将修改前的标准的Linux中断描述符保存起来。一方面,在卸载Adeos模块的时候可以恢复(在函数__adeos_disable_pipeline中调用),另一方面,在Adeos的代码中还需要调用标准描述符中的代码(例如,在__adeos_override_irq_mask中)。
unsigned long __adeos_virtual_irq_map = 0;
在文件kernel/adeos.c中定义的;变量__adeos_virtual_irq_map记录着虚拟中断的分配情况,在32位处理器上,共有32个虚拟中断,从0到31;当某一位为1时,表示虚拟中断已经被分配了,为0时,则没有分配;Adeos用虚拟中断来提供域之间的通信,当某个域需要执行另一个域的某些操作时,可以触发一个虚拟中断,当这个虚拟中断沿着中断管道传播时,接收这个虚拟中断的域就会执行相应的操作,这在处理上和硬件中断类似,只不过这个中断是由域触发的而已。
int adp_pipelined = 0;
在文件arch/arm/kernel/adeos.c中定义的;表示Adeos是否已经接管根域的中断管理,也就是说,表示中断管道当前是否有效。很多代码都要通过判断这个标志量来确定应该执行的操作。 ---------------------------- 3.2.2 域(Domain)相关实体 Adeos中的每个域都由一个域结构变量表示,这个结构变量保存着域的属性和和状态;域结构adomain_t的定义如下:
typedef struct adomain {
/* -- Section: offset-based references are made on these fields from inline assembly code. Please don't move or reorder. */ void (*dswitch)(void); /* Domain switch hook */ int *esp[ADEOS_NR_CPUS]; /* Domain stack pointers */ /* -- End of section. */
int *estackbase[ADEOS_NR_CPUS];
unsigned domid;
const char *name;
int priority;
struct adcpudata { unsigned long status; unsigned long irq_pending_hi; unsigned long irq_pending_lo[IPIPE_IRQ_IWORDS]; unsigned irq_hits[IPIPE_NR_IRQS]; adevinfo_t event_info; } cpudata[ADEOS_NR_CPUS];
struct { int (*acknowledge)(unsigned irq); void (*handler)(unsigned irq); unsigned long control; } irqs[IPIPE_NR_IRQS];
struct { void (*handler)(adevinfo_t *evinfo); } events[ADEOS_NR_EVENTS];
int ptd_keymax; int ptd_keycount; unsigned long ptd_keymap; void (*ptd_setfun)(int, void *); void *(*ptd_getfun)(int);
struct adomain *m_link; /* Link in mutex sleep queue */
struct list_head p_link; /* Link in pipeline */
} adomain_t;
dswitch是一个函数指针,指向一个钩子函数,这个函数在域的切换过程中被调用;esp[ADEOS_NR_CPUS]是一个指针数组,域在不同的CPU上运行的时候有不同的栈,而esp则用来在域切换之前保存当前栈的位置;estackbase[ADEOS_NR_CPUS]则保存着每个栈的栈底;domid是域的识别号,这个值对每个域都是唯一的;name则是域的名字;priority表示域的优先级,也就是域在中断管道中的优先级;cpudata和irqs都与中断处理有关,在后面分析Adeos的中断处理的时候再详细讨论;events保存着域的事件处理程序;所有以ptd_开头的字段,都和所谓的Per-thread data key有关;m_link是一个指向域的指针,用于域睡眠时加入等待队列;p_link则是用于中断管道的连接。 ------------------------ 3.2.3 Adeos初始化 Adeos的初始化是在Linux的初始化过程中调用的,是由函数start_kernel来调用,初始化函数为__adeos_init();这个函数的定义在文件kernel/adeos.c中,它完成的主要工作如下: 初始化根域adeos_root_domain; 分配一个虚拟中断__adeos_printk_virq,主要用来完成其它域对根域请求的printk内核输出操作; Adeos接管Linux的中断管理机制,这是通过调用__adeos_takeover()实现的; __adeos_takeover()仅仅调用__adeos_enable_pipeline()来完成中断的接管操作;__adeos_enable_pipeline()定义在adeos/armv.c中,它的主要功能如下: 1.调用函数adeos_virtualize_irq指定所有根域中断的中断处理函数和中断确认函数; 2.将Linux原来的中断描述符保存在数组__adeos_std_irq_desc[]中; 3.用Adeos相关的中断屏蔽、中断响应等函数替换原有Linux的相应函数; 4.将标志变量adp_pipelined置为1; 至此,Adeos的初始化就已经完成;初始化完成后,系统中存在着一个域,那就是根域(root domain),这时Linux的中断管理机制已经由Adeos来管理。 ---------------- 3.2.4 域注册 Adeos系统中,除了根域(root domain)是在Adeos初始化过程中被静态创建的之外,其它的所谓的客户域(client domain),在使用Adeos提供的服务之前,都必须通过域注册函数int adeos_register_domain (adomain_t *adp, adattr_t *attr)先进行注册。参数adp指向将要被注册的域实体结构变量,参数attr则指向一个域属性结构变量,注册函数将根据attr中的属性对adp进行初始化;域注册函数完成的功能如下: 1.检查当前域是否是根域(Linux),只有根域才可以注册其它的域,因为在目前的Adeos实现下,其它的域都要以Linux下内核动态模块的形式存在; 2.遍历所有的域,检查当前需要注册的域是否已经存在(根据域标志domid来判断),如果已经存在,则出错返回; 3.对域结构变量内的字段进行初始化;通过__adeos_init_domain()函数对域的栈进行初始化,并给栈赋初值,为域的首次运行做好准备; 4.查找中断管道的域列表,根据域的优先级找到新域在中断管道中的位置;并将新域插入中断管道; 5.调用__adeos_switch_to()函数切换到新域; 在attr所指的域属性结构中,有一个字段attr->entry,它是一个函数指针,它指向域自己定义的初始化函数,这个函数地址在初始化域的程序栈的时候被保存在了栈中,当域第一次执行的时候,将会从栈中取出这个初始化函数的地址,执行域自己定义的初始化。 ---------------------------- 3.2.5 Adeos中断处理 Adeos中断处理函数的入口是asmlinkage int __adeos_handle_irq (int irq, struct pt_regs *regs),它的定义在arch/arm/kernel/adeos.c中;这个函数既用来处理硬件中断,也用来处理软件中断(这里所说的软件中断是由域触发,用于域之间的通信);所以,这个函数在两个地方被调用。 第一个,是在系统的中断处理入口,在基于ARM的平台中,中断处理入口都在文件arch/arm/kernel/entry-armv.S中,Adeos用这个函数替换了Linux原来的中断处理函数do_IRQ(),例如,USER MODE模式下中断处理代码片断如下:
__irq_usr: sub sp, sp, #S_FRAME_SIZE stmia sp, {r0 - r12} @ save r0 - r12 ldr r4, .LCirq add r8, sp, #S_PC ldmia r4, {r5 - r7} @ get saved PC, SPSR stmia r8, {r5 - r7} @ save pc, psr, old_r0 stmdb r8, {sp, lr}^ alignment_trap r4, r7, __temp_irq zero_fp 1: get_irqnr_and_base r0, r6, r5, lr movne r1, sp adrsvc ne, lr, 1b @ @ routine called with r0 = irq number, r1 = struct pt_regs * @ #ifdef CONFIG_ADEOS_CORE bne __adeos_handle_irq #else /* !CONFIG_ADEOS_CORE */ bne do_IRQ #endif /* CONFIG_ADEOS_CORE */ mov why, #0 get_current_task tsk b ret_to_user
第二个,是在函数adeos_trigger_irq中被调用,当某个域想要触发某个软件中断时,就可以调用软件中断触发函数adeos_trigger_irq产生一个软件中断,这个软件中断就会沿着中断管道传播,触发其它域执行相应的处理函数,这样就可以达到域之间通信的目的。 Adeos中断处理函数__adeos_handle_irq完成的功能如下: 1.判断标志变量adp_pipelined的值,如果为零,则说明中断管道现在不可用,则调用Linux的中断处理入口函数do_IRQ来进行中断处理; 2.依次遍历中断管道,如果域接受相应的中断,则将此中断记录在域的中断日志中,包括记录中断类型和中断次数,然后调用中断应答函数; 3.调用函数__adeos_walk_pipeline,沿着中断管道执行域的中断处理函数,在执行完域的中断处理函数后,Adeos会切换到这个域,也就是将CPU的控制权交给这个域,当这个域执行完所有的操作后,域就主动挂起,控制权交回Adeos,Adeos在执行中断管道中随后的域的中断处理程序并根据需要交换控制权,直到所有的域都执行完。 每个域与中断相关的数据主要存放在域结构变量的两个字段中,分别是cpudata和irqs,它们的定义如下:
struct adcpudata { unsigned long status; unsigned long irq_pending_hi; unsigned long irq_pending_lo[IPIPE_IRQ_IWORDS]; unsigned irq_hits[IPIPE_NR_IRQS]; adevinfo_t event_info; } cpudata[ADEOS_NR_CPUS];
struct { int (*acknowledge)(unsigned irq); void (*handler)(unsigned irq); unsigned long control; } irqs[IPIPE_NR_IRQS];
在结构变量cpudata[ADEOS_NR_CPUS]中,每个元素包含在某个CPU上的中断信息;其中,字段status表示当前域的状态,它的取值范围如下: IPIPE_STALL_FLAG:表示域禁止中断,当硬件中断沿着中断管道传递到这个域的时候,Adoes将不会执行域的中断处理程序,也不会将中断沿着中断管道继续向下传播; IPIPE_SYNC_FLAG:表示Adoes正在为这个域执行中断处理程序(被称为IRQ同步),保护同步代码不被同步执行; IPIPE_XPEND_FLAG:表示域收到了异常(或者说系统事件,如进入、退出系统调用等)通知消息,而且还没有对其进行处理; IPIPE_SLEEP_FLAG:表示域已经将自己挂起,也就是说,现在是其它的域在运行;当一个域刚被注册的时候就是处于被挂起的状态; 在cpudata中,字段irq_pending_hi,irq_pending_lo[IPIPE_IRQ_IWORDS]和irq_hits[IPIPE_NR_IRQS]用于保存域接收到的中断信息。IPIPE_NR_IRQS为系统中断个数(包括用于域间通信的软件中断)irq_hits[]的每个元素对应于某一个中断发生的次数;irq_pending_lo[]是一个映射表,每一位对应于一个中断,当某个中断发生时,就将这个映射表中的相应位置1表示相应的中断发生了;irq_pending_hi也是一个映射表,其中的每一位对应于irq_pending_lo数组中的每一个元素;例如,当接收到32号中断后,irqs_hits[32]的值加1,irq_pending_lo[1]的位0被置1(相当于映射表的位32),irq_pending_hi的位1被置1(表示数组irq_pending_lo的第2个元素有位被置1)。 在cpudata中的字段event_info则是用来保存接收到的系统事件通知的信息,它的结构如下:
typedef struct adevinfo { unsigned domid; unsigned event; void *evdata; volatile int propagate; /* Private */ } adevinfo_t;
其中的字段domid表示系统事件是在哪个域发生的;event表示事件类型;evdata则是传递给事件处理函数的参数;propagate指示执行完事件处理函数后是否将这个事件通知传递给中断管道中后面的域。 irqs[IPIPE_NR_IRQS]保存着中断的中断处理函数、中断应答函数和中断的处理模式;中断处理模式字段control是一个位掩码,目前中断处理共有四种模式: 1.域不处理中断而直接将中断传递给低优先级的域(control = IPIPE_PASS_MASK); 2.域对中断进行处理,但不再将中断传递给低优先级的域(control = IPIPE_HANDLE_MASK); 3.域对中断进行处理并将之传递给低优先级的域(control = IPIPE_HANDLE_MASK | IPIPE_PASS_MASK); 4.域既不对中断进行处理,也不将中断传递给中断管道中优先级的域; 从上面的分析可以看出,Adeos的中断入口函数仅仅只是将中断信息纪录在每个域的中断相关的日志内,每个域的中断处理函数的执行实际上是由中断管道来调度的(根据域的优先级);中断管道对域的中断函数的执行是通过对函数__adeos_walk_pipeline的调用实现的(在函数__adeos_handle_irq的最后被调用)。 在分析__adeos_walk_pipeline之前,先考虑一下一个域在Adeos系统中所处的状态,将会有助于对后面分析的理解。Adeos系统中的每个域,都处于以下3种状态之一: 第一种是域处于运行状态,在这种情况下,中断管道中比当前运行状态的域优先级高的域都处于挂起状态,也就是说,高优先级的域都没有任务、中断或系统事件需要处理,他们都通过调用函数adeos_suspend_domain将本身挂起;而中断管道中比当前运行域优先级的的域则或者处于挂起状态或者处于被中断状态; 第二种是域处于挂起状态,这说明这个域是通过调用adeos_suspend_domain将本身挂起的,在挂起的时候域没有任务或中断或事件需要处理。但经过一段时间后,处于挂起状态的域,可能也有需要处理的中断或事件通知,只不过现在有更高优先级的域在运行而已。 第三种是域处于被中断的状态,当一个域正处于运行态时,如果这个时候产生中断,或是触发了一个系统事件,Adeos就开始进行中断的处理,如有更高优先级的域需要处理这个中断的时候,Adoes就会调度高优先级的域先运行,那么,这个被中断的域就处于被中断状态了。 __adeos_walk_pipeline定义在文件arch/arm/kernel/adeos.c中,它完成的主要功能如下: 1.沿着中断管道从头开始对域进行处理; 2.如果域禁止中断,则不再对之后的域进行中断的处理; 3.如果某个域有需要处理的中断,则调用域切换函数__adeos_switch_to从当前域切换到那个域,那个域在恢复运行的时候会通过调用函数__adeos_sync_stage执行中断处理函数; 4.如果遍历到了当前域,则直接调用__adeos_sync_stage执行当前域的中断处理函数,然后就可以退出函数__adeos_walk_pipeline,紧接着退出Adeos的中断处理函数__adeos_handle_irq,返回被中断的域继续运行。 __adeos_sync_stage的主要功能是执行域的中断处理函数,并更新域结构的中断相关日志。 __adeos_switch_to的功能就是通过调用汇编函数__adeos_switch_domain进行域切换,然后再调用域切换的钩子函数dswitch。 |