APPUNTI DI SISTEMI OPERATIVI 1 PER LE ESERCITAZIONI IN LABORATORIO

CHIAMATE DI SISTEMA - PARTE 2B 2C

****************************************

SEGNALI

****************************************

I segnali sono il meccanismo con cui il sistema operativo comunica ad un processo il verificarsi di un evento, tipicamente inatteso, che può essere:
Ad ognuno di questi tipi di eventi è associato un numero di segnale, "nascosto" in una costante simbolica definita in un opportuno file header signal.h da includere nei programmi che utilizzano i segnali.
Nel primo, secondo e quarto esempio di cui sopra le costanti si chiamano: SIGINT, SIGALRM, SIGSEGV, mentre con la chiamata di sistema e il comando kill si può inviare qualunque segnale. Tuttavia è bene evitare di inviare segnali come SIGSEGV che hanno un preciso significato, ha invece senso inviare i segnali il cui scopo è terminare il processo, come SIGTERM e SIGKILL, o i segnali il cui significato è "definito dall'utente" SIGUSR1 e SIGUSR2, sebbene questi ultimi possano essere utilizzati solo per meccanismi molto rudimentali di sincronizzazione tra processi. Successivamente ai segnali, sono state introdotte in Unix altre primitive di sincronizzazione descritte più avanti nel corso ed è bene utilizzare quelle.

Per ogni segnale esiste un comportamento di default associato dal sistema operativo che può essere:
Il man ("man -s 7 signal" su Linux) descrive l''elenco completo dei tipi di segnali con il loro identificatore numerico, il corrispondente nome simbolico, il comportamento associato per default al segnale, e il significato del segnale (tipicamente, evento in conseguenza del quale viene inviato).
Un processo può modificare il proprio comportamento associato alla ricezione del segnale, tranne per il segnale SIGKILL che esiste proprio per assicurare un modo di terminare un processo contro cui il processo stesso non può fare nulla.

********************************************************************

Una interfaccia semplice, ma non robusta, di gestione dei segnali, (la documentazione si riferisce a questo caso con il termine unreliable signals - segnali inaffidabili) che è rimasta parte dello standard dell'ANSI C ma non fa parte dello standard POSIX dei sistemi Unix.
In questo caso il comportamento associato ad un segnale può essere modificato con la funzione "signal"

Una seconda interfaccia per i segnali (reliable signals - segnali affidabili - nella documentazione) è definita nello standard POSIX e prevede la seguente chiamata di sistema:

SIGACTION:
  int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
La "struct sigaction" passata come secondo parametro include i seguenti campi:
  void (*sa_handler)(int);    /* SIG_DFL, SIG_IGN, or pointer to a function */
  sigset_t sa_mask;   /* Additional signals to be blocked during execution of
                              signal-catching function */
  int sa_flags;       /* Special flags to affect behavior of signal */
Questa funzione setta il comportamento associato al segnale "signum" a quello specificato in *act (se act non è NULL) e restituisce in *oldact  (se oldact non è NULL) il comportamento precedente.
Nel caso in cui non ci interessi memorizzare il comportamento precedente, si può chiamare:
  act.sa_handler=f;  /* oppure SIG_IGN oppure SIG_DFL */
sigemptyset(&act.sa_mask);
sa.sa_flags = 0;
  sigaction(s,&act,NULL)
In base allo standard (e quindi su tutte le implementazioni conformi allo standard), per default, cioè se non lo si chiede con opportune opzioni, il comportamento NON viene rimpiazzato con quello per default quando il segnale viene ricevuto (come invece avviene per la funzione "signal").
La chiamata di sigaction associa alla ricezione del segnale ("signum" nel prototipo) il comportamento specificato dal campo sa_handler della struttura puntata dal secondo argomento; tale campo può essere:
E' possibile (vedere il man, i testi etc) utilizzare come "handler" funzioni con un altro prototipo (usando un altro campo della "struct sigaction").

Catturare i segnali può servire a vari scopi, ad esempio:

Il seguente esempio illustra come modificare il comportamento associato ai segnali, prevedendo l'esecuzione di una funzione:

::::::::::::::
sig.c
::::::::::::::
#include <stdio.h>
#include <signal.h>

void f_intr(int sig)
{
printf("ricevuto il segnale numero : %d\n",sig);
}
int main(int argc, char *argv[])
{
int m,n;
struct sigaction sa;

sa.sa_handler = f_intr;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT,&sa,NULL);
for (m=0;m<100;m++)
{
for (n=0;n<200000000;n++);
printf("working\n");
}
return 0;
}

::::::::::::::
Dando il carattere di interruzione (CTRL-c) viene chiamata f_intr.

