Możliwości alokacji pamięci do modułowego projektowania oprogramowania układowego w C.


16

podejścia modułowe są ogólnie bardzo przydatne (przenośne i czyste), więc staram się programować moduły tak niezależnie, jak to możliwe. Większość moich podejść opiera się na strukturze opisującej sam moduł. Funkcja inicjalizacji ustawia podstawowe parametry, a następnie program obsługi (wskaźnik do struktury opisowej) jest przekazywany do dowolnej funkcji w module, która jest wywoływana.

W tej chwili zastanawiam się, jakie może być najlepsze podejście do alokacji pamięci dla struktury opisującej moduł. Jeśli to możliwe, chciałbym:

  • Nieprzezroczysta struktura, więc strukturę można zmienić tylko przy użyciu dostarczonych funkcji interfejsu
  • Wiele wystąpień
  • pamięć przydzielona przez linker

Widzę następujące możliwości, że wszystkie są sprzeczne z jednym z moich celów:

globalna deklaracja

wiele instancji przypisanych przez linker, ale struct nie jest nieprzejrzysty

(#includes)
module_struct module;

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

malloc

nieprzezroczysta struktura, wiele instancji, ale allocotion na stercie

w module.h:

typedef module_struct Module;

w module init moduł c, malloc i wskaźnik powrotu do przydzielonej pamięci

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

w main.c

(#includes)
Module *module;

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

deklaracja w module

nieprzezroczysta struktura, przydzielana przez linker, tylko z góry określona liczba instancji

trzymaj całą strukturę i pamięć wewnętrznie w module i nigdy nie ujawniaj programu obsługi ani struktury.

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

Czy istnieje jakaś kombinacja tych opcji dla nieprzezroczystej struktury, linkera zamiast alokacji sterty i wielu / dowolnej liczby instancji?

rozwiązanie

jak zaproponowano w niektórych odpowiedziach poniżej, myślę, że najlepszym sposobem jest:

  1. zarezerwować miejsce na MODULE_MAX_INSTANCE_COUNT modułów w pliku źródłowym modułów
  2. nie definiuj MODULE_MAX_INSTANCE_COUNT w samym module
  3. dodaj #ifndef MODULE_MAX_INSTANCE_COUNT # błąd do pliku nagłówka modułów, aby upewnić się, że użytkownik modułów wie o tym ograniczeniu i określa maksymalną liczbę żądanych instancji dla aplikacji
  4. przy inicjalizacji instancji zwróć adres pamięci (* void) struktury opisowej lub indeks modułów (cokolwiek bardziej chcesz)

12
Większość projektantów oprogramowania wbudowanego unika dynamicznego przydzielania, aby zachować deterministyczne i proste użycie pamięci. Zwłaszcza jeśli jest to czysty metal i nie ma systemu operacyjnego do zarządzania pamięcią.
Eugene Sh.

Właśnie dlatego chcę, aby linker dokonał alokacji.
L. Heinrichs,

4
Nie jestem do końca pewien, czy rozumiem ... Jak możesz przypisać pamięć linkerowi, jeśli masz dynamiczną liczbę instancji? Wydaje mi się to dość ortogonalne.
jcaron

Dlaczego nie pozwolić linkerowi przydzielić jedną dużą pulę pamięci i wykonać własne przydziały z tego, co daje również korzyść z zerowego narzutu. Możesz ustawić obiekt puli statyczny dla pliku za pomocą funkcji alokacji, aby był prywatny. W części mojego kodu dokonuję wszystkich alokacji w różnych procedurach inicjujących, a następnie drukuję, ile przydzielono, więc w końcowej kompilacji produkcyjnej ustawiłem pulę dokładnie na ten rozmiar.
Lee Daniel Crocker,

2
Jeśli jest to decyzja w czasie kompilacji, możesz po prostu zdefiniować liczbę w swoim Makefile lub ekwiwalencie i wszystko jest gotowe. Liczba nie będzie w źródle modułu, ale będzie specyficzna dla aplikacji i wystarczy użyć numeru instancji jako parametru.
jcaron

Odpowiedzi:


4

Czy istnieje możliwość połączenia ich w jakiś sposób dla anonimowej struktury, linkera zamiast alokacji sterty i wielu / dowolnej liczby instancji?

Pewnie że jest. Najpierw jednak zauważ, że „dowolna liczba” instancji musi zostać ustalona, ​​lub przynajmniej ustalona górna granica, w czasie kompilacji. Jest to warunek wstępny statycznych alokacji instancji (tzw. „Alokacja linkera”). Możesz ustawić liczbę bez modyfikacji źródła, deklarując makro, które ją określa.

Następnie plik źródłowy zawierający rzeczywistą deklarację struktury i wszystkie powiązane z nią funkcje deklarują również tablicę instancji z wewnętrznym łączeniem. Zapewnia albo tablicę z zewnętrznym połączeniem wskaźników do instancji, albo funkcję dostępu do różnych wskaźników za pomocą indeksu. Wariant funkcji jest nieco bardziej modułowy:

moduł.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];
}

