Tutorial di chiamata di sistema Linux con C

Tutorial di chiamata di sistema Linux con C
Nel nostro ultimo articolo sulle chiamate di sistema Linux, ho definito una chiamata di sistema, ho discusso dei motivi per cui si potrebbe usarli in un programma e approfondire i loro vantaggi e svantaggi. Ho anche dato un breve esempio in assemblaggio all'interno di C. Ha illustrato il punto e ha descritto come fare la chiamata, ma non ha fatto nulla di produttivo. Non esattamente un elettrizzante esercizio di sviluppo, ma ha illustrato il punto.

In questo articolo, utilizzeremo le chiamate di sistema effettive per svolgere un lavoro reale nel nostro programma C. Innanzitutto, esamineremo se è necessario utilizzare una chiamata di sistema, quindi fornire un esempio utilizzando la chiamata SendFile () che può migliorare drasticamente le prestazioni della copia del file. Infine, esamineremo alcuni punti da ricordare durante l'utilizzo delle chiamate di sistema Linux.

Hai bisogno di una chiamata di sistema?

Sebbene sia inevitabile, utilizzerai una chiamata di sistema ad un certo punto della tua carriera di sviluppo C, a meno che tu non stia prendendo di mira prestazioni elevate o una particolare funzionalità di tipo, la libreria GLIBC e altre librerie di base incluse nelle principali distribuzioni Linux si occuperanno della maggior parte dei I tuoi bisogni.

La libreria standard GLIBC fornisce un framework multipiattaforma e ben testata per eseguire funzioni che altrimenti richiederebbero chiamate di sistema specifiche del sistema. Ad esempio, puoi leggere un file con fscanf (), fread (), getc (), ecc., oppure puoi utilizzare la chiamata di sistema LEAD () Linux. Le funzioni GLIBC forniscono più funzionalità (i.e. Migliore gestione degli errori, IO formattato, ecc.) e lavorerà su qualsiasi supporto di GLIBC.

D'altra parte, ci sono momenti in cui le prestazioni intransantili e l'esecuzione esatta sono fondamentali. Il wrapper che Fread () fornisce aggiungerà sovraccarico e, sebbene minore, non è del tutto trasparente. Inoltre, non è possibile desiderare o aver bisogno delle funzionalità extra fornite dal wrapper. In tal caso, è meglio servire con una chiamata di sistema.

È inoltre possibile utilizzare le chiamate di sistema per eseguire funzioni non ancora supportate da GLIBC. Se la tua copia di GLIBC è aggiornata, questo difficilmente sarà un problema, ma lo sviluppo di distribuzioni più vecchie con kernel più recenti potrebbe richiedere questa tecnica.

Ora che hai letto le dichiarazioni di non responsabilità, avvertimenti e potenziali deviazioni, ora scaviamo in alcuni esempi pratici.

Su cosa ci stiamo facendo la CPU?

Una domanda che la maggior parte dei programmi probabilmente non pensa di chiedere, ma nefolistica. Questo è un esempio di una chiamata di sistema che non può essere duplicato con glibc e non è coperto da un wrapper Glibc. In questo codice, chiameremo la chiamata getCpu () direttamente tramite la funzione syscall (). La funzione Syscall funziona come segue:

syscall (sys_call, arg1, arg2, ...);

Il primo argomento, sys_call, è una definizione che rappresenta il numero della chiamata di sistema. Quando includi sys/syscall.h, questi sono inclusi. La prima parte è sys_ e la seconda parte è il nome della chiamata di sistema.

Argomenti per la chiamata vanno in arg1, arg2 sopra. Alcune chiamate richiedono più argomenti e continueranno in ordine dalla loro pagina uomo. Ricorda che la maggior parte degli argomenti, in particolare per i rendimenti, richiederà indicatori per carbonizzare array o memoria allocata tramite la funzione Malloc.

Esempio 1.C

#includere
#includere
#includere
#includere
int main ()
CPU non firmata, nodo;
// Ottieni il core CPU corrente e il nodo NUMA tramite chiamata di sistema
// Nota che questo non ha un wrapper Glibc, quindi dobbiamo chiamarlo direttamente
syscall (sys_getcpu, & cpu, & node, null);
// Visualizza informazioni
printf ("Questo programma è in esecuzione su CPU Core %U e NUMA Node %U.\ n \ n ", cpu, nodo);
restituzione 0;

Per compilare ed eseguire:
Esempio GCC1.C -O Esempio1
./Esempio 1

Per risultati più interessanti, è possibile girare i thread tramite la libreria PThreads e quindi chiamare questa funzione per vedere quale processore è in esecuzione.

Sendfile: prestazioni superiori

Sendfile fornisce un eccellente esempio di miglioramento delle prestazioni tramite le chiamate di sistema. La funzione SendFile () copia i dati da un descrittore di file a un altro. Invece di usare più funzioni Fread () e FWRITE (), SendFile esegue il trasferimento nello spazio del kernel, riducendo le spese generali e aumentando così le prestazioni.

In questo esempio, copriremo 64 MB di dati da un file a un altro. In un test, utilizzeremo i metodi di lettura/scrittura standard nella libreria standard. Nell'altro, utilizzeremo le chiamate di sistema e la chiamata SendFile () per far esplodere questi dati da una posizione a un'altra.

Test1.C (GLIBC)

#includere
#includere
#includere
#includere
#define buffer_size 67108864
#define buffer_1 "buffer1"
#define buffer_2 "buffer2"
int main ()
File *fout, *pin;
printf ("\ ni/o test con le funzioni tradizionali GLIBC.\ n \ n ");
// Prendi un buffer buffer_size.
// Il buffer avrà dati casuali in esso ma non ci importa.
printf ("allocando buffer da 64 mb:");
char *buffer = (char *) malloc (buffer_size);
printf ("done \ n");
// Scrivi il buffer su fout
printf ("Scrivere dati al primo buffer:");
fout = fopen (buffer_1, "wb");
fwrite (buffer, sizeof (char), buffer_size, fout);
fclose (fout);
printf ("done \ n");
printf ("Copia dei dati dal primo file al secondo:");
fin = fopen (buffer_1, "rb");
fout = fopen (buffer_2, "wb");
Fread (buffer, sizeof (char), buffer_size, pin);
fwrite (buffer, sizeof (char), buffer_size, fout);
fclose (pin);
fclose (fout);
printf ("done \ n");
printf ("buffer di liberazione:");
gratuito (buffer);
printf ("done \ n");
printf ("eliminazione di file:");
rimuovere (buffer_1);
rimuovere (buffer_2);
printf ("done \ n");
restituzione 0;

test2.C (chiamate di sistema)

#includere
#includere
#includere
#includere
#includere
#includere
#includere
#includere
#includere
#define buffer_size 67108864
int main ()
int fout, pin;
printf ("\ ni/o test con sendFile () e chiamate di sistema correlate.\ n \ n ");
// Prendi un buffer buffer_size.
// Il buffer avrà dati casuali in esso ma non ci importa.
printf ("allocando buffer da 64 mb:");
char *buffer = (char *) malloc (buffer_size);
printf ("done \ n");
// Scrivi il buffer su fout
printf ("Scrivere dati al primo buffer:");
fout = open ("buffer1", o_rdonly);
scrivere (fout e buffer, buffer_size);
chiudere (fout);
printf ("done \ n");
printf ("Copia dei dati dal primo file al secondo:");
fin = open ("buffer1", o_rdonly);
fout = open ("buffer2", o_rdonly);
Sendfile (fout, pin, 0, buffer_size);
Close (pin);
chiudere (fout);
printf ("done \ n");
printf ("buffer di liberazione:");
gratuito (buffer);
printf ("done \ n");
printf ("eliminazione di file:");
UNLINK ("buffer1");
UNLINK ("Buffer2");
printf ("done \ n");
restituzione 0;

Prova a compilazione e in esecuzione 1 e 2

Per creare questi esempi, avrai bisogno degli strumenti di sviluppo installati sulla distribuzione. Su Debian e Ubuntu, puoi installarlo con:

APT Installare Build-Essentials

Quindi compilare con:

GCC Test1.c -o test1 && gcc test2.c -o test2

Per eseguire entrambi e testare le prestazioni, eseguire:

tempo ./test1 && time ./test2

Dovresti ottenere risultati come questo:

Test I/O con le funzioni tradizionali GLIBC.

Allocazione di buffer da 64 MB: fatto
Scrivere dati al primo buffer: fatto
Copia dei dati dal primo file al secondo: fatto
Buffer di liberazione: fatto
Eliminazione di file: fatto
reale 0m0.397s
utente 0m0.000
sys 0m0.203s
Test I/O con SendFile () e chiamate di sistema correlate.
Allocazione di buffer da 64 MB: fatto
Scrivere dati al primo buffer: fatto
Copia dei dati dal primo file al secondo: fatto
Buffer di liberazione: fatto
Eliminazione di file: fatto
reale 0m0.019s
utente 0m0.000
sys 0m0.016s

Come puoi vedere, il codice che utilizza le chiamate di sistema funziona molto più velocemente dell'equivalente GLIBC.

Cose da ricordare

Le chiamate di sistema possono aumentare le prestazioni e fornire funzionalità aggiuntive, ma non sono prive di svantaggi. Dovrai valutare le chiamate del sistema a benefici che forniscono contro la mancanza di portabilità della piattaforma e talvolta ridotta rispetto alle funzioni della libreria.

Quando si utilizza alcune chiamate di sistema, è necessario fare attenzione a utilizzare le risorse restituite dalle chiamate di sistema piuttosto che dalle funzioni della libreria. Ad esempio, la struttura dei file utilizzata per le funzioni Fopen (), Fread (), FWRITE () e fclose () di GLIBC non sono uguali al numero del descrittore dei file dalla chiamata di sistema Open () (restituita come intero). Mescolarli può portare a problemi.

In generale, le chiamate di sistema Linux hanno meno corsie per paraurti rispetto alle funzioni GLIBC. Mentre è vero che le chiamate di sistema hanno alcuni errori di gestione e reporting, otterrai funzionalità più dettagliate da una funzione Glibc.

E infine, una parola sulla sicurezza. Le chiamate di sistema si interfaccia direttamente con il kernel. Il kernel Linux ha ampie protezioni contro gli shenanigans dalla terra dell'utente, ma esistono bug non scoperti. Non fidarti che una chiamata di sistema convaliderà il tuo input o ti isolerà da problemi di sicurezza. È saggio garantire che i dati che consegnate a una chiamata di sistema siano disinfettati. Naturalmente, questo è un buon consiglio per qualsiasi chiamata API, ma non puoi essere attento quando lavori con il kernel.

Spero che ti sia piaciuta questa immersione più profonda nella terra delle chiamate del sistema Linux. Per un elenco completo delle chiamate di sistema Linux, consultare il nostro elenco master.