Contiki教程——进程

概述

Contiki中的代码可以运行在下列两种执行上下文之一:合作式或者抢占式。合作式代码按顺序运行,抢占式代码可以暂停正在运行的合作式代码。Contiki中的进程运行在合作式上下文中,而中断和实时定时器运行在抢占式上下文中。


所有的Contiki程序都被叫做进程。一个进程是Contiki系统中被常规执行的一个代码片段。当系统启动时,或者一个包含进程的模块被加载到系统中时,进程开始运行。当一些事发生时,进程会运行,比如一个定时器到期了,或者有一个外部事件产生。

Contiki中的代码可以运行在下列两种执行上下文之一:合作式或者抢占式。合作式代码按顺序运行。合作式代码一旦运行就要运行到完成,然后其它合作式代码才能被调度运行。抢占式代码可以在任何时刻暂停合作式代码。当抢占式代码暂停合作式代码后,合作式代码必须等到抢占式代码运行结束后才能再次恢复运行。Contiki中两种调度上下文的概念如上图所示。

普通进程总是运行在合作式上下文中。实时任务和设备驱动中的中断处理程序可运行在抢占式上下文中。我们将在讨论定时器timers的时候再回到实时任务的主题上。


进程的结构

Contiki的进程由两部分组成:进程控制块和进程线程。进程控制块存储在内存中,它包含进程运行时的信息,比如进程名、进程状态、指向进程线程的指针。进程线程是存储在ROM中的一个代码块。

进程控制块PCB

进程控制块包含每个进程的信息,比如进程状态、指向进程的线程的指针,进程的文本名称。进程控制块只在内核内部使用,不能被进程直接访问。

进程控制块的内部结构体。用户代码不能直接访问进程控制块的任何成员。

 struct process {
   struct process *next;
   const char *name;
   int (* thread)(struct pt *,
                  process_event_t,
 		 process_data_t);
   struct pt pt;
   unsigned char state, needspoll;
 };

进程控制块是轻量级的,它只需要几个字节的内存。进程控制块的结构体如上面所示。该结构体中任何成员都不能被直接访问。只有进程管理函数能够访问这些成员。我们在这里描述该结构仅仅是为了说明进程控制块是多么地轻量级。进程控制块的第一个成员,next,指向了进程链表中的下一个进程控制块。成员name指向了进程的文本类型的名字。成员thread是一个函数指针,指向了进程的线程。成员stateneedspoll是内部标志,当进程被轮训时,通过函数process_poll()修改该标志。

进程控制块不是直接定义和申明的,而是通过宏PROCESS()。该宏有两个参数:用于访问该进程的进程控制块变量名,用于调试和打印进程的进程文本名字。Hello World例程中进程控制看的定义如下:

进程控制块的例子:

PROCESS(hello_world_process, "Hello world process");

进程线程

进程线程包含进程的代码。进程线程是一个单一的protothread,由进程调度器调度。进程线程的例子如下。

进程线程的例子:

 PROCESS_THREAD(hello_world_process, ev, data)
 {
   PROCESS_BEGIN();
 
   printf("Hello, world\n");
 
   PROCESS_END();
 }

Protothreads

当在等待某个事件发生时,protothread允许系统运行其它活动。protothread的概念是在开发Contiki的过程中提出来的,但是这个概念不是与Contiki绑定在一起的。protothread也可以很好地运行在许多其它的系统中。

Contiki运行在内存受限的系统之上,减小内存负载显得尤为重要。protothread提供了一种很好的方法,可以让C函数在没有传统线程内存负载的情况下,以类似线程的方式运行。

protothread可以看做是一个常规C函数。该函数的使用两个特殊的宏作为开始和结束:PT_BEGIN()PT_END()。在这两个宏之间,你可以使用一系列的protothread函数。

C预处理器实现了protothread的主要操作:

struct pt { lc_t lc };
 #define PT_WAITING 0
 #define PT_EXITED  1
 #define PT_ENDED   2
 #define PT_INIT(pt)          LC_INIT(pt->lc)
 #define PT_BEGIN(pt)         LC_RESUME(pt->lc)
 #define PT_END(pt)           LC_END(pt->lc);    \
                              return PT_ENDED
 #define PT_WAIT_UNTIL(pt, c) LC_SET(pt->lc);    \
                              if(!(c))           \
                                return PT_WAITING
 #define PT_EXIT(pt)          return PT_EXITED


