FreeRTOS基础以及UIP之协程--C语言剑走偏锋

在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);
  }
时间: 2024-10-09 23:41:13

FreeRTOS基础以及UIP之协程--C语言剑走偏锋的相关文章

基础入门_Python-进线协程.分分钟玩转multiprocessing多进程编程?

简单介绍: 此模块主要为了解决PYTHON非真正多线程导致无法充分利用多核CPU资源问题,提供了Process,Lock,Semaphore,Event,Queue,Pipe,Pool等组件实现子进程,通信,共享数据,同步方式等 快速安装: pip install multiprocessing 公共属性: multiprocessing.current_process() -> Process 说明: 返回当前运行的子进程对象 multiprocessing.cpu_count() -> i

GoLang之协程

GoLang之协程 目前,WebServer几种主流的并发模型: 多线程,每个线程一次处理一个请求,在当前请求处理完成之前不会接收其它请求:但在高并发环境下,多线程的开销比较大: 基于回调的异步IO,如Nginx服务器使用的epoll模型,这种模式通过事件驱动的方式使用异步IO,使服务器持续运转,但人的思维模式是串行的,大量回调函数会把流程分割,对于问题本身的反应不够自然: 协程,不需要抢占式调度,可以有效提高线程的任务并发性,而避免多线程的缺点:但原生支持协程的语言还很少. 协程(corout

理解Go协程与并发

协程 Go语言里创建一个协程很简单,使用go关键字就可以让一个普通方法协程化: package main import ( "fmt" "time" ) func main(){ fmt.Println("run in main coroutine.") for i:=0; i<10; i++ { go func(i int) { fmt.Printf("run in child coroutine %d.\n", i)

给协程加上同步互斥机制

前面一篇文章介绍了Linux内的同步互斥的概念.内核态和用户态Linux提供的同步/互斥接口.这里本文介绍下如何给协程加上同步.互斥机制. 简单说下协程coroutine: 参考文章 操作系统的课本中对进程.线程的定义:进程是最小的资源分配单位,线程是最小的调度单位. 随着互联网的飞速发展,互联网后台Server服务通常要面临高请求.高并发的挑战,一些业务Server通常要面临很高的网络IO请求.这也就是C10K问题. 现在对C10K问题的解决方案已经很成熟了,主要是 非阻塞IO+IO复用(ep

Python开发基础--- 进程间通信、进程池、协程

进程间通信 进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的. 进程队列queue 不同于线程queue,进程queue的生成是用multiprocessing模块生成的. 在生成子进程的时候,会将代码拷贝到子进程中执行一遍,及子进程拥有和主进程内容一样的不同的名称空间. 示例1: 1 import multiprocessing 2 def foo(): 3 q.put([11,'hello',True]

python基础之进程间通信、进程池、协程

进程间通信 进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的. 进程队列queue 不同于线程queue,进程queue的生成是用multiprocessing模块生成的. 在生成子进程的时候,会将代码拷贝到子进程中执行一遍,及子进程拥有和主进程内容一样的不同的名称空间. 示例1: 1 import multiprocessing 2 def foo(): 3 q.put([11,'hello',True]

协程基础

协程基础 一.引言 之前我们学习了线程.进程的概念,了解了在操作系统中进程是资源分配的最小单位,线程是CPU调度的最小单位.按道理来说我们已经算是把CPU的利用率提高很多了.但是我们知道无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程.创建线程.以及管理他们之间的切换. 随着我们对于效率的追求不断提高,基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发.这样就可以节省创建线进程所消耗的时间. 为此我们需要先回顾下并发的本质

协程基础_context系列函数

近期想看看协程,对这个的详细实现不太了解.查了下,协程最常规的做法就是基于makecontext,getcontext,swapcontext这类函数在用户空间切换用户上下文. 所以在这通过样例代码尽量把context相关的函数弄清楚先. #include <ucontext.h> #include <stdio.h> #include <stdlib.h> static ucontext_t uctx_main, uctx_func1, uctx_func2; #de

Python基础—线程、进程和协程

今天已是学习Python的第十一天,来干一碗鸡汤继续今天的内容,今天的鸡汤是:超越别人对你的期望.本篇博客主要介绍以下几点内容: 线程的基本使用: 线程的锁机制: 生产者消费之模型(队列): 如何自定义线程池: 进程的基本使用: 进程的锁机制: 进程之间如何实现数据共享: 进程池: 协程的基本使用. 一.线程 1.创建线程 上篇博客已经介绍过如何创建多线程的程序,在这里在复习一下如何创建线程过程以及线程的一些方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1