Wydaje mi się, że znasz już sposób, w jaki nagłówek zadeklaruje strukturę jako niekompletny typ i zadeklaruje wszystkie funkcje (zapisane w kategoriach wskaźników do tego typu). Na przykład:

moduł.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

Teraz struct modulejest nieprzezroczysty w jednostkach innych niż tłumaczenie module.c, * i można uzyskać dostęp i używać do liczby przypadków określonych w czasie kompilacji bez dynamicznej alokacji.


* Oczywiście, chyba że skopiujesz jego definicję. Chodzi o to, że module.htego nie robi.


Myślę, że dziwne jest przekazywanie indeksu spoza klasy. Kiedy implementuję takie pule pamięci, pozwalam, aby indeks był prywatnym licznikiem, zwiększając się o 1 dla każdej przydzielonej instancji. Dopóki nie osiągniesz „NUM_MODULE_INSTANCES”, gdzie konstruktor zwróci błąd braku pamięci.
Lundin,

To słuszna kwestia, @Lundin. Ten aspekt projektu zakłada, że ​​indeksy mają nieodłączne znaczenie, co może, ale nie musi mieć miejsca. Dotyczy to , choć trywialnie, przypadku początkowego PO. Takie znaczenie, jeśli istnieje, może być dalej wspierane przez zapewnienie środków do uzyskania wskaźnika instancji bez inicjowania.
John Bollinger,

Więc zasadniczo rezerwujesz pamięć dla n modułów, bez względu na to, ile z nich zostanie wykorzystanych, a następnie zwraca wskaźnik do następnego nieużywanego elementu, jeśli aplikacja go zainicjuje. Myślę, że to może zadziałać.
L. Heinrichs,

@ L.Heinrichs Tak, ponieważ systemy wbudowane mają charakter deterministyczny. Nie ma czegoś takiego jak „nieskończona ilość obiektów” ani „nieznana ilość obiektów”. Obiekty często są również singletonami (sterownikami sprzętowymi), więc pula pamięci często nie jest potrzebna, ponieważ istnieje tylko jedna instancja obiektu.
Lundin,

Zgadzam się w większości przypadków. Pytanie to miało również pewne teoretyczne znaczenie. Ale mógłbym użyć setek 1-przewodowych czujników temperatury, jeśli jest wystarczająca liczba dostępnych IO (jako jeden przykład, który mogę teraz wymyślić).
L. Heinrichs,

22

Programuję małe mikrokontrolery w C ++, który osiąga dokładnie to, czego chcesz.

Moduł nazywany jest klasą C ++, może zawierać dane (dostępne zewnętrznie lub nie) i funkcje (podobnie). Konstruktor (funkcja dedykowana) inicjuje ją. Konstruktor może pobierać parametry czasu wykonywania lub parametry (moje ulubione) czasu kompilacji (szablon). Funkcje w klasie domyślnie uzyskują zmienną klasy jako pierwszy parametr. (Lub, często według moich preferencji, klasa może działać jako ukryty singleton, więc dostęp do wszystkich danych nie ma tego narzutu).

