C Programmazione

Leggi Syscall Linux

Leggi Syscall Linux
Quindi devi leggere i dati binari? Potresti voler leggere da una FIFO o da una presa? Vedi, puoi usare la funzione della libreria standard C, ma così facendo non beneficerai delle funzionalità speciali fornite dal kernel Linux e POSIX. Ad esempio, potresti voler utilizzare i timeout per leggere in un determinato momento senza ricorrere al polling. Inoltre, potresti aver bisogno di leggere qualcosa senza preoccuparti se si tratta di un file o socket speciale o qualsiasi altra cosa. Il tuo unico compito è leggere alcuni contenuti binari e ottenerli nella tua applicazione. Ecco dove brilla la syscall letta.

Leggere un file normale con una syscall Linux

Il modo migliore per iniziare a lavorare con questa funzione è leggere un file normale. Questo è il modo più semplice per usare quella syscall, e per un motivo: non ha tanti vincoli come altri tipi di stream o pipe. Se ci pensi è logico, quando leggi l'output di un'altra applicazione, devi avere un output pronto prima di leggerlo e quindi dovrai aspettare che questa applicazione scriva questo output.

Innanzitutto, una differenza fondamentale con la libreria standard: non c'è alcun buffering. Ogni volta che chiami la funzione di lettura, chiamerai il kernel Linux, quindi ci vorrà del tempo -‌ è quasi istantaneo se lo chiami una volta, ma può rallentarti se lo chiami migliaia di volte in un secondo. In confronto, la libreria standard bufferizzerà l'input per te. Quindi ogni volta che chiami read, dovresti leggere più di pochi byte, ma piuttosto un grande buffer come pochi kilobyte - tranne se ciò di cui hai bisogno sono davvero pochi byte, ad esempio se controlli se un file esiste e non è vuoto.

Questo però ha un vantaggio: ogni volta che chiami read, sei sicuro di ottenere i dati aggiornati, se qualsiasi altra applicazione modifica attualmente il file. Ciò è particolarmente utile per file speciali come quelli in /proc o /sys.

È ora di mostrartelo con un esempio reale. Questo programma C controlla se il file è PNG o meno. Per fare ciò, legge il file specificato nel percorso fornito nell'argomento della riga di comando e controlla se i primi 8 byte corrispondono a un'intestazione PNG.

Ecco il codice:

#includere
#includere
#includere
#includere
#includere
#includere
#includere
 
typedef enum
IS_PNG,
TROPPO CORTO,
INVALID_HEADER
pngStatus_t;
 
unsigned int isSyscallSuccessful(const ssize_t readStatus)
return readStatus >= 0;
 

 
/*
* checkPngHeader sta controllando se l'array pngFileHeader corrisponde a un PNG
* intestazione del file.
*
* Attualmente controlla solo i primi 8 byte dell'array. Se l'array è minore
* di 8 byte, viene restituito TOO_SHORT.
*
* pngFileHeaderLength deve contenere il kength dell'array tye. Qualsiasi valore non valido
* può portare a comportamenti indefiniti, come l'arresto anomalo dell'applicazione.
*
* Restituisce IS_PNG se corrisponde a un'intestazione di file PNG. Se c'è almeno
* 8 byte nell'array ma non è un'intestazione PNG, viene restituito INVALID_HEADER.
*
*/
pngStatus_t checkPngHeader(const unsigned char* const pngFileHeader,
size_t pngFileHeaderLength) const unsigned char previstoPngHeader[8] =
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A;
int i = 0;
 
if (pngFileHeaderLength < sizeof(expectedPngHeader))
ritorna TOO_SHORT;
 

 
per (i = 0; i < sizeof(expectedPngHeader); i++)
if (pngFileHeader[i] != previstoPngHeader[i])
restituire INVALID_HEADER;
 


 
/* Se arriva qui, tutti i primi 8 byte sono conformi a un'intestazione PNG. */
restituisce IS_PNG;

 
int main(int argumentLength,  char *argumentList[])
char *pngFileName = NULL;
unsigned char pngFileHeader[8] = 0;
 
ssize_t readStatus = 0;
/* Linux usa un numero per identificare un file aperto. */
int pngFile = 0;
pngStatus_t pngCheckResult;
 
if (argomentLength != 2)
fputs("Devi chiamare questo programma usando isPng il tuo nome file.\n", stderr);
restituisce EXIT_FAILURE;
 

 
pngFileName = argumentList[1];
pngFile = open(pngFileName, O_RDONLY);
 
if (pngFile == -1)
perror("Aprire il file fornito non è riuscito");
restituisce EXIT_FAILURE;
 

 
/* Legge alcuni byte per identificare se il file è PNG. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));
 
if (isSyscallSuccessful(readStatus))
/* Controlla se il file è un PNG poiché ha ottenuto i dati. */
pngCheckResult = checkPngHeader(pngFileHeader, readStatus);
 
if        (pngCheckResult == TOO_SHORT)
printf("Il file %s non è un file PNG: è troppo corto.\n", pngFileName);
 
else if (pngCheckResult == IS_PNG)
printf("Il file %sèun file PNG!\n", pngFileName);
 
altro
printf("Il file %s non è in formato PNG.\n", pngFileName);
 

 
altro
perror("Lettura del file non riuscita");
restituisce EXIT_FAILURE;
 

 
/* Chiude il file... */
if (close(pngFile) == -1)
perror("La chiusura del file fornito non è riuscita");
restituisce EXIT_FAILURE;
 

 
pngFile = 0;
 
restituire EXIT_SUCCESS;
 

Vedi, è un esempio completo, funzionante e compilabile. Non esitare a compilarlo tu stesso e testarlo, funziona davvero. Dovresti chiamare il programma da un terminale come questo:

./isPng il tuo nome file

Ora, concentriamoci sulla chiamata di lettura stessa:

pngFile = open(pngFileName, O_RDONLY);
if (pngFile == -1)
perror("Aprire il file fornito non è riuscito");
restituisce EXIT_FAILURE;

/* Legge alcuni byte per identificare se il file è PNG. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));

La firma di lettura è la seguente (estratta dalle pagine man di Linux):

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

Primo, l'argomento fd rappresenta il descrittore di file. Ho spiegato un po' questo concetto nel mio articolo sulla forcella.  Un descrittore di file è un int che rappresenta un file aperto, socket, pipe, FIFO, dispositivo, beh, sono molte le cose in cui i dati possono essere letti o scritti, generalmente in modo simile a un flusso. Ne parlerò più approfonditamente in un prossimo articolo.

la funzione open è uno dei modi per dire a Linux: voglio fare cose con il file in quel percorso, per favore trovalo dove si trova e dammi l'accesso ad esso. Ti restituirà questo int chiamato descrittore di file e ora, se vuoi fare qualcosa con questo file, usa quel numero. Non dimenticare di chiamare close quando hai finito con il file, come nell'esempio.

Quindi è necessario fornire questo numero speciale per leggere. Poi c'è l'argomento buf. Dovresti qui fornire un puntatore all'array in cui read memorizzerà i tuoi dati. Infine, conta quanti byte leggerà al massimo.

Il valore restituito è di tipo ssize_t. Tipo strano, non è vero?? Significa "signed size_t", in pratica è un lungo int. Restituisce il numero di byte che legge con successo, o -1 se c'è un problema. Puoi trovare la causa esatta del problema nella variabile globale errno creata da Linux, definita in . Ma per stampare un messaggio di errore, usare perror è meglio in quanto stampa errno per tuo conto.

Nei file normali - e solo in questo caso - read restituirà meno di count solo se hai raggiunto la fine del file. L'array buf che fornisci dovere essere abbastanza grande da contenere almeno il conteggio dei byte, o il tuo programma potrebbe bloccarsi o creare un bug di sicurezza.

Ora, leggere non è utile solo per i file normali e se vuoi sentire i suoi super poteri - Sì, lo so che non è in nessun fumetto Marvel ma ha dei veri poteri - vorrai usarlo con altri flussi come tubi o prese. Diamo un'occhiata a questo:

File speciali di Linux e lettura della chiamata di sistema

Il fatto che read funzioni con una varietà di file come pipe, socket, FIFO o dispositivi speciali come un disco o una porta seriale è ciò che lo rende davvero più potente. Con alcuni adattamenti, puoi fare cose davvero interessanti. In primo luogo, questo significa che puoi letteralmente scrivere funzioni lavorando su un file e usarlo invece con una pipe. È interessante passare i dati senza mai colpire il disco, garantendo le migliori prestazioni.

Tuttavia, questo attiva anche regole speciali. Facciamo l'esempio di una lettura di una riga da terminale rispetto ad un normale file. Quando si chiama read su un file normale, sono necessari solo pochi millisecondi a Linux per ottenere la quantità di dati richiesta.

Ma quando si tratta di terminale, questa è un'altra storia: diciamo che chiedi un nome utente. L'utente sta digitando nel terminale il suo nome utente e preme Invio. Ora segui il mio consiglio sopra e chiami read con un grande buffer come 256 byte.

Se la lettura funzionava come con i file, attendeva che l'utente digitasse 256 caratteri prima di tornare before! Il tuo utente aspetterebbe per sempre e poi sfortunatamente ucciderebbe la tua applicazione. Non è certamente quello che vuoi, e avresti un grosso problema.

Ok, potresti leggere un byte alla volta ma questa soluzione alternativa è terribilmente inefficiente, come ti ho detto sopra. Deve funzionare meglio di così.

Ma gli sviluppatori Linux pensavano di leggere diversamente per evitare questo problema:

  • Quando leggi i file normali, cerca il più possibile di leggere i byte di conteggio e otterrà attivamente i byte dal disco se necessario.
  • Per tutti gli altri tipi di file, tornerà non appena ci sono alcuni dati disponibili e al massimo conta i byte:
    1. Per i terminali, è in genere quando l'utente preme il tasto Invio.
    2. Per i socket TCP, non appena il tuo computer riceve qualcosa, non importa la quantità di byte che ottiene.
    3. Per FIFO o pipe, è generalmente la stessa quantità di ciò che ha scritto l'altra applicazione, ma il kernel Linux può fornire meno alla volta se è più conveniente.

Così puoi chiamare in sicurezza con il tuo buffer da 2 KiB senza restare bloccato per sempre. Nota che può anche essere interrotto se l'applicazione riceve un segnale. Poiché la lettura da tutte queste fonti può richiedere secondi o addirittura ore - fino a quando l'altra parte non decide di scrivere, dopotutto - essere interrotti da segnali permette di smettere di rimanere bloccati per troppo tempo.

Questo ha anche uno svantaggio: quando vuoi leggere esattamente 2 KiB con questi file speciali, dovrai controllare il valore di ritorno di read e chiamare read più volte. read raramente riempirà l'intero buffer. Se la tua applicazione utilizza segnali, dovrai anche verificare se la lettura non è riuscita con -1 perché è stata interrotta da un segnale, utilizzando errno.

Lascia che ti mostri come può essere interessante utilizzare questa proprietà speciale di read:

#define _POSIX_C_SOURCE 1 /* sigaction non è disponibile senza questo #define. */
#includere
#includere
#includere
#includere
#includere
#includere
/*
* isSignal dice se read syscall è stato interrotto da un segnale.
*
* Restituisce TRUE se la syscall letta è stata interrotta da un segnale.
*
* Variabili globali: legge errno definito in errno.h
*/
unsigned int isSignal(const ssize_t readStatus)
return (readStatus == -1 && errno == EINTR);

unsigned int isSyscallSuccessful(const ssize_t readStatus)
return readStatus >= 0;

/*
* shouldRestartRead indica quando la syscall letta è stata interrotta da a
* segnalare l'evento o meno, e dato che questo motivo di "errore" è transitorio, possiamo
* riavvia in sicurezza la chiamata di lettura.
*
* Attualmente, controlla solo se la lettura è stata interrotta da un segnale, ma
* potrebbe essere migliorato per verificare se il numero di byte di destinazione è stato letto e se lo è
* non è il caso, ritorna TRUE per rileggere.
*
*/
unsigned int shouldRestartRead(const ssize_t readStatus)
return isSignal(readStatus);

/*
* Abbiamo bisogno di un gestore vuoto poiché la syscall letta verrà interrotta solo se il
* il segnale viene gestito.
*/
void emptyHandler(int ignorato)
ritorno;

int main()
/* È in secondi. */
const int alarmInterval = 5;
const struct sigaction emptySigaction = emptyHandler;
char lineBuf[256] = 0;
ssize_t readStatus = 0;
unsigned int waitTime = 0;
/* Non modificare sigaction se non sai esattamente cosa stai facendo. */
sigaction(SIGALRM, &emptySigaction, NULL);
allarme(IntervalloAlarm);
fputs("Il tuo testo:\n", stderr);
fare
/* Non dimenticare '\0' */
readStatus = read(STDIN_FILENO, lineBuf, sizeof(lineBuf) - 1);
if (isSignal(readStatus))
waitTime += alarmInterval;
allarme(IntervalloAlarm);
fprintf(stderr, "%u secondi di inattività… \n", waitTime);

while (dovrebbeRestartRead(readStatus));
if (isSyscallSuccessful(readStatus))
/* Termina la stringa per evitare un bug quando la si fornisce a fprintf. */
lineBuf[readStatus] = '\0';
fprintf(stderr, "Hai digitato %lu chars. Ecco la tua stringa:\n%s\n", strlen(lineBuf),
lineBuf);
altro
perror("Lettura da stdin fallita");
restituisce EXIT_FAILURE;

restituire EXIT_SUCCESS;

Ancora una volta, questa è un'applicazione C completa che puoi compilare ed eseguire effettivamente.

Fa quanto segue: legge una riga dallo standard input. Tuttavia, ogni 5 secondi, stampa una riga che informa l'utente che non è stato ancora fornito alcun input.

Esempio se aspetto 23 secondi prima di digitare "Penguin":

$ alarm_read
Il tuo testo:
5 secondi di inattività..
10 secondi di inattività..
15 secondi di inattività..
20 secondi di inattività..
Pinguino
Hai digitato 8 caratteri. Ecco la tua stringa:
Pinguino

È incredibilmente utile. Può essere utilizzato per aggiornare spesso l'interfaccia utente per stampare lo stato di avanzamento della lettura o dell'elaborazione della tua applicazione che stai facendo. Può essere utilizzato anche come meccanismo di timeout. Potresti anche essere interrotto da qualsiasi altro segnale che potrebbe essere utile per la tua applicazione. Ad ogni modo, questo significa che la tua applicazione ora può essere reattiva invece di rimanere bloccata per sempre.

Quindi i vantaggi superano l'inconveniente sopra descritto. Se ti chiedi se dovresti supportare file speciali in un'applicazione che normalmente funziona con file normali - e così chiamando leggere in un ciclo - Direi di farlo tranne se sei di fretta, la mia esperienza personale ha spesso dimostrato che sostituire un file con una pipe o FIFO può letteralmente rendere un'applicazione molto più utile con piccoli sforzi. Ci sono anche funzioni C predefinite su Internet che implementano quel ciclo per te: si chiama readn functions.

Conclusione

Come puoi vedere, fread e read potrebbero sembrare simili, non lo sono. E con solo poche modifiche su come funziona read per lo sviluppatore C, read è molto più interessante per progettare nuove soluzioni ai problemi che incontri durante lo sviluppo dell'applicazione.

La prossima volta ti dirò come funziona la scrittura di syscall, poiché leggere è bello, ma essere in grado di fare entrambe le cose è molto meglio. Nel frattempo sperimenta leggi, conoscila e ti auguro un Felice Anno Nuovo!

Strumenti utili per i giocatori Linux
Se ti piace giocare su Linux, è probabile che tu abbia utilizzato app e utilità come Wine, Lutris e OBS Studio per migliorare l'esperienza di gioco. O...
Giochi rimasterizzati in HD per Linux che non hanno mai avuto una versione Linux prima
Molti sviluppatori ed editori di giochi stanno realizzando remaster HD di vecchi giochi per prolungare la vita del franchise, per favore i fan richied...
Come utilizzare AutoKey per automatizzare i giochi Linux
AutoKey è un'utilità di automazione desktop per Linux e X11, programmata in Python 3, GTK e Qt. Utilizzando la sua funzionalità di scripting e MACRO, ...