目录
一、线程
1. 相关概念
2. 线程创建方式1[动态创建]
2.1 创建线程示例
3. 线程创建方式2[静态创建]
3.1 创建线程示例
3.2 关于线程优先级和延迟启动问题
4. 结束一个线程
4.1 线程的正常结束
4.2 异常结束
4.3 调用API结束
5. 线程的选项字
5.1 必须线程(essential thread)
5.2 线程使用 CPU 的浮点寄存器和 SSE 寄存器
6. 线程的调度问题
6.1 线程休眠函数k_sleep()
6.2 示例代码
7. 线程的挂起和恢复
7.1 线程挂起
7.2 线程取消挂起(恢复执行)
7.3 示例
8. 总结
8.1 线程挂起和结束的区别
8.2 线程挂起和休眠的区别
线程是轻量级的,不支持抢占的。一般用于设备驱动和其他比较重要的任务。线程的调度以优先级为参考,高优先级的线程会先得到执行。被调度的线程会持续执行,直到有阻塞操作才会停止。
(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()。