LEZIONE del 15 Ottobre

Nella lezione precedente si è visto con un certo dettaglio quale sia la sequenza di boot eseguita dal sistema dall'istante dell'accensione del PC fino al momento in cui il sistema è pronto ad accettare i login degli utenti. La configurazione finale si sviluppa a vari livelli, partendo dal riconiscemento dei diversi dispositivi hardware di cui è fornito il sistema fino alla creazione dei servizi che permettono al sistema di essere effettivamente utilizzato.

Fra questi, il meccanismo di montaggio dei file-system è uno dei più importanti perchè consente una strutturazione delle informazioni accessibili del sistema arbitrariamente complessa. Inoltre è possibile mantenere una configurazione aperta, nel senso che il sistema riconosce il nuovo insieme di dati solo nel momento in cui il supporto fisico, su cui tali dati sono realmente registrati, è effettivamente presente (introduzione di floppy disk, cdrom, flash memory, ecc...).

La versatilità di tale meccanismo si basa sull'idea di poter sempre associare un dispositivo ad una directory di un file-system specificato, detto mounting point. Naturalmente si deve specificare, implicitamente od esplicitamente, qual'è la struttura effettiva delle informazioni contenute nel dispositivo, il cosiddetto file-system type in modo da poter associare, nel punto di montaggio l'opportuna interfaccia di conversione delle informazioni.

In questo modo è possibile navigare all'interno delle informazioni contenute nei vari dispositivi (che possono essere anche partizioni di dischi locali e remoti) in modo completamente uniforme, come se si stesse esplorando le sottodirectory e i file di una qualunque directory. L'eleganza della soluzione è tale da essere implementata in tutti i sistemi.

Nucleo di un Sistema Operativo

Il cuore dei Sistemi Operativi è sempre costituito dal suo nucleo, o kernel, il cui compito è quello di fornire alla sottostante piattaforma hardware quell'insieme di funzionalità che rendono l'elaboratore praticamente utilizzabile dall'utente finale.

Si parla allora di servizi che sono realizzati sia come programmi esterni che come chiamate di procedure interne al kernel. In entrambi i casi si utilizzano le chiamate di sistema le quali si basano sulla momentanea sospensione dell'attività corrente, ripresa dopo la fornitura del servizio.

Senza entrare nel dettaglio si può dire che un sistema operativo deve fornire un'interfaccia utente, che si basa su quello che costituisce lo scheletro del nucleo, ossia, la

al quale si aggiunge normalmente la gestione del file-system e della memoria virtuale. Inoltre, un sistema operativo deve provvedere alla gestione di tutti i dispositivi periferici (porte seriali e parallele, i bus USB, IDE o SCSI, schede grafiche, di rete, ecc...) che possono essere connessi all'elaboratore, compresa la connessione con altri elaboratori.

Ogni sistema operativo utilizza un proprio particolare metodo per generare tali funzionalità che possono ricondursi alla creazione di un certo numero di processi. Nel sistema operativo Unix tale soluzione è particolarmente evidente grazie alla manipolazione esplicita delle entità che rappresentano i programmi in esecuzione, detti perciò processi.

Ad esempio, con il comando

§ ps ax

il sistema Linux fornisce la seguente lista di informazioni

  PID TTY      STAT   TIME COMMAND
    1 ?        S      0:04 init [5]
    2 ?        SW     0:00 [keventd]
    3 ?        SW     0:00 [kapm-idled]
    4 ?        SW     0:00 [kswapd]
    5 ?        SW     0:00 [kreclaimd]
    6 ?        SW     0:00 [bdflush]
    7 ?        SW     0:00 [kupdated]
    8 ?        SW<    0:00 [mdrecoveryd]
   67 ?        SW     0:00 [khubd]
  394 ?        S      0:00 syslogd -m 0
  399 ?        S      0:00 klogd -2
  413 ?        S      0:00 portmap
  428 ?        S      0:00 rpc.statd
  480 ?        S      0:00 /sbin/cardmgr
  533 ?        S      0:00 /usr/sbin/apmd -p 10 -w 5 -W -P /etc/sysconfig/apm-sc
  582 ?        S      0:00 /usr/sbin/automount --timeout 60 /misc file /etc/auto
  594 ?        S      0:00 /usr/sbin/atd
  644 ?        S      0:00 sendmail: accepting connections
  657 ?        S      0:00 gpm -t ps/2 -m /dev/mouse
  669 ?        S      0:00 crond
  705 ?        S      0:01 xfs -droppriv -daemon
  736 tty1     S      0:00 /sbin/mingetty tty1
  737 tty2     S      0:00 /sbin/mingetty tty2
  738 tty3     S      0:00 /sbin/mingetty tty3
  739 tty4     S      0:00 /sbin/mingetty tty4
  740 tty5     S      0:00 /sbin/mingetty tty5
  741 tty6     S      0:00 /sbin/mingetty tty6
  742 ?        S      0:00 /usr/bin/kdm -nodaemon
  752 ?        S     77:35 /etc/X11/X -auth /etc/X11/xdm/authdir/authdir/authfil
  756 ?        S      0:00 -:0
  771 ?        S      0:00 ksmserver --restore
  850 ?        S      0:00 kdeinit: dcopserver --nosid
  852 ?        S      0:00 kdeinit: klauncher
  854 ?        S      0:00 kdeinit: kded
  857 ?        S      0:00 artsd -F 10 -S 4096
  862 ?        S      0:00 kdeinit: kxmlrpcd
  874 ?        S      0:00 kdeinit: Running...
  876 ?        S      0:00 knotify
  877 ?        S      0:09 kdeinit: kwin
  879 ?        S      0:01 kdeinit: kdesktop
  883 ?        S      0:03 kdeinit: kicker
  886 ?        S      0:05 kdeinit: klipper -icon klipper -miniicon klipper
  888 ?        S      0:00 kdeinit: khotkeys
  890 ?        S      0:00 kdeinit: kwrited

Esempio di Processi attivi di un Sistema

in cui ciascuna riga rappresenta uno dei processi sequenziali che, nel loro insieme, caratterizzano il funzionamento del sistema.

Configurazione del Sistema

Init è il padre di tutti i processi. Il suo compito principale è quello di creare processi da uno script immagazzinato nel file /etc/inittab. Questo file ha, di solito, delle voci che fanno sì che init avvii getty su ogni riga nella quale possono loggarsi gli utenti. Controlla anche processi autonomi richiesti da qualsiasi sistema.

Un runlevel è una configurazione software del sistema che permette l'esistenza solo di un gruppo selezionato di processi. I processi avviati da init per ognuno di questi runlevel sono definiti nel file /etc/inittab. Init può trovarsi in uno degli otto runlevel, da 0 a 6 e S o s. Il runlevel è cambiato da un utente privilegiato lanciando /sbin/telinit, il quale invia un segnale appropriato a Init, indicandogli a quale runlevel passare.

I runlevel 0, 1 e 6 sono riservati. Il runlevel 0 è usato per fermare (halt) il sistema, il runlevel 6 è usato per riavviare (reboot) il sistema e il runlevel 1 è usato per portare il sistema nel modo single user. Il runlevel S non è realmente pensato per essere direttamente usato, ma piuttosto per gli script che sono eseguiti quando si entra nel runlevel 1. Per maggiori informazioni si consultino le pagine di manuale relative ai comandi shutdown e inittab.

Sono validi anche i runlevel 7-9, sebbene non realmente documentati. Ciò perchè le varianti di Unix "tradizionali" non li usano. Si tenga presente che i runlevel S e s sono di fatto lo stesso perchè internamente sono un alias per lo stesso runlevel. Si tratta, in effetti, di ciè che rimane quando fu scritto per la la prima volta sysvinit.

AVVIO DEL SISTEMA

Dopo che init è stato lanciato come ultimo passo dell'avvio (boot) del kernel, cerca se nel file /etc/inittab c'è una voce del tipo initdefault (si veda inittab) con la quale viene specificato il runlevel iniziale del sistema. Se non c'è tale voce o non c'è il file /etc/inittab, è necessario immettere un runlevel dalla console del sistema.

Il runlevel S o s porta il sistema in modalità utente singolo (single user mode) e non necessita del file /etc/inittab. In modalità single user, viene lanciato /bin/sh sul dispositivo /dev/console.

Quando si entra in single user mode, init legge lo stato ioctl della console da /etc/ioctl.save. Se questo file non esiste, init inizializza la linea di collegamento a 9600 baud e con le impostazioni CLOCAL. Quando init lascia il single user mode, salva le impostazioni ioctl della console in questo file cosicchè possa riusarli nella successiva sessione in single-user.

Quando si entra per la prima volta in modalità multiutente, init esegue le voci boot e bootwait che permettono al filesystem di essere montato prima che un utente possa loggarsi. Poi sono eseguite tutte le voci corrispondenti al runlevel.

Quando avvia un nuovo processo, init per prima cosa guarda se esiste il file /etc/initscript. Se c'è, usa questo script per far partire il processo. Ogni volta che un processo figlio termina, init salva questa informazione e la ragione per la quale è morto nei file /var/run/utmp e /var/log/wtmp (se esistono).

CAMBIO DI RUNLEVEL

Dopo aver avviato tutti i processi specificati, init aspetta che uno dei suoi processi figlio muoia, un segnale di powerfail, o fino a che non riceve un segnale da /sbin/telinit per cambiare il runlevel del sistema. Quando avviene una delle tre condizioni suddette, riesamina il file /etc/inittab. A questo file possono essere aggiunte nuove voci quando si vuole. Comunque, init aspetta sempre che avvenga una delle tre condizioni suddette. Per ottenere una risposta istantanea, si possono usare i comandi Q o q per risvegliare init e fargli riesaminare il file /etc/inittab.

Se init non è in single user mode e riceve un segnale di powerfail, saranno invocate apposite voci per il powerfail. Quando a init è richiesto di cambiare il runlevel, invia il segnale di avviso SIGTERM a tutti i processi che non sono definiti nel nuovo runlevel. Poi aspetta 5 secondi prima di terminare forzatamente questi processi tramite il segnale di kill SIGKILL. Si noti che init assume che tutti questi processi (e i loro discendenti) rimangano nello stesso gruppo di processi nel quale init li ha creati in origine. Se un qualsiasi processo cambia il suo process group non riceverà questi segnali. Questi processi devono essere terminati a parte.

TELINIT

/sbin/telinit è linkato a /sbin/init. Accetta un carattere singolo come argomento e segnala a init di effettuare le azioni appropriate. I seguenti argomenti servono come direttive a /sbin/telinit:

/sbin/telinit può dire a init anche quanto tempo dovrebbe attendere tra l'invio ai processi del TERM signal e l'invio del KILL signal di default è 5 secondi, ma può essere cambiato con l'opzione -t sec. /sbin/telinit può essere invocato solo da utenti con i privilegi appositi.

STRUTTURA DI INITTAB

Il file inittab descrive quali processi sono avviati all'avvio e durante le normali operazioni. Init distingue tra diversi runlevel, ognuno dei quali può avere il suo insieme di processi da avviare. I runlevel validi sono quelli indicati in precedenza e compaiono una per riga secondo il seguente formato:

id:runlevel:azione:processo

