[深入理解Contiki事件驱动] 事件 event

事件驱动事件驱动,没有事件如何驱动,所以我们学习学习事件。

事件的概念

1
typedef unsigned char process_event_t;

可以看出,事件的本质是一个无符号字符类型的整数,为了便于理解,我们可以将其理解为一个编号。

这就是编程抽象思维的一种体现。

系统预定义事件

1
2
3
4
5
6
7
8
9
10
11
#define PROCESS_EVENT_NONE            0x80
#define PROCESS_EVENT_INIT 0x81
#define PROCESS_EVENT_POLL 0x82
#define PROCESS_EVENT_EXIT 0x83
#define PROCESS_EVENT_SERVICE_REMOVED 0x84
#define PROCESS_EVENT_CONTINUE 0x85
#define PROCESS_EVENT_MSG 0x86
#define PROCESS_EVENT_EXITED 0x87
#define PROCESS_EVENT_TIMER 0x88
#define PROCESS_EVENT_COM 0x89
#define PROCESS_EVENT_MAX 0x8a

再次证明,事件的本质只是一个无符号字符类型的整数。

此外,系统还定义了一个静态全局的变量 lastevent,它始终指向系统中编号最大的事件,并在线程环境初始化时被初始化:

1
2
3
4
5
6
7
static process_event_t lastevent;

void process_init(void)
{
lastevent = PROCESS_EVENT_MAX;
......
}

分配事件

1
2
3
4
5
6
7
除了上面预定义的事件外,我们还可以根据需要申请新的事件: ::

process_event_t
process_alloc_event(void)
{
return lastevent++;
}

它先返回编号最大的事件,然后再将 lastevent 递增。

事件驱动

线程的执行实体接收到事件(即一个事件编号)后,会根据这个编号来做相应的处理。我们可以简单地将一个线程实体概括如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PROCESS_THREAD(thread_name, ev, data)
{
PROCESS_BEGIN();

if (ev == 事件1){
xxxx
} else if (ev == 事件2) {
xxxx
} else if (ev == 事件3) {
xxxx
} ......

PROCESS_END();
}

也就是说,线程实体根据传入的事件,来做相应的处理,即一个给定的事件,可以驱使线程做与这个事件相关的事儿。这就是为什么将 Contiki 叫做事件驱动的操作系统的原因。

上面使用的 if…else if…else if… 的方式来判断事件的类型,而没有使用 switch..case,这与 Contiki 设计的线程模型相关,具体信息请参考 线程切换 一节。我们只需要记住,尽量别用 switch…case 这种语句。

事件的控制结构

线程有一个线程控制结构体,同理,事件也有一个事件控制结构体。

struct event_data:

1
2
3
4
5
struct event_data {
process_event_t ev;
process_data_t data;
struct process *p;
};

其成员变量:

  • ev:事件,即一个编号。
  • data:指向需要传递给线程实体(入口函数)的数据。
  • p:该事件所绑定的线程。

事件内部的缓冲模型

Contiki 的内核维护了一个环形缓冲队列,这个队列里的每个元素都是一个事件的控制结构。这个环形缓冲队列是由一个数组构成的: ::

static struct event_data events[PROCESS_CONF_NUMEVENTS];

这个环形缓冲最多能容纳 PROCESS_CONF_NUMEVENTS 个事件的控制结构。

此外,内核还定义了两个变量:

1
static process_num_events_t nevents, fevent;

其中:

  • nevents:表示该缓冲队列中已经存放的事件控制结构的个数。
  • fevent:是一个下标索引,“指向”缓冲中存放的第一个事件控制结构。

关于这个缓冲模型如何使用,在本节后续部分会体现处理。

向线程投递事件

一个线程的执行实体要被调用,必须将一个事件投递给该线程。事件的投递分为两类:

  • 同步投递:指的是将事件投递给一个线程后,该线程会立即执行。
  • 异步投递:指的是将事件投递给一个线程后,该线程不会立即执行。具体的做法是,先将事件的相关控制信息保存到事件的缓冲队列中,随后调度器会从这个缓冲队列中取出事件的控制信息,然后调用线程的执行实体。

