eCos中断模型

0 中断相关硬件描述

现代嵌入式设备中,设备通过中断控制器向CPU报告自身发生了某些事,如图0所示。

 

图0

图0所示的已经是一个功能比较齐全的SOC架构了。中断控制器中这两个寄存器不一定存在,但是其所代表的功能是存在的,同样,CPU中某寄存器中的两个比特位也不一定都存在,但是CPU中一定有别的东西来代替它(至少i-bit的概念一定是存在的)。

0.1 综述

下面以图0所示架构描述中断发生的情况。

当中断源dev想通过中断的方式告诉CPU某些事情的时候,就在中断线上发出一个信号,这就是中断触发。根据信号实现的不一样,中断触发分为:电平触发和边沿触发。下面的描述以低电平和下降沿两种典型为例。

1)      每隔1(或n)个clock,中断控制器检测与之相连的中断线上信号,如果发现当前为低电平或者下降沿,则把SRCPND寄存器中与该中断线相关的某bit置位;

2)      在没有mask该中断源的前提下,经过仲裁后会把INTPND寄存器中与该中断线相关的某bit置位;

3)      随后中断控制器发request,和CPU们协商(CPU之间通信)把这个中断交给其中一个CPU处理,此时该CPU先去check自身的I-bit(interrupt bit)和F-bit(fast-interrupt bit)。如果当前request是普通中断,且I-bit为0,则CPU accept这个中断,处理之,F-bit也类似。

Note说明下fast interrupt,一般来说只有一个中断源可以被设置为快速中断,所谓快速,即是它无需仲裁,只要存在于SRCPND中就会直接发request而不会像IRQ那样置于INTPND中。

总结下,从外设触发中断到CPU真正处理有两道关卡,首先中断控制器仲裁以决定当前要不要为之发request,其次,request该中断后,CPU要根据自身情况(I,F bits)决定是否accept之。

0.2 ack, mask & clear中断

软件处理中断时经常遇到的问题是mask和ack & clear。

  • Mask某个中断源意味着告诉中断控制器仲裁的时候skip该中断源,不要将之放入INTPND;
  • ack就是清INTPND中相应bit,也就是告诉中断控制器我cpu已经accept你的这个request了,你不需要继续保持这个bit位继续发request;
  • 而clear(清中断)则是让外设在中断线上发一个非中断信号(比如低电平触发时拉高电平),目的是清SRCPND中相应bit位。具体点说是因为中断控制器每隔几个count就会更新SRCPND,当中断线上信号改变后就将SRCPND相应bit置位。

0.3 电平触发和边沿触发的区别

这两者的区别很明显,电平触发时只要中断源不clear中断,信号线会一直保持这个状态(比如低电平),不论interrupt controller什么时间进行check都会检测到该中断的到来;而边沿触发只有在跳变时才会被检测到。

我们更加关心的是这两者带给我们软件处理上的差异。

  • 电平触发:cpu进入ISR中,软件首先需要做的是mask该中断,随后ack,最后clear并重新unmask。在cpu accept本次中断后,由于电平触发的特性,controller再次检测到同一个中断,如果没有mask,它将再次被放入INTPND中,然后controller再次为之发request,等到cpu enable I-bit(或者SMP时有别的cpu空闲)时就会处理这个重复的中断,也就是说同一个中断被cpu处理了两次。当然,如果没有ack,controller也会不断的发request,导致cpu重复处理同一个中断;另一方面,当cpu处理完当前中断后直接clear & unmask中断,如果处理的过程中真的来了同类型中断,该信息也被最后那个clear给抹杀干净了。因为此时是mask的,它不会被仲裁到INTPND中,而clear则从源头把这一信息删除。所以,mask的时间太长容易丢中断Notemask和ack的顺序不能变,如果先ack再mask且中间时差超过一个clock,则这一个clock再次检测到同一个中断,此时还没有mask,这又会导致同一个中断被处理两次的错误。
  • 边沿触发:根据前面论述的边沿触发的特点可以知道,边沿触发时检测到的中断次数是真实有效的,即如果连续两次检测到了同类型中断,说明真的来了两次同种中断,所以当软件处理中断(ISR)时不必mask

        考虑下面的情形:

         i.   clock n时检测到中断,置入SRCPND,仲裁后置入INTPND,controller发request,cpu accept并进入ISR,且在下一个clock到来                            前还只是ack但没有mask;

         ii.   clock n+1时没有检测到中断(和电平触发的差异在此时出现,如果是电平触发就会再次检测到中断),也就没有后面发request等事情,所以即使不mask也不存在同一个中断被处理两次的情况。

         iii.   clock n+2时再次检测到中断,此时的情形和1一样。

