在C中为模块化固件设计分配内存的可能性


16

模块化方法通常非常方便(便携式且简洁),因此我尝试将模块编程为尽可能独立于其他模块。我的大多数方法都是基于描述模块本身的结构。初始化函数设置主要参数,然后将处理程序(目标字符串的指针)传递给模块内部调用的任何函数。

现在,我想知道为模块描述结构的最佳分配内存方法是什么。如果可能,我想要以下内容:

  • 不透明的结构,因此只能通过使用提供的接口功能来更改结构
  • 多个实例
  • 链接器分配的内存

我看到以下可能性,所有这些都与我的目标之一冲突:

全球宣言

多个实例,由链接器分配,但是struct不是不透明的

(#includes)
module_struct module;

void main(){
   module_init(&module);
}

分配

不透明的结构,多个实例,但在堆上分配

在module.h中:

typedef module_struct Module;

在module.c初始化函数中,malloc并返回指向已分配内存的指针

module_mem = malloc(sizeof(module_struct ));
/* initialize values here */
return module_mem;

在main.c中

(#includes)
Module *module;

void main(){
    module = module_init();
}

模块中的声明

链接器分配的不透明结构,仅预定义数量的实例

将整个结构和内存保留在模块内部,并且永远不要公开处理程序或结构。

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

是否可以选择将这些方式组合为不透明的结构,链接器而不是堆分配和多个/任意数量的实例?

如以下一些答案中所建议,我认为最好的方法是:

  1. 在模块源文件中为MODULE_MAX_INSTANCE_COUNT个模块保留空间
  2. 不要在模块本身中定义MODULE_MAX_INSTANCE_COUNT
  3. 将#ifndef MODULE_MAX_INSTANCE_COUNT #error添加到模块头文件中,以确保模块用户知道此限制并定义应用程序所需的最大实例数
  4. 在实例初始化时,返回描述性结构的内存地址(* void)或模块索引(无论您喜欢什么)

12
大多数嵌入式FW设计人员都在避免动态分配,以保持确定和简单的内存使用。特别是如果它是裸机并且没有底层操作系统来管理内存。
尤金(Eugene Sh)。

就是这样,这就是为什么我希望链接器执行分配。
海因里希斯

4
我不太确定我能理解...如果实例数是动态的,那么如何由链接器分配内存?这似乎与我完全正交。
jcaron

为什么不让链接器分配一个大内存池,并从中分配自己的内存,这也使您受益于零开销的分配器。您可以使用分配功能使池对象对文件静态,因此它是私有的。在我的一些代码中,我在各种init例程中进行了所有分配,然后打印出分配了多少,因此在最终生产编译中,我将池设置为该确切大小。
李·丹尼尔·克罗克

2
如果是编译时的决定,则只需在Makefile或等效文件中定义数字即可,一切就绪。该编号将不在模块的来源中,而是特定于应用程序的,您只需使用实例编号作为参数即可。
jcaron

Answers:


4

是否可以通过某种方式将这些组合用于匿名结构,链接器而不是堆分配和多个/任意数量的实例?

当然可以。但是,首先要认识到,“任意数量”的实例必须在编译时是固定的,或者至少要确定一个上限。这是静态分配实例(称为“链接器分配”)的先决条件。您可以通过声明一个指定数字的宏来使数字可调,而无需修改源代码。

然后,包含实际struct声明及其所有关联函数的源文件还将声明一个具有内部链接的实例数组。它提供了一个带有外部链接的指向实例的指针的数组,或者提供了一个通过索引访问各种指针的函数。功能变化更加模块化:

module.c

#include <module.h>

// 4 instances by default; can be overridden at compile time
#ifndef NUM_MODULE_INSTANCES
#define NUM_MODULE_INSTANCES 4
#endif

struct module {
    int demo;
};

// has internal linkage, so is not directly visible from other files:
static struct module instances[NUM_MODULE_INSTANCES];

// module functions

struct module *module_init(unsigned index) {
    instances[index].demo = 42;
    return &instances[index];
}

我猜您已经熟悉了标头随后将结构声明为不完整类型并声明所有函数(以指向该类型的指针的形式编写)的方式。例如:

模块

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

现在struct module是比其他的翻译单元不透明module.c*您可以访问和使用最多在编译时定义的实例数没有任何动态分配。


*当然,除非您复制其定义。关键是module.h不这样做。


我认为从类外部传递索引是一种奇怪的设计。当我实现这样的内存池时,我让索引成为一个私有计数器,每个分配的实例增加1。在到达“ NUM_MODULE_INSTANCES”之前,构造函数将在其中返回内存不足错误。
伦丁

这很公平,@ Lundin。设计的那个方面假设索引具有固有的重要性,实际上可能是事实,也可能不是。对于OP的开始情况,确实是这样,尽管很琐碎。如果存在,则可以通过提供一种无需初始化即可获取实例指针的手段来进一步支持这种重要性。
John Bollinger

因此,基本上,您将为n个模块保留内存,无论将使用多少个模块,如果应用程序对其进行初始化,则返回指向下一个未使用元素的指针。我想那可能行得通。
海因里希斯

@ L.Heinrichs是的,因为嵌入式系统具有确定性。没有“无数的对象”或“未知的对象”之类的东西。对象通常也是单例对象(硬件驱动程序),因此通常不需要内存池,因为仅存在一个对象实例。
伦丁

我同意大多数情况。这个问题也有一些理论上的兴趣。但是,如果有足够的IO,我可以使用数百个1线温度传感器(作为我现在可以举出的一个例子)。
海因里希斯

22

我用C ++编写了小型微控制器,可以完全实现您想要的功能。

您所谓的模块是C ++类,它可以包含数据(可以从外部访问或不能从外部访问)和函数(同样可以)。构造函数(专用函数)将其初始化。构造函数可以采用运行时参数或(我最喜欢的)编译时(模板)参数。类中的函数隐式地将类变量作为第一个参数。(或者,通常,根据我的喜好,该类可以充当隐藏的单例,因此无需此开销即可访问所有数据)。

类对象可以是全局的(因此您可以在链接时知道所有内容都适用),也可以是堆栈局部的(大概在主对象中)。(由于不确定的全局初始化顺序,我不喜欢C ++全局变量,因此我更喜欢使用本地堆栈)。

我首选的编程风格是模块是静态类,并且它们的(静态)配置是通过模板参数来进行的。这避免了几乎所有的大修并实现了优化。将此与计算堆栈大小的工具结合使用,您可以安心入睡:)

我的演讲以这种方式在C ++中进行编码:对象?不用了,谢谢!

许多嵌入式/微控制器程序员似乎不喜欢C ++,因为他们认为这将迫使他们使用所有 C ++。那绝对不是必须的,这将是一个非常糟糕的主意。(您可能也不会全部使用C!想想堆,浮点,setjmp / longjmp,printf等)。


Adam Haun在评论中提到RAII和初始化。IMO RAII与解构有更多关系,但他的观点是正确的:全局对象将在您的主启动之前构建,因此它们可能会在无效的假设下工作(例如稍后将更改的主时钟速度)。这是不使用全局代码初始化对象的另一个原因。(我使用的链接描述文件会在我拥有全局代码初始化的对象时失败。)IMO这样的“对象”应明确创建并传递。这包括提供wait()函数的“等待”设施“对象”。在我的设置中,这是设置芯片时钟速度的“对象”。

谈论RAII:这是C ++的另一项功能,在小型嵌入式系统中非常有用,尽管不是出于它在大型系统中最常用的原因(内存释放)(小型嵌入式系统通常不使用动态内存释放)。考虑锁定资源:您可以使锁定的资源成为包装对象,并限制只能通过锁定包装来访问资源。当包装超出范围时,资源将被解锁。这样可以防止未经锁定的访问,并且更不可能忘记解锁。使用一些(模板)魔术,可以零开销。


最初的问题没有提到C,因此我以C ++为中心。如果真的是C ....

您可以使用宏技巧:公开声明您的结构,因此它们具有类型并可以全局分配,但是除非可用性有所不同,否则请破坏其组件名称,除非某些宏的定义不同(在模块的.c文件中就是这种情况)。为了获得额外的安全性,您可以在修改中使用编译时间。

或者,在结构的公共版本中没有任何用处,而私有版本(包含有用的数据)仅在您的.c文件中,并断言它们的大小相同。某些make-file欺骗可能会自动执行此操作。


@Lundins对不良(嵌入式)程序员的评论:

  • 您所描述的程序员的类型可能会以任何语言造成混乱。宏(存在于C和C ++中)是一种显而易见的方法。

  • 工具可以在某种程度上有所帮助。对于我的学生,我要求构建一个脚本,该脚本指定no-exception,no-rtti,并在使用堆或存在代码初始化的全局变量时给出链接器错误。并且它指定warning = error并启用几乎所有警告。

  • 我鼓励使用模板,但是使用constexpr和概念时,元编程的需求越来越少。

  • “困惑的Arduino程序员”我非常想用现代的C ++方法替换Arduino(接线,在库中复制代码)编程风格,该方法可以更容易,更安全并且产生更快和更小的代码。如果我有时间和力量...


感谢您的回答!使用C ++是一种选择,但是我们在我的公司中使用C(我没有明确提到过)。我已经更新的问题,让人们知道林谈论C.
L.海因里希

为什么要(仅)使用C?也许这给你机会来说服他们至少要考虑C ++ ......你想要什么essentialy是(一小部分)C ++在C实现
沃特面包车Ooijen

我在第一个“真正的”嵌入式爱好项目)中的工作是在构造函数中初始化简单的默认值,并对相关类使用单独的Init方法。另一个好处是我可以传递存根指针用于单元测试。
Michel Keijzers

2
@Michel是一个业余项目,您可以自由选择语言吗?使用C ++!
Wouter van Ooijen

4
虽然确实有可能为嵌入式系统编写良好的C ++程序,但问题是所有嵌入式系统程序员中> 50%左右是庸医,困惑的PC程序员,Arduino爱好者等。这些人根本无法使用干净的C ++子集,即使您面对他们解释也是如此。给他们C ++,在不知不觉中,他们将使用整个STL,模板元编程,异常处理,多重继承等。结果当然是完整的垃圾。不幸的是,这就是10个嵌入式C ++项目中约有8个最终结果的方式。
伦丁

7

我相信FreeRTOS(也许是另一个OS?)通过定义2个不同版本的结构来实现您所需要的功能。
OS函数内部使用的“真实”对象,和“真实”对象的大小相同,但内部没有任何有用的成员(只是一堆,int dummy1类似)的“假”对象。
只有'fake'结构暴露在OS代码之外,并且用于将内存分配给该结构的静态实例。
在内部,当调用OS中的函数时,会将其传递给外部“假”结构的地址作为句柄,然后将其类型转换为指向“真实”结构的指针,以便OS函数可以执行其所需的操作做。


好主意,我想我可以使用--- #define BUILD_BUG_ON(condition)((void)sizeof(char [1-2 * !!(condition)]))--- BUILD_BUG_ON(sizeof(real_struct)!= sizeof( fake_struct))----
L. Heinrichs

2

匿名结构,因此只能通过使用提供的接口函数来更改结构

我认为这毫无意义。您可以在此处添加评论,但试图进一步隐藏它毫无意义。

C永远不会提供如此高的隔离度,即使没有声明该结构,也很容易因错误的memcpy()或缓冲区溢出而意外覆盖它。

取而代之的是,只给结构命名,并信任其他人也可以编写出色的代码。当该结构具有可用于引用它的名称时,这还将使调试更加容易。


2

最好在/programming/上询问纯软件问题。

正如您所描述的,将不完整类型的结构暴露给调用者的概念通常称为“不透明类型”或“不透明指针”-匿名结构完全意味着其他东西。

这样做的问题是,调用者将无法分配对象的实例,而只能分配指向该对象的指针。在PC上,您可以malloc在对象“构造函数”内部使用,但是malloc在嵌入式系统中是行不通的。

因此,您在嵌入式系统中要做的就是提供一个内存池。您的RAM数量有限,因此限制可以创建的对象数量通常不是问题。

请参见SO上的静态不透明数据类型分配


欧,感谢您为我澄清命名混乱,请调整OP。我当时想堆栈溢出,但是决定我想专门针对嵌入式程序员。
海因里希斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.