一、Protothread的特点
Protothread是专为资源有限的系统设计的一种耗费资源特别少并且不使用堆栈的线程模型,其特点是:
以纯C语言实现,无硬件依赖性;
极少的资源需求,每个Protothread仅需要2个额外的字节;
可以用于有操作系统或无操作系统的场合;
支持阻塞操作且没有栈的切换。
使用Protothread实现多任务的最主要的好处在于它的轻量级。每个Protothread不需要拥有自已的堆栈,所有的Protothread共享同一个堆栈空间,这一点对于RAM资源有限的系统尤为有利。相对于操作系统下的多任务而言,每个任务都有自已的堆栈空间,这将消耗大量的RAM资源,而每个Protothread仅使用一个整型值保存当前状态。
二、contiki进程调度
Protothread是contiki进程采用的一种机制,结合了事件驱动和进程的特点。Contiki的进程由两部分组成:进程控制块和进程线程。进程控制块存储在内存中,它包含进程运行时的信息,比如进程名、进程状态、指向进程线程的指针。进程线程是存储在ROM中的一个代码块。
struct process {
struct process* next;
const char* name;
int (*thread)(struct pt* p, process_event_t ev, process_data_t data);
struct pt pt;
unsigned char state, needspoll;
};
其中: struct pt { lc_t lc; } ;
进程控制块是轻量级的,它只需要几个字节的内存。如上面所示,该结构体中任何成员都不能被直接访问,只有进程管理函数能够访问这些成员。
- 进程控制块的第一个成员next指向了进程链表中的下一个进程控制块。
- 成员name指向了进程的文本类型的名字。
- 成员thread是一个函数指针,指向了进程的线程。
- 成员state和needspoll是内部标志,当进程被轮询时,通过process_poll()修改该标志。
进程控制块通过宏PROCESS()定义。进程线程包含进程的代码。进程线程是一个单一的protothread,由进程调度器调度。
PROCESS(hello_world_func, "HelloFunc");
PROCESS_THREAD(hello_world_func, ev, data) {
PROCESS_BEGIN();
printf("Hello, World\n");
PROCESS_END();
}
三、LC的代码实现
LC是local continuation,是Protothread机制的底层支持,用来保存进程运行状态的地方,其实就是保存进程实体函数上次阻塞的位置。
1、GCC的C语言拓展
LC_SET(s)采用GCC _label_拓展特性 定义一个标号 resume,然后用 GCC && 拓展特性将标号resume的地址存储在s中,记录阻塞位置,s是lc_t类型。LC_RESUME(s)采用goto语句来恢复到上次阻塞的位置,与LC_SET(s)相对应。这种方法只支持GCC编译器。
2、C语言switch语句实现
LC_SET(s)采用标准__LINE__宏语句,将阻塞时程序执行到的行号记录到s中。LC_RESUME(s)采用switch语句,来恢复到上次阻塞的位置,与LC_SET(s)相对应。这种方法不可嵌套switch语句。
四、pt的代码实现
初始化Protothread,初始化必须在执行进程实体前初始化。pt是指向pt结构体的指针,底层也就是初始化LC。
PT_BEGIN中,先设置PT_YIELD_FLAG为1,表示已经YIELD过了,配合YIELD命令,然后执行LC_RESUME恢复到上次阻塞的地方,如果是第一次运行,则从头开始运行。PT_END中,只是LC_END跟PT_BEGIN配合,还有重新做一些初始化工作,并返回PT_ENDED。
PT_WAIT_UNTIL(pt, condition) 等待某个条件(条件可以为时钟或其它变量,IO等)成立。如果不满足,进程实体函数返回PT_WAITING值则退出,下一次进入本 函数就直接跳到这个地方判断处理;否则一直阻塞,直到condition成立。
PT_YIELD中,功能是进程无条件阻塞。第一次运行时,先设置PT_YIELD_FLAG为0,然后保存这次无条件阻塞的位置,进程实体函数返回PT_YIELDED值,退出。YIELD后,重新执行进程实体时,执行PT_BEGIN后,PT_YIELD_FLAG变为1,跳转到上次阻塞的位置后,这次就不会退出了,接着运行。
五、测试Demo
我的测试代码如下:
使用gcc -E demo.c -o demo.i 进行预编译宏替换后,得到调用函数的实现如下:
可以看到,是通过记录当前执行位置后直接退出函数来实现的让出cpu, 恢复执行时通过 switch 的 case 跳转回上次执行的位置来实现的恢复执行。
当计数的count不满足条件则退出后面的处理,下次直接进入这里再次检测,因为count自增后,条件满足了,就往后继续执行。打印的结果如下: