Programma preliminare (A.A. 2016-2017)
Primo modulo (SC1)
Il primo modulo del corso raffina le capacità di programmazione acquisite nei corsi di programmazione precedenti studiando la relazione tra i programmi e il loro ambiente di esecuzione, in particolare:
- come programmare sfruttando i servizi offerti dai sistemi operativi?
- come avviene la traduzione di un programma C in assembly?
- come avviene l'esecuzione di un programma?
- come viene gestita la memoria?
- come la conoscenza dell'architettura di calcolo, del sistema operativo e del compilatore utilizzato permette di scrivere programmi più robusti ed efficienti?
I concetti sono esemplificati utilizzando come caso di studio Linux su architetture x86. Le funzionalità dei sistemi operativi e delle architetture di calcolo sono presentate
dal punto di vista del programmatore, lasciando ulteriori approfondimenti sui loro meccanismi interni ai corsi del terzo anno di
sistemi operativi e
architetture del calcolatori.
Parte I: come programmare in un sistema operativo moderno?
- Nozioni di base sui sistemi di calcolo e la loro programmazione:
- sistema operativo come ambiente per l'esecuzione dei programmi e per consentire l'utilizzo del calcolatore da parte degli utenti
- hardware (architettura di Von Neumann): CPU (controllo, ALU, registri), bus, I/O (I/O bridge, controller, adattatori), memoria
- Panoramica sui servizi principali forniti da un sistema operativo:
- gestione dei processi (creazione, esecuzione, comunicazione, sincronizzazione, controllo, eliminazione)
- gestione della memoria (allocazione della memoria ai processi)
- gestione dei file
- gestione dei dispositivi di I/O
- sicurezza e protezione
- controllo delle prestazioni, accounting, gestione degli errori
- Interfaccia del sistema operativo verso l'utente:
- struttura di base del filesystem: file, directory, struttura ad albero, root, home, path assoluti e relativi, file "." e "..", "~", case-sensitivity, file nascosti, link simbolici
- utenti e gruppi, permessi rwx
- shell bash, comandi interni (built-in), comandi esterni
- interfaccia dei comandi (es. coreutil), desktop environment e server grafico (es. X, gnome, kde)
- comandi principali:
- navigazione e manipolazione file system: cd, ls, ln, pwd, mkdir, rmdir, rm, cp, mv, touch
- ispezione file: cat, less, head, tail, grep, whereis, which, type,
- documentazione: man, opzione --help
- permessi: chmod
- processi: jobs e ps, background (&, CTRL+z) e foreground (fg), terminazione processo (kill, CTRL+c), overview processi attivi (top)
- canali stdin, stdout, stderr, redirezione > e <, shell pipe | (es: ps | less)
- variabili di ambiente: variabile PATH, comando env, file .bashrc
- Interfaccia del sistema operativo verso i programmi:
- passaggio dei parametri a un programma C: argc/argv
- valore di ritorno di un processo: exit(), return della funzione main
- accesso alle variabili di ambiente: getenv, setenv
- programmazione di sistema: system call e standard POSIX
- differenza tra librerie utente (es. libc, fopen, fwrite) e system call (es. open, write)
- Tool per lo sviluppo:
- compilazione di programmi C con gcc:
- pipeline di compilazione: preprocessore (cpp), compilatore (cc0), assemblatore (as), linker (ld)
- file sorgente (.c), assembly (.s), oggetto (.o), eseguibile
- disassemblamento: objdump -d
- debugging in ambiente Linux: gdb e valgrind; errori tipici: puntatore non inizializzato, offset by 1, memory leak, accesso a memoria deallocata (stack o heap), double free, conditional jump depends on uninitialised value, ecc.
Parte II: come viene compilato un programma C in assembly?
- ISA e ABI come astrazioni per il programmatore, ISA IA32 (architetture x86), ABI System V (calling conventions)
- Metodologia generale: riformulare programma C in modo da essere direttamente traducibile in IA32 (es. x=y+1 -> x=y; x=x+1)
- Traduzione programmi C -> assembly IA32 (sintassi AT&T)
- tipi primitivi (char, short, int, long, float, double, long double) e tipi puntatore -> tipi IA32 (Byte, Word, Double Word, Single Precision, Double Precision, Extended Precision), suffissi delle istruzioni IA32 (b, w, l)
- variabili locali (parte I) -> registri general-purpose A, B, C, D, SI, DI e loro accesso a 8, 16, 32 bit (es. %al, %ax, %eax)
- assegnamento -> istruzione MOV
- letterali interi -> operandi numerici immediati decimali (es. $100) ed esadecimali (es. $0xCAFE)
- operatori aritmetici ed espressioni intere -> istruzioni aritmetiche NEG, ADD, SUB, IMUL, INC, DEC, LEA
- array e aritmetica dei puntatori -> indirizzamento a memoria indiretto a registro e con base, indice e scala (es. (%eax, %ecx, 4))
- conversioni di tipo intero (type cast) -> istruzioni MOVZ e MOVS
- selezione (if ... else) e iterazione (while, for) -> registro EFLAGS, condition code (e, ne, g, ge, l, le, a, ae, b, be) e, istruzioni di confronto CMP e TEST, istruzioni di salto JMP, Jcc, assegnamento condizionale CMOVcc
- chiamata e ritorno da funzione -> istruzioni CALL e RET, creazione e distruzione di stack frame, calling convention System V (registri callee-save e caller save, valore di ritorno intero di una funzione), PUSH, POP
- variabili locali (parte II), passaggio dei parametri, parametri formali -> layout dello stack frame, indirizzamento a memoria indiretto a registro con base (SP) e spiazzamento (es. 4(%esp))
- operatori relazionali ed espressioni booleane -> istruzioni logiche bitwise AND, OR, NOT, XOR e istruzione SETcc
- chiamate al sistema operativo -> INT e passaggio dei parametri alle system call
Parte III: come viene eseguito un programma?
- Dall'eseguibile al processo:
- process control block e risorse associate ai processi: PID, registri, PID processo genitore, puntatori alla memoria, privilegi del processo, accounting (tempo speso dal processo, limiti, ecc.), priorità dello scheduling, risorse I/O (file aperti ecc.)
- loader e anatomia dello spazio logico di un processo:
- code, data, heap, librerie dinamiche, stack
- caso di studio Linux/x86: ispezione immagine di un processo mediante /proc/pid/maps
- Esecuzione di un processo:
- ciclo fetch-decode-execute
- pipelining e stalli (panoramica non dettagliata)
- esecuzione in modalità utente (programmi utente) e in modalità supervisore (kernel)
- flusso del controllo eccezionale
- trap e interrupt
- esecuzione di system call mediante trap
- stati di un processo: running user, running kernel, waiting
- Esecuzione simultanea di più processi:
- scheduling dei processi:
- context switch
- altri stati di un processo: started, ready, zombie
- esempio del time sharing round-robin mediante timer e interrupt
- albero dei processi
- system call per il controllo dei processi: creazione (exec), terminazione (exit), attesa terminazione (wait), segnali e distruzione di processi (kill)
Parte IV: come vengono organizzati i dati in memoria?
- Come è strutturata la memoria?
- accesso a memoria: transazioni in lettura e scrittura
- cache istruzioni e dati, principio di località (spaziale, temporale)
- gerarchie di memoria
- allineamento
- Come viene gestita la memoria?
- memoria fisica (gestita dal sistema operativo) e memoria virtuale (visibile ai processi)
- allocazione dinamica della memoria
- frammentazione interna ed esterna
- qualità di un allocatore: tempo e spazio
- allocazione in cascata della memoria
- allocazione nella memoria fisica: memoria virtuale
- mapping tra indirizzi virtuali e indirizzi fisici: MMU
- paginazione
- come avviene il mapping degli indirizzi logici su quelli fisici?
- quanto spazio occupa una tabella delle pagine?
- paginazione con bit di validità
- paginazione con bit di validità e swapping su disco
- thrashing
- allocazione nella memoria logica: malloc e free, system call sbrk
Parte V: come viene ottimizzato un programma?
- Efficienza prestazionale come moneta per pagare altre qualità del software (affidabilità, usabilità, manutenibilità, portabilità, modularità, ecc.)
- Metriche prestazionali classiche:
- tempo di esecuzione: cicli macchina (assoluti e aggregati, CPI, IPC), secondi (user/system/real)
- spazio di memoria richiesto: byte allocati, memory footprint
- cenni ad altre metriche: energia
- Misurazione del tempo speso da un programma: hardware counter, risoluzione del timer e precisione della misura, comando time, funzioni clock (cross-platform) e clock_gettime (POSIX)
- Metodologie sperimentali:
- effetti imprevedibili che possono influenzare misurazioni: scheduling dei processi, swapping su disco
- trial, aggregazione (media, varianza), intervallo di confidenza
- pericoli nel trarre conclusioni dagli esperimenti
- Ottimizzazioni del programmatore (algoritmo e codice) e del compilatore (codice)
- Livelli di ottimizzazione in gcc
- Quali parti di un programma ottimizzare?
- speedup e legge di Amdahl
- profilazione delle prestazioni: gprof, valgrind massif
- Ridurre il numero di istruzioni eseguite
- espressioni: constant folding, constant propagation, common subexpression elimination
- cicli: loop-invariant code motion (hoisting), loop reverse
- funzioni: inlining, tail recursion
- Ridurre il costo delle istruzioni eseguite sfruttando l'hardware
- tempi di latenza tipici in un sistema di calcolo (da accesso a registro a round-trip time pacchetto Internet)
- sfruttare i registri: allocazione dei registri
- sfruttare la pipeline: istruzioni branchless
- sfruttare istruzioni equivalenti meno costose: operator strength reduction
- sfruttare la cache: migliorare la località degli accessi a memoria
- sfruttare la memoria: accesso allineato
- sfruttare unità di calcolo parallele: cenni a multicore e instruction-level parallelism (es. AVX)
- Ridurre lo spazio di memoria
- ottimizzare lo spazio richiesto dalle strutture C
- ridurre la dimensione del codice: dead code elimination, rimpiazzo di istruzioni con altre che richiedono meno byte (es. movl $1, D -> incl D)
- Le ottimizzazioni dei compilatori sono le migliori possibili?
Secondo modulo (SC2)
Vedere gli argomenti trattati nel
diario delle lezioni A.A. 2016-2017.