Возможности выделения памяти для модульного дизайна прошивки на С


16

Модульные подходы в целом довольно удобны (переносимы и чисты), поэтому я стараюсь программировать модули как можно более независимо от любых других модулей. Большинство моих подходов основаны на структуре, которая описывает сам модуль. Функция инициализации устанавливает первичные параметры, после чего обработчик (указатель на дескриптивную структуру) передается любой вызываемой функции внутри модуля.

Сейчас мне интересно, каков наилучший подход к распределению памяти для структуры, описывающей модуль. Если возможно, я бы хотел следующее:

  • Непрозрачная структура, поэтому структура может быть изменена только с использованием предоставленных интерфейсных функций
  • Несколько экземпляров
  • память, выделенная компоновщиком

Я вижу следующие возможности, которые все противоречат одной из моих целей:

глобальная декларация

несколько экземпляров, все ссылки на компоновщик, но структура не непрозрачна

(#includes)
module_struct module;

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

таНос

непрозрачная структура, несколько экземпляров, но все в куче

в module.h:

typedef module_struct Module;

в модуле init.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 избегают динамического распределения, чтобы сохранить использование памяти детерминированным и простым. Особенно, если он «голый металл» и не имеет базовой ОС для управления памятью.
Евгений Ш.

Именно поэтому я хочу, чтобы компоновщик делал выделения.
Л. Хайнрихс

4
Я не совсем уверен, что понимаю ... Как вы могли бы выделить память компоновщиком, если у вас динамическое число экземпляров? Это кажется мне совершенно ортогональным.
jcaron

Почему бы не позволить компоновщику выделить один большой пул памяти и делать свои собственные выделения из этого, что также дает вам преимущество распределителя с нулевыми накладными расходами. Вы можете сделать объект пула статическим для файла с помощью функции выделения, чтобы он был закрытым. В некотором моем коде я делаю все выделения в различных подпрограммах инициализации, а затем распечатываю, сколько было выделено, поэтому в окончательной рабочей компиляции я установил пул на этот точный размер.
Ли Даниэль Крокер

2
Если это решение во время компиляции, вы можете просто определить число в вашем Makefile или его эквиваленте, и все готово. Номер не будет в источнике модуля, но будет зависеть от приложения, и вы просто используете номер экземпляра в качестве параметра.
jcaron

Ответы:


4

Есть ли возможность объединить их как-то для анонимной структуры, компоновщика вместо выделения кучи и нескольких / любого количества экземпляров?

Конечно, есть. Однако сначала следует признать, что «любое количество» экземпляров должно быть фиксированным или, по крайней мере, должна быть установлена ​​верхняя граница во время компиляции. Это является обязательным условием для статического размещения экземпляров (то, что вы называете «распределением компоновщика»). Вы можете настроить число без изменения источника, объявив макрос, который его определяет.

Затем исходный файл, содержащий фактическое объявление структуры и все связанные с ним функции, также объявляет массив экземпляров с внутренней связью. Он предоставляет либо массив с внешней связью указателей на экземпляры, либо функцию доступа к различным указателям по индексу. Вариация функции немного более модульная:

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];
}

Я предполагаю, что вы уже знакомы с тем, как заголовок затем объявит структуру как неполный тип и объявит все функции (написанные в терминах указателей на этот тип). Например:

module.h

#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. Этот аспект дизайна предполагает, что индексы имеют неотъемлемое значение, что на самом деле может иметь или не иметь место. Это является случаем, хотя и тривиальным так, для исходного дела ФПА в. Такое значение, если оно существует, может быть дополнительно поддержано путем предоставления средства для получения указателя экземпляра без инициализации.
Джон Боллинджер

Таким образом, в основном вы резервируете память для n модулей, независимо от того, сколько их будет использовано, а затем возвращаете указатель на следующий неиспользуемый элемент, если приложение его инициализирует. Я думаю, это может сработать.
Л. Хайнрихс

@ L.Heinrichs Да, поскольку встроенные системы имеют детерминированный характер. Нет такого понятия, как «бесконечное количество объектов» или «неизвестное количество объектов». Объекты также часто являются одиночными (аппаратные драйверы), поэтому пул памяти часто не нужен, поскольку существует только один экземпляр объекта.
Лундин

Я согласен в большинстве случаев. Этот вопрос также имел некоторый теоретический интерес. Но я мог бы использовать сотни однопроводных датчиков температуры, если имеется достаточно входов / выходов (как пример, который я могу привести сейчас).
Л. Хайнрихс

22

Я программирую небольшие микроконтроллеры на C ++, которые достигают именно того, что вы хотите.

То, что вы называете модулем, является классом C ++, он может содержать данные (доступные извне или нет) и функции (аналогично). Конструктор (выделенная функция) инициализирует его. Конструктор может принимать параметры времени выполнения или (мой любимый) параметры времени компиляции (шаблон). Функции в классе неявно получают переменную класса в качестве первого параметра. (Или, часто мое предпочтение, класс может действовать как скрытый синглтон, поэтому все данные доступны без этих накладных расходов).

Объект класса может быть глобальным (поэтому вы знаете, что во время компоновки все подойдет) или локальным в стеке, предположительно в основном. (Мне не нравятся глобальные переменные C ++ из-за неопределенного глобального порядка инициализации, поэтому я предпочитаю локальный стек).

Мой предпочтительный стиль программирования заключается в том, что модули являются статическими классами, а их (статическая) конфигурация определяется параметрами шаблона. Это позволяет избежать почти всех перегрузок и позволяет оптимизировать. Объедините это с инструментом, который рассчитывает размер стека, и вы можете спать без забот :)

Мой разговор об этом способе кодирования в C ++: объекты? Спасибо, не надо!

Многим программистам встраиваемых / микроконтроллеров не нравится C ++, потому что они думают, что это заставит их использовать весь C ++. Это абсолютно не обязательно, и было бы очень плохой идеей. (Вы, вероятно, тоже не используете все C! Думайте, куча, с плавающей точкой, setjmp / longjmp, printf, ...)


В комментарии Адам Хаун упоминает RAII и инициализацию. IMO RAII больше связан с деконструкцией, но его смысл верен: глобальные объекты будут создаваться до вашего основного запуска, поэтому они могут работать с неверными предположениями (например, с основной тактовой частотой, которая будет изменена позже). Это еще одна причина НЕ использовать глобальные объекты, инициализированные кодом. (Я использую скрипт компоновщика, который завершится ошибкой, когда у меня будут глобальные объекты, инициализированные кодом.) IMO такие «объекты» должны быть явно созданы и переданы. Это включает в себя объект «объекта ожидания», который предоставляет функцию wait (). В моей настройке это «объект», который устанавливает тактовую частоту чипа.

Говоря о RAII: это еще одна особенность C ++, которая очень полезна в небольших встроенных системах, хотя и не по той причине (освобождение памяти), которая больше всего используется в больших системах (небольшие встроенные системы в основном не используют динамическое освобождение памяти). Подумайте о блокировке ресурса: вы можете сделать заблокированный ресурс объектом-оберткой и ограничить доступ к ресурсу, чтобы он был возможен только через блокировку-обертку. Когда оболочка выходит из области видимости, ресурс разблокируется. Это предотвращает доступ без блокировки и делает гораздо менее вероятным забыть разблокировку. с некоторой (шаблонной) магией это может быть ноль.


В первоначальном вопросе не упоминался C, поэтому мой C ++ -центричный ответ. Если это действительно должно быть C ....

Вы можете использовать трюки с макросами: публично объявлять свои структуры, чтобы они имели тип, и их можно было распределять глобально, но искажать имена их компонентов за пределами удобства использования, если какой-то макрос не определен иначе, как в случае с файлом .c вашего модуля. Для дополнительной безопасности вы можете использовать время компиляции в календаре.

Или имейте общедоступную версию вашей структуры, в которой нет ничего полезного, и используйте закрытую версию (с полезными данными) только в вашем файле .c, и утверждайте, что они имеют одинаковый размер. Немного хитрости в make-file может автоматизировать это.


Комментарий @Lundins о плохих (встроенных) программистах:

  • Тип программиста, которого вы описываете, вероятно, создаст беспорядок на любом языке. Макросы (присутствующие в C и C ++) являются одним из очевидных способов.

  • Инструменты могут помочь в некоторой степени. Для моих учеников я предписываю встроенный скрипт, который задает no-excptions, no-rtti и выдает ошибку компоновщика, когда используется либо куча, либо глобальные символы с инициализацией кода. И это указывает предупреждение = ошибка и включает почти все предупреждения.

  • Я рекомендую использовать шаблоны, но с constexpr и концепциями метапрограммирование все меньше и меньше требуется.

  • «запутанные программисты Arduino» Я бы очень хотел заменить стиль программирования Arduino (проводка, репликация кода в библиотеках) на современный подход C ++, который может быть более простым, более безопасным и производить более быстрый и меньший код. Если бы у меня было время и силы ....


Спасибо за этот ответ! Использование C ++ является вариантом, но мы используем C в моей компании (о чем я не упомянул явно). Я обновил вопрос, чтобы люди знали, что я говорю о С.
Л. Хайнрихс

Почему вы используете (только) C? Может быть, это дает вам шанс убедить их хотя бы рассмотреть C ++ ... То, что вы хотите - это существенная часть C ++, реализованная в C.
Wouter van Ooijen

В моем (первом «реальном» внедренном хобби-проекте) я делаю инициализацию простого конструктора по умолчанию и использую отдельный метод Init для соответствующих классов. Еще одним преимуществом является то, что я могу передать указатели заглушки для модульного тестирования.
Мишель Кейзерс

2
@ Мишель для хобби проекта, вы можете свободно выбирать язык? Бери С ++!
Ваутер ван Ойджен

4
И хотя действительно можно писать хорошие программы на C ++ для встраиваемых систем, проблема в том, что около 50% программистов встраиваемых систем - это кряки, запутанные программисты на ПК, любители Arduino и т. Д. И т. П. Такие люди просто не могут использовать чистое подмножество C ++, даже если вы объясните это их лицу. Дайте им C ++, и, прежде чем вы это узнаете, они будут использовать весь STL, шаблонное метапрограммирование, обработку исключений, множественное наследование и так далее. И в результате, конечно, полный мусор. К сожалению, именно так заканчиваются 8 из 10 встроенных C ++ проектов.
Лундин

7

Я считаю, что FreeRTOS (может быть, другая ОС?) Делает что-то вроде того, что вы ищете, определяя 2 разные версии структуры.
«Настоящий», используемый внутренне для функций ОС, и «поддельный», который имеет тот же размер, что и «настоящий», но не имеет внутри каких-либо полезных элементов (просто куча int dummy1и тому подобное).
Только «поддельная» структура предоставляется вне кода ОС, и это используется для выделения памяти статическим экземплярам структуры.
Внутренне, когда функции в ОС вызываются, им передают адрес внешней «поддельной» структуры как дескриптор, и это затем преобразуется в тип как указатель на «реальную» структуру, поэтому функции ОС могут делать то, что им нужно делать.


Хорошая идея, я думаю, я мог бы использовать --- #define BUILD_BUG_ON (условие) ((void) sizeof (char [1 - 2 * !! (условие)])) --- BUILD_BUG_ON (sizeof (real_struct)! = Sizeof ( fake_struct)) ----
Л. Хайнрихс

2

Анонимная структура, поэтому структура может быть изменена только с помощью предоставленных интерфейсных функций

На мой взгляд, это бессмысленно. Вы можете оставить комментарий там, но нет смысла пытаться скрыть его дальше.

C никогда не обеспечит такую ​​высокую изоляцию, даже если для структуры нет объявления, будет легко случайно перезаписать ее, например, по ошибке memcpy () или переполнением буфера.

Вместо этого просто дайте структуре имя и доверьте другим людям писать хороший код. Это также облегчит отладку, когда структура имеет имя, которое вы можете использовать для обращения к ней.


2

Вопросы о чистом ПО лучше задать на /programming/ .

Концепция предоставления объекту неполного типа вызывающей стороне , как вы описываете, часто называется «непрозрачный тип» или «непрозрачные указатели» - анонимная структура означает что-то совсем другое.

Проблема в том, что вызывающая сторона не сможет выделить экземпляры объекта, только указатели на него. На ПК вы бы использовали mallocвнутри объектов «конструктор», но malloc не используется во встроенных системах.

Итак, что вы делаете во встроенном - это предоставление пула памяти. У вас ограниченный объем оперативной памяти, поэтому ограничение количества создаваемых объектов обычно не является проблемой.

См. Статическое распределение непрозрачных типов данных в SO.


Спасибо, что разъяснили путаницу с именами с моей стороны, плохо настроите OP. Я думал о возможности переполнения стека, но решил, что хотел бы специально ориентироваться на программистов встраиваемых систем.
Л. Хайнрихс
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.