概述
导读
本文通过分析Contiki的源码,梳理Contiki的process-event模型中的process机制。
引入:
通过前文的阐述我们明白了Contiki的协程工作的逻辑层面。本文在上一篇的基础上,进一步分析协程在语法层面的实现,而这也正是Contiki设计中最精妙的部分。
还是以看门狗协程为例
PROCESS(dog,"dog");//看门狗任务
PROCESS_THREAD(dog, ev, dataa)
{
static struct etimer et;
PROCESS_BEGIN();
etimer_set(&et,CLOCK_SECOND*2);//初始化etimer,并设置延时为2s
while(1)
{
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));//等待etimer过期
spin_watchdog_clear();//喂狗
etimer_restart(&et);//重启etimer
}
PROCESS_END();
}
在之前我们说过上面这段代码中,所有的大写字符串都是宏定义,而这些宏定义正是Contiki的协程在语法层面的具体实现,现在我们就要展开它们,并分析它们是如何工作的。
宏PROCESS(dog,"dog")[1]
#define PROCESS(name, strname)
PROCESS_THREAD(name, ev, dataa);
struct process name = { NULL,
process_thread_##name }
#define PROCESS_THREAD(name, ev, dataa)
static PT_THREAD(process_thread_##name(struct pt *process_pt,
process_event_t ev,
process_data_t dataa))
#define PT_THREAD(name_args) char name_args
以上就是宏展开的全部结果,通过手动替换,我们可以写出宏PROCESS(dog,“dog”)替换的结果:
static char process_thread_dog(struct pt *process_pt,process_event_t ev,process_data_t dataa);
struct process dog = { NULL,process_thread_dog}
替换后的结果简单明了,原来宏PROCESS(dog,“dog”)是声明了一个名称为process_thread_dog的函数,并且定义了一个process,把声明的函数赋值给process。
宏PROCESS_THREAD(dog, ev, dataa)
#define PROCESS_THREAD(name, ev, dataa)
static PT_THREAD(process_thread_##name(struct pt *process_pt,
process_event_t ev,
process_data_t dataa))
同样展开宏,查看结果:
static char process_thread_dog(struct pt *process_pt,process_event_t ev,process_data_t dataa)
原来这个宏是刚才声明的process_thread_dog函数的实现的地方,即协程的实现。
宏PROCESS_BEGIN()
struct pt {
lc_t lc;//保存的正是本协程的行数
};
#define PROCESS_BEGIN()
PT_BEGIN(process_pt)
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; if (PT_YIELD_FLAG) {;} LC_RESUME((pt)->lc)
#define LC_RESUME(s) switch(s) { case 0:
上面正是宏PROCESS_BEGIN()展开的全部结果,看着有些奇怪,不用着急,之所以奇怪是因为宏PROCESS_BEGIN()其实要和其他的宏组合使用,等它们组合到一起,就会发现一切都很合理。
先简单剖析一下这段宏,通过上面分析我们知道执行协程时会传入参数struct pt *process_pt,这个参数记录了协程当前所在行数,我们看到process_pt也确实被传入了宏里面,通过层层展开,PROCESS_BEGIN()变成了如下结果:
char PT_YIELD_FLAG = 1;
if (PT_YIELD_FLAG) {;}
switch(process_pt->lc) { case 0:
我们先把展开的结果搁置,等最后组合时再分析。
宏PROCESS_WAIT_EVENT_UNTIL(c)
#define PROCESS_WAIT_EVENT_UNTIL(c) PROCESS_YIELD_UNTIL(c)
#define PROCESS_YIELD_UNTIL(c)
PT_YIELD_UNTIL(process_pt, c)
//使用这个,会让process执行到这儿时,必然无条件放弃一次cpu,等到下次条件满足时才会获取cpu
#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)
#define LC_SET(s) s = __LINE__; case __LINE__:
这个宏的直观作用就是当条件满足时,顺序执行下面的语句,否则放弃cpu。我们只展开宏,等到组合之后再分析,展开结果:
do {
PT_YIELD_FLAG = 0;
process_pt->lc = __LINE__;
case __LINE__:;
if((PT_YIELD_FLAG == 0) || !(c)) {
return PT_YIELDED;
}
} while(0)
宏PROCESS_END()
#define PROCESS_END()
PT_END(process_pt)
#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0;
PT_INIT(pt); return PT_ENDED; }
#define LC_INIT(s) s = 0;
#define LC_END(s) }
这个宏的作用就是在协程结束时,做清理工作,我们先展开,之后组合起来分析,展开结果:
};
PT_YIELD_FLAG = 0;
process_pt->lc=0;
return PT_ENDED; //PT_ENDED是宏定义,值是3
}
协程语法分析
经过宏展开,我们把所有的展开结果组合起来,分析Contiki的协程工作原理。
原始的看门狗协程定义:
PROCESS(dog,"dog");//看门狗任务
PROCESS_THREAD(dog, ev, dataa)
{
static struct etimer et;
PROCESS_BEGIN();
etimer_set(&et,CLOCK_SECOND*2);//初始化etimer,并设置延时为2s
while(1)
{
PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));//等待etimer过期
spin_watchdog_clear();//喂狗
etimer_restart(&et);//重启etimer
}
PROCESS_END();
}
宏展开的看门狗协程定义:
static char process_thread_dog(struct pt *process_pt,process_event_t ev,process_data_t dataa);
struct process dog = { NULL,process_thread_dog};
static char process_thread_dog(struct pt *process_pt,process_event_t ev,process_data_t dataa)
{
static struct etimer et;
char PT_YIELD_FLAG = 1;
if (PT_YIELD_FLAG) {;}
switch(process_pt->lc) { case 0:
etimer_set(&et,CLOCK_SECOND*2);//初始化etimer,并设置延时为2s
while(1){
do {
PT_YIELD_FLAG = 0;
process_pt->lc = __LINE__;
case __LINE__:
if((PT_YIELD_FLAG == 0) || !(etimer_expired(&et))) {
return PT_YIELDED;
}
} while(0)
spin_watchdog_clear();//喂狗
etimer_restart(&et);//重启etimer
}
};
PT_YIELD_FLAG = 0;
process_pt->lc=0;
return PT_ENDED; //PT_ENDED是宏定义,值是3
}
展开之后的代码很好理解,主要抓住process_pt->lc[2]变量,这个变量在协程第一次启动时被设置为0。第一次进入协程时,case 0的代码区域会被执行,并更新process_pt->lc的值。由于没有break[3],所以switch会继续判断case _LINE,于是case LINE_区域代码也被执行。由于PT_YIELD_FLAG[4] == 0和!(etimer_expired(&et)条件同时满足,故而协程返回,不再执行后面的语句。继续分析第二次进入协程的情况,这时传入的process_pt->lc的值是__LINE,故而直接跳到case __LINE__区域执行。于是此时若(etimer_expired(&et)到期,则协程不会返回,会执行spin_watchdog_clear()和etimer_restart(&et)。之后再继续执行while(1)循环时,会因为PT_YIELD_FLAG == 0条件满足,而从协程返回。
最后,协程的清理工作主要是PT_YIELD_FLAG和process_pt->lc(行数)清0。并向Contiki返回协程结束的状态值。
这里顺带提一下,__LINE__是c语言编译器的内置宏,这个宏记录了当前语句所在的行数。
综观Contiki对协程的设计,其主要是利用了switch-case的语法特点和c编译器的__LINE__宏,并通过宏的方式包装了这些繁琐的语法结构,努力向使用者展示一个语法优雅语意流畅的编程范式。同时又极大的节省了process的栈空间占用。每个process的cpu环境仅仅是2字节的行数信息,不过这也限制了在process中定义临时变量。然而我们可以用static变量代替临时变量,和节省的栈空间相比,额外的这一点语法限制是值得的。
结束语
至此我们完成了对Contiki中协程的逻辑层面的分析和语法层面的分析。理解了Contiki如此小巧轻量级的根本原因,理解了Contiki协作式调度的工作原理和特点,理解了Contiki精巧的语法设计和封装后编程范式的优雅流畅。Contiki中的event-process模型彻底结束了,而Contiki另一个核心模型,定时器模型也开始了,我们在下一篇开始探寻。
项目地址:https://github.com/zhangoneone/stc89c52
参考
^事实上,“dog”并不是必要的,它是给开发者调试用的
^这个变量保存了协程当前的行数,协程执行时使用它,协程放弃时更新它
^c语言中的switch-case语法,如果case不以break结束,会继续下一个case的判断
^PT_YIELD_FLAG这个变量非常重要,它保证了协程逻辑的正确性
最后
以上就是受伤百褶裙为你收集整理的Contiki的内核分析-协程机制(二)的全部内容,希望文章能够帮你解决Contiki的内核分析-协程机制(二)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复