一、概述
很多传感器操作系统都是基于事件驱动模型的,事件驱动模型不用为每个进程都分配一个进程栈,这对内存资源受限的无线传感器网络嵌入式系统尤为重要。
然而事件驱动模型不支持阻塞等待抽象语句,因此程序员通常用状态机来实现控制流,但这都很复杂。
例子:一个假想的MAC层协议
用状态机实现:
实现上述代码,需要先提炼出准确特定的状态state,上述代码有三个状态:ON、OFF、WAITING。
要提炼出这几个状态并不简单,而且状态机实现后的代码跟系统功能没有相互对应,可阅读性差。
Contiki采用一种Protothread机制,来化简这个问题。
Protothread可以看作是事件驱动和进程的结合,从进程中继承了“阻塞等待”语义,如Protothread提供PT_WAIT_UNTIL等阻塞语句。
Protothread从事件驱动中继承了“低内存开销”和“无栈性(所有进程共用一个栈)”。
Protothread实现:
二、实现
1、几个概念
这里要先明确几个概念:Process,Protothread,LC(Local Continuation)
Process是进程,包括两个部分。其中Process Control Block是控制进程的数据结构,The Process Thread是进程执行实体函数。
Process Control Block:
struct process { struct process *next; #if PROCESS_CONF_NO_PROCESS_NAMES #define PROCESS_NAME_STRING(process) "" #else const char *name; #define PROCESS_NAME_STRING(process) (process)->name #endif PT_THREAD((* thread)(struct pt *, process_event_t, process_data_t)); struct pt pt; unsigned char state, needspoll; };
The Process Thread:
PROCESS_THREAD(hello_world_process, ev, data) { PROCESS_BEGIN(); printf("Hello, world\n"); PROCESS_END(); }
Protothread是contiki进程采用的一种机制,结合了事件驱动和进程的特点。
相应数据结构pt
struct pt { lc_t lc; };
LC是local continuation,是Protothread机制的底层支持,用来保存进程运行状态的地方,其实就是保存进程实体函数上次阻塞的位置。
lc_t lc
这几个概念对后续理解contiki进程运行过程有很大帮助。
2、LC代码实现
(1)GCC c 语言拓展实现
lc_t类型如下,是一个指向void的指针:
typedef void * lc_t;
LC_SET(s)采用GCC _label_拓展特性 定义一个标号 resume,然后用 GCC && 拓展特性将标号resume的地址存储在s中,记录阻塞位置,s是lc_t类型。
#define LC_SET(s) \ do { ({ __label__ resume; resume: (s) = &&resume; }); }while(0)
LC_RESUME(s)采用goto语句来恢复到上次阻塞的位置,与LC_SET(s)相对应。
#define LC_RESUME(s) \ do { \ if(s != NULL) { \ goto *s; \ } \ } while(0)
执行前,s初始化为null
#define LC_INIT(s) s = NULL
LC_END(s)为空
#define LC_END(s)
注:这种方法只支持GCC编译器
(2)C Switch 语句实现
lc_t类型如下,是short型
typedef unsigned short lc_t;
LC_SET(s)采用标准__LINE__宏语句,将阻塞时程序执行到的行号记录到s中。
#define LC_SET(s) s = __LINE__; case __LINE__:
LC_RESUME(s)采用switch语句,来恢复到上次阻塞的位置,与LC_SET(s)相对应。
#define LC_RESUME(s) switch(s) { case 0:
执行前s初始化为0。
#define LC_INIT(s) s = 0;
和LC_RESUME(s)中的switch() {相对应。
#define LC_END(s) }
注:这种方法不可嵌套switch语句
注:上述两种方法局部变量在阻塞时都不会保存,可加static关键字解决这个问题。
3、pt代码实现
(1)PT_INIT
#define PT_INIT(pt) LC_INIT((pt)->lc)
初始化Protothread,初始化必须在执行进程实体前初始化。
pt是指向pt结构体的指针。
底层也就是初始化LC。
(2)PT_BEGIN、PT_YIELD、PT_END
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; if (PT_YIELD_FLAG) {;} LC_RESUME((pt)->lc) #define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \ PT_INIT(pt); return PT_ENDED; } #define PT_YIELD(pt) \ do { \ PT_YIELD_FLAG = 0; \ LC_SET((pt)->lc); \ if(PT_YIELD_FLAG == 0) { \ return PT_YIELDED; \ } \ } while(0)
PT_BEGIN中,先设置PT_YIELD_FLAG为1,表示已经YIELD过了,配合YIELD命令。
然后执行LC_RESUME恢复到上次阻塞的地方,如果是第一次运行,则从头开始运行。
PT_END中,只是LC_END,跟PT_BEGIN配合。还有重新做一些初始化工作,并返回PT_ENDED。
PT_YIELD中,功能是进程无条件阻塞。
第一次运行时,先设置PT_YIELD_FLAG为0,然后保存这次无条件阻塞的位置,进程实体函数返回PT_YIELDED值,退出。
YIELD后,重新执行进程实体时,执行PT_BEGIN后,PT_YIELD_FLAG变为1,跳转到上次阻塞的位置后,这次就不会退出了,接着运行。
(3)PT_WAIT_UNTIL
#define PT_WAIT_UNTIL(pt, condition) \ do { \ LC_SET((pt)->lc); \ if(!(condition)) { \ return PT_WAITING; \ } \ } while(0)
先用LC_SET保存阻塞时的位置
然后判断条件condition是否成立,如果不成立,进程实体函数返回PT_WAITING值,退出。
一直阻塞,直到condition成立。
(4)PT_SPAWN
#define PT_SPAWN(pt, child, thread) \ do { \ PT_INIT((child)); \ PT_WAIT_THREAD((pt), (thread)); \ } while(0)
pt,child都是指向结构体pt的指针,pt是父进程的,child是子进程的。
thread是指向子进程的执行实体函数的指针。
PT_INIT((child))先初始化子protothread
#define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread))
#define PT_WAIT_WHILE(pt, cond) PT_WAIT_UNTIL((pt), !(cond))
#define PT_SCHEDULE(f) ((f) < PT_EXITED)
PT_WAIT_WHILE是当条件cond成立时,一直阻塞。PT_WAIT_UNTIL是一直阻塞,直到condition成立。
PT_SCHEDULE(f)判断进程执行实体函数f是否已经退出或者执行完毕。
最后展开为:
PT_WAIT_UNTIL((pt), !((thread) < PT_EXITED)
也就是父进程一直阻塞,直到子进程退出(PT_EXITED)或者执行完毕(PT_ENDED),返回值的相关定义如下
#define PT_WAITING 0 #define PT_YIELDED 1 #define PT_EXITED 2 #define PT_ENDED 3
(5)PT_THREAD
#define PT_THREAD(name_args) char name_args
声明或者定义进程实体函数,name_args包括函数名和参数。
(6)PT_RESTART
#define PT_RESTART(pt) \ do { \ PT_INIT(pt); \ return PT_WAITING; \ } while(0)
重新执行进程实体函数。
(7)PT_EXIT
#define PT_EXIT(pt) \ do { \ PT_INIT(pt); \ return PT_EXITED; \ } while(0)
强制退出进程实体函数。
(8)PT_YIELD_UNTIL
#define PT_YIELD_UNTIL(pt, cond) \ do { \ PT_YIELD_FLAG = 0; \ LC_SET((pt)->lc); \ if((PT_YIELD_FLAG == 0) || !(cond)) { \ return PT_YIELDED; \ } \ } while(0)
YIELD直到条件cond成立为止
三、参考资料
- Protothreads: Simplifying event-driven programming of memory-constrained embedded systems. Adam Dunkels, Oliver Schmidt, Thiemo Voigt, and Muneeb Ali. ACM SenSys 2006.
- 源码:$contiki$\core\sys\pt.h、$contiki$\core\sys\lc.h、$contiki$\core\sys\lc-switch.h、$contiki$\core\sys\lc-addrlabels.h