Obiekt klasy może być globalny (więc wiesz w czasie łączenia, że ​​wszystko będzie pasować) lub lokalny stos, prawdopodobnie w głównym. (Nie lubię globałów C ++ z powodu nieokreślonej globalnej kolejności inicjalizacji, więc wolę lokalny stos).

Mój preferowany styl programowania polega na tym, że moduły są klasami statycznymi, a ich konfiguracja (statyczna) zależy od parametrów szablonu. Pozwala to uniknąć prawie wszystkich przewartościowań i umożliwia optymalizację. Połącz to z narzędziem, które oblicza rozmiar stosu i możesz spać bez obaw :)

Moje przemówienie na temat tego sposobu kodowania w C ++: Objects? Nie, dziękuję!

Wielu programistów osadzonych / mikrokontrolerów wydaje się nie lubić C ++, ponieważ uważają, że zmusiłoby ich to do użycia całego C ++. To absolutnie nie jest konieczne i byłby to bardzo zły pomysł. (Prawdopodobnie nie używasz również całego C! Pomyśl o stercie, zmiennoprzecinkowym, setjmp / longjmp, printf, ...)


W komentarzu Adam Haun wspomina o RAII i inicjalizacji. IMO RAII ma więcej wspólnego z dekonstrukcją, ale jego racja jest słuszna: obiekty globalne zostaną zbudowane przed twoimi głównymi startami, więc mogą działać na niepoprawnych założeniach (takich jak główna częstotliwość zegara, która zostanie później zmieniona). Jest to kolejny powód, dla którego NIE należy używać globalnych obiektów inicjowanych kodem. (Używam skryptu linkera, który zawiedzie, gdy będę miał globalne obiekty inicjowane kodem.) IMO takie „obiekty” powinny być jawnie tworzone i przekazywane. Obejmuje to obiekt „oczekiwania”, który udostępnia funkcję wait (). W moim ustawieniu jest to „obiekt”, który ustawia szybkość zegara układu.

Mówiąc o RAII: jest to jeszcze jedna funkcja C ++, która jest bardzo przydatna w małych systemach wbudowanych, chociaż nie z tego powodu (dezalokacja pamięci) jest najczęściej używana w dużych systemach (małe systemy wbudowane najczęściej nie używają dynamicznego zwalniania pamięci). Pomyśl o zablokowaniu zasobu: możesz uczynić zablokowany zasób obiektem opakowującym i ograniczyć dostęp do zasobu, aby był możliwy tylko za pomocą opakowującego blokowania. Gdy opakowanie opuści zakres, zasób zostaje odblokowany. Zapobiega to dostępowi bez blokowania i znacznie bardziej prawdopodobne jest, aby zapomnieć o odblokowaniu. z pewną magią (szablonową) może być zerowa.


Oryginalne pytanie nie wspominało o C, stąd moja C ++ - centryczna odpowiedź. Jeśli to naprawdę musi być C ....

Możesz użyć sztuczek z makrami: zadeklaruj publicznie swoje kanały, aby miały one typ i mogły być przydzielane globalnie, ale zmieniają nazwy swoich komponentów poza użyteczność, chyba że niektóre makro jest zdefiniowane inaczej, co ma miejsce w pliku .c modułu. Dla dodatkowego bezpieczeństwa możesz użyć czasu kompilacji w manipulacji.

Możesz też mieć publiczną wersję swojej struktury, która nie ma w niej nic użytecznego, i mieć wersję prywatną (z przydatnymi danymi) tylko w pliku .c i twierdzić, że mają ten sam rozmiar. Trochę sztuczek tworzenia plików może to zautomatyzować.


