本章是我们真正从从 0 到 1 写 RT-Thread 的第一章,属于基础中的基础,必须要学会创建线程,并重点掌握线程是如何切换的。因为线程的切换是由汇编代码来完成的,所以代码看起来比较难懂,但是我会尽力把代码讲得透彻。如果本章内容学不会,后面的内容根本无从下手。
在这章中,我们会创建两个线程,并让这两个线程不断地切换,线程的主体都是让一个变量按照一定的频率翻转,通过 KEIL 的软件仿真功能,在逻辑分析仪中观察变量的波形变化,最终的波形图具体见图 6-1。
其实,图 6-1 的波形图的效果,并不是真正的多线程系统中线程切换的效果图,这个效果其实可以完全由裸机代码来实现,具体见代码清单 6-1。
在多线程系统中,两个线程不断切换的效果图应该像图 6-2 所示那样,即两个变量的波形是完全一样的,就好像 CPU 在同时干两件事一样,这才是多线程的意义。虽然两者的波形图一样,但是,代码的实现方式是完全不一样的,由原来的顺序执行变成了线程的主动切换,这是根本区别。这章只是开始,我们先掌握好线程是如何切换,在后面章节中,我们会陆续的完善功能代码,加入系统调度,实现真正的多线程。千里之行,始于本章节,不要急。
《什么是线程》
在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多线程系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为线程。线程的大概形式具体见代码清单 6-2。
《创建线程》
1、定义线程栈
我们先回想下,在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生。那么系统在运行的时候,全局变量放在哪里,子函数调用时,局部变量放在哪里,中断发生时,函数返回地址发哪里。如果只是单纯的裸机编程,它们放哪里我们不用管,但是如果要写一个 RTOS,这些种种环境参数,我们必须弄清楚他们是如何存储的。在裸机系统中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由 C 库函数_main 进行初始化。
但是,在多线程系统中,每个线程都是独立的,互不干扰的,所以要为每个线程都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM 中。
本章我们要实现两个变量按照一定的频率轮流的翻转,每个变量对应一个线程,那么就需要定义两个线程栈,具体见代码清单 6-3。在多线程系统中,有多少个线程就需要定义多少个线程栈。
代码清单 6-3 (1):线程栈其实就是一个预先定义好的全局数据,数据类型为rt_uint8_t,大小我们设置为 512。在 RT-Thread 中,凡是涉及到数据类型的地方,RT-Thread 都会将标准的 C 数据类型用 typedef 重新取一个类型名,以“rt”前缀开头。这些经过重定义的数据类型放在 rtdef.h(rtdef.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 rtt/source 这个组文件)这个头文件,具体见代码清单 6-4。代码清单 6-4 中除了rt_uint8_t 外,其它数据类型重定义是本章后面内容需要使用到,这里统一贴出来,后面将不再赘述。
代码清单 6-3 (2):设置变量需要多少个字节对齐,对在它下面的变量起作用。ALIGN是一个带参宏,在 rtdef.h 中定义,具体见代码清单 6-4。RT_ALIGN_SIZE 是一个在rtconfig.h(rtconfig.h 第一次使用需要在 User 文件夹下面新建然后添加到工程 user 这个组文件)中定义的宏,默认为 4,表示 4 个字节对齐,具体见代码清单 6-5。
2、定义线程函数
线程是一个独立的函数,函数主体无限循环且不能返回。本章我们在 main.c 中定义的两个线程具体见代码清单 6-6。
代码清单 6-6 (1)、(2):正如我们所说的那样,线程是一个独立的、无限循环且不能返回的函数。
3、定义线程控制块
在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多线程系统中,线程的执行是由系统调度的。系统为了顺利的调度线程,为每个线程都额外定义了一个线程控制块,这个线程控制块就相当于线程的身份证,里面存有线程的所有信息,比如线程的栈指针,线程名称,线程的形参等。有了这个线程控制块之后,以后系统对线程的全部操作都可以通过这个线程控制块来实现。定义一个线程控制块需要一个新的数据类型,该数据类型在rtdef.h 这个头文件中声明,具体的声明见代码清单 6-7,使用它可以为每个线程都定义一个线程控制块实体。
代码清单 6-7 (1):目前线程控制块结构体里面的成员还比较少,往后我们会慢慢在里面添加成员。代码清单 6-7 (2):在 RT-Thread 中,都会给新声明的数据结构重新定义一个指针。往后如果要定义线程控制块变量就使用 struct rt_thread xxx 的形式,定义线程控制块指针就使用 rt_thread_t xxx 的形式。在本章实验中,我们在 main.c 文件中为两个线程定义的线程控制块,具体见代码清单6-8。
4、实现线程创建函数
线程的栈,线程的函数实体,线程的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由线程初始化函数 rt_thread_init()来实现,该函数在 thread.c(thread.c 第一次使用需要自行在文件夹 rtthread/3.0.3/src 中新建并添加到工程的 rtt/source组)中定义,在 rtthread.h 中声明,所有跟线程相关的函数都在这个文件定义。rt_thread_init()函数的实现见代码清单 6-9。
代码清单 6-9:rt_thread_init 函数遵循 RT-Thread 中的函数命名规则,以小写的 rt 开头,
表示这是一个外部函数,可以由用户调用,以_rt 开头的函数表示内部函数,只能由 RT
Thread 内部使用。紧接着是文件名,表示该函数放在哪个文件,最后是函数功能名称。
代码清单 6-9 (1):thread 是线程控制块指针。
代码清单 6-9 (2):entry 是线程函数名, 表示线程的入口。
代码清单 6-9 (3):parameter 是线程形参,用于传递线程参数。
代码清单 6-9 (4):stack_start 用于指向线程栈的起始地址。
代码清单 6-9 (5):stack_size 表示线程栈的大小,单位为字节。