UNIXwork

WTF C Teil 3

16. January 2020

Aus der beliebten Serie WTF C:

bool trigraphs_are_available() {
    // Are trigraphs available??/
    return false;
    return true;
}

Was passiert hier? Zwei return-Statements hintereinander ergeben doch keinen Sinn! Kann die Funktion jemals true zurückgeben?

Auch dieses mal handelt es sich um standardkonformes C und es gibt auch Fälle, in denen die Funktion true liefert. Der Trick ist hier, dass im Kommentar etwas versteckt ist, nämlich am Ende ein Trigraph. Es ist nämlich in C möglich, manche Sonderzeichen durch alternative Zeichenketten auszudrücken. Am Ende des Kommentars steht der Trigraph ??/, der zu einem Backslash aufgelöst wird und ein Backslash am Ende eines einzeiligen Kommentars erweitert diesen auf die nächste Zeile. Wenn also der Compiler Trigraphen unterstützt, sind die ersten beiden Zeilen der Funktion ein Kommentar und übrig bleibt nur return true.

Gefunden habe ich das Beispiel hier.

Autor: Olaf | 0 Kommentare | Tags: c, wtf

Meine Lektionen aus der Entwicklung von dav 1.3

23. December 2019

Die Entwicklung von dav 1.3 ist ganz und gar nicht so verlaufen wie ich mir das vorgestellt hatte. Über ein Jahr später als geplant, konnte ich das Release dann vor kurzem endlich fertig stellen. Ich entwickel das jetzt allerdings im meiner Freizeit, daher variiert es natürlich, wie viel Zeit ich in die Entwicklung stecke. Trotzdem muss ich zugeben, dass die Zeit, die ich investiert habe, nicht unbedingt optimal genutzt wurde.

Schauen wir uns zunächst ein paar Zahlen der Entwicklung an.

version  date        loc          new-loc  n-month  lines/month
---------------------------------------------------------------
begin    2012-11-30      0 lines  0        0        0
1.0.0    2017-08-13  14633 lines  14633    57       256
1.1.0    2017-10-07  15416 lines  783      2        391      
1.2.0    2018-06-26  20465 lines  5049     8        631
1.3.0    2019-12-15  29743 lines  9278     18       515

Für die erste Version von dav habe ich natürlich relativ lange gebraucht. Es ist vermutlich deutlich einfacher, an eine bestehende Software, die man auch noch gut kennt, neues Zeug ranzubasteln, als etwas komplett neues aus dem Boden zu stampfen.

Bei der Entwicklung von dav 1.2 war ich am produktivsten. Aber wenn man nur nach lines/month geht, dann war dav 1.3 eigentlich kein Desaster (im Vergleich zu den anderen Releases). Was man aber sieht ist, dass sich die Code-Basis drastisch vergrößert hat. Viel zu viele neue Features haben den Weg in das Release gefunden. Genau das ist eines der Probleme.

Das größte Ärgernis dabei war, dass ich zwar relativ zügig neue Features implementiert hatte, aber sobald die minimal irgendwie funktioniert haben, bin ich zum nächsten neuen Feature übergegangen. Am Ende hatte ich haufen Neues, das aber nicht gut funktionierte.

Dazu kam auch ein größeres Refactoring der dav-sync Codebasis. Dieses hat natürlich bereits bestehende Funktionalität, die fehlerfrei lief, auch etwas zerstört.

Am Ende war ich Monatelang damit beschäftig, massenweise Tests zu schreiben, um überhaupt wieder Vertrauen in den Code zu entwickeln. Dies war jedoch recht erfolgreich, die Grundfunktionalität dürfte stabiler als vorher sein.

Was ich hoffentlich daraus gelernt habe: Dinge zuende entwickeln. Keine halben Sachen liegen lassen. Und am besten schließt man die Entwicklung eines neuen Features ab, in dem man diverse Tests dafür geschrieben hat, die alle problemlos durchlaufen.

Auch sollte man sich nicht zu viel auf einmal vornehmen. Ich hoffe ich kann den Spruch Release early, release often in Zukunft mehr anwenden. Das hat durchaus seine Vorteile.

Autor: Olaf | 0 Kommentare | Tags: dav, c

Kind-Prozesse erstellen mit posix_spawn

1. December 2019

Ich hatte mal eine Anleitung dazu geschrieben, wie mittels fork und exec Kind-Prozesse erstellt und Programme ausgeführt werden. In diesem Artikel möchte ich die Funktion posix_spawn vorstellen, die im Prinzip das gleiche kann und dabei ein paar Vorteile hat.

In Unix werden traditionell Programme ausgeführt, in dem der aktuelle Prozess geforked, also dupliziert, wird und im neuen Kind-Prozess wird ein anderes Programm-Image mittels exec geladen.

Das Problem an der Sache ist, dass fork mehr oder weniger Overhead haben kann, je nach Betriebsystem. Linux ist da recht effizient, es werden aber immer noch die Page Tables des Parent kopiert. Dazu kommt, dass es auch etwas umständlicher zu programmieren ist, als nur eine einzige Funktion aufzurufen.

Daher wurde irgendwann die Funktion posix_spawn eingeführt:

int posix_spawn(pid_t *pid, const char *path,
       const posix_spawn_file_actions_t *file_actions,
       const posix_spawnattr_t *attrp,
       char *const argv[], char *const envp[]);

Um z.B. das Programm /bin/echo in einem separaten Prozess auszuführen, reicht folgender Code:

char *args[3];
args[0] = "echo";
args[1] = "Hello World";
args[2] = NULL;

pid_t pid; // child pid
posix_spawn(&pid, "/bin/echo", NULL, NULL, args, NULL);

Das ist ein sehr einfaches Beispiel. Was man eigentlich noch bräuchte, ist eine Möglichkeit, die Filedescriptoren für stdin, stdout und stderr zu modifizieren. Hierfür gibt es den Parameter file_action. Um den zu verstehen, sollte man sich noch mal anschauen, wie Programme mittels fork und exec ausgeführt werden, daher noch mal eine kleine Zusammenfassung meines Artikels dazu.

Stdin, stdout und stderr sind Filedescriptoren, die immer die Nummern 0, 1 und 2 haben. Mit der Funktion dup2 können wir einen Filedescriptor duplizieren und der Kopie eine exakte Nummer zuweisen. Was man also machen muss ist, vor dem Erstellen des neuen Prozesses erstmal neue Filedescriptoren für stdin/stdout/stderr zu erstellen (z.B. mit pipe oder open) und nach dem Erstellen des Kind-Prozesses aber noch vor exec werden mittels dup2 die Filedescriptoren auf die Positionen 0, 1 und 2 gelegt.

Der file_actions Parameter bei posix_spawn ist genau dafür gedacht. Diesem können Aktionen wie dup2 oder auch close und open zugeordnet werden, die dann nach dem Erstellen des Kind-Prozesses verarbeitet werden.

Zuerst muss file_actions hierfür initialisiert werden:

posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);

Danach können Actions zugewiesen werden:

// stderr im Kind-Prozess schließen
posix_spawn_file_actions_addclose(&actions, 2);

// stdout in die Datei out.txt umleiten
posix_spawn_file_actions_addopen(&actions, 1, "out.txt", O_CREAT|O_WRONLY, 0644);

Nach dem Ausführen von posix_spawn sollte der Speicher wieder freigegeben werden:

posix_spawn_file_actions_destroy(&actions);

Ich habe auch ein ausführliches Beispiel geschrieben, in dem die Ausgabe in eine Pipe mit Hilfe von posix_spawn_file_actions_adddup2 umgeleitet wird, die dann im Parent ausgelesen wird.

Autor: Olaf | 0 Kommentare | Tags: unix, c

Linkdump

13. March 2019
Autor: Olaf | 0 Kommentare | Tags: links, shell, bash, c, sun

Idee für Struct-Serialisierung

1. May 2018

Eine kleine semi-sinnvolle Idee, wie man Serialisierung in C realisieren kann, die ich einfach mal teilen möchte. Wer hingegen nach einer ordentlichen Serialisierungsmethode sucht, der sollte besser Codegeneratoren verwenden, wie z.B. Google Protocol Buffers für C++ oder protobuf-c für C.

Meine Idee hingegen basiert darauf, dass man seine Daten gar nicht erst serialisieren muss, weil sie bereits seriell im Speicher liegen.

Der triviale Fall

Structs, die keine Pointer enthalten, kann man direkt so wie sie im Speicher liegen in eine Datei schreiben oder aus ihr lesen.

struct mydata {
    int a;
    int b;
    float c;
};

...

struct mydata data;
data.a = 12;
data.b = 14;
data.c = 13.37;

write(fd, &data, sizeof(struct mydata));

...

struct mydata d2;
read(fd2, &d2, sizeof(struct mydata);

Das Konzept

Doch was machen wir, wenn eine Struct Pointer auf Structs oder andere Daten (z.B. Strings) enthält? Wenn man nur die Adresse speichert, bringt einem das beim Deserialisieren nichts, denn es fehlen nicht nur die Daten, der Pointer ist vielleicht auch ungültig.

Die Idee, die ich nun hatte, ist, dass die ganze Objekt-Hierarchie sich nur in einem festgelegten großen Speicherblock befinden darf. Die Structs enthalten dann auch keine echten Pointer mehr, sondern nur noch eine relative Adresse zu diesem Speicherblock, also quasi ein Index.

Hier ein kleines Beispiel, wie ich mir das vorstelle. Angenommen man hat folgende Structs:

typedef struct {
    int x;
    int y;
    int z;
} ChildObj;

typedef intptr_t ChildObjPtr;

typedef struct {
    ChildObjPtr child1;
    ChildObjPtr child2;
} RootObj;

Zuerst alloziert man genug Speicher, der (hoffentlich) für alle Daten reicht.

char *block = malloc(EVERYTHING_YOU_NEED);
size_t i = 0;

Will man jetzt Speicher für eine Struct reservieren, nutzt man dann nicht mehr malloc, sondern man greift auf den vorher reservierten Speichier zurück:

RootObj *root = (RootObj*)(block+i);
i += sizeof(RootObj);

ChildObj *c1 = (ChildObj*)(block+i);
i += sizeof(ChildObj);

ChildObj *c2 = (ChildObj*)(block+i);
i += sizeof(ChildObj);

c1->x = 1;
c1->y = 2;
c1->z = 3;
c2->x = 10;
c2->y = 20;
c2->z = 30;

Im RootObj möchte man jetzt Verweise auf die beiden anderen Structs haben. Hierfür müssen wir nur aus den Pointern c1 und c2 die Adressen relativ zu dem Pointer block berechnen.

root->child1 = (ChildObjPtr) (char*)c1 - block;
root->child2 = (ChildObjPtr) (char*)c2 - block;

Im Speicher liegen jetzt hintereinander die Daten. Zuerst RootObj und dann zwei mal ChildObj. Das kann man so direkt in Dateien schreiben und auch wieder daraus lesen.

Ein Problem ist, dass ich nicht direkt auf die Felder child1 und child2 zugreifen kann. Also statt root->child1 müsste man wieder die Adresse umrechnen:

((ChildObj*)(block + root->child1))->x = 5;

Optische Verbesserungen

Das ganze ist natürlich ganz schön hässlich. Und unpraktisch. Man kann kein malloc mehr verwenden und -> geht auch nicht. Dafür hat man viel Schreibarbeit und Rumgerechne, was viel Potential für Fehler hat.

Man könnte sich aber einen eigenen malloc-ähnlichen Memory-Allocator schreiben, der ein ähnliches Interface für Speicherverwaltung zur Verfügung stellt, aber auf einem zusammenhängenden Speicherbereich operiert. Die UCX-Bibliothek enthält eine einfache Implementierung hierfür.

Und für das Arbeiten mit den relativen Pointern kann man sich ein paar Makros schreiben:

// stores an absolut pointer as a relative pointer
#define SPTR(root, ptr) (intptr_t)((char*)ptr - (char*)root)

// converts a relative pointer to an absolut pointer
#define CPTR(root, relptr) (void*)((char*)root + relptr)

Mit den Makros sieht der Code von oben dann etwas schöner aus:

root->child1 = SPTR(root, c1); // still not as nice as root->child1 = c1  :'(

ChildObj* child = CPTR(root, root->child1);

Natürlich nicht ganz perfekt, schöner geht das aber leider nicht.

Probleme

Das Konzept ist, dass einfach der ganze Speicherblock beim Serialisieren irgendwo hin geschrieben wird, oder zumindestens so viel davon, wie belegt ist. Um aber halbwegs komformabel zu arbeiten benötigt man ein malloc/free ähnliches Interface zur Verwaltung seines Speicherblocks. In der Praxis kommt es häufig vor, dass sich Daten auch ändern. Speicher wird freigegeben und neuer alloziert. Dabei würden dann im Speicher Lücken entstehen. Beim Transferieren des Speicherblocks würden somit viele unnötige Daten übertragen werden.

Ein weiteres Problem ist, dass man vorher wissen muss, wie viel Speicher man benötigt. Diesen müsste man auch von Anfang an komplett allozieren, selbst wenn man erstmal nur wenig benötigt. Und falls er nicht reicht kann man ganz aufgeben.

Das Konzept kann aber abgewandelt werden, um die Probleme zu lösen. Wir können auch einen eigenen Allocator schreiben, der beliebig den Speicher verwaltet und nicht nur einen großen Block. Und anstatt beim serialisieren einfach nur einen großen Block zu schreiben, brauchen wir einen richtigen Serialisierer für den Allocator, der also alle vom Allocator ausgestellten Speicherbereiche irgendwie serialisiert, so dass genau diese Speicherbereiche in der Form wiederhergestellt werden können. Freie Speicherbereiche können dabei dann natürlich weggelassen werden.

Die Idee mit den relativen Pointern funktioniert dann natürlich nicht mehr, aber statt Pointer auf Kind-Objekte könnte man immer noch Integer verwenden, die dann vom Allocator für einen Lookup des echten Pointers verwendet werden. Man könnte daher immer noch mit den beiden Makros arbeiten, nur dass diese dann komplexere Logik als eine einfache Addition enthalten würden.

Zusammenfassung

Anstatt also eine Struct bzw ein Objekt mit allen ihren Membern zu serialisieren, serialisiert man einen Allocator, der für die Speicherreservierung der Objekte verwendet wurde. Objekte enthalten keine direkten Pointer auf die Speicheradresse anderer Objekte, sondern nur einen Index (oder ein Integer mit anderer Bedeutung).

Ist dieses Konzept sinnvoll? Ich weiß es nicht. Jedenfalls hab ich nicht vor, es irgendwo produktiv anzuwenden. Deswegen habe ich mir auch ein komplettes Code-Beispiel gespart.

Autor: Olaf | 0 Kommentare | Tags: c
Weiter