您的当前位置:首页正文

zephyr-os 线程

2024-11-30 来源:个人技术集锦


一、线程

线程是轻量级的,不支持抢占的。一般用于设备驱动和其他比较重要的任务。线程的调度以优先级为参考,高优先级的线程会先得到执行。被调度的线程会持续执行,直到有阻塞操作才会停止。

1. 相关概念

(1)栈区大小:这是一段内存区域,是线程栈区。可以根据实际情况自定义栈区的大小,单位为字节。

(2)线程入口函数:线程启动时调用的函数(执行的函数)。该函数最多可接收三个参数。

(3)线程调度的优先级:决定内核调度器给该线程分配的CPU时间(系统在某个时刻只能执行一个线程,大多数系统都用的是时间片轮换算法,就是多个进程在分配到的极短时间片轮流使用CPU,可参考“调度”这节内容)。

(4)线程选项:内核支持一系列 线程选项(thread options),允许线程在特殊情况下被特殊对待。

(5)启动延时:在启动线程之前,设置延时启动时间,即允许线程延迟启动。

2. 线程创建方式1[动态创建]

调用线程创建函数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)

函数功能

创建一个线程。

参数

(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:栈区的大小。

(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

2.1 创建线程示例

main.c

/*
 * Copyright (c) 2012-2014 Wind River Systems, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr.h>
#include <misc/printk.h>

#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<kernel.h>

3. 线程创建方式2[静态创建]

可以直接使用宏K_THREAD_DEFINE()静态创建线程。

3.1 创建线程示例

main.c

/*
 * Copyright (c) 2012-2014 Wind River Systems, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
#include <zephyr.h>
#include <misc/printk.h>
#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);
	}
}

3.2 关于线程优先级和延迟启动问题

A.静态创建一个立即启动的线程,需要注意线程的优先级。

如果线程优先级大于main()的优先级,那么线程可以先于mian()执行,如果低于main()的优先级,则后于mian()执行。之后就是调度的事了。

例:上面的代码中定义线程的优先级是5  #define MY_PRIORITY 5,而主线程(main()线程)的优先级是6。主线程的优先级在工程配置文件prj.conf里面有定义,如下:

 对应编译出来的.config文件

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

 B.关于延迟启动问题

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

例:延迟3秒启动线程

 这是串口打印的内容

 所以需要特别注意的一个问题,如果在线程做一些初始化操作,要注意有可能初始化没完成,其他线程就会去使用。

4. 结束一个线程

4.1 线程的正常结束

可以在入口函数里面直接返回(return)跳出while(1),同步结束执行,这种方式称为正常结束。伪代码如下:

void my_entry_point(int unused1, int unused2, int unused3) {
    while (1) {
        ...
        if (<some condition>) {
            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");
}

在入口函数内部设置终止条件,满足条件则直接返回,正常结束线程,之后就只有主线程在运行。串口打印如下:

4.2 异常结束

如果线程触发了一个致命错误,内核将自动终止该线程。

4.3 调用API结束

线程自己或者其他线程调用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 <zephyr.h>
#include <misc/printk.h>

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

串口打印:

5. 线程的选项字

内核支持一系列线程选项(thread options),以允许线程在特殊情况下被特殊对待。这些与线程关联的选项在线程创建时就被指定了。

如果不使用选项字,则该参数填零。如果线程需要选项,可以通过选项名指定。如果需要多个选项,使用符号 | 作为分隔符。(即按位或操作符)。

这些选项字都以宏定义的形式定义在kernel.h中:

5.1 必须线程(essential thread)

选项字为:K_ESSENTIAL。表明线程是不可以被中止的,所以不管线程是正常结束或者是异常中止,内核都认为是产生了一个致命的系统错误。

示例代码:

/*
 * Copyright (c) 2012-2014 Wind River Systems, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
#include <zephyr.h>
#include <misc/printk.h>
#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:

注意:一般情况下,普通创建的线程都不是必须线程。

5.2 线程使用 CPU 的浮点寄存器和 SSE 寄存器

指定线程使用CPU的浮点寄存器:K_FP_REGS

指定线程使用CPU的SSE寄存器:K_SSE_REGS

这两个选项都是跟X86架构相关的选项,可以不用理会。

6. 线程的调度问题

详细的调度相关理论放到另一个文档,目前只需要知道线程是如何依靠优先级进行调度的。内核调度线程的基本依据:(1)优先级  (2)线程休眠(让出CPU使用权)

6.1 线程休眠函数k_sleep()

函数原型

void k_sleep(s32_t duration){

……………………………………………………………….

}

函数功能

休眠当前线程,让出CPU使用权,后面按照优先级进行排队的线程才会得以执行。如果不休眠,则会一直卡在当前线程,其他线程得不到调度。------->线程调度

参数

休眠时间。

返回值

定义处(源文件)

xxxxxx\kernel\sched.c

声明处(头文件)

xxxxxx\include\kernel.h

6.2 示例代码

示例1

主线程优先级:6     自定义线程1优先级:7    自定义线程2优先级:8

main.c

/*
 * Copyright (c) 2012-2014 Wind River Systems, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr.h>
#include <misc/printk.h>

#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 <zephyr.h>
#include <misc/printk.h>

#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使用权。

7. 线程的挂起和恢复

7.1 线程挂起

函数原型

void k_thread_suspend(struct k_thread *thread){

……………………………………………………………………………

}

函数功能

线程被挂起,则线程就会停止执行。可以挂起包括调用线程在内的所有线程(在线程内部调用函数将自己挂起或者将别的线程挂起),对已经挂起的线程再次挂起时不会产生任何效果。

线程一旦被挂起,它将一直不能被调度,除非另一个线程调用 k_thread_resume() 取消挂起(恢复执行)指定的线程。

参数

线程ID

返回值

定义处(源文件)

ATS350B\kernel\thread.c

声明处(头文件)

ATS350B\include\kernel.h

7.2 线程取消挂起(恢复执行)

函数原型

void k_thread_resume(struct k_thread *thread){

…………………………………………………………………………..

}

函数功能

取消挂起(恢复执行)指定的线程。

参数

线程ID

返回值

定义处(源文件)

ATS350B\kernel\thread.c

声明处(头文件)

ATS350B\include\kernel.h

7.3 示例

通过判断获取到的shell命令行的参数来决定来挂起或者取消挂起指定的线程。

#include <zephyr.h>
#include <misc/printk.h>
#include <shell/shell.h> /*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

8. 总结

8.1 线程挂起和结束的区别

(1) 线程的挂起和恢复,仅仅是线程的暂停执行和继续执行,并不是完全退出,可以看到,线程恢复执行的时候,并没有执行这行代码printk("%s\r\n", "my_entry_point1");

(2) 而结束一个线程之后,只能再次重新创建。

8.2 线程挂起和休眠的区别

线程可以使用 k_sleep() 睡眠一段指定的时间。不过,这与挂起不同,睡眠线程在睡眠时间完成后会自动运行,而挂起的话,再次运行需要调用k_thread_resume()。

显示全文