Contiki学习笔记(重写版)

16930阅读 7评论2014-09-14 Jelline
分类:嵌入式

2013年写硕士毕业论文时,基于之前Contiki学习笔记的系列博文,对Contiki学习笔记重新整理,有的甚至是重写,并修改了一些错误,增加了很多图,便于理解。

我在word是排好版的,本来想的是用离线博客工具(如Zoundry Raven, Windows Live Writer)发布(在线编辑器,插入图片很麻烦),但现在CU不支持离线博客工具了,只好放弃。后面想着,把文档上传到百度文库,然后以内嵌代码的形式将文章置于该页面,但刚才试了,行不通。最后,只能上传整个文档,并将文本附在文件之后(注:图片没法正常显示)。强烈建议您,下载pdf文档阅读,有目录,层次更清晰。


Contiki学习笔记(重写版).pdf


Contiki学习笔记(重写版) 

Contiki是由瑞典计算机科学研究所开发专用的网络节点操作系统,自2003年发布1.0版本以来,得到飞速发展,成为一个完整的操作系统,包括文件系统Coffee、网络协议栈uIP和Rime、网络仿真器COOJA,并于2012年发布全新版本2.6。Contiki由标准C语言开发,具有很强的移植性,已被移植到多种平台,包括8051、MSP430、AVR、ARM,并得到广泛应用。除此之外,Contiki将Protothreads轻量级线程模型和事件机制完美整合到一起,Proththreads机制使得系统占用内存极小,事件机制保证了系统低功耗,非常适合资源受限、功耗敏感的传感器网络。

本工作开展之初,Contiki最高版本为2.5,除了几篇官方发表的论文及少许的介绍性资料外,没有详细的参考资料。为了深入理解无线传感器网络中的文件系统和重编程技术,不得不深入分析源码重现Contiki的技术细节。本文关于Contiki所有讨论是基于2.5版本。

1.1运行原理

嵌入式系统可以看作是一个运行着死循环主函数系统,Contiki内核是基于事件驱动的,系统运行可以视为不断处理事件的过程。Contiki整个运行是通过事件触发完成,一个事件绑定相应的进程。当事件被触发,系统把执行权交给事件所绑定的进程。一个典型基于Contiki的系统运行示意图如下:

2-  SEQ _2- \* ARABIC 1 Contiki运行原理示意图

事实上,上述的框图几乎是主函数的流程图。通常情况下,应用程序作为一个进程放在自启动的指针数组中,系统启动后,先进行一系列的硬件初始化(包括串口、时钟),接着初始化进程,启动系统进程(如管理etimer的系统进程etimer_process)和用户指定的自启动进程,然后进入处理事件的死循环(如上图右边框框所示,实际上是process_run函数的功能)。通过遍历执行完所有高优先级的进程,而后转去处理事件队列的一个事件,处理该事件(通常对应于执行一个进程)之后,需先满足高优先级进程才能转去处理下一个事件。将process_run代码展开加到main函数,保留关键代码,如下:

int main()

{

  clock_init();                          //时钟初始化

  process_init();                        //进程初始化

  process_start(&etimer_process, NULL);   //启动系统进程

  autostart_start(autostart_processes);       //启动用户自启动进程

 

  while(1)

  {

    /***函数process_run的功能***/

    if(poll_requested)

    {

      do_poll();                        //执行完所有高优先级的进程

    }

    do_event();                         //仅处理事件队列的一个事件

  }

  return 0;

}

1.2 Contiki内核

进程无疑是一个系统最重要的概述,Contiki的进程机制是基于Protothreads线程模型,为确保高优先级任务尽快得到响应,Contiki采用两级进程调度。

1.2.1 Protothreads

Contiki使用Protothreads[ NOTEREF _Ref349908876 \h  \* MERGEFORMAT 8]轻量级线程模型,在Protothreads基础上进行封装。为了适应内存受限的嵌入式系统,瑞典计算机科学研究所设计Protothreads,实际上是一种轻量级无线结构的线程库。传统的桌面操作系统甚至服务器操作系统,每个进程都拥有自己的栈,进行进程切换时,将进程相关的信息(包括局部变量、断点、寄存器值)存储在栈中。然而,对于嵌入式系统,尤其是内存受限的传感器节点几乎不现实,基于这点考虑,Protothreads巧妙地让所有进程共用一个栈,传统的进程与Protothreads对比示意图如下:

2-  SEQ _2- \* ARABIC 2 ThreadProtothreads栈对比示意图

从图可以看出,原本需要3个栈的Thread机制,在Protothreads只需要一个栈,当进程数量很多的时候,由栈空间省下来的内存是相当可观的。保存程序断点在传统的Thread机制很简单,只需要要保存在私有的栈,然而Protothreads不能将断点保存在公有栈中。Protothreads很巧妙地解决了这个问题,即用一个两字节静态变量存储被中断的行,因为静态变量不从栈上分配空间,所以即使有任务切换也不会影响到该变量,从而达到保存断点的。下一次该进程获得执行权的时候,进入函数体后就通过switch语句跳转到上一次被中断的地方。

(1)保存断点

保存断点是通过保存行数来完成的,在被中断的地方插入编译器关键字__LINE__,编译器便自动记录所中断的行数。展开那些具有中断功能的宏,可以发现最后保存行数是宏LC_SET,取宏PROCESS_WAIT_EVENT()为例,将其展开得到如下代码:

#define PROCESS_WAIT_EVENT() PROCESS_YIELD()

#define PROCESS_YIELD() PT_YIELD(process_pt)

#define PT_YIELD(pt) \

do{ \

  PT_YIELD_FLAG = 0; \

  LC_SET((pt)->lc); \

  if(PT_YIELD_FLAG == 0) \

  {

      return PT_YIELDED; \

  } \

}while(0)

 

#define LC_SET(s) s = __LINE__; case __LINE__:  //保存程序断点,下次再运行该进程直接跳到case __LINE__

值得一提的是,宏LC_SET展开还包含语句case __LINE__,用于下次恢复断点,即下次通过switch语言便可跳转到case的下一语句。

(2)恢复断点

被中断程序再次获得执行权时,便从该进程的函数执行体进入,按照Contiki的编程替换,函数体第一条语句便是PROCESS_BEGIN宏,该宏包含一条switch语句,用于跳转到上一次被中断的行,从而恢复执行,宏PROCESS_BEGIN展开的源代码如下:

#define PROCESS_BEGIN() PT_BEGIN(process_pt)

#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)

#define LC_RESUME(s) switch(s) { case 0:    //switch语言跳转到被中断的行

1.2.2 进程控制块

正如Linux一样,Contiki也用一个结构体来描述整个进程的细节,所不同的是,Contiki进程控制块要简单得多。使用链表将系统所有进程组织起来,如下图所示(将PT_THREAD宏展开):

2-  SEQ _2- \* ARABIC 3 Contiki进程链表process_list

Contiki系统定义一个全局变量process_list作为进程链表的头,还定义了一个全局变量process_current用于指向当前进程。成员变量next指向下一个进程,最后一进程的next指向空。name是进程的名称,可以将系统配置(定义变量PROCESS_CONF_NO_PROCESS_NAMES为0)成没有进程名称,此时name为空字符串。变量state表示进程的状态,共3种,即PROCESS_STATE_RUNNING、PROCESS_STATE_CALLED、PROCESS_STATE_NONE。变量needspoll标识进程优先级,只有两个值0和1,needspoll为1意味着进程具有更高的优先级。

(1)成员变量thread

进程的执行体,即进程执行实际上是运行该函数。在实际的进程结构体代码中,该变量由宏PT_THREAD封装,展开即为一个函数指针,关键源代码如下:

PT_THREAD((*thread)(struct pt *, process_event_t, process_data_t));

#define PT_THREAD(name_args) char name_args

/***宏展开***/

char (*thread)(struct pt *, process_event_t, process_data_t);

(2)成员变量pt

正如上文所述一样,Contiki进程是基于Protothreads,所以进程控制块需要有个变量记录被中断的行数。结构体pt只有一个成员变量lc(无符号短整型),可以将pt简单理解成保存行数的,相关源代码如下:

struct pt

