C-Programmierung

Syscall Linux lesen

Syscall Linux lesen
Sie müssen also Binärdaten lesen? Vielleicht möchten Sie aus einem FIFO oder Socket lesen? Sie sehen, Sie können die C-Standardbibliotheksfunktion verwenden, profitieren aber dadurch nicht von den speziellen Funktionen von Linux-Kernel und POSIX. Sie möchten beispielsweise Timeouts verwenden, um zu einer bestimmten Zeit zu lesen, ohne auf Abfragen zurückgreifen zu müssen. Außerdem müssen Sie möglicherweise etwas lesen, ohne sich darum zu kümmern, ob es sich um eine spezielle Datei oder einen Socket oder etwas anderes handelt. Ihre einzige Aufgabe besteht darin, einige binäre Inhalte zu lesen und in Ihre Anwendung zu übertragen. Da glänzt der Read-Systemaufruf.

Eine normale Datei mit einem Linux-Systemaufruf lesen

Der beste Weg, um mit dieser Funktion zu arbeiten, ist das Lesen einer normalen Datei. Dies ist der einfachste Weg, diesen Systemaufruf zu verwenden, und das aus einem bestimmten Grund: Er unterliegt nicht so vielen Einschränkungen wie andere Arten von Streams oder Pipes. Wenn Sie darüber nachdenken, ist das logisch, wenn Sie die Ausgabe einer anderen Anwendung lesen, müssen Sie vor dem Lesen eine Ausgabe bereithalten und warten, bis diese Anwendung diese Ausgabe schreibt.

Zunächst ein wesentlicher Unterschied zur Standardbibliothek: Es gibt überhaupt keine Pufferung. Jedes Mal, wenn Sie die Lesefunktion aufrufen, rufen Sie den Linux-Kernel auf, und dies wird also einige Zeit dauern -‌ es geht fast sofort, wenn du es einmal anrufst, kann dich aber verlangsamen, wenn du es in einer Sekunde tausende Male anrufst. Im Vergleich dazu puffert die Standardbibliothek die Eingaben für Sie. Wenn Sie also read aufrufen, sollten Sie mehr als ein paar Bytes lesen, sondern einen großen Puffer wie einige Kilobytesbyte - außer wenn Sie wirklich wenige Bytes benötigen, zum Beispiel wenn Sie prüfen, ob eine Datei existiert und nicht leer ist.

Dies hat jedoch einen Vorteil: Bei jedem Aufruf von read können Sie sicher sein, dass Sie die aktualisierten Daten erhalten, falls eine andere Anwendung derzeit die Datei ändert. Dies ist besonders nützlich für spezielle Dateien wie die in /proc oder /sys.

Zeit, es Ihnen mit einem echten Beispiel zu zeigen. Dieses C-Programm prüft, ob die Datei PNG ist oder nicht. Dazu liest es die Datei, die in dem von Ihnen im Befehlszeilenargument angegebenen Pfad angegeben ist, und prüft, ob die ersten 8 Byte einem PNG-Header entsprechen.

Hier ist der Code:

#einschließen
#einschließen
#einschließen
#einschließen
#einschließen
#einschließen
#einschließen
 
typedef-Aufzählung
IS_PNG,
ZU KURZ,
INVALID_HEADER
pngStatus_t;
 
unsigned int isSyscallSuccessful(const ssize_t readStatus)
readStatus >= 0 zurückgeben;
 

 
/*
* checkPngHeader prüft, ob das pngFileHeader-Array einem PNG entspricht
* Datei-Header.
*
* Derzeit werden nur die ersten 8 Bytes des Arrays überprüft. Wenn das Array kleiner ist
* als 8 Byte, wird TOO_SHORT zurückgegeben.
*
* pngFileHeaderLength muss die Größe des Arrays enthalten. Jeder ungültige Wert
* kann zu undefiniertem Verhalten führen, z. B. zum Absturz der Anwendung.
*
* Gibt IS_PNG zurück, wenn es einem PNG-Datei-Header entspricht. Wenn es wenigstens
* 8 Byte im Array, aber kein PNG-Header, INVALID_HEADER wird zurückgegeben.
*
*/
pngStatus_t checkPngHeader(const unsigned char* const pngFileHeader,
size_t pngFileHeaderLength) const unsigned char erwartetPngHeader[8] =
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A;
int i = 0;
 
