Interruzione delle chiamate di sistema quando viene rilevato un segnale


29

Dalla lettura delle pagine man su read()e write()chiamate sembra che queste chiamate vengano interrotte da segnali indipendentemente dal fatto che debbano essere bloccati o meno.

In particolare, supponiamo

  • un processo stabilisce un gestore per alcuni segnali.
  • un dispositivo viene aperto (per esempio un terminale) con il O_NONBLOCK non impostato (cioè operando in modalità di blocco)
  • il processo quindi effettua una read()chiamata di sistema per leggere dal dispositivo e di conseguenza esegue un percorso di controllo del kernel nello spazio kernel.
  • mentre il precesso sta eseguendo il suo read()nello spazio kernel, il segnale per il quale il gestore è stato installato in precedenza viene consegnato a quel processo e il suo gestore del segnale viene invocato.

Leggendo le pagine man e le sezioni appropriate in SUSv3 'System Interfaces volume (XSH)' , si scopre che:

io. Se a read()viene interrotto da un segnale prima di leggere qualsiasi dato (cioè deve bloccarsi perché non erano disponibili dati), restituisce -1 con errnoimpostato su [EINTR].

ii. Se a read()viene interrotto da un segnale dopo aver letto correttamente alcuni dati (ovvero è stato possibile iniziare immediatamente a servire la richiesta), restituisce il numero di byte letti.

Domanda A): ho ragione a supporre che in entrambi i casi (blocco / nessun blocco) la consegna e la gestione del segnale non siano completamente trasparenti al read()?

Caso I. sembra comprensibile poiché il blocco read()normalmente posizionerebbe il processo nello TASK_INTERRUPTIBLEstato in modo tale che quando un segnale viene consegnato, il kernel pone il processo nello TASK_RUNNINGstato.

Tuttavia, quando read()non è necessario bloccare (caso ii.) Ed è in fase di elaborazione della richiesta nello spazio kernel, avrei pensato che l'arrivo di un segnale e la sua gestione sarebbero stati trasparenti proprio come l'arrivo e la corretta gestione di un HW l'interruzione sarebbe. In particolare avrei assunto che al momento della consegna del segnale, il processo sarebbe stato messo temporaneamente in modalità utente ad eseguire il suo gestore di segnale da cui sarebbe tornato alla fine per finire l'elaborazione del interrotta read()(nello spazio del kernel) in modo che l' read()esegue il suo ovviamente, al termine del quale il processo ritorna al punto immediatamente successivo alla chiamata read()(nello spazio utente), con la lettura di tutti i byte disponibili.

Ma ii. sembra implicare che read()sia interrotto, poiché i dati sono immediatamente disponibili, ma restituisce restituisce solo alcuni dei dati (anziché tutti).

Questo mi porta alla mia seconda (e ultima) domanda:

Domanda B): se la mia ipotesi in A) è corretta, perché read()viene interrotta, anche se non è necessario bloccare perché ci sono dati disponibili per soddisfare immediatamente la richiesta? In altre parole, perché read()non viene ripreso dopo aver eseguito il gestore del segnale, risultando infine nella restituzione di tutti i dati disponibili (che erano disponibili dopo tutto)?

Risposte:


29

Riepilogo: hai ragione a dire che la ricezione di un segnale non è trasparente, né nel caso i (interrotto senza aver letto nulla) né nel caso ii (interrotto dopo una lettura parziale). In caso contrario, richiederei di apportare modifiche fondamentali sia all'architettura del sistema operativo sia all'architettura delle applicazioni.

La vista dell'implementazione del sistema operativo

Considera cosa succede se una chiamata di sistema viene interrotta da un segnale. Il gestore del segnale eseguirà il codice in modalità utente. Ma il gestore syscall è il codice del kernel e non si fida di alcun codice in modalità utente. Esploriamo quindi le scelte per il gestore syscall:

  • Terminare la chiamata di sistema; segnala quanto è stato fatto al codice utente. Spetta al codice dell'applicazione riavviare in qualche modo la chiamata di sistema, se lo si desidera. Ecco come funziona unix.
  • Salvare lo stato della chiamata di sistema e consentire al codice utente di riprendere la chiamata. Questo è problematico per diversi motivi:
    • Mentre il codice utente è in esecuzione, potrebbe accadere qualcosa per invalidare lo stato salvato. Ad esempio, se si legge da un file, il file potrebbe essere troncato. Quindi il codice del kernel avrebbe bisogno di molta logica per gestire questi casi.
    • Allo stato salvato non è consentito conservare alcun blocco, poiché non esiste alcuna garanzia che il codice utente riprenderà mai la syscall e quindi il blocco rimarrà per sempre.
    • Il kernel deve esporre nuove interfacce per riprendere o annullare le syscall in corso, oltre alla normale interfaccia per avviare una syscall. Questa è una grande complicazione per un caso raro.
    • Lo stato salvato dovrebbe utilizzare le risorse (almeno memoria); tali risorse dovrebbero essere allocate e trattenute dal kernel ma essere conteggiate rispetto all'assegnazione del processo. Questo non è insormontabile, ma è una complicazione.
      • Si noti che il gestore del segnale potrebbe effettuare chiamate di sistema che vengono esse stesse interrotte; quindi non puoi semplicemente avere un'assegnazione statica delle risorse che copre tutte le possibili syscall.
      • E se le risorse non possono essere allocate? Quindi la syscall dovrebbe fallire comunque. Ciò significa che l'applicazione dovrebbe avere un codice per gestire questo caso, quindi questo design non semplificherebbe il codice dell'applicazione.
  • Rimanere in corso (ma sospeso), creare un nuovo thread per il gestore del segnale. Questo, ancora una volta, è problematico:
    • Le prime implementazioni di unix avevano un singolo thread per processo.
    • Il gestore del segnale rischierebbe di oltrepassare le scarpe del syscall. Questo è comunque un problema, ma nell'attuale progetto unix è contenuto.
    • Le risorse dovrebbero essere allocate per il nuovo thread; vedi sopra.

