这一篇我们来看一下tx_thread_create.c这个文件,由于是第一篇深入源代码,会多写一些ThreadX所有源代码都通用的内容。
/**************************************************************************/
/* */
/* Copyright (c) Microsoft Corporation. All rights reserved. */
/* */
/* This software is licensed under the Microsoft Software License */
/* Terms for Microsoft Azure RTOS. Full text of the license can be */
/* found in the LICENSE file at https://aka.ms/AzureRTOS_EULA */
/* and in the root directory of this software. */
/* */
/**************************************************************************/
用VSCode打开整个ThreadX文件夹并选中tx_thread_create.c这个文件。首先看到的版权申明,已经被微软收购,打上了微软的名称。说真的,在MCU领域能看到微软的LOGO还是比较意外的。ThreadX采用了Microsoft Azure RTOS专门的License,具体的还没怎么了解过,后续有时间看一下。
/**************************************************************************/
/**************************************************************************/
/** */
/** ThreadX Component */
/** */
/** Thread */
/** */
/**************************************************************************/
/**************************************************************************/
第二段是阐述这个API属于哪个模块。
#define TX_SOURCE_CODE
/* Include necessary system files. */
#include "tx_api.h"
#include "tx_trace.h"
#include "tx_thread.h"
#include "tx_initialize.h"
第三段是一个宏定义TX_SOURCE_CODE和头文件引用。这个TX_SOURCE_CODE只有define,但并没有define的内容,一般这样的用法是代表一个条件编译的开关。用查询功能能发现它在好多API文件中都被define,但只有两个地方被使用,那么我们就先跳过去看一下,
/* Define the system API mappings based on the error checking
selected by the user. Note: this section is only applicable to
application source code, hence the conditional that turns off this
stuff when the include file is processed by the ThreadX source. */
#ifndef TX_SOURCE_CODE
从描述来看,这个宏使用来界定是threadX源码调用了相关API还是用户调用相关API。threadX源码的.c文件中定义了TX_SOURCE_CODE以后,本.c文件之后调用的API就能判断出是threadX的源码调用的。在后面的代码中应该会有对应的体现。
一共引用了4个头文件,从字面上大致猜一下:
"tx_api.h": ThreadX对外暴露API;
"tx_trace.h":用来实现trace功能;
"tx_thread.h":线程相关;
"tx_initialize.h":初始化相关
/**************************************************************************/
/* */
/* FUNCTION RELEASE */
/* */
/* _tx_thread_create PORTABLE C */
/* 6.0 */
/* AUTHOR */
/* */
/* William E. Lamie, Microsoft Corporation */
/* */
/* DESCRIPTION */
/* */
/* This function creates a thread and places it on the list of created */
/* threads. */
/* */
/* INPUT */
/* */
/* thread_ptr Thread control block pointer */
/* name Pointer to thread name string */
/* entry_function Entry function of the thread */
/* entry_input 32-bit input value to thread */
/* stack_start Pointer to start of stack */
/* stack_size Stack size in bytes */
/* priority Priority of thread */
/* (default 0-31) */
/* preempt_threshold Preemption threshold */
/* time_slice Thread time-slice value */
/* auto_start Automatic start selection */
/* */
/* OUTPUT */
/* */
/* return status Thread create return status */
/* */
/* CALLS */
/* */
/* _tx_thread_stack_build Build initial thread stack */
/* _tx_thread_system_resume Resume automatic start thread */
/* _tx_thread_system_ni_resume Noninterruptable resume thread*/
/* */
/* CALLED BY */
/* */
/* Application Code */
/* _tx_timer_initialize Create system timer thread */
/* */
/* RELEASE HISTORY */
/* */
/* DATE NAME DESCRIPTION */
/* */
/* 05-19-2020 William E. Lamie Initial Version 6.0 */
/* */
/**************************************************************************/
UINT _tx_thread_create(TX_THREAD *thread_ptr, CHAR *name_ptr, VOID (*entry_function)(ULONG id), ULONG entry_input,
VOID *stack_start, ULONG stack_size, UINT priority, UINT preempt_threshold,
ULONG time_slice, UINT auto_start)
接下来是API的函数原型及其注释,可见这个注释是相当的详细,注释主要包括:函数名,作者,版本,描述,入参,返回值,调用了那些其他api,被哪些api调用。这些都是必须的,大致体现了api的方方面面,在日常工作中的函数定义可以参照相关的格式。
这个API函数带有10个参数:
TX_THREAD *thread_ptr:可能是线程控制块的指针,它代表了线程,操作线程都要基于线程控制块;
CHAR *name_ptr:线程的名称;
VOID (*entry_function)(ULONG id):线程执行的函数;
ULONG entry_input:32位的输入,可能是entry_function的参数;
VOID *stack_start:线程堆栈起始地址;
ULONG stack_size:堆栈容量;ThreadX每个thread有自己独立的堆栈,用来保存上下文切换和局部堆栈变量;
UINT priority:优先级,默认可以从0-31;(既然是0-31,为什么类型需要采用了UINT类型,而不是UCHAR呢?)
UINT preempt_threshold:抢占的阈值;(目前不太了解这个参数的作用)
ULONG time_slice:线程时间片大小;
UINT auto_start:自动启动选择;(不太理解类型需要采用UINT?一个字节表示布尔类型)
目前有几个参数有相当的疑惑,这需要后面阅读代码来解惑了。从函数名来看,这个函数应该不是用户直接调用的函数,因为名称是以“_”开头的,一般这样的函数都是内部调用的函数。我们利用编辑器的Find All Reference来查看一下有哪些其他函数调用了这个函数。一共有两个地方调用了这个API:
第一个是在tx_timer_initialize.c文件中,从名字看定时器初始化的API,我们先不看。重点看一下第二个,文件名称是txe_thread_create.c,txe开头的,这是什么鬼?我们点进去看看:
...
/* Determine if everything is okay. */
if (status == TX_SUCCESS)
{
/* Call actual thread create function. */
status = _tx_thread_create(thread_ptr, name_ptr, entry_function, entry_input,
stack_start, stack_size, priority, preempt_threshold,
time_slice, auto_start); //(1)
}
/* Return completion status. */
return(status);
}
可以看到,他在API的结尾(1)的地方调用了_tx_thread_create,在往上看看,这个函数上面将_tx_thread_create需要调用的参数一一进行检查,只有检查通过了才能调用_tx_thread_create。也就是说,_txe_thread_create是在参数检查后调用_tx_thread_create的一个包装函数,这也是ThreadX安全性的一个体现,我们来看看他都做了哪些检查。
UINT _txe_thread_create(TX_THREAD *thread_ptr, CHAR *name_ptr,
VOID (*entry_function)(ULONG id), ULONG entry_input,
VOID *stack_start, ULONG stack_size,
UINT priority, UINT preempt_threshold,
ULONG time_slice, UINT auto_start, UINT thread_control_block_size) //(2)
{
TX_INTERRUPT_SAVE_AREA //(3)
UINT status;
UINT break_flag;
ULONG i;
TX_THREAD *next_thread;
VOID *stack_end;
UCHAR *work_ptr;
#ifndef TX_TIMER_PROCESS_IN_ISR
TX_THREAD *current_thread;
#endif
/* Default status to success. */
status = TX_SUCCESS; //(4)
/* Check for an invalid thread pointer. */
if (thread_ptr == TX_NULL) //(5)
{
/* Thread pointer is invalid, return appropriate error code. */
status = TX_THREAD_ERROR;
}
...
/* Determine if everything is okay. */
if (status == TX_SUCCESS)
{
/* Call actual thread create function. */
status = _tx_thread_create(thread_ptr, name_ptr, entry_function, entry_input,
stack_start, stack_size, priority, preempt_threshold,
time_slice, auto_start);
}
/* Return completion status. */
return(status); //(6)
}
(2) _txe_thread_create和_tx_thread_create的入参除了最后一个thread_control_block_size,其他都是一模一样的,这些入参被检查后会被原封不动的传入_tx_thread_create。
(3) 这里有一个TX_INTERRUPT_SAVE_AREA,这是一个宏
#define TX_INTERRUPT_SAVE_AREA unsigned int interrupt_save;
它是用来定义临界区保护需要的用来保存状态寄存器的一个临时变量。
(4) 这里先将status初始化为TX_SUCCESS,这个变量会保存检查的结果,只有通过了检查才能进一步调用_tx_thread_create
(5) 对thread_ptr进行空检查,如果为空就将status变为TX_THREAD_ERROR。
(6) 我们先略去其他参数检查,先看结尾部分,这里会检查status是否通过检查,如果通过检查的话才能进一步调用_tx_thread_create,取得_tx_thread_create的执行结果后返回status;如果不通过检查,直接返回status。
这里就有一个问题了,为什么不在(4)这里检查到参数错误后直接return TX_THREAD_ERROR呢?
代码就变成这样,代码可以变得更精简。
...
if (thread_ptr == TX_NULL)
{
/* Thread pointer is invalid, return appropriate error code. */
return TX_THREAD_ERROR;
}
...
/* Determine if everything is okay. */
if (status == TX_SUCCESS)
{
/* Call actual thread create function. */
return _tx_thread_create(thread_ptr, name_ptr, entry_function, entry_input,
stack_start, stack_size, priority, preempt_threshold,
time_slice, auto_start);
}
其实,这就涉及到一个安全代码静态检查规则的一个问题了。在安全领域,有一套针对C语言的控制风险的代码静态检查规则,在汽车行业,检查规则有一个标准叫做MISRA C。其中有一条就是所有的代码段都必须只有一个出口。如果按照后面一种写法,这个函数就会有很多个return,也就是很多个出口,那么这个函数是无法通过安全认证的。因此,ThreadX为了满足安全规范,采用了这样的写法,这也是标准的做法,但这样写会导致代码if嵌套很多,不便于阅读,但在安全领域,一切都要让步于安全。
我们接着看参数检查,接下来是thread_control_block_size的检查,目的是检查传进来的thread_ptr是否真正是一个TX_THREAD指针。
/* Now check for invalid thread control block size. */
else if (thread_control_block_size != (sizeof(TX_THREAD)))
{
/* Thread pointer is invalid, return appropriate error code. */
status = TX_THREAD_ERROR;
}
else
{
/* Disable interrupts. */
TX_DISABLE //(7)
/* Increment the preempt disable flag. */
_tx_thread_preempt_disable++; //(8)
/* Restore interrupts. */
TX_RESTORE //(9)
(7) 进入临界区保护,是个宏
#define TX_DISABLE interrupt_save = __disable_interrupts();
它会首先将CPU状态寄存器保存在(3)定义的局部变量中,然后修改状态寄存器中的中断使能位来关闭中断。
(8)将_tx_thread_preempt_disable全局变量加1,这个全局变量的定义
/* Define the global preempt disable variable. If this is non-zero, preemption is
disabled. It is used internally by ThreadX to prevent preemption of a thread in
the middle of a service that is resuming or suspending another thread. */
volatile UINT _tx_thread_preempt_disable;
这个变量为0才能使能调度器的抢占,相当于调度器锁。它也是可嵌套的,每锁一次调度器都会加1,每解锁一次减1,减到0才能解锁调度器。volatile修饰符表明这个变量会被其他线程或者中断修改,不能优化位寄存器变量。
(9) 退出临界区,是个宏
#define TX_RESTORE __restore_interrupts(interrupt_save);
它会将保存在局部变量中的CPU状态恢复到CPU状态寄存器。与(8)配合完成了临界区保护,这个临界区保护是可嵌套的,只有所有的进入临界区保护都退出后,才能很正的退出临界区保护。
/* Next see if it is already in the created list. */
break_flag = TX_FALSE;
next_thread = _tx_thread_created_ptr; //(10)
work_ptr = TX_VOID_TO_UCHAR_POINTER_CONVERT(stack_start);
work_ptr = TX_UCHAR_POINTER_ADD(work_ptr, (stack_size - ((ULONG) 1)));
stack_end = TX_UCHAR_TO_VOID_POINTER_CONVERT(work_ptr);
for (i = ((ULONG) 0); i < _tx_thread_created_count; i++) //(11)
{
/* Determine if this thread matches the thread in the list. */
if (thread_ptr == next_thread)
{
/* Set the break flag. */
break_flag = TX_TRUE; //(12)
}
/* Determine if we need to break the loop. */
if (break_flag == TX_TRUE)
{
/* Yes, break out of the loop. */
break; //(13)
}
/* Check the stack pointer to see if it overlaps with this thread's stack. */
if (stack_start >= next_thread -> tx_thread_stack_start)
{
if (stack_start < next_thread -> tx_thread_stack_end)
{
/* This stack overlaps with an existing thread, clear the stack pointer to
force a stack error below. */
stack_start = TX_NULL;
/* Set the break flag. */
break_flag = TX_TRUE; //(14)
}
}
/* Check the end of the stack to see if it is inside this thread's stack area as well. */
if (stack_end >= next_thread -> tx_thread_stack_start)
{
if (stack_end < next_thread -> tx_thread_stack_end)
{
/* This stack overlaps with an existing thread, clear the stack pointer to
force a stack error below. */
stack_start = TX_NULL;
/* Set the break flag. */
break_flag = TX_TRUE; //(15)
}
}
/* Move to the next thread. */
next_thread = next_thread -> tx_thread_created_next; //(16)
}
下一步检查传入的thread_ptr是否已经在已创建列表中了,从(10)可以知道已被创建的线程列表保存在_tx_thread_created_ptr中,它指向了第一个被创建的线程控制块,线程控制块本身是一个链表,它包含有下一个控制块地址的成员变量tx_thread_created_next (16)。从(11)可以知道,系统中看出系统中被创建线程的总数保存在_tx_thread_created_count中,通过这个这个for循环来遍历所有被创建的线程信息。我们看一下哪些线程信息被检查了:
1. 检查将被创建的线程控制块是否与已被创建的线程控制块相同
相同的话代表线程被重复创建了,需要终止这次线程创建。
2. 检查传入的线程堆栈是否与已被创建的线程重叠(起始地址和结束地址检查)
因为线程的局部变量,上下文信息都被保存在线程堆栈中,因此线程堆栈不能有任何的重叠。
这段程序还有一个有意思的点,在for循环中,提前退出循环使用的是break,但只有(13)有一个break,而(12)、(14)和(15)三个退出点反而是将break_flag置成TX_TRUE,等下一次for循环执行到(13)break退出for循环。为什么要这么设计呢?还是和之前return是一样的,for循环也是用{}包裹起来的,也是一个代码段,只要是代码段,只能有一个出口。因此,只允许有一个break存在。ThreadX的写法也是标准的写法,后面应该会大量看到这样的写法。
/* Disable interrupts. */
TX_DISABLE
/* Decrement the preempt disable flag. */
_tx_thread_preempt_disable--;
/* Restore interrupts. */
TX_RESTORE
重新开启调度器,和之前关闭调度器一样,需要进入临界区。这里,我们可以思考一下,为什么在检查thread_ptr时需要关闭调度器呢?我想是因为在检查时,会使用到一些调度器的全局变量,比如_tx_thread_created_count。如果调度器在工作的时候去遍历所有的线程,那么有可能在遍历过程中,有任务被删除或被创建。有任务被创建的话,有可能导致任务被重复创建;任务被删除的话,极有可能访问到被删除的任务,造成整个系统的崩溃。因此,检查thread_ptr必须关闭调度器。
_tx_thread_system_preempt_check();
/* DESCRIPTION */
/* */
/* This function checks for preemption that could have occurred as a */
/* result scheduling activities occurring while the preempt disable */
/* flag was set. */
这个函数用来检查有没有需要调度的情况存在,因为刚才有一段时间关闭了调度器,在这段时间有可能发生了需要出发调度的时间,需要在开启调度器后第一时间检查一下,有需要则调度,无需要则继续执行。这种设计体现了RTOS系统的实时特性,在保证安全的情况下,最快速度响应事件。
/* At this point, check to see if there is a duplicate thread. */
if (thread_ptr == next_thread) //(17)
{
/* Thread is already created, return appropriate error code. */
status = TX_THREAD_ERROR;
}
/* Check for invalid starting address of stack. */
else if (stack_start == TX_NULL) //(18)
{
/* Invalid stack or entry point, return appropriate error code. */
status = TX_PTR_ERROR;
}
/* Check for invalid thread entry point. */
else if (entry_function == TX_NULL) //(19)
{
/* Invalid stack or entry point, return appropriate error code. */
status = TX_PTR_ERROR;
}
/* Check the stack size. */
else if (stack_size < ((ULONG) TX_MINIMUM_STACK)) //(20)
{
/* Stack is not big enough, return appropriate error code. */
status = TX_SIZE_ERROR;
}
/* Check the priority specified. */
else if (priority >= ((UINT) TX_MAX_PRIORITIES)) //(21)
{
/* Invalid priority selected, return appropriate error code. */
status = TX_PRIORITY_ERROR;
}
/* Check preemption threshold. */
else if (preempt_threshold > priority) //(22)
{
/* Invalid preempt threshold, return appropriate error code. */
status = TX_THRESH_ERROR;
}
/* Check the start selection. */
else if (auto_start > TX_AUTO_START) //(23)
{
/* Invalid auto start selection, return appropriate error code. */
status = TX_START_ERROR;
}
(17)看注释是检查有没有被复制的线程,不太明白,需要了解更多的代码后才能了解;
(18)检查线程堆栈起始地址是否为0;
(19)检查线程执行函数体是否为空;
(20)检查线程堆栈大小是否小于配置的最小堆栈容量;
(21)检查线程优先级是否大于等于最高优先级,ThreadX的优先级看来是数字越高,任务优先级越高;
(22)检查抢占阈值是否大于优先级,对于抢占阈值也不太明白,需要后续才能了解;
(23)检查auto_start是否大于TX_AUTO_START,TX_AUTO_START是一个宏定义为1,还有一个是TX_DONT_START定义为0;
else
{
#ifndef TX_TIMER_PROCESS_IN_ISR //(24)
/* Pickup thread pointer. */
TX_THREAD_GET_CURRENT(current_thread)
/* Check for invalid caller of this function. First check for a calling thread. */
if (current_thread == &_tx_timer_thread)
{
/* Invalid caller of this function, return appropriate error code. */
status = TX_CALLER_ERROR;
}
#endif
(24)这边有一个宏,代表软件定时器如果不是在中断中处理,需要执行这段代码。检查了当前线程是否是软件定时器线程,如果是的话,需要报错。言下之意就是,软件定时器线程中不允许调用创建线程的API。
/* Check for interrupt call. */
if (TX_THREAD_GET_SYSTEM_STATE() != ((ULONG) 0)) //(25)
{
/* Now, make sure the call is from an interrupt and not initialization. */
if (TX_THREAD_GET_SYSTEM_STATE() < TX_INITIALIZE_IN_PROGRESS)
{
/* Invalid caller of this function, return appropriate error code. */
status = TX_CALLER_ERROR;
}
}
/* Define the current state variable. When this value is 0, a thread
is executing or the system is idle. Other values indicate that
interrupt or initialization processing is active. This variable is
initialized to TX_INITIALIZE_IN_PROGRESS to indicate initialization is
active. */
THREAD_DECLARE volatile ULONG _tx_thread_system_state;
(25)系统状态为0代表空闲或者有线程在被执行(空闲也是一种线程),否则的话,就是在中断中。
如果是在中断中,检查系统是否还在初始化阶段,是的话,报错,也就是不允许在系统还未初始化完成的情况下,在中断中调用创建线程的API。
OK,至此,参数检查全部完成,今天先到这里吧,这篇有点长了,下次继续。