Le righe che iniziano con `#' sono ignorate.

Le azioni valide sono:

Il campo runlevel può contenere più caratteri per i diversi runlevel, p.es. &123 se il processo dovesse essere avviato nei runlevel 1, 2 e 3. Le voci ondemand possono contenere un A, B o C. I campi runlevel delle voci sysinit, boot e bootwait sono ignorati.

Quando si cambia runlevel, qualsiasi processo in esecuzione che non è specificato nel nuovo runlevel verrà terminato, prima con SIGTERM, e poi con SIGKILL.

ESEMPI

Il primo esempio di tabella inittab è basato sulle originarie tabelle delle prime distribuzioni di Linux. In essa si vede che l'ultima fase del boot è determinata dall'esecuzione dei comandi contenuti in /etc/rc per poi lanciare getty sui terminali alfanumerici tty1-tty4.

# inittab per Linux
id:1:initdefault:
rc::bootwait:/etc/rc
1:1:respawn:/etc/getty 9600 tty1
2:1:respawn:/etc/getty 9600 tty2
3:1:respawn:/etc/getty 9600 tty3
4:1:respawn:/etc/getty 9600 tty4
Esempio 1 di Tabella initab

Il secondo esempio proposto presenta una tabella inittab molto più elaborata in cui compaiono diversi runlevel (si vedano i commenti all'interno) nello stile delle tabelle inittab delle attuali distribuzioni di Linux.

# Livello in cui si entra
id:2:initdefault:

# Inizializzazione del sistema prima di ogni altra cosa.
si::sysinit:/etc/rc.d/bcheckrc

# I runlevel 0,6 sono halt e reboot, 1 e' il modo per la
# manutenzione
l0:0:wait:/etc/rc.d/rc.halt
l1:1:wait:/etc/rc.d/rc.single
l2:2345:wait:/etc/rc.d/rc.multi
l6:6:wait:/etc/rc.d/rc.reboot

# Cosa fare quando viene premuta la combinazione di tasti CTRL+ALT+DEL.
ca::ctrlaltdel:/sbin/shutdown -t5 -rf now

# Runlevel 2&3: getty sulla console, il livello 3 fa
# anche getty sulla porta del modem.
1:23:respawn:/sbin/getty tty1 VC linux
2:23:respawn:/sbin/getty tty2 VC linux
3:23:respawn:/sbin/getty tty3 VC linux
4:23:respawn:/sbin/getty tty4 VC linux
S:23:respawn:/sbin/uugetty ttyS2 M19200

Esempio 2 di Tabella inittab

Come si è già accennato ad inizio di lezione, un aspetto molto importante della configurazione iniziale del sistema e realizzata durante l'ultima fase del boot, è quello che si chiama il montaggio dei file-system. Come'è noto il sistema operativo UNIX mette a disposizione dell'utente un certo numero di file-system che possono risiedere localmente sulla propria stazione di lavoro oppure remotamente su server NFS.

Il comando mount, previa consultazione del file /etc/fstab esegue il cosidetto montaggio dei file-system. La tabella seguente ne è un esempio standard per le distribuzioni di Linux.

/dev/hda2        /                ext2      defaults         1 1
/dev/hda5        /usr             ext2      defaults         1 2
/dev/hda6        /home            ext2      defaults         1 2
/dev/hda7        swap             swap      defaults         0 0
/dev/hda8        /var             ext2      defaults         1 2
/dev/hda1        /mnt/dos         fat32     noauto           0 0
/dev/fd0         /mnt/floppy      auto      noauto,owner     0 0
/dev/cdrom       /mnt/cdrom       iso9660   noauto,owner,ro  0 0
none             /proc            proc      defaults         0 0
none             /dev/pts         devpts    gid=5,mode=620   0 0

Esempio di Tabella fstab

Come si può notare ciscuna riga specifica un dispositivo, a cui rimane associato un driver ed una partizione (per esempio, hda per il disco fisso con 5 per indicare una possibile partizione), la radice del file-system, tipicamente strutturata ad albero (per esempio, /usr), un tipo (per esempio, ext2) ed alcune informazioni supplementari.

Linguaggio dei Comandi

Il Sistema Operativo UNIX mette a disposizione dell'utente un certo numero di linguaggi di comandi, ciascuno dei quali caratterizzato da un diverso livello di flessibilità e da differenti standard di riferimento. Fra questi i più noti sono la Bourne shell e la C shell, la prima essendo uniforme rispetto ai diversi dialetti di Unix (bsd4.2, SunOS, Solaris, Ultrix, Linux, Aix, SystemV, ecc...), la seconda potendo differire anche sostanzialmente fra piattaforme e versioni.

Caratteristica fondamentale di tutti questi linguaggi è la struttura generale dei comandi

CommandName   -Option   Argument

a cui si aggiunge un elevata flessibilità nella gestione dei file, attraverso l'uso delle pipe e la ridirezione dei flussi di input e di output. A seconda delle necessità i dati possono essere fatti fluire da un comando all'altro, essere prelevati dall'input o da qualunque variabile, prodotti in output o concatenati ad altri file, ecc... Inoltre è disponibile un manuale in linea in grado di fornire con sufficiente livello di dettaglio informazioni su tutti i comandi disponibili relativamente alla piattaforma in uso.

Eseguendo i comandi

man sh
man csh
è possibile ottenere abbastanza informazioni per utilizzare correttamente la Bourne shell e la C shell, rispettivamente. Per quanto riguarda l'impiego dei comandi e l'accesso all'input/output valgono le seguenti osservazioni

Parametro

Il carattere dollaro ($) aggiunto in testa ad una stringa di caratteri serve a specificare che questa rappresenta un parametro della shell. I parametri della shell possono essere di due tipi L'uso delle parentesi quadre serve ad identificare le diverse componenti di un parametro, tipicamente, quando questo rapparesenta un vettore di elementi.

Costrutti di Controllo

Per quanto riguarda i costrutti di controllo questi sono diversi a seconda che si tratti di Bourne shell o C shell. La tabella che segue ne da la lista mettendo in evidenza analogie e differenze.

COSTRUTTO
BOURNE SHELL
C-LIKE SHELL

Ciclo For

for name in word do
....
done
foreach var (wordlist)
....
end

Alternativa-1

if command; then .... ; if (expr) command

Alternativa-2

if command; then
.... ;
elif command; then
.... ;
else
.... ;
fi
if (expr) then
....
else if (expr2) then
....
else
....
endif

Assegnazione

var=value
var[n]=word
set var = value
set var[n] = word

Selezione

case word in
pattern) .... ;;
pattern) .... ;;
....
esac
switch (string)
case label:
....
breaksw
endsw

Ciclo While

while list do
....
done
while (expr)
....
end

Si tenga, comunque, presente che nel caso della Bourne shell non esistono espressioni booleane in senso stretto da valutare, ma viene utilizzato il codice di ritorno associato all'esecuzione di ciascun comando o lista di comandi per ottenere il corretto valore true o false. Il comando che esegue i "test" nella Bourne shell può essere indifferentemente test oppure [ ... ].

Negli esempi che seguono sono riportati alcune caratteristiche salienti sia della Bourne shell che della C shell. In essi compaiono alcuni comandi di utilità generale il cui impiego e spiegazione dettagliata possono ottenersi consultando il manuale in linea con man <nomecomando>.

Identificazione di un Processo

Il primo esempio proposto consente di determinare l'identificatore di processo associato all'esecuzione di un dato programma.

# fpid <progname>
# fpid : find the process identifier associated
# to a given running program
#
if [ "$#" -eq 0  ]; then
  echo "Usage: fpid <processname>"
  exit 1
fi
#
A=`ps -e | grep -e "\b$1\b" | grep -v grep` ;
if [ "$A" == "" ]
  then
    echo "no such a process" ;
    exit 2 ;
fi
K=1
P=1
for ITEM in $A ;
do
  let "M = $K % 4" ;
  if [ "$M" -eq 1 ]; then
    echo "<$P> : $ITEM" ;
    let "P = $P + 1" ;
  fi
  let "K = $K + 1" ;
done
exit 0
Quale Programma esegue un dato Processo

Dislocazione dei Comandi

Il seguente programma permette di determinare dove risiede il comando il cui nome è passato come argomento. Viene utilizzato il comando sed il cui scopo è quello di fornire funzioni di editing.

#! /bin/sh
#
#  where cmd : where is (the path of) the cmd command
#
if [ "$#" -eq 0  ]; then
  echo "Usage: where <commandname>"
  exit 1
fi
#
opath=$PATH
PATH=/bin:/usr/bin

case $# in
0) echo 'Usage: where command'  1>&2; exit 2
esac
for i in `echo $opath | sed 's/::/.:/
                             s/::/:.:/g
                             s/:$/:./
                             s/:/ /g'`
do
   if test -f $i/$1
     then
       echo $i/$1
       exit 0
   fi
done
exit 1
Dislocazione degli Eseguibili