在FreeRTOS中和UIP中,都使用到了一種C語言實現的多任務計數,專業的定義叫做協程(coroutine),顧名思義,這是一種協作的例程, 跟具有操作系統概念的線程不一樣,協程是在用戶空間利用程序語言的語法語義就能實現邏輯上類似多任務的編程技巧。
意思就是說協程不需要每次調用的時候都為任務准備一次空間,我們知道像ucos這種操作系統,它內置的多任務是需要在中斷過程中切換堆棧的,開銷較大,而協程的功能就是在盡量降低開銷的情況下,實現能夠保存函數上下文快速切換的辦法,用操作系統的概念來說,一千個一萬個協程對應的其實還是一個任務,也可以這樣人物,對應的就是一個很長的函數,函數中途會返回,但是返回之后再次進入函數的時候,會從上次我們返回的地方繼續執行.
還有蠻多理論上的東西,比如消費者-創造者模型等等,就不空談了,直接上代碼
int function(void) { static int i, state = 0; //注意這是靜態變量 switch (state) { case 0: //這里是開始入口 for (i = 0; i < 10; i++) { state = 1; //現在設置靜態變量為1了 return i; case 1:; //到這里選擇會被跳出 } } }
這段代碼要看懂需要費點功夫,注意這里面有兩個靜態變量,靜態變量在編譯的時候就已經固定好了,存放在堆中的,並不會被銷毀.
首先第一次調用這個函數,state被設置成1,函數返回0重要的是接下來,static變量已經被設置了,不會在此設置為0,那么直接匹配到case1,case1沒東西,可是case1在循環體內,下一次循環的時候state又被設置1,此時因為i也是static變量,所以這時候i返回的是1,再接着調用會依次返回0-9,直到i=10,在這個程序就不會返回東西了.
所以你看,我們沒有定義外部的變量,但是這個函數每次進行切換的時候都能保存之前的上下文,造成的開銷就是兩個字節的靜態變量,這就是協程啦,協程上下文切換不需要堆棧的參與.,而第一次的state = 0,相當於任務啟動信號(這段代碼着實變態!!!)
既然已經這樣了不妨再來一下,每次用0 1 2 3 4 寫起來也麻煩,讓宏定義參與進來不是更好
int function(void) { static int i, state = 0; switch (state) { case 0: /* start of function */ for (i = 0; i < 10; i++) { state = __LINE__ + 2; //__LINE__ 標識當前處於第幾行 return i; case __LINE__:; //上面的那個__LINE__+2其實就等於現在的__LINE__,因為代碼又增加了兩行 //所以這里的代碼結構不能變哦 } } }
這樣我們就可以在原來的基礎上再用宏把代碼提煉一下
#define Begin() static int state=0; switch(state) { case 0: #define Yield(x) do { state=__LINE__; return x; case __LINE__:; } while (0) #define End() } int function(void) { static int i; Begin(); for (i = 0; i < 10; i++) Yield(i); End(); }
展開和上面是一樣一樣的
實際上我們利用了 switch-case 的分支跳轉特性,以及預編譯的 __LINE__ 宏,實現了一種隱式狀態機,最終實現了“yield 語義”。
但是, 這就使得代碼不具備可重入性和多線程應用,因為static是不可重入的,所以使用協程和多線程要注意,不能再兩個任務中同時使用一個協程
行,說到這里基本說明白了協程,接着我們分析分析uip的協程源碼,uip使用的協程我們一般叫做Protothreads,包括lc.h lc_switch.h lc_addrlabels.h pt.h
首先看他的數據結構
struct pt { lc_t lc; }; typedef unsigned short lc_t;
一個short型數據,長度是編譯器默認長度, 實際上它就是協程的上下文結構體,用以保存狀態變量,
#define LC_INIT(s) s = 0; #define LC_RESUME(s) switch(s) { case 0: #define LC_SET(s) s = __LINE__; case __LINE__: #define LC_END(s) }
四句協程原語,和之前我們自己提煉的類似,只不過他把state換成個s
但是吧,這里的原語有一個漏洞, 無法在 LC_RESUME 和 LC_END (或者包含它們的組件)之間的代碼中使用 switch-case語句,因為這會引起外圍的 switch 跳轉錯誤, 為 此,protothreads 又實現了基於 GNU C 的調度“原語”。在 GNU C 下還有一種語法糖叫做標簽指針,就是在一個 label 前面加 &&(不是地址的地址,是 GNU 自定義的符號),可以用 void 指針類型保存,然后 goto 跳轉
typedef void * lc_t; #define LC_INIT(s) s = NULL #define LC_RESUME(s) \ do { \ if(s != NULL) { \ goto *s; \ } \ } while(0) #define LC_SET(s) \ do { ({ __label__ resume; resume: (s) = &&resume; }); }while(0) #define LC_END(s)
__label__這個就是label
現在准備條件都做好了, Protothreads真正的實現是在pt.h文件中,有着如下接口
#define PT_WAITING 0 //設定等待 #define PT_EXITED 1 //退出 #define PT_ENDED 2 //結束 #define PT_YIELDED 3 //阻塞 /* 初始化一個協程,也即初始化狀態變量 */ #define PT_INIT(pt) LC_INIT((pt)->lc) /* 聲明一個函數,返回值為 char 即退出碼,表示函數體內使用了 proto thread,(個人覺得有些多此一舉) */ #define PT_THREAD(name_args) char name_args /* 協程入口點, PT_YIELD_FLAG=0表示出讓,=1表示不出讓,放在 switch 語句前面,下次調用的時候可以跳轉到上次出讓點繼續執行 */ #define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc) /* 協程退出點,至此一個協程算是終止了,清空所有上下文和標志 */ #define PT_END(pt) LC_END((pt)->lc); /*PT_YIELD_FLAG = 0;*/ \ PT_INIT(pt); return PT_ENDED; } /* 協程阻塞點(blocking),本質上等同於 PT_YIELD_UNTIL,只不過退出碼是 PT_WAITING,用來模擬信號量同步 */ #define PT_WAIT_UNTIL(pt, condition) \ do { \ LC_SET((pt)->lc); \ if(!(condition)) { \ return PT_WAITING; \ } \ } while(0) /* 同 PT_WAIT_UNTIL 條件反轉 */ #define PT_WAIT_WHILE(pt, cond) PT_WAIT_UNTIL((pt), !(cond)) //協程等待 #define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread)) /* 用於協程嵌套調度,child 是子協程的上下文句柄 */ #define PT_SPAWN(pt, child, thread) \ do { \ PT_INIT((child)); \ PT_WAIT_THREAD((pt), (thread)); \ } while(0) //協程重啟 #define PT_RESTART(pt) \ do { \ PT_INIT(pt); \ return PT_WAITING; \ } while(0) //協程退出 #define PT_EXIT(pt) \ do { \ PT_INIT(pt); \ return PT_EXITED; \ } while(0) //協程調度 #define PT_SCHEDULE(f) ((f) == PT_WAITING) /* 協程出讓點,如果此時協程狀態變量 lc 已經變為 __LINE__ 跳轉過來的,那么 PT_YIELD_FLAG = 1,表示從出讓點繼續執行。 */ #define PT_YIELD(pt) \ do { \ PT_YIELD_FLAG = 0; \ LC_SET((pt)->lc); \ if(PT_YIELD_FLAG == 0) { \ return PT_YIELDED; \ } \ } while(0) /* 附加出讓條件 */ #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)
通過這些宏定義就可以完善的處理協程了,而且我們還可以在上面擴展,例如我們想添加一個信號量控制,那這樣
struct pt_sem { unsigned int count; }; #define PT_SEM_INIT(s, c) (s)->count = c #define PT_SEM_WAIT(pt, s) \ do { \ PT_WAIT_UNTIL(pt, (s)->count > 0); \ --(s)->count; \ } while(0) #define PT_SEM_SIGNAL(pt, s) ++(s)->count
就可以了
現在我們可以看看UIP利用協程實現的DHCP了,直接在源碼里面說吧
static PT_THREAD(handle_dhcp(void))//這是一個函數,同時也表明這是一個協程 { PT_BEGIN(&s.pt);//協程啟動 /* try_again:*/ s.state = STATE_SENDING; s.ticks = CLOCK_SECOND; do { send_discover(); timer_set(&s.timer, s.ticks); //等待一個事件 PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); if(uip_newdata() && parse_msg() == DHCPOFFER) { s.state = STATE_OFFER_RECEIVED; break; } if(s.ticks < CLOCK_SECOND * 60) { s.ticks *= 2; } } while(s.state != STATE_OFFER_RECEIVED); s.ticks = CLOCK_SECOND; do { send_request(); timer_set(&s.timer, s.ticks); //再次等待一個事件 PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); if(uip_newdata() && parse_msg() == DHCPACK) { s.state = STATE_CONFIG_RECEIVED; break; } if(s.ticks <= CLOCK_SECOND * 10) { s.ticks += CLOCK_SECOND; } else { //協程重啟 PT_RESTART(&s.pt); } } while(s.state != STATE_CONFIG_RECEIVED); #if 0 printf("Got IP address %d.%d.%d.%d\n", uip_ipaddr1(s.ipaddr), uip_ipaddr2(s.ipaddr), uip_ipaddr3(s.ipaddr), uip_ipaddr4(s.ipaddr)); printf("Got netmask %d.%d.%d.%d\n", uip_ipaddr1(s.netmask), uip_ipaddr2(s.netmask), uip_ipaddr3(s.netmask), uip_ipaddr4(s.netmask)); printf("Got DNS server %d.%d.%d.%d\n", uip_ipaddr1(s.dnsaddr), uip_ipaddr2(s.dnsaddr), uip_ipaddr3(s.dnsaddr), uip_ipaddr4(s.dnsaddr)); printf("Got default router %d.%d.%d.%d\n", uip_ipaddr1(s.default_router), uip_ipaddr2(s.default_router), uip_ipaddr3(s.default_router), uip_ipaddr4(s.default_router)); printf("Lease expires in %ld seconds\n", ntohs(s.lease_time[0])*65536ul + ntohs(s.lease_time[1])); #endif dhcpc_configured(&s); /* timer_stop(&s.timer);*/ /* * PT_END restarts the thread so we do this instead. Eventually we * should reacquire expired leases here. */ while(1) { PT_YIELD(&s.pt);//協程出讓 } PT_END(&s.pt);//最后完成 } /*---------------------------------------------------------------------------*/ void dhcpc_init(const void *mac_addr, int mac_len) { uip_ipaddr_t addr; s.mac_addr = mac_addr; s.mac_len = mac_len; s.state = STATE_INITIAL; uip_ipaddr(addr, 255,255,255,255); s.conn = uip_udp_new(&addr, HTONS(DHCPC_SERVER_PORT)); if(s.conn != NULL) { uip_udp_bind(s.conn, HTONS(DHCPC_CLIENT_PORT)); } //初始化協程 PT_INIT(&s.pt); }
其實上面這段代碼是有BUG的,在兩個do_while的循環中都沒有進行標志位的清空, 導致程序誤判以為是dhcp已經接收到下一個數據了.另外沒有dhcp租約機制沒有寫進去.這一點我自己改好了,如下
static PT_THREAD(handle_dhcp(void)) { PT_BEGIN(&s.pt); /* try_again:*/ s.state = STATE_SENDING; s.ticks = CLOCK_SECOND; do { send_discover(); timer_set(&s.timer, s.ticks); PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); if(uip_newdata() && parse_msg() == DHCPOFFER) { s.state = STATE_OFFER_RECEIVED; break; } if(s.ticks < CLOCK_SECOND * 60) { s.ticks *= 2; } } while(s.state != STATE_OFFER_RECEIVED); s.ticks = CLOCK_SECOND; //連接的狀態標志清零 uip_flags = 0; request_pro: do { send_request(); timer_set(&s.timer, s.ticks); PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); if(uip_newdata() && parse_msg() == DHCPACK) { s.state = STATE_CONFIG_RECEIVED; break; } if(s.ticks <= CLOCK_SECOND * 10) { s.ticks += CLOCK_SECOND; } else { PT_RESTART(&s.pt); } } while(s.state != STATE_CONFIG_RECEIVED); #if 1 printf("Got IP address %d.%d.%d.%d\r\n", uip_ipaddr1(s.ipaddr), uip_ipaddr2(s.ipaddr), uip_ipaddr3(s.ipaddr), uip_ipaddr4(s.ipaddr)); printf("Got netmask %d.%d.%d.%d\r\n", uip_ipaddr1(s.netmask), uip_ipaddr2(s.netmask), uip_ipaddr3(s.netmask), uip_ipaddr4(s.netmask)); printf("Got DNS server %d.%d.%d.%d\r\n", uip_ipaddr1(s.dnsaddr), uip_ipaddr2(s.dnsaddr), uip_ipaddr3(s.dnsaddr), uip_ipaddr4(s.dnsaddr)); printf("Got default router %d.%d.%d.%d\r\n", uip_ipaddr1(s.default_router), uip_ipaddr2(s.default_router), uip_ipaddr3(s.default_router), uip_ipaddr4(s.default_router)); printf("Lease expires in %ld seconds\r\n", ntohs(s.lease_time[0]) * 65536ul + ntohs(s.lease_time[1])); #endif dhcpc_configured(&s); /* timer_stop(&s.timer);*/ /* * PT_END restarts the thread so we do this instead. Eventually we * should reacquire expired leases here. */ /* 判斷超時 租約到期重連*/ timer_set(&s.timer,(ntohs(s.lease_time[0]) * 65536ul + ntohs(s.lease_time[1]))*50); PT_WAIT_UNTIL(&s.pt, timer_expired(&s.timer)); /* 超時了 */ goto request_pro; while (1) { PT_YIELD(&s.pt); } PT_END(&s.pt); }