Contiki 中的 Protothread 实现 OS -- 为什么状态变量不是静态的?

Protothread implementation in Contiki OS -- why the state variable is not static?

我正在阅读 Contiki OS 中原线程实现的源代码,它是由瑞典 SICS 的 Adam Dunkels 开发的。我真的很困惑,它的实现与 Simon Tatham -- that is, why the state variable has not to be static in Adam's protothread 实现在 Simon 的论文中声明为 static 时所展示的协同例程想法之间存在细微差别?

我们先来仔细看看Simon的讨论。例如,如果能够编写一个函数

int function(void) {
   int i;
   for(i=0; i<10; i++)
       return i; //actually won't work in C
}

并连续十次调用函数 return 数字 0 到 9。

这可以通过在此函数中使用以下宏来实现:

#define crBegin static int state=0; switch(state) { case 0:
#define crReturn(i,x) do { state=__LINE__; return x; \
case __LINE__:; } while (0)
#define crFinish }
int function(void) {
    static int i;
    crBegin;
    for (i = 0; i < 10; i++)
        crReturn(1, i);
    crFinish;
}

如预期的那样,调用此函数十次将得到 0 到 9。

不幸的是,如果我们像这样使用 Adam 的局部延续宏包装 switch-case(Contiki src 树中的 /core/sys/lc-switch.h),这将不起作用,即使您将状态变量设置为静态:

typedef unsigned short lc_t;
#define LC_INIT(s) s = 0; // the ";" must be a mistake...
#define LC_RESUME(s) switch(s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) }
int function(void) {
    static int i;
    lc_t s;
    LC_INIT(s);
    LC_RESUME(s);
    for (i = 0; i < 10; i++)
    {    return i;
         LC_SET(s);
    }
    LC_END(s);
}

在这里,就像 Simon 的例子一样,s 作为状态变量工作,它保留由 LC_SET(s) 设置的位置(屈服点)。当函数稍后恢复执行时(从头开始),它将根据 s 的值进行切换。此行为产生的效果是函数在上一次调用设置的屈服位置后继续 运行。

这两组宏的区别是:

  1. 状态变量 s 在 Simon 的示例中是静态的,但在 Adam 的 LC 定义中是非静态的;
  2. crReturn 在它 returns 结果之前设置状态,而在 Adam 的 LC 定义中,LC_SET(s) 纯粹设置状态并标记屈服点。

当然后者不会在函数中使用这个for循环的情况。此 "return and continue" 行为的关键在于状态变量是静态的,并且状态设置在 return 语句之前。显然,LC 宏不满足这两个要求。那么为什么LC宏要这样设计呢?

我现在可以推测的是,这些 LC 宏只是非常低级的原语,不应该以这个 for 循环示例中所示的方式使用。我们需要进一步构建包含这些 LC 原语的 PT 宏,使它们真正有用。并且 crReturn 宏仅用于演示目的,专门适合 for 循环情况,因为并非每次您都想通过 return 从函数中产生执行。

正如您猜对的那样,所有应该在协程 return 之间保存其值的函数局部变量应该是静态的,此外, lc_t 类型的变量描述了协程的当前状态 应该是静态的。要修复您的示例,请在 s.

声明前添加 static

另一件事是你想要return一个值。 Contiki protothreads 不支持 returning 任意值;它们只是描述线程是否仍处于活动状态或已经完成的代码(PT_WAITINGPT_YIELDEDPT_EXITEDPT_ENDED 状态)。

但是,您可以使用 LC_xxx 宏轻松完成这项工作;你还需要一个标志(这个想法与 PT_YIELD() 中的相同):

int function(void) {
    static int i;
    static lc_t s;
    int flag = 0; // not static!
    LC_INIT(s);
    LC_RESUME(s);
    for (i = 0; i < 10; i++) {
        flag = 1;
        LC_SET(s);
        if (flag) { /* don't return if came to this point straight from the invocation of the coroutine `function` */
          return i;
        }
    }
    LC_END(s);
}

Contiki protothread 库使用这些 LC_xxx 宏来实现 PT_xxx 宏,这些宏又用于创建对已处理的应用程序级别的支持(PROCESS_xxx 宏)。

lc_t状态变量实际上与原线程的状态相同:在https://github.com/contiki-os/contiki/blob/master/core/sys/pt.h中,pt结构简单定义为:

struct pt {
  lc_t lc;
};

pt 结构又作为成员包含在 process 结构中(参见 https://github.com/contiki-os/contiki/blob/master/core/sys/process.h)。 Contiki 中的进程结构是全局变量,因此原线程状态存储在原线程协程的不同调用中。

大多数协程局部变量也需要是静态的这一事实通常(在研究论文中)被描述为该编程模型的主要限制之一,但在实践中这并不是什么大问题。

状态变量确实需要位于静态分配的内存中,其中包括您链接到的 Dunkel 示例中的全局变量。如果它是一个自动变量(在堆栈上,而不是静态的),它的值将从函数的一次调用到下一次调用丢失,除了在最普通的程序中。

当使用 lc-switch 实现时,您可以将 Tatham 的 crReturn() 修改为 LC_SET_AND_RETURN() 宏,为 return 添加能力,如下所示,用于需要的功能return 一个值,然后为 void 函数调用 LC_SET(s); return;

#include "lc.h"
#define LC_SET_AND_RETURN(lc, retval) do { lc = __LINE__ ; return retval; case __LINE__: } while (0)

int function(void) {
    static int i;
    static lc_t s;
    LC_RESUME(s);
    for (i = 0; i < 10; i++) {
        LC_SET_AND_RETURN(s, i);
    }
    return -1; // done
    LC_END(s);
}

LC_INIT() 看起来就像 LC_INIT(static lc_t s); 一样被调用。代码

static lc_t s;
LC_INIT(s);

扩展到

static lc_t s;
s = 0;;

这不等同于 static lc_t s = 0;,并导致代码以意想不到的方式运行。

您可以使用 static lc_t LC_INIT(s); 扩展到 static lc_t s = 0;;,但这看起来很有趣。