Protothread 机制

一、概述

很多传感器操作系统都是基于事件驱动模型的,事件驱动模型不用为每个进程都分配一个进程栈,这对内存资源受限的无线传感器网络嵌入式系统尤为重要。

然而事件驱动模型不支持阻塞等待抽象语句,因此程序员通常用状态机来实现控制流,但这都很复杂。

 

例子:一个假想的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成立为止

 

三、参考资料

posted @ 2016-08-22 17:35  我是老邱  阅读(6502)  评论(0编辑  收藏