以Linux kernel为例,边沿触发时的行为都是先只ack,如果此后在当前request还没处理完前又来了一次同类型中断,那么controller同样发出request,但因为当前cpu正在处理中断中,其I-bit是置位的,所以SMP时会有另外一个cpu request它,但那个cpu不做任何实际事情,只是标志一个状态位(纯软件的不涉及寄存器),当当前cpu处理完第一个中断后检查这个状态位,如果发现置位了就会知道在它处理期间来了一个新的同类型中断,所以就接着处理,直到该状态位不再置位,事实上它在处理第二个同类型中断时,它就不仅仅是ack了而是mask_ack_irq,所以事实上Linux只允许挂起一次。为了不丢中断,最根本的还是要求ISR尽量短。

1 eCos中断模型

eCos3.0的中断处理流程表示如下,它的主框架也代表着一个common的中断处理:

 

2 eCos中断处理的限制

驱动程序编写时有一个通用规则:在中断处理中不能调用会引起阻塞(block)的函数。具体原因我没有见到一个官方的说法,有论坛对此有过一些很激烈的讨论,下面说说我的看法。

首先,阻塞意味着当前执行体(先这么称呼着,用以指代线程和异常处理执行体)切出去,什么时候再回来不一定,所以中断里面阻塞会导致中断函数至少有一部分得不到及时的处理,这和中断处理要求的及时响应的初衷是相违背的。例如,在timer中断中sleep了一下,那完了,整个系统时钟都乱了,调度啥的都没谱了。所以,中断里面不应该阻塞。

2.1 general OS设计层面分析(理论)

接下来我们分析下中断里面是否可以阻塞,如果阻塞会导致什么后果。

在分析这之前,先说说异常处理执行体和线程的区别。通常情况下,OS用scheduler维护了一个调度队列,保存每个线程的状态(running, sleep, wait等)等信息,使得各线程在一个公平的环境下都有机会获得CPU去运行。而对异常处理执行体(这里把中断当作异步异常看待),没有人使用额外资源去维护它,没有状态,没有调度队列。正如上面所说,异常处理就应该简短、一次性全部处理完,不应该使用额外资源使之变成线程的样子。

线程之所以可以阻塞是因为它可以被再次调度回来,那么中断处理是否可以阻塞就看其是否能再次调度回来。理论上,scheduler把线程当作一个调度对象,在适当时候将之从队列里面拉出来,从该线程栈中恢复之前的运行环境(各寄存器),而异常处理执行体本身不是一个调度对象,它如果执行了一半却去执行别的线程代码,谁来恢复它的继续执行?No one。

以上从理论上论述了中断处理不能线程调度,简单来说,就是因为调度是scheduler的事情,而scheduler仅把线程当作调度对象。所以我们会看到在进行中断处理时是关闭scheduler的(scheduler被lock上)。

2.2 eCos中断中的线程切换

=======================================================================

通常情况下,调度、线程切换和阻塞是同义词,但是在eCos下有点不一样。在wait semaphore或lock mutex时会引发线程调度,但同时会改变当前线程状态(sleep也是),如从running到wait,而在signal semaphore或release mutex时仅将等待队列中的线程取出并改变其状态并重新调度,也就是说它不改变当前线程状态。所以,线程切换是调度的子集,而阻塞会引发调度。注意: ITron中release mutex等simple synchronization methods只是改变目标线程状态而不在这个点进行调度,Linux也同样没有实际的调度动作。

这点对下面中断处理函数注意事项的讨论非常重要,故在此事先言明。

2.2.1 eCos的调度时机