if (pngFileHeaderLength < sizeof(expectedPngHeader))
TOO_SHORT zurückgeben;
 

 
für (i = 0; i < sizeof(expectedPngHeader); i++)
if (pngFileHeader[i] != erwarteter PngHeader[i])
INVALID_HEADER zurückgeben;
 


 
/* Wenn es hier ankommt, entsprechen alle ersten 8 Byte einem PNG-Header. */
IS_PNG zurückgeben;

 
int main(int argumentLength,  char *argumentList[])
char *pngFileName = NULL;
unsigniertes Zeichen pngFileHeader[8] = 0;
 
ssize_t readStatus = 0;
/* Linux verwendet eine Zahl, um eine geöffnete Datei zu identifizieren. */
int pngDatei = 0;
pngStatus_t pngCheckResult;
 
if (ArgumentLänge != 2)
fputs("Sie müssen dieses Programm mit isPng Ihr Dateiname aufrufen.\n", stderr);
EXIT_FAILURE zurückgeben;
 

 
pngFileName = argumentList[1];
pngFile = open(pngFileName, O_RDONLY);
 
if (pngDatei == -1)
perror("Das Öffnen der bereitgestellten Datei ist fehlgeschlagen");
EXIT_FAILURE zurückgeben;
 

 
/* Lesen Sie einige Bytes, um festzustellen, ob die Datei PNG ist. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));
 
if (isSyscallSuccessful(readStatus))
/* Überprüfen Sie, ob die Datei eine PNG-Datei ist, da sie die Daten erhalten hat. */
pngCheckResult = checkPngHeader(pngFileHeader, readStatus);
 
if        (pngCheckResult == TOO_SHORT)
printf("Die Datei %s ist keine PNG-Datei: sie ist zu kurz.\n", pngDateiname);
 
sonst if (pngCheckResult == IS_PNG)
printf("Die Datei %s ist eine PNG-Datei!\n", pngDateiname);
 
sonst
printf("Die Datei %s ist nicht im PNG-Format.\n", pngDateiname);
 

 
sonst
perror("Das Lesen der Datei ist fehlgeschlagen");
EXIT_FAILURE zurückgeben;
 

 
/* Datei schließen… */
if (close(pngFile) == -1)
perror("Das Schließen der bereitgestellten Datei ist fehlgeschlagen");
EXIT_FAILURE zurückgeben;
 

 
pngDatei = 0;
 
EXIT_SUCCESS zurückgeben;
 

Sehen Sie, es ist ein ausgewachsenes, funktionierendes und kompilierbares Beispiel. Zögern Sie nicht, es selbst zusammenzustellen und zu testen, es funktioniert wirklich. Sie sollten das Programm von einem Terminal wie folgt aufrufen:

./isPng Ihr Dateiname

Konzentrieren wir uns nun auf den Leseaufruf selbst:

pngFile = open(pngFileName, O_RDONLY);
if (pngDatei == -1)
perror("Das Öffnen der bereitgestellten Datei ist fehlgeschlagen");
EXIT_FAILURE zurückgeben;

/* Lesen Sie einige Bytes, um festzustellen, ob die Datei PNG ist. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));

Die Lesesignatur ist die folgende (aus den Linux-Man-Pages extrahiert):

ssize_t read(int fd, void *buf, size_t count);

Zuerst stellt das Argument fd den Dateideskriptor dar. Ich habe dieses Konzept in meinem Fork-Artikel ein wenig erklärt.  Ein Dateideskriptor ist ein Int, das eine offene Datei, einen Socket, eine Pipe, ein FIFO, ein Gerät darstellt. Ich werde in einem zukünftigen Artikel ausführlicher darauf eingehen.

Open-Funktion ist eine der Möglichkeiten, Linux mitzuteilen: Ich möchte Dinge mit der Datei in diesem Pfad tun, bitte suchen Sie sie dort, wo sie ist, und geben Sie mir Zugriff darauf. Es wird Ihnen diesen int genannten Dateideskriptor zurückgeben und jetzt, wenn Sie etwas mit dieser Datei machen möchten, verwenden Sie diese Nummer. Vergessen Sie nicht, close aufzurufen, wenn Sie mit der Datei fertig sind, wie im Beispiel.

Sie müssen also diese spezielle Nummer zum Lesen angeben. Dann gibt es das buf-Argument. Sie sollten hier einen Zeiger auf das Array angeben, in dem read Ihre Daten speichert. Schließlich ist count, wie viele Byte es maximal lesen werden.

Der Rückgabewert ist vom Typ ssize_t. Seltsamer Typ, nicht wahr? Es bedeutet "signed size_t", im Grunde ist es ein langes Int. Es gibt die Anzahl der erfolgreich gelesenen Bytes zurück oder -1, wenn ein Problem auftritt. Die genaue Ursache des Problems finden Sie in der von Linux erstellten globalen Variable errno, definiert in . Aber um eine Fehlermeldung zu drucken, ist die Verwendung von perror besser, da es in Ihrem Namen errno druckt.

In normalen Dateien - und nur in diesem Fall – read gibt nur dann weniger als count zurück, wenn Sie das Ende der Datei erreicht haben. Das von Ihnen bereitgestellte buf-Array Muss groß genug sein, um mindestens Bytes zu zählen, oder Ihr Programm kann abstürzen oder einen Sicherheitsfehler verursachen.

Jetzt ist Lesen nicht nur für normale Dateien nützlich und wenn Sie seine Superkräfte spüren möchten - Ja, ich weiß, es ist in keinem Marvel-Comic, aber es hat wahre Kräfte true - Sie möchten es mit anderen Strömen wie Rohren oder Muffen verwenden. Schauen wir uns das an:

Linux-Spezialdateien und Systemaufruf lesen

Die Tatsache, dass read mit einer Vielzahl von Dateien wie Pipes, Sockets, FIFOs oder speziellen Geräten wie einer Festplatte oder einer seriellen Schnittstelle funktioniert, macht es wirklich leistungsstärker. Mit einigen Anpassungen können Sie wirklich interessante Dinge tun. Erstens bedeutet dies, dass Sie buchstäblich Funktionen schreiben können, die an einer Datei arbeiten, und sie stattdessen mit einer Pipe verwenden. Das ist interessant, um Daten zu übertragen, ohne jemals die Festplatte zu berühren, um die beste Leistung zu gewährleisten.

Dies löst jedoch auch Sonderregeln aus. Nehmen wir das Beispiel des Lesens einer Zeile aus dem Terminal im Vergleich zu einer normalen Datei. Wenn Sie read für eine normale Datei aufrufen, benötigt Linux nur wenige Millisekunden, um die angeforderte Datenmenge zu erhalten.

Aber wenn es um Terminal geht, ist das eine andere Geschichte: Nehmen wir an, Sie fragen nach einem Benutzernamen. Der Benutzer gibt seinen Benutzernamen in das Terminal ein und drückt die Eingabetaste. Jetzt befolgst du meinen obigen Rat und rufst read mit einem großen Puffer wie 256 Bytes auf.

Wenn das Lesen wie bei Dateien funktioniert, würde es warten, bis der Benutzer 256 Zeichen eingegeben hat, bevor es zurückkehrt! Ihr Benutzer würde ewig warten und dann leider Ihre Anwendung beenden. Es ist sicherlich nicht das, was Sie wollen, und Sie hätten ein großes Problem.

Okay, Sie könnten ein Byte nach dem anderen lesen, aber diese Problemumgehung ist schrecklich ineffizient, wie ich Ihnen oben gesagt habe. Das muss besser funktionieren.

Aber Linux-Entwickler dachten anders, um dieses Problem zu vermeiden:

  • Wenn Sie normale Dateien lesen, versucht es so viel wie möglich, die Anzahl der Bytes zu lesen, und holt sich aktiv Bytes von der Festplatte, wenn dies erforderlich ist.
  • Für alle anderen Dateitypen wird es zurückgegeben sobald es sind einige Daten verfügbar und maximal Byte zählen:
    1. Für Terminals ist es allgemein wenn der Benutzer die Eingabetaste drückt.
    2. Bei TCP-Sockets ist es so, dass Ihr Computer etwas empfängt, egal wie viele Bytes er bekommt.
    3. Für FIFO oder Pipes ist es im Allgemeinen die gleiche Menge wie die andere Anwendung, aber der Linux-Kernel kann weniger auf einmal liefern, wenn dies bequemer ist.

So können Sie mit Ihrem 2 KiB Puffer sicher telefonieren, ohne für immer eingesperrt zu bleiben. Beachten Sie, dass es auch unterbrochen werden kann, wenn die Anwendung ein Signal empfängt. Da das Lesen aus all diesen Quellen Sekunden oder sogar Stunden dauern kann - bis die andere Seite sich doch dazu entschließt zu schreiben - durch Signale unterbrochen zu werden, erlaubt es, nicht zu lange blockiert zu bleiben.

Dies hat jedoch auch einen Nachteil: Wenn Sie mit diesen speziellen Dateien genau 2 KiB lesen möchten, müssen Sie den Rückgabewert von read überprüfen und read mehrmals aufrufen. read wird selten deinen gesamten Puffer füllen. Wenn Ihre Anwendung Signale verwendet, müssen Sie auch überprüfen, ob das Lesen mit -1 fehlgeschlagen ist, weil es durch ein Signal unterbrochen wurde, mit errno.

Lassen Sie mich Ihnen zeigen, wie es interessant sein kann, diese spezielle Eigenschaft von read zu verwenden:

#define _POSIX_C_SOURCE 1 /* sigaction ist ohne dieses #define nicht verfügbar. */
#einschließen
#einschließen
#einschließen
#einschließen
#einschließen
#einschließen
/*
* isSignal teilt mit, ob der Read-Syscall durch ein Signal unterbrochen wurde.
*
* Gibt TRUE zurück, wenn der Read-Systemaufruf durch ein Signal unterbrochen wurde.
*
* Globale Variablen: es liest errno definiert in errno.ha
*/
unsigned int isSignal(const ssize_t readStatus)
return (readStatus == -1 && errno == EINTR);

unsigned int isSyscallSuccessful(const ssize_t readStatus)
readStatus >= 0 zurückgeben;

/*
* shouldRestartRead teilt mit, wenn der Read-Systemaufruf durch a . unterbrochen wurde
* Ereignis signalisieren oder nicht, und da dieser "Fehler"-Grund vorübergehend ist, können wir
* den Leseaufruf sicher neu starten.
*
* Derzeit wird nur geprüft, ob das Lesen durch ein Signal unterbrochen wurde, aber es
* könnte verbessert werden, um zu überprüfen, ob die Zielanzahl von Bytes gelesen wurde und ob dies der Fall ist
* nicht der Fall, geben Sie TRUE zurück, um erneut zu lesen.
*
*/
unsigned int shouldRestartRead(const ssize_t readStatus)
Rückgabe isSignal(readStatus);

/*
* Wir brauchen einen leeren Handler, da der Read-Syscall nur unterbrochen wird, wenn die
* Signal wird verarbeitet.
*/
void emptyHandler(int ignoriert)
Rückkehr;

int main()
/* Ist in Sekunden. */
const int alarmInterval = 5;
const struct sigaction emptySigaction = emptyHandler;
char lineBuf[256] = 0;
ssize_t readStatus = 0;
unsigned int waitTime = 0;
/* Sigaction nicht ändern, es sei denn, Sie wissen genau, was Sie tun. */
sigaction(SIGALRM, &emptySigaction, NULL);
Alarm (AlarmIntervall);
fputs("Ihr Text:\n", stderr);
tun
/* '\0' nicht vergessen */
readStatus = read(STDIN_FILENO, lineBuf, sizeof(lineBuf) - 1);
if (isSignal(readStatus))
Wartezeit += AlarmIntervall;
Alarm (AlarmIntervall);
fprintf(stderr, "%u Sekunden Inaktivität ... \n", waitTime);

while (sollteRestartRead(readStatus));
if (isSyscallSuccessful(readStatus))
/* Beende den String, um einen Fehler bei der Übergabe an fprintf zu vermeiden. */
lineBuf[readStatus] = '\0';
fprintf(stderr, "Sie haben %lu chars eingegeben. Hier ist Ihr String:\n%s\n", strlen(lineBuf),
lineBuf);
sonst
perror("Lesen von stdin fehlgeschlagen");
EXIT_FAILURE zurückgeben;

EXIT_SUCCESS zurückgeben;

Auch hier handelt es sich um eine vollständige C-Anwendung, die Sie kompilieren und tatsächlich ausführen können.

Es tut Folgendes: Es liest eine Zeile von der Standardeingabe. Alle 5 Sekunden wird jedoch eine Zeile gedruckt, die dem Benutzer mitteilt, dass noch keine Eingabe erfolgt ist.

Beispiel, wenn ich 23 Sekunden warte, bevor ich „Penguin“ tippe:

$alarm_read
Dein Text:
5 Sekunden Inaktivität…
10 Sekunden Inaktivität…
15 Sekunden Inaktivität…
20 Sekunden Inaktivität…
Pinguin
Sie haben 8 Zeichen eingegeben. Hier ist deine Saite:
Pinguin

Das ist unglaublich nützlich. Es kann verwendet werden, um die Benutzeroberfläche häufig zu aktualisieren, um den Fortschritt des Lesens oder der Verarbeitung Ihrer Anwendung zu drucken. Es kann auch als Timeout-Mechanismus verwendet werden. Sie könnten auch von anderen Signalen unterbrochen werden, die für Ihre Anwendung nützlich sein könnten. Auf jeden Fall bedeutet dies, dass Ihre Anwendung jetzt reaktionsschnell sein kann, anstatt für immer festzustecken.

Der Nutzen überwiegt also den oben beschriebenen Nachteil. Wenn Sie sich fragen, ob Sie spezielle Dateien in einer Anwendung unterstützen sollen, die normalerweise mit normalen Dateien arbeitet - und so rufen lesen in einer Schleife - Ich würde sagen, tun Sie es, es sei denn, Sie haben es eilig, meine persönliche Erfahrung hat oft gezeigt, dass das Ersetzen einer Datei durch eine Pipe oder FIFO eine Anwendung mit geringem Aufwand buchstäblich viel nützlicher machen kann. Es gibt sogar vorgefertigte C-Funktionen im Internet, die diese Schleife für Sie implementieren: sie heißt readn-Funktionen.

Fazit

Wie Sie sehen, sehen Fread und Read ähnlich aus, sind es aber nicht. Und mit nur wenigen Änderungen an der Funktionsweise von Read für den C-Entwickler ist read viel interessanter, um neue Lösungen für die Probleme zu entwickeln, die bei der Anwendungsentwicklung auftreten.

Das nächste Mal erzähle ich dir, wie Write syscall funktioniert, denn Lesen ist cool, aber beides zu können ist viel besser. In der Zwischenzeit experimentieren Sie mit Lesen, lernen Sie es kennen und ich wünsche Ihnen ein frohes neues Jahr!

Beste Spiele zum Spielen mit Handtracking
Oculus Quest hat kürzlich die großartige Idee des Hand-Trackings ohne Controller vorgestellt. Mit einer ständig steigenden Anzahl von Spielen und Akti...
So zeigen Sie OSD-Overlay in Vollbild-Linux-Apps und -Spielen an
Das Spielen von Spielen im Vollbildmodus oder die Verwendung von Apps im ablenkungsfreien Vollbildmodus kann Sie von relevanten Systeminformationen ab...
Top 5 Karten zur Spielaufnahme
Wir alle haben Streaming-Gameplays auf YouTube gesehen und geliebt. PewDiePie, Jakesepticye und Markiplier sind nur einige der Top-Gamer, die Millione...