{

  lc_t lc;

};

typedef unsigned short lc_t;

1.2.3 进程调度

Contiki只有两种优先级,用进程控制块中变量needspoll标识,默认情况是0,即普通优先级。想要将某进程设为更高优先级,可以在创建之初指定其needspoll为1,或者运行过程中通过设置该变量动态提升其优先级。实际的调度中,会先运行有高优先级的进程,而后再去处理一个事件,随后又运行所有高优先级的进程。通过遍历整个进程链表,将needspoll为1的进程投入运行,关键代码如下:

/***do_poll()关键代码,由process_run调用***/

for(p = process_list; p != NULL; p = p->next)   //遍历进程链表

{

  if(p->needspoll)

  {

    p->state = PROCESS_STATE_RUNNING;           //设置进程状态

    p->needspoll = 0;

    call_process(p, PROCESS_EVENT_POLL, NULL);  //将进程投入运行

  }

}

以上是进程的总体调度,具体到单个进程,成员变量state标识着进程的状态,共有三个状态PROCESS_STATE_RUNNING、PROCESS_STATE_CALLED、PROCESS_STATE_NONE。Contiki进程状态转换如下图:

2-  SEQ _2- \* ARABIC 4 Contiki进程状态转换图

创建进程(还未投入运行)以及进程退出(但此时还没从进程链表删除),进程状态都为PROCESS_STATE_NONE。通过进程启动函数process_start将新创建的进程投入运行队列(但未必有执行权),真正获得执行权的进程状态为PROCESS_STATE_CALLED,处在运行队列的进程(包括正在运行和等待运行)可以调用exit_process退出。

(1)进程初始化

系统启动后需要先将进程初始化,通常在主函数调用,进程初始化主要完成事件队列和进程链表初始化。将进程链表头指向为空,当前进程也设为空。process_init源代码如下:

void process_init(void)

{

  /***初始化事件队列***/

  lastevent = PROCESS_EVENT_MAX;

  nevents = fevent = 0;

  process_maxevents = 0;

  /***初始化进程链表***/

  process_current = process_list = NULL;

}

(2)创建进程

创建进程实际上是定义一个进程控制块和定义进程执行体的函数。宏PROCESS的功能包括定义一个结构体,声明进程执行体函数,关键源代码如下(假设进程名称为Hello world):

PROCESS(hello_world_process, "Hello world");

 

/***PROCESS宏展开***/

PROCESS_THREAD(name, ev, data); \

