我曾经介绍过在基于多核操作系统中的一种可能被提供的服务——微线程(详见:http://blog.csdn.net/zenny_chen/archive/2008/07/14/2650389.aspx)。不过,基于其工作机制,我这里想对它的称呼作一下修改,改为“微协作者线程”(Micro Co-worker Thread,MCT)。
这个机制主要是想解决嵌入式RTOS中对于两个核在协同工作时,如果一个核已经完成了该作业,那么另一个核也就可以不必往下做了,直接可以获得当前作业结果,从而可以马上做下面的作业。比如说,我要进行一次大数据量的搜索。那么在通过双核进行搜索操作时可以让一个核搜索前一半元素,让另一个核搜另一半。那么对于传统的方法是在当前线程中再创建两个线程用于搜索,而这两个线程可以被两个核随意调度。这种机制非常适合于对通用型要求比较高的PC操作系统,但是对于嵌入式RTOS而言新创建一个线程,然后再进行上下文保护和恢复的代价可能会比较高(Blackfin561多达45个寄存器需要作为上下文被保存),因此我在此引入微工作者线程这个概念能够使得主作业线程不必进行线程创建而马上进行搜索功能。
这里主要需要解决的问题就是,当一个核完成作业后应当可以立即通知另一个核,说明作业已经完成,可以退出这个子任务的处理。这是需要仔细设计的部分。
以下,我就基于由AnalogDevices(http://www.analog.com)公司提供的Blackfin561 Dual-core DSP来对此进行详细设计。
下面对这款DSP做简单介绍。它的存储器架构是类哈佛的,采用三级存储。L1数据、指令存储是每个核私有的,它无法被其它核访问,并且可以将其一部分配置成Cache,那么可以的L1数据存储空间共有16KB,L1 SRAM的访问速度非常快。L2 SRAM有128KB,其中即可以放数据也可以放代码。它是多核共享的。L3映射到片外存储器,可以是SDRAM,当然,你也可以接DDR。L2和L3根据LDF文件也可以将其中一部分配置为核私有的存储段。L2 SRAM无法作为Cache。也就是说只有L1才能被配置为cache,这意味着Cache是核私有的。这一点很重要。然后,Blackfin561的每个核拥有4KB的Scratch Memory。这个Memory相当于寄存器,它的访问速度同通用寄存器的访问速度是一样的,呵呵。那么在RTOS中,我把它作为系统栈区。一般根据你具体的应用,可以将其中一部分作为系统栈区,而剩下一点可以为一些计算性较强的算法留作临时变量使用。
Blackfin561的函数返回地址被保存在RETS寄存器中,执行RTS指令进行地址返回时就是取RETS寄存器中的值;而RETI保存中断例程的返回地址,执行RTI进行中断返回时,所取的就是RETI中的值。还有一个特殊的地方就是进入中断例程时,系统将会自动切断可屏蔽中断,直到有[--sp] = reti这条指令发布或rti这条指令发布;如果有reti = [sp++]这条指令,那么后面又将处于可屏蔽中断信号切断状态。
为了要完成这个任务对操作系统的一些结构也需要做一些更为灵活的设计。比如说,可以在线程控制块中注册线程启动函数。当这个线程被切换运行前将会执行这个函数,如果有的话;同样可以注册线程中止处理函数,当这个线程被打断或阻塞时,在进行另一个线程的切换前执行这个函数。这个设计目前看来是非常有用的,包括以后的功能扩展都会有帮助。
我目前的RTOS中还引入了DSR(Deferred Interrupt Service Routine)。由于某些任务在高优先级的中断处理例程中去做可能会比较拖累整个系统的实时响应,因此可以先将它们暂存起来,然后到某一合适的时候进行批处理。那么我是用IVG14,也就是软件中断向量1中做这样的事情,然后这个中断处理例程中还包括线程调度处理,所以这个例程的负荷可能会比较高。(软件中断2,也就是IVG15在进入main之前就被开启了,这意味着我们总是在监管模式下工作,对于这样的RTOS而言,使用用户模式,再进行一些系统调用显然是浪费时间的,所以这里对于权限问题不予考虑)。
有了以上背景,我们下面就可以开始对此进行设计了。我将会使用类似PASCAL或VB的伪代码,然后再夹杂C语言的伪代码。每个块内部是对上层的具体实现。比如:
- DO_PRINT
- puts("Hello, world!");
- END_DO
里面,puts("Hello,world");是DO_PRINT的具体实现。
这里还想再提一点。由于在很多嵌入式应用上采用静态表驱动的调度方法,所以会在程序运行之前将自己配置好的线程放入数组中。由于对于本DSP环境拥有核私有的L1和核共享的L2,那么如果线程数不多的话,并且是核私有的,那么可以将其中一些调度频率较高的放入L1,那么将另一些调度频率较高的,并且是可共享的(即这些线程即可能被核A调用也可能被核B调用)放入L2。为了减小线程控制块的尺寸,对于一些不是很重要的信息可以放入用户栈。也就是说用户栈的前几个字节可以被系统占用用来存放一些OS所要用到的一些信息。这些信息可以通过接口的方式被用户获得。这些信息可以包括该线程的事件标志、事件处理结果值、事件触发函数入口地址等。而像线程ID、栈起始地址及大小应该放入控制块内。尤其是ID,因为这可以用来标识一个线程。如果一个线程被动态创建,然后被彻底销毁,连存根(STUB)都没了,这时对它的访问就会出现混乱。因此在一些参数检查时先要用os_find_thread(u32 threadID);这样的接口先检查一下这个线程是否存在。除非你完全使用静态表驱动进行调度。
下面先讲如何创建一个微协作者线程。
- u32 ThreadProc(void* vpParam)
- {
- // Do Something here...
- // Begin Sort Job
- MicroCoworkerThreadHandle mctHandle;
- os_begin_micro_coworker_thread(&mctHandle, &UserSortJob, NULL);
- os_end_micro_coworker_thread(&mctHandle);
- // Do other things here...
- return errCode;
- }
从上面代码可以看出,创建一个微协作者线程的代码同创建一个普通线程的代码差不多。只不过创建普通线程可能还要再设置一下用户栈的起始地址、大小以及线程优先级等属性。而创建一个微协作者线程所需要的信息更少。一个微协作者线程是完全依附于当前线程的,也就是说对于当前线程而言,它是整个线程执行流中的其中一个串行部分。下面先讲一下上面代码中提到的第一个函数:
- static inline u32 sys_get_ret_address(void)
- {
- u32 retAddress;
- asm("%0 = rets;" :
- "=d"(retAddress) :
- );
- return retAddress;
- }
- void os_begin_micro_coworker_thread(
- MicroCoworkerThreadHandle* pHandle,
- void (*pFunc)(void*),
- void* vpParam)
- {
- current_thread->initSP[MCT_RETADDRESS] = sys_get_ret_address();
- sys_set_event(&t_thread->initSP[EVENT_FLAG], EVT_MCT);
- (*pFunc)(vpParam);
- }
上述代码中,由于os_前缀的函数都是OS系统接口函数,所以可以直接访问current_thread,指向当前执行线程的控制块。
另外,sys_get_ret_address()返回的是os_begin_micro_coworker_thread()函数的调用的下一条指令地址,也就是对os_end_micro_coworker_thread()的调用代码。这就意味着,系统可以通过事件触发直接将(*pFunc)(vpParam)的执行给撤掉。这里面可能会发生的问题就是用户栈。好在只要遵守Blackfin系统的函数调用约定就不会有事。这里有一对宏指令——link和unlink。只要在最后使用unlink就能够马上恢复函数调用的上下文。所以这里有一点比较重要的是os_end_micro_coworker_thread()不应该作为真正的函数调用,只能作为宏或内联函数。
然后,上面的有一个sys_set_event函数用来告诉系统,当前线程已经动用了MCT,这个标志的清除必须在用户例程中或是(*pFunc)(vpParam)下面要进行调用一次,这说明本次MCT任务已经结束,即使另一个核也完成作业也不应该被打扰。
好,下面是比较关键的,也是比较核心的部分了,也就是另一个核已经完成了作业,通知这个核马上结束掉作业处理。
那么,双核之间的事件通信是通过IVG7,也就是中断向量7进行的。先来描述一下IVG7:
- void sys_ivg7_isr(void)
- {
- SAVE_CONTEXT();
- // Rolling the event queue and find out the MCT event
- // Then, fetch the thread ID
- if(threadID == current_thread_id)
- {
- sys_set_event(&t_thread->initSP[EVENT_FLAG],
- EVT_MCT_ABORT);
- asm("raise 14;");
- }
- else
- {
- ThreadHandle handle = os_find_thread(threadID);
- if(handle != NULL)
- os_add_thread_starting_routine(handle, &MCTAbortProc);
- }
- }
当然,IVG7向量中还有其它种类的事件要进行处理,这里就针对MCT中止事件专门拎出来讲。这里有一点比较重要的是,如果当前执行MCT的线程被调度走了,那么在它再次被调回来前应该使它中止掉MCT,那么这件事可以放在启动线程例程中进行。而MCTAbortProc要做的也是很简单,就是做current_thread->context[RETI] = current_thread->initSP[MCT_RETADDRESS]即可。
代码中,raise 14就是设置IVG14中断,准备对MCT中止事件进行处理。
下面就来看一下IVG14中的情况。
- void sys_ivg14_isr(void)
- {
- DO_SAVE_CONTEXT
- DO_SAVE_ALL_EXCPT_RETI
- END_DO
- asm("[--sp] = reti;");
- END_DO
- DO_DSR_PROCESSING
- END_DO
- ThreadHandle previousThread = current_thread;
- u32 mask = cli();
- current_thread = sys_schedual_thread();
- if(current_thread != previous_thread)
- {
- if(previous_thread->initSP[EVENT_FLAG] & EVT_MCT_ABORT)
- {
- previous_thread->context[RETI] =
- previous_thread->initSP[MCT_RETADDRESS];
- }
- sys_do_schedual();
- }
- else
- {
- asm("reti = [sp++];");
- sti(mask);
- if(previous_thread->initSP[EVENT_FLAG] & EVT_MCT_ABORT)
- asm("reti = %0;" :
- :
- "d"(previous_thread->initSP[MCT_RETADDRESS]));
- DO_RESTORE_OTHERREGS
- END_DO
- }
- }
这里有几点需要注意。首先,sys_do_schedual()中已经包含sti()操作,所有sys_do_schedual()前面不需要加sti()了,而且如果加的话就有可能会出错,即,使得外部事件发生miss现象。而发布reti = [sp++];这条指令以后,所有可屏蔽中断都会被屏蔽掉,这样可以安全地使外部事件信号得以保存。
这里在进行调度之前先判断是否有其它优先级更高的任务可用,可用的话将会进行调度,这时后也需要执行RETI的赋值操作,因为这之前触发的事件在处理时,状态是current_thread->id == registered_thread_id,所以执行的是触发IVG14动作。而由于此时IVG14已经在处理了,所以这个触发实际上就是无效的。所以在被调度之前应该也要进行一次判断,否则也会发生遗漏。而在调度前进行cli()关中处理,实际上也是这个道理。
如果调度后的线程就是当前线程,那么就直接中断返回。这时如果有MCT_ABORT事件的话,RETI的值就是os_begin_micro_coworker_thread()函数之后的指令地址。
最后是结束MCT例程。这个例程其实很简单,就是做一些清理工作。
- inline void os_end_micro_coworker_thread(MicroCoworkerThreadHandle
- mctHandle)
- {
- if(current_thread->initSP[EVENT_FLAG] & EVT_MCT_ABORT)
- {
- sys_clear_event(¤t_thread->initSP[EVENT_FLAG],
- EVT_MCT_ABORT);
- asm("unlink;");
- DO_RESTORE_OTHER_CONTEXT
- END_DO
- }
- }
当然,可以在开始MCT例程中,在MCT控制块中增加一些寄存器的保存。根据Blackfin53x/561的函数调用约定,只有R7:4,P5:3以及RETS这几个寄存器需要保存,其它都不必要保存。所以如果要对寄存器进行保存和恢复的话只需要保存和恢复这几个即可。在此,我建议在用户例程内调用一次清楚MCT的事件。因为如果在OS接口函数中完成的话可能会引起退出的函数层级会有1层或2层不同的层级。而如果在用户例程最后调用的话则可以保证是2层函数,那么unlink只需连续调用两次即可,否则的话就很难去判断了。或者也可以索性将FP与SP也保存到MCT控制块中,这样就不会有这种问题的困扰了,呵呵。
以上就是对微协作者线程处理的详细设计。这里还没有引出如何创建核B的线程。这个方式有很多,在此也不作介绍了。由于在深更半夜写的,所以可能有些地方写得不严密,欢迎大家指出。如果大家有更好的解决方案或理念也可以提出,我们可以一起探讨。