Die libc-Streams, repräsentiert durch FILE-Pointer, können nicht nur mit fopen
erzeugt werden. Eine ganze Reihe an Funktionen erstellt ebenfalls diese Streams, die dann auch mit den stdio-Funktionen wie z.B. fprintf
genutzt werden können.
fdopen
FILE *fdopen(int fd, const char *mode);
Mit fdopen erstellt man aus einem Unix-Filedescriptor ein FILE. Diese Funktion steht unter jedem POSIX-kompatiblen Betriebsystem zur Verfügung.
fmemopen
FILE *fmemopen(void *buf, size_t size, const char *mode);
Diese Funktion erstellt aus einem Buffer ein Stream. Der Buffer hat eine feste Größe, und gelangt der Stream an dessen Ende, wird nicht weiter geschrieben. Diese Funktion wurde durch die glibc eingeführt. Sie ist mitlerweile zwar im Posix-Standard, aber noch nicht überall verfügbar.
open_memstream
FILE *open_memstream(char **bufp, size_t *sizep);
Wie bei fmemopen
erhält man mit dieser Funktion einen Stream für den Zugriff auf einen Buffer, allerdings wird dieser dynamisch mit malloc erstellt. Wenn nötig wird der Buffer auch automatisch vergrößert. Auch diese Funktion ist mitlerweile Posix-Standardisiert.
Beispiel:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
char *buf;
size_t len;
FILE *f = open_memstream(&buf, &len);
fprintf(f, "test\n");
fprintf(f, "%d\n", 5);
fprintf(f, "%s\n", "hello world");
fclose(f);
printf("%s", buf);
free(buf);
return 0;
}
funopen (BSD)
FILE *
funopen(void *cookie, int (*readfn)(void *, char *, int),
int (*writefn)(void *, const char *, int),
off_t (*seekfn)(void *, off_t, int), int (*closefn)(void *));
Die BSDs haben die sehr mächtige Funktion funopen, die einen Stream anhand eigener I/O-Funktionen erstellt. Die I/O-Operationen, die durch die libc-Funktionen, wie z.B. fgetc, fputc, fprintf, usw., erzeugt werden, werden dann an die eigenen I/O-Funktionen, die man funopen übergeben hat, weitergeleitet. So kann man für praktisch alles was man sich vorstellen kann ein FILE*
-kompatibles Interfaces erstellen.
Diese Funktion ist nur unter den BSD-Betriebsystemen verfügbar. NetBSD hat sogar die noch mächtigere Funktion funopen2.
fopencookie (glibc)
FILE *fopencookie(void *cookie, const char *mode,
cookie_io_functions_t io_funcs);
Die glibc folgt dem BSD-Vorbild, leider waren sie natürlich nicht in der Lage, das bereits bestehende Interface zu übernehmen. Trotzdem macht fopencookie genau das gleiche wie funopen
. Man erzeugt einen Stream mit eigenen I/O-Funktionen. Der Unterschied ist hier, dass die Funktionspointer nicht einzelnd übergeben werden, sondern es wird eine Struct, die die Pointer enthält, übergeben.
Beispielcode ohne großartige Erklärung.
stdoutfilter.c
In dem Programm wird die Ausgabe von stdout durch ein Log-Prefix in jeder Zeile ergänzt.
Um in C ein externes Programm auszuführen, und dabei auf die Standardein- und Ausgabe des Programms zugreifen, muss man dies per Hand mit ein paar Syscalls machen. Es gibt zwar auch die Funktion popen, aber damit kann man nur in stdin des Programms schreiben, oder stdout lesen.
Programme werden in der Unixwelt gestartet, in dem mit fork der aktuelle Prozess kopiert wird. In dem neuen Kind-Prozess wird dann mit exec das gewünschte Programm ausgeführt.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char **argv) {
pid_t pid = fork();
if(pid == 0) {
// child
return execl("/usr/bin/echo", "echo", "hello world");
} else if(pid > 0) {
// parent
int status = -1;
waitpid(pid, &status, 0); // wait for child termination
} else {
perror("fork");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Dies ist ein einfaches Grundgerüst, um ein Programm zu starten. Jetzt müssen wir noch stdin, stdout und stderr im Kind-Prozess umleiten. Dafür werden 3 Pipes benötigt, die vor dem Fork erstellt werden müssen.
int newin[2];
int newout[2];
int newerr[2];
if(pipe(newin) || pipe(newout) || pipe(newerr)) {
perror("pipe");
return EXIT_FAILURE;
}
Der Syscall pipe liefert uns 2 File-Deskriptoren, aus dem 1. im Array kann gelesen und in den 2. geschrieben werden. Nach dem Fork stehen die Pipes in beiden Prozessen zur Verfügung. Was jetzt noch fehlt ist, dass im Kind-Prozess die Filedeskriptoren für stdin, stdout und stderr auf die Pipe-Filedeskriptoren zeigen. Dies erledigen wir mit dup2. Dies kopiert unsere Pipe-Filedeskriptoren und ersetzt damit stdin, stdout und stderr im Kind-Prozess.
// child
// we need stdin, stdout and stderr refer to the previously
// created pipes
if(dup2(newin[0], STDIN_FILENO) == -1) {
perror("dup2");
return EXIT_FAILURE;
}
if(dup2(newout[1], STDOUT_FILENO) == -1) {
perror("dup2");
return EXIT_FAILURE;
}
if(dup2(newerr[1], STDERR_FILENO) == -1) {
perror("dup2");
return EXIT_FAILURE;
}
Danach würde im Kind-Prozess ein einfaches printf("Hello World!\n");
in unserer Pipe landen und könnte im Parent aus der Pipe gelesen werden.
Jetzt fehlt nur noch eine Kleinigkeit. Durch fork wurden alle Filedeskriptoren kopiert. Einige davon müssen geschlossen werden, weil sonst das Ende der Streams nicht erreicht werden könnte.
Im Kind-Prozess:
close(newin[1]);
Im Eltern-Prozess:
close(newout[1]);
close(newerr[1]);
Den kompletten Beispielcode gibt es hier. Es wird dort das Programm bc aufgerufen, die Standardeingabe davon befüllt und danach die die Ausgabe des Programms gelesen.
Vor kurzem hab ich versucht ein kleines Stück Software auf einer exotischen RISC-Plattform zum Laufen zu bringen, doch bei der Ausführung crashte das Programm. Der Grund war Code der in etwa das machte:
int *intptr = (int*)(str + 15);
Um von einer bestimmten Stelle aus einem Speicherbereich ein int
zu lesen wurde ein char*
verschoben und zu einem Integer-Pointer gecasted. Beim anschließenden Zugriff auf diesen Pointer stürzte das Programm ab. Der Grund ist, dass nicht alle CPU-Architekturen beliebig wahllose Speicherzugriffe erlauben, sondern nur auf richtig ausgerichtete Adressen, die ein Vielfaches von n (meistens 4) sind. Siehe Data structure alignment
Jetzt sind allerdings die verbreitetsten CPU-Architekturen x86 und ARM, und die kommen auch mit misaligned Memory klar. Warum sollte man das dann überhaupt beachten? Weil auch wenn es auf diesen CPUs fehlerfrei funktioniert, hat es zumindestens Performance-Auswirkungen. Es werden dann nämlich einfach 2 Speicherzugriffe gemacht. Ich habe ein interessanten Blogpost zu dem Thema gefunden, in dem jemand ein Performance-Test gemacht hat. Das Ergebnis ist, dass im schlimmsten Fall die Speicherzugriffe doppelt so lange dauern.
Wenn man jetzt aber doch auf nicht-ausgerichteten Speicher zugreifen will, kann man immer noch memcpy nutzen, denn das kann die Adresse berichtigen und dann größtenteils optimierte Speicherzugriffe machen.
Der Syscall stat wird benutzt um an Informationen wie Dateigröße oder Änderungsdatum einer Datei zu gelangen. Die Funktion erwartet als Argumente nur einen Dateipfad und einen Buffer. Daneben gibt es noch die Variante fstat, die statt eines Dateipfades einen Filedescriptor erwartet, und fstatat, die den Filedescriptor eines geöffneten Verzeichnisses und einen Pfad relativ zu dem Verzeichnis erwartet.
int stat(const char *restrict path, struct stat *restrict buf);
int fstat(int fildes, struct stat *buf);
int fstatat(int fd, const char *restrict path,
struct stat *restrict buf, int flag);
Mich hat jetzt ein Performancevergleich zwischen den Funktionen interessiert. Zum einen für den Fall, dass man für jede Datei in einem Verzeichnis stat
aufrufen will. Hier würde sich der Einsatz von fstatat
anbieten. Der andere Fall wäre, wenn man eine Datei öffnen und stat-en will. Um dies zu testen hab ich ein primitives Programm geschrieben, dass man hier findet. Das Programm erwartet als Argument einen Pfad zu einem Verzeichnis, welches es zunächst ließt. Danach führt es 4 Tests durch und misst für jeden die Zeit:
- test_stat: Für jede Datei des Verzeichnisses wird
stat
aufgerufen.
- test_fstatat: Für jede Datei wird
fstatat
aufgerufen.
- test_open_stat: Jede Datei wird mit
open
geöffnet und danach noch stat
aufgerufen.
- test_fstat: Jede Datei wird geöffnet und
fstat
aufgerufen.
Ein kleiner Test unter Linux ergab:
test 195 files:
test_stat
time: 496386 ns
test_fstatat
time: 246039 ns
----------------------------------------
test_open_stat
time: 1106593 ns
test_open_fstat
time: 885549 ns
Ähnliche Ergebnisse konnte ich auch unter FreeBSD und Solaris, mit Festplatten und SSDs, reproduzieren. Der Vergleich zwischen test_stat und test_fstatat zeigt, dass fstatat
deutlich schneller ist. In beiden Tests wird auch nur pro Datei jeweils ein Syscall aufgerufen. Bei test_open_stat und test_open_fstat wird in beiden Fällen zunächst open
benutzt um die Datei zu öffnen und es zeigt sich, dass in diesem Fall fstat
auch schneller ist als stat
, allerdings fällt der Unterschied hier nicht so sehr ins Gewicht.
Kommentare
Rudi | Artikel: Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45
Peter | Artikel: XNEdit - Mein NEdit-Fork mit Unicode-Support
Damit wird Nedit durch XNedit ersetzt.
Danke!
Olaf | Artikel: XNEdit - Mein NEdit-Fork mit Unicode-Support
Anti-Aliasing hängt von der Schriftart ab. Mit einem bitmap font sollte die Schrift klassisch wie in nedit aussehen.
Einfach unter Preferences -> Default Settings -> Text Fonts nach einer passenden Schriftart suchen.
Peter | Artikel: XNEdit - Mein NEdit-Fork mit Unicode-Support
Mettigel | Artikel: Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45
Ich hatte gedacht, dass der GX-415 im s920 deutlich mehr Dampf hat als der Raspi4.
Mein Thinclient verbraucht mit 16 GB RAM ~11 W idle, das ist das Dreifache vom RP4. Das muss man dem kleinen echt lassen... Sparsam ist er.
Olaf | Artikel: Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45
Ergebnisse von der Ultra 80 wären natürlich interessant, insbesondere im Vergleich mit dem rpi1.
kosta | Artikel: Raspberry Pi1 vs Raspberry Pi4 vs Fujitsu s920 vs Sun Ultra 45
ich hätt hier zugriff auf Ultra-80 4CPU 4GB 2x Elite3D.
RO | Artikel: Benutzt bitte nur noch Unicode!
Sehe ich genauso.