La differenza principale con un interrupt è che il codice di interrupt è attendibile e fortemente vincolato. Di solito non è consentito allocare risorse, o eseguire per sempre, o prendere blocchi e non rilasciarli o fare qualsiasi altro tipo di cose cattive; dal momento che il gestore di interrupt è scritto dallo stesso implementatore del sistema operativo, sa che non farà nulla di male. D'altra parte, il codice dell'applicazione può fare qualsiasi cosa.

La vista di progettazione dell'applicazione

Quando un'applicazione viene interrotta nel mezzo di una chiamata di sistema, il syscall dovrebbe continuare al completamento? Non sempre. Ad esempio, considera un programma come una shell che legge una riga dal terminale e l'utente preme Ctrl+C, attivando SIGINT. La lettura non deve essere completa, ecco di cosa tratta il segnale. Si noti che questo esempio mostra che readsyscall deve essere interrompibile anche se non è stato ancora letto alcun byte.

Quindi deve esserci un modo per l'applicazione di dire al kernel di annullare la chiamata di sistema. Sotto il design unix, ciò accade automaticamente: il segnale fa tornare la syscall. Altri progetti richiederebbero un modo per l'applicazione di riprendere o annullare la syscall a suo piacimento.

La readchiamata di sistema è come è perché è la primitiva che ha un senso, dato il design generale del sistema operativo. Ciò significa, approssimativamente, "leggi il più possibile, fino a un limite (la dimensione del buffer), ma fermati se succede qualcos'altro". Leggere effettivamente un buffer completo implica l'esecuzione readin un ciclo fino a quando non sono stati letti quanti più byte possibili; questa è una funzione di livello superiore, fread(3). A differenza di read(2)quale è una chiamata di sistema, freadè una funzione di libreria, implementata nello spazio utente sopra read. È adatto per un'applicazione che legge per un file o muore provandoci; non è adatto per un interprete della riga di comando o per un programma di rete che deve limitare le connessioni in modo pulito, né per un programma di rete che ha connessioni simultanee e non utilizza thread.

L'esempio di lettura in un ciclo è fornito nella programmazione del sistema Linux di Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Si prende cura di case ied case iie pochi di più.


Grazie mille Gilles per una risposta molto concisa e chiara che corrobora opinioni simili avanzate in un articolo sulla filosofia progettuale UNIX. Mi sembra molto convincente che il comportamento dell'interruzione syscall abbia a che fare con la filosofia del design UNIX piuttosto che con i vincoli o gli impedimenti tecnici
darbehdar,

@darbehdar Sono tutte e tre le cose: filosofia del design unix (qui principalmente che i processi sono meno affidabili del kernel e possono eseguire codice arbitrario, anche che processi e thread non sono creati in modo implicito), vincoli tecnici (sulle allocazioni delle risorse) e design dell'applicazione (lì sono casi in cui il segnale deve annullare la syscall).
Gilles 'SO- smetti di essere malvagio' il

2

Per rispondere alla domanda A :

Sì, la consegna e la gestione del segnale non sono completamente trasparenti al read().

La read()corsa a metà strada potrebbe occupare alcune risorse mentre è interrotta dal segnale. E l'handler di segnale del segnale può chiamare anche un altro read()(o qualsiasi altro syscalls sicuro per il segnale asincrono ). Pertanto, l' read()interruzione del segnale deve essere prima interrotta per liberare le risorse che utilizza, altrimenti la read()chiamata dal gestore del segnale accederà alle stesse risorse e causerà problemi di rientro.

Poiché le chiamate di sistema diverse da quelle che read()potrebbero essere chiamate dal gestore del segnale possono occupare un insieme identico di risorse read(). Per evitare problemi rientranti sopra, il progetto più semplice e sicuro è quello di interrompere l'interruzione read()ogni volta che si verifica un segnale durante la sua esecuzione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.