Possibilità di allocare memoria per la progettazione di firmware modulare in C


16

gli approcci modulari sono piuttosto utili in generale (portatili e puliti), quindi provo a programmare moduli il più indipendenti possibile da qualsiasi altro modulo. La maggior parte dei miei approcci si basa su una struttura che descrive il modulo stesso. Una funzione di inizializzazione imposta i parametri primari, successivamente un gestore (puntatore alla struttura descrittiva) viene passato a qualsiasi funzione all'interno del modulo viene chiamata.

In questo momento, mi chiedo quale potrebbe essere l'approccio migliore della memoria di allocazione per la struttura che descrive un modulo. Se possibile, vorrei quanto segue:

  • Struct opaca, quindi la struct può essere modificata solo mediante l'uso delle funzioni di interfaccia fornite
  • Istanze multiple
  • memoria allocata dal linker

Vedo le seguenti possibilità, tutte in conflitto con uno dei miei obiettivi:

dichiarazione globale

istanze multiple, assegnate dal linker, ma struct non è opaco

(#includes)
module_struct module;

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

malloc

struttura opaca, istanze multiple, ma allcotion su heap

nel modulo.h:

typedef module_struct Module;

nella funzione module.c init, malloc e restituisce il puntatore alla memoria allocata

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

in main.c

(#includes)
Module *module;

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

dichiarazione nel modulo

struttura opaca, allocata dal linker, solo un numero predefinito di istanze

mantenere l'intera struttura e la memoria interne al modulo e non esporre mai un gestore o una struttura.

(#includes)

void main(){
    module_init(_no_param_or_index_if_multiple_instances_possible_);
}

Esiste un'opzione per combinarli in qualche modo per una struttura opaca, un linker invece dell'allocazione dell'heap e più / un numero qualsiasi di istanze?

soluzione

come proposto in alcune risposte di seguito, penso che il modo migliore sia:

  1. riservare spazio per i moduli MODULE_MAX_INSTANCE_COUNT nel file di origine dei moduli
  2. non definire MODULE_MAX_INSTANCE_COUNT nel modulo stesso
  3. aggiungere un #ifndef MODULE_MAX_INSTANCE_COUNT #error al file di intestazione dei moduli per assicurarsi che l'utente dei moduli sia a conoscenza di questa limitazione e definisca il numero massimo di istanze desiderate per l'applicazione
  4. all'inizializzazione di un'istanza restituisci l'indirizzo di memoria (* vuoto) della struttura distruttiva o l'indice dei moduli (qualunque cosa ti piaccia di più)

12
La maggior parte dei progettisti di FW embedded sta evitando l'allocazione dinamica per mantenere l'utilizzo della memoria deterministico e semplice. Soprattutto se è bare metal e non ha un sistema operativo sottostante per gestire la memoria.
Eugene Sh.

Esatto, ecco perché voglio che il linker esegua le allocazioni.
L. Heinrichs,

4
Non sono del tutto sicuro di capire ... Come è possibile disporre della memoria allocata dal linker se si dispone di un numero dinamico di istanze? Mi sembra abbastanza ortogonale.
jcaron,

Perché non lasciare che il linker alloca un grande pool di memoria e fai le tue allocazioni da quello, il che ti dà anche il vantaggio di un allocatore zero-overhead. È possibile rendere l'oggetto pool statico al file con la funzione di allocazione in modo che sia privato. In alcuni dei miei codici, eseguo tutte le allocazioni nelle varie routine di init, quindi stampo in seguito quanto è stato allocato, quindi nella compilazione di produzione finale ho impostato il pool su quella dimensione esatta.
Lee Daniel Crocker,

2
Se è una decisione in fase di compilazione, potresti semplicemente definire il numero nel tuo Makefile o equivalente, e sei pronto. Il numero non si troverà nell'origine del modulo, ma sarebbe specifico dell'applicazione e si utilizzerà semplicemente un numero di istanza come parametro.
jcaron,

Risposte:


4

Esiste un'opzione per combinarli in qualche modo per struttura anonima, linker anziché allocazione di heap e più / qualsiasi numero di istanze?

Certo che c'è. Innanzitutto, tuttavia, riconoscere che "qualsiasi numero" di istanze deve essere corretto, o almeno stabilito un limite superiore, al momento della compilazione. Questo è un prerequisito per l'allocazione statica delle istanze (quella che stai chiamando "allocazione linker"). È possibile rendere il numero regolabile senza modifiche all'origine dichiarando una macro che lo specifica.

Quindi il file di origine che contiene l'effettiva dichiarazione struct e tutte le sue funzioni associate dichiara anche una matrice di istanze con collegamento interno. Fornisce una matrice, con collegamento esterno, di puntatori alle istanze oppure una funzione per accedere ai vari puntatori per indice. La variazione della funzione è un po 'più modulare:

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

Immagino che tu abbia già familiarità con il modo in cui l'intestazione dichiarerebbe quindi la struttura come un tipo incompleto e dichiarerà tutte le funzioni (scritte in termini di puntatori a quel tipo). Per esempio:

module.h

#ifndef MODULE_H
#define MODULE_H

struct module;

struct module *module_init(unsigned index);

// other functions ...

#endif

Ora struct moduleè opaco in unità di traduzione diverse da module.c, * e puoi accedere e utilizzare fino al numero di istanze definite al momento della compilazione senza alcuna allocazione dinamica.


* A meno che tu non copi la sua definizione, ovviamente. Il punto è che module.hnon lo fa.


Penso che sia un disegno strano passare l'indice al di fuori della classe. Quando implemento pool di memoria come questo, lascio che l'indice sia un contatore privato, aumentando di 1 per ogni istanza allocata. Fino a quando non raggiungi "NUM_MODULE_INSTANCES", dove il costruttore restituirà un errore di memoria esaurita.
Lundin,

Questo è un punto giusto, @Lundin. Tale aspetto del progetto suppone che gli indici abbiano un significato intrinseco, il che potrebbe essere o meno vero. Esso è il caso, seppur banalmente così, per il caso di avviamento del PO. Tale significato, se esiste, potrebbe essere ulteriormente supportato fornendo un mezzo per ottenere un puntatore di istanza senza inizializzare.
John Bollinger,

Quindi in pratica riservi la memoria per n moduli, indipendentemente da quanti ne verranno utilizzati, quindi restituisci un puntatore al successivo elemento inutilizzato se l'applicazione lo inizializza. Immagino che potrebbe funzionare.
L. Heinrichs,

@ L.Heinrichs Sì, poiché i sistemi incorporati sono di natura deterministica. Non esiste "quantità infinita di oggetti" né "quantità sconosciuta di oggetti". Gli oggetti sono spesso anche singoli (driver hardware), quindi spesso non è necessario il pool di memoria, in quanto esiste una sola istanza dell'oggetto.
Lundin,

Sono d'accordo per la maggior parte dei casi. La domanda aveva anche un certo interesse teorico. Ma potrei usare centinaia di sensori di temperatura a 1 filo se ci sono abbastanza IO disponibili (come l'esempio che posso trovare ora).
L. Heinrichs,

22

Programma piccoli micro-controller in C ++, che ottengono esattamente ciò che desideri.

Quello che chiami un modulo è una classe C ++, può contenere dati (accessibili dall'esterno o meno) e funzioni (allo stesso modo). Il costruttore (una funzione dedicata) lo inizializza. Il costruttore può prendere parametri di runtime o (il mio preferito) parametri di compilazione (template). Le funzioni all'interno della classe ottengono implicitamente la variabile di classe come primo parametro. (O, spesso la mia preferenza, la classe può fungere da singleton nascosto, quindi tutti i dati sono accessibili senza questo sovraccarico).

L'oggetto class può essere globale (in modo da sapere al momento del collegamento che tutto si adatterà), o stack-local, presumibilmente nel main. (Non mi piacciono i globi C ++ a causa dell'ordine di inizializzazione globale non definito, quindi preferisco stack-local).

Il mio stile di programmazione preferito è che i moduli sono classi statiche e la loro configurazione (statica) è basata su parametri del modello. Ciò evita quasi tutti i overhad e consente l'ottimizzazione. Combina questo con uno strumento che calcola le dimensioni dello stack e puoi dormire senza preoccupazioni :)

Il mio discorso su questo modo di scrivere codice in C ++: Objects? No grazie!

Molti programmatori di microcontrollori / embedded sembrano non apprezzare il C ++ perché pensano che li costringerebbe ad usare tutto il C ++. Non è assolutamente necessario e sarebbe una pessima idea. (Probabilmente non usi nemmeno tutta la C! Pensa a heap, virgola mobile, setjmp / longjmp, printf, ...)


In un commento Adam Haun menziona RAII e l'inizializzazione. IMO RAII ha più a che fare con la decostruzione, ma il suo punto è valido: gli oggetti globali saranno costruiti prima dell'inizio principale, quindi potrebbero funzionare su ipotesi non valide (come una velocità di clock principale che verrà modificata in seguito). Questa è un'altra ragione per NON usare oggetti inizializzati con codice globale. (Uso uno script di linker che fallirà quando avrò oggetti con inizializzazione di codice globale.) IMO tali "oggetti" dovrebbero essere esplicitamente creati e fatti passare. Ciò include un 'oggetto' di 'attesa' che fornisce una funzione wait (). Nella mia configurazione questo è "oggetto" che imposta la velocità di clock del chip.

Parlando di RAII: questa è un'altra funzionalità C ++ molto utile in piccoli sistemi embedded, sebbene non per il motivo (deallocazione della memoria) per cui è maggiormente usata nei sistemi più grandi (i piccoli sistemi embedded per lo più non usano la deallocazione dinamica della memoria). Pensa al blocco di una risorsa: puoi rendere la risorsa bloccata un oggetto wrapper e limitare l'accesso alla risorsa solo tramite il wrapper di blocco. Quando il wrapper esce dall'ambito, la risorsa viene sbloccata. Ciò impedisce l'accesso senza blocco e rende molto più improbabile dimenticare lo sblocco. con un po 'di magia (modello) può essere zero overhead.


La domanda originale non menzionava C, quindi la mia risposta centrata sul C ++. Se davvero deve essere C ....

Puoi usare i trucchi per le macro: dichiarare pubblicamente le tue strutture, quindi hanno un tipo e possono essere allocate a livello globale, ma manipolare i nomi dei loro componenti oltre l'usabilità, a meno che alcune macro non siano definite in modo diverso, come nel file .c del tuo modulo. Per una maggiore sicurezza, è possibile utilizzare il tempo di compilazione nel mangling.

Oppure disponi di una versione pubblica della tua struttura che non contenga nulla di utile e abbia la versione privata (con dati utili) solo nel tuo file .c e asserisci che hanno le stesse dimensioni. Un po 'di trucco in make-file potrebbe automatizzare questo.


Commento di @Lundins su programmatori (incorporati) difettosi:

  • Il tipo di programmatore che descrivi probabilmente creerebbe confusione in qualsiasi lingua. Le macro (presenti in C e C ++) sono un modo ovvio.

  • Gli utensili possono aiutare in una certa misura. Per i miei studenti ho mandato uno script incorporato che specifica no-eccezioni, no-rtti e fornisce un errore del linker quando viene utilizzato l'heap o sono presenti globi inizializzati dal codice. E specifica warning = error e abilita quasi tutti gli avvisi.

  • Incoraggio l'uso di modelli, ma con constexpr e concetti la metaprogrammazione è sempre meno richiesta.

  • "programmatori Arduino confusi" Mi piacerebbe molto sostituire lo stile di programmazione Arduino (cablaggio, replica del codice nelle librerie) con un moderno approccio C ++, che può essere più semplice, più sicuro e produrre codice più veloce e più piccolo. Se solo avessi il tempo e il potere ...


Grazie per questa risposta! L'uso del C ++ è un'opzione, ma stiamo usando il C nella mia azienda (ciò che non ho menzionato esplicitamente). Ho aggiornato la domanda per far sapere alla gente che sto parlando di C.
L. Heinrichs il

Perché stai usando (solo) C? Forse questo ti dà la possibilità di convincerli a considerare almeno il C ++ ... Ciò che vuoi è essenzialmente (una piccola parte di) C ++ realizzato in C.
Wouter van Ooijen,

Quello che faccio nel mio (primo progetto di hobby incorporato "reale") è l'inizializzazione del semplice default nel costruttore e l'uso di un metodo Init separato per le classi pertinenti. Un altro vantaggio è che posso passare puntatori stub per test unitari.
Michel Keijzers,

2
@Michel per un progetto di hobby sei libero di scegliere la lingua? Prendi C ++!
Wouter van Ooijen,

4
E mentre è davvero perfettamente possibile scrivere buoni programmi C ++ per embedded, il problema è che circa il 50% di tutti i programmatori di sistemi embedded là fuori sono ciarlatani, programmatori di PC confusi, hobbisti di Arduino ecc. Ecc. Questo tipo di persone semplicemente non possono usare un sottoinsieme pulito di C ++, anche se lo spieghi in faccia. Dai loro C ++ e prima che tu lo sappia, useranno l'intero STL, la metaprogrammazione dei modelli, la gestione delle eccezioni, l'ereditarietà multipla e così via. E il risultato è ovviamente spazzatura completa. Questo è, purtroppo, il risultato di circa 8 progetti C ++ integrati su 10.
Lundin,

7

Credo che FreeRTOS (forse un altro sistema operativo?) Faccia qualcosa di simile a quello che stai cercando definendo 2 diverse versioni della struttura.
Quello "reale", utilizzato internamente dalle funzioni del sistema operativo, e uno "falso" che ha le stesse dimensioni di quello "reale", ma non contiene membri utili (solo un gruppo di int dummy1e simili).
Solo la struttura "falsa" è esposta al di fuori del codice del sistema operativo e viene utilizzata per allocare memoria alle istanze statiche della struttura.
Internamente, quando vengono chiamate funzioni nel sistema operativo, viene passato l'indirizzo della struttura "falsa" esterna come handle, e questo viene quindi convertito come puntatore a una struttura "reale" in modo che le funzioni del sistema operativo possano fare ciò di cui hanno bisogno fare.


Buona idea, immagino di poter usare --- #define BUILD_BUG_ON (condition) ((void) sizeof (char [1 - 2 * !! (condition)])) --- BUILD_BUG_ON (sizeof (real_struct)! = Sizeof ( fake_struct)) ----
L. Heinrichs,

2

Struct anonima, quindi la struttura può essere modificata solo mediante l'uso delle funzioni di interfaccia fornite

Secondo me, questo è inutile. Puoi inserire un commento lì, ma inutile cercare di nasconderlo ulteriormente.

C non fornirà mai un isolamento così elevato, anche se non ci sono dichiarazioni per la struttura, sarà facile sovrascriverlo accidentalmente con ad es. Memcpy () errato o buffer overflow.

Invece, basta dare un nome alla struttura e fidarsi di altre persone a scrivere anche un buon codice. Semplifica anche il debug quando la struttura ha un nome che puoi usare per fare riferimento ad esso.


2

Le domande su SW puro sono poste meglio su /programming/ .

Il concetto con l'esposizione di una struttura di tipo incompleto al chiamante, come tu descrivi, è spesso chiamato "tipo opaco" o "puntatori opachi": la struttura anonima significa qualcos'altro.

Il problema è che il chiamante non sarà in grado di allocare istanze dell'oggetto, ma solo puntatori ad esso. Su un PC useresti mallocall'interno del "costruttore" degli oggetti, ma malloc non è un sistema operativo nei sistemi embedded.

Quindi, ciò che fai in embedded è fornire un pool di memoria. Hai una quantità limitata di RAM, quindi limitare il numero di oggetti che è possibile creare di solito non è un problema.

Vedere Allocazione statica di tipi di dati opachi su SO.


Ou grazie per aver chiarito la confusione dei nomi da parte mia, mal regolerò l'OP. Stavo pensando di impilare l'overflow, ma ho deciso di scegliere come target i programmatori integrati.
L. Heinrichs,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.