Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Fabio Fontana
2 Shell e terminali 4
2.1 Shell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2 Terminale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.3 Virtual console e pseudo-terminali . . . . . . . . . . . . . . . . . 4
2.3.1 Standard file descriptor . . . . . . . . . . . . . . . . . . . 5
2.3.2 Disciplina di linea . . . . . . . . . . . . . . . . . . . . . . 5
2.4 Ancora su shell . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.5 Job control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
3 File e processi 7
3.1 Utilizzo delle system call . . . . . . . . . . . . . . . . . . . . . . . 7
3.1.1 Syscall e funzioni wrapper . . . . . . . . . . . . . . . . . . 7
3.2 File (API) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2.1 File descriptors e permessi . . . . . . . . . . . . . . . . . . 7
3.2.2 API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.2.3 File offset, metadati e limiti . . . . . . . . . . . . . . . . . 9
3.3 Programmi binari e processi . . . . . . . . . . . . . . . . . . . . . 10
3.4 Processi (API) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.4.1 API per la gestione dei processi . . . . . . . . . . . . . . . 12
3.4.2 Processi zombie . . . . . . . . . . . . . . . . . . . . . . . . 14
3.5 Redirezione dell’I/O . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.5.1 Dup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.5.2 Relazione fra file descriptor e file aperti: POSIX . . . . . 15
3.5.3 Pipe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
4 Xv6 16
4.1 Avvio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
4.2 Programmi utente . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.3 System call . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.4 Scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5 Scheduling 21
5.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.1.1 FIFO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.1.2 Shortest-Job first . . . . . . . . . . . . . . . . . . . . . . . 21
5.1.3 Shortest Time-To-Completion First . . . . . . . . . . . . . 21
5.1.4 Round-Robin . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.1.5 I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.2 Multi-level Feedback Queue . . . . . . . . . . . . . . . . . . . . . 22
5.3 Proportional share . . . . . . . . . . . . . . . . . . . . . . . . . . 22
6 Multi-threading 24
6.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
6.1.1 Creazione, attesa e terminazione di un thread . . . . . . . 24
6.1.2 Sezioni critiche e race-condition . . . . . . . . . . . . . . . 24
6.2 Primitive di sincronizzazione . . . . . . . . . . . . . . . . . . . . 25
6.2.1 Implementazione di lock . . . . . . . . . . . . . . . . . . . 25
6.2.2 Spin-lock . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
6.3 Bug tipici della concorrenza . . . . . . . . . . . . . . . . . . . . . 27
6.3.1 Deadlock . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
7 Sicurezza 29
7.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
7.2 Autenticazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
7.3 Autorizzazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
7.3.1 Principio del minimo privilegio . . . . . . . . . . . . . . . 30
7.3.2 Chroot jails, namespace e container . . . . . . . . . . . . 31
8 Persistenza 32
8.1 Dispositivi a blocchi . . . . . . . . . . . . . . . . . . . . . . . . . 32
8.1.1 Partizionamento . . . . . . . . . . . . . . . . . . . . . . . 32
8.1.2 Gestione file speciali . . . . . . . . . . . . . . . . . . . . . 33
8.2 File system . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
8.2.1 inode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
8.2.2 Directory, link e cancellazione . . . . . . . . . . . . . . . . 35
8.2.3 Path resolution . . . . . . . . . . . . . . . . . . . . . . . . 36
8.2.4 Consistenza del file-system . . . . . . . . . . . . . . . . . 36
2.2 Terminale
Per interagire con la shell si deve utilizzare un terminale. Un terminale è
l’evoluzione di un teletype (TTY). In Unix quasi tutto è un file e file speciali
corrispondono ai terminali collegati. Il nome TTY è rimasto, infatti \dev\tty è
sinonimo del terminale associato a un processo.
Nelle versioni recenti di distro Linux-based tty1 è il login manager, tty2 l’ambiente
grafico e solo le console da tty3 a tty6 sono in modalità testo.
I pty sono una coppia di dispoditivi a caratteri detti master e slave che permet-
tono l’emulazione di terminali. Creando il lato master un processo (es. Konsole)
crea un nuovo terminale virtuale. Altri processi possono aprire il corrispondende
slave (/dev/pts/^^.) e interagire con esso come fosse un vero terminale.
Una syscall viene effettuata mediante una trap (Linux INT 0x80, Xv6 INT 0x40).
Il kernel controlla la validità del numero della syscall e degli argomenti, esegue
la richiesta e scrive il risultato in un registro. Infine ritorna alla modalità utente
(IRET).
3.2.2 API
int open(const char *pathname, int flags[, mode_t mode]);
– lettura/scrittura
– O_APPEND
– O_NONBLOCK
– FD_CLOEXEC chiude il FD quando una exec ha successo
– …
• mode è utilizzato solo quando viene creato un file
– ovvero se flags contiene O_CREAT oppure O_TMPFILE
– è comodo specificarlo in ottale
– stiamo ignorando umask(2)
– mode vale per le prossime aprture: si può scrivere in un file a sola
lettura se è stato specificato O_WRONLY o O_RDWR
• viene sempre restituito il più piccolo FD disponibile o -1 in caso di errore
Dopo una open l’offset è 0 e viene spostato da read e write oppure da una
syscall apposita off_t lseek(int fd, off_t offset, int whence); dove whence
può essere SEEK_SET, SEEK_CUR o SEEK_END.
L’offset può essere spostato oltre la fine del file ma ciò NON comporta un cam-
bio di dimensione dello stesso. Un file può avere dei buchi che corrispondono
(logicamente) a byte 0.
I metadati sono delle informazioni sul file. Per recuperarli si possono usare
queste syscall:
int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf); ^/ come stat ma non segue i symlink
int fstat(int fd, struct stat *statbuf);
struct stat {
dev_tst_dev; ^/ major (12 bits) + minor (20 bits)
ino_tst_ino; ^/ Inode numbermode_t
st_mode; ^/ File type and modenlink_t
st_nlink; ^/ Number of hard links
uid_tst_uid; ^/ User ID of owner
gid_tst_gid; ^/ Group ID of owner
dev_tst_rdev; ^/ Device ID (if special file)
off_tst_size; ^/ Total size, in bytes
blksize_t st_blksize; ^/ Block size for filesystem I/O
blkcnt_t st_blocks; ^/ Number of 512B blocks allocated
struct timespec st_atim; ^/ Time of last access
struct timespec st_mtim; ^/ Time of last modification
struct timespec st_ctim; ^/ Time of last status change
}
Ogni tipo di filesystem ha dei limiti, per cui, per esempio, la lunghezza massima
di un nome file può variare a seconda della directory considerata. Per leggere i
ld, il link editor (AKA linker statico), mette assieme file oggetto/librerie,
eseguendo rilocazione e risoluzione dei simboli:
ld
∗.o (+ ∗ .a + ∗.so + script.ld) −→ a.out
10
Per fare girare un programma oltre a mappare codice e dati da un ELF serve
anche:
• stack: variabili locali/temporanei, parametri, indirizzi di ritorno
• heap: memoria dinamica
• kernel: non accessibile in modalità utente
Il codice di programmi e, grazie al linking-dinamico, librerie può essere condiviso
(e mappato a indirizzi diversi) in processi diversi.
Si può ottenere il PID del processo e del padre con pid_t getpid(void) e
pid_t getppid(void).
11
• vari file (maggiormente in sola lettura) per esempio maps mostra lo spazio
di indirizzamento
• vedere proc(5)
Ogni processo ha una:
• directory root che viene usata per tutti i percorsi assoluti (che iniziano
con /), non necessariamente la root del FS; servono privilegi per cambiarla
• directory di lavoro che viene usata per tutti i percorsi relativi, getcwd(2),
chdir(2) e fchdir(2) la modificano
pid_t fork(void)
La fork clona un processo creandone uno nuovo detto ”figlio”. In caso di suc-
cesso ritorna due volte restituendo il PID del figlio al padre e 0 al figlio.
12
int execve(const char *pathname, char *const argv[], char *const envp[]);
– le variabili d’ambiente
– gli argomenti della riga di comando
– altro
• il kernel (”invisibile”)
Poi in caso di linking dinamico fa la stessa cosa ricorsivamente per codice e dati
delle librerie necessarie (+ linking). Inoltre quando viene creato un thread viene
anche allocato un nuovo stack.
13
Funziona come dup ma usa newfd (se necessario lo chiude, se è uguale a oldfd
non fa nulla).
14
Figure 3.3: Relazione tra file descriptors, descrizioni dei file aperti e inodes.
3.5.3 Pipe
int pipe(int pipefd[2]);
Quello che viene scritto nella pipe finisce in un buffer del kernel. Quando
il buffer è pieno le write vengono messe in attesa o falliscono se O_NONBLOCK
è abilitato (se tutti i FD di lettura sono stati chiusi write provoca SIGPIPE).
Quando il buffer è vuoto le read vengono messe in attesa o falliscono come per
la write (se tutti i FD di scrittura sono stati chiusi restituisce 0 come EOF).
15
4.1 Avvio
A 232 − 16 deve essere presente il firmware, in (EP)ROM, che:
1. testa/inizializza l’HW
2. cerca un dispositivo a blocchi da cui poter fare il boot, il cui primo settore
contenga la firma 0x55 0xAA agli offset 510 e 511
3. carica il primo settore detto boot block all’indirizzo fisico 0x7c00
4. lo esegue
È stato scelto 0x7c00 quando negli anni ’80 il DOS 1.0 richiedeva almeno 32KB.
Gli sviluppatori BIOS hanno pensato di lasciare più spazio possibile per il SO
perciò hanno scelto l’ultimo KB disponibile (512B per il boot-sector e 512 per
dati/stack). Da allora per compatibilità continua ad essere quello.
16
Con pagine da 4KB l’MMU prende i primi 10 bit e cerca corrispondenze nella
page directory che da l’indirizzo fisico della page table, si prendono gli altri 10
bit e si indicizza nella page table che ci darà i 20 bit dell’indirizzo del frame al
quale saranno aggiunti i 12 bit di offset.
17
18
Se l’interruzione arriva in modalità utente la CPU passa allo stack kernel e salva
il valore dello stack utente sullo stack kernel. Successivamente vengono salvati
EFLAGS e CS:EIP.
In Xv6 ci sono gli handler per ogni interrupt che si occupa di aggiungere un
error code (0) agli interrupt che non lo hanno e di pusharlo sullo stack insieme
al numero di interrupt. Dopodichè salta a alltraps che:
1. costruisce un trap-frame contentente struct trapframe, il numero di
trap, l’error code e una copia di tutti i registri (ad uso generale e utente
(se cambio di stato)) e lo passa a trap
2. trap usa trapno per decidere cosa effettuare, se è una syscall (T_SYSCALL)
salca il trap-frame nel PCB e chiama syscall
3. al ritorno ripristina i registri ed esce dalla gestione interrupt
Gli stati di un processo possono essere:
enum procstate { UNUSED, EMBRYO ^* AKA: INIT ^/, SLEEPING,
RUNNABLE ^* AKA: READY ^/, RUNNING, ZOMBIE };
4.4 Scheduling
Per virtualizzare la CPU si utilizza il time sharing, ovvero ogni processo riceve
un la CPU per un periodo di tempo. Questa operazione deve essere efficiente
e deve controllare che un processo non esegua operazioni non permesse e che
rilasci la CPU.
19
Quindi questa struttura sarà puntata dal valore in esp caricato nel PCB:
struct context { ^* definita in kernel/proc.h ^/
uint edi;
uint esi;
uint ebx;
uint ebp;
uint eip; ^* inserito "automaticamente" da CALL ^/
}; ^* esp usato per puntare a questa struttura ^/
20
5.1.1 FIFO
L’algoritmo FIFO esegue i processi in ordine di arrivo. FIFO soffre dell’effetto
convoglio, ovvero se un processo molto lungo arriva insieme/prima di uno
breve il TAT peggiora notevolmente in quanto il secondo processo prima di
essere eseguito deve aspettare tutta la terminazione del primo processo.
5.1.4 Round-Robin
Nei sistemi iterattivi è importante un’altra metrica, il response time: Tresponse =
Tf irstrun − Tarrival . Gli algoritmi visti precedentemente hanno un pessimo re-
sponse time in quanto se dovessero arrivare 10 job contemporaneamente l’ultimo
dovrebbe aspettare la terminazione degli altri prima di essere mandato in ese-
cuione.
21
Dal punto di vista del turnaround RR si comporta molto male in quanto il tempo
di completamento si allunga e alcuni job potrebbero non essere mai eseguiti
causando starvation.
5.1.5 I/O
Se un processo deve fare I/O ne viene schedulato un altro ottimizzando gli spazi
buchi invece che riservare la CPU per poi non utilizzarla.
22
Si applica un fattore di scala per tenere conto del valore nice (priorità) di ogni
processo, per cui:
wk
slicek = max(sched_latency ∗ ∑i=0 , min_granularity)
n−1 wi
runtimek
vruntimek + =
wk
Per trovare efficientemente il minimo/aggiornare il vruntime dei processi pronti
essi vengono tenuti in un albero binario bilanciato di tipo rosso/nero: 0(log(n)).
23
Ogni thread ha il proprio stack e condivide codice e dati con gli altri
thread dello stesso processo (gli stack sono nello stesso spazio di indirizzamento
quindi gli altri thread potrebbero accederci (grave)).
24
Un lock:
Se un programma usa più strutture dati diverse (condivise) conviene avere più
lock:
• aumenta l’efficienza ma complica la gestione
• non si deve esagerare per evitare overhead (non conviene usare un lock
per ogni nodo di una lista, generalmente)
Tutte le strutture dati di Xv6 sono protette da uno spinlock (array che contiene
i file aperti, array dei processi, …).
• performance
Ha senso disabilitare/abilitare le interruzioni? No:
• servono istruzioni privilegiate
• un porcesso potrebbe monopolizzare la CPU
25
Ha senso una struct con un flag che indica lo stato del lock e una funzione lock
che con un while aspetta che si liberi?
No: un thread dopo aver passato il while potrebbe essere deschedulato prima
di settare il lock, a questo punto un altro thread potrebbe essere rischedulato
(non necessario se multi-core), prendere il lock e poi essere deschedulato per far
tornare in esecuzione il primo thread. A questo punto ci ritroviamo con due
thread che hanno preso il lock e non c’è mutua esclusione.
6.2.2 Spin-lock
typedef struct ^_lock_t {
int is_locked;
} lock_t;
26
Funziona?
• Questo metodo garantisce mutua esclusione
• Non è fair, quindi non c’è garanzia che un thread in attesa prima o poi
otterrà il lock; starvation
• In single core se un thread che ha il lock viene deschedulato gli altri spre-
cano tempo e CPU, in multi-core funziona discretamente bene
Ha senso aspettare in un loop (consumando CPU)?
2. T2 è in attesa e va in esecuzione T1
3. T1 acquisisce un lock L
4. T2 torna ready
5. lo scheduler deschedula T1 e manda in esecuzione T2
Violazione dell’atomicità
Puntatore globale che viene settato da un thread se non è NULL; dopo il check
qualcuno potrebbe metterlo a NULL; Tanti auguri.
Soluzione: mutex.
27
Violazione dell’ordine
Creo un thread che esegue una funzione che al suo interno legge un valore del
thread creato (variabile globale); se va subito in esecuzione il nuovo thread la
variabile non è ancora stata inizializzata; Tanti auguri.
6.3.1 Deadlock
Indica una situazione in cui due o più processi o azioni si bloccano a vicenda,
aspettando che uno esegua una certa azione che serve all’altro e viceversa.
Per esempio:
Thread_A ^> lock(m1); lock(m2);
Thread_B ^> lock(m2); lock(m1);
28
29
7.2 Autenticazione
Nei sistemi Unix-like tipicamente gli utenti si identificano con username e
password. In /etc/passwd sono salvati gli utenti del sistema:
login_name : x : UID : GID : user_name : user_home_dir : shell
Il kernel identifica ogni utente con l’UID, analogamente per i gruppi GID (id).
L’UID zero corrisponde a root, l’amministratore di sistema. I processi sono
divisi in privilegiati se UID=0 e non privilegiati con UID̸=0.
7.3 Autorizzazione
Ci sono due approcci generali all’autorizzazione:
• access control list: per ogni object c’è una lista delle coppie di sub-
ject/access; Unix ACL di 9 bit r,w,x
• capabilities (non sono le Linux capabilities): per ogni object ci sono delle
chiavi che ne permettono l’uso
Bisogna anche considerare dove memorizzare queste informazioni, quanto spazio
serve, cosa fare quando si aggiungono/eliminano gli utenti.
30
• saved UID
• su Linux anche FS UID che non verrà spiegato (vedi capabilities(7))
Al login tutte tutti e tre coincidono. Tramite setuid(2) un processo può modifi-
care l’effective UID facendolo diventare come il real o il saved (se il chiamante
è privilegiato può modificare tutti e 3).
Un nuovo processo ”eredita” gli id del parent. Di solito execve non cambia gli
id del processo chiamante a meno che il file eseguibile ha il bit set-userid abil-
itato allora: effective UID e saved UID diventano quelli del proprietario del file.
31
Con i dischi veri il costo di accesso ai dati variava in base alla posizione della
testina. Un SSD moderno è composto da dispositivi elettronici senza un vero e
proprio disco.
8.1.1 Partizionamento
Per varie ragioni si può voler partizionare un disco. Ogni partizione si può usare
come un dispositivo a blocchi a sè stante:
• MBR: boot sector + informazioni sulle partizioni (4 partizioni primarie +
altre estese) (scambiare il bus potrebbe cambiare i nomi)
• GPT: partizioni identificate da un UUID (comodo perchè è univoco e
permette di riconoscere sempre la il disco)
Nei sistemi Unix-like file speciali a caratteri o blocchi corrispondono a
dispositivi di I/O, identificati da un major (classe) e un minor number (par-
ticolare): per esempio /dev/sda è un disco e /dev/sda1 è una partizione (anche
/dev/disk/by-uuid).
Questi file speciali possono essere creati con mknod da root. Per essere utilizzati
vanno montati traminte mount.
32
In un sistema Unix c’è un solo file-system (root FS); mount aggancia l’albero di
file e directory di un dispositivo all’albero globale su un mount-point.
Sono necessarie anche altre strutture dati per gestire l’accesso ai file da
parte dei processi utente. Per esempio quando un processo fa la open il kernel
deve:
1. recuperare l’inode corrispondente al percorso specificato come stringa
2. allocare una struttura che corrisponde al file aperto che punta a (1)
3. allocare un FD nel PCB del processo che punta a (2)
Spesso i settori da 512B si raggruppano in blocchi logici, i cluster (da 4K).
Non si può utilizzare tutto il disco per i dati in quanto si devono memorizzare
i metadati sui file.
33
Infatti:
• per ogni file abbiamo un inode
• gli inode sono contenuti nella tabella degli inode (numero max di file)
• per ogni inode bisogna sapere se usato o meno: inode bitmap
Si usa una bitmap perchè si può portare tutta in RAM ed evitare di andare ad
effettuare operazioni costose su disco.
8.2.1 inode
Ogni inode contiene i metadati relativi a un file (TRANNE il nome):
• tipo di file: regolare, directory, symlink, FIFO, socket, dispositivo a
caratteri/blocchi
• UID/GID di proprietario e gruppo
• dimensione in byte
• maschera dei bit relativa ai permessi
• date di crezione/modifica/…
• numero di hardlink
• puntatori ai blocchi dati
34
rm(1) che si appoggia a unlink(2) rimuove un hard link, e se tutti gli hard link
dovessero essere cancellati il file viene tenuto in vita finchè ci saranno FD aperti.
35
36
37