[深入理解Contiki事件驱动] 线程调度

在上一节中,我们简单知道了 Contiki 中的线程长啥样。一个线程只有被调度才能体现其意义。这一节我们来了解线程是如何被调度的。

我们这里先了解下线程的基本概念,然后再学习线程是如何调度的。

线程链表

Contiki 维护了一个全局的线程链表 struct process *process_list,所有被启动的线程都将加入到这个线程链表中。

当前线程

除 process_list 外,Contiki 还维护了另一个全局变量 struct process *process_current,它时钟指向当前正字被调度的线程。

严格地说,并非 始终 指向,因为它在线程被调度前就被赋值了。

线程的状态

Contiki 中的线程有三种状态:

  • PROCESS_STATE_NONE:无效态。表示一个线程没有被启动,或者已经被退出。处于这种状态的线程不在线程链表中。
  • PROCESS_STATE_CALLED:执行态。表示一个线程正在被调度,即正在执行线程的执行实体(入口函数)。处于这种状态的线程在线程链表中。
  • PROCESS_STATE_RUNNING:可执行态(就绪态)。表示线程处于可执行态,即线程位于线程链表中,等待某一个时刻被调度、执行。

线程的优先级

在 Contiki 中,线程有两个优先级:

  • 普通优先级。线程结构体 struct process 中的成员 neeedspoll 为 0 的线程。
  • 高优先级。线程结构体 struct process 中的成员 neeedspoll 为 1 的线程。

我们可以在代码的运行过程中通过设置 needspool 来动态地调整线程的优先级。

此外,Contiki 中还维护了一个全局变量 poll_requested,用来表示当前所有线程中是否有高优先级的线程。poll_request 为 1 表示有高优先级线程;否则,无高优先级线程。

启动一个线程

函数 process_start() 用于启动一个已经创建的线程。创建线程的方法已经在上节中介绍。::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void
process_start(struct process *p, process_data_t data)
{
struct process *q;

// 如果线程已经在全局线程链表中(即已经被启动),直接返回
for(q = process_list; q != p && q != NULL; q = q->next);
if(q == p) {
return;
}
// 将该线程加入到线程链表的头部
p->next = process_list;
process_list = p;
// 设置线程的状态为可执行态
p->state = PROCESS_STATE_RUNNING;
// 初始化线程的上下文。详细信息请参考 线程切换 一节。
PT_INIT(&p->pt);

PRINTF("process: starting '%s'\n", PROCESS_NAME_STRING(p));

// 先线程投递一个同步事件 PROCESS_EVENT_INIT
// 关于投递事件,请参考 初始事件 一节
process_post_synch(p, PROCESS_EVENT_INIT, data);
}

总结一下啊,启动线程时将线程加入到线程链表的头部,然后设置一些状态等信息,向该线程投递一个同步事件 PROCESS_EVENT_INIT。下面我们看看 process_post_synch() 内部做了什么。

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;
}

由于 call_process() 的内部会改变 process_current 这个变量的值,所以在进入 call_process() 前先将其保存到一个临时变量 caller,从 call_process() 退出后再从这个临时变量恢复回来。

call_process() :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static void
call_process(struct process *p, process_event_t ev, process_data_t data)
{
int ret;

#if DEBUG
if(p->state == PROCESS_STATE_CALLED) {
printf("process: process '%s' called again with event %d\n", PROCESS_NAME_STRING(p), ev);
}
#endif /* DEBUG */

// 如果线程是处于可执行态,且线程的入口函数不为 NULL,则执行该线程的执行实体(入口函数)。
if((p->state & PROCESS_STATE_RUNNING) && p->thread != NULL) {
PRINTF("process: calling process '%s' with event %d\n", PROCESS_NAME_STRING(p), ev);
// 将 process_current 指向即将被调度的线程
process_current = p;
// 设置线程的状态为“正在被执行态”
p->state = PROCESS_STATE_CALLED;
// 调用该线程的执行实体,即切换到该线程去执行
ret = p->thread(&p->pt, ev, data);
// 代码走到这里,说明已经执行完 p->thread 这个线程实体了。在 p->thread 这个线程实体内部,
// 会返回一个返回代码,表示该线程执行实体是由于什么原因而返回。关于这个返回状态,更详细的信
// 息请参考 线程切换 一节。
if(ret == PT_EXITED || ret == PT_ENDED || ev == PROCESS_EVENT_EXIT) {
// 根据线程入口函数的返回值,判断是否要将该线程从线程链表中删除
// exit_process() 会对线程做一些清理工作,然后将其从线程链表中删除
exit_process(p, p);
} else {
// 将线程的状态由“正在被执行态”切换回“等待被执行态”
p->state = PROCESS_STATE_RUNNING;
}
}
}

所以,函数 call_process() 才会真正去执行一个线程。此时我们再回头看一下,可以得出一个结论:process_start() 在启动一个线程时,会立即执行该线程的线程实体

设置线程的优先级

通过 process_poll 函数可以将一个线程设置为高优先级的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
process_poll(struct process *p)
{
if(p != NULL) {
if(p->state == PROCESS_STATE_RUNNING || p->state == PROCESS_STATE_CALLED) {
// 现将线程的 needspoll 成员置为 1
p->needspoll = 1;
// 再将全局变量 poll_requested 置为 1,以告诉线程的调度器
// 现在有线程处于高优先级状态。然后调度器在下次调度线程时,会
// 优先调度高优先级的线程
poll_requested = 1;
}
}
}

poll 的本意是轮询,但是在 Contiki 中所有 needspoll 被设为 1 的线程会比一般线程先被轮询,所以直接将 process_poll 叫做设置线程的优先级,这样更容易理解。

线程的调度算法

Contiki 中,线程的调度策略体现在 process_run() 这个函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int
process_run(void)
{
// 如果有线程被置为高优先级,则先调度这些线程
if(poll_requested) {
do_poll();
}

// 再调度一个普通线程
do_event();

// nevents 表示当前系统还有多少个事件需要处理,更多信息请参考 初识事件 一节。
return nevents + poll_requested;
}

处理高优先级线程

do_poll():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
do_poll(void)
{
struct process *p;

poll_requested = 0;
// 轮询线程链表中的所有线程,如果该线程为高优先级的线程,则执行该线程
for(p = process_list; p != NULL; p = p->next) {
if(p->needspoll) {
p->state = PROCESS_STATE_RUNNING;
p->needspoll = 0;
// 调用 call_process 调度/执行该线程
call_process(p, PROCESS_EVENT_POLL, NULL);
}
}
}

注意,do_poll() 这个函数将线程链表中所有需要高优先级的线程都执行了之后才返回的,这与后面我们要学习的 do_event() 有所不同。

处理低优先级线程

当调度器处理完所有高优先级的线程后,会再使用函数 do_event() 调度低优先级的线程。这部分的内容涉及到事件驱动,我们后面单独拿一节来讲解。

总结

本节我们先学习了线程的一些基础知识,比如全局的线程链表、当前线程、线程的状态、优先级,在理解了这些基础概念之后,我们再学习了线程的调度,比如启动一个线程,轮询线程链表中所有高优先级的线程,设置线程的优先级等。这些概念,第一次接触的时候可能有很多疑惑,此时不要停下脚步,待学习完后面的所有章节后,我们再回来回顾一下,那时就会明白很多。