eCos中只要也只有调用unlock_inner(cyg_scheduler_sched_lock)会引发调度,无论cyg_scheduler_sched_lock值是不是0。而unlock是要等cyg_scheduler_sched_lock为1时才调用unlock_inner(0 =  cyg_scheduler_sched_lock – 1,注意此时cyg_scheduler_sched_lock的值没变)的,所以unlock操作不一定会引发block(线程切换)。

Wait semaphore一定会引发block,而post semaphore则不一定,它在中断上下文中不会引发调度。这两个函数的实现都是在禁止内核抢占的方式下进行的:lock(禁止内核抢占) -> do something -> unlock。

wait semaphore直接调用unlock_inner()(通过reschedule()函数)以保证任何情况下都进行线程切换。而post semaphore只是:把semaphore等待队列中的线程取出放入可调度队列run_queue并设置一些线程属性,所以在normal线程中(cyg_scheduler_sched_lock为1),post semaphore会进行调度;而在ISR中,如图1所示进入ISR前已经调度加锁(cyg_scheduler_sched_lock至少为2),故此时unlock不会调用unlock_inner;同样的在ISR后进入DSR前(cyg_scheduler_sched_lock >=  1),在cyg_scheduler_sched_lock值为1的条件下,interrupt_end()调用unlock(0),调用call_pending_dsr(),此时用户注册的DSR如果使用post semaphore将进入unlock(2),不会调用unlock_inner, 即post semaphore在DSR中也不会引发调度。

综上所述,eCos的线程调度只会发生在(mutex,sleep之类的类似于semaphore,不单独列出):

1) wait semaphore;

2) normal情况下的post semaphore;

3) 内核抢占(执行完中断的DSRs之后).

注意,线程时间片time_slice轮转就是使用内核抢占的方式达到线程切换的目的。

2.2.2 DSR的执行时机

在RTOS中执行体的执行优先级依次是ISR, DSR以及normal线程。但是它们各自执行的准确时机是在何时呢?

ISR的执行永远是最及时的,只要CPU没有忽略PIC的中断请求,ISR就会立即得到执行。 那什么时候CPU会忽略PIC请求呢?除了用户在代码中显式disable中断,一般就只有在进入ISR前eCos会disable中断,此后,如果在ISR执行过程中的PIC请求将被置为Pending,等待当前ISR执行完后enable中断后进行处理。所以eCos不支持ISR被打断这种意义上的中断嵌套。eCos的中断嵌套表现为:中断被分为ISR和DSR两个部分,进入DSR前enable中断使得中断的第二部分DSR可被打断。

DSR的执行大概有以下两种情况:

  • 未禁止内核抢占时,ISR(s)执行完毕(包括多个ISR被依次执行)后将立即执行DSR(s);

中断处理的第一部分不会被打断:包括ISR和把对应的DSR放入DSR队列,结束后,interrupt_end()中的unlock会调用unlock_inner(),在其中执行call_interrupt_dsr。

  • 禁止抢占内核(lock()函数)时,ISRs执行完之后切到被中断线程执行,unlock()函数不会调用unlock_inner(),即DSR在此次无法得到执行;随即返回到被中断线程继续执行,发现要执行unlock函数,而cyg_scheduler_sched_lock值经过两次unlock递减后为1,故将执行unlock_inner(),继而执行pending dsr这样的函数也挺多,比如normal情形下调用同步原语的相关函数:sleep, wait, post等。

2.2.3 内核抢占引发线程切换

内核抢占的意思是说,当前线程A执行过程中来了中断,用户注册的ISR,DSR处理完毕后要有一次调度机会,根据调度算法,谁有资格执行就让谁执行,结果不一定是A,而非内核抢占的情形是:处理完ISR,DSR后不加判断直接回到线程A中继续run。

如何禁止内核抢占?如wait/post semaphore所示,在执行函数体前调用unlock()即可。这个函数仅将全局调度锁cyg_scheduler_sched_lock加一,如果发生中断,如图1所示进入ISR前中断执行体将cyg_scheduler_sched_lock加一,执行完ISR后调用interrupt_end()时将调用unlock(2),此时将不调用unlock_inner,也就不会调用DSR和线程调度了。

前面说中断处理中不能进行线程调度,中断处理是指用户注册的ISR和DSR,而非整个中断上下文中。事实上,内核抢占中线程切换就是在中断上下文中,看下技术上如何实现:

1) DSR执行完后,把当前中断处理context保存到被中断线程栈中;

2) 切换到目标线程继续执行,这个时候已经是normal状态了,有调度器等;

3) 运行了一段时间后,再次切换到这个被中断线程,从栈中恢复它的context继续执行(发现是中断上下文中也无所谓,因为后面restore、eret等操作时开关中断都是独立的,和ISR,DSR中的上下文不一样,ISR/DSR中依赖的东西太多,无法和normal线程相互切来切去还当没事发生一样,具体的后面会进一步分析)。

这里的技巧是:线程切换时把中断上下文放在被中断线程栈中,这个方式和normal情况下线程切换所用的方式一样,即共享线程栈空间。值得一说的是线程切换函数hal_thread_switch_context实现的很巧妙,考虑下这个问题:PC寄存器值如何保存?因为当前执行体执行到哪里是PC决定的,我们需要将之保存到栈中,而PC是我们无法访问的寄存器。它是这样做的:用汇编写一个叶子函数,没有开辟栈空间,在其中保存ra,当上层调用该函数时,ra被硬件自动置为上层调用时的PC值,等下次恢复时只要jr ra就可以接着上次调用该函数的地方继续run了。

2.2.4 DSR中允许simple synchronization methods

如前所述,wait semaphore直接调用unlock_inner(),一定会引发调度,而post semaphore在ISR和DSR中都不会block,所以DSR中可以调用post semaphore,但是不能调用wait semaphore。

首先,wait semaphore会改变当前线程状态,这是一个错误。也就是说,中断处理过程中无缘无故改变了被中断线程的状态,这是误操作。本来嘛,同步原语是由scheduler控制的,本来就是为了线程调度而开发的,现在你中断跑过去插一脚就没什么道理了。其次,它导致DSR不能快速执行,这和用户注册DSR的目的是违背的,DSR的执行本就该快于任何线程,地位仅次于ISR。

2.2.5 ISR中“不允许”任何同步原语操作

ISR的执行是在关中断情况下执行的,如果此时切换到别的线程执行,那么整个系统永远没有开中断的机会,除非你指望用户线程跑去开个中断。不开中断,整个系统就都挂了,系统timer也没了,就算时间片到了也不会调度。

所以wait semaphore肯定是不能调用的。

但在我看来,这里的不允许是有待商榷的,如前所述,post semaphore在ISR和DSR中都不会block。所以理论上来说,在ISR中可以post semaphore,但是查看eCos的官方文档还是说不能调用任何同步原语,我想这应该是一种trade off,因为ISR应该尽最大能力的快速处理,所以这种可以放到DSR中处理的都应该放到DSR中。但是这个理论我还没验证,有时间可以试试。

2.3 eCos中断处理程序注意事项

除了2.2.2和2.2.3中所论述的的两条限制:

1) DSR中允许简单同步原语;

2) ISR不允许任何同步原语或睡眠,

外,还有一点,就是:

3) 在ISR中mask同类型中断并在DSR退出前unmask该中断。

具体原因在图1中的4)已经说明。除去那个原因,有时候ISR和DSR会共享一些信息如硬件设备上的某些数据ISR还没读完需要DSR接着读,有些数据结构ISR赋了值等着DSR去使用,如果DSR执行前或者执行中被同一个ISR打断,那么前一个共享信息将被覆盖。

吐槽

软件工程中,软件质量除了高效(时间复杂度和空间复杂度)外,还讲究低耦合等因素。但OS似乎有点特殊,在我看来,无论是eCos还是Linux甚至是我之前玩的ITron在耦合性方面做得都不够好。就我经验而言,如果想看明白OS某一个方面,你需要知道OS大多数方面的细节。当然,文件系统和内存管理似乎比较独立,其余的中断处理、调度器和同步原语等相关性则很大。

就eCos而言,我觉得惊讶的是它居然用c++实现了一个OS,用c++去写高效的代码是比较难的(相对于c而言),我只能说eCos的作者对自己c++的功底相当自信。面向对象只是思想,难道c做不到吗?

posted @ 2013-04-14 19:45  randyqiu  Views(1350)  Comments(0Edit  收藏  举报