Contiki的内核分析-协程机制(一)

血红的双手。 提交于 2020-02-16 23:33:33

导读

本文通过分析Contiki的源码,梳理Contiki的process-event模型中的process机制。

通过前文的阐述我们贯通了event机制的原理。从本文开始,分析Contiki的process机制。

综述

Contiki的process机制实质上是一种协程调度机制,区别于抢占式调度,它只使用了2个字节的变量保存了process的“栈环境[1]”。所有的process以链表的形式保存。官方对此的称呼是protothread,即轻量级线程。但是协程是对它更好的诠释。在协程的环境下,每个process被动的获得cpu执行权,主动的放弃cpu执行权。因此在编程时务必要小心,防止产生死循环,导致整个系统宕机。

为了完全贯通Contiki中的协程机制,我们需要分为2个方面去阐述它:协程逻辑和协程实现,本篇主要阐述协程的逻辑层面,下篇阐述协程的语法实现层面。

协程运行的预备工作

在旅程的最开始,我们先来看一看Contiki的协程----process的类型定义。删除了一些无关紧要的部分,使代码更简洁。

#define PT_THREAD(name_args) char name_args
struct process {
  //链接域
  struct process *next;
  //和process绑定的函数指针
  PT_THREAD((* thread)(struct pt *, process_event_t, process_data_t));
  //保存了行数
  struct pt pt;
  //process状态和poll请求
  unsigned char state, needspoll;
};

删去了边边角角的process的类型定义简洁明了,甚至不需要额外解释。

为了使Contiki的process真正的运作起来,我们需要先执行process_init和process_start[2]函数,使etimer_process开始发挥作用。下面我们剖析这2个函数。

void
process_init(void)
{
  lastevent = PROCESS_EVENT_MAX;
  nevents = fevent = 0;
  process_current = process_list = NULL;
}

同样的删去了无关紧要的部分使代码更简洁。这里可以看到Contiki的最小未使用事件被置为了PROCESS_EVENT_MAX,归0了event循环队列的首尾指针[3],归0了当前运行的process和process链表。一段很简单的初始化函数。接下来的process_start才是重点。

void
process_start(struct process *p, process_data_t dataa)
{
  struct process *q;

  /* First make sure that we don't try to start a process that is
     already running. */
  for(q = process_list; q != p && q != NULL; q = q->next);

  /* If we found the process on the process list, we bail out. */
  if(q == p) {
    return;
  }
  /* Put on the procs list.*/
  p->next = process_list;
  process_list = p;
  p->state = PROCESS_STATE_RUNNING;
  PT_INIT(&p->pt);	  //设置初始行数是0
  /* Post a synchronous initialization event to the process. */
  process_post_synch(p, PROCESS_EVENT_INIT, dataa);
}

这里依旧是先检查是否在process链表中重复插入。之后将该process插入并且设置初始参数,插入方式是头插法。可以看到最后执行了process_post_synch并传入了PROCESS_EVENT_INIT事件,这也就意味着process_start传入的process会立即被执行一次,而不像其他的os那样,先放入就绪列表,等待os调度。

分析到这里,我们已经明白了如何让Contiki执行一个我们自己实现的process了。但是这还不够,在实际应用中,我们的process必须在恰当的时候放弃cpu,又在恰当的时候获得cpu。

协程放弃cpu

以看门狗为例,我们看协程如何放弃cpu。

PROCESS(dog,"dog");//看门狗任务
PROCESS_THREAD(dog, ev, dataa)
{
    static struct etimer et;
    PROCESS_BEGIN(); 
    etimer_set(&et,CLOCK_SECOND*2);//初始化etimer,并设置延时为2s
    while(1)
	{
	    PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));//等待etimer过期
	    spin_watchdog_clear();//喂狗
	    etimer_restart(&et);//重启etimer
	}
   PROCESS_END();
}

放弃cpu的逻辑处理很简单,PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et))条件不满足时即主动放弃了cpu。值得注意的是,主动放弃cpu的方式有很多种,比如无条件放弃,条件不满足时放弃,条件满足时放弃,先放弃一次再判断条件是否满足等等…在Contiki中封装了很多条宏,以满足多种多样的情况。

当然协程放弃cpu并不是只有这么多,具体的实现机制我们会在后面深入剖析。

协程获得cpu

仍然以看门狗为例,我们看协程如何获取cpu。这里的看门狗业务函数,就是一个协程。

PROCESS(dog,"dog");//看门狗任务
PROCESS_THREAD(dog, ev, dataa)
{
    static struct etimer et;
    PROCESS_BEGIN(); 
    etimer_set(&et,CLOCK_SECOND*2);//初始化etimer,并设置延时为2s
    while(1)
	{
	    PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et));//等待etimer过期
	    spin_watchdog_clear();//喂狗
	    etimer_restart(&et);//重启etimer
	}
   PROCESS_END();
}

上面这段代码,所有的大写字符串都是宏定义,我不准备在这时候拆开它们分析,在后文会讲到。当看门狗协程执行到PROCESS_WAIT_EVENT_UNTIL(etimer_expired(&et))时,会因为etimer尚未到期而主动放弃了cpu。在etimer到期时,被动获得了cpu,并根据保存的行数,跳转到上次放弃cpu语句的下一句继续执行。那么看门狗协程是如何被动获取cpu的?结合之前的文章,我们分析其过程。

假设在时刻n,绑定了看门狗的etimer到期,在硬件定时器中断服务函数中,etimer_request_poll被执行,接着process_poll被执行,并置位etimer_process协程的poll请求。于是在Contiki下一次的do_poll时,etimer_process的协程获得执行,在该协程中,会设置看门狗的etimer到期标志位,并且会异步post一个etimer到期事件到event循环队列中。于是在Contiki紧接着的do_event中,取出该事件,并调用该事件绑定的协程。此时看门狗协程获取cpu,根据行数,跳转到对应行,判断etimer到期条件为真,故顺序执行下一语句。

这就是协程被动获取cpu的过程,由etimer事件结合异步post实现,事件驱动机制的典型实现。也说明了etimer定时器的重要性。值得注意的是,目前阐述的协程机制主要是基于协程运行逻辑层面的,协程的实现机制我们会在后文深入剖析。

协程的善后工作

最后,我们还需要理解协程的善后工作,即如何结束一个协程。

static void
exit_process(struct process *p, struct process *fromprocess)
{
  register struct process *q;
  struct process *old_current = process_current;
  /* Make sure the process is in the process list before we try to
     exit it. */
  for(q = process_list; q != p && q != NULL; q = q->next);
  if(q == NULL) {
    return;
  }
  if(process_is_running(p)) {
    /* Process was running */
    p->state = PROCESS_STATE_NONE;
    /*
     * Post a synchronous event to all processes to inform them that
     * this process is about to exit. This will allow services to
     * deallocate state associated with this process.
     */
    for(q = process_list; q != NULL; q = q->next) {
      if(p != q) {
	call_process(q, PROCESS_EVENT_EXITED, (process_data_t)p);
      }
    }

    if(p->thread != NULL && p != fromprocess) {
      /* Post the exit event to the process that is about to exit. */
      process_current = p;
      p->thread(&p->pt, PROCESS_EVENT_EXIT, NULL);
    }
  }

  if(p == process_list) {
    process_list = process_list->next;
  } else {
    for(q = process_list; q != NULL; q = q->next) {
      if(q->next == p) {
	q->next = p->next;
	break;
      }
    }
  }

  process_current = old_current;
}

协程的善后工作也不难理解。老规矩,先检查协程是否在process链表中。另外如果协程正在运行,协程的state会被置为PROCESS_STATE_NONE,这是迄今为止我们看到的第3个状态,也是最后一个状态,其实它的意思就是协程处于结束状态。另外,会发出一个广播事件,通知该协程正在结束,如果其他协程有资源绑定给了该协程,则可以趁机解绑,保证了资源和协程的安全。如果是本协程让其他协程结束,那么被结束的协程还会获得最后一次执行的机会,以便于处理结束事宜。最后恢复process_current 指针,完成本次的协程结束工作。

可以看到协程结束函数是一个内部函数,一般不允许我们直接调用的,Contiki提供了另外一个封装过的函数,让我们安全的退出一个协程,即process_exit。

结束语

到这里,我们明白了协程的就绪,运行,阻塞,结束的逻辑层面的工作原理。还未涉及到Contiki的作者[4]在实现协程机制时最精巧的思想,而这个思想也正是Contiki如此轻量级的最根本原因。我们将在下篇探究它,体会作者天才般的设计思想。

项目地址:https://github.com/zhangoneone/stc89c52
参考

^仅仅是该process执行到的行数
^autostart机制是锦上添花,不是必要的设计
^注意这里指针指的是下标
^Contiki的核心部分是瑞典计算机科学研究所(SICS)的网络内嵌系统小组的 Adam Dunkels 开发的
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!