一、概述
很多傳感器操作系統都是基於事件驅動模型的,事件驅動模型不用為每個進程都分配一個進程棧,這對內存資源受限的無線傳感器網絡嵌入式系統尤為重要。
然而事件驅動模型不支持阻塞等待抽象語句,因此程序員通常用狀態機來實現控制流,但這都很復雜。
例子:一個假想的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成立為止
三、參考資料
- Protothreads: Simplifying event-driven programming of memory-constrained embedded systems. Adam Dunkels, Oliver Schmidt, Thiemo Voigt, and Muneeb Ali. ACM SenSys 2006.
- 源碼:$contiki$\core\sys\pt.h、$contiki$\core\sys\lc.h、$contiki$\core\sys\lc-switch.h、$contiki$\core\sys\lc-addrlabels.h