Chiamate di Sistema in Linux
Le chiamate di sistema sono il punto di ingresso controllato nel kernel, attraverso il quale un processo richiede al kernel di eseguire operazioni per suo conto.
Questa lezione descrive il loro meccanismo, le differenze con le funzioni di libreria e gli aspetti legati alla portabilità.
Chiamate di Sistema (System Calls)
Una chiamata di sistema è un punto di ingresso controllato nel kernel, che permette a un processo di chiedere al kernel di eseguire qualche azione per suo conto. Il kernel rende una serie di servizi accessibili ai programmi tramite l'API (Application Programming Interface) delle chiamate di sistema. Tali servizi includono, ad esempio, la creazione di un nuovo processo, operazioni di I/O e la creazione di una pipe per la comunicazione interprocesso. (La pagina di manuale syscalls(2) elenca le chiamate di sistema di Linux.)
Prima di entrare nei dettagli di come funziona una chiamata di sistema, osserviamo alcuni punti generali:
- Una chiamata di sistema cambia lo stato del processore da modalità utente a modalità kernel, così che la CPU possa accedere alla memoria protetta del kernel.
- L'insieme delle chiamate di sistema è fisso. Ogni chiamata è identificata da un numero unico. (Questo schema di numerazione non è normalmente visibile ai programmi, che le identificano per nome.)
- Ogni chiamata può avere un insieme di argomenti che specificano le informazioni da trasferire dallo spazio utente (cioè lo spazio di indirizzi virtuale del processo) allo spazio kernel e viceversa.
Dal punto di vista della programmazione, invocare una chiamata di sistema assomiglia molto a chiamare una funzione C. Tuttavia, dietro le quinte, avvengono molti passaggi durante l'esecuzione di una chiamata di sistema. Per illustrare ciò, consideriamo i passaggi nell'ordine in cui avvengono su una specifica implementazione hardware, la x86‑32. I passaggi sono i seguenti:
- Il programma applicativo effettua una chiamata di sistema invocando una funzione wrapper nella C library.
- La funzione wrapper deve rendere tutti gli argomenti della chiamata disponibili alla routine di gestione della trappola del kernel (descritta più avanti). Questi argomenti vengono passati al wrapper tramite lo stack, ma il kernel li attende in registri specifici. Il wrapper copia quindi gli argomenti in tali registri.
- Poiché tutte le chiamate di sistema entrano nel kernel nello stesso modo, il kernel ha bisogno di un metodo per identificare la chiamata. Per consentirlo, il wrapper copia il numero della chiamata di sistema in un registro CPU specifico (
%eax). - Il wrapper esegue un'istruzione di trappola (
int 0x80), che fa passare il processore da modalità utente a modalità kernel ed esegue il codice puntato dalla posizione 0x80 (128 decimale) del vettore di trappole del sistema. Le architetture x86‑32 più recenti implementano l'istruzionesysenter, che fornisce un metodo più veloce per entrare in modalità kernel rispetto all'istruzione di trappola tradizionaleint 0x80. L'uso disysenterè supportato nel kernel 2.6 e da glibc 2.3.2 in poi. -
In risposta alla trappola alla posizione 0x80, il kernel invoca la sua routine
system_call()(situata nel file assemblyarch/i386/entry.S) per gestire la trappola. Questo handler:- a) Salva i valori dei registri sullo stack del kernel (vedi Sezione sui processi).
- b) Verifica la validità del numero della chiamata di sistema.
- c) Invoca la routine di servizio della chiamata di sistema appropriata, trovata indicizzando una tabella di tutte le routine di servizio (la variabile kernel
sys_call_table). Se la routine di servizio ha argomenti, ne verifica prima la validità; ad esempio controlla che gli indirizzi puntino a posizioni valide nella memoria utente. Poi la routine di servizio esegue il compito richiesto, che può comportare la modifica di valori agli indirizzi specificati negli argomenti e il trasferimento di dati tra memoria utente e memoria kernel (ad esempio nelle operazioni di I/O). Infine, la routine di servizio restituisce uno stato di risultato alla routinesystem_call(). - d) Ripristina i valori dei registri dallo stack del kernel e pone il valore di ritorno della chiamata di sistema sullo stack.
- e) Ritorna alla funzione wrapper, riportando simultaneamente il processore in modalità utente.
-
Se il valore di ritorno della routine di servizio indica un errore, la funzione wrapper imposta la variabile globale
errno(vedi la lezione sulla gestione degli errori) usando tale valore. Il wrapper quindi ritorna al chiamante, fornendo un valore intero che indica il successo o il fallimento della chiamata di sistema.Su Linux, le routine di servizio delle chiamate di sistema seguono la convenzione di restituire un valore non negativo per indicare il successo. In caso di errore, la routine restituisce un numero negativo, che è il valore negato di una delle costanti
errno. Quando viene restituito un valore negativo, la funzione wrapper della C library lo nega (per renderlo positivo), copia il risultato inerrnoe restituisce-1come valore di ritorno della wrapper per segnalare l'errore al programma chiamante.Questa convenzione si basa sull'assunzione che le routine di servizio non restituiscano valori negativi in caso di successo. Tuttavia, per alcune di queste routine l'assunzione non è valida. Normalmente ciò non crea problemi, poiché l'intervallo dei valori
errnonegati non si sovrappone ai valori di ritorno negativi validi. Tuttavia, la convenzione genera un problema in un caso: l'operazioneF_GETOWNdella chiamata di sistemafcntl(), descritta nella Sezione sull'I/O Alternativo.
Le informazioni fornite nei paragrafi precedenti sono più di quanto normalmente sia necessario conoscere per il resto di questo libro. Tuttavia, illustrano il punto importante che, anche per una chiamata di sistema semplice, è necessario svolgere un lavoro considerevole, e quindi le chiamate di sistema hanno un piccolo ma apprezzabile overhead.
Come esempio dell'overhead di una chiamata di sistema, consideriamo la chiamata getppid(), che restituisce semplicemente l'ID del processo padre del processo chiamante. Su uno dei sistemi x86‑32 dell'autore, con Linux 2.6.25, 10 milioni di chiamate a getppid() hanno richiesto circa 2,2 secondi per completarsi. Questo corrisponde a circa 0,3 microsecondi per chiamata. In confronto, sullo stesso sistema, 10 milioni di chiamate a una funzione C che restituisce semplicemente un intero hanno richiesto 0,11 secondi, ovvero circa un ventesimo del tempo richiesto dalle chiamate a getppid(). Naturalmente, la maggior parte delle chiamate di sistema ha un overhead significativamente maggiore rispetto a getppid().
Poiché, dal punto di vista di un programma C, chiamare la funzione wrapper della C library è sinonimo di invocare la routine di servizio della chiamata di sistema corrispondente, nel resto di questo libro utilizziamo espressioni come "invocare la chiamata di sistema xyz()" per intendere "chiamare la funzione wrapper che invoca la chiamata di sistema xyz()".
Nelle prossime lezioni studieremo il comando strace, che può essere usato per tracciare le chiamate di sistema effettuate da un programma, sia a scopo di debug sia semplicemente per investigare cosa sta facendo un programma.
In Sintesi
Le chiamate di sistema sono il ponte controllato tra lo spazio utente e il kernel, richiedendo un insieme di passaggi complessi che introducono un piccolo overhead.
Il processo prevede l'uso di wrapper della C library, la preparazione di registri e l'esecuzione di una trappola (int 0x80 o sysenter).
Gli errori vengono segnalati tramite la variabile globale errno, mentre un valore di ritorno positivo indica il successo della chiamata.