FreeRTOS 本身是设计为可重入的 RTOS 内核,但 它的可重入性 依赖于你使用的 API 和上下文环境(任务、ISR、中断嵌套等)。
我们分情况来看:
✅ 可重入
xQueueSend
、xSemaphoreGive
、xTimerStart
等都具有线程安全机制。✅ 可重入(可在多个任务/中断中使用,注意是否需要使用 FromISR 版本)
xQueueSend()
,应使用 xQueueSendFromISR()
。✅ 可重入(前提是使用正确版本的 API)
你自己的函数是否可重入,取决于你写的方式。如果:
✅/❌ 可重入性由你自己控制,需加锁(如 mutex)保护共享资源
int counter = 0;
void Task1(void *param) {
counter++; // 未加保护,两个任务同时修改,数据竞争
}
void Task2(void *param) {
counter++;
}
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
xSemaphoreTake(xMutex, portMAX_DELAY);
counter++;
xSemaphoreGive(xMutex);
项目 | 是否可重入 | 说明 |
---|---|---|
FreeRTOS 内核(调度器等) | ✅ | 内部临界保护 |
FreeRTOS 队列/信号量 API | ✅ | 多任务安全 |
FromISR 系列 API(中断中) | ✅ | 专门设计用于 ISR |
用户自定义回调/共享资源访问 | ❌/✅ | 需加锁保护 |
newlib
是一种 C 标准库实现(比如 printf()
、malloc()
等),它默认不是线程安全(不可重入)的。
但它可以通过用户提供某些钩子函数实现“线程可重入”,典型方式就是实现以下函数:
struct _reent *__getreent(void);
FreeRTOS 不是 POSIX 系统,没有线程上下文切换钩子,所以默认 不支持 newlib 的线程安全特性,你需要手动配置。
这是因为:
STM32CubeMX 默认生成的裸机代码不涉及多线程。
所以没有默认启用 newlib 的重入支持,因为:
printf()
只会在一个上下文中运行malloc()
、errno
等全局变量也只在一个任务里用在你使用 FreeRTOS + newlib 的场景下,如果要使用 printf()
、malloc()
、errno
等函数:
STM32F4 默认不会自动帮你启用,需要你手动开启。
打开 CubeMX
Enable FreeRTOS
在配置中找到 USE_NEWLIB_REENTRANT
(有些版本叫 USE_NEWLIB_REENTRANT
宏)
勾选后会在 FreeRTOSConfig.h
中添加:
#define configUSE_NEWLIB_REENTRANT 1
在 FreeRTOSConfig.h
中手动添加:
#define configUSE_NEWLIB_REENTRANT 1
还需要在每个任务创建前,为其分配 _reent
结构:
#include
static struct _reent my_reent;
myTaskHandle = xTaskCreateStatic(...);
myTaskHandle->pxNewlibReent = &my_reent;
或者使用 FreeRTOS 的 vTaskSetThreadLocalStoragePointer()
结合 newlib。
printf()
输出错乱(两个任务打印内容交错)malloc()
返回错误,甚至 heap 崩溃errno
错误码混乱问题 | 解答 |
---|---|
STM32F4 为何没有 newlib 可重入设置? | 因为默认是裸机开发,不涉及多任务,不需要。 |
什么时候需要? | 使用 FreeRTOS 并调用 printf() /malloc() 等时。 |
如何启用? | 设置 configUSE_NEWLIB_REENTRANT = 1 并确保每个任务有 _reent 结构体。 |
在 FreeRTOS 中启用 newlib
的可重入支持(configUSE_NEWLIB_REENTRANT = 1
)时,确实需要为每个任务提供它自己的 _reent
结构体,否则 newlib
仍然会使用全局 _impure_ptr
,就失去了线程安全的意义。
_reent
?在 FreeRTOS 里:
newlib
通过 __getreent()
获取线程局部的 _reent
结构体。configUSE_NEWLIB_REENTRANT = 1
,FreeRTOS 会在切换任务时尝试配合 newlib 使用线程局部 _reent
。_reent
空间的分配!_reent
结构体。_reent
?如果使用 xTaskCreate()
,且 FreeRTOS 端口支持 configUSE_NEWLIB_REENTRANT
,它会自动分配 _reent
(某些版本会在任务 TCB 中加字段)。
这种方式 依赖你的移植层支持。如果不放心,推荐你手动指定
#include
_reent
结构体你可以为每个任务分配一个静态或动态的 _reent
:
static struct _reent myTaskReent;
使用 vTaskSetThreadLocalStoragePointer()
绑定 _reent
:
#define TLS_INDEX_REENT 0 // 任务局部存储的索引位(0~4)
xTaskCreate(myTaskFunction, "Task", 512, NULL, 1, &xHandle);
vTaskSetThreadLocalStoragePointer(xHandle, TLS_INDEX_REENT, (void *)&myTaskReent);
__getreent()
你需要提供一个你自己的 __getreent()
函数,这样 newlib
在访问 _impure_ptr
时就能访问到每个任务自己的:
#include
#include "FreeRTOS.h"
#include "task.h"
#define TLS_INDEX_REENT 0
struct _reent *__getreent(void) {
if (xTaskGetCurrentTaskHandle() != NULL) {
void *ptr = pvTaskGetThreadLocalStoragePointer(NULL, TLS_INDEX_REENT);
if (ptr != NULL) {
return (struct _reent *)ptr;
}
}
// fallback: single-threaded or early boot
return _impure_ptr;
}
printf()
、malloc()
、strtok()
、errno
等都依赖 _reent
。_reent
,即使你开启了 configUSE_NEWLIB_REENTRANT
,也还是会有线程不安全的问题。_reent
(但这很局限)。问题 | 是否需要自己提供 |
---|---|
开启 configUSE_NEWLIB_REENTRANT 后 |
✅ 是的,你必须为每个任务提供独立 _reent 结构体,并在 __getreent() 中返回它 |
使用裸机 / 单任务环境 | ❌ 不需要,使用默认 _impure_ptr 即可 |
下面是一个完整的 FreeRTOS + newlib 可重入 printf 示例工程代码框架,适用于 STM32F4 + FreeRTOS 环境,确保 printf()
在线程中使用时是线程安全的(可重入)。
newlib
的 _reent
结构体。__getreent()
,让 newlib
知道每个任务使用自己的 _reent
。printf()
、errno
等函数。main.c
reent_hook.c
FreeRTOSConfig.h
FreeRTOSConfig.h
设置确保加上这个宏:
#define configUSE_NEWLIB_REENTRANT 1
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 1
reent_hook.c
:重定义 __getreent
#include "FreeRTOS.h"
#include "task.h"
#include
#define TLS_INDEX_REENT 0
// 自定义的 __getreent 实现
struct _reent *__getreent(void) {
if (xTaskGetCurrentTaskHandle() != NULL) {
void *ptr = pvTaskGetThreadLocalStoragePointer(NULL, TLS_INDEX_REENT);
if (ptr != NULL) {
return (struct _reent *)ptr;
}
}
// fallback to global
return _impure_ptr;
}
main.c
示例#include "FreeRTOS.h"
#include "task.h"
#include
#include
#define TLS_INDEX_REENT 0
static void Task1(void *params);
static void Task2(void *params);
int main(void)
{
// 初始化硬件(略)
// 创建任务
xTaskCreate(Task1, "Task1", 512, NULL, 1, NULL);
xTaskCreate(Task2, "Task2", 512, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
while (1);
}
static void Task1(void *params)
{
// 每个任务一个独立的 _reent 结构体
static struct _reent reent1;
vTaskSetThreadLocalStoragePointer(NULL, TLS_INDEX_REENT, (void *)&reent1);
while (1) {
printf("Hello from Task 1\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
static void Task2(void *params)
{
static struct _reent reent2;
vTaskSetThreadLocalStoragePointer(NULL, TLS_INDEX_REENT, (void *)&reent2);
while (1) {
printf("Task 2 is running...\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
TLS_INDEX_REENT
索引要在 FreeRTOS 支持的范围内(一般是 0~4)。_reent
。newlib
是编译为可重入支持版本(大多数 STM32 工具链默认是支持的)。两个任务都可以调用 printf()
,并且不会交叉输出或破坏内部缓冲区。你也可以放心使用 errno
、strtok()
、gmtime()
等依赖 _reent
的函数。
在 FreeRTOS 中,TLS 插槽(Thread Local Storage Slot) 就是每个任务(线程)专属的一块存储空间,用于保存任务私有的数据——其他任务访问不到这个数据。每个“插槽”可以看作是一个 void *
指针,指向你想关联的数据(如 reent
结构、日志缓冲区、状态结构等)。
假设你有多个任务,每个任务都运行 printf()
,但你又不想 errno
、malloc
之类的数据全局共享。你可以这样做:
struct _reent
结构体+----------------------------+
| Task A |
| TLS Slot[0] -> &reent_A |
+----------------------------+
+----------------------------+
| Task B |
| TLS Slot[0] -> &reent_B |
+----------------------------+
+----------------------------+
| Task C |
| TLS Slot[0] -> NULL |
+----------------------------+
函数 | 说明 |
---|---|
pvTaskGetThreadLocalStoragePointer(TaskHandle_t xTask, BaseType_t xIndex) |
获取指定任务的插槽数据指针 |
vTaskSetThreadLocalStoragePointer(TaskHandle_t xTask, BaseType_t xIndex, void *pvValue) |
设置指定任务的插槽数据 |
通过配置:
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 1 // 你需要几个插槽就写几
void * tlsPointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS]
放在每个 TCB
(任务控制块)中。#define TLS_IDX_REENT 0
void *get_reent_for_current_task(void)
{
return pvTaskGetThreadLocalStoragePointer(NULL, TLS_IDX_REENT);
}
void setup_task_tls()
{
struct _reent *r = malloc(sizeof(struct _reent));
_REENT_INIT_PTR(r); // 初始化 newlib 所需结构
vTaskSetThreadLocalStoragePointer(NULL, TLS_IDX_REENT, r);
}
newlib
中为每个任务挂自己的 _reent
,解决 errno
、malloc
等重入问题“在任务中给 malloc/printf/errno 加锁就安全吗?有没有死锁风险?”——是的,有死锁风险,而且这种风险在嵌入式使用 newlib 时非常隐蔽。
这是因为:
newlib
自己已经内建了对 malloc、printf 等的内部锁机制(如果使用了 configUSE_NEWLIB_REENTRANT = 1
或默认启用了 __malloc_lock()
和 __sfp_lock()
等钩子)。
xSemaphoreTake(global_lock);
printf("Hello");
xSemaphoreGive(global_lock);
printf()
内部会干什么?printf() ->
vfprintf() ->
_fwrite_r() ->
malloc() ->
可能触发内部的 __malloc_lock()
如果:
printf()
前已经获取了全局锁 global_lock
malloc()
或 vfprintf()
里面又尝试调用内部的锁(也可能是你同一个锁)你的任务:
获取 global_lock →
调用 printf →
printf 内部调用 malloc →
malloc 尝试加锁(但 global_lock 已被你占用) → ❌ 死锁
newlib 提供了以下可钩住的锁函数,你可以重定义它们:
void __malloc_lock(struct _reent *r);
void __malloc_unlock(struct _reent *r);
void __sfp_lock_acquire(void);
void __sfp_lock_release(void);
不要自己在外部给 printf/malloc 加锁
使用 newlib 的线程安全机制:
configUSE_NEWLIB_REENTRANT
_reent
__getreent()
返回线程局部 _reent
这样,所有锁都由 newlib 自己在内部控制,FreeRTOS 会自动协作,不会死锁。
加锁方式 | 死锁风险 | 推荐度 |
---|---|---|
你手动写锁包 malloc/printf | ✅ 高(嵌套冲突) | ❌ 不推荐 |
用 _reent 实现线程安全 |
❌ 无 | ✅ 强烈推荐 |
newlib
没启用 __malloc_lock()
/ __sfp_lock()
(可重编译 newlib)printf/malloc/errno
的地方严格遵守一致的加锁规范否则,一次不小心,就可能踩到 隐性死锁地雷 。
✅ 你说得完全对!在使用 newlib + FreeRTOS 的 标准配置 时,确实 不需要手动声明或维护 struct _reent xNewLib_reent;
—— 因为:
configUSE_NEWLIB_REENTRANT = 1
时 FreeRTOS 已经自动处理:当你在 FreeRTOSConfig.h
中设置:
#define configUSE_NEWLIB_REENTRANT 1
FreeRTOS 会:
struct _reent
成员;__getreent()
所返回的 _impure_ptr
;__getreent()
是由 newlib 自动实现的(如果你使用 --specs=nano.specs
并启用了 -u __getreent
)只要你不去定义它,链接器自动从 libc_nano.a
中拉入 getreent.o
:
-u __getreent -u _impure_ptr
配合 configUSE_NEWLIB_REENTRANT=1
,newlib 会根据当前线程环境访问正确的 _impure_ptr
,FreeRTOS 会在任务切换时设置它。
struct _reent xNewLib_reent;
也不应该实现自己的 __getreent()
—— 否则你会引起符号冲突。
在 FreeRTOSConfig.h
中:
#define configUSE_NEWLIB_REENTRANT 1
在链接器命令中保留:
--specs=nano.specs -u __getreent -u _impure_ptr
不实现自己的 __getreent()
,也不手动分配 _reent
结构,一切交给 FreeRTOS + newlib。