Frage vorweg: kann ich in einem Template Klassen und einfache Typen
unterschiedlich behandeln?
Ich habe in Mbed eine Mail Klasse, diese kombiniert eine Queue mit einem
Memorypool. Zweck ist z.B. Daten aus einer ISR oder einem Sendethread in
einen Verarbeitungsthread zu transportieren, siehe auch das Beispiel am
Ende der Doku auf:
https://os.mbed.com/docs/mbed-os/v6.9/apis/mail.html
Das funktioniert auch solange T z.B. eine Datenstruktur wie im Beispiel
ist. Wenn T eine Klasse ist, dann kompiliert das zwar fehlerfrei, aber
der Konstruktor von T wird nicht aufgerufen und Classmember sind nicht
initialisert.
Mail benutzt einen Memorypool für Elemente gleicher Größe, im
Konstruktor sieht man das nur Speicher geholt wird aber keine
Konstruktoren für die Elemente aufgerufen werden:
static_assert(pool_sz>0,"Invalid memory pool size. Must be greater than 0.");
4
public:
5
/** Create and Initialize a memory pool.
6
*
7
* @note You cannot call this function from ISR context.
8
*/
9
MemoryPool()
10
{
11
memset(_pool_mem,0,sizeof(_pool_mem));
12
osMemoryPoolAttr_tattr={0};
13
attr.mp_mem=_pool_mem;
14
attr.mp_size=sizeof(_pool_mem);
15
attr.cb_mem=&_obj_mem;
16
attr.cb_size=sizeof(_obj_mem);
17
_id=osMemoryPoolNew(pool_sz,sizeof(T),&attr);
18
MBED_ASSERT(_id);
19
}
Hier könnte ich einbauen das Elemente initialisiert werden:
1
for(uint32_ti=0;i<pool_sz;i++){
2
T*pT=newT;
3
//pT in Array speichern
4
}
so ist das natürlich nicht gut weil da nochmal Speicher geholt würde.
Ich müsste ein new haben dem ich eine Speicheradresse mitgeben kann oder
den Konstruktor explizit für das noch nicht initialisierte Objekt
aufrufen können. Würde mir etwas aus der C++ Standard Library helfen?
Und kann man im template die Konstruktoren nur für Klassen aufrufen und
tote Strukturen einfach so allozieren wie es der Code jetzt macht?
Edit:
std::construct_at() habe ich gefunden, das ist mit C++20 aber noch zu
modern.
Johannes S. schrieb:> Frage vorweg: kann ich in einem Template Klassen und einfache Typen> unterschiedlich behandeln?
Ja, auch das geht: nennt sich old-school type-trait oder etwas moderner
eine Meta-Funktion. In diesem Fall: std::is_fundamental<>
Das placement new hat funktioniert, es hat aber trotzdem wieder geknallt
:(
Der Memorypool vom RTX verwaltet seine freien Blocks in den _pool_mem
Daten. Die Blockzeiger überschreibe ich wenn ich den Speicher mit den
Konstruktoren manipuliere. Da müsste ich die blocksize um einen Pointer
vergrößern, das scheint mir aber auch fragil zu sein. Alternative wäre
wohl eine std::list.
Die type traits habe ich auch entdeckt, damit könnte man zumindest in
der Mail Klasse einen Fehler werfen wenn die mit einer class/struct
benutzt wird.
Johannes S. schrieb:> Der Memorypool vom RTX verwaltet seine freien Blocks in den _pool_mem> Daten. Die Blockzeiger überschreibe ich wenn ich den Speicher mit den> Konstruktoren manipuliere.
Warum?
> Da müsste ich die blocksize um einen Pointer vergrößern, das scheint mir aber> auch fragil zu sein. Alternative wäre wohl eine std::list.
Tut mir leid, irgendwie verstehe ich nicht, was du meinst.
das ist der MemoryPool wie im ersten Post:
https://github.com/ARMmbed/mbed-os/blob/master/rtos/include/rtos/MemoryPool.h
da wird in der Initialisierung osMemoryPoolNew() aufgerufen, und das
legt eine verkettete Liste in den _pool_mem Daten an, also an die ersten
4 Byte wird ein Pointer auf den nächsten Block geschrieben.
https://github.com/ARMmbed/mbed-os/blob/73896715e6202e187ba624fc3853299e73390f19/cmsis/CMSIS_5/CMSIS/RTOS2/RTX/Source/rtx_mempool.c#L66-L73
und diese Initialisierung mache ich platt wenn ich danach den Speicher
mit den Objektdaten überschreibe. Ist also nicht richtig C++ kompatibel
bzw. man sollte den Speicher wohl in Ruhe lassen.
Wenn dieses Verhalten so bleibt könnte ich den Speicher pro Block auf
sizeof(void*)+sizeof(T) vergrößern und mein Objekt damit hinter den
Zeiger legen.
Mein Anwendungsbeispiel hatte ich hier angehängt:
Beitrag "Re: Einstieg(scontroller) in Ethernet Kommunikation mit STM32"
Da hatte ich mich über die Hardfaults gewundert und die MidiMessage so
geändert das die auch ohne Initialisierung funktioniert. Was aber nur
ein kurieren von Symptomen war. Eine Templateklasse sollte schon mit
allen Typen funktionieren oder einen Fehler werfen wenn sie nicht
geeignet ist.
Johannes S. schrieb:> da wird in der Initialisierung osMemoryPoolNew() aufgerufen, und das> legt eine verkettete Liste in den _pool_mem Daten an, also an die ersten> 4 Byte wird ein Pointer auf den nächsten Block geschrieben.
Ok, aber das ist doch ein Implementierungsdetail des Memory-Pools. Warum
kommst du damit überhaupt in Berührung? osMemoryPoolAlloc() bzw.
try_alloc() gibt dir nachher einen Zeiger auf einen Speicherblock der
Größe sizeof(T) zurück, den du frei nach belieben beschreiben kannst.
Dieser Pool-interne Zeiger auf den nächsten Block ist außerhalb davon.
Sofern dein Konstruktor keinen Fehler hat, sollte der dort also nie was
hinschreiben.
Rolf M. schrieb:> Johannes S. schrieb:> Ok, aber das ist doch ein Implementierungsdetail des Memory-Pools. Warum> kommst du damit überhaupt in Berührung? osMemoryPoolAlloc() bzw.> try_alloc() gibt dir nachher einen Zeiger auf einen Speicherblock der> Größe sizeof(T) zurück, den du frei nach belieben beschreiben kannst.
ja, das ist richtig. Es funktioniert ja auch, aber nur solange ich keine
Objekte als Mailtype habe. Es gibt Tests für Mail und Memorypool, die
verwenden aber nur Strukturen wie z.B.:
1
typedefstruct{
2
uint16_tdata;
3
uint8_tthread_id;
4
}mail_t;
5
6
Mail<mail_t,64>mailbox_struct;
In diesem Fall wird ein Block mit try_alloc() geholt und beschrieben,
ok. Vor dem ausbuchen mit alloc wird der interne Zeiger in eine
Verwaltungsstruktur gerettet und ich kann mit dem Speicher machen was
ich will. Beim Zurückgeben mit free() wird wieder ein Zeiger
eingetragen.
Jetzt kommt Johannes und möchte ein Objekt Message als
Mailboxtyp/Memorypooltyp benutzen. Die Message kann größer sein und holt
sich im Konstruktor Speicher vom Heap:
1
classeMessage{
2
Message::Message(){
3
data=newuint8_t[256];
4
length=0;
5
};
6
7
Message::from_raw(uint8_t*source,uint32_tlen){
8
for(uint8_ti=0;i<len;i++){
9
data[i]=source[i];
10
};
11
12
uint8_t*data;
13
uint16_tlength;
14
}
15
16
Mail<Message,64>mailbox_objects;
Der Compiler versteht mich, alles gut. Aber jetzt liefert mir
try_alloc() ein nicht initialisiertes Message Objekt. Nachträglich den
Konstruktor aufrufen ist nicht gut, new im ISR Kontext knallt und da
möchte man so eine teure Operation auch nicht haben.
Also im oben gezeigten MemoryPool Konstruktor den gesamten Pool mit dem
placement new behandeln und alle Objekte sind initialisiert. Aber das
beißt sich jetzt mit der Initialsierung die osMemoryPoolNew()
durchgeführt hat weil die Daten implizit auch die Verwaltung enthalten.
Man könnte die Verwaltungszeiger in ein extra Array legen, das braucht
dann aber mehr Speicher wenn die Blöcke keinen Konstruktor brauchen. Und
die RTX Quellen möchte ich nicht verändern, das ist eine 3rd Party
Komponente die beim Update ja wieder modifiziert werden müsste. Sowas
möchte man als PR sicher nicht haben.
Also bleibt eigentlich nur für den Fall 'T hat Konstruktor' die Variante
die Blockgröße um den Platz für den Verwaltungszeiger zu vergrößern
damit dieser und die initialisierten Objektdaten überleben.
Ich hoffe das war jetzt nicht zu kompliziert. Das man initialisierte
Objekte von 'T* try_alloc()' bekommt halte ich für RAII konform und
wichtig. Alternativ könnte man einen Fehler melden wenn Mail<> einen
nicht unterstützten Typ mit Konstruktor bekommt (kann ja auch eine
struct sein). Aber für den Fall müsste man das Rad neu erfinden,
Mail/MemoryPool bietet ja schon eine Menge incl. Timeout Behandlung.
So sieht mein MemoryPool Konstruktor mit placement new jetzt aus, aber
schreddert jetzt wie gesagt die Verzeigerung der freigen Blöcke:
Johannes S. schrieb:> Nachträglich den Konstruktor aufrufen ist nicht gut, new im ISR Kontext> knallt und da möchte man so eine teure Operation auch nicht haben.
Ok, so weit klar.
> Also im oben gezeigten MemoryPool Konstruktor den gesamten Pool mit dem> placement new behandeln und alle Objekte sind initialisiert.
Aber dort sind das ja in dem Sinne noch keine Objekte, sondern einfach
nur ein Block "roher" Speicher, der intern für den MemoryPool ist. Was
du darin als Message-Objekt initialisieren kannst, weißt du erst, wenn
du try_allocate() aufgerufen und den entsprechenden Zeiger
zurückbekommen hast, der auf den für dein Message-Objekt vorgesehenen
Speicherblock zeigt.
> Aber das beißt sich jetzt mit der Initialsierung die osMemoryPoolNew()> durchgeführt hat weil die Daten implizit auch die Verwaltung enthalten.
Ja, ok, jetzt verstehe ich. Wie gesagt ist das ja auch kein Speicher, in
dem du einfach frei rumschreiben solltest.
> Man könnte die Verwaltungszeiger in ein extra Array legen, das braucht> dann aber mehr Speicher wenn die Blöcke keinen Konstruktor brauchen.
Wenn du kein Problem damit hast, die Objekte alle gleich von Anfang an
initialisiert zu haben, warum hast du dann überhaupt einen Memorypool
und nicht einfach ein Array aus Message-Objekten?
> Also bleibt eigentlich nur für den Fall 'T hat Konstruktor' die Variante> die Blockgröße um den Platz für den Verwaltungszeiger zu vergrößern> damit dieser und die initialisierten Objektdaten überleben.
Damit machst du dich halt vom internen Aufbau der verwendeten Komponente
abhängig. So richtig sauber finde ich das nicht.
Rolf M. schrieb:> Wenn du kein Problem damit hast, die Objekte alle gleich von Anfang an> initialisiert zu haben, warum hast du dann überhaupt einen Memorypool> und nicht einfach ein Array aus Message-Objekten?
Der Memory Pool hat ja zusätzlich die Verwaltung von benutzten/freien
Blöcken, das ist schonmal praktisch und mit der Verzeigerung auch
schnell und effizient. Das habe ich erst so gesehen als ich den Fehler
gesucht habe, da war erst mal der sportliche Ehrgeiz zu verstehen was da
passiert. Als use case passte das Beispiel 'Daten in thread/ISR erzeugen
und in einem anderen verarbeiten' ja auch zunächst.
Der MemoryPool hat ja noch die Eigenschaft das mehrere Blöcke
gleichzeitig ausgebucht sein dürfen und in beliebiger Reihenfolge
zurückgegeben werden dürfen. Für den vereinfachten Fall in der Anwendung
mit Fifo und sequentieller Verarbeitung sind wir ja beim beliebten
Circular Buffer. Die fertigen RTOS Komponenten haben da noch zusätzlich
die EventFlags und blockierendes Warten ohne Polling ist damit einfach.
Rolf M. schrieb:> Damit machst du dich halt vom internen Aufbau der verwendeten Komponente> abhängig. So richtig sauber finde ich das nicht.
ja, dünnes Eis. Wobei dieser MemoryPool sich vermutlich nicht mehr
verändern wird, aber mal sehen was die Mbed Mädels und Jungs sagen.