在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 ", uip_ipaddr1(s.ipaddr), uip_ipaddr2(s.ipaddr), uip_ipaddr3(s.ipaddr), uip_ipaddr4(s.ipaddr)); printf("Got netmask %d.%d.%d.%d ", uip_ipaddr1(s.netmask), uip_ipaddr2(s.netmask), uip_ipaddr3(s.netmask), uip_ipaddr4(s.netmask)); printf("Got DNS server %d.%d.%d.%d ", uip_ipaddr1(s.dnsaddr), uip_ipaddr2(s.dnsaddr), uip_ipaddr3(s.dnsaddr), uip_ipaddr4(s.dnsaddr)); printf("Got default router %d.%d.%d.%d ", 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 ", 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 ", uip_ipaddr1(s.ipaddr), uip_ipaddr2(s.ipaddr), uip_ipaddr3(s.ipaddr), uip_ipaddr4(s.ipaddr)); printf("Got netmask %d.%d.%d.%d ", uip_ipaddr1(s.netmask), uip_ipaddr2(s.netmask), uip_ipaddr3(s.netmask), uip_ipaddr4(s.netmask)); printf("Got DNS server %d.%d.%d.%d ", uip_ipaddr1(s.dnsaddr), uip_ipaddr2(s.dnsaddr), uip_ipaddr3(s.dnsaddr), uip_ipaddr4(s.dnsaddr)); printf("Got default router %d.%d.%d.%d ", 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 ", 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); }