目录
线程是轻量级的,不支持抢占的。一般用于设备驱动和其他比较重要的任务。线程的调度以优先级为参考,高优先级的线程会先得到执行。被调度的线程会持续执行,直到有阻塞操作才会停止。
| (1)栈区大小:这是一段内存区域,是线程栈区。可以根据实际情况自定义栈区的大小,单位为字节。 (2)线程入口函数:线程启动时调用的函数(执行的函数)。该函数最多可接收三个参数。 (3)线程调度的优先级:决定内核调度器给该线程分配的CPU时间(系统在某个时刻只能执行一个线程,大多数系统都用的是时间片轮换算法,就是多个进程在分配到的极短时间片轮流使用CPU,可参考“调度”这节内容)。 (4)线程选项:内核支持一系列 线程选项(thread options),允许线程在特殊情况下被特殊对待。 (5)启动延时:在启动线程之前,设置延时启动时间,即允许线程延迟启动。
|
调用线程创建函数k_thread_create()来创建一个线程,并决定是否立刻启动该线程。
| 函数原型 | k_tid_t k_thread_create(struct k_thread *new_thread, k_thread_stack_t stack,size_t stack_size, void (*entry)(void *, void *, void*),void *p1, void *p2, void *p3, int prio, u32_t options, s32_t delay) |
| 函数功能 | 创建一个线程。 |
| 参数 | (1)struct k_thread *new_thread:线程控制块。是一个结构体指针,传入的是结构体的地址。 (2)k_thread_stack_t stack指向线程的栈区的指针。跳转代码,可以发现,k_thread_stack_t是个指针数据类型,如下:
注意到这句注释: Stacks should always be created with K_THREAD_STACK_DEFINE(). 即栈区的定义,需要使用到这个宏来定义,并且,要定义成全局的[main()之外,各种函数体之外]。
跳到K_THREAD_STACK_DEFINE()的定义处:
这是个带参数的宏,第一个参数:指向栈区的符号名称;第二个参数:栈区的大小。可以发现,实际上这块空间被定义成了一个数组,而数组名代表数组的首地址(第一个元素的)。 (3)size_t stack_size:栈区的大小。 (4)void (*entry)(void *, void *, void*):入口函数。函数名本身就是地址,所以定义好函数,直接传入函数名即可。 (5)void *p1, void *p2, void *p3:在启动线程的时候,可以向入口函数传递三个参数。这里就很灵活,可以传任何数据类型的数据,定义对应即可。 (6)int prio:该线程的优先级。 (7)u32_t options:该线程的一些特殊选项。 (8)s32_t delay:决定是否需要延时启动线程【单位:ms】,如果需要创建一个立即启动的线程,那么就填入K_NO_WAIT。实际上K_NO_WAIT被定义成0,也就是延时0ms,就是不延时。
|
| 返回值 | 线程的标识符(ID号) |
| 定义处(源文件) | xxxxxx\kernel\thread.c
|
| 声明处(头文件) | xxxxxx\include\kernel.h
|
main.c
- /*
- * Copyright (c) 2012-2014 Wind River Systems, Inc.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
- #include
- #include
-
- #define MY_STACK_SIZE 500
- #define MY_PRIORITY 5
- #define main_sleep_time 2000
- #define pthread_sleep_time 3000
-
- //这行代码一定要定义成全局的,否则编译不通过
- K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
-
- // 线程入口函数
- void my_entry_point(void *str1, void *str2, void *str3) {
- // user application code
- printk("%s\r\n", "my_entry_point");
- while(1){
- k_sleep(pthread_sleep_time);
- printk("str1=%s str2=%s str3=%s\r\n",str1,str2,str3);
- }
- }
-
- void main(void)
- {
- printk("CONFIG_ARCH=%s\n", CONFIG_ARCH);
- struct k_thread my_thread_data;
- k_tid_t my_tid = k_thread_create(&my_thread_data, my_stack_area, /* 线程栈指针 */
- K_THREAD_STACK_SIZEOF(my_stack_area), /* 栈大小 */
- my_entry_point, /* 线程处理函数 */
- "123", "456", "789", /* 执行入口函数时传入的参数 */
- MY_PRIORITY, /* 线程优先级 */
- 0, /* 不使用选项字 */
- K_NO_WAIT); /* 立即启动 */
- while(1){
- k_sleep(main_sleep_time);
- printk("CONFIG_K_THREAD_SIZE=%d\r\n", CONFIG_K_THREAD_SIZE);
- }
- }
Zephyr.h里面有包含kernel.h,所以可以不用单独#include