struct process name = { NULL, strname, process_thread_##name }

 

/***PROCESS_THREAD宏展开***/

static PT_THREAD(process_thread_##name(struct pt *process_pt, process_event_t ev, process_data_t data))

#define PT_THREAD(name_args) char name_args

 

/***将参数代入,PROCESS宏最后展开结果***/

static char process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data);

struct process hello_world_process =  \

{NULL, "Hello world", process_thread_hello_world_process };

可见,PROCESS宏实际上声明一个函数并定义一个进程控制块,新创建的进程next指针指向空,进程名称为“Hello world”,进程执行体函数指针为process_thread_hello_world_process,保存行数的pt为0,状态为0(即PROCESS_STATE_NONE),优先级标记位needspoll也为0(即普通优先级)。

PROCESS定义了结构体并声明了函数,还需要实现该函数,通过宏PROCESS_THREAD实现。值得注意的是,尽管PROCESS宏展开包含了宏PROCESS_THREAD,用于声明函数,而这里是定义函数,区别在于前者宏展开后面加了个分号。定义函数框架代码如下:

PROCESS_THREAD(hello_world_process, ev, data)

//static char process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data)

{

  PROCESS_BEGIN(); //函数开头必须有

  /***代码放在这***/

  PROCESS_END(); //函数末尾必须有

}

欲实现的代码必须放在宏PROCESS_BEGIN与PROCESS_END之间,这是因为这两个宏用于辅助保存断点信息(即行数),宏PROCESS_BEGIN包含switch(process_pt->lc)语句,这样被中断的进程再次获利执行便可通过switch语句跳转到相应的case,即被中断的行。

(3)启动进程

函数process_start用于启动一个进程,首先进行参数验证,即判断该进程是否已经在进程链表中,而后将进程加到链表,给该进程发一个初始化事件PROCESS_EVENT_INIT。函数process_start流程图如下:

2-  SEQ _2- \* ARABIC 5函数process_start流程图

process_start将进程状态设为PROCESS_STATE_RUNNING,并调用PT_INIT宏将保存断点的变量设为0(即行数为0)。调用process_post_synch给进程触发一个同步事件,事件为PROCESS_EVENT_INIT。考虑到进程运行过程中可能被中断(比如中断),在进程运行前将当前进程指针保存起来,执行完再恢复。进程运行是由call_process函数实现。

2-  SEQ _2- \* ARABIC 6 call_process流程图

call_process首先进行参数验证,即进程处于运行状态(退出尚未删除的进程状态为PROCESS_STATE_NONE)并且进程的函数体不为空,接着将进程状态设为PROCESS_STATE_CALLED,表示该进程拥有执行权。接下来,运行进程函数体,根据返回值判断进程是否结束(主动的)或者退出(被动的),若是调用exit_process将进程退出,否则将进程状态设为PROCESS_STATE_RUNNING,继续放在进程链表。


(4) 进程退出

进程运行完或者收到退出的事件都会导致进程退出。根据Contiki编程规划,进程函数体最后一条语句是PROCESS_END(),该宏包含语句return PT_ENDED,表示进程运行完毕。系统处理事件时(事件绑定进程,事实上执行进程函数体),倘若该进程恰好收到退出事件,thread便返回PT_EXITED,进程被动退出。还有就是给该进程传递退出事件PROCESS_EVENT_EXIT也会导致进程退出。进程退出函数exit_process流程图如下:

2-  SEQ _2- \* ARABIC 7 exit_process流程图

进程退出函数exit_process首先对传进来的进程p进行参数验证,确保该进程在进程链表中并且进程状态为PROCESS_STATE_CALLED/RUNNING(即不能是NONE),接着将进程状态设为NONE。随后,向进程链表的所有其他进程触发退出事件PROCESS_EVENT_EXITED,此时其他进程依次执行处理该事件,其中很重要一部分是取消与该进程的关联。进程执行函数体thread进行善后工作,最后将该进程从进程链表删除。

1.2.4 事件调度

事件驱动机制广泛应用于嵌入式系统,类似于中断机制,当有事件到来时(比如按键、数据到达),系统响应并处理该事件。相对于轮询机制,事件机制优势很明显,低功耗(系统处于休眠状态,当有事件到达时才被唤醒)和MCU利用率高。

Contiki将事件机制融入Protothreads机制,每个事件绑定一个进程(广播事件例外),进程间的消息传递也是通过事件来传递的。用无符号字符型来标识事件,事件结构体event_data定义如下:

struct event_data

{

  process_event_t ev;

  process_data_t data;