I segnali possono essere inviati anche con la s.c. kill e il comando kill (vedere il man corrispondente).
La sleep utilizzata in precedenza è tipicamente realizzata mediante i segnali (vedere man -s 3 sleep, man -s 2 alarm, e man -s 2 pause): alarm(n) "prenota" un segnale SIGALRM che verrà inviato al processo corrente dopo n secondi; pause() sospende il processo fino all'arrivo di un (qualunque) segnale, quindi se nel frattempo arriva qualche altro segnale, il processo viene risvegliato prima degli n secondi.

ESERCIZIO 2.8: eseguire l'eseguibile "sig" di "sig.c" in una finestra; da un'altra, individuare con "ps -a" il pid del processo che esegue "sig" ed inviargli con il comando kill il segnale di interruzione (SIGINT), quello di terminazione (SIGTERM) e (in una diversa esecuzione) quello di accesso non valido alla memoria (SIGSEGV).

ESERCIZIO 2.9: modificare il programma in modo da ignorare il segnale di interruzione.

ESERCIZIO 2.10: verificare, integrando gli esempi precedenti che utilizzano fork, exec e sigaction, se le disposizioni "ignorare il segnale" ed "eseguire una funzione" vengono:
Un'altra caratteristica utile dell'interfaccia detta "reliable signals" è la possibilità per un processo di chiedere che la consegna di alcuni segnali inviati al processo venga bloccata fino a quando il processo non dispone lo sblocco, o dispone di ignorare il segnale (si veda Stevens & Rago, "Advanced Programming in the Unix Environment").

****************************************

PIPES

****************************************

La system call:

     int pipe(int fildes[2]);

crea una pipe, valorizzando fildes[0] con un file descriptor (indice nella tabella dei files aperti) che può essere usato per leggere dalla pipe, e fildes[1] con uno che può essere utilizzato per scrivere.

L'esempio seguente qualcosa di simile a quanto avviene nell'interprete dei comandi per eseguire:
ls -l | wc -l
in cui lo standard output del comando "ls -l" (elenco in formato lungo dei file nella directory corrente) diventa lo standard input del comando "wc" (conta le righe di un file testo).

::::::::::::::
pipes.c
::::::::::::::

#include <stdio.h>
#include <unistd.h>
#include <wait.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
int pipefd[2];

pipe (pipefd);

if (fork() == (pid_t)0) {
/* primo figlio esegue "wc -l " (conta righe) prendendo
l'input da pipefd[0] */
dup2(pipefd[0],0);
close(pipefd[0]);
close(pipefd[1]);
execlp("wc","wc","-l",NULL);
perror("exec wc fallita");
}
else if (fork() == (pid_t)0) {
/* secondo figlio esegue "ls -l" mandando l'output
su pipefd[1] */
dup2(pipefd[1],1);
close(pipefd[0]);
close(pipefd[1]);
execlp("ls","ls","-l",NULL);
perror("exec ls fallita");
}
else
{ /* processo padre chiude entrambi gli estremi della pipe
e attende i figli */
close(pipefd[0]);
close(pipefd[1]);
wait(NULL);
wait(NULL);
}
return 0;
}
::::::::::::::

N.B. un motivo per cui ci sono le varie "close" è la buona norma di chiudere i file descriptor non utilizzati da un processo, per non occupare inutilmente elementi della tabella dei file aperti.
Inoltre, la chiusura della pipe in scrittura da parte dei processi che non la usano per scrivere (padre e primo figlio in questo caso) può essere essenziale per la terminazione del processo che legge dalla pipe: infatti "wc" continua a leggere finché non trova EOF (e non scrive finché non legge l'ultima riga, dovendole contare).
L'EOF si ottiene su una pipe quando non ci sono più processi che hanno la pipe aperta in scrittura (in questo caso, quando il processo che esegue "ls" termina).

ESERCIZIO 2.11: verificare che cosa accade se nel padre la close(pipefd[1]) viene tolta oppure messa dopo le wait.

La creazione della pipe avviene a livello delle system calls; per leggere e scrivere sulla pipe, si possono usare le chiamate di sistema read e write, o le funzioni di libreria:
Le pipes possono essere usate anche per comunicazioni "uno a molti", "molti a uno", "molti a molti". Si ha un comportamento nondeterministico del tipo "produttori e consumatori": se diversi processi scrivono sulla stessa pipe, nel flusso di dati risultante l'output dei diversi processi viene intercalato a seconda dell'effettivo ordine delle chiamate di sistema "write" sulla pipe da parte dei diversi processi; analogamente, se diversi processi leggono dalla stessa pipe, il flusso di dati viene consumato in parte da un processo, in parte dall'altro etc. a seconda di quando occorrono effettivamente le chiamate di sistema "read" e di quanti dati sono presenti nella pipe nel momento in cui occorrono.