同步、异步还可以这样理解:

  • 同步是做程序必须等待该事件完成后才能继续执行下一行代码;
  • 异步是指程序不必等待该事件完成就能执行下一行代码,且事件具体完成的时间是事先无法预料的。

同步投递

其实同步投递已经在上一节中介绍过了,即 process_post_synch() 这个函数:

1
2
3
4
5
6
7
8
void
process_post_synch(struct process *p, process_event_t ev, process_data_t data)
{
struct process *caller = process_current;

call_process(p, ev, data);
process_current = caller;
}

这次我们主要关注该函数的三个参数:

  • p:要接收事件的线程
  • ev:要投递给线程的事件
  • data:指向要传递给线程的执行实体的数据

咦,这不就是事件的控制结构体的各个成员嘛!

异步投递

函数 process_post 用于向线程投递一个异步事件: ::

int process_post(struct process *p, process_event_t ev, process_data_t data)
{
static process_num_events_t snum;

  if(nevents == PROCESS_CONF_NUMEVENTS) {
     // 事件的缓冲队列满了,返回一个错误码给调用线程
     return PROCESS_ERR_FULL;
  }

  // fevent 是事件缓冲队列中缓存的第一个元素的索引
  // nevents 是事件缓存队列中已缓存的元素的个数
  // fevent + nevents 就表示缓存队列中已缓存数据(最后一个元素)的下一个索引,
  // 也就是用来存储此次事件控制结构的索引
  // 对 PROCESS_CONF_NUMEVENTS 进行求模运算是因为缓冲队列是环形的
  snum = (process_num_events_t)(fevent + nevents) % PROCESS_CONF_NUMEVENTS;
  // 将事件的控制结构信息保存到该索引所对应的内存空间里面。
  events[snum].ev = ev;
  events[snum].data = data;
  events[snum].p = p;
  // 缓冲队列中元素已缓冲元素个数递增 1
  ++nevents;

  return PROCESS_ERR_OK;

}

该函数的三个参数与同步投递的参数是一样的:

  • p:要接收事件的线程
  • ev:要投递给线程的事件
  • data:指向要传递给线程的执行实体的数据

所谓的异步投递,指的是将事件的控制结构信息保存到事件的缓冲队列里面,等待调度机处理。

处理缓冲队列中的事件

Contiki 中的调度器使用 do_event() 函数处理缓冲在事件缓冲队列中的事件:

.. code-block:: bash
:linenos:

static void do_event(void)
{
static process_event_t ev;
static process_data_t data;
static struct process *receiver;
static struct process *p;

  // 如果缓冲队列中缓冲了数据,处理之
  if(nevents > 0) {
     // 取出缓冲队列中的第一个数据,即索引 fevent 处存放的数据
     ev = events[fevent].ev;
     data = events[fevent].data;
     receiver = events[fevent].p;

     // 取出数据后,fevent + 1
     // 对 PROCESS_CONF_NUMEVENTS 进行求模运算是因为缓冲队列是环形的
     fevent = (fevent + 1) % PROCESS_CONF_NUMEVENTS;
     // 取出数据后,缓冲队列缓冲的总元素个数递减 1
     --nevents;

     // Contiki 中定义了一个特殊的线程,叫做广播线程,即 PROCESS_BROADCAST
     // 当向该线程投递事件时,实际上相当于给所有的线程都投递了一个事件
     if(receiver == PROCESS_BROADCAST) {
        // 如果接收线程是广播线程,遍历线程链表,依次调用所有线程
        for(p = process_list; p != NULL; p = p->next) {
           if(poll_requested) {
              // 如果有高优先级的线程,则先调度高优先级的线程
              do_poll();
           }
           // 调用线程
           call_process(p, ev, data);
        }
     } else {
        if(ev == PROCESS_EVENT_INIT) {
           // 如果该事件是一个初始化事件,先修改该线程的状态为可执行态
           receiver->state = PROCESS_STATE_RUNNING;
        }
        // 调用线程
        call_process(receiver, ev, data);
     }
  }

}

注意到一点,函数 do_event() 只从事件的缓冲队列中取出了一个事件,然后投递给对于的线程,即 do_event() 每次只处理一个事件。而 do_poll() 会处理线程链表中所有高优先级的线程。