  struct process *p;

};

 

typedef unsigned char process_event_t;

typedef void *process_data_t;

用无符号字符型标识一个事件,Contiki定义了10个事件(0x80~0x8A),其他的供用户使用。每个事件绑定一个进程,如果p为NULL,表示该事件绑定所有进程(即广播事件PROCESS_BROADCAST)。除此之外,事件可以携带数据data,可以利用这点进行进程间的通信(向另一进程传递带数据的事件)。

Contiki用一个全局的静态数组存放事件,这意味着事件数目在系统运行之前就要指定(用户可以通过PROCESS_CONF_NUMEVENTS自选配置大小),通过数组下标可以快速访问事件。系统还定义另两个全局静态变量nevents和fevent,分别用于记录未处理事件总数及下一个待处理的位置。事件逻辑组成环形队列,存储在数组里,如下图:

2-  SEQ _2- \* ARABIC 8 Contiki事件队列示意图

可见对于Contiki系统而言,事件并没有优先级之分,而是先到先服务的策略,全局变量fevent记录了下一次待处理事件的下标。

(1)事件产生

Conitki有两种方式产生事件,即同步和异步。同步事件通过process_post_synch函数产生,事件触发后直接处理(调用call_process函数)。而异步事件产生是由process_post产生,并没有及时处理,而是放入事件队列等待处理,process_post流程图如下:

 

2-  SEQ _2- \* ARABIC 9 process_post函数流程图

process_post首先判断事件队列是否已满,若满返回错误,否则取得下一个空闲位置(因为是环形队列,需做余操作),而后设置该事件并将未处理事件总数加1。

(2)事件调度

事件没有优先级,采用先到先服务策略,每一次系统轮询(process_run函数)只处理一个事件,do_event函数用于处理事件,其流程图如下:

2-  SEQ _2- \* ARABIC 10 do_event函数流程图

do_event首先取出该事件(即将事件的值复制到一个新变量),更新总的未处理事件总数及下一个待处理事件的数组下标(环形队列,需要取余操作)。接着判断事件是否为广播事件PROCESS_BROADCAST,若是,考虑到处理广播事件可能需要更多的时间,为保证系统实时性,先运行高优先级的进程,而后再去处理事件(调用call_process函数)。如果事件是初始化事件PROCESS_EVENT_INIT (创建进程的时候会触发此事件),需要将进程状态设为PROCESS_STATE_RUNNING。

(3)事件处理

实际的事件处理是在进程的函数体thread,正如上文所说的那样,call_process会调用tread函数,执行该进程。关键代码如下:

ret = p->thread(&p->pt, ev, data);

1.2.5 定时器

Contiki内核是基于事件驱动和Protothreads机制,事件既可以是外部事件(比如按键,数据到达),也可以是内部事件(如时钟中断)。定时器的重要性不言而喻,Contiki提供了5种定时器模型,即timer(描述一段时间,以系统时钟嘀嗒数为单位)、stimer(描述一段时间,以秒为单位)、ctime(定时器到期,调用某函数,用于Rime协议栈)、etime(定时器到期,触发一个事件)、rtimer(实时定时器,在一个精确的时间调用函数)。

鉴于etimer在Contiki使用的广泛性,管理这些etimer由系统进程etimer_process管理,本小节详细简单etimer相关技术细节。

(1) etimer组织结构

etimer作为一类特殊事件存在,也是跟进程绑定。除此之外,还需变量描述定时器属性,etimer结构体定义如下:

struct etimer

{

  struct timer timer;    //包含起始时刻和间隔两成员变量

  struct etimer *next;   //指向下一个etimer

  struct process *p;

};

成员变量timer用于描述定时器属性,包含起始时刻及间隔,将起始时刻与间隔相加与当前时钟对比,便可知道是否到期。变量p指向所绑定的进程(p为NULL则表示该定时器与所有进程绑定)。成员变量next,指向下一个etimer,系统所有etimer被链接成一个链表,如下图所示:

2-  SEQ _2- \* ARABIC 11 timer链表timer_list示意图

(2)添加etimer

定义一个etimer结构体,调用etimer_set函数将etimer添加到timerlist,函数etimer_set流程图如下:

2-  SEQ _2- \* ARABIC 12 etimer_set流程图

etimer_set首先设置etimer成员变量timer的值(由timer_set函数完成),即用当前时间初始化start,并设置间隔interval,接着调用add_timer函数,该函数首先将管理etimer系统进程etimer_process优先级提升,以便定时器时间到了可以得到更快的响应。接着确保欲加入的etimer不在timerlist中(通过遍历timerlist实现),若该etimer已经在etimer链表,则无须将etimer加入链表,仅更新时间。否则将该etimer插入到timerlist链表头位置,并更新时间(update_time)。这里更新时间的意思是求出etimer链表中,还需要多长next_expiration(全局静态变量)时间,就会有etimer到期。

(3) etimer管理

Contiki用一个系统进程etimer_process管理所有etimer定时器。进程退出时,会向所有进程发送事件PROCESS_EVENT_EXITED,当然也包括etimer系统进程etimer_process。当etimer_process拥有执行权的时候,便查看是否有相应的etimer绑定到该进程,若有就删除这些etimer。除此之外,etimer_process还会处理到期的etimer,etimer_process的thread函数流程图如下:

2-  SEQ _2- \* ARABIC 13 etimer_process的函数thread流程图

etimer_process获得执行权时,若传递的是退出事件,遍历整个timerlist,将与该进程(通过参数data传递)相关的etimer从timerlist删除,而后转去所有到期的etimer。通过遍历整个etimer查看到期的etimer,若有到期,发绑定的进程触发事件PROCESS_EVENT_TIMER,并将etimer的进程指针设为空(事件已加入事件队列,处理完毕),接着删除该etimer,求出下一次etimer到期时间,继续检查是否还有etimer到期。提升etimer_process优先级,若接下来都没有etimer到期了,就退出。总之,遍历timerlist,只要etimer到期,处理之后重头遍历整个链表,直到timerlist没有到期的etimer就退出。

1.3 Rime协议栈

传统的分层通信架构(communication architectures)很难满足资源受限的传感器网络,于是研究者转向跨层优化(比如将顶层数据聚合功能放在底层实现),但这导致系统变得更脆弱以及难以控制(fragile and unmanageable systems)。因此,传统分层通信结构再次得到重视,同时研究发现,传统分层效率几乎可以与跨层优化相媲美[]。基于此,Rime也采用分层结构。

1.3.1协议栈结构

Rime是针对传感器网络轻量级、层次型协议栈,也是低功耗、无线网络协议栈,旨在简化传感器网络协议及代码重用,属于Contiki的一部分(Contiki还支持uIPv4、uIPv6、LwIP)。Rime协议栈结构框图如下:

2-  SEQ _2- \* ARABIC 14 Rime协议栈结构框图

上图中单跳单播各个缩写含义如下:

rucb

rucb是单跳单播的最顶层,将数据以块为单位进行传输(Bulk transfer)。

ruc

ruc是指Reliable communication。可靠通信由两层实现:Stubborn transmission、Reliable transmission。该层主要实现确认和序列功能(acknowledgments and sequencing)

suc

suc指Stubborn transmission,是可靠通信的另一层。suc这一层在给定的时间间隔不断地重发数据包,直到上层让其停止。为了防止无限重发,需要指定最大重发次数(maximum retransmission number)。

ibc

ibc表示identified sender best-effort broadcast,将上层的数据包添加一个发送者身份(sender identity)头部。

uc

uc意思是unicast abstraction,将上层的数据包添加一个接收者头部。

abc

abc意思是anonymous broadcast,匿名广播。即将数据包通过无线射频驱动(radio driver)发出去,接收来自无线射频驱动所有的包并交给上层。

2.5.2 建立连接

使用Rime协议栈进行通信之前,需要建立连接。Rime协议栈提供单跳单播、单跳广播、多跳三种功能。在此,仅介绍单跳单播(Single-hop unicast)连接建立过程。

建立连接的实质是保存该连接一些信息(如发送者、接收者),Rime协议栈用一系列结构体保存这些链接状态信息。Rime每一层都有相应的连接结构体(以_conn结尾),上层嵌套下层,如下:

rucb_conn --> runicast_conn --> stunicast_conn --> unicast_conn --> broadcast_conn --> abc_conn

每个连接结构体都有相应的回调结构体(以_callbacks后缀结尾),该结构体的成员变量实为发送、接收函数指针。当接收到一个数据报,会调用该结构体相应的函数。回调结构体层次如下:

rucb_callbacks --> runicast_callbacks --> stunicast_callbacks --> unicast_callbacks --> broadcast_callbacks --> abc_callbacks

综上,连接建立_open、连接结构体_conn、回调结构体_callbacks间的关系如下图:

2-  SEQ _2- \* ARABIC 15 opencooncallbacks对应关系

(1)连接结构体

建立连接,实质是初始化结构体rucb_conn各个成员变量,结构体rucb_conn定义如下:

struct rucb_conn

{

  struct runicast_conn c;

  const struct rucb_callbacks *u;

  rimeaddr_t receiver, sender;

  uint16_t chunk;

  uint8_t last_seqno;

};

结构体rucb_conn各成员变量含义如下:

c

uc(unicast abstraction)将上层的数据包添加一个接收者头部传递给下一层,这里的c指的是下一层连接结构体。

u

结构体rucb_callbacks有3个函数指针成员变量写数据块write_chunk、读数据块read_chunk、超时timedout,需要用户自己实现。

receiversender

用于标识接收者和发送者。这里的receiver是指目的节点的接收地址。

chunk

数据块数目。

last_seqno

一次数据发送多个片段的最后一个序列号,当接收端接收到数据时,判断其序列号是否等于最后一个序列号,若等于则不接收(即接收到最后一个数据块,停止接收)。

1.3.3 数据发送

Rime协议栈建立连接后,就可以进行通信了(发送、接收数据),Rime协议栈提供单跳单播、单跳广播、多跳三种功能。在此,仅介绍单跳单播(Single-hop unicast)发送数据情型。

Rime是层次型协议栈,整个发送数据过程是通过上层调用下层服务来完成的,具体如下:

rucb_send --> runicast_send --> stunicast_send_stubborn --> unicast_send --> broadcast_send --> abc_send --> rime_output --> NETSTACK_MAC.send

rucb是块传输(Bulk transfer)层,可以理解成传输层,数据发送函数rucb_send源代码如下:

int rucb_send(struct rucb_conn *c, const rimeaddr_t *receiver)

{

  c->chunk = 0;

  read_data(c);

  rimeaddr_copy(&c->receiver, receiver);

  rimeaddr_copy(&c->sender, &rimeaddr_node_addr);

  runicast_send(&c->c, receiver, MAX_TRANSMISSIONS);

  return 0;

}

c->chunk将数据块数目初始化为0,read_data进行一些Rime缓冲区初始化相关工作。rimeaddr_copy函数设置接收者receiver和发送者sender的Rime地址,rimeaddr_node_addr用于标识本节点的Rime地址。接下来,调用下一层的发送函数runicast_send完成发送。

1.3.4 数据接收


Rime协议栈建立连接后,就可以调用数据接收函数recv来接收数据,整个接收数据过程是通过上层调用下层服务来完成的,具体如下:

recv --> recv_from_stunicast --> recv_from_uc --> recv_from_broadcast --> recv_from_abc

函数recv首先判断该数据包是不是最后一个序列(数据包被拆分的情况下),若不是,将收到的数据写入物理存储介质。recv函数流程图如下:

 

2-  SEQ _2- \* ARABIC 16 recv函数流程图

函数recv首先判断接收到的包是不是最后一个序列(数据包太大时,需要拆分),如果是最后一个就返回(用最后序列号标识包传递完毕)。若不是最后一个序列,意味着还有数据要接收。接着判断发送者地址是否为空,若是,说明节点未曾接收该包的任何序列,则建立文件以存放数据。确保发送地址无误之后,若块小于块的最大值(RUCB_DATASIZE),即这是数据包最后的一块,写入最后一块,否则正常写入这块的数据。把块的数目累加,接着判断这块是否是最后一块(最后一块意味着数据包传输完毕),若是则将发送者地址设为空,否则返回。

1.3.5 释放连接

数据通信完毕之后,需要释放连接,以供其他进程使用。关闭连接实质上是将相应的连接结构体从链接表中删除。整个调用过程如下:

rucb_close -> runicast_close -> stunicast_close -> unicast_close -> broadcast_close -> abc_close -> channel_close -> list_remove

1.4 小结


本章深入浅出介绍Contiki操作系统内核和Rime协议栈的技术细节。首先,从全局视角出发描述了整个系统是如何运行的,即通过反复执行所有高优先级进程以及处理事件的方式。接着,循序渐进对Contiki两个核心机制进行剖析。先是介绍了Protothreads原理以及如何减少内存使用,分析了进程控制块以及进程调度,包括总体调度策略、进程状态转换、进程初始化、创建进程、启动进程、进程退出。随后介绍了事件机制,包括事件产生、事件调度、事件处理。除此之外,还分析了定时器这类特殊事件,包括创建定时器以及系统如何管理这些定时器。最后,剖析了Rime协议栈,先给出整体结构,而后分别介绍连接建立、数据发送、数据接收、释放连接。


上一篇:我的近况(从Contiki到网络编码)
下一篇:没有了

文章评论