[深入理解Contiki事件驱动] 初识线程 pthread

Contiki 里的线程是一个经过巧妙设计的、利用单线程模拟实现的多线程,因此请不要将其与传统意义上的线程/进程混淆。至于它是如何模拟的,我们学习到后面时就会逐渐理解。本节我们只是先初步窥视其表象。

事实上,很多 RTOS 里面都没有线程这个概念,而有一个类似的叫做 task 的概念。

线程是什么?站在应用开发者的角度,一个线程就是实现某个特定功能的代码(即一个函数)。事实上,如果你站在线程设计者的角度来看,它包含两部分:

  • 线程的执行实体:它是一个函数,也就是应用开发者眼中的线程。
  • 线程的控制结构:描述线程执行实体的一些控制信息,线程的调度器会根据这些控制信息对线程进行调度。

关于线程调度,请参考 线程调度 一节。

线程的控制结构

结构体 struct process 描述了线程相关的所有信息,它的作用类似于操作系统课程中所讲的 PCB(进程控制块),其源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
struct process {
struct process *next;
#if PROCESS_CONF_NO_PROCESS_NAMES
#define PROCESS_NAME_STRING(process) ""
#else
const char *name;
#define PROCESS_NAME_STRING(process) (process)->name
#endif
PT_THREAD((* thread)(struct pt *, process_event_t, process_data_t,
struct net_buf *, void *user_data));
struct pt pt;
unsigned char state, needspoll;
};

主要成员:

  • next:线程通常会被放到一个线程链表中,使用 next 指向线程链表中的下一个线程。
  • name:该线程的名字,其类型是字符串。如果宏 PROCESS_CONF_NO_PROCESS_NAMES 为 1,则这个线程的控制结构中包含有线程的名字;否则,线程的名字是个空字符串。
  • thread:该线程的执行实体,调度器调度线程时会调用该函数。执行实体也叫做入口函数。
  • pt:用于保存线程切换时的上下文。具体信息请参考 线程切换 一节。
  • state:线程的状态标志。具体信息请参考 线程调度 一节。
  • needspoll:线程的优先级标志,表示该线程需要被优先轮询。具体信息请参考 线程调度 一节。

此外,还定义了一个宏 PROCESS_NAME_STRING(process) 来获取线程的名字。如果线程没名字,返回空字符串。

尽管 process 的本意是 进程,但是它实际上是描述 线程 的一些控制信息,所以我们在这里将其统一叫做线程。

定义一个线程

使用宏 PROCESS() 定义一个线程

1
2
3
4
5
#define PROCESS(name, strname)   \
PROCESS_THREAD(name, ev, data, buf, user_data); \
struct process name = { NULL, \
strname,\
process_thread_##name }

我们可以看到,定义一个线程,其实包括两部分:

  • 定义一个线程的执行实体:由宏 PROCESS_THREAD()完成。
  • 定义该入口实体的控制信息:定义了一个 struct process 类型的变量,并对其成员做初始化。需要注意的是,struct process 一个有 6 个成员,但是只对前 3 个成员进行了初始化。后 3 个成员在啥时候初始化的呢?

该宏有两个参数:

  • name:线程的执行实体的“名字”,是函数的名字。调度器调度线程时,最终会调用/执行这个函数。
  • strname:线程的名字,是一个字符串。

线程的入口函数

使用宏 PROCESS_THREAD() 描述一个线程的执行实体

1
2
3
4
5
6
7
8
#define PROCESS_THREAD(name, ev, data, buf, user_data)		\
static PT_THREAD(process_thread_##name(struct pt *process_pt, \
process_event_t ev, \
process_data_t data, \
struct net_buf *buf, \
void *user_data))

#define PT_THREAD(name_args) char name_args

其中,“##”是连接符号,编译器在预编译阶段,会直接将“##”前面的部分和后面的部分组合起来。例如,代码 hello_##world 经过预处理后会变成 hello_world。

将上面的宏 PROCESS_THREAD() 展开

1
2
3
4
5
static char process_thread_name(struct pt * process_pt, \
process_event_t ev, \
process_data_t data, \
struct net_buf *buf, \
void *user_data)

我们可以看到,它其实就是定义了一个函数(的函数名、返回值、入参)。

线程的使用方法举例

在 Contiki 中,定义一个新线程的步骤是这样的:

  • 先使用宏 PROCESS() 定义一个线程
  • 再使用宏 PROCESS_THREAD() 实现线程的执行实体

相关代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
PROCESS(hello_world_process, "Hello world process");

PROCESS_THREAD(hello_world_process, ev, data)
{
// 这个是线程头部的固定格式。
PROCESS_BEGIN();

// 中间是线程实际的执行代码
printf("Hello, world\n");

// 这个是线程尾部的固定格式
PROCESS_END();
}

其中,PROCESS_BEGIN() 和 PROCESS_END() 是 Contiki 中定义一个线程的固定格式,我们暂且可以不深究,只需要记住这样使用即可。具体信息,请参考 线程切换 一节。

先暂不考虑 PROCESS_BEGIN() 和 PROCESS_END() 两个宏的作用,我们将上面的代码展开如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 申明线程的入口函数
static char process_thread_hello_world_process(/* 这里是参数 */);

// 定义一个线程,并对其成员进行初始化
struct process hello_world_process = {
NULL,
"Hello world process",
process_thread_hello_world_process
}


// 实现函数的入口函数
static char process_thread_hello_world_process(/* 这里是参数 */)
{
printf("Hello, world\n");
}

到这里,终于看到了我们熟悉的 C 语言写法。