You are on page 1of 35

Fondamenti di Informatica

Appunti dalle lezioni

Stefano Marrone

October 6, 2019
Questa dispensa ha lo scopo di fornire allo studente una trattazione sintetica degli
argomenti teorici relativi all’insegnamento di Fondamenti di Informatica per il corso
di Laurea in Matematica presso il Dipartimento di Matematica e Fisica dell’Università
della Campania “Luigi Vanvitelli”.
1 Introduzione
1.1 Studio dell’informatica
L’informatica riveste un ruolo essenziale nella vita di tutti i giorni diventando uno degli
strumenti principali nell’analisi e nella manipolazione del mondo contemporaneo. La
preparazione di uno studente in materie scientifiche non puó, pertanto, prescindere
dallo studio di tale materia.
La nostra vita é ormai permeata in tutte le sue forme dalla presenza del calcolatore:
non esiste nessun aspetto in cui il calcolatore non abbia fatto sentire il suo peso cam-
biando non solo il modo in cui i prodotti ed i servizi sono realizzati ed offerti, ma anche
l’interazione tra l’uomo e gli oggetti stessi. Basti pensare ad ambiti quali il diverti-
mento (cinema, videogiochi, intrattenimento, ...) oppure le comunicazioni (Internet,
telefonia mobile e fissa, email, ...) fino ad arrivare a settori vitali come la medicina con
la presenza di dispositivi come i pacemaker. Questo fenomeno é ormai cosı́ irreversibile
che l’immagine stessa di alcuni prodotti e servizi é totalmente cambiata da quando il
calcolatore é entrato in essi (es. l’auto).
L’informatica ed i calcolatori sono ormai diventati essenziali anche nello studio delle
scienze. Intuitivamente le scienze di tipo sperimentale traggono grande giovamento
dalla presenza del calcolatore: il computer é diventato ormai il centro di tutti i labora-
tori poiché unisce alla possibilitá di ricreare i piú disparati ambienti attraverso simu-
lazioni software, la capacitá di calcolare funzioni estremamente complesse con grande
precisione ed affidabilitá. Inoltre la sua grande velocitá lo rende strumento indispens-
abile nell’acquisizione, memorizzazione ed analisi di grandi quantitá di dati, al centro
di ogni esperimento di scienza applicata (fisica sperimentale, biologia, ...). Oltre a tali
discipline, il calcolatore sta diventando uno strumento sempre piú importante anche
nella matematica dato il suo utilizzo per risolvere problemi difficilmente risolvibili per
via analitica. Tali problemi includono non solo equazioni differenziali spesso risolti in
via numerica ma anche teoremi dimostrati solo dopo l’avvento del calcolatore (come
ad esempio il teorema dei quattro colori).

1.2 Concetti fondamentali


Alla base dello studio dei calcolatori e della programmazione c’é il concetto di elabo-
razione:

Y = F (X)

dove:

3
• X: insieme degli ingressi;
• Y: insieme delle uscite;

• F: funzione che esegue in modo automatico (non necessariamente in un solo


passo) la corrispondenza tra gli ingressi e le uscite.

Il fulcro é costituito dalla caratteristica di automazione del calcolo della funzione:


non basta che la funzione sia calcolabile, che un uomo possa fare la computazione nel
modo piú naturale possibile: é necessario che il calcolo possa essere effettuato senza
l’ausilio dell’uomo affinché si possa parlare di calcolo automatico, ossia di argomenti di
interesse per l’informatica. Esempi di elaborazione sono il calcolo della primalitá di un
numero intero, l’arrangiamento crescente vettore di numeri reali oppure l’elaborazione
grafica di disegni complessi.
Una ulteriore importante caratteristica che esce fuori dalla definizione sopra descritta
é legata alla natura degli insiemi di ingresso e di uscita della funzione, ossia ad X ed
Y. Siamo interessati ad insimi di natura discreta.
Questa funzione viene in genere costruita attraverso la definizione di un algoritmo.
Si definisce algoritmo una sequenza finita di passi che risolve automaticamente un
problema. Esempi di algoritmi si possono trovare nella vita di tutti i giorni: conside-
riamo come esempio quello di fare la spesa al supermercato (vedi Fig. 1.1).
In che senso l’esempio ricalca la definizione data? Innanzitutto vediamo che :
• sequenza: le azioni vengono svolte una alla volta (non c’é parallelismo);

• finita: la lista della spesa é un qualcosa di finito ed inoltre il numero di azioni che
si svolgono é sempre finito (non si rischia di stare in eterno nel supermercato!);
• passi : ogni singolo passo (riempire il carrello, cercare sullo scaffale, ...) non im-
plica per sé il concetto di spesa: é solo nella sequenza (ossia nella concatenazione
intelligente di tali passi) che il problema viene risolto;

• automaticamente: una volta scritto e lanciato l’algoritmo, non c’é bisogno di


intervento esterno; l’esecutore ha tutti gli elementi per poter risolvere il problema.
Un programma invece é l’implementazione di un algoritmo in un ben definito
linguaggio di programmazione.
Chi esegue un algoritmo? Qualsiasi entitá in grado di eseguire un algoritmo viene
detta esecutore di quell’algoritmo. Relativamente all’esempio precedente, il cliente
del supermercato é l’esecutore dell’algoritmo della spesa. Tra i possibili esecutori di un
algoritmo annoveriamo il calcolatore e l’uomo. Quali sono le differenze fondamentali
tra i due (quando ovviamente li vediamo come esecutori)? Il computer ha dalla sua
una grande precisione e velocitá che fa di lui l’esecutore ideale per algoritmi dove é
richiesta una grande quantitá di calcoli con requisiti di precisione molto spinti. D’altro
canto il piú grande difetto di un calcolatore é il fatto che non é cosciente di sé e quindi
non é cosciente delle cose che fa. Un calcolatore, anche il piú potente e sofisticato,
potrebbe calcolare la primalitá di migliaia di numeri interi senza sapere il significato dei

4
Leggi dalla lista INIZIO

Cerca prodotto

NO
è sullo scaffale?

SI

Metti prodotto
nel carrello

Cancella prodotto
dalla lista

NO SI
lista vuota? Vai alla cassa FINE

Figure 1.1: Esempio di algoritmo

test che sta eseguendo. Inoltre un calcolatore esegue solo ció per cui é programmato:
non improvvisa e non inventa (se non nei limiti di quanto é programmato appunto!).
Un concetto fondamentale é quello di sequenza statica e dinamica. La sequenza stat-
ica definisce le azioni da svolgere indipendentemente dai dati specifici che ritroviamo
in ingresso: le azioni che un esecutore del nostro algortimo di esempio deve svolgere
in modo del tutto indipendente da quanti (e quali) elementi sono presenti nella lista.
Questa é una sequenza “astratta”, nel senso che non tiene conto di attivitá che possono
ripetersi (considerando ad esempio una lista con due o piú prodotti presenti sullo scaf-
fale, l’azione Metti prodotto nel carrello verrá eseguita piú volte). Di natura contraria
é la sequenza dinamica che fotografa la sequenza esatta di azioni considerando anche
le ripetizioni: ovviamente tale sequenza é dipendente dai dati in ingresso. Facciamo
un esempio sulla base dell’algoritmo prima descritto:
Caso 1; Lista=(petto di pollo, vino bianco, insalata):
Sequenza dinamica
1. INIZIO;
2. leggi dalla lista (petto di pollo);

5
3. cerca prodotto;
4. (é sullo scaffale?: SI) metti prodotto nel carrello;

5. cancella prodotto dalla lista;


6. (lista vuota?: NO) leggi dalla lista (vino bianco);
7. cerca prodotto;
8. (é sullo scaffale?: SI) metti prodotto nel carrello;

9. cancella prodotto dalla lista;


10. (lista vuota?: NO) leggi dalla lista (insalata);
11. cerca prodotto;

12. (é sullo scaffale?: SI) metti prodotto nel carrello;


13. cancella prodotto dalla lista;
14. (lista vuota?: SI) Vai alla cassa;
15. FINE.

Alternativamente presentiamo un secondo esempio che, sulla base dello stesso algo-
ritmo (e quindi della stessa sequenza statica di istruzioni) genera una lista dinamica
diversa.
Caso 2: Lista=(caffé, filetto di tricheco)
Sequenza dinamica

1. INIZIO;
2. leggi dalla lista (caffé);
3. cerca prodotto;
4. (é sullo scaffale?: SI) metti prodotto nel carrello;

5. cancella prodotto dalla lista;


6. (lista vuota?: NO) leggi dalla lista (filetto di tricheco);
7. cerca prodotto;

8. (é sullo scaffale?: NO) cancella prodotto dalla lista;


9. (lista vuota?: SI) Vai alla cassa;
10. FINE.

6
1.3 Informazione, dato e valore
Cosa é e da dove viene l’informazione? Cerchiamo di spiegare questo concetto a partire
da esempi concreti. Analizziamo le seguenti frasi: π vale 3,14159...; oggi non piove;
questo é il corso di Fondamenti di Informatica. Queste sono chiaramente frasi che
portano una qualche informazione: ma cosa hanno in comune? Scorgiamo quelli che
sono i fattori unificatori: ognuna delle frasi d’esempio determina una risposta (val-
ore) rispetto ad un insieme di risposte possibili (tipo) all’interno di un contesto ben
preciso (attributo). L’informazione puó essere dunque formalizzata come una tripla di
(attributo,tipo,valore). Rispetto ai nostri esempi:

• (il numero π, insieme dei numeri reali, 3,14159...);


• (stato delle precipitazioni odierne, {vero,falso}, falso);
• (nome del corso, insieme delle stringhe, “Fondamenti di Informatica”).

Il tipo di un’informazione é dunque l’insieme dei possibili valori che quell’informazione


puó assumere. Un attributo é invece il contesto dell’informazione é da il significato
all’informazione stessa: senza l’attributo, il valore non avrebbe significato; senza il
tipo, non si capirebbe in quale intervallo varia il valore.
Non dobbiamo adesso confondere il valore di un’informazione con la sua rappre-
sentazione. Questo concetto viene chiaramente definito dalla nozione di dato ossia il
modo in cui un valore viene rappresentato. A seconda del tipo di rappresentazione
che utilizziamo possiamo notare in modo differente anche valori uguali. Considerando
un valore di tipo intero pari a tre, questo puó essere rappresentato sia dalla notazione
decimale “3” sia attraverso la notazione numerale romana “III”. Le funzioni che trasfor-
mano un valore in dato e un dato in valore si chiamano rispettivamente funzioni di
codifica e decodifica.
Tutti i concetti sopra esposti sono raffigurati in Fig. 1.2.
Cerchiamo di formalizzare adesso i concetti di codifica e decodifica.
Consideriamo un tipo T con i suoi valori:

T = {x1 , . . . , xn }

e sia E un insieme di simboli:

E = {e1 , . . . , ek }

Su tale insieme definiamo l’insieme E + come l’insieme di tutte le stringhe di lunghezza


arbitraria ottenute concatenando i simboli di E. Ad esempio, considerando E = {α, β},
avremo che E + = {α, β, αα, αβ, βα, ββ, ααα, ααβ, . . .}.
Definiamo come codifica, la funzione C:

C : T −→ E +

dove si dice li lunghezza della codifica di xi il numero dei simboli contenuti in C(xi )
ossia |C(xi )|. La funzione inversa a quella di codifica sui dice decodifica:

7
INFORMAZIONE

TIPO ATTRIBUTO
VALORE

DECODIFICA CODIFICA

DATO

Figure 1.2: Informazione

D : E + −→ T

tale che ∀xi ∈ T, D(C(xi )) = xi . Una delle prime classificazioni che é possibile
fare in tale ambito é quella tra codifiche a lunghezza fissa e variabile. Una codifica
si dice di lunghezza fissa se e solo se ∀xi ∈ T, |C(xi )| = m, ossia se tutte le stringhe
codificanti i valori di T hanno la stessa lunghezza; in caso contrario si parla di codifica
a lunghezza variabile. Ecco alcuni esempi: supposti T = {a, b, c, d} e E = {α, β}, una
possibile codifica a lunghezza fissa é quella per cui:

• C(a) = αα,

• C(b) = αβ,
• C(c) = βα,
• C(d) = ββ

mentre una possibile codifica a lunghezza variabile puó essere:

• C(a) = α,
• C(b) = βββ,
• C(c) = βα,

• C(d) = βαβ

Senza entrare troppo nel dettaglio di una trattazione formale, enucleiamo alcune
caratteristiche che distinguono le codifiche fisse e quelle variabili: quelle a lunghezza
fissa sono di per certo piú facili da interpretare in quanto sappiamo a priori la lunghezza
delle stringhe di dato e quindi riusciamo facilmente a dire quando finisce la codifica di

8
un valore dell’insieme di partenza e quando comincia il successivo; tale semplicitá fa sı́
che la codifica e decodifica a lunghezza fissa siano facilmente implementabili attraverso
circuiti elettronici. Una codifica a lunghezza variabile invece puó essere piú efficiente
se consideriamo che i simboli non sono equiprobabili, ad esempio se nel nostro caso
il valore a si presenta nella stragrande maggioranza dei casi, assegnando ad esso una
stringa di lunghezza minore (anche minore di quella che avremmo nella codifica di
lunghezza fissa), avremmo un risparmio nella lunghezza media delle stringhe di dato.
Introduciamo adesso l’alfabeto binario B = {0, 1} avente due simboli detti BIT (BI-
nary digiT); esso é l’alfabeto di simboli per la codifica piú usato nella programmazione
e nel mondo di calcolatori data la sua estrema semplicitá e versatilitá. I Bit sono il
fondamento dei calcolatori elettronici.

1.3.1 Sistemi di numerazione


In questa sezione parleremo piú nello specifico di sistemi di rappresentazione numerica
e dei concetti di base della numerazione.
Cosı́ come la conosciamo noi, la numerazione si basa sul concetto di notazione po-
sizionale dove, a dispetto di un numero finito di simboli (le cifre di un alfabeto), é
possibile rappresentare un insieme infinito di valori (numerici). Ció viene realizzato
dando un valore diverso ad ogni simbolo a seconda della posizione che occupa all’interno
della stringa di dato. Un esempio é dato dalla rappresentazione decimale del numero
trenta:

30 = 3 · 101 + 0 · 100

oppure centosettantadue e quindici centesimi che viene codificato secondo la no-


tazione decimale in:

172, 15 = 1 · 102 + 7 · 101 + 2 · 100 + 1 · 10−1 + 5 · 10−2

Questo principio non si applica esclusivamente alla rappresentazione decimale: es-


istono ulteriori sistemi di numerazione caratterizzati dalla loro base, ossia dal numero
di simboli che sono ivi contenuti. Abbiamo:

• sistema binario (base 2): {0, 1};

• sistema ottale (base 8): {0, 1, 2, 3, 4, 5, 6, 7};


• sistema decimale (base 10): {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
• sistema esadecimale (base 16): {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F }.

In generale quindi possiamo scrivere una famiglia di funzioni di decodifica (dal dato
al valore) relativamente ad un sistema di rappresentazione posizionale in base b.
Data una stringa xn−1 xn−2 . . . x1 x0 , x−1 x−2 . . . x−m+1 x−m , tale che:

∀i ∈ {−m, . . . , n − 1} 0 ≤ xi ≤ b − 1

9
il valore di tale numerale é dato da:
Pn−1
i=−m xi · bi

Applichiamo tale concetto dunque anche a sistemi di numerazione non decimale;

1001B = 1 · 23 + 0 · 22 + 0 · 21 + 1 · 20 = 9

oppure

F 4H = 15 · 161 + 4 · 160 = 244

Per quanto riguarda invece la codifica evidenziamo una procedura di passaggio da


una notazione decimale a quella binaria. Questa procedura é descritta dall’algoritmo
illustrato in Figura 1.3.

INIZIO

assegna il valore di
naq

q/b

assegna a q il Prendi il resto e fanne la


quoziente di q/b cifra meno significativa

NO

il quoziente è nullo?

SI

FINE

Figure 1.3: Algoritmo per la codifica nei sistemi di numerazione

Innanzitutto si prende il numero da codificare e si divide per la base: il resto di tale


divisione diventa la cifra meno significativa dela stringa codificante il nostro numero.
Successivamente si testa se il quoziente di tale divisione é nullo, in tal caso, l’algoritmo

10
si arresta, altrimenti si ripete l’operazione sul quoziente finché esso non si annulla.
Facciamo l’esempio del numero 123: in pratica basta scrivere il numero 123 su un
foglio e tracciare accanto ad esso una linea verticale verso il basso che ci aiuterá nel
calcolo. A questo punto dividiamo il numero 123 per 2 (ottenendo 61) e scriviamo il
resto (1) alla destra della riga. Il 61 ottenuto lo scriveremo invece sotto al numero
precedente (123) e ripeteremo l’operazione fino a che il numero alla sinistra della riga
non diventi 1. A questo punto basta leggere la serie di 1 e 0 ottenuta (dal basso verso
l’alto) per ottenere il corrispondente binario del nostro 123.
Un caso particolare é dato dalla conversione tra le basi 16 e 2. Infatti essendo 16
una potenza di 2 (secondo l’esponente 4) é possibile passare da binario a esadecimale
(e viceversa) in modo semplice. Data una stringa di n bit possiamo raggrupparli a
partire dalla cifra meno significativa a quella piú significativa in gruppi da 4 e far
corrispondere ad ogni gruppo una cifra esadecimale: in altre parole volendo convertire
il numero binario 101100101001 possiamo scrivere:

|{z } 0010
1011 | {z} 1001
|{z }
B 2 9

oppure verso il sistema ottale:

101 |{z}
|{z} 100 |{z}
101 |{z}
001
5 4 5 1

Dualmente possibile passare dall’esadecimale (ottale) al binario decodificando ogni


cifra in quattro (tre) cifre binarie. Esempio:

F |{z}
|{z} 8 |{z}
C |{z}
1
1111 1000 1100 0001

1.4 Algebra di Boole (cenni)


Consideriamo l’insieme K={0,1} e siano su di esso definite le seguenti operazioni:

• + : K × K → K OR
• · : K × K → K AND
• · : K → K NOT
Tali funzioni sono definite cosı́ come illustrato nelle tabelle di veritá espresse:
Definiamo Algebra di Boole < K, +, ·, · >. In tale algebra si puó dimostrare che
valgono le seguenti proprietá:

∀x ∈ K
x+0=x
x·1=x
x+1=1

11
Table 1.1: Tabella di veritá della OR
A B A+B
0 0 0
0 1 1
1 0 1
1 1 1

Table 1.2: Tabella di veritá della AND


A B A·B
0 0 0
0 1 0
1 0 0
1 1 1

x·0=0
x+x=x
x·x=x
x+x=1
x·x=0
x=x

Valgono inoltre le proprietá commutative ed associative di somma e prodotto nonché


le proprietá distributive di somma e prodotto. Vale inoltre il teorema di De Morgan:

∀x, y ∈ K, x + y = x · y e x · y = x + y.

E’ ulteriormente dimostrabile che l’insiemde delle operazioni AND, NOT e OR costi-


tuiscono un’insieme funzionalmente completo, ossia é possibile a partire da tali funzioni
costruire una qualsivoglia operazione logica sull’insieme K in un qualsiasi numero di
variabili.

1.5 Automi
Altro concetto cardine dell’informativa é quello di automa a stati finiti. Si definisce
automa a stati finiti la sestupla:

Table 1.3: Tabella di veritá della NOT


A A
0 1
1 0

12
< Q, I, O, q0 , δ, ε > dove:
Q: insieme non nullo degli stati,
I: insieme dei simboli (alfabeto) in ingresso,
O: insieme dei simboli (alfabeto) in uscita,
q0 : stato iniziale dell’automa,
δ: funzione di transizione di stato tale che δ : Q × I → Q,
ε: funzione di uscita tale che ε : Q × I → O.

Esistono essenzialmente due tipi di automi a stati finiti: gli automi di Mealy e quelli
di Moore. La differenza sostanziale tra questi due tipi di automi risiede nel fatto che,
in sostanza, negli automi di Moore l’uscita é funzione del solo stato e non degli ingressi.
Formalmente:
AM oore < Q, I, O, q0 , δ, ε > dove:
Q: insieme non nullo degli stati,
I: insieme dei simboli (alfabeto) in ingresso,
O: insieme dei simboli (alfabeto) in uscita,
q0 : stato iniziale dell’automa,
δ: funzione di transizione di stato tale che δ : Q × I → Q,
ε: funzione di uscita tale che ε : Q → O.

Di particolare interesse risultano le notazioni attraverso le quali specificare le due


funzioni (δ e ε): ve ne sono due principali, quella grafica e quella tabellare del tutto
equivalenti nella loro capacitá espressiva. Al fine di essere chiaro nella trattazione si
fará l’esempio del riconoscitore di sequenze binarie 1101.

1.5.1 Notazione grafica


Il riconoscitore di sequenze 1101 é descritto dalle Fig. 1.4 e Fig. 1.5. In tali figure
sono descritti gli stati come cerchi e gli archi come le possibili transizioni da uno stato
all’altro. Lo stato iniziale é notato come un doppio cerchio concentrico. La Fig. 1.4
descrive l’automa di Mealy dove la funzione di transizione δ é espressa attraverso un
arco orientato (dal vecchio al nuovo stato) mentre la funzione di uscita ε é espressa
nell’etichetta dell’arco (ingresso/uscita).
L’automa di Moore in Fig. 1.5 invece esprime l’uscita direttamente nello stato
(nome/uscita).

1.5.2 Notazione tabellare


Accanto alle notazioni grafiche sopra illustrate esistono notazioni tabellare meno in-
tuitive ma piú chiare da schematizzare e quindi di piú facile implementazione in un
calcolatore. Per quanto riguarda la macchina di Mealy otteniamo che la Tab. 1.4 rapp-
resenta la macchina sopra illustrata mentre le Tab. 1.5 e Tab. 1.6 illustrano la analoga
di Moore.

13
Table 1.4: Versione tabellare della macchina di Mealy in Fig.1.4
0 1
S0 S0/0 S1/0
S1 S0/0 S2/0
S2 S3/0 S2/0
S3 S0/0 S0/1

Table 1.5: Versione tabellare (funzione δ) della macchina di Moore in Fig.1.5


0 1
S0 S0 S1
S1 S0 S2
S2 S2 S3
S3 S0 S4
S4 S0 S1

Table 1.6: Versione tabellare (funzione ε) della macchina di Moore in Fig.1.5


S0 0
S1 0
S2 0
S3 0
S4 1

14
1/0

0/0
S0 S1

0/0

0/0
1/0
1/1

1/0

S3 S2
0/0

Figure 1.4: Automa di Mealy del riconoscitore di sequenze 1101

1.6 Macchina di Turing


Nel 1936 Alan Turing formuló un modello matematico di una macchina poi rivelatasi
di fondamentale importanza per il calcolo automatico tutto: la macchina di Turing.
Essenzialmente immaginiamo di avere un nastro di lunghezza infinita, diviso in celle,
ognuna delle quali in grado di memorizzare due simboli (0 ed 1 per comoditá) oppure
la loro assenza (simbolo di blank β). Sia inoltre presente una testina in grado di
legger/scrivere su una cella del nastro e di muoversi lungo esso (sia verso destra che
verso sinistra) (vedi Fig. 1.6).
Piú formalmente una macchina di Turing é costituita dalla seguente: < S, F, s0 , A, δ, β >
dove:
S: insieme non nullo degli stati,
F : insieme degli stati finali della macchina (S ⊃ F ),
s0 : stato iniziale della macchina (s0 ∈ S),
A: alfabeto di ingresso/uscita della macchina (anche detto insieme dei simboli del
nastro),
δ: funzione di transizione della macchina tale che δ : S × A → S × A × {−1, 0, 1}1
β: simbolo di blank.
Si voglia fare l’esempio di una macchina di Turing che controlli se il numero di uno
su un nastro é pari o dispari e che scriva, all’occorrenza del primo blank, 1 se tale
valore é dispari, zero altrimenti. La Tab. 1.7 illustra tale esempio.

1 -1,0,1
sono tre simboli convenzionali che stanno per muovi di uno step a destra, stai fermo e muovi
di una posizione a sinistra

15
1

0
S0/0 S1/0

0 1
1
0

S4/1 1

S2/0
0
1 S3/0

Figure 1.5: Automa di Moore del riconoscitore di sequenze 1101

Table 1.7: Esempio di macchina di Turing


0 1 β
S0 S0/0/1 S1/1/1 SSTOP/0/0
S1 S1/0/1 S0/1/1 SSTOP/1/0
SSTOP - - -

Si S0 lo stato iniziale e SSTOP quello finale.


Si riportano adesso alcuni risultati teorici molto importanti. Tutto ció che é com-
putabile puó essere realizzato attraverso una macchina di Turing. Tutto ció che non
puó essere realizzato attraverso una macchina di Turing non é calcolabile, ossia non é
esprimibile mediante una funzione automatica.

1.7 Modello di Von Neumann


Introduciamo in questa sezione il modello teorico di esecutore che sta alla base dei
moderni calcolatori elettronici.
In Fig. 1.7 sono raffigurati i componenti di questo modello. Tali elementi rappre-
sentano le funzionalitá di un esecutore nel modello di Von Neumann e sono collegati
attraverso due tipologie di flussi. Il primo é il flusso dei dati (tratto grigio continuo)
che rappresenta il passaggio di dati da un elemento all’altro del modello; il secondo

16
Figure 1.6: Schema di principio della macchina di Turing

MEMORIA

DATI

UNITA’ DI
PROGRAMMI UNITA’ DI
INGRESSO
USCITA

UNITA’ DI UNITA’ LOGICO-


CONTROLLO ARITMETICA

Figure 1.7: Modello di Von Neumann

é relativo al flusso di controllo (linea nera tratteggiata) che indica invece che un
elemento controlla un altro, ossia determina cosa il secondo componente deve fare. Ci
sono cinque elementi nel modello: l’unitá di ingresso, responsabile dell’acquisizione dei
dati di ingresso dal mondo esterno, la memoria, responsabile della memorizzazione dei
dati in attesa di una loro elaborazione, l’unitá logico-aritmetica, in grado di svolgere le
operazioni logiche ed aritmetiche necessarie alla trasformazione dei dati memorizzati;
l’unitá di uscita, in grado di interfacciarsi con le periferiche di uscita del nostro calcola-
tore e l’unitá di controllo avente la responsabilitá di controllare l’intero procedimento
di elaborazione.
Una delle caratteristiche piú importanti del modello di Von Neumann é che sia i
programmi che i dati sono memorizzati in modo indifferenziato nella stessa memoria
e solo lo stato dell’elaborazione determina se si sta leggendo dalla memoria un dato o
un’istruzione del programma.
Per comprendere fino in fondo il modello di Von Neumann occorre introdurre il mod-
ello comportamentale del processore, anche detto ciclo del processore (vedi Fig.1.8).
All’accensione del calcolatore il processore avvia la fase di bootstrap (avvio). Quando
il processore si é avviato, ad ogni ciclo vengono svolte le seguenti attivitá:

• fetch: il processore preleva una nuova istruzione dalla memoria;

17
BOOTSTRAP

Fetch

Decode

Operand Assembly

Execute

Figure 1.8: Ciclo del processore

• decode: l’istruzione viene interpretata: a questo punto si comprende cosa si deve


eseguire in questo ciclo;
• operand assembly: vengono prelevati dalla memoria (o da registri) i dati che
devono essere computati (ad esempio gli addendi di un’addizione);

• execute: l’operazione prelevata, decodifica e preparata viene effettivamente ese-


guita.

18
2 Il calcolatore
In questo capitolo analizzeremo i principali elementi dell’architettura di un calcola-
tore al fine di comprendere le motivazioni di diverse caratteristiche dei linguaggi di
programmazione.

2.1 Architettura di un calcolatore


Nella Sezione 1.7 abbiamo visto il modello funzionale di un calcolatore attraverso il
modello di Von Neumann.

Periferica 1 CPU

BUS

Periferica N Memoria

Figure 2.1: Architettura di un calcolatore

La Fig. 2.1 rappresenta un modello di riferimento di architettura di un calcolatore;


in essa sono evidenziati i seguenti componenti:
• CPU: cervello del calcolatore contenente la circuiteria che presiede a tutte le
attivitá decisionali del sistema;
• Memoria: elemento che si occupa della memorizzazione temporanea dei dati1 ;
• Periferiche: sistemi di collegamento tra la parte interna del calcolatore e le
unitá periferiche di Ingresso/Uscita (I/O). Tra esse annoveriamo: lettore floppy,
lettore CD-DVD, dischi rigidi, periferiche USB, tastiera, mouse, schede di rete,
schermo, ...;
• Bus: sistema di comunicazione che collega su un unico canale condiviso tutte le
unitá sopra descritte.
1 Sidice che la memoria é volatile in quanto, allo spegnimento del computer, perde i dati che aveva
in esso memorizzati. Esistono altri tipi di memorie non volatili (memorie secondarie): hard disks,
drives usb, ...

19
Analizziamo nel dettaglio alcuni di questi elementi.

2.1.1 La memoria
Possiamo considerare la memoria come una grande matrice di celle ognuna capace di
memorizzare un bit. Le celle sono organizzate in righe e colonne come raffigurato in
Fig.2.2: ad esse é possibile accedere in lettura o scrittura.

2^N

Figure 2.2: Organizzazione della memoria

Vi sono dunque K celle di memoria ogni riga: questo numero viene detto parallelismo
della memoria e rappresenta il numero di bit presenti in ogni riga. Ci sono invece un
numero di righe pari a una potenza di due, nella fattispecie 2N . La minima quantitá
di memoria accessibile in memoria (sia in lettura che in scrittura) é la parola (ossia la
riga); per potervi accedere viene fornito quello che si definisce indirizzo di memoria.
L’indirizzo della parola i-esima non é altro dunque che la codifica binaria del numero
i-12 . Ad esempio supponendo di avere 8 parole di memoria, esse saranno indirizzate
dai seguenti: 000, 001, 010, 011, 100, 101, 110 e 111.

2.1.2 La CPU
La CPU (Central Processing Unit) rappresenta il centro delle attivitá del calcolatore
e presiede e a tutte le attivitá di controllo nonché a quelle di elaborazione e trasfor-
mazione dei dati. Rispetto al modello di Von Neumann (vedi Figura1.7) il processore
implementa le funzionalitá di unitá di controllo e di unitá logico-aritmetico.
2 Per convenzione la prima parola di memoria é 0, quindi l’indirizzo della i-esima riga corrisponde al

20
PC
MAR

IR
MDR

R1
ALU

RN

Figure 2.3: Architettura di un processore

Gli elementi evidenti nel modello di Fig.2.3 (ad esclusione dell’ALU) sono registri.
Un registro é una piccola porzione di memoria all’interno del processore che il pro-
cessore stesso utilizza sia per operazioni di carattere generale (ossia per i dati) con i
registri R1 ...RN , sia per operazioni di tipo dedicato. Tra questi annoveriamo:
• MAR: Memory Address Register, contiene l’indirizzo di memoria del dato che si
vuole leggere/scrivere dalla memoria;

• MDR: Memory Data Register, contiene il dato da leggere/scrivere in memoria;


• IR: Instruction Register, contiene l’istruzione di linguaggio macchina che il pro-
cessore sta correntemente eseguendo;
• SR: Status Register, contiene informazioni sull’esecuzione delle operazioni all’interno
del processore (errori, risultato zero, negativo, overflow, etc.);

• PC : Program Counter, contiene l’indirizzo della prossima istruzione che dovrá


essere eseguita dal processore.
In fine all’interno del processore é presente la ALU (Unitá Logico-Artimetico) cos-
tituita dai circuiti elettronici capaci di svolgere le operazioni aritmetiche (somma,
differenza, prodotto, ...) e quelle logiche (negazione, and, or, ...). Tutti questi compo-
nenti sono collegati ad un Bus interno alla CPU attraverso cui i dati possono passare
da una parte all’altra della CPU e verso l’esterno.

numero i-1

21
2.1.3 Il bus
Il bus collega dunque la memoria, il processore e le altre periferiche del calcolatore.
Esso é strutturato in tre parti distinte ognuna della quali specifica per alcuni tipi di
informazioni:
• dati : ivi viaggiano i dati che vengono scambiati tra gli elementi collegati al bus;
• indirizzi : vi viaggiano gli indirizzi di memoria che il processore richiede (lettura
e/o scrittura);

• controllo: linee speciali che servono per comunicare i tipi di operazioni richieste
agli elementi dal processore (ad esempio se un indirizzo di memoria deve essere
letto o scritto dalla memoria).

2.1.4 Accesso alla memoria


Riassumiamo i concetti sopra espressi attraverso l’esempio di funzionamento della let-
tura e della scrittura di un dato da processore a memoria.

Lettura dalla memoria

Memoria
CPU

MDR MAR

(6) (5)
(7) (4)
(8) (3)
(1)
MDR MAR
(10)

(9) (2)

BUS CONTROLLO

BUS INDIRIZZI

BUS DATI

Figure 2.4: Lettura di dati dalla memoria

Facendo riferimento alla Fig.2.4, illustriamo il processo di lettura di una dato dalla
memoria. Innanzitutto quando il processore stabilisce di voler leggere un dato dalla
memoria, scrive nel suo registro MAR l’indirizzo (ad N bit) relativo a quale parola si
vuole leggere (1), tale valore viene trasferito sul bus all’analogo registro presente nei

22
circuiti di memoria (2). Successivamente il processore dice (sul bus dei controlli) di
voler leggere il dato (3) e tale comando arriva alla memoria (4). A questo punto i
circuiti della memoria leggono il MAR (5) ed estraggono tra le parole quella relativa
all’indirizzo selezionato copiando il contenuto di tale parola (k bits) nel registro MDR
(6) ed attivano un segnale di risposta verso il processore appena l’operazione si é
conclusa (7), comando che, arrivato al processore (8), permette il trasferimento del
dato dalla memoria al processore sul bus dei dati (9) e (10).

Scrittura in memoria

Memoria

CPU MAR
MDR (8) (7)
(9) (6)
MDR MAR
(10) (5)
(3) (1)
(4) (2)

BUS CONTROLLO

BUS INDIRIZZI

BUS DATI

Figure 2.5: Scrittura di dati in memoria

Un processo del tutto analogo avviene per la scrittura di un dato in memoria (vedi
Fig.2.5). In questo contesto, il processore stabilisce quale dato deve essere scritto
e dove; innanzitutto dunque scrive nel suo registro MAR l’indirizzo relativo a quale
parola si vuole scrivere (1), tale valore viene trasferito sul bus all’analogo registro
presente nei circuiti di memoria (2). Ugualmente si procede per il dato da scrivere (3)
e (4). Successivamente il processore dice (sul bus dei controlli) di voler scrivere il dato
(5) e tale comando arriva alla memoria (6). A questo punto i circuiti della memoria
leggono il MAR (7) ed il MDR (8) e scrivono il contenuto di quest’ultimo nella parola
individuata dal primo. Alla fine di questa operazione, la memoria attiva un comando
di risposta verso il processore (9) e (10).

23
2.2 Memoria cache
Le prestazioni dei calcolatori basati sul modello di Von Neumann sono fortemente
influenzate dagli accessi alla memoria; infatti, dato che ad ogni ciclo ci possono essere
diversi accessi alla memoria (sia in lettura con le istruzioni ed i dati, sia in scrittura con i
dati), occorre velocizzare tale attivitá al fine di migliorare globalmente le prestazioni del
calcolatore. Migliorare le prestazioni di una memoria non é peró semplicissimo: infatti
le memorie sono caratterizzate da costi, velocitá e capacitá. Ebbene non possiamo
pensare di avere contemporaneamente memoria poco costose, molto veloci e molto
capienti. In genere se scegliamo due di queste caratteristiche (ad esempio grandi
velocitá e bassi costi), dobbiamo accontentarci di memorie poco capienti.
Occorre enucleare adesso due regole empiriche nate dall’osservazione statistica dei
programmi di maggior uso:

• principio di localitá spaziale: afferma che se ad un certo istante di tempo si


fa la richiesta di un dato dalla memoria é molto probabile che negli istanti di
tempo successivi si fará la richiesta di un dato “vicino” (in termini di indirizzi
di memoria) a quello richiesto in precedenza;
• principio di localitá temporale: afferma che se ad un certo istante di tempo si fa
la richiesta di un dato dalla memoria é molto probabile che negli istanti di tempo
successivi si rifará la richiesta dello stesso dato.

Diciamo memoria cache quella memoria che viene frapposta tra il processore e la
memoria principale e che conserva una copia dei dati. La caratteristica principale
della memoria cache é una maggiore velocitá (altrimenti sarebbe inutile averla!). Lo
schema architetturale del calcolatore con la presenza della cache diventa dunque quella
descritto in Fig.2.6.

CPU

Periferica 1 CACHE

BUS

Periferica N Memoria

Figure 2.6: Schema di memoria cache

Vediamo che la cache viene posta tra il processore ed il bus. Il suo funzionamento é
il seguente: inizialmente la cache é vuota e quindi quando il processore chiede il dato

24
alla cache, quest’ultima lo richiede alla memoria. A questo punto il trasferimento di
dati dalla memoria alla cache non interessa solo la parola che il processore ha richiesto
ma un blocco di dati relativo a quella locazione di memoria ed a quelle adiacenti.
Appena il dato viene letto, la cache non viene svuotata ma il blocco trasferito dalla
memoria resta nella cache. Quando invece il processore richiede un dato alla cache ed
esso si trova giá in cache, allora la cache non passa per la memoria. In questo caso si
dice che si é avuto un cache hit: diciamo PHIT la probabilitá di avere un tale evento.
Questi due accorgimenti permettono un notevole miglioramento delle prestazioni:
• innanzitutto il fatto che si trasferisce un blocco di dati dalla memoria alla cache
invece della singola parola permette di sfruttare a pieno il principio della localitá
spaziale in quanto se un dato adiacente viene chiesto c’é un’alta probabilitá che
esso si trovi giá in cache;
• in secondo luogo, il fatto che si conserva il dato in cache anche dopo che lo si é
usato, permette di sfruttare il principio della localitá temporale in quanto se il
dato in questione viene richiesto giá si trova in cache.
Quando viene rimosso un blocco dalla cache? Quando ne viene chiesto un altro non
presente e non c’é piú spazio libero in cache: in questo caso si prende uno dei blocchi
“vecchi” e lo si elimina dalla cache (scrivendo in memoria quanto era stato scritto
precedentemente in cache).
Quali sono dunque le prestazione di un sistema in presenza della memoria cache?
Supponendo TM il tempo di accesso di un dato dalla memoria principale e TC il tempo
di accesso alla memoria cache abbiamo che ogni accesso in assenza di cache é dunque
TM , mentre in presenza di cache abbiamo:

TT OT = PHIT · TC + (1 − PHIT ) · TM

Considerando che PHIT ' 0.8 e che TM ' 50 · TC possiamo capire quanto buono é
il guadagno che si ottiene in presenza di cache.
Le limitazioni della cache sono legate soprattutto alle sue dimensioni. Infatti essendo
una memoria molto veloce, é necessario, al fine di contenere i costi, di limitarne le
dimensioni; infatti se le cache fossero veloci e grandi il loro costo renderebbe impossibile
(dal punto di vista economico) la loro esistenza.

25
3 La programmazione dei calcolatori
3.1 Linguaggio macchina
Nel capitolo precedente abbiamo illustrato quelli che sono gli elementi fondamentali
dell’architettura di un calcolatore elettronico. Il grande potere di un calcolatore é che
il limite delle cose che puó fare é dettato esclusivamente da come é stato programmato,
dai programmi che su di esso vengono eseguiti. Qui evidenzieremo gli aspetti legati
alla programmazione dei calcolatori parlando innanzitutto dei linguaggi in cui i tali
programmi sono espressi.
Cosa esprime una istruzione all’interno del calcolatore? Abbiamo visto nel modello
di Von Neumann diverse tipologie di controllo che l’unitá di controllo stessa effettua
sulle diverse unitá del processore. Questa diversitá riflettono un’analoga diversitá tra
le possibili categorie di istruzioni:

• ingresso/uscita: istruzioni legate al trasferimento di dati tra la memoria ed una


periferica;
• memorizzazione: a questa categoria appartengono le istruzioni legate alla lettura
e alla scrittura di dati da e verso la memoria da parte del processore (ossia
istruzioni di trasferimento dati tra registri interni al processore e la memoria);
• calcolo logico/aritmetico: istruzioni che permettono di utilizzare l’ALU nell’esecuzione
di operazioni quali la somma, la differenza o altro tra registri o tra un registro
ed una locazione di memoria.

A queste si aggiunge una categoria legata alla gestione del flusso di controllo stesso
(istruzioni effettuate su altre istruzioni ossia legate all’individuazione della prossima
istruzione all’interno di un programma).
Tipicamente la forma di un’istruzione é la seguente:

CodOp Op1 Op2

dove CodOp é detto codice operativo e rappresenta la tipologia di istruzione (esempio


Leggi da memoria), Op1 rappresenta il primo operando (l’indirizzo della parola di
memoria da accedere) e Op2 il secondo operando (esempio il registro in cui conservate
il dato letto dalla memoria)1 .
Continuiamo considerando che la macchina di Von Neumann tratta le istruzioni
come dati e che in memoria sono conservate in modo indifferenziato sia istruzioni che
1 Notache non tutte le istruzione hanno due operandi, alcune ne hanno solo una (ad esempio la
negazione di un valore) mentre alcune possono non avere affatto operandi.

26
dati. Detto questo risulta naturale pensare che anche per quanto riguarda le istruzioni
esista una codifica verso il codice binario: tale codice viene detto linguaggio macchina.
Questo é infatti l’unico metodo attraverso cui il calcolatore puó interpretare ed eseguire
le istruzioni.
Il linguaggio macchina presenta peró due grandissime limitazioni che ne rendono
l’uso (dal punto di vista della programmazione) pressocché limitato a pochi casi spo-
radici. Il primo di questi problemi é quello della complessitá: essendo il linguaggio
macchina di un livello di astrazione estremamente basso, specificare anche il piú sem-
plice programma richiede diverse righe di codice. Pertanto scrivere programmi comp-
lessi risulta pressocché impossibile. La seconda limitazione riguarda la portabilitá ossia
la possibilitá di scrivere un programma una sola volta e di eseguirlo su quanti piú tipi
di macchine possibile. Il linguaggio macchina é, per sua stessa definizione, dipendente
fortemente dalla macchina e pertanto un programma scritto in tale linguaggio non puó
essere eseguito (portato) su un computer di tipo diverso (che monti cioé un processore
diverso).
Per ovviare parzialmente a tali inconvenienti si sono diffusi velocemente i linguaggi
assemblativi (assembler) che sostituiscono ai codici binari condici simbolici: ad esempio
l’istruzione di esempio mentre se scritta in linguaggio macchina apparirebbe come:
000101 110100010 1101001
in assembler potrebbe apparire come:
M OV E A R0
ossia sposta il dato dalla locazione A al registro R0. Un programma scritto in
assembler deve poi esser trasformato in linguaggio macchina attraverso un apposito
programma (l’assemblatore). L’assembler per quanto migliori la leggibilitá di un pro-
gramma non risolve totalmente i problemi di portabilitá e complessitá primi descritti.

3.2 Linguaggi di alto livello


Divenne presto ben chiaro sin dai primi tempi dell’informatica, che il basso livello di
astrazione del linguaggio macchina (ed anche di quello assemblativo) era un fattore
molto limitante allo sviluppo di programmi complessi e corretti. Sorse subito l’esigenza
di avere a disposizione strumenti linguistici (linguaggi di programmazione e meccanismi
per l’esecuzione dei programmi) che consentissero ai programmatori di elevare il livello
di astrazione dei loro programmi permettendo di non pensare piú a dettagli specifici
della macchina considerata. Inoltre il fatto che ogni calcolatore avesse il suo linguaggio
macchina (e di conseguenza il suo linguaggio assemblativo) limitava la portabilitá di
programmi ossia la capacitá si spostare un programma da una macchina all’altra senza
dover riscrivere tutto. A tal fine sono stati introdotti fin dai primi anni dell’informatica
i linguaggi di terza generazione (3GL) anche detti linguaggi di alto livello.
A questa categoria appartengono diversi linguaggi quasi tutti basati su parole-chiave
riferite alla lingua inglese. Ció facilita molto sia la stesura che la rilettura di un pro-
gramma, ma non mette il computer in condizione di capire direttamente cosa vogliamo

27
in quanto un calcolatore, sulla base di quanto giá detto alla Sez.3.1, comprende esclu-
sivamente il linguaggio macchina. Sono dunque necessari degli strumenti in grado di
colmare il divario tra i linguaggi 3GL ed il linguaggio macchina.
Generalmente si tende a suddividere i linguaggi ad alto livello in 3 categorie2 :
• imperativi: sono composti da una sequenza di istruzioni in grado di modificare il
contenuto della memoria del computer o di determinare le modalitá di esecuzione
di altre istruzioni. Tipici linguaggi imperativi sono: Pascal, Basic, Fortran, C,
...;

• dichiarativi: il programma é considerato come un’affermazione (teorema) da


dimostrare e la dimostrazione é la sua esecuzione. Tipici linguaggi dichiarativi
sono il Prolog e il SQL3 ;
• funzionali: calcolano il valore di una funzione (molto piú vicini alla formaliz-
zazione matematica rispetto a tutti gli altri). Tipici linguaggi funzionali sono
ML e Lisp.

3.2.1 Compilatori ed interpreti


Esistono due tecniche per colmare il divario tra i linguaggi 3GL e quello macchina:
la compilazione e l’interpretazione. Alla base di entrambi c’é il concetto che le
espressioni del linguaggio di alto livello (piú sintetiche e quindi in minor numero)
debbano essere tradotte in una piú lunga lista di istruzioni elementari. Il programma
3GL che si vuole tradurre viene detto programma sorgente (o codice sorgente) mentre il
programma in linguaggio macchina risultato della compilazione/interpretazione viene
detto programma target (codice oggetto o eseguibile). Questo secondo programma viene
poi eseguito sulla macchina al fine di ottenerne l’elaborazione (si dice che viene lanciata
la sua esecuzione).
Nella compilazione, la traduzione e l’esecuzione sono due fasi distinte dello sviluppo
del programma. Innanzitutto viene scritto dal programmatore il programma sorgente.
Successivamente viene eseguito un programma esterno (il compilatore) che esegue la
compilazione e genera tutto il codice oggetto relativo all’intero programma sorgente.
Fatto questo l’eseguibile puó essere poi successivamente lanciato sulla macchina senza
dover essere necessariamente compilato.
Diversamente lavora l’interpretazione. In questo processo le fasi di traduzione ed
esecuzione sono contemporanee: in altre parole l’interprete scandisce il programma
sorgente riga per riga e ad ogni istruzione di tale programma, provvede una traduzione
simultanea e l’esecuzione di tali righe di codice oggetto.
Questi due schemi di ragionamento diversi presentano ovviamente delle differenze.
Per quanto riguarda la compilazione, questo processo permette di individuare gli er-
rori presenti nel programma prima che esso venga eseguito permettendo che alcuni
controlli di correttezza del programma possano essere eseguiti prima dell’esecuzione.
2 Esistono in realtá tante altre categorie di linguaggi di programmazione relative alle piú diverse
caratteristiche.
3 All’interno di tale categoria distinguiamo la sottocategoria dei linguaggi logici tra cui il Prolog.

28
La compilazione inoltre traduce il programma nella sua interezza permettendo quindi
di ottimizzare il codice oggetto ossia di renderlo (a paritá di funzionalitá svolte) piú
veloce oppure occupante meno spazio. Durante l’esecuzione inoltre si ha giá a dispo-
sizione tutte le istruzioni che verranno eseguite facendo sı́ dunque che il codice prodotto
sia molto piú veloce dell’analogo interpretato. D’altro canto l’interpretazione permette
uno sviluppo piú interattivo del programma in quanto le istruzioni vengono tradotte ed
eseguite una alla volta. Gli interpreti, inoltre, mancando di tutta una serie di controlli
che il compilatore fa e mancando del tutto della fase di ottimizzazione del codice, hanno
una struttura piú semplice (a paritá del linguaggio) rispetto agli analoghi compilatori.
Una cosa che accomuna interpreti e compilatori é la dipendenza dall’architettura.
Abbiamo visto nella sezione precedente che il linguaggio macchina é caratteristico di
un’architettura e quindi i programmi scritti in linguaggio macchina (e possiamo dire
anche assembler) funzionano solo sull’architettura per la quale sono stati sviluppati
risultati dunque scarsamente portabili. I linguaggi di alto livello invece si astraggono
dall’architettura considerata: infatti quando si scrive un programma in C, ad esempio,
e lo si compila su una certa macchina, si produce un codice che funzionerá su tutte
le macchine di quel tipo. Per poter portare il codice su un’altra architettura, sará
necessario cambiare compilatore scegliendone uno che riesca a tradurre il linguaggio
C nel linguaggio macchina della nuova architettura mantenendo inalterato il codice
sorgente. Analoghe considerazioni valgono per gli interpreti.
Occorre peró precisare che é possibile l’esistenza, per un qualsiasi linguaggio di
programmazione, sia di compilatori che di interpreti. Non solo, esistono modalitá
ibride che prevedono l’uso combinato (unendo i vantaggi di entrambi) sia della fase
compilativa che di quella interpretativa. In questi casi la compilazione avviene sempre
prima dell’interpretazione ed ha come linguaggio target non il linguaggio macchina ma
una sorta di linguaggio intermedio (come avviene ad esempio nel bytecode di Java).

29
4 Sistemi operativi
I calcolatori elettronici, nella loro complessitá, vengono schematizzati, studiati e pro-
gettati secondo modelli che riescono a dominare quanto piú possibile tale complessitá.
Uno dei modelli piú utilizzati in tale ambito descrive il calcolatore attraverso l’astrazione
dei livelli. In tale modello, raffigurato in Fig 4.1, possiamo vedere diversi livelli ognuno
dei quali si poggia su altri. Verso il livello superiore, ogni livello fornisce dei servizi
mentre allo stesso momento richiede servizi al livello inferiore.

Sw Applicativo
Servizi
Esportati
Sw di base
Servizi
richiesti

Hardware

Figure 4.1: Modello hw/sw a livelli

Nel nostro modello in particolare vediamo tre livelli:

1. hardware: il livello piú basso, é quello della macchina fisica cosı́ come visto nel
Cap. 2: es. server dual-core piuttosto che netbook di fascia bassa;
2. sw di base: su tale livello si poggia quello hardware ed ha lo scopo di fornire
tutta quella serie di programmi in grado di far funzionare la macchina hardware
e di fornire inoltre un’interfaccia unica che nasconda i dettagli della macchina
stessa: es. Windows, Linux, Mac;
3. sw applicativo: software in grado di fornire funzionalitá ben specifiche e creati
per scopi particolari: es. word processor, multimedia player, browsers. Gli
applicativi sono realizzati attraverso la stesura di un apposito programma scritto
con le tecniche ed i linguaggi evidenziati in Cap. 3.

30
4.1 Funzionalitá
Un sistema operativo é un sistema complesso in sé dato il suo duplice compito di gestire
da una parte hardware sempre piú complessi e dall’altra di fornire funzionalitá sempre
piú sofisticate ed interfacce utente intuitive e gradevoli. A tal fine si preferisce iniziare
a descrivere i sistemi operativi attraverso un ulteriore modello a livelli (raffigurato in
termini concentrici) che prende anche il nome di schema a cipolla (vedi Fig. 4.2).

Shell ed Interpreti

File System

Gestione periferiche

Gestione Memoria

Kernel

Hardware

Figure 4.2: Modello a cipolla

Rispetto al modello a livelli come visto in Fig. 4.1, si deve considerare qui lo strato
piú interno come quello piú basso. Partendo dall’interno vediamo che il sistema oper-
ativo si appoggia sull’hardware (e sui servizi direttamente offerti da questo). Il primo
livello del sistema operativo vero e proprio é il nucleo (kernel) che offre funzionalitá
di gestione dei processi e di gestione della risorsa piú importante della macchina, os-
sia il processore. Successivamente troviamo quelle routine che si occupano di gestire
la memoria ossia di stabilire quanta memoria ed a chi viene stata allocata (il ge-
store della memoria). Successivamente incontriamo il livello di gestione delle
periferiche che si occupa di interfacciarsi con le periferiche del calcolatore al fine di
avere delle interfacce e dei modi di comunicazione con tali elementi unici. Ancora piú
su troviamo il livello del file system, ossia di gestione dei file e delle informazioni
persistenti del sistema. All’ultimo livello troviamo l’interprete dei comandi, os-
sia l’insieme di software deputato all’interfacciamento verso l’utente e la gestione dei
comandi da questi ricevuti.

31
4.1.1 Il kernel
Alla base della gestione di un sistema operativo c’é il concetto di processo. Abbiamo
visto nei Capitoli 1 e 3 che i programmi sono costituiti dal codice oggetto generato
dalla compilazione del codice sorgente e che rappresentano quindi concetti statici. Di-
versamente, il processo é l’entitá utilizzata dal sistema operativo per rappresentare
una specifica esecuzione di un programma (concetto dunque dinamico): oltre al codice
sorgente (il programma), un processo contiene anche i dati relativi alla specifica es-
ecuzione nonché tutte le informazioni che ne definiscono lo stato, come il contenuto
della memoria indirizzata, i file e le periferiche in uso, etc...
In un sistema operativo moderno, ci sono piú processi attivi contemporaneamente.
Uno dei compiti piú importanti di tali sistemi é appunto quello di gestire opportuna-
mente tutti questi processi rendendoli attivi uno per volta. I primi sistemi operativi
erano monoprocesso ossia riuscivano a gestire solo un processo alla volta: i vari processi
procedevano in batch (lotti) ossia l’i-esimo non poteva iniziare prima che l’(i-1)-esimo
avesse finito (vedi Fig. 4.3): questo prende il nome di schedulazione FIFO.

processo A

processo B

processo C

tempo

Figure 4.3: Schedulazione FIFO

Questo modo di schedulare, ossia di pianificare nel tempo l’esecuzione di tali processi,
ha come inconveniente il permettere che alcuni processi monopolizzino il processore
per troppo tempo. Infatti come si evince dalla figura il processo C, che é il piú piccolo,
deve aspettare che tutti gli altri finiscano. Obiettivo diverso ha invece la seconda
modalitá di schedulazione (Round Robin) che permette, attraverso la suddivisione del
tempo in quanti (time slice) di eseguire un pó di processo per volta, facendo insomma
“un pó di processore per uno” tra i processi permettendo dunque a quelli piú veloci
di non dover aspettare troppo tempo prima di poter essere eseguito (vedi Fig. 4.4).
Questa si chiama schedulazione Round-Robin ed ha come vantaggio tangibile quello

32
di una diminuzione media della durata del tempo di esecuzione dei processi.

processo A

processo B

processo C

tempo

Figure 4.4: Schedulazione Round-Robin

Un processo vive diverse fasi: richiede dati alle periferiche e quindi “perde tempo” in
attesa di dati che vengono dall’esterno. In un sistema monoprocesso in questa perdita
di tempo viene subita anche dal processore in quanto, in questo tempo, il processo
continua a detenere la CPU.
Facendo dunque riferimento alla Fig. 4.5, quando viene attivato un processo si trova
in modalitá READY dove viene inserito in una coda di processi pronti per essere
eseguiti e che aspettano solo il processore. Quando il processore non ha nulla da fare,
viene scelto un processo dalla coda (in genere quello che ci sta da piú tempo) e lo si
fa diventare RU N N IN G ossia viene allocato a lui il processore e gli viene permesso
di essere eseguito. Quando il tempo a disposizione del processo finisce (si esaurisce il
quanto di tempo precedentemente descritto) l’esecuzione del processo viene interrotta
ed il processo viene riaccodato ai READY . Quando invece, dallo stato di RU N N IN G
un processo fa una richiesta di I/O (ad esempio richiede dei dati ad una periferica)
esso viene messo della coda dei W AIT , dalla quale si esce solo in caso di avvenuto
trasferimento dati di I/O. A questo punto il processo viene rimesso nella coda dei
READY .

4.1.2 Gestione della memoria


Il gestore della memoria é il componente del sistema operativo dedito alla gestione della
memoria disponibile sul computer. Esso si occupa di allocare, deallocare e gestire la
memoria che viene assegnata agli applicativi e allo stesso sistema operativo. Tutti i
moderni sistemi operativi sono dotati di memoria virtuale. Il gestore della memoria
si preoccupa di decidere quali blocchi di memoria sono poco utilizzati dal sistema e

33
Primo in
coda
RUNNING Nascita
READY

Tempo scaduto
Fine

Evento esterno
Richiesta
periferica
WAIT

Figure 4.5: Modalitá di un esecuzione di un processo

possono essere spostati sull’unitá a disco senza deprimere eccessivamente le prestazioni


del sistema.

4.1.3 File Systems


Un file system é la parte del Sistema Operativo attraverso cui i file sono immagazzinati
e organizzati su un dispositivo di archiviazione, come un disco rigido o un CD-ROM.
L’astrazione piú comunemente usata é quella di albero. Quasi tutti i file system mod-
erni gestiscono le risorse memorizzate attraverso una struttura ad albero. In questo
albero si scorgono due elementi strutturali: i file e le directory. I primi sono collezioni
di dati (sequenze) che vengono memorizzati in modo permanente sulla memoria sec-
ondaria, le directory invece sono cartelle che possono contenere directory o altri file1 .
Uno schema di esempio é raffigurato in Fig. 4.6.
Un concetto fondamentale nei file system é quello dei path. Un path é un percorso,
ossia una sequenza di directory che identifica una risorsa (file o directory). Un path
pu essere relativo (se fa riferimento alla directory corrente) o assoluto (se parte da una
directory di riferimento). Ad esempio a partire dalla Fig. 4.7: il path relativo di F4 da
D3 é D2-D1-F4 mentre il suo path assoluto é F4 (si assume come riferimento il nodo
radice dell’albero).
Il sistema operativo gestisce in questo contesto la struttura nonché fornisce all’esterno
(utente) primitive per la manipolazione del file system stesso: operazioni di creazione,
cancellazione, spostamento di file e directory.

1 Possiamo dire che mentre le directory sono i nodi non terminali dell’albero del file system, i file ne
sono i nodi terminali.

34
DIR

DIR FIle

FIle DIR

DIR FIle FIle

Figure 4.6: File system

D1

D2 F4

F1 D3

D4 F2 F3

Figure 4.7: Paths

35

You might also like