通过C语言switch语句实现了Local continuations:

 typedef unsigned short lc_t;
 #define LC_INIT(c)   c = 0
 #define LC_RESUME(c) switch(c) { case 0:
 #define LC_SET(c)    c = __LINE__; case __LINE__:
 #define LC_END(c)    }

进程中的Protothreads

Contiki进程自身实现了一套protothread,它允许进程等待即将到来的事件。因此,Contiki进程中使用的protothread语句与上面章节介绍的纯protothread语句有微小的差异。

Contiki进程中使用的进程相关的protothread宏:

PROCESS_BEGIN(); // Declares the beginning of a process' protothread. 
PROCESS_END(); // Declares the end of a process' protothread. 
PROCESS_EXIT(); // Exit the process. 
PROCESS_WAIT_EVENT(); // Wait for any event. 
PROCESS_WAIT_EVENT_UNTIL(); // Wait for an event, but with a condition.
PROCESS_YIELD(); // Wait for any event, equivalent to PROCESS_WAIT_EVENT().
PROCESS_WAIT_UNTIL(); // Wait for a given condition; may not yield the process.
PROCESS_PAUSE(); // Temporarily yield the process.


事件

在Contiki中,当进程接收到一个事件后就会运行。Contiki中有两种事件:异步事件和同步事件。

当一个异步事件被发出时,该事件被放到内核中的事件队列中,并在一段时间后被传递到接收进程中。

当一个同步事件被发出是,该事件被立即传递到接收进程中。

异步事件

异步事件在被发出一段时间后才能被传递到接收进程。在事件被发出后和被传递前的这段时间内,它被保存在Contiki内核的事件队列中。

内核负责将事件队列中的事件传递到接收进程。内核循环遍历事件队列,通过调用进程将队列中的事件传递到进程中。

异步事件的接收者就可以是一个特殊进程,也可以是所有正在运行的通用进程。当接收者是一个特殊进程,内核就调用该进程并传递事件到该进程中。当事件接收者是系统中的所有通用进程,内核将一个接一个地顺序传递事件到所有的进程中。

异步事件通过函数process_post()发出。process_post()的内部实现很简单,它先检查当前事件队列的大小,检查是否还有可以存放事件的空间,然后再做决定。如果没有足够的空间,该函数返回一个错误。如果有足够的空间,该函数将事件插入到事件队列的末尾,然后返回。

同步事件

与异步事件不同的是,同步事件被发出后不经过事件队列,会被直接传递。同步事件只能被发出给一个特定进程。

由于同步事件直接被传递,因此传递一个同步事件在功能上等同于一个函数调用:接收进程被直接调用,发送进程在接收进程完成处理事件前一直处于阻塞状态。不过,接收进程不会被告诉所发出的事件是同步事件还是异步事件。

轮询

轮询请求是一个特殊的事件。进程可以通过调用函数process_poll()请求被轮询。进程请求轮询后,会尽可能快地被调用。当轮询到该进程时,会传递一个特殊的事件到进程中。

Polling is the way to make a process run from an interrupt. Theprocess_poll() function is the only function in the process modulethat is safe to call from preemptive mode.

事件标识符

事件被事件标识符所标识。事件标识符是一个8比特的数组,会被传递到接收进程中。接收进程可以根据接收到的不同的事件标识符来做相应不同的处理。

事件标识符的范围是0~255。在127以下的事件标识符可以在一个用户进程中自由使用,在128以上的事件标识符只能在不同的进程间使用。在128以上的事件标识符被内核所管理。

从128开始的数字被内核静态分配,用于实现不同的目的。这些标识符的定义如下。

Contiki内核保留的事件标识符:

 #define PROCESS_EVENT_NONE            128
 #define PROCESS_EVENT_INIT            129
 #define PROCESS_EVENT_POLL            130
 #define PROCESS_EVENT_EXIT            131
 #define PROCESS_EVENT_CONTINUE        133
 #define PROCESS_EVENT_MSG             134
 #define PROCESS_EVENT_EXITED          135
 #define PROCESS_EVENT_TIMER           136

这些事件标识符作用如下:

PROCESS_EVENT_NONE : 该事件标识符没有被使用。
PROCESS_EVENT_INIT : 该事件被发送到一个正在初始化的新进程中。
PROCESS_EVENT_POLL : 该事件被发送到一个轮询进程中。
PROCESS_EVENT_EXIT : 该事件被发送到一个正在被内核杀死的进程中。进程接收到该事件后,因为可能不会被再次调用,因此它可以选择清空自己分派到的资源。
PROCESS_EVENT_CONTINUE : 该事件被内核发送到一个执行了PROCESS_YIELD()而正在等待的进程。
PROCESS_EVENT_MSG : 该事件被发送到一个已经接收到通信消息的进程。它一般被用于IP栈去通知进程有消息到来了,也可以用于两个进程间表示一个通用消息到来了。
PROCESS_EVENT_EXITED : 当一个进程将要退出时,该事件被发送到所有进程。发送事件的同时,还会发送一个指向正在退出的进程的进程控制块的指针。当接收到该事件时,接收进程将清除将要退出进程所分配的状态。
PROCESS_EVENT_TIMER : 该事件被发送都一个事件定时器etimer到期的进程。

除静态分配事件号之外,进程可以分配用于进程间的大于128的事件标识符。被分配的事件标识符被存储在一个变量中,接收进程可以使用该变量来匹配事件标识符。

进程调度器

进程调度器的作用是调用进程。进程调度器通过调用实现进程线程的函数来调用进程。Contiki中的所有进程被设计为响应传递到进程中的事件,或者响应进程请求的轮询。进程调度器在调度进程的时候会将事件标识符和一个不透明指针传递到进程中。该指针由进程调用者提供,可以设置为NULL(该事件不需要传递数据)。当进程请求轮询时,不会传递数据。

开始进程

process_start()开始一个进程。该函数的目的是设置进程控制块,将进程加入到内核有效进程链表中,然后调用进程线程中的初始化代码。process_start()被调用后,该进程就开始运行了。

process_start()函数先做一个明智的检查,检查是否该进程已经存在进程链表中。如果是,表明该进程已经被运行了,process_start()函数直接返回。

在确认该进程没有开始运行后,内核将该进程加入到链表中,并设置进程控制块。进程的状态被设置为PROCESS_STATE_RUNNING,进程的线程被PT_INIT()初始化。

最后,内核为进程发送一个同步事件PROCESS_EVENT_INIT,且传递一个不透明指针给进程。该指针是由调用process_start()的进程传入的,用于传递给要运行的进程一些信息。不过,这个指针一般都设为NULL。

当进程接收到它的第一个事件PROCESS_EVENT_INIT,进程将执行该进程线程的第一部分。通常,这部分包含进程开始时将运行的初始化代码。

退出进程和杀死进程

进程退出有两种方法:进程自动退出、被其它进程杀死。调用函数PROCESS_EXIT()或者当进程线程执行到PROCESS_END()语句时,进程将退出。进程可以调用函数process_exit()杀死另一个进程。

当一个进程退出(无论是主动退出还是被其它进程杀死而退出),Contiki内核会发送一个事件通知其它进程。这可以让其它进程释放退出进程所占有的资源。例如,uIP TCP/IP协议栈将关闭和移除所有退出进程拥有的网络连接。事件PROCESS_EVENT_EXITED将会被以同步事件的方式发送到其它所有进程。

当一个进程被另一个进程杀死时,被杀的进程将会接收到同步事件PROCESS_EVENT_EXIT。该事件通知将要被杀死的进程和能够释放已分配资源的进程,或者其它将要退出的进程。

在Contiki内核发送事件通知将要退出的进程后,将从进程链表中删除该进程。

自启动进程

Contiki提供了一个机制,在系统启动时,或者包含进程的模块被加载时,自动运行进程。该机制由自启动模块实现。该机制能够让模块开发者告知系统该模块中包含什么进程。当模块从内存中移除时,也能让系统做相应的处理。

自启动进程保存在一个链表中,自启动模块利用该链表实现自动运行进程。进程启动的顺序与它在链表中的顺序一致。

自启动进程有两种时机:系统启动时、模块被加载时。所有需要在系统启动时自启动的进程必须被包含在一个单一的、系统级的链表中。这个自启动链表由用户提供,且一般在一个用户模块中。当这个模块被用作可加载模块时,链表能够让系统知道模块被加载时需要运行什么进程。

当加载一个模块时,模块加载器将查找自启动进程链表,并在模块被加载到内存后启动链表中的进程。当模块将要被卸载时,模块加载器利用该链表杀死在模块加载是启动的进程。

在Contiki中,自启动机制是启动用户进程最常用的方式。

一个进程的例子

为了更具体地讨论进程的工作过程,现在我们转到一个由两个进程的例子中来。该例程不仅展示了进程代码看上去的样子,还展示了进程生命周期的哥哥步骤。下面是一个简短的“Hello,World”风格的程序,但是这个例子更精细。