Komentarz @Lundins na temat złych (osadzonych) programistów:

  • Opisany typ programisty prawdopodobnie spowodowałby bałagan w dowolnym języku. Makra (obecne w C i C ++) są jednym oczywistym sposobem.

  • Oprzyrządowanie może w pewnym stopniu pomóc. Dla moich uczniów zlecam wbudowany skrypt, który określa brak wyjątków, no-rtti i daje błąd linkera, gdy używana jest sterty lub występują globały inicjowane kodem. Określa ostrzeżenie = błąd i włącza prawie wszystkie ostrzeżenia.

  • Zachęcam do używania szablonów, ale przy constexpr i pojęciach metaprogramowanie jest coraz mniej potrzebne.

  • „zdezorientowani programiści Arduino” Bardzo chciałbym zastąpić styl programowania Arduino (okablowanie, replikacja kodu w bibliotekach) nowoczesnym podejściem do C ++, które może być łatwiejsze, bezpieczniejsze i tworzyć szybszy i mniejszy kod. Gdybym tylko miał czas i moc ...


Dzięki za tę odpowiedź! Używanie C ++ jest opcją, ale używamy C w mojej firmie (o czym wyraźnie nie wspomniałem). Zaktualizowałem pytanie, aby ludzie wiedzieli, że mówię o C.
L. Heinrichs

Dlaczego używasz (tylko) C? Być może daje to szansę przekonania ich do przynajmniej rozważenia C ++ ... To, czego chcesz, to niezbędna (niewielka część) C ++ realizowana w C.
Wouter van Ooijen

What I do in my (first 'real' embedded hobby project) is initializing simple default in the constructor, and use a separate Init method for relevant classes. Another benefit is that I can pass stub pointers for unit testing.
Michel Keijzers

2
@Michel for a hobby project you are free to choose the language? Take C++!
Wouter van Ooijen

4
I chociaż rzeczywiście jest całkiem możliwe pisanie dobrych programów C ++ dla osadzonych, problem polega na tym, że około> 50% wszystkich programistów systemów wbudowanych to znachorzy, zdezorientowani programiści PC, hobbystycy Arduino itp. Tacy ludzie po prostu nie mogą używać wyczyść podzbiór C ++, nawet jeśli wytłumaczysz to sobie. Daj im C ++, a zanim się zorientujesz, będą używać całego STL, metaprogramowania szablonów, obsługi wyjątków, wielokrotnego dziedziczenia i tak dalej. Rezultatem są oczywiście kompletne śmieci. Niestety tak kończy się 8 z 10 osadzonych projektów C ++.
Lundin

7

I believe FreeRTOS (maybe another OS?) does something like what you're looking for by defining 2 different versions of the struct.
The 'real' one, used internally by the OS functions, and a 'fake' one which is the same size as the 'real' one, but doesn't have any useful members inside (just a bunch of int dummy1 and similar).
Only the 'fake' struct is exposed outside of the OS code, and this is used to allocate memory to static instances of the struct.
Internally, when functions in the OS are called, they are passed the address of the external 'fake' struct as a handle, and this is then typecast as a pointer to a 'real' struct so the OS functions can do what they need to do.


Good idea, I guess i could use --- #define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)])) --- BUILD_BUG_ON(sizeof(real_struct) != sizeof(fake_struct)) ----
L. Heinrichs

2

Anonymous struct, so the struct may only be altered by the use of provided interface functions

In my opinion, this is pointless. You can put a comment there, but no point trying to hide it further.

C will never provide such high isolation, even if there is no declaration for the struct, it will be easy to accidentally overwrite it with e.g. mistaken memcpy() or buffer overflow.

Instead, just give the struct a name and trust other people to write good code also. It will also make debugging easier when the struct has a name you can use to refer to it.


2

Pure SW questions are better asked at /programming/.

The concept with exposing a struct of incomplete type to caller, like you describe, is often called "opaque type" or "opaque pointers" - anonymous struct means something else entirely.

The problem with this is that the caller won't be able to allocate instances of the object, only pointers to it. On a PC you would use malloc inside the objects "constructor", but malloc is a no-go in embedded systems.

So what you do in embedded is to provide a memory pool. You have a limited amount of RAM, so restricting the number of objects that can be created is usually not a problem.

See Static allocation of opaque data types over at SO.


Ou thank you for clarifying the naming confusion on my end, ill adjust the OP. I was thinking of going to stack overflow, but decided i would like to target embedded programmers specifically.
L. Heinrichs
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.