【Contiki】Protothread机制

00. 目录

01. 概述

2.1 概述

传统的操作系统使用栈保存进程上下文,每个进程需要一个栈,这对于内存极度受限的传感器设备将难以忍受。protothread机制恰解决了这个问题,通过保存进程被阻塞处的行数(进程结构体的一个变量,unsiged short类型,只需两个字节),从而实现进程切换,当该进程下一次被调度时,通过switch(LINE)跳转到刚才保存的点,恢复执行。整个Contiki只用一个栈,当进程切换时清空,大大节省内存。

2.2 特点

protothread(- Lightweight, Stackless Threads in C)最大特点就是轻量级,每个protothread不需要自己的堆栈,所有的protothread使用同一个堆栈,而保存程序断点用两个字节保存被中断的行数即可。具体特点如下[1]:

Very small RAM overhead - only two bytes per protothread and no extra stacks

Highly portable - the protothreads library is 100% pure C and no architecture specific assembly code

Can be used with or without an OS

Provides blocking wait without full multi-threading or stack-switching

Freely available under a BSD-like open source license

protothread机制很早就有了,Contiki OS只要运用这种机制,protothread机制还可以用于以下情形[1]:

Memory constrained systems

Event-driven protocol stacks

Small embedded systems

Sensor network nodes

Portable C applications

2.3 编程提示

谨慎使用局部变量。当进程切换时,因protothread没有保存堆栈上下文(只使用两个字节保存被中断的行号),故局部变量得不到保存。这就要求使用局部变量要特别小心,一个好的解决方法,使用局部静态变量(加static修饰符),如此,该变量在整个生命周期都存在。

2.4 调度[2]

直接看原文吧。A protothread is driven by repeated calls to the function in which the protothread is running. Each time the function is called, the protothread will run until it blocks or exits. Thus the scheduling of protothreads is done by the application that uses protothreads.

如果还想进一步了解protothread,可以到[3]下载源码,仔细研读。

02. 场景分析

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

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

例子:一个假想的MAC层协议

在这里插入图片描述

用状态机实现:

在这里插入图片描述

实现上述代码,需要先提炼出准确特定的状态state,上述代码有三个状态:ON、OFF、WAITING

要提炼出这几个状态并不简单,而且状态机实现后的代码跟系统功能没有相互对应,可阅读性差。

Contiki采用一种Protothread机制,来化简这个问题。

Protothread可以看作是事件驱动进程的结合,从进程中继承了“阻塞等待”语义,如Protothread提供PT_WAIT_UNTIL等阻塞语句。

Protothread从事件驱动中继承了“低内存开销”和“无栈性(所有进程共用一个栈)”。

Protothread实现:
在这里插入图片描述

03. 相关概念

这里要先明确几个概念: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进程运行过程有很大帮助。

源码目录

c o n t i k i contiki contiki\core\sys\pt.h、 c o n t i k i contiki contiki\core\sys\lc.h、 c o n t i k i contiki contiki\core\sys\lc-switch.h、 c o n t i k i contiki contiki\core\sys\lc-addrlabels.h

04. 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关键字解决这个问题。

05. 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成立为止

06. 附录

参考:Contiki 2.6: Protothreads

参考:dunkels06protothreads.pdf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沧海一笑-dj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值