一个接收事件并打印其数字的进程的例子:

#include "contiki.h"
 
 PROCESS(example_process, "Example process");
 AUTOSTART_PROCESSES(&example_process);
 
 PROCESS_THREAD(example_process, ev, data)
 {
   PROCESS_BEGIN();
 
   while(1) {
     PROCESS_WAIT_EVENT();
     printf("Got event number %d\n", ev);
   }
 
   PROCESS_END();
 }


上面的代码是一个Contiki进程的完整例子。该进程被申明、定义、自动运行。我们将逐行分析这个例子。

在第1行,我们包含了Contiki头文件。contiki.h中包含了实现Contiki基本函数所需的所有头文件。在第3行,我们定义了进程控制块。进程控制块定义了进程控制块的名字example_process和文本的、人类可读的进程名字Example process

在定义进程控制块后,我们可以在其它表达式中使用该变量名。在第4行, 语句AUTOSTART_PROCESSES()告诉Contiki在系统启动时(或者如果这个模块被编译为可加载模块的情况下被加载时)自动启动进程example_process。自启动链表由指向进程控制块的指针组成,所以我们需要在example_process前面加&符号取其地址。

在第6行,我们开始定义进程线程。它包含了进程的变量名example_process和传递事件的变量ev以及数据data

在第8行,我们使用PROCESS_BEGIN()定义一个进程的开始。该定义标志着进程线程的开始。在进程每次被调度运行的时候,该申明语句上面的代码都会运行(或者重新运行)。该申明语句下面的代码根据实际进程线程控制流执行。在大多数情形下,你不需要在PROCESS_BEGIN()之上放任何代码。

在第10行,我们开始进程的主循环。正如我们之前所说的,Contiki进程不能包含永不结束的死循环。但是在这种情形下,我们的代码是安全的,因为进程将会等待时间。当一个Contiki进程在等待事件时,它会将控制权返回给Contiki内核。在该进程等待期间,内核将会为其它进程提供服务。

在第11行,进程等待事件的发生。表达式PROCESS_WAIT_EVENT()将返回控制权给Contiki内核,并等待内核传递事件到该进程。当Contiki内核传递事件给该进程后,PROCESS_WAIT_EVENT()后面的代码将被执行。在进程被唤醒后,将执行第12行的语句printf()。这一行只是打印进程接收到的事件编号。如果同时传如了一个指针,该指针变量就是data。不过,在本例中,我们忽略了这个指针。

第15行的语句PROCESS_END()标识进程的结束。每个Contiki进程必须包含PROCESS_BEGIN()PROCESS_END()。当执行到PROCESS_END()时,该进程就结束了,并从内核中的进程链表中移除。不过,在本例中,因为第10行和第13行之间存在死循环,永远不会执行到PROCESS_END()。只有系统被关掉、或者该进程被process_exit()杀死的时候才会停止运行。

一个启动进程并发送事件的函数:

 static char msg[] = "Data";
 
 static void
 example_function(void)
 {
   /* Start "Example process", and send it a NULL
      pointer. */
 
   process_start(&example_process, NULL);
  
   /* Send the PROCESS_EVENT_MSG event synchronously to
      "Example process", with a pointer to the message in the
      array 'msg'. */
   process_post_synch(&example_process,
                      PROCESS_EVENT_CONTINUE, msg);
  
   /* Send the PROCESS_EVENT_MSG event asynchronously to 
      "Example process", with a pointer to the message in the
      array 'msg'. */
   process_post(&example_process,
                PROCESS_EVENT_CONTINUE, msg);
 
   /* Poll "Example process". */
   process_poll(&example_process);
 }


两个进程间通过事件完成交互。上面例子中的函数启动了一个进程,并向它发送了一个同步事件和轮询请求。

总结

进程是Contiki中应用程序运行的基本方法。进程由进程控制块和进程线程组成。进程控制块包含进程运行的实时信息,进程线程包含进程实际执行的代码。protothread是为内存受限系统设计的轻量级线程。

在Contiki中,代码有两种执行上下文:共享式(代码用于不会抢占其它代码)和抢占式(抢占共享式代码并在执行完后返回控制权)。普通进程总是运行在共享式上下文,中断运行在抢占式上下文。唯一可以被抢占式模式调用的进程控制函数是process_poll()

进程通过发送事件实现与其它进程通信。当一个进程开始和介绍时都会接收到事件。

阅读更多
想对作者说点什么?
相关热词

博主推荐

换一批

没有更多推荐了,返回首页