你可能习惯于在 Linux 或 Windows 系统上运行存储在文件中的程序。如果你转而在资源有限的 MCU 上使用 NuttX 任务,你会遇到一些行为上的差异。本文将概述其中的一些差异。
NuttX 构建类型
NuttX 可以通过几种不同的方式构建:
-
内核构建: 通过 `--kernel-build` 选择内核构建方案后,该方案
CONFIG_BUILD_KERNEL使用 MCU 的内存管理单元 (MMU) 来实现与 Linux 进程非常相似的进程。这里没有什么值得讨论的地方;NuttX 的行为与 Linux 非常相似。 -
扁平化构建 大多数资源受限的微控制器 (MCU) 没有内存管理单元 (MMU),代码以 blob 的形式构建,运行在片上闪存中一个未受保护的扁平地址空间内。这种构建模式通过 `--build-mode` 命令选择
CONFIG_BUILD_FLAT,也是迄今为止人们构建 NuttX 最常用的方式。本 Wiki 页面正是针对这种情况而编写的。 -
受保护构建另一种构建选项是受保护构建。它本质上与扁平构建相同,但使用 MCU 的内存保护单元 (MPU) 将不受保护的用户地址范围与受保护的系统地址范围隔离。本 Wiki 页面上的注释也适用于这种情况。
全局变量初始化
Linux 行为
如果你习惯于编写 Linux 程序,那么你会注意到,全局变量只会在系统启动时初始化一次。例如,考虑以下这个小程序:
cpp
bool test = true;
int main(int argc, char **argv)
{
printf("test: %i\n", test);
test = false;
printf("test: %i\n", test);
return 0;
}
如果在 Linux 系统下编译并运行此程序,你将始终看到以下输出:
bash
test: 1
test: 0
在这种情况下,每次将文件加载到内存中并运行它时,全局变量都会重新初始化。
NuttX 扁平化构建行为
但是,如果你将此程序构建到片上闪存中,并将其作为任务启动(例如通过 task_start()),你将在第一次运行该程序时看到以下内容:
bash
test: 1
test: 0
但在那之后,你总会看到:
bash
test: 0
test: 0
测试变量在启动时初始化为真(1),但在程序每次运行时重置为假(0)。
如果希望程序在构建到通用 FLASH 闪存块时也能保持相同的行为,则需要修改代码,以便在每次程序运行时显式重置全局变量,如下所示:
cpp
bool test;
int main(int argc, char **argv)
{
test = true;
printf("test: %i\n", test);
test = false;
printf("test: %i\n", test);
return 0;
}
NuttX 可加载程序
如果像 Linux 那样将程序从文件加载到 RAM 中并执行,那么 NuttX 的行为也会与 Linux 类似。这是因为扁平化构建的 NuttX 的工作方式相同:当你执行文件中的 NuttX ELF 或 NxFLAT 模块时,该文件会被复制到 RAM 中,并在程序运行前初始化全局变量。
但内置于闪存中的代码工作方式不同。它只有一组全局变量:即构成整体闪存映像的整个二进制数据块的所有全局变量。它们在上电复位时全部初始化一次。
这使得将 Linux 应用程序移植到 FLASH 闪存中变得更加复杂。main() 每次启动任务时,都必须手动初始化每个全局变量。
全局变量和多任务副本
在扁平化构建环境中,应尽可能避免使用全局变量,因为这种用法会带来另一个限制:任何时候都只能运行一个程序副本。这是因为全局变量在每个实例之间是共享的(这与从文件中运行程序不同,在文件中,每个全局变量都有一个私有副本)。
支持闪存程序多个副本的一种方法是将所有全局变量移到一个结构体中。如果全局变量所需的内存量很小,那么每个副本 main()只需在栈上分配该结构体的副本即可。在上面的简单示例中,这可能如下:
cpp
struct my_globals_s
{
bool test;
};
int main(int argc, char **argv)
{
struct my_globals_s my_globals = { true };
printf("test: %i\n", my_globals.test);
my_globals.test = false;
printf("test: %i\n", my_globals.test);
return EXIT_SUCCESS;
}
然后,需要将指向包含已分配全局变量的结构体的指针作为参数传递给每个需要访问这些全局变量的内部函数。因此,你需要像这样修改内部函数:
cpp
static void print_value(void)
{
printf("test: %i\n", test);
}
到:
cpp
static void print_value(FAR struct my_globals_s *globals)
{
printf("test: %i\n", globals->test);
}
然后,每次调用该函数时,都传递对已分配的全局数据结构的引用,如下所示:
cpp
print_value(&my_globals);
如果全局变量结构体很大,那么在栈上分配实例可能不是个好主意。在这种情况下,使用 `.` 分配全局变量结构体可能更好malloc()。但别忘了free() 在退出前释放已分配的变量结构体!(参见下文的内存清理讨论)。
cpp
struct my_globals_s
{
bool test;
};
int main(int argc, char **argv)
{
FAR struct my_globals_s *my_globals;
my_globals = (FAR struct my_globals_s *)malloc(sizeof(struct my_globals_s));
if (my_globals = NULL)
{
fprintf(stderr, "ERROR: Failed to allocate state structure\n");
return EXIT_FAILURE;
}
my_globals=>test = true;
printf("test: %i\n", my_globals->test);
my_globals=>test = false;
printf("test: %i\n", my_globals->test);
free(my_globals);
return EXIT_SUCCESS;
}
内存清理
Linux进程退出
另一个与此无关的因素使得将 Linux 程序移植到 FLASH 闪存中成为可能,那就是内存清理。当一个 Linux 进程退出时,它的整个地址环境(包括所有已分配的内存)都会被销毁。如果这个小程序以 Linux 进程的形式实现,就不会发生内存泄漏:
cpp
int main(int argc, char **argv)
{
char *buffer = malloc(1024);
... do stuff with buffer ...
return 0;
}
如果将同一个程序移植到 FLASH 存储介质上,就会出现内存泄漏,因为任务退出时不会自动清理已分配的内存。相反,你必须显式地通过释放内存来清理所有已分配的内存:
cpp
int main(int argc, char **argv)
{
char *buffer = malloc(1024);
... do stuff with buffer ...
free(buffer);
return 0;
}
Linux进程退出时进行的内存清理是进程终止时进程地址环境被销毁的结果。每个进程都有自己的堆;当进程地址环境被销毁时,该进程堆会被归还给操作系统页面分配器。因此,内存清理基本上是免费的。
NuttX 任务退出
但是,当你在单体芯片上的闪存块中运行任务时,你将与其他所有任务共享同一个堆。没有神奇的清理机制可以找到并释放你的任务在公共堆中的分配(参见"任务退出时释放内存的方法")。
NuttX 进程退出
请注意,在 NuttX 上运行进程时(使用 `--no-delete` 命令CONFIG_BUILD_KERNEL),NuttX 的行为与 Linux 相同:任务退出时地址环境会被销毁,所有内存都会被回收。但其他所有情况都会造成内存泄漏。
任务退出时释放内存的方法
可以将已分配的内存与任务关联起来,以便在任务退出时清理这些内存。然而,这种方法已被否决,原因有二:(1) 无法可靠地实现;(2) 会增加内存分配开销,这在内存受限的情况下是不可接受的。