可以直接使用宏K_THREAD_DEFINE()静态创建线程。
main.c
- /*
- * Copyright (c) 2012-2014 Wind River Systems, Inc.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
- #include
- #include
- #define MY_STACK_SIZE 500
- #define MY_PRIORITY 5
- #define main_sleep_time 2000
- #define pthread_sleep_time 3000
-
- // 线程入口函数
- void my_entry_point(void *str1, void *str2, void *str3) {
- // user application code
- printk("%s\r\n", "my_entry_point");
- while(1){
- k_sleep(pthread_sleep_time);
- printk("str1=%s str2=%s str3=%s\r\n",str1,str2,str3);
- }
- }
-
- K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
- my_entry_point, "123", "456", "789",
- MY_PRIORITY, 0, K_NO_WAIT);
-
- void main(void)
- {
- printk("CONFIG_ARCH=%s\n", CONFIG_ARCH);
- while(1){
- k_sleep(main_sleep_time);
- printk("CONFIG_K_THREAD_SIZE=%d\r\n", CONFIG_K_THREAD_SIZE);
- }
- }
A.静态创建一个立即启动的线程,需要注意线程的优先级。
如果线程优先级大于main()的优先级,那么线程可以先于mian()执行,如果低于main()的优先级,则后于mian()执行。之后就是调度的事了。
例:上面的代码中定义线程的优先级是5 #define MY_PRIORITY 5,而主线程(main()线程)的优先级是6。主线程的优先级在工程配置文件prj.conf里面有定义,如下:

对应编译出来的.config文件

也就是说,静态创建的这个线程应该是先于main()执行的,运行串口打印信息如下:

B.关于延迟启动问题
虽然创建的线程优先级比较高,但是如果延时启动该线程,那么它还是会后于main()得到执行(这是调度的内容),所以并不是说,优先级高的线程就会先执行。
例:延迟3秒启动线程

这是串口打印的内容

所以需要特别注意的一个问题,如果在线程做一些初始化操作,要注意有可能初始化没完成,其他线程就会去使用。
可以在入口函数里面直接返回(return)跳出while(1),同步结束执行,这种方式称为正常结束。伪代码如下:
- void my_entry_point(int unused1, int unused2, int unused3) {
- while (1) {
- ...
- if (
) { - return; /* thread terminates from mid-entry point function */
- }
- ...
- }
- /* thread terminates at end of entry point function */
- }
示例代码:
- // 线程入口函数
- void my_entry_point(void *str1, void *str2, void *str3)
- {
- // user application code
- int Cnt=0;
- printk("%s\r\n", "my_entry_point");
- while(1){
- k_sleep(pthread_sleep_time);
- printk("str1=%s str2=%s str3=%s\r\n",str1,str2,str3);
- Cnt++;
- if(Cnt==3){
- Cnt=0;
- //break; //下面的语句能得到执行
- return; //函数直接返回,下面的语句得不到执行
- }
- }
- printk("my_entry_point---exit()\r\n");
- }
在入口函数内部设置终止条件,满足条件则直接返回,正常结束线程,之后就只有主线程在运行。串口打印如下:

如果线程触发了一个致命错误,内核将自动终止该线程。
线程自己或者其他线程调用k_thread_abort()函数来终止线程。
| 函数原型 | void k_thread_abort(k_tid_t thread) { ………………………………………………………………. } |
| 函数功能 | 中止(abord)一个线程的执行,后面的代码都得不到执行。跟直接return的效果是一样的。 |
| 参数 | 创建线程时,返回的线程ID,也就是指定要结束的线程的ID号。 |
| 返回值 | 无 |
| 定义处(源文件) | xxxxxx\kernel\thread_abort.c |
| 声明处(头文件) | xxxxxx\include\kernel.h |
示例代码:
- /*
- * Copyright (c) 2012-2014 Wind River Systems, Inc.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
- #include
- #include
-
- #define MY_STACK_SIZE 500
-
- //主线程的优先级是6
- //这里设置线程的优先级为8[让main()先跑]
- #define MY_PRIORITY 8
-
- #define main_sleep_time 2000
- #define pthread_sleep_time 3000
-
- k_tid_t my_tid=NULL;
-
- //这行代码一定要定义成全局的,否则编译不通过
- K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
-
- // 线程入口函数
- void my_entry_point(void *str1, void *str2, void *str3)
- {
- // user application code
- int Cnt=0;
- printk("%s\r\n", "my_entry_point");
- while(1){
- k_sleep(pthread_sleep_time);
- printk("str1=%s str2=%s str3=%s\r\n",str1,str2,str3);
- Cnt++;
- if(Cnt==2){
- Cnt=0;
- //return;//正常结束
- //break;
- if(my_tid!=NULL)
- k_thread_abort(my_tid);//调用API结束
- }
- }
- printk("my_entry_point---exit()\r\n");
- }
-
- void main(void)
- {
- printk("CONFIG_ARCH=%s\n", CONFIG_ARCH);
- struct k_thread my_thread_data;
- my_tid = k_thread_create(&my_thread_data, my_stack_area,/* 线程栈指针 */
- K_THREAD_STACK_SIZEOF(my_stack_area), /* 栈大小 */
- my_entry_point, /* 线程处理函数和传入参 */
- "123", "456", "789",
- MY_PRIORITY, /* 线程优先级 */
- 0, /* 不使用选项字 */
- K_NO_WAIT); /* 立即启动 */
-
- if(my_tid==NULL){
- printk("fail to create thread\r\n");
- }else{
- printk("success to create thread\r\n");
- }
- while(1){
- k_sleep(main_sleep_time);
- printk("CONFIG_K_THREAD_SIZE=%d\r\n", CONFIG_K_THREAD_SIZE);
- }
- }
串口打印:

内核支持一系列线程选项(thread options),以允许线程在特殊情况下被特殊对待。这些与线程关联的选项在线程创建时就被指定了。
如果不使用选项字,则该参数填零。如果线程需要选项,可以通过选项名指定。如果需要多个选项,使用符号 | 作为分隔符。(即按位或操作符)。
这些选项字都以宏定义的形式定义在kernel.h中:

选项字为:K_ESSENTIAL。表明线程是不可以被中止的,所以不管线程是正常结束或者是异常中止,内核都认为是产生了一个致命的系统错误。
示例代码:
- /*
- * Copyright (c) 2012-2014 Wind River Systems, Inc.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
- #include
- #include
- #define MY_STACK_SIZE 500
-
- //主线程的优先级是6
- //这里设置线程的优先级为8[让main()先跑]
- #define MY_PRIORITY 8
-
- #define main_sleep_time 2000
- #define pthread_sleep_time 3000
- k_tid_t my_tid=NULL;
-
- //这行代码一定要定义成全局的,否则编译不通过
- K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
-
- // 线程入口函数
- void my_entry_point(void *str1, void *str2, void *str3)
- {
- // user application code
- int Cnt=0;
- printk("%s\r\n", "my_entry_point");
- int *pt=0;
- while(1){
- k_sleep(pthread_sleep_time);
- printk("str1=%s str2=%s str3=%s\r\n",str1,str2,str3);
- Cnt++;
- if (Cnt==2) {
- Cnt=0;
- //return;//正常结束
- //break;
-
- printf("pt=%d",*pt+2); //异常结束
-
- #if 0
- if(my_tid!=NULL)
- k_thread_abort(my_tid);//调用API结束
- #endif
- }
- }
- printk("my_entry_point---exit()\r\n");
- }
-
- void main(void)
- {
- printk("CONFIG_ARCH=%s\n", CONFIG_ARCH);
- struct k_thread my_thread_data;
- my_tid = k_thread_create(&my_thread_data, my_stack_area,/* 线程栈指针*/
- K_THREAD_STACK_SIZEOF(my_stack_area),/* 栈大小*/
- my_entry_point, /* 线程处理函数和传入参数*/
- "123", "456", "789",
- MY_PRIORITY, /* 线程优先级 */
- K_ESSENTIAL, /* 不可中止的线程 */
- K_NO_WAIT); /* 立即启动 */
- if (my_tid==NULL) {
- printk("fail to create thread\r\n");
- } else {
- printk("success to create thread\r\n");
- }
-
- while(1){
- k_sleep(main_sleep_time);
- printk("CONFIG_K_THREAD_SIZE=%d\r\n", CONFIG_K_THREAD_SIZE);
- }
- }
按照官方文档的说法,在不可中止的线程里面操作空指针,是会导致系统奔溃的。一般来说,空指针的操作会导致崩溃。比如X86平台的VS:

但是应用实际中可能还跟实现相关,空指针跟CPU架构、芯片的设计(0地址是否有效,是否已经映射使得0地址合法)。
注意:一般情况下,普通创建的线程都不是必须线程。
指定线程使用CPU的浮点寄存器:K_FP_REGS
指定线程使用CPU的SSE寄存器:K_SSE_REGS
这两个选项都是跟X86架构相关的选项,可以不用理会。
详细的调度相关理论放到另一个文档,目前只需要知道线程是如何依靠优先级进行调度的。内核调度线程的基本依据:(1)优先级 (2)线程休眠(让出CPU使用权)
| 函数原型 | void k_sleep(s32_t duration){ ………………………………………………………………. } |
| 函数功能 | 休眠当前线程,让出CPU使用权,后面按照优先级进行排队的线程才会得以执行。如果不休眠,则会一直卡在当前线程,其他线程得不到调度。------->线程调度 |
| 参数 | 休眠时间。 |
| 返回值 | 无 |
| 定义处(源文件) | xxxxxx\kernel\sched.c |
| 声明处(头文件) | xxxxxx\include\kernel.h |
示例1
主线程优先级:6 自定义线程1优先级:7 自定义线程2优先级:8
main.c
- /*
- * Copyright (c) 2012-2014 Wind River Systems, Inc.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
- #include
- #include
-
- #define MY_STACK_SIZE 500
- #define MY_PRIORITY1 7
- #define MY_PRIORITY2 8
-
- void my_entry_point1(void *pt1,void *pt2,void *pt3)
- {
- printk("%s\r\n", "my_entry_point1");
- while(1){
- k_sleep(1000);
- printk("pthread1_run\r\n");
- }
- }
-
- void my_entry_point2(void *pt1,void *pt2,void *pt3)
- {
- printk("%s\r\n", "my_entry_point2");
- while(1){
- k_sleep(1000);
- printk("pthread2_run\r\n");
- }
- }
-
- K_THREAD_DEFINE(my_tid1, MY_STACK_SIZE,my_entry_point1, NULL, NULL, NULL,MY_PRIORITY1, 0, 0);
- K_THREAD_DEFINE(my_tid2, MY_STACK_SIZE,my_entry_point2, NULL, NULL, NULL,MY_PRIORITY2, 0, 0);
-
- void main(void)
- {
- while(1){
- k_sleep(1000);
- printf("main_run\r\n");
- }
- }
调度的顺序应该是按照优先级从高到低,串口打印如下:

示例2:主线程不休眠,不让出CPU使用权。
| void main(void) { while(1){ //k_sleep(1000); //printf("main_run\r\n"); } } |
后面的两个线程根本得不到调度,串口打印如下:

示例3:线程1不休眠,不让出CPU使用权
- #include
- #include
-
- #define MY_STACK_SIZE 500
- #define MY_PRIORITY1 7
- #define MY_PRIORITY2 8
-
- void my_entry_point1(void *pt1,void *pt2,void *pt3)
- {
-
- printk("%s\r\n", "my_entry_point1");
- while(1){
- //k_sleep(1000);
- printk("pthread1_run\r\n");
- }
- }
-
- void my_entry_point2(void *pt1,void *pt2,void *pt3)
- {
-
- printk("%s\r\n", "my_entry_point2");
- while(1){
- k_sleep(1000);
- printk("pthread2_run\r\n");
- }
- }
- K_THREAD_DEFINE(my_tid1, MY_STACK_SIZE,my_entry_point1, NULL, NULL, NULL,MY_PRIORITY1, 0, 0);
- K_THREAD_DEFINE(my_tid2, MY_STACK_SIZE,my_entry_point2, NULL, NULL, NULL,MY_PRIORITY2, 0, 0);
-
- void main(void)
- {
- while(1){
- k_sleep(1000);
- //printf("main_run\r\n");
- }
- }
主线程是最先启动的,然后让出CPU使用权,自定义线程1优先级是7,所以轮到线程1执行,到它执行的时候,就不让出CPU使用权,CPU就一直执行线程1了(不会在三个线程中正常调度,轮流执行)。串口打印:

同样的,如果调度到线程2,没有k_sleep(),不让出CPU使用权,也会是同样的效果。跟main()线程不让出CPU使用权一样的道理。
所以,写应用,写多线程的时候,要注意两个问题:(1)线程的优先级[决定线程占有CPU的先后顺序] (2)k_sleep(),是否让出CPU使用权。
| 函数原型 | void k_thread_suspend(struct k_thread *thread){ …………………………………………………………………………… } |
| 函数功能 | 线程被挂起,则线程就会停止执行。可以挂起包括调用线程在内的所有线程(在线程内部调用函数将自己挂起或者将别的线程挂起),对已经挂起的线程再次挂起时不会产生任何效果。 线程一旦被挂起,它将一直不能被调度,除非另一个线程调用 k_thread_resume() 取消挂起(恢复执行)指定的线程。 |
| 参数 | 线程ID |
| 返回值 | 无 |
| 定义处(源文件) | ATS350B\kernel\thread.c |
| 声明处(头文件) | ATS350B\include\kernel.h |
| 函数原型 | void k_thread_resume(struct k_thread *thread){ ………………………………………………………………………….. } |
| 函数功能 | 取消挂起(恢复执行)指定的线程。 |
| 参数 | 线程ID |
| 返回值 | 无 |
| 定义处(源文件) | ATS350B\kernel\thread.c |
| 声明处(头文件) | ATS350B\include\kernel.h |
通过判断获取到的shell命令行的参数来决定来挂起或者取消挂起指定的线程。
- #include
- #include
- #include
/*Shell*/ -
- #define MY_STACK_SIZE 500
- #define MY_PRIORITY1 7
- #define MY_PRIORITY2 8
-
- void my_entry_point1(void *pt1,void *pt2,void *pt3)
- {
- printk("%s\r\n", "my_entry_point1");
- while(1){
- k_sleep(2000);
- printk("pthread1_run\r\n");
- }
- }
-
- void my_entry_point2(void *pt1,void *pt2,void *pt3)
- {
- printk("%s\r\n", "my_entry_point2");
- while(1){
- k_sleep(2000);
- printk("pthread2_run\r\n");
- }
- }
-
- K_THREAD_DEFINE(my_tid1, MY_STACK_SIZE,my_entry_point1, NULL, NULL, NULL,MY_PRIORITY1, 0, 0);
- K_THREAD_DEFINE(my_tid2, MY_STACK_SIZE,my_entry_point2, NULL, NULL, NULL,MY_PRIORITY2, 0, 0);
-
- /*Shell*/
- static int get_shell_dat(int argc, char *argv[])
- {
- #if 0
- for (int i=0; i < argc; i++)
- printk("Argument %d is %s\r\n", i, argv[i]);
- #endif
-
- #if 0
- //这地方不会相等
- if (argv[1] == "suspend1") {
- k_thread_suspend(my_tid1);
- }
- #endif
-
- //只能判断是否包含
- if (strstr(argv[1],"suspend1")) {
- k_thread_suspend(my_tid1);
- } else if (strstr(argv[1],"resume1")) {
- k_thread_resume(my_tid1);
- } else if(strstr(argv[1],"suspend2")) {
- k_thread_suspend(my_tid2);
- } else if(strstr(argv[1],"resume2")) {
- k_thread_resume(my_tid2);
- } else{
- ;
- }
- }
-
-
- /*Shell*/
- static const struct shell_cmd consumer_commands[] = {
- { "1", get_shell_dat, "consumer" }, /*前缀*/
- };
-
- int main(void)
- {
- /*Shell*/
- SHELL_REGISTER("1", consumer_commands); /*前缀*/
- while(1){
- printf("main_run\r\n");
- k_sleep(2000);
- }
- return 0;
- }
注:
1) 跟获取Shell命令行参数相关的几个地方,看注释/*Shell*/
2) 在Shell中输入参数后,按下回车键,shell子系统才会获取到参数
所以,参数中可能多了回车或者换行符,因此不能直接进行判断,具体看代码里面的注释,关键地方如下:
| ………………………………………………………………………………….. #if 0 //这地方不会相等 if(argv[1] == "suspend1"){ k_thread_suspend(my_tid1); } #endif
//只能判断是否包含 if(strstr(argv[1],"suspend1")){ k_thread_suspend(my_tid1); } ………………………………………………………………………………….. |
3) shell命令行输入的命令
| 1 1 suspend1 //前面两个是前缀,可自由定义,具体对应代码里面的注释/*前缀*/ 1 1 resume1
1 1 suspend2 1 1 resume2 |
4) 串口打印
| ***** BOOTING ZEPHYR OS v1.9.0 - BUILD: Nov 6 2019 15:00:54 ***** main_run my_entry_point1 my_entry_point2
main_run pthread1_run pthread2_run //主线程和两个自定义线程都正常运行和调度
shell> 1 1 suspend1 //从Shell中输入挂起线程1指令 shell> main_run //线程1已被挂起(暂停执行),只有主线程和线程2在跑 pthread2_run
main_run pthread2_run
shell> 1 1 suspend2 //从Shell中输入挂起线程2指令 shell> main_run //线程2也被挂起(暂停执行)了,只剩下主线程在跑 main_run main_run main_run main_run main_run main_run
shell> 1 1 resume1 //从Shell中输入取消挂起线程1指令 shell> pthread1_run //线程1继续执行 main_run
pthread1_run main_run shell> 1 1 resume2 //从Shell中输入线程2恢复执行指令 shell> pthread2_run //线程2继续执行
main_run //主线程和两个自定义线程都正常运行和调度 pthread1_run pthread2_run
main_run pthread1_run pthread2_run |
(1) 线程的挂起和恢复,仅仅是线程的暂停执行和继续执行,并不是完全退出,可以看到,线程恢复执行的时候,并没有执行这行代码printk("%s\r\n", "my_entry_point1");
(2) 而结束一个线程之后,只能再次重新创建。
线程可以使用 k_sleep() 睡眠一段指定的时间。不过,这与挂起不同,睡眠线程在睡眠时间完成后会自动运行,而挂起的话,再次运行需要调用k_thread_resume()。