2021年(1)
分类: 嵌入式
2021-01-08 00:16:03
原文地址:Contiki学习笔记(重写版) 作者:Jelline
2013年写硕士毕业论文时,基于之前Contiki学习笔记的系列博文,对Contiki学习笔记重新整理,有的甚至是重写,并修改了一些错误,增加了很多图,便于理解。
我在word是排好版的,本来想的是用离线博客工具(如Zoundry Raven, Windows Live Writer)发布(在线编辑器,插入图片很麻烦),但现在CU不支持离线博客工具了,只好放弃。后面想着,把文档上传到百度文库,然后以内嵌代码的形式将文章置于该页面,但刚才试了,行不通。最后,只能上传整个文档,并将文本附在文件之后(注:图片没法正常显示)。强烈建议您,下载pdf文档阅读,有目录,层次更清晰。
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版本。
嵌入式系统可以看作是一个运行着死循环主函数系统,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; } |
进程无疑是一个系统最重要的概述,Contiki的进程机制是基于Protothreads线程模型,为确保高优先级任务尽快得到响应,Contiki采用两级进程调度。
Contiki使用Protothreads[ NOTEREF _Ref349908876 \h \* MERGEFORMAT 8]轻量级线程模型,在Protothreads基础上进行封装。为了适应内存受限的嵌入式系统,瑞典计算机科学研究所设计Protothreads,实际上是一种轻量级无线结构的线程库。传统的桌面操作系统甚至服务器操作系统,每个进程都拥有自己的栈,进行进程切换时,将进程相关的信息(包括局部变量、断点、寄存器值)存储在栈中。然而,对于嵌入式系统,尤其是内存受限的传感器节点几乎不现实,基于这点考虑,Protothreads巧妙地让所有进程共用一个栈,传统的进程与Protothreads对比示意图如下:
图 2- SEQ 图_2- \* ARABIC 2 Thread与Protothreads栈对比示意图
从图可以看出,原本需要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语言跳转到被中断的行 |
正如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; |
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进行善后工作,最后将该进程从进程链表删除。
事件驱动机制广泛应用于嵌入式系统,类似于中断机制,当有事件到来时(比如按键、数据到达),系统响应并处理该事件。相对于轮询机制,事件机制优势很明显,低功耗(系统处于休眠状态,当有事件到达时才被唤醒)和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); |
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就退出。
传统的分层通信架构(communication architectures)很难满足资源受限的传感器网络,于是研究者转向跨层优化(比如将顶层数据聚合功能放在底层实现),但这导致系统变得更脆弱以及难以控制(fragile and unmanageable systems)。因此,传统分层通信结构再次得到重视,同时研究发现,传统分层效率几乎可以与跨层优化相媲美[]。基于此,Rime也采用分层结构。
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)发出去,接收来自无线射频驱动所有的包并交给上层。
使用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 open、coon、callbacks对应关系
(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,需要用户自己实现。
receiver、sender
用于标识接收者和发送者。这里的receiver是指目的节点的接收地址。
chunk
数据块数目。
last_seqno
一次数据发送多个片段的最后一个序列号,当接收端接收到数据时,判断其序列号是否等于最后一个序列号,若等于则不接收(即接收到最后一个数据块,停止接收)。
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完成发送。
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),即这是数据包最后的一块,写入最后一块,否则正常写入这块的数据。把块的数目累加,接着判断这块是否是最后一块(最后一块意味着数据包传输完毕),若是则将发送者地址设为空,否则返回。
数据通信完毕之后,需要释放连接,以供其他进程使用。关闭连接实质上是将相应的连接结构体从链接表中删除。整个调用过程如下:
rucb_close -> runicast_close -> stunicast_close -> unicast_close -> broadcast_close -> abc_close -> channel_close -> list_remove
本章深入浅出介绍Contiki操作系统内核和Rime协议栈的技术细节。首先,从全局视角出发描述了整个系统是如何运行的,即通过反复执行所有高优先级进程以及处理事件的方式。接着,循序渐进对Contiki两个核心机制进行剖析。先是介绍了Protothreads原理以及如何减少内存使用,分析了进程控制块以及进程调度,包括总体调度策略、进程状态转换、进程初始化、创建进程、启动进程、进程退出。随后介绍了事件机制,包括事件产生、事件调度、事件处理。除此之外,还分析了定时器这类特殊事件,包括创建定时器以及系统如何管理这些定时器。最后,剖析了Rime协议栈,先给出整体结构,而后分别介绍连接建立、数据发送、数据接收、释放连接。