Facoltà di Ingegneria Corso di Studi in Ingegneria Informatica

tesi di laurea Il sistema operativo Mac OS X

Anno Accademico 2006/2007

relatore Ch.mo prof. Domenico Cotroneo

candidato Massimiliano Di Cesare matr. 41/2764

Ad Anastasia “Per aspera sic itur ad astra”

Indice

Introduzione 6

Capitolo 1. Architettura del Mac OS X 23

1.1 Il sottosistema Darwin 25 1.2 Il kernel XNU 26 1.2.1 27 1.2.2 BSD 28 1.2.3 I/O Kit 30 1.2.4 Libreria libkern 31 1.2.5 Libreria libsa 32 1.2.6 Platform export 33 1.2.7 Kernel extensions 34 1.3 Il 34 1.4 La sicurezza 36 1.4.1 Kernel-space security 37 1.4.2 User-space security 39 1.4.3 Amministrazione del sistema 41 1.4.4 Sistema di verifica 43

Capitolo 2. Il Kernel XNU 45

2.1 XNU Source 45 2.2 Mach 51 2.2.1 Tasks e Threads 53 2.2.2 Ports 55 2.2.3 Messages 57 2.2.4 Virtual Memory e Memory Objects 58 2.2.5 Exception Handling 59 2.3 All’interno del kernel 61 2.3.1 Tipi di Trasferimento del Controllo 62 2.3.1.1 External Hardware Interrupts 63 2.3.1.2 Processor Traps 63 2.3.1.3 Software Traps 64 2.3.1.4 System Calls 64

III

Capitolo 3. I Processi 65

3.1 Astrazioni, Strutture dati e APIs del Mach 67 3.1.1 Processor sets 68 3.1.2 Processors 71 3.1.3 Tasks 75 3.1.4 Threads 77 3.1.4.1 Kernel Threads 82 3.1.5 Astrazioni relative al 85 3.1.5.1 Chiamata a procedura remota 85 3.1.5.2 Attivazione e Shuttle 86 3.1.5.3 Migrazione del thread 88 3.1.5.4 Continuazioni 89 3.1.6 I threads del Mac OS X 91 3.2 Lo Scheduling 92 3.2.1 Scheduler Operation 93 3.2.2 Scheduling Policies 100

Capitolo 4. La memoria 105

4.1 La memoria virtuale 108 4.1.1 Task Address Spaces 110 4.1.2 VM Maps 110 4.1.3 VM Map Entries 111 4.1.4 VM Objects 112 4.1.5 Pagers 114 4.1.6 Copy-on-Write 120 4.1.7 The Physical Map (pmap) 123 4.2 La memoria residente 125 4.2.1 Page Faults 129 4.3 Universal Page Lists (UPLs) 130 4.4 Unified Buffer Cache (UBC) 131 4.5 Il programma Dynamic Pager 135 4.6 L’Update Daemon 136 4.7 La memoria condivisa di sistema 136 4.8 Task Working Set 137 4.9 Memory-mapped files 140

Capitolo 5. Interprocess Communication 148

5.1 L’IPC del Mach 150 5.1.1 Le porte Mach 150 5.1.2 I messaggi IPC del Mach 153 5.2 IPC Mach: implementazione nel Mac OS X 155 5.2.1 Gli spazi IPC 155 5.2.2 Anatomia di una porta Mach 157 5.2.3 Tasks e IPC 159

IV

5.2.4 Threads e IPC 161 5.2.5 Allocazione di una porta 162 5.2.6 Implementazione del messaging 164 5.3 Lo standard POSIX 166

Capitolo 6. Il file system HFS+ 169

6.1 Concetti fondamentali 169 6.1.1 B-Trees 172 6.2 La struttura di un volume HFS+ 179 6.2.1 Frammentazione 180 6.3 Particolarità dell’HFS+ 182 6.3.1 Permessi 182 6.3.2 Journaling 183 6.3.3 Deframmentazione on-the-fly 186 6.3.4 Zona metadata 186 6.3.5 Hot File Clustering 187

Bibliografia 191

V Il sistema operativo Mac OS X

Introduzione

Alla fine del 1975, Stephen Gary “Steve” Wozniak terminò il suo prototipo di computer “fatto in casa” utilizzando componenti a basso costo. Il primo aprile del 1976 Steven Paul “Steve” Jobs, Steve Wozniak e Ronald Wayne fondarono la società Apple1, ed il loro primo prodotto fu il computer di Wozniak: l’Apple I. Alla fine dell’anno Wozniak creò l’Apple ][, basato sullo stesso processore, ma presentato come un computer “integrato”. Apple rilasciò nel luglio del 1978 la sua prima versione di DOS: l’Apple DOS 3.12. L’impegno della Apple per rendere il personal computing appetibile economicamente per le masse riuscì con l’Apple ][, ma restava la difficoltà per tali masse di interagire con i computer. Questo fatto fu percepito anche da Apple come un forte impedimento alla crescita della diffusione dei personal computers.

Nel 1979 nacque il progetto LISA (Local Integrated Software Architecture) con l’intento di creare un microcomputer integrato, autonomo, monoutente e soprattutto facile da usare. LISA fu introdotto con un sistema operativo proprietario (Lisa OS, Lisa Office System) ed una suite d’applicazioni da ufficio. Molte caratteristiche di Lisa faranno parte dei sistemi Apple a venire; infatti, molti di questi concetti esistono anche in Mac OS X.

1 Ronald Wayne lasciò l’Apple dopo meno di due settimane, poiché ritenne troppo rischioso l’investimento. 2 Il numero di versione fu 3.1 e non 1.0 perché Paul Laughton (uno degli implementatori) incrementò il contatore di revisioni ad ogni ricompilazione del codice sorgente. Il contatore partì da 0.1 ed incrementò di .1 ad ogni ricompilazione. L’Apple DOS fu quindi testato in beta come versione 3.0 e rilasciato come versione 3.1. 6 Il sistema operativo Mac OS X

Il progetto Lisa fu fallimentare a causa dei costi elevati e fu abbandonato definitivamente nel 1985. Nel settembre del 1989 l’Apple arrivò al punto di mandare in discarica circa 2700 computers, poiché lo sgravio fiscale per la rottamazione di un PC Lisa aveva superato il suo valore commerciale. C’è da ricordare che, nonostante quest’insuccesso commerciale, Lisa può considerarsi un successo tecnologico dell’Apple. Nella primavera del 1979 il progetto Annie (nato per creare una versione economica di Lisa) fu affidato a Jef Raskin, il quale ritenne che l’uso di nomi come “Lisa” e “Annie” fosse una discriminante sessuale nei suoi confronti e quindi rinominò il progetto in , un deliberato refuso di McIntosh3. Durante la battaglia legale con la McIntosh Labs (società produttrice di stereo) per il trademark, l’Apple vagliò anche degli acronimi come ad esempio MAC (Mouse Activated

Computer4). Prima che Macintosh divenisse una realtà, Raskin lasciò il progetto ed in seguito anche la società. A lui subentrò Steve Jobs che, abbandonato il progetto (business-oriented) di Lisa, riversò le sue energie nel progetto (home-oriented) Annie/Macintosh. Il prodotto Macintosh fu presentato il 24 gennaio del 1984 a Cupertino, in occasione del meeting annuale degli azionisti Apple, con un’eccellente operazione di marketing5. A differenza di Lisa, il Macintosh non fu pensato per far funzionare più sistemi operativi. Le sue ROM contenevano sia codice di basso livello (inizializzazione hardware, diagnostica, drivers, etc) che codice di alto livello (Toolbox: una collezione di routines a

disposizione delle applicazioni, più o meno come una shared library). Dopo il rilascio, l’Apple spese gli anni successivi nel miglioramento del sistema operativo Macintosh dal System 2 al . Nel marzo del 1988 un gruppo d’ingegneri e managers si riunirono e crearono 3 moduli: blue, pink e red. Blue era il modulo per migliorare il sistema operativo Macintosh esistente, e formò la base del .

3 La McIntosh è una varietà di mela. 4 Qualcuno sostiene che all’interno di Apple alcuni commentarono che la sigla MAC stesse per Meaningless Acronym Computer (computer con acronimo senza significato). 5 Steve Jobs esordì citando un brano di ‘The times they are a-changin’ di Bob Dylan e dopo la presentazione delle potenzialità del sistema (di derivazione Lisa) mise in scena un “dialogo” con un programma vocale in esecuzione sul Macintosh. 7 Il sistema operativo Mac OS X

Pink era il modulo per creare un sistema operativo orientato agli oggetti. Avrebbe dovuto avere la completa protezione della memoria, il multitasking con threads leggeri, un gran numero di spazi degli indirizzi protetti e numerose altre caratteristiche moderne. Dopo un lungo periodo d’inattività, il progetto Pink venne trasferito alla , una società congiunta tra Apple e IBM. Red era il modulo contenente le innovazioni del Pink ritenute troppo avanzate.

Figura I-1 L’architettura di A/UX

Sul finire degli anni 80 il software di sistema era alla versione 6. L’Apple presentò due interessanti sistemi operativi in questo periodo: il GS/OS e l’A/UX. Quest’ultimo, rilasciato nell’inverno del 1988, rappresentava la versione Apple conforme alle norme POSIX dell’Unix. La prima versione dell’A/UX si basava su 4.2BSD e AT&T Unix System

8 Il sistema operativo Mac OS X

Release 2, ma le versioni successive si ispirarono da 4.3BSD e varie releases susseguenti del System V. Le versioni 3.x dell’A/UX combinano un ambiente Unix System V con il Macintosh System 7 (presentato nel 1991). L’ultima versione dell’A/UX (la 3.1.1) fu rilasciata nel 1995. L’introduzione del System 7 rappresentò un enorme balzo in avanti rispetto alle precedenti versioni di Macintosh. In particolare introdusse il multitasking, ma non la protezione della memoria. Nello stesso periodo l’Apple formò un’alleanza6 con IBM e Motorola per far creare una piattaforma hardware comune (CHRP, Common Hardware Reference Platform). Grazie a tale alleanza i PowerPC approdarono nel mondo Macintosh.

Figura I-2 Transizione all’architettura PowerPC

L’architettura PowerPC nasce da un progetto dell’IBM denominato POWER (Performance

Optimized With Enhanced RISC7). Il primo PowerPC fu il 601 e fu introdotto nel settembre del 1993. Il 601 implementava il subset a 32bit dell’architettura PowerPC, ma non il set completo: ciò avrebbe permesso ai venditori di effettuare la transizione verso i PowerPC.

6 Tale sodalizio fu chiamato AIM dalle iniziali delle tre società: Apple, IBM e Motorola. 7 RISC è l’acronimo di Reduced Instruction Set Computer (computer a set di istruzioni ridotto), in contrapposizione al ‘tradizionale’ CISC (Complex Istruction Set Computer). 9 Il sistema operativo Mac OS X

Il Macintosh System 7.1.2 fu il primo sistema operativo Apple a funzionare su un PowerPC, anche se molte parti del codice non fossero native. Il porting di tutte le componenti del sistema operativo verso la nuova architettura avrebbe richiesto un tempo proibito per Apple. Inoltre era importante garantire che le applicazioni scritte per il Motorola 68K continuassero a funzionare anche sui PowerPC. L’architettura di sistema (a nanokernel) creata incluse uno strato d’astrazione hardware (HAL, layer) ed un emulatore 68K. Nel frattempo (dal 2001) la Microsoft di Bill Gates aveva lanciato con successo il suo sistema operativo Windows 3.x. Apple doveva rispondere all’assalto di Microsoft prima del rilascio di , che si proponeva come un sistema operativo end-user. I progetti Red e Pink risultarono inappropriati8 ed Apple decise di continuare a cercare una soluzione al “problema SO” in un modo o nell’altro. Star Trek fu un corposo progetto che Apple portò avanti in collaborazione con Novell per effettuare il porting del Mac OS sulle piattaforme x869. Nonostante il team congiunto d’ingegneri fosse riuscito a creare un prototipo efficace in tempi brevissimi, il progetto fu cancellato per vari motivi: Apple aveva già commissionato il PowerPC; molti all’interno ed all’esterno della Apple ritennero che il progetto potesse corrompere l’esistente modello economico; il feedback dei venditori non era affatto incoraggiante. Il progetto Star Trek fu “vendicato” nel 2005 con l’annuncio della transizione di Mac OS X sulla piattaforma x86. La prima macchina Macintosh basata sull’x86 venne presentata nel gennaio del 2006 a San Francisco. All’inizio del 1994 la Apple annunciò l’intenzione di convogliare oltre dieci anni d’esperienza nella nuova release del sistema operativo Macintosh, il Mac OS 8 (nome in codice Copland).

8 Il progetto Red fu rinominato Raptor, ma fu chiuso per mancanza di fondi e (soprattutto) per attriti interni al team di sviluppo. Il progetto Pink, che si proponeva come un sistema operativo orientato agli oggetti, fu affidato alla Taligent (controllata da Apple e IBM) nel 1992 e si trasformò in un ambiente orientato agli oggetti chiamato CommonPoint. Taligent Object Services (TalOS) fu il nome di un set di tecnologie di basso livello, costruite intorno al Mach 3.0, con l’intento di creare un sistema operativo orientato agli oggetti snello e flessibile. Nel 1995 la società Taligent passò sotto il controllo esclusivo della IBM, rendendo di fatto il progetto Pink l’ennesima occasione mancata dalla Apple. 9 Qualche anno dopo ‘Darwin’ (il core di Mac OS X) funzionerà sia su PowerPC e x86. 10 Il sistema operativo Mac OS X

Purtroppo, nonostante le grandi attese, il progetto continuava a slittare e la sua uscita prevista, e fortemente sperata, per il 1996 non sembrò fattibile. Il CEO dell’Apple, , definì lo stato di Copland come “… solo una collezione di pezzi separati, ognuno sviluppato da un team diverso… che in qualche modo dovrebbero magicamente unirsi …”

Figura I-3 L’architettura di Copland

L’Apple decise di cancellare Copland nel maggio del 1996. Amelio annunciò che le parti migliori di Copland sarebbero state utilizzate nelle future releases dei loro sistemi, sin dall’imminente System 7.6, il cui nome fu formalmente cambiato in ‘Mac OS 7.6’. Per un breve periodo Apple vagliò anche l’idea di una partenership per creare un Apple OS basandosi su altri sistemi operativi: furono presi in considerazione Windows NT (Microsoft), Solaris (Sun Microsystems) e BeOS (Be). Nei fatti fu l’acquisizione di Be da parte di Apple quella che andò materializzandosi. La Be fu fondata nel 1990 da Jean-Louis Gassée, ex capo Product Development dell’Apple. Purtroppo, nonostante il team d’ingegneri della società fosse riuscito a sviluppare un impressionante sistema operativo, BeOS non era ancora un prodotto completo ed inoltre c’erano ancora pochissime applicazioni scritte per tale sistema.

11 Il sistema operativo Mac OS X

Gassée negoziò a lungo con Apple per la vendita di Be, ma alla fine non trovarono un accordo sul prezzo e l’affare non fu concluso.10

Figura I-4 Storia dei sistemi operativi NeXT

Be ebbe un duro avversario in NeXT, una compagnia fondata da un altro ex dirigente dell’Apple: Steve Jobs. Tutte le responsabilità operative di Steve Jobs all’Apple furono

10 Gassée chiedeva 275 milioni di dollari, mentre la Apple ne offrì al massimo 200. La società Be fallì infine e le sue tecnologie furono acquisite da ‘Palm, Inc.’ nel 2001. 12 Il sistema operativo Mac OS X

“rimosse” il 31 maggio del 1985. Nel successivo mese di settembre, Jobs “reclutò” cinque impiegati Apple e cominciò un nuovo progetto: creare il computer perfetto per la ricerca.11 La Apple lo citò, ma dopo pochi mesi lasciò cadere le accuse a patto che Jobs rinunciasse a costruire computer competitivi e si impegnasse a non assumere impiegati dell’Apple per sei mesi. Nacque così la ‘NeXT Computer, Inc.’ che, partita con i soli finanziamenti personali di Jobs, trovò ben presto investitori. Il 12 ottobre del 1988, a San Francisco, Steve Jobs presentò il suo ‘NeXT cube’ corredato dal sistema operativo NEXTSTEP. Questo sistema operativo era basato sul porting del kernel CMU Mach 2.0 con un ambiente 4.3BSD ed un windows server basato su Display PostScript12. Il sistema NEXTSTEP usava come linguaggio di programmazione nativo l’Objective-C13. Al momento del lancio di NeXT cube il sistema era alla versione 0.8 e ci volle un anno affinché venisse alla luce una matura versione 1.0 ed un altro anno per la 2.0. Al NeXTWORLD Expo del 1992 fu presentato NEXTSTEP 486, per piattaforma x86.

L’ultima versione di NEXTSTEP (la 3.3) fu presentata nel febbraio del 1995.14 NEXTSTEP ha funzionato sulle piattaforme 68K, x86, PA-RISC e SPARC. Fu possibile creare una singola versione di un’applicazione contenente i binari per tutte le architetture supportate. Tali binari multi-architettura sono noti come “fat binaries”15. Nonostante l’eleganza dell’hardware e le virtù del software, la compagnia NeXT non risultò essere economicamente stabile negli anni: all’inizio del 1993 annunciò il piano di abbandonare lo sviluppo hardware per permettere al progetto NEXTSTEP di progredire

per la piattaforma x86.

11 Steve Jobs aveva persino cercato di convincere il premio Nobel per la biochimica, Paul Berg, ad utilizzare uno dei suoi computers per le simulazioni. 12 Nel 1986, Sun Microsystems annunciò il proprio Display Postscript Window System chiamato NeWS. 13 Objective-C è un linguaggio di programmazione compilato orientato agli oggetti, inventato da Brad Cox e Tom Love nei primi anni 80. Si tratta di un superset orientato agli oggetti del C, con binding dinamico e sintassi di messaging ispirata a Smalltalk. Volendo essere più semplice del C++ ne ha meno caratteristiche (come ereditarietà multipla e overloading degli operatori). Cox e Love fondarono la StepStone Corporation da cui NeXT comprò la licenza del linguaggio e creò il proprio compiler. Nel 1995, NeXT comprò tutti i diritti intellettuali relativi all’Objective-C. Il compilatore Objective-C della Apple utilizzato in Mac OS X è una versione modificata del compiler GNU. 14 Nell’inverno del 1990, Timothy John “Tim” Berners-Lee del CERN creò il primo web browser con WYSIWYG browsing ed authoring. Il browser fu sviluppato su una macchina NeXT. 15 Mac OS X supporta i fat binaries. In particolare, un fat binary può essere utilizzato per contenere le versioni a 32bit e 64bit di un programma Mac OS X dalla versione 10.4 e successive. I cosiddetti “Universal Binaries” sulla versione x86 del Mac OS X altro non sono che semplici fat binaries. 13 Il sistema operativo Mac OS X

NeXT instaurò una collaborazione con Sun Microsystems per rilasciare congiuntamente le specifiche di OpenStep, una piattaforma aperta comprendente molte API e strutture che permettono lo sviluppo di implementazioni proprie di un sistema operativo orientato agli oggetti. Le API OpenStep furono implementate su SunOS, HP-UX e Windows NT. L’implementazione propria della NeXT, essenzialmente una versione di NEXTSTEP conforme alle specifiche di OpenStep, fu rilasciata come OPENSTEP 4.0 nel luglio del 1996 (le versioni 4.1 e 4.2 seguirono a breve). Le API OpenStep ed il sistema operativo OPENSTEP, nonostante un oggettivo interesse, non cambiarono molto la situazione di NeXT. Questa cominciò a spostare i propri interessi al progetto WebObjects, un ambiente multipiattaforma per la rapida realizzazione e rilascio di applicazioni web-based.16 Insieme al sistema operativo della NeXT arrivò il suo kernel, che diventò la base per i sistemi futuri dell’Apple, in particolare del Mac OS X. Un gruppo di ricercatori dell’Università di Rochester, New York, cominciò lo sviluppo di un sistema gateway “intelligente” (RIG, Rochester’s Intelligent Gateway) nel 1975. Jerry Feldman, coniatore dell’acronimo, fece gran parte del progetto iniziale. Il RIG fu pensato per garantire un accesso uniforme, tramite terminali, ad una moltitudine di risorse locali (dischi, nastri magnetici, stampanti, plotters etc) e remote (accessibili medianti network come ARPANET). Il sistema operativo del RIG, chiamato ‘Aleph’, funzionava su un minicomputer Data

General Eclipse ed il suo kernel fu strutturato intorno ad un sistema di IPC (Interprocess Communication). I processi di RIG potevano mandarsi messaggi l’un l’altro, con un porta17 che ne specificava la destinazione. Ogni processo poteva avere più porte definite, ognuna delle quali poteva essere usata per attendere l’arrivo di un messaggio. Un processo X poteva essere shadow o interpose rispetto ad un processo Y. Nel caso dello shadowing, il processo X riceveva una copia d’ogni messaggio inviato a Y. Nel caso invece di interposing, X intercetta tutti i messaggi da e per Y. Questo sistema IPC basato

16 Il sistema operativo Mac OS X si basa sulle tecnologie di NeXT ed Apple utilizza WebObjects per i propri siti. 17 Una porta era una coda di messaggi intra-kernel globalmente unificata da una coppia di interi nel formato <α.β> indicanti il numero di processo ed il numero di porta. 14 Il sistema operativo Mac OS X

su messaggi e porte fu un mattone fondamentale del sistema operativo. Il progetto RIG fu terminato qualche anno dopo a causa di svariate imperfezioni nel progetto. fu una delle persone che lavorarono al progetto RIG. Nel 1979 si spostò alla Carnegie Mellon University, dove iniziò a lavorare su ‘Accent’, il kernel per un sistema operativo di rete. Lo sviluppo attivo dell’Accent iniziò nell’aprile del 1981. Così come RIG, anche Accent era un sistema orientato alla comunicazione che usava l’IPC come strumento base della sua struttura. Tuttavia Accent risolse molte delle imperfezioni di RIG. Si potrebbe pensare che Accent non sia altro che un’evoluzione del RIG con memoria virtuale e messaggistica network-trasparent. Accent funzionava su macchine PERQ, delle workstations grafiche prodotte da Three Rivers Corporation dal 1980. QNIX fu un ambiente UNIX, sviluppato da Spider Systems, basato su AT&T System V UNIX che funzionava al di sotto di Accent sulle macchine PERQ. 18 Una macchina LISP (SPICE LISP) fu disponibile per Accent insieme ad altri linguaggi come Ada, PERQ Pascal, C e Fortran. Il progetto Matchmaker partì nel 1981 come parte del progetto SPICE. Si trattava di un linguaggio per la specifica dell’interfaccia nato per lavorare con i linguaggi di programmazione esistenti. Attraverso Matchmaker, era possibile specificare interfacce per RPC (Remote Procedure Call) orientate agli oggetti. La

specifica era poi convertita in codice interfaccia da un compilatore multitarget. Matchmaker è comparabile al protocollo rpcgen ed al suo linguaggio. Il programma MIG (Mach Interface Generator) usato nel Mac OS X è derivato da Matchmaker. Nel giro di pochi anni, il futuro dell’Accent non risultò promettente come sembrava. Aveva bisogno di una nuova base hardware, supporto per multiprocessori, portabilità su altre tipologie hardware ed inoltre aveva difficoltà nel supporto di software UNIX. Il successore dell’Accent fu chiamato ‘Mach’, un sistema concepito come ispirato ad Accent ma pienamente compatibile UNIX. Retrospettivamente si può sostenere che RIG e

18 PERQ poteva interpretare i bytecode in hardware, in analogia con il successivo meccanismo di Java. 15 Il sistema operativo Mac OS X

Accent furono, rispettivamente, la versione 0.8 e 0.9 del Mach. Quando fu sviluppato il Mach, l’Unix era ormai in giro da oltre 15 anni e la sua complessità era cresciuta al punto da non essere facilmente modificabile. Il progetto Mach iniziò nel 1984 con l’obiettivo di creare un che sarebbe potuto essere la base per la creazione d’altri sistemi operativi. 19 Il Mach fu pensato soprattutto per implementare il processore e la gestione della memoria, ma non il file system, il networking o l’I/O: il “vero” sistema operativo doveva agire come un task a livello utente del Mach. L’implementazione del Mach utilizzava come codice base il 4.3BSD, come riferimenti per la sezione di messaggistica aveva RIG e Accent, e per quanto riguardava il sottosistema di gestione della memoria virtuale s’ispirò al sistema operativo TOPS-20 della DEC. Man mano che procedeva lo sviluppo di Mach, intere porzioni di codice del kernel BSD furono

sostituite dalle equivalenti versioni Mach con l’aggiunta di nuove componenti. 20 Il kernel Mach fu presentato, nel 1986, come la nuova base per lo sviluppo di UNIX; nonostante non tutti lo vedessero in questo modo, si accinse a diventare un sistema abbastanza popolare.21 La decisione di non supportare nessun file system, networking o I/O fu presa allo scopo di mantenere la semplicità e promuovere la portabilità dei sistemi operativi sviluppati attraverso il Mach. Uno o più sistemi operativi potrebbero funzionare su un Mach come tasks a livello utente. Tuttavia, le implementazioni reali deviarono da questo concetto. La

release 2.0 del Mach, così come la successiva e riuscitissima 2.5, ha implementazioni monolitiche in cui Mach e BSD risiedono nello stesso spazio degli indirizzi. Una delle più importanti decisioni della CMU (Carnegie Mellon University) fu di fornire tutto il software Mach con licenza non restrittiva (esente da tasse o diritti d’autore di distribuzione). La OSF (Open Software Foundation) utilizzò il Mach 2.5 per realizzare

19 Essendo scritto in C, il kernel Mach risulta molto portabile. 20 L’architettura della memoria virtuale di FreeBSD è basata sul Mach. 21 Avadis Tevanian, uno degli inventori del Mach e futuro Chief Software Technology Officer della Apple, racconta sull’origine del nome Mach. Durante un pranzo a Pittsburgh con Richard Rashid (futuro capo della Microsoft Reasearch) si discuteva dell’acronimo del progetto e Tevanian propose MUCK (Multi-User Communication Kernel oppure Multiprocessor Universal Communication Kernel). Rashid passò tale sigla ad un collega italiano, tale Dario Giuse, che pronunciò erroneamente “mach”… a Rashid piacque. 16 Il sistema operativo Mac OS X

buona parte dei servizi nel sistema operativo OSF/1. Le versioni Mach 2.x furono utilizzate anche in Mt. Xinu, Multimax (Encore), Omron LUNA/88k, NEXTSTEP e OPENSTEP. Il progetto Mach 3.0, iniziato alla CMU e continuato dall’OSF, fu la prima vera versione a microkernel: BSD funzionava come un task user-space, con le sole caratteristiche fondamentali fornite dal kernel Mach. I benefici intesi nella politica dei sistemi operativi microkernel-based, come appunto il Mach 3.0, furono presto spiazzati dai problemi prestazionali che una tale scelta comportava. Furono creati emulatori Mach per BSD, DOS, HP-UX, OS/2, OSF/1, SVR4, VMS e persino per sistemi operativi Macintosh. L’Apple e l’OSF iniziarono un progetto per eseguire il porting del Linux in modo da farlo funzionare su varie architetture PowerPC, con Linux hostato su un’implementazione Mach dell’OSF. Il progetto portò ad un core system chiamato ‘osmfk’, mentre l’intero sistema fu conosciuto come ‘MkLinux’. La prima versione, basata su Linux 1.3, fu rilasciata come MkLinux DR1 nei primi mesi del 1996. MkLinux usava un approccio a singolo server: il kernel monolitico di Linux funzionava come un singolo task sul Mach. Il Mac OS X usa una base kernel derivata da osmfk, e include molti dei miglioramenti del MkLinux. In ogni caso, tutte le componenti del kernel del Mac OS X, incluse le porzioni BSD, risiedono nello stesso spazio degli indirizzi.22

La prima release di un sistema operativo Apple, dopo l’acquisto della NeXT, fu il Mac OS 7.623 nel tardo 1996. Il piano dell’Apple era di rilasciare un’istallazione completa (stand- alone) ogni anno, intervallata da vari upgrades. Molti dei modelli e PowerBook non supportati dal Mac OS 7.6 furono integrati nell’update 7.6.1 successivo. In quel periodo c’erano due fenomeni che stavano spazzando il mondo dei computer:

22 Oltre all’A/UX e MkLinux ci fu un “terzo incontro” tra i sistemi Macintosh ed il mondo UNIX: il MachTen della Tenon System. Si trattava di un’applicazione che girava sul sistema operativo Apple, in contrasto con l’A/UX che funzionava direttamente sull’hardware. Il MacTen fu basato sul kernel Mach, calato in un ambiente BSD. Forniva il preempitive multitasking per le applicazioni Unix, mentre nell’ambiente MacOS l’esecuzione rimaneva in . 23 Fu il primo sistema ad essere nominato ‘Mac OS’. 17 Il sistema operativo Mac OS X

Internet e 95. L’Apple enfatizzò la sua compatibilità con Windows 95 e pose l’accento sula capacità di integrazione con Internet del suo sistema. Il sistema che avrebbe dovuto essere il Mac OS 7.7 diventò in realtà il Mac OS 8. Come previsto molte delle caratteristiche sviluppate per Copland furono integrate nelle versioni 8 e 9 del Mac OS. La versione 8.5 fu realizzata solo per piattaforma PowerPC. Il nanokernel fu ispezionato nel Mac OS 8.6 per integrare il multitasking ed il multiprocessing, includendo un allocatore di memoria -safe. Nel 1999 uscì il Mac OS 9, e fu salutato dall’Apple come “best Internet ever”. Fu il primo sistema operativo a poter essere aggiornato attraverso Internet. Un elemento importante del Mac OS 9 fu una matura istallazione delle API di Carbon (che all’epoca rappresentavano circa il 70% delle API dell’intero sistema). Carbon garantiva la compatibilità con le versioni 8.1 e successive. L’ultima release del Mac OS 9, la 9.2.2, fu rilasciata alla fine del 2001. Con l’avvento del Mac OS X, queste “vecchie” versioni di Mac OS furono riferite come Classic.

Il progetto, chiamato Rhapsody, dei sistemi operativi di nuova generazione basati su OPENSTEP fu presentato la prima volta al WWDC (World Wide Developers Conference) del 1997. Rhapsody consisteva nelle seguenti componenti primarie:

• Il kernel ed i relativi sottosistemi, basati su Mach e BSD; • Un sottosistema compatibile con Mac OS, chiamato Blue Box; • Un’implementazione estesa delle API OpenStep, detta Yellow Box; • Una macchina virtuale Java (JVM, Java Virtual Machine); • Un sistema a finestre basato su Display PostScript; • Un’interfaccia utente simile a quella del Mac OS ma con caratteristiche prese dall’OPENSTEP. L’Apple pianificò di portare su Rhapsody molte delle strutture chiave del Mac OS. Ci furono due releases per sviluppatori del Rhapsody, la DR1 e la DR2, e furono entrambe rilasciate sia per PowerPC sia per le piattaforme x86. Il Rhapsody supportò inoltre una moltitudine di file systems tra cui AFP (), FAT, HFS, HFS Plus, ISO

18 Il sistema operativo Mac OS X

9660 e UFS. Poco dopo il rilascio del Rhapsody DR1, l’Apple estese la versione PowerPC con un ambiente compatibile con Mac OS, chiamato ‘Blue Box’. Implementato da

un’applicazione Rhapsody (MacOS.app), il Blue Box era un ambiente virtuale che appariva come un nuovo modello hardware Macintosh. Inizialmente il Blue Box virtualizzò24 il Mac OS 8.x in full screen; in seguito fu aggiunto il supporto alle nuove versioni del Mac OS e fu introdotta la possibilità d’esecuzione in finestra. Nel Mac OS X, l’ambiente Blue Box sarà conosciuto come Classic environment (e sarà fornito

dall’applicazione ‘Classic Startup.app’). La piattaforma di sviluppo di Rhapsody fu chiamata ‘Yellow Box’. Oltre ad essere disponibile sul Rhapsody per Power Macintosh e per x86, esisteva anche una versione indipendente per Microsoft Windows. La Yellow Box comprendeva i tools di sviluppo di derivazione NeXT. Le implementazioni per Windows NT e le prime implementazioni delle API OpenStep (per piattaforme come Solaris) utilizzarono architetture simili. La Yellow Box evolse nelle API Cocoa del Mac OS X.

Dopo il rilascio di Rhapsody DR2, Apple cambiò nuovamente la sua strategia per i sistemi operativi, ma stavolta riuscì nell’intento di avere un sistema veramente nuovo. Durante il WWDC del 1998, Adobe’s Photoshop fu fatto funzionare su quello che, 3 anni dopo, sarebbe stato il Mac OS X. Nel marzo del 1999, mentre si aspettava il rilascio di Rhapsody DR3, Apple annunciò l’uscita del Mac OS X Server 1.0: essenzialmente una versione potenziata del Rhapsody con l’incorporazione di strumenti per la gestione di reti. Contemporaneamente annunciò anche una nuova iniziativa (una “” del progetto Rhapsody) chiamata Darwin: la base open-source dei futuri sistemi operativi Apple. Nel corso dei tre anni successivi (1998-2001) man mano che erano rilasciati nuovi updates per i prodotti server, progrediva lo sviluppo dei prodotti desktop, con la versione server che godeva di molti dei miglioramenti della versione desktop. Miglioramenti sostanziali

24 L’ambiente Blue Box è uno strato di virtualizzazione, non di emulazione. Le istruzioni “innocue” vengono eseguite in modo nativo sul processore, mentre quelle “dannose” (come quelle che riguardano l’hardware) sono trappate e maneggiate in modo appropriato. 19 Il sistema operativo Mac OS X furono introdotti nel corso delle quattro Developer Preview del Mac OS X. Nella prima (Mac OS X DP1, maggio 1999) ci fu l’implementazione delle API di Carbon, per consentire agli sviluppatori il passaggio dal Mac OS 9 al Mac OS X. Un’applicazione Classic richiedeva che il Mac OS X funzionasse su un’istallazione di Mac OS 9, mentre un’applicazione Carbon poteva essere compilata per funzionare in modo nativo su entrambi. Nella seconda (Mac OS X DP2, novembre 1999) la Yellow Box diventò Cocoa. Fu introdotta una JDK (Java Development Kit) ed un compilatore JIT (just-in-time). Il Blue

Box fu fornito attraverso Classic.app (una nuova versione di MacOS.app) funzionante come un processo chiamato TruBlueEnvironment. L’ambiente UNIX si basava su 4.4BSD. Furono introdotte numerose API: BSD, Carbon, Classic, Cocoa e Java. Nella terza (Mac OS X DP3, gennaio 2000) furono introdotti (la nuova interfaccia utente grafica) e Dock. Nella quarta (Mac OS X DP4, maggio 2000) il fu rinominato Desktop. Fece la sua prima comparsa l’applicazione (Preferences.app), permettendo all’utente di configurare una moltitudine di preferenze di sistema come: Classic, ColorSync, Date&Time, Energy Saver, Internet, keyboard, Login Items, Monitor, Mouse, Network, Password e così via. Il Dock fu scorporato dal Finder e reso un’applicazione a se stante (Dock.app). Apple rilasciò una versione beta di Mac OS X all’Apple Expo di Parigi il 13 settembre del 2000. In vendita a $29.95 e disponibile in Inglese, Francese e Tedesco si trattava essenzialmente di una preview pubblicamente ottenibile per la valutazione e lo sviluppo: la confezione conteneva un messaggio dell’Apple ai beta testers che recitava “You are holding the future of the Macintosh in your hands”. Anche se la ‘Mac OS X Public Beta’ fosse priva d’importanti caratteristiche e apparentemente mancasse di stabilità e prestazioni, riuscì a mostrare molte importanti tecnologie Apple all’opera (in modo particolare a coloro che non avevano seguito l’evoluzione delle varie releases DP). Con Darwin, l’Apple cercò continuamente di fare leva su di un gran numero di software open-source per utilizzarli con (ed a volte integrarli in) Mac OS X.

20 Il sistema operativo Mac OS X

L’Apple e l’Internet System Consortium, Inc. (ISC) fondarono congiuntamente il progetto OpenDarwin nell’aprile del 2002 per promuovere lo sviluppo open-source di Darwin.25 Il kernel di Darwin si chiama XNU, ufficiosamente acronimo ricorsivo per “Xnu is Not Unix”26. Basato fondamentalmente su Mac e FreeBSD, include anche codice e concetti da varie fonti come ad esempio il progetto MkLinux (sostenuto direttamente dall’Apple), il lavoro svolto sul Mach dall’University of Utah, NetBSD e OpenBSD.

Figura I-5 Storia del sistema Mac OS X

25 GNU-Darwin è un sistema operativo open-source basato su Darwin. 26 Sembra essere anche un tributo al fatto che effettivamente lo XNU per il Mac OS X rappresenta il NuKernel (mai riuscito a realizzare) per i sistemi precedentemente realizzati da Apple. 21 Il sistema operativo Mac OS X

La prima versione del Mac OS X fu rilasciata il 24 marzo del 2001 con il nome di Mac OS X 10.0 Cheetah. Poco tempo dopo, lo schema delle versioni del prodotto server fu rivisto per sincronizzarsi con quello del prodotto desktop. Da allora la tendenza è il rilascio di una nuova versione desktop, seguita a breve dall’equivalente revisione server.27

Version Codename Release Date 10.0 Cheetah March 24, 2001 10.1 Puma September 29, 2001 10.2 Jaguar August 23, 2002 10.3 Panther October 24, 2003 10.4 Tiger April 29, 2005 10.5 Leopard August 7, 200628

27 I primi computer Macintosh con piattaforma x86 venduti, utilizzavano il Mac OS X 10.4.4 come sistema operativo. 28 Il ‘Mac OS X 10.5 Leopard’ è in commercio dal 26 ottobre 2007. 22 Il sistema operativo Mac OS X

Capitolo 1 Architettura del Mac OS X

Il Mac OS X è un miscuglio di differenti tecnologie che differiscono non solo in ciò che fanno ma anche nella provenienza, nella filosofia che rappresentano e nel modo in cui sono implementate. All’utente finale, tuttavia, il Mac OS X si presenta con un’immagine coesa e consistente. Il fatto che i computers Apple abbiano una base hardware limitata e ben definita ne ha sicuramente aiutato il successo, a fronte di un certo ecletticismo del software che compone il sistema operativo. Da un punto di vista high-level, il Mac OS X può essere considerato come composto da tre classi di tecnologie: quelle originate all’interno dell’Apple, quelle originate all’interno della NeXT e quelle originate “in qualunque altro posto”29. Da una parte una tale confluenza di fonti rende in qualche modo difficile visualizzare chiaramente la struttura del Mac OS X, e potrebbe creare un muro per i nuovi programmatori che volessero avvicinarsi al mondo della mela. Dall’altra parte i programmatori con un minimo d’esperienza hanno a loro disposizione un ambiente variopinto in cui dare sfogo ai propri fervori creativi. L’utente finale rimane in ogni caso il maggior beneficiario, potendo contare su una varietà di software mai vista su una singola piattaforma. Il Mac OS X fornisce i benefici di un tipico sistema UNIX mantenendo la tradizionale facilità d’uso del Macintosh. Il suo ambiente UNIX è sufficientemente conforme allo standard da garantire

29 Questa terza classe è principalmente composta di software open-source sviluppato in altri ambiti. Notiamo comunque che anche nelle altre due classi è presente software open-source. 23 Il sistema operativo Mac OS X

la portabilità della maggior parte dei software per UNIX30. Molti altri programmi non- UNIX (come o Adobe Creative Suite) sono disponibili in modo nativo per Mac OS X, per non parlare poi della varietà di programmi propri della Apple.

Figura 1.1 Architettura high-level del Mac OS X

Nella figura 1.1 possiamo vedere una rappresentazione stratificata dell’architettura. L’immagine è da considerarsi approssimata poiché è inutile cercare di dividere il sistema

in strati assolutamente separati: spesso ci sono sovrapposizioni degli strati dovute a componenti complesse. Il firmware non è tecnicamente parte del Mac OS X, ma gioca un ruolo importante nelle operazioni di un computer Apple. Laddove i computer Apple PowerPC-based utilizzano Open Firmware, i sistemi x86-based utilizzano Extensible Firmware Interface (EFI). Open Firmware è un boot firmware non proprietario, non dipendente dalla piattaforma, che risiede nella boot ROM di un computer Apple-PowerPC. Il suo ruolo nell’avvio del

30 Il Mac OS X è considerato da molti come “un sistema UNIX per le masse”. 24 Il sistema operativo Mac OS X

sistema è in qualche modo analogo a quello del PC BIOS di un’architettura x86, con l’aggiunta di altre capacità come: booting personalizzato, diagnosi, debuggind ed anche programmazione. Nei fatti l’Open Firmware è, a suo modo, un ambiente di runtime e di programmazione accessibile dall’utente. L’EFI è concettualmente molto simile all’Open Firmware. Il bootloader della versione PowerPC del Mac OS X si chiama BootX, che risiede come un singolo file nel file system. L’Open Firmware carica il file da una periferica di boot31 e lo esegue nel suo ambiente runtime. Il codice del BootX esegue una serie di passi affinché il kernel del Mac OS X possa funzionare, ed eventualmente lancia il kernel stesso. La

versione x86 del Mac OS X usa un bootloader chiamato boot.efi, che è un eseguibile che gira in ambiente EFI. La nostra trattazione riguarderà solo i livelli più bassi dell’architettura del Mac OS X.

1.1 Il sottosistema Darwin Darwin nasce come una fork di una developer release di Rhapsody, il precursore del Mac OS X. Un’importante componente di Darwin è l’ambiente kernel del Mac OS X che, unito all’ambiente user del Darwin stesso, rende questo un sistema operativo stand-alone. L’Apple annunciò la transizione del Mac OS X su piattaforme x86 solo nel 2005, mentre Darwin fu sempre supportato sia su piattaforme PowerPC che x86. Darwin può essere meglio compreso come una collezione di tecnologie open source che

sono state integrate da Apple per formare una parte fondamentale di Mac OS X. L’Apple mette a disposizione Darwin come un set di packages, dove ogni pacchetto è un archivio contenente il codice sorgente di una parte del Mac OS X. Il numero di pacchetti presenti varia da release a release, ad esempio nel Darwin 8.6 per PowerPC (relativo al Mac OS X 10.4.6) sono presenti circa 350 packages. Come nello stile Apple, anche Darwin contiene codice sorgente sia di Apple che di terze parti tra le quali Open Source e Free Software Foundation (FSF). Le componenti sviluppate all’interno della società sono generalmente rilasciate con licenza APSL (Apple

31 Con dispositivo di boot, in questo caso, si intende un dispositivo di memorizzazione locale o la rete. 25 Il sistema operativo Mac OS X

Public Source License)32, che dalla versione 2.0 è stata classificata dalla FSF come una ‘free software license’. Darwin quindi rappresenta una grande quantità di software che l’Apple ha riunito da una varietà di fonti come NEXTSTEP e OPENSTEP, Mach, vari flavors di BSD (principalmente FreeBSD), la GNU software suite, il progetto XFree86 e così via. La cosa più importante, però, è che tutti questi software “esterni” sono stati integrati nel Mac OS X molto efficacemente, grazie ad importanti modifiche e ottimizzazioni. Anche se è possibile configurare e controllare la maggior parte di tale software così come si farebbe normalmente su un tradizionale sistema UNIX, il Mac OS X mette a disposizione interfacce utente semplificate e più consistenti che riescono a nascondere la sottostante complessità. Questa efficienza nell’adottare tecnologie da diverse fonti, integrandole in modo sinergico, è una delle grandi forze del Mac OS X. È importante notare che Darwin è solo un sottoinsieme del Mac OS X (è essenzialmente le fondamenta su cui si basa) e che, pur essendo privo delle tecnologie visive del Mac OS X, è possibile far girare Darwin con l’X Window System che ne fornisce l’interfaccia utente grafica.

1.2 Il kernel XNU Il kernel del Mac OS X si chiama XNU e, in maniera semplicistica, può essere visto composto da un core basato su Mach 3, una “personalità” da sistema operativo basata sul

FreeBSD 5 ed un ambiente runtime orientato agli oggetti per le estensioni del kernel (drivers compresi). Un kernel in esecuzione contiene numerosi drivers che non risiedono nel codice base del kernel, ma hanno i propri packages Darwin: in questo senso possiamo affermare che il kernel di Mac OS X è qualcosa di più di XNU, ma noi ci riferiremo con il termine XNU alla combinazione del codice di base (quello implementato nel package XNU di Darwin per intenderci) e di tutte le estensioni del kernel.

32 Il codice esterno è fornito con le relative licenze, quali ad esempio la GNU General Public License (GPL), la BSD License, la Carnegie Mellon University License e così via. 26 Il sistema operativo Mac OS X

Fatta questa premessa, possiamo dividere il kernel del Mac OS X nelle seguenti parti:

• Mach – lo strato di servizio • BSD – il fornitore primario dell’interfaccia di programmazione di sistema • L’I/O Kit – l’ambiente runtime per i drivers • libkern – una libreria interna al kernel • libsa – una libreria interna al kernel utilizzata solo all’inizio del system startup • il Platform Expert – il modulo di astrazione hardware • Kernel – varie famiglie di I/O Kit, la maggior parte dei drivers di periferica caricabili e qualche estensione non appartenente all’I/O Kit Il package XNU del Darwin contiene approssimativamente un milione di linee di codice, in cui una metà sono riconducibili al BSD ed un terzo al Mach. Le varie estensioni del kernel, di cui non tutte sono necessarie (o caricate) in un dato sistema, formano nell’insieme un altro milione di linee di codice. Il numero di kernel extensions caricate in un certo momento in un dato sistema sono significativamente meno di quelle presenti nel sistema33.

1.2.1 Mach Se il kernel XNU è il cuore del Mac OS X, allora il Mach può essere considerato il cuore di XNU. Il Mach fornisce servizi di basso livello critici che sono trasparenti alle applicazioni. Gli aspetti del sistema di cui è responsabile il Mach includono:

• Astrazione hardware (in parte) • Gestione del processore (incluso lo scheduling ed il multiprocessing simmetrico) • Preempitive multitasking (incluso il supporto ai tasks ed ai threads) • Gestione della memoria virtuale (incluso il paging di basso livello, la protezione della memoria, la condivisione e l’ereditarietà)

• Meccanismi IPC di basso livello (che sono la base dello scambio di messaggi nel kernel)

• Supporto real-time (che permette alle applicazioni time-sensitive di avere accesso

33 Il comando kextstat può essere utilizzato per ottenere una lista delle estensioni caricate. 27 Il sistema operativo Mac OS X

latency-bounded alle risorse del processore)

• Supporto al debugging del kernel34 • Console I/O Il Mach è spesso inequivocabilmente identificato come microkernel, anche se solo dalla versione 3 fu realmente tale. Le precedenti versioni, inclusa la 2.5 da cui nacque il sistema operativo OSF/1 dell’Open Software Foundation, avevano un’implementazione monolitica in cui BSD e Mach risiedevano nello stesso spazio degli indirizzi del kernel. Anche se Apple utilizza una implementazione del Mach che deriva dalla versione 3, lo XNU non utilizza il Mach come un microkernel tradizionale. Vari sottosistemi che sarebbero implementati come servers nello user-space di un vero sistema microkernel, sono parti proprie del kernel nel Mac OS X. In particolare la porzione BSD dello XNU, l’I/O Kit ed il Mach risiedono nello stesso spazio degli indirizzi: tuttavia hanno responsabilità ben definite che li separano in termini di funzioni ed implementazione.

1.2.2 BSD Il kernel XNU contiene una quantità sostanziale di codice di derivazione BSD, ed è quello a cui collettivamente ci riferiamo quando ci riferiamo a BSD nel contesto del Mac OS X. Non è un caso se un kernel BSD ben definito giri all’interno di XNU, sia come un singolo task del Mach che viceversa. Tuttavia se alcune porzioni di XNU di derivazione BSD sono molto simili alle loro forme

originali, altre porzioni invece sono abbastanza differenti visto che sono state realizzate per coesistere con entità estranee al BSD (come ad esempio l’I/O Kit ed il Mach). Di conseguenza è possibile trovare parecchi casi di codice proveniente da diverse origini intessuti nel kernel XNU. Le funzioni di cui il codice BSD è responsabile includono:

• Il modello dei processi nello stile BSD • I Segnali • Gli User IDs, i permessi e le politiche base della sicurezza

34 Il kernel debugger di basso livello integrato nello XNU si chiama KDB (o DDB). È implementato nella porzione Mach del kernel, così come KDP che è il protocollo remoto di debugging del kernel usato dal debugger GNU (GDB). 28 Il sistema operativo Mac OS X

• Le APIs del POSIX • Le APIs di I/O asincrono (AIO, Asynchronous I/O) • Le chiamate di sistema nello stile BSD • Lo stack TCP/IP, i sockets BSD ed il firewalling • Le Network Kernel Extensions (NKEs)35 • Lo strato VFS (Virtual File System) e numerosi file systems, incluso un meccanismo di journaling a livello VFS indipendente dal file system

• I meccanismi IPC del System V e del POSIX • Una struttura crittografica intra-kernel • Un sistema di notifiche (basato sul meccanismo kqueue/kevent del FreeBSD), che è un servizio a livello di sistema che abilita notifiche tra applicazioni e dal kernel alle applicazioni

• Il meccanismo di notifica del cambio di file system fsevents (utilizzato dalla tecnologia di ricerca )

• Le liste di controllo degli accessi (ACLs, Access Control Lists) e la struttura d’autorizzazioni kauth36

• Varie primitive di sincronizzazione Alcune funzionalità del kernel hanno implementazioni a basso livello in una porzione del kernel con strati d’astrazione d’alto livello in un’altra porzione. Per esempio la

tradizionale struttura dei processi (struct proc), che è la struttura dati primaria del kernel rappresentante un processo UNIX, è contenuta nella porzione BSD così come la u- area37. Parlando in senso stretto, nel Mac OS X non è eseguito nessun processo BSD: esso coincide con un task Mach che contiene uno o più threads Mach, e sono questi threads ad

35 Si tratta di un tipo particolare di estensioni del kernel che rendono l’architettura networking di BSD pienamente compatibile con XNU. Prima del Mac OS X 10.4, un NKE era un’estensione del kernel appositamente progettata. A partire dalla versione Tiger, le funzionalità di NKE sono accessibili ad una regolare estensione del kernel attraverso un set di KPIs (Kernel Programming Interfaces). 36 A partire dalla versione 10.4 del Mac OS X, il kauth è utilizzato per la valutazione delle ACLs. Si tratta di una struttura estensibile d’autorizzazioni a carattere generale. 37 Storicamente l’area utente (user area o u-area) è il nome di strutture contenenti dati per-process o per-thread che è possibile scambiare. 29 Il sistema operativo Mac OS X

essere eseguiti. Consideriamo ad esempio la chiamata di sistema fork() che, a meno di varianti come vfork(), è il solo modo per creare un nuovo processo in un sistema UNIX. Nel Mac OS X i task ed i threads Mach sono creati e manipolati attraverso delle ‘Mach calls’, che l’utente non utilizza direttamente: l’implementazione BSD-style della fork utilizza queste chiamate per creare un task (ed allocarne e inizializzarne la struttura processo associata) ed un thread. Dal punto di vista del chiamante la fork(), questa appare atomica e con le strutture dati Mach e BSD-style sincronizzate. Similmente la UBC (Unified Buffer Cache) del BSD si aggancia al sottosistema della memoria virtuale del Mach. La UBC permette al file system ed al sottosistema della memoria virtuale di condividere i buffers di memoria del kernel. Ogni memoria virtuale di un processo contiene in genere un mappa sia della memoria fisica che dei file su disco. Unificando la buffer cache si ottiene un singolo “deposito” per varie entità, riducendo il numero di accessi al disco e la quantità di memoria utilizzata.

1.2.3 I/O Kit Lo XNU ha un framework orientato agli oggetti per i drivers di periferica chiamato I/O Kit, che utilizza un ristretto sottogruppo del C++38 come linguaggio di programmazione. L’implementazione dell’I/O Kit consiste nelle librerie C++ residenti nel kernel (libkern e

I/O Kit) ed un framework per lo spazio utente (IOKit.framework). Le librerie residenti nel kernel sono disponibili ai drivers caricabili.

Notiamo che il framework del kernel (Kernel.framework) incapsulano le librerie residenti nel kernel in modo da esportare i loro header files – il codice eseguibile per

queste librerie è contenuto nel kernel. L’ IOKit.framework è un framework convenzionale utilizzato per scrivere programmi user-space che comunicano con l’I/O Kit. L’architettura runtime dell’I/O Kit è modulare ed a strati. Fornisce un’infrastruttura per catturare, rappresentare e mantenere relazioni tra varie componenti hardware e software che sono coinvolte nelle connessioni di I/O. In questo modo l’I/O Kit presenta delle

38 Le caratteristiche non supportate da questo sottogruppo dell’Embedded C++ comprendono: eccezioni, ereditarietà multipla, templates, costruttori complicati, liste di inizializzazione e RTTI (runtime type identification). L’I/O Kit comunque implementa una propria, minima, RTTI. 30 Il sistema operativo Mac OS X

astrazioni dell’hardware sottostante al resto del sistema. Ad esempio l’astrazione di una partizione disco comprende relazioni dinamiche tra numerose classi di I/O: il disco fisico, il controller del disco, il bus a cui è attaccato il controller e così via. Il modello di fornito con l’I/O Kit ha una moltitudine di utili caratteristiche, tra cui le seguenti:

• Interfacce di programmazione estese, incluse interfacce per applicazioni e drivers user-space per comunicare con l’I/O Kit

• Numerose famiglie di periferiche come ATA/ATAPI, FireWire, Graphics, HID, Network, PCI e USB

• Astrazioni orientate agli oggetti delle periferiche • Plug-and-play e hot-plugging (il dynamic device management) • Power management • Preempitive multitasking, threading, multiprocessing simmetrico, protezione della memoria e data management

• Identificazione e caricamento dinamico di drivers per bus di tipo multiplo • Registro di I/O, un database per tracciare e mantenere dettagliate informazioni sugli oggetti stanziati

• Catalogo di I/O, un database di tutte le classi di I/O Kit a disposizione in un sistema • Driver stacking Le periferiche standard, conformi alle ben definite e ben supportate specifiche, non richiedono tipicamente dei driver di I/O Kit specifici. Anche se una periferiche dovesse richiedere un driver custom, avrebbe bisogno solo di un driver user-space se utilizzasse una connessione FireWire o USB.

1.2.4 Libreria libkern La libreria libkern implementa il sistema di runtime per il ristretto sottogruppo del C++ utilizzato dal modello di programmazione dell’I/O Kit. Oltre a fornire i normali servizi necessari ai drivers, libkern contiene anche classi che sono generalmente utili allo sviluppo

di software del kernel. In particolare definisce la classe OSOBject che, oltre ad essere la

31 Il sistema operativo Mac OS X

root base class per il kernel del Mac OS X, implementa il dynamic typing per il supporto ai moduli del kernel caricabili. I seguenti sono alcuni esempi delle funzionalità fornite da libkern:

• Allocazione, costruzione e distruzione dinamica di oggetti (con supporto per una varietà di oggetti incorporati come arrays, booleans e dictionaries)

• Operazioni atomiche e funzioni varie come bcmp(), memcmp() e strlen() • Funzione per il byte-swapping • Disposizioni per tracciare il numero di istanze correnti per ogni classe • Meccanismi per alleviare il C++ “fragile base-class problem”39 • 1.2.5 Libreria libsa La libsa40 è una libreria di supporto (essenzialmente un linker) in-kernel, utilizzata durante le prime fasi dell’avvio del sistema per caricare le estensioni del kernel. Le estensioni del kernel del Mac OS X sono normalmente caricate, su richiesta, attraverso

il daemon dello spazio utente kextd (/usr/libexec/kextd). Durante le prime fasi del bootstrapping il kextd non è disponibile, quindi la libsa ne fornisce un subset al kernel. Esempi delle specifiche funzionalità implementate dalla libsa per caricare, lineare e registrare i file oggetto dell’estensioni del kernel includono i seguenti:

• Allocazione di memoria semplice • Ricerca binaria • Ordinamento • Funzioni varie di manipolazione delle stringhe • Symbol remangling • Un pacchetto del grafico di dipendenza utilizzato durante la determinazione delle

39 Il problema della fragile base-class si ha quando modifiche ad un classe non-foglia, provocano la rottura di una classe derivata. Una classe si dice non-foglia quando è la base di almeno un’altra classe. La suddetta rottura avviene perché la classe derivata conta (implicitamente o esplicitamente) sulla conoscenza di alcune caratteristiche della classe non- foglia. Esempi di queste caratteristiche sono la dimensione della tabella virtuale della classe base (vtable), offset nella vtable, offset dei dati protetti della classe e offset dei dati pubblici. La libekern fornisce metodi per creare un numero limitato, ovviamente, di spazi riservati all’inserimento di futuri data members e funzioni virtuali. 40 Il finale ‘sa’ nel suo nome è un riferimento al suo essere una libreria che fornisce funzioni che sono usate da applicazioni stand-alone, il kernel in questo caso. Libreria stand-alone esistono in altri sistemi operativi, con il nome libstand, per fornire un ambiente runtime minimo. 32 Il sistema operativo Mac OS X

dipendenze tra le estensioni del kernel

• Decompressione di kernels compressi e verifica dei checksum Notiamo che libsa non è una libreria del kernel generalmente disponibile: in un tipico scenario di bootstrap, il codice libsa è rimosso dal kernel appena diventa disponibile

kextd. Anche nel caso in cui fosse presente, le sue funzioni costituenti non sono disponibili al kernel in nessun’interfaccia di programmazione.41

1.2.6 Platform Expert Il Platform Expert è un oggetto (essenzialmente un driver specifico della motherboard) che conosce il tipo di piattaforma sui cui sta girando il sistema. L’I/O Kit registra un nub42 per il Platform Expert durante l’inizializzazione del sistema. Una istanza della classe IOPlatformExpertDevice diventa la radice dell’albero della periferica, quindi il root nub carica il corretto driver della specifica piattaforma, che successivamente scopre i busses presenti nel sistema ed assegna un nub per ogni bus trovato. L’I/O Kit carica un corrispondente driver per ogni bus nub, che in seguito scopre le periferiche collegate al bus e così via. L’astrazione del Platform Experts fornisce l’accesso ad una vasta varietà di funzioni e informazioni specifiche della piattaforma, come quelle relative a:

• Costruire gli alberi di periferica • Analizzare di alcuni argomenti di boot • Identificare la macchina (che include la determinazione della velocità di clock del processore e del bus)

• Accedere alle informazioni di Power Management • Recuperare e settare il tempo di sistema • Recuperare e settare le informazioni di console

41 Il kernel accede alla funzionalità di caricamento delle estensioni di libsa attraverso un puntatore a funzione condiviso tra libsa ed il kernel. La funzione costruttore di libsa inizializza tale puntatore in modo da puntare alla relativa funzione di libsa. 42 Nel contesto dell’I/O Kit, un “nub” è un oggetto che definisce un punto di accesso ed un canale di comunicazione per una periferica fisica o un servizio logico. Un periferica fisica può essere un bus, una partizione disco, una scheda grafica e così via; esempi di servizi logici sono l’arbitraggio, il riconoscimento del driver ed il power management. 33 Il sistema operativo Mac OS X

• Fermare e riavviare la macchina • Accedere al controllore delle interruzioni • Creare la stringa del numero di serie del sistema • Salvare le informazioni di kernel panic • Inizializzare un’interfaccia utente da utilizzare in caso di kernel panics • Leggere e scrivere la memoria non volatile (NVRAM) • Leggere e scrivere la memoria di parametro (PRAM)

1.2.7 Kernel extensions Oltre al nucleo del kernel, l’ambiente kernel del Mac OS X include le estensioni del kernel che sono caricate dinamicamente quando necessarie. Molte delle estensioni standard sono indirizzate all’I/O Kit, ma ci sono eccezioni come quelle collegate alla rete ed al

filesystem (come ad esempio webdav_fs.kext e PPP.kext). In una tipica istallazione del Mac OS X ci possono essere circa mille estensioni del kernel caricate nello stesso momento, ma molte altre si trovano nella directory

/System/Library/Extensions/.

1.3 Il file system Il file system, o meglio il suo contenuto ed il suo layout, è fondamentale per il modo in cui gli utenti interagiscono con il sistema. Il layout del file system del Mac OS X è

principalmente una superimposizione di file systems di derivazione UNIX e NEXTSTEP, con molte influenze provenienti dal tradizionale Macintosh. Mac OS X divide concettualmente il file system in quattro domini: User, Local, Network e System. Il dominio User contiene risorse specifiche dell’utente ed è quello che nella terminologia UNIX sarebbe la user’s home directory. Per un utente di nome massimiliano, la locazione

di default della home directory locale è /Users/massimiliano/, e la locazione di default della network home directory è /Network/Users/massimiliano/. Una user’s home directory contiene generalmente molte directory standard come ad esempio:

34 Il sistema operativo Mac OS X

.Trash, Applications, Desktop, Documents, Library, Movies, , Pictures, Public e Sites. Alcune directory per-user (come Public e Sites) sono pensate per essere pubblicamente accessibili ed a volte hanno permessi di lettura verso altri utenti. Il dominio Local contiene risorse disponibili a tutti gli utenti su un singolo sistema, include applicazioni e documenti condivisi ed è, usualmente, localizzato nel boot volume

(che in genere è anche il root volume), la directory /Applications/ risiede nel dominio Local. In contrapposizione al dominio User, che può essere manipolato arbitrariamente dal proprio utente, solo un utente con i privilegi di amministratore di sistema può modificare il dominio Local. Il dominio Network contiene risorse disponibili a tutti gli utenti della LAN, come ad esempio applicazioni e documenti che sono condivise attraverso una rete, normalmente localizzato in un file server localmente montato su una macchina client sotto

/Network/. Solo un utente con privilegi di amministratore di rete può modificare tale dominio. Directories specifiche del dominio Network includono Applications, Library, Servers e Users. Il dominio System contiene le risorse appartenenti al Mac OS X (come il sistema operativo, le librerie, i programmi, gli scripts ed i files di configurazione) e risiede, come il dominio Local, in un volume boot/root. La sua locazione standard è la directory

/System/. Il sistema ricerca le risorse, come fonts e plug-ins, nei vari domini partendo dal più specifico fino al più generico, secondo l’ordine User→Local→Network→System. Ogni dominio del file system contiene diverse directories standard, alcune delle quali esistono in più domini o addirittura in tutti. Forse la più interessante di queste è la

Library, che contiene una gerarchia di diverse subdirectories standard. In particolare, una parte sostanziale del sistema operativo risiede in /System/Library/.

35 Il sistema operativo Mac OS X

1.4 La sicurezza Con security, nel nostro ambito, si intende l’unione di software, hardware, politiche e pratiche che permettono ad un sistema ed ai suoi utenti di:

• Verificare le identità degli utenti e dei servizi di sistema • Salvaguardare le informazioni sensibili43 durante l’immagazzinamento, la trasmissione e l’utilizzo Una risorsa di sistema (incluse quelle esterne, condivise) è vulnerabile agli attacchi: dall’esterno e a volte anche dall’interno. Possiamo pensare ad un vulnerabilità come ad un potenziale (come ad esempio un bug software, di un errore di progetto, di una errata configurazione e così via) per un uso non previsto. In questi termini potremmo, in modo molto informale, definire con il termine security quella condizione in cui tutte le risorse del sistema sono, in ogni caso, utilizzate “as intended”. Quando sfruttate attraverso degli attacchi, le vulnerabilità possono portare a danni, tangibili o meno. I seguenti sono esempi di alcuni comuni tipi di danno potenziale:

• Fuoriuscita di dati sensibili • Modifica di dati sensibili • Distruzione di dati sensibili • Uso non autorizzato di un servizio di sistema • Rifiuto di un sistema in modo che i suoi utenti legittimi non possano utilizzarlo • Distruzione o degradamento di un’operazione di sistema Una risorsa di sistema può essere abusata senza rifiutare il servizio verso utenti legittimi o senza causare nessun danno apparente al sistema stesso. Per esempio, se una risorsa di sistema sta giacendo inattiva, può essere abusata come punto d’ingresso per infiltrarsi in un altro sistema. Le caratteristiche della sicurezza del Mac OS X possono essere divise a

livello kernel e a livello utente44.

43 Sono, ad esempio, informazioni sensibili i dati personali degli utenti, le chiavi crittografiche e le password. 44 Esiste anche una password a livello di firmware che potrebbe essere usata in un computer Apple. 36 Il sistema operativo Mac OS X

1.4.1 Kernel-space security Il modello della sicurezza al livello di kernel del Mac OS X comprende sia le caratteristiche specifiche del sistema operativo, sia quelle tipiche dei sistemi UNIX. Ad esempio caratteristiche relative alla sicurezza al livello del kernel sono:

• Identificatori di utenti e gruppi BSD (UIDs e GIDs) – i tradizionali UIDs e GIDs sono la base, poco flessibile, dei sistemi di sicurezza. Esempi di politiche di sicurezza basate sugli ID includono la gestione della proprietà ed i permessi (lettura, scrittura ed esecuzione) degli oggetti del file system oltre a operazioni ristrette a processi con UID effettivo pari a 0 (root euid policy) oppure operazioni su un oggetto ristretto ad un processo legato al proprietario dell’oggetto stesso o ad un processo con UID effettivo pari a 0 (owner or root euid policy).

• Diritti sulle porte Mach (Mach port rights) – oltre ad essere canali IPC, una porta Mach può rappresentare una varietà di risorse, come ad esempio tasks, threads, blocchi di memoria, processori e altre periferiche. Inoltre Mach è un sistema in cui i diritti sulle porte determinano quali operazioni un task può compiere o no sulle porte in questione. Il kernel gestisce e protegge le porte, assicurando che solo i tasks con i diritti richiesti possano eseguire operazioni privilegiate.

• Sistema di verifica (auditing system) – il Mac OS X implementa un sistema di verifica basato su BSM (Basic Security Module), che è sia un format di verifica della sicurezza, sia una API utilizzata per tracciare gli eventi relativi alla sicurezza che avvengono nel sistema.

• Accounting di processo (process accounting) – l’accounting a livello del sistema per ogni processo in esecuzione può essere abilitato o disabilitato mediante il

comando accton. Quando l’accounting di processo è abilitato, il comando lastcomm fornisce informazioni sull’ultimo comando eseguito. • Memoria virtuale criptata (encrypted virtual memory) – il kernel può, eventualmente, utilizzare l’algoritmo AES per criptare le pagine di memoria virtuale che sono scambiate con la memoria secondaria.

• ACLs – le ACLs del file system sono supportate per un raffinato e flessibile

37 Il sistema operativo Mac OS X

controllo delle ammissioni quando si utilizzano informazioni residenti sul disco. All’interno del file system le ACLs relative ad ogni singolo file sono implementate come attributi estesi.

• Kauth – il Kauth è un meccanismo in-kernel flessibile ed estensibile per la valutazione delle ACLs. Permette ai programmatori del kernel di istallare i propri callbacks (o listeners) per le richieste d’autorizzazione nel kernel. Quando un’entità vuole eseguire un’operazione su di un oggetto, tutti i listeners registrati sono invocati e vengono loro fornite informazioni contestuali riguardanti le credenziali del richiedente e l’operazione richiesta: un listener può permettere, negare o rinviare45 la richiesta.

Figura 1.4.1 Architettura della sicurezza nel Mac OS X

45 Il rinvio serve sostanzialmente a tirare fuori dalla decisione un listener, in modo che sia gli altri (o eventualmente il default listener) a decidere se soddisfare o meno la richiesta. 38 Il sistema operativo Mac OS X

1.4.2 User-space security Al livello utente, il Mac OS X fornisce un modello molto flessibile di sicurezza largamente basato sul CDSA (Common Data Security Architecture), un’architettura open source adottata come standard tecnico dall’Open Group46. Il CDSA consiste in un framework crittografico e vari livelli di servizi per la sicurezza. L’implementazione specifica della Apple è rappresentata nella figura 1.4.1. Il CDSA permette l’implementazioni di caratteristiche di sicurezza come encryption, autenticazione dell’utente, gestione dei permessi di accesso e immagazzinamento sicuro dei dati. Lo strato più basso del CDSA consiste in plug-ins utilizzati dallo strato superiore. Lo standard CDSA non impone una limitazione nel numero di plug-ins e permette che essi si invochino l’un l’altro. Il cuore del CDSA è un set di moduli chiamati CSSM (Common Security Services Manager). Nella figura 1.4.1 possiamo vedere i Managers CSSM implementati nell’Apple CDSA, che nell’insieme formano l’API del CSSM. Fintanto che i plug-ins rispettano le regole di interfaccia con i managers CSSM, essi possono implementare qualsiasi sottogruppo delle caratteristiche del CDSA, incluse le combinazioni di caratteristiche associate a due o più managers. Le applicazioni del Mac OS X utilizzano normalmente delle APIs del middleware di sicurezza della Apple, costruite sulle APIs CSSM per accedere alle funzionalità CDSA. È comunque possibile che un’applicazione utilizzi direttamente l’API CSSM. Esempi di servizi forniti dalle APIs middleware includono:

• Keychan Services che forniscono l’immagazzinamento sicuro per certificati, chiavi, passwords e altre informazioni arbitrarie

• Secure transport che forniscono comunicazioni di rete sicure attraverso l’implementazione dei protocolli SSL (Secure Socket Layer) e TSL ()

46 Le specifiche del CDSA furono inizialmente sviluppate da Intel Architecture Labs, ma l’attuale standard è frutto della collaborazione di molte organizzazioni e compagnie tra le quali Apple e IBM. 39 Il sistema operativo Mac OS X

• Certificate, Key e Trust Services che rispettivamente crea, accede e manipola i certificati; crea chiavi di crittazione e gestiscono le trust policies

• Authorization Services che è utilizzata come API primaria dalle applicazioni per autorizzare l’accesso ad azioni specifiche (ad esempio creare un file in una restricted directory) o dati. Come mostrato in figura 1.4.1, gli Authorization Services comunicano con il Security Server (che non fa parte del CDSA), ed è questo che poi utilizza le APIs del CDSA. Oltre all’autorizzazione, l’API dei servizi di autorizzazione gestisce anche l’autenticazione47.

Il Security Server (/usr/sbin/securityd) si comporta come un arbitro di svariati tipi d’accessi e operazioni relative alla sicurezza. L’API dell’Authorization Services include funzioni per aggiungere, cancellare, modificare e leggere oggetti del database delle politiche di sicurezza (policy database). Quando

un’applicazione richiede un diritto, ad esempio it.unina.Test.DoSomething48, fa

una chiamata d’autorizzazione instradata al securityd che consulta il policy database alla ricerca di una regola che collimi con il diritto richiesto. Nel caso in cui nessuna regola

soddisfi la richiesta, il securityd utilizza le regole wildcards alla ricerca della prima regola con la maggiore compatibilità alla richiesta. Se anche in questo caso la ricerca non dovesse produrre risultati, si utilizza allora la regola generale, che è utilizzata per i diritti che non hanno specifiche regole. Se l’autenticazione dell’utente avviene con successo, il

securityd crea una credenziale valida per 5 minuti49. L’applicazione Security Agent (SecurityAgent.app) è il gestore dell’interfaccia utente del securityd, poiché questo non interagisce direttamente con l’utente ma lancia il Security Agent come un processo separato, che mostra la richiesta dello username e password. In questo modo il Security Agent assicura l’interazione con la GUI (Graphic ), che normalmente garantisce la presenza fisica. Altre caratteristiche di sicurezza direttamente utilizzabili, o comunque controllabili, dall’utente finale sono:

47 L’autorizzazione consiste nel chiedere se una data entità è abilitata a compiere una data operazione. Prima che la richiesta possa essere fatta, il richiedente deve essere autenticato, ovvero deve dimostrare in qualche modo di essere chi afferma di essere. Solo dopo l’autenticazione del richiedente può essere verificata l’autorizzazione della richiesta. 48 Regole e diritti sono convenzionalmente nominati nel policy database utilizzando uno schema inverso del DNS. 49 Il tempo di validità della credenziale è specificato nella chiave timeout del policy database. 40 Il sistema operativo Mac OS X

• FileVault, un’immagine disco criptata con l’algoritmo AES che contiene i dati relativi ad una user’s home directory. Per esempio, se esiste un utente di nome

Massimiliano, la sua direcotry home /Users/Massimiliano avrà un file di tipo immagine disco chiamata Massimiliano.sparseimage che conterrà un volume HFS Plus. Tale file sarà visibile, da un account administrator, quando l’utente Massimiliano non risulterà loggato. Nel momento in cui Massimiliano dovesse effettuare il log-in, il volume con l’immagine disco è montato su

/Users/Massimiliano, mentre il precedente contenuto di tale directory (in particolare il file immagine stesso) è spostato in /Users/.Massimiliano. • Eliminazione sicura dei file attraverso il Finder’s Secure Empty Trash ed il

programma da linea di comando srm. L’applicazione Disk Utility (Disk Utility.app) permette ai dischi ed ai volumi di essere cancellati in sicurezza utilizzando uno dei vari schemi messi a disposizione: scrittura di zeri su tutti i dati del disco (zero-out data), scrittura dei dati dell’intero disco per sette volte (7-pass erase) o anche trentacinque (35-pass erase). Inoltre il recupero di files già cancellati può essere reso difficoltoso dalla cancellazione sicura dello spazio libero di un volume.

• Crittografia della memoria virtuale, che può essere abilitata o disabilitata mediante il pannello Security dell’applicazione System Preferences. Al tempo di boot, il

sistema operativo controlla la variabile di shell encryptswap per determinare se la memoria virtuale deve essere criptata o meno. Il valore della variabile è posto a

yes o no in /etc/hostconfig in relazione alla configurazione del System Preferences.

1.4.3 Amministrazione del sistema Il sistema Mac OS X può essere amministrato effettivamente sia tramite interfaccia grafica che mediante linea di comando. Andremo ora ad esaminare alcuni aspetti specifici dell’amministrazione di sistema che utilizzano la linea di comando.

41 Il sistema operativo Mac OS X

Il comando security fornisce l’accesso alle funzionalità del Security framework (Security.framework). in particolare, questo comando può essere utilizzato per accedere e manipolare certificati, chiavi, keychains e password. La versione server del Mac OS X utilizza il software Open Directory, basato su LDAP (Lightweight Directory Access Protocol), per fornire i servizi di directory e autenticazione dei clients Mac OS X, Unix e Windows. Un servizio di directory è semplicemente un deposito centrale in cui memorizzare informazioni riguardo gli utenti, i computers, le stampanti e altre risorse di rete all’interno di un’organizzazione. Le applicazioni ed i software di sistema possono accedere a queste informazioni per svariati utilizzi: autenticare i logins, localizzare le user’s home directories, far rispettare le quote delle risorse, controllare gli accessi al file system e così via. Tradizionalmente i sistemi UNIX immagazzinano tali informazioni all’interno file di testo

semplice come quelli della directory /etc ed, in effetti, tale direcoty può essere considerata come un primitivo servizio di directory. Altri esempi di servizi di directory sono il NIS (Network Information Service) della Sun e l’Active Directory della Microsoft50. Il servizio di directory ereditato dal Mac OS X Server si chiama NetInfo e, nonostante non sia più utilizzato per le directory condivise, viene ancora utilizzato nel dominio locale.

Il comando dscl può essere utilizzato per operare sulle sorgenti dei dati, che possono essere nomi di nodo directory oppure hosts su cui stanno girando servizi di directory. In

maniera simile il comando niutil può essere utilizzato per operare sui domini NetInfo. Notiamo infine che Open Directory include un plug-in NetInfo chge permette di interoperare con NetInfo.

Il comando scutil può essere utilizzato per accedere e manipolare vari aspetti della configurazione del sistema locale. Il daemon System Configuration

(/usr/sbin/configd) memorizza i dati di configurazione in un deposito dinamico accessibile mediante scutil. Tale daemon utilizza svariati “agenti di configurazione”,

50 Il NIS è conosciuto anche come Yellow (yp). Le recenti versioni del Solaris hanno abbandonato il NIS ed il NIS+ (che ne è il successore) in favore di un servizio di directory basato su LDAP. Anche l’Active Directory si basa su LDAP. 42 Il sistema operativo Mac OS X

ognuno dei quali si occupa di una particolare area di gestione della configurazione, in modo da formare una visione di insieme della configurazione del sistema. Tali agenti sono

contenuti in pacchetti all’interno di /System/Library/SystemCOnfiguration/. 1.4.4 Sistema di verifica Il sistema di verifica del Mac OS X consiste nel supporto al kernel ed una suite di programmi dello user-space51. Il kernel memorizza gli eventi di verifica all’interno di un file registro (un audit trail file) basandosi su vari criteri. Un daemon dello spazio utente,

chiamato auditd, “ascolta” i trigger events dal kernel ed i control events dai programmi utente: i primi avvisano il daemon se il file di log si è riempito o se lo spazio libero del file

system è sceso sotto una determinata soglia, nel qual caso auditd cercherà di rimediare alla situazione; i secondi sono utilizzati per far rileggere il file di configurazione, passare ad un nuovo file di log o terminare l’auditing system.

File/Directory Description /usr/sbin/auditd Audit log management daemon – receives “trigger” messages form the kernel and “control” messages from the audit management utility /usr/sbin/audit Audit management utility – used to control the audit daemon by sending it control messages /usr/sbin/auditreduce Utility that selects records from the audit trail files based on the specified criteria and prints matching records in raw form – either to a file or to the standard output /usr/sbin/praudit Utility that prints selected records in human-readable format /var/audit Directory for storing audit trail files /etc/security/rc.audit Script executed durino system startup by the /etc/rc master script to start the audit daemon /etc/security/audit_control Default audit policy file – contains global audit parameters /etc/security/audit_class File containing descriptions of audit event classes /etc/security/audit_event File containing descriptions of audit events /etc/security/audit_user File specifying event classes tha are to be audited on a per- user basis /etc/security/audit_warn Administrator-configurable script run when the audit daemon generates a warning

51 Nel Mac OS X 10.4 i programmi di verifica ed i file di configurazione dello user-space sono forniti dal pacchetto denominato Common Criteria Tools, che non è istallato di default. 43 Il sistema operativo Mac OS X

La variabile audit contenuta nel file /etc/hostconfig può assumere diversi valori: yes per abilitare il sistema di verifica, no per disabilitarlo, failstop o failhalt per abilitarlo con condizioni particolari. Nel primo caso auditd è lanciato con l’argomento –s, che specifica che ogni singolo processo deve essere fermato se l’audit log si riempie (continuare l’esecuzione comporterebbe la perdita d’informazioni di verifica).

Nel secondo caso il daemon è lanciato con l’argomento –h, che specifica che il sistema debba andare in halt in caso di fallimento dell’audit. Il kernel registra su di un solo file audit trail alla volta. Questi files utilizzano un sistema dei nomi specifico: una stringa formata dal tempo di creazione del file, seguita da un punto, seguita dal tempo di terminazione. Il nome del file attivo, che è l’unico a non essere terminato, contiene la stringa ‘not_terminated’ invece del tempo. Entrambe le sottostringhe di tempo utilizzano il formato %Y%m%d%H%M%S della funzione strftime().

44 Il sistema operativo Mac OS X

Capitolo 2 Il Kernel XNU

Come abbiamo già visto, l’ambiente kernel del Mac OS X è composto di derivazioni del Mach e del BSD, il driver framework dell’I/O Kit, librerie in-kernel ed altre estensioni caricabili. Anche se il pacchetto XNU del Darwin contiene solo la metà del codice che potenzialmente gira nell’ambiente del kernel, noi consideriamo XNU “il” kernel. In questo capitolo esamineremo varie astrazioni e meccanismi proprio dello XNU.

2.1 XNU Source Per meglio capire com’è organizzato il codice sorgente ne faremo un panoramica. Ovviamente, essendo composto di quasi 3000 files, andremo ad esaminare solo le directories principali. Al livello più alto, XNU contiene le directories indicate nella tabella 2-1. Oltre a queste esistono altri files e directories che però non rientrano nella nostra trattazione.

Table 2-1 Primary Components of the XNU Kernel Source Directory Component bsd/ The BSD Kernel config/ Lists of per-subsystem exported functions, property list files for pseudo-extensions iokit/ The I/O Kit kernel runtime libkern/ The kernel library libsa/ The stand-alone library osmfk/ The Mach kernel pexpert/ The Platform Export 45 Il sistema operativo Mac OS X

Nella tabella 2-2 riportiamo una parte del contenuto della directory bsd/.

Table 2-2 Primary Contents of the bsd/ Directory Directory Description bsm/ Basic Security Module (BSM) headers used by the kernel’s auditing mechanism. BSM is both a security audit format and an API used to track security-related events in the operating system. crypto/ Various cipher and hash implementations: AES (Rijndael), Blowfish, CAST-128, DES, MD5, RC4, SHA-1, and SHA-2. dev/memdev.c A RAM disk driver (for /dev/mdX devices) dev/ppc/ BSD drivers for entities such as /dev/console, /dev/mem, /dev/kmem, /dev/zero, and a BSD driver wrapper for the NVRAM. The latter calls Platform Export functions to perform the actual work. The BSD device switch tables for block and character devices are also initialized here. Also present are some machine-dependent functions used in the BSD subsystem, such as unix_syscall(), unix_syscall_return(), ppc_gettimeofday(), and signal- handling functions. dev/random/ An implementation of the Yarrow* pseudorandom number generator (PRNG) and the /dev/random device. dev/unix_startup.c Functions that initialize various BSD-related data structures dureing system startup. dev/vn/ The vnode disk driver, which provides block and character interfaces to a vnode, allowing file sto be treated as disks. The /usr/libexec/vndevice utility is used to control the driver. hfs/ The HFS and HFS Plus file systems. isofs/ The ISO 9660 file system for read-only optical discs. kern/ The core of XNU’s BSD component. It contains implementations of asynchronous I/O calls, the Kauth mechanism, the audit mechanism, process-related system calls, sysctl calls, POSIX IPC, System V IPC, the unified buffer cache, sockets, mbufs, and various other system calls. libkern/ Utility routines such as bcd(), bcmp(), inet_ntoa(), rindex(), and strtol(). miscfs/ Miscellaneous file systems: the dead file system for vnodes whose underlying file system has been dissociated (deadfs), the device file system (devfs), the file descriptor file system (fdesc), the fifo file system (fifofs), the null mount file system (nullfs), the file system for device- special file (specfs), the in-memory synthfs used for synthesizing mount points, the union mount file system (union), and the volume ID file system (volfs).

46 Il sistema operativo Mac OS X

net/ Networking: the Berkeley racket filter (BPF), bridging, data link interface layer (DLIL), Ethernet, ARP, PPP, routing, IEEE 802.1q (VLAN), IEEE 802.3ad (Link Aggregation), etc. netat/ Apple Talk Networking. netinet/ IPv4 Networking: BOOTP, DHCP, ICMP, TCP, UDP, IP, the “dummynet” bandwidth limiter, and divert sockets. netinet6/ IPv6 Networking. netkey/ pf_key Key Management API (RFC 2367). nfs/ NFS client and the kernel portion of the NFS server. ufs/ An implementation of UFS based on the fast file system (ffs). uxkern/ A Mach exception handler that translates Mach exceptions into UNIX signals. vfs/ The BSD virtual file system layer. vm/ Vnode pager (swap to/from vnodes, demand paging from files), shared memory server calls.

*. Yarrow gets its name from a flowering plant woth distintive flat flowers heads and lacy leaves. In China, its stalks have been used as a randomizer in divination since the second millenium B.C.

Nella tabella 2-3 riportiamo una parte del contenuto della directory iokit/.

Table 2-3 Primary Contents of the iokit/ Directory Directory Description Drivers/platform/ Implementations of I/O Kit classes listed in the KernelConfigTables array – e.g., AppleCPU, AppleNMI, and AppleNVRAM. The I/O Catalog is inizialized with the contents of this array. Families/IONVRAM/ Subclass of the NVRAM controller class – simply calls the Platform Export to register the NVRAM controller, which publishes the “IONVRAM” resource in the I/O Kit. Families/IOSystemManagement/ Watchdog timer. IOKit/ I/O Kit header files. Kernel/ Implementations of core I/O Kit classes and utility functions. KernelConfigTables.cpp Declarations of the list of “fake” kernel extensions and the KernelConfigTables array. bsddev/ Support functions for BSD – e.g., the di_root_image() hook called by BSD to mount a disk image as the root device, and several other functions used by BSD while searching for a root device.

47 Il sistema operativo Mac OS X

Nella tabella 2-4 riportiamo una parte della directory libkern/.

Table 2-4 Primary Contents of the libkern/ Directory Directory Description c++/ Implementations of various libkern classes gen/ High-level-language wrappers around assembly functions for atomic operations, miscellaneous debugging functions. kmod/ Start and stop routines for the kernel’s C++ and C language runtime environments. libkern/ libkern header files. mach-o/ A header describing the format of Mach-O files (loader.h), and another header containing definitions for accessing Mach-O headers (mach_header.h). ppc/ Implementations of PowerPC-specific bcmp(), memcmp(), strlen(), and atomic increment/decrement functions. stdio/ An implementation of scanf(). uuid/ Routines for parsine and generatine universally unique identifiers (UUIDs) based on the first Ethernet device’s hardware address and the current time.

La libreria libkern è parte del framework del Kernel ed il suo header si trova in

/System/Library/Frameworks/Kernel.framework/Headers/libkern/. Nella tabella 2-5 riportiamo le più importanti classi contenute nella libreria.

Table 2-5 libkern Classes and Routines Base and Abstract Classes OSObject The abstract base class for the Mac OS X kernel. It derives from the true root class OSMetaClassBase. It implements basic functionality such as allocation primitives, reference counting, and type-safe object casting. OSMetaClass A peer class to OSObject. It derives from the true root class OSMetaClassBase. An instance of this class represents one class that is know by the I/O Kit’s RTTI system. OSCollection The abstract superclass for all collections. OSIterator The abstract superclass for iterator classes. Collection Classes OSArray A class for maintaining lists of object references. OSDictionary A class for maintaining dictionaries of object references. OSOrderedSet A class for maintaining and sorting sets of OSMetaClassBase- derived objects. OSSet A class for storing OSMetaClassBase-derived object.

48 Il sistema operativo Mac OS X

OSCollectionIterator A class that provides a mechanism to iterate over OSCollection- derived collections. Container Classes OSBoolean A class for Booelan values. OSData A class for managing byte arrays. OSNumber A class for numeric values. OSString A class for managing strings. OSSymbol A class for representing unique string values. OSSerialize A class used by the container classe sto serialize their instance data. OSUnserializeXML A class that recreates a container object from its serialized instance data in an XML buffer.

Nella tabella 2-6 riportiamo una parte del contenuto della directory libsa/.

Table 2-6 Primary Contents of the libsa/ Directory File Description bootstrap.cpp Constructor and destructor functions for libsa. bsearch.c, dgraph.c, Functions for binary searching, directed graphs, and heap sort – used sort.c for supportino kernel extension loading. c++rem3.c Symbol remangler for code compiled with version 2.95 of the GNU C++ compiler – invoked during symbol table parsing when Mach-O object file (typically a kernel extension) is mapped. catalogue.cpp I/O Catalog routines, such as those for accessing and manipulating kernel extension dictionaries, accessing mkext caches, and recording boot-time kernel extensions into dictionaries. kext.cpp, The core of libsa’s functionality: routines for resolving kernel extension kld_patch.c, kmod.cpp, load.c dependancies, retrieving kernel extension version, loading kernel extensions, patching vtables, etc. malloc.c Simple implementations of malloc() and realloc(). mkext.c Routines for LZSS compression/decompression, and for computing 32- bit Adler checksum. strrchr.c, strstr.c String functions. vers_rsrc.c Routines for parsine and generatine version string.

Sappiamo che la libreria stand-alone libsa è utilizzata solamente per caricare le estensioni del kernel durante l’avvio del sistema. In un tipico scenario di booting, il daemon kextd manda un messaggio kIOCatalogRemoveKernelLinker all’I/O Catalog nel kernel. Questo messaggio comunica all’I/O Catalog che il kextd è pronto a gestire le estensioni

49 Il sistema operativo Mac OS X del kernel dallo spazio utente. Inoltre richiede all’I/O Catalog l’invocazione del distruttore per il segmento __KLD del kernel (che è il segmento contente il codice di libsa) e la sua deallocazione (insieme al segmento __LINKEDIT). Nella tabella 2-7 riportiamo una parte del contenuto della directory osfmk/.

Table 2-7 Primary Contents of the osfmk/ Directory Directory or File Description UserNOtification/ Kernel portion of the Kernel User Notification Center (KUNC) mechanism, which can be used by software running in the kernel to execute user-space programs and to display notices or alert messages. The /usr/libexec/kuncd daemon is the user-space agent that processes such requests from the kernel. console/i386/ VGA test console, x86 serial console. console/iso_font.c Data for the ISO Latin-1 font console/panic_dialog.c Panic user-interface routines, includine routines for drawing, amanging, and testing the panic dialog. console/panic_image.c Pixel data for the default panic image – an 8-bit, 472x255 image. console/panic_ui/ Panic image files and utilities to convert the minto a kernel- usable format. console/ppc/ Fast video scrollino, PowerPC serial console. console/rendered_numbers.c Pixel data for hexadecimal digits 0 through F and the colon character. console/video_console.c Hardware-independent portion of the video console. ddb/ Built-in kernel debugger. default_pager/ Default pager, including the back-end for managing swap files. device/ Mach support for the I/O Kit, including device representation through Mach ports. The I/O Kit master port is also set here. ipc/ The core of Mach’s IPC facility implementation. kdp/ A kernel debugging protocol called KDP that uses a TFTP-like UDP-based tranfer mechanism. kern/ The core of Mach kernel: implementations of abstractions such as processors, processor sets, tasks, threads, memory allocation, and timers. IPC interfaces are also implemented here. mach/ Mach headers and MIG definition files. mach-o/ Functions for accessing Mach-O headers. mach_debug/ Mach debugging headers and MIG definition files. machine/ Headers that are wrappers for machine-dependent headers.

50 Il sistema operativo Mac OS X

ppc/ PowerPC-specific code: machine startup, exception vectors, trap handling, low-level context-switching code, low-level memory management, diagnostic calls, Classic support functions, machine-dependent debugger components, virtual machine monitor, kernel components for Apple’s CHUD Tools, etc. profiling/ Kernel profiling support, which must be explicitly compiled in. The kgmon utility is used to control the profiling mechanism: it can stop or start the collection of kernel profiling data, dump the contents of the profile buffers, reset all the profile buffers, and retrieve specific named values from the kernel. sys/ Miscellaneous headers. vm/ Mach virtual memory subsystem, including the in-kernel shared memory server.

Nella tabella 2-8 riportiamo una parte del contenuto della directory pexpert/.

Table 2-8 Primary Contents of the pexpert/ Directory Directory or File Description gen/bootargs.c Boot-argument parsing routines. gen/device_tree.c Routines for accessing device tree entries and their properties. gen/pe_gen.c Miscellaneous functions, including an 8-bit lookup table used during bootstrapping. i386/ Machine modification, debugging output support, keyboard driver, generic interrupt handler, polled-mode serial port driver, and other platform- dependent routines such as for reading the timestamp counter, setting and clearing interruptus, generatine a fake device tree, etc. pexpert/ Miscellaneous platform headers, including those containing image data for the rotatine gearwheel image shown at startup to indicate boot progress. ppc/ Machine identification, debugging output support, clock speed determination by running timed loops, timebase value retrieval, and other platform functions.

2.2 Mach Il Mach è stato progettato come un kernel per sistemi operativi orientati alla comunicazione con pieno supporto al multiprocesso. Nell’idea degli sviluppatori dovrebbe essere un microkernel in cui i tradizionali servizi del sistema operativo (come il file system, l’I/O, i gestori della memoria, i networking stacks etc.) sono residenti nello spazio

51 Il sistema operativo Mac OS X utente, con una chiara e logica separazione modulare tra loro ed il kernel. Nella pratica, le releases precendeti la 3 avevano un’implementazione monolitica. Il progetto iniziato alla CMU (Carnegie Mellon University) e continuato dalla OSF (Open Software Foundation) diede vita alla release 3 del Mach: la prima vera versione a microkernel del Mach, in cui BSD gira come un task utente. La porzione Mach dello XNU fu originalmente basata sul sistema Mach Mk 7.3 dell’Open Group, che a sua volta era basato sul Mach 3. Questa porzione contiene i miglioramenti sviluppati nell’MkLinux ed usufruisce del lavoro svolto sul Mach dall’University of Utah. Va precisato che lo XNU non è un microkernel. Tutte le componenti del kernel del Mac OS X risiedono in un singolo spazio di memoria kernel. Anche se modulare e flessibile resta un kernel monolitico, tuttavia va notato che il kernel lavora a stretto contatto con pochi daemons dello spazio utente come dynamic_pager, kextd e kuncd. Il Mach fornisce un’interfaccia di macchina virtuale agli alti strati creando un’astrazione dell’hardware di sistema, cosa molto comune in molti sistemi operativi. Pensato per essere semplice ed estensibile, il cuore del kernel Mach fornisce un meccanismo IPC che è il fondamento per una moltitudine dei servizi offerti dal kernel. In particolare, l’unione dell’IPC e del sottosistema della memoria virtuale porta a varie ottimizzazioni e semplificazioni. Il Mach ha cinque astrazioni base dal punto di vista del programmatore:

• Task • Thread • Port • Message • Memory Object Oltre a fornire le astrazioni base, il Mach rappresenta varie risorse hardware e software come dei ‘port objects’, permettendo la manipolazione di tali risorse attraverso il meccanismo IPC.

52 Il sistema operativo Mac OS X

2.2.1 Tasks e Threads Il Mach divide le astrazioni tradizionali dei processi UNIX in due parti: un task ed un thread. Nel Mac OS X, i termini “thread” e “process” hanno delle connotazioni specifiche a seconda del contesto ovvero dell’ambiente user-space. All’interno del kernel, un processo BSD (che è analogo ad un tradizionale processo UNIX), è una struttura dati con una corrispondenza 1-a-1 verso un task del Mach. Un task del Mach ha le seguenti caratteristiche chiave:

• È un ambiente d’esecuzione ed un’entità statica. Un task non esegue, cioè non compie computazione da solo, ma fornisce una struttura nella quale altre entità (i threads) eseguono.

• È l’unità base dell’allocazione delle risorse e può essere pensata come resource container. Un task contiene un collezione di risorse come accesso ai processori, spazio di indirizzi virtuali paginato (memoria virtuale), spazio IPC, gestori delle eccezioni, credenziali, descrittori di file, stato della protezione, stato della gestione dei segnali e statistiche. Notiamo che le risorse del task includono oggetti UNIX, che in Mac OS X sono contenuti in un task mediante la sua corrispondenza 1-a-1 con la struttura del processo BSD.

• Rappresenta il contorno di protezione di un programma. Un task non può accedere alle risorse di un altro task senza aver ottenuto una esplicita autorizzazione mediante l’uso di una specifica interfaccia. Un thread è l’attuale entità di esecuzione nel Mach ed è un punto di controllo di flusso all’interno di un task. Ha le seguenti caratteristiche:

• Esegue all’interno di un task, rappresentando un program counter indipendente (uno stream d’istruzioni) nel task. Un thread è anche l’entità schedabile fondamentale, con attributi e priorità di scheduling associate. Ogni thread è schedato pre-empitivamente ed indipendentemente dagli altri threads, sia quelli dello stesso task che quelli di qualsiasi altro task.

• Il codice eseguito da un thread risiede nello spazio degli indirizzi del proprio task. • Ogni task può contenere nessuno o più threads, ma ogni thread porta ad uno ed un

53 Il sistema operativo Mac OS X

solo task. Un task senza threads, anche se legittimo, non può andare in esecuzione.

• Tutti i threads interni ad un task condividono le risorse del task. In particolare, poiché tutti i threads condividono la stessa memoria, un thread può sovrascrivere la memoria di un altro, senza la necessità di ottenere nessun privilegio aggiuntivo. Poiché possono esserci molti threads in esecuzione concorrente all’interno di un task, i threads appartenenti ad un task devono cooperare.

• Un thread può avere il proprio gestore delle eccezioni (exception handler). • Ogni thread ha un proprio stato di computazione, che include i registri di processore, un program counter ed uno stack. Notiamo che nonostante lo stack di un thread sia indicato come “privato”, esso risiede nello stesso spazio degli indirizzi di tutti gli altri threads appartenenti allo stesso task. Di conseguenza, per quanto detto, ogni thread può accedere agli stacks degli altri threads dello stesso task.

• Un thread utilizza uno stack del kernel, delle dimensioni di 16KB, per amministrare le chiamate di sistema. Ricapitolando: un task è passivo, con risorse proprie e costituisce l’unità base della protezione; un thread è attivo, esegue istruzioni ed è l’unità base del controllo di flusso. Un processo tradizionale UNIX a singolo thread è analogo ad un task del Mach con un singolo thread, mentre un processo UNIX multi-thread è analogo ad un task Mach con molti threads. Un task è considerevolmente più costoso da creare o distruggere di un thread. Mentre ogni thread ha il suo task contenitore, un task Mach non è in relazione con il suo task creatore (al contrario dei processi UNIX). Ad ogni modo il kernel mantiene le relazioni padre-figlio a livello di processo nelle strutture di processo BSD. Tuttavia possiamo considerare un task che ne crea un altro come “task padre” ed il neonato come “task figlio”. Durante la creazione il figlio eredita lacune aspetti del padre, come le porte registrate, le porte di eccezione e bootstrap, tokens di verifica e sicurezza, regioni di mapping condivise e set di processori. Notiamo che se il processor set del padre è marcato come inattivo, il figlio è assegnato al processor set di default.

54 Il sistema operativo Mac OS X

Una volta creato un task, chiunque con un valido identificatore di task (ed gli appropriati diritti ad una porta IPC Mach) può compiere operazioni sul task. Un task può spedire il proprio identificatore ad un altro task in un messaggio IPC, se lo desidera. Il kernel utilizza le astrazioni di task e thread per dividere le proprie funzionalità in vari flussi d’esecuzione. Il kernel utilizza un singolo task (il kernel task) con threads multipli che compiono le operazioni del kernel, come scheduling, thread reaping, callout management, paging e UNIX exception handling. Quindi XNU è un kernel monolitico, contenente componenti marcatamente differenti (come Mach, BSD e l’I/O Kit) che girano tutte come gruppi di threads all’interno di un singolo task nello stesso spazio degli indirizzi.

2.2.2 Ports Una porta Mach è un’astrazione con varie sfaccettature, tradizionalmente implementata come una coda a lunghezza finita di messaggi. Oltre alle porte Mach, il Mac OS X fornisce molti altri meccanismi IPC, sia nello spazio kernel sia nello spazio utente. Esempi di tali meccanismi includono le IPC del POSIX e del System V, il descriptor passing, le notifiche multiple e gli Apple Events. L’astrazione port, insieme alle sue operazioni (tra le quali le principali sono la send e la receive) costituiscono la base per la comunicazione nel Mach. Ogni porta ha dei diritti (rights) associati, gestiti dal kernel, che un task che voglia

manipolare tale porta deve avere. Ad esempio, i rights determinano quali tasks possono mandare dei messaggi ad una data porta p quale task può ricevere i messaggi destinati ad essa. Notiamo che mentre è possibile avere più tasks con i send rights verso una particolare porta, solo un task può avere i receive rights di tale porta. Nella accezione orientata agli oggetti, una porta è un riferimento ad un oggetto. Svariate astrazioni del Mach, incluse le strutture dati ed i servizi, sono rappresentate da porte. In questo senso una porta funziona come un provider di accesso protetto ad una risorsa di sistema. Si accede agli oggetti come tasks, threads o memory object52 attraverso le

52 Con l’eccezione della memoria virtuale, tutte le risorse di sistema nel Mach sono accedute tramite porte. 55 Il sistema operativo Mac OS X

rispettive porte. Ad esempio ogni task ha una task port che lo rappresenta nelle chiamate al kernel. Similmente un punto di controllo di un thread è accessibile ai programmi utente attraverso una thread port. Ognuno di questi accessi richiede una capacità (capability) della porta, vale a dire il diritto di mandare o ricevere messaggi verso tale porta, ovvero l’oggetto che tale porta rappresenta. In particolare, le operazioni su un oggetto sono eseguite mediante messaggi mandati su una delle sue porte53. L’oggetto che detiene i diritti di ricezione della porta può quindi ricevere il messaggio, processarlo e possibilmente effettuare un’operazione richiesta nel messaggio. Facciamo due esempi ti tale meccanismo:

• Un window può rappresentare ogni finestra che gestisce mediante una porta. Il suo client task esegue le operazioni sulla finestra spedendo messaggi sulle appropriate window ports. Il task del window manager riceve e processa queste operazioni.

• Ogni task, ed ogni thread al suo interno, hanno un exception port. Un gestore degli errori può registrare una delle sue porte come una exception port di thread. Quando avviene un’eccezione, un messaggio è spedito a tale porta ed il gestore può riceverlo e processarlo. In modo simile, un debugger può registrare una delle sue porte come la exception port del task. Da quel momento in poi, fino a quando un thread non registra esplicitamente la propria exception port, le eccezioni in tutti threads del task saranno comunicate al debugger. Poiché una porta è una risorsa relativa al task, tutti i threads in un task accedono automaticamente alle porte del task. Un task può permettere l’accesso ad una o più delle sue porte ad altri tasks, mediante il passaggio dei diritti attraverso messaggi IPC. Inoltre un thread può accedere ad una porta solo se tale porta è nota al suo task contenitore, poiché non esiste uno spazio dei nomi di porta globale di sistema. Molte porte possono essere raggruppate insieme in un port set. Tutte le porte in un set condividono la stessa coda. Pur continuando ad esserci un singolo ricevente, ogni

53 Gli oggetti possono avere molte porte rappresentanti differenti tipologie di funzionalità o livelli di accesso. Per esempio, un risorsa privilegiata può avere un porta di controllo accessibile solo dal superuser ed un porta di informazioni disponibile a tutti gli utenti. 56 Il sistema operativo Mac OS X

messaggio contiene un identificatore per la specifica porta del set sulla quale deve essere ricevuto il messaggio54. Le porte del Mach sono progettate per essere trasparenti rispetto alla rete, permettendo ai tasks sulle macchine collegate di comunicare l’un l’altro senza preoccuparsi di dove si trovino. In questo scenario viene tipicamente utilizzato un server di rete per i messaggi

(netmsgserver) come mediatore di fiducia. I tasks possono pubblicizzare i propri servizi registrandoli al netmsgserver con un nome unico. Gli altri tasks, compresi quelli su altre macchine, possono cercare i nomi dei servizi sul server utilizzando una sua porta disponibile a tutti. In questo modo il server può propagare i diritti delle porte attraverso tutta la rete. Il Mac OS X non supporta questa caratteristica di IPC distribuito del Mach, e di conseguenza non ha nessun server dei messaggi di rete, ne interno ne esterno. L’IPC distribuito è comunque possibile anche sul Mac OS X, attraverso meccanismi di alto livello come l’API Distributed Objects del Cocoa. Notiamo che una porta può essere utilizzata per mandare messaggi in una sola direzione, quindi una porta non rappresenta l’end point di un canale di comunicazione bidirezionale, al contrario del socket BSD. Se un messaggio di richiesta è spedito su una certa porta ed il mittente ha bisogno di una risposta, deve essere disponibile un’altra porta su cui inviarla.

2.2.3 Messages I messaggi IPC del Mach sono data objects che i threads si scambiano per comunicare. La comunicazione tra tasks, inclusa quella tra il kernel ed i tasks utente, avviene tramite messaggi. Un messaggio può contenere dati inline attuali oppure puntatori a dati out-of- line (OOL). Il trasferimento di dati OOL è una ottimizzazione per i grandi trasferimenti: il kernel alloca una regione di memoria per il messaggio nello spazio degli indirizzi del ricevente, senza creare una copia fisica del messaggio. Le pagine di memoria condivisa sono marcate copy-on-write (COW). Un messaggio può contenere dati arbitrari di programma, copie di blocchi di memoria, eccezioni, notifiche, capacità di porta e così via. In particolare, l’unico modo per trasferire

54 Tale funzionalità è simile alla chiamata di sistema UNIX select(). 57 Il sistema operativo Mac OS X

le capacità di porta da un task ad un altro è attraverso messaggi. I messaggi Mach sono trasferiti in modo asincrono. Anche se un solo task può usufruire dei diritti di ricezione di una porta, molti threads in un task possono cercare di ricevere messaggi su una porta. In questo caso, solo uno dei threads avrà successo nella ricezione di un dato messaggio.

2.2.4 Virtual Memory e Memory Objects Il sistema della memoria virtuale (VM, Virtual Memory) del Mach può essere nettamente suddiviso nella parte dipendente dalla macchina, come la mappa fisica (pmap), e quella indipendente, come le mappe degli indirizzi, gli oggetti della memoria, le mappe condivise e la memoria residente. Le caratteristiche VM Mach includono:

• Il Mach fornisce spazi degli indirizzi protetti per task, con layout a memoria sparsa. Una descrizione di spazio degli indirizzi di un task è una lista lineare di regioni di

memoria (vm_map_t), dove ogni regione punta ad un oggetto della memoria (vm_object_t). • La mappatura degli indirizzi dipendente dalla macchina è contenuta in un oggetto

pmap (pmap_t). • Un task può allocare o deallocare le regioni di memoria virtuale sia nel proprio spazio degli indirizzi, sia negli spazi degli indirizzi di altri tasks.

• I tasks possono specificare le proprietà di protezione ed ereditarietà della memoria su una base “per pagina”. La pagine di memoria possono essere tolte dalla condivisione tra tasks o condivise utilizzando i modi copy-on-write o read-write. Ogni gruppo di pagine, ovvero regione di memoria (memory region), ha due valori di protezione: corrente (current) e massima (maximum). La protezione corrente corrisponde all’attuale protezione hardware utilizzata per le pagine, mentre la protezione massima è il più alto valore che la protezione corrente possa raggiungere. La protezione massima è un limite superiore assoluto che non può mai essere innalzato (reso più permissivo) ma soltanto abbassato (reso più restrittivo). Perciò la protezione massima rappresenta il massimo accesso che può essere avuto

58 Il sistema operativo Mac OS X

da una regione di memoria. Un oggetto della memoria (memory object) è un contenitore di dati, è mappato nello spazio degli indirizzi di un task e serve da canale per fornire memoria ai tasks. Il Mach tradizionalmente permette che un memory object sia gestito da un gestore esterno della memoria, in cui i page faults e le page-out data request possano essere gestiti nello spazio utente. Un paginatore esterno può anche essere utilizzato per implementare la memoria virtuale di rete. Questa external memory management (EMM) del Mach non è utilizzata nel Mac OS X. Lo XNU fornisce servizi di paginazione base nel kernel attraverso tre pagers:

• Il default pager gestisce la memoria anonima, vale a dire quella che non ha un paginatore esplicitamente designato.

• Il vnode pager è utilizzato per i files mappati in memoria55. • Il device pager è utilizzato per la memoria non general-purpose. È implementato nello strato Mach, ma è utilizzato dall’I/O Kit.

2.2.5 Exception Handling Un’eccezione Mach è un’interruzione sincrona dell’esecuzione di un programma. Le cause d’eccezione possono essere condizioni errate come l’esecuzione di un’istruzione illegale, la divisione per zero o l’accesso ad un indirizzo di memoria invalido. Le eccezioni possono anche essere deliberatamente causate, come ad esempio quando si raggiung un

breakpoint durante il debugging. L’implementazione Mach dello XNU associa un array di porte d’eccezione ad ogni task, ed un altro per ogni thread nel task. Ognuno di questi array ha tante celle quanti sono i tipi d’eccezioni implementate (la cella zero non è valida). Tutte le porte d’eccezione d’ogni

thread sono inizializzate sulla porta nulla (IP_NULL) al momento della creazione, mentre le porte d’eccezione del task sono ereditate da quelle del task padre. Il kernel permette comunque al programmatore di leggere o scrivere individualmente le porte d’eccezione sia

55 Da quando il VFS del Mac OS X si trova nella porzione BSD del kernel, il paginatore vnode è implementato nello strato BSD. 59 Il sistema operativo Mac OS X per i tasks sia per i threads. Di conseguenza, un programma può avere diversi exception handlers, anche se un gestore solo può manipolare diversi tipi di eccezioni. La preparazione tipica per la gestione di un’eccezione consiste nell’allocazione di una o più porte con cui il kernel possa scambiare messaggi di notifica dell’eccezione. La porta può quindi essere registrata come un’exception port per uno o più tipi di eccezioni per un thread o un task. Il codice di un exception handler gira tipicamente in un thread dedicato, restando nell’attesa di messaggi di notifica dal kernel. L’exception handling nel Mach può essere vista come una meta-operazione consistente in diverse sotto-operazioni. Il thread che causa un’eccezione è chiamato victim, mentre quello che la gestisce è detto handler. Quando una vittima causa un’eccezione, il kernel sospende il victim thread e spedisce un messaggio all’appropriata porta d’eccezione, che può essere una thread exception port (più specifica) oppure una task exception port (se il thread non ha messo un’exception port). All’atto della ricezione del messaggio, l’handler thread processa l’eccezione – operazione che può consistere nel recuperare lo stato della vittima, prepararne la terminazione, registrare un errore e così via. L’handler a questo punto risponde al messaggio indicando se l’eccezione è stata processata con successo (exception cleared). A questo punto il kernel effettua il resume del thread vittima oppure lo termina. Una porta d’eccezione di thread è tipicamente rilevante per la gestione degli errori. Ogni thread può avere i propri gestori delle eccezioni che processano le eccezioni corrispondenti agli errori che riguardano solo threads individuali. Una porta d’eccezione di task è tipicamente rilevante per il debugging. Un debugger si può collegare ad un task mediante la registrazione di una delle sue porte come la debugged task’s exception port. Dal momento che un task eredita le sue porte d’eccezione dal task creatore, il debugger che controlla il padre può controllare anche i processi figlio. Inoltre, le notifiche d’eccezione relative a tutti i threads che non hanno registrato un porta d’eccezione, sono indirizzate alla task exception port.

60 Il sistema operativo Mac OS X

2.3 All’interno del Kernel In un tipico sistema operativo, i processi utente sono isolati logicamente dalla memoria del kernel mediante l’utilizzo di differenti modi di esecuzione. Il kernel del Mac OS X è eseguito nel modo privilegiato PowerPC OEA (Operating Environment Architecture) superiore a quelli, PowerPC UISA (User Instruction Set Architecture) e VEA (Virtual Environment Architecture), di qualsiasi programma utente. Ogni processo utente, ossia ogni task Mach, ha il proprio spazio di indirizzo virtuale. In modo simile, il kernel ha il proprio distinto spazio d’indirizzo virtuale che non occupa una sottogamma del massimo spazio d’indirizzi possibile per un processo utente. Nello specifico, il kernel del Mac OS X ha uno spazio d’indirizzo virtuale privato a 32-bit (4GB), così come ogni processo utente a 32-bit. Similmente, un processo utente a 64-bit ha anche uno spazio d’indirizzo virtuale

privato che non è suddiviso in parte kernel e parte utente56. Ci riferiremo allo spazio d’indirizzo virtuale del kernel semplicemente come allo spazio kernel. Inolte, anche se ogni processo utente ha il proprio spazio d’indirizzo, spesso utilizzeremo la frase “lo spazio utente” quando lo specifico processo non è rilevante. In questa accezione, possiamo pensare che tutti i processi utente risiedano nello spazio utente. Elenchiamo ora alcune importanti caratteristiche degli spazi kernel e utente:

• Lo spazio kernel è inaccessibile ai tasks utente. Il kernel garantisce tale protezione utilizzando un hardware per la gestione della memoria, in modo da creare una separazione tra codice kernel-level e user-level.

• Lo spazio utente è pienamente accessibile al kernel. • Il kernel normalmente impedisce ad un task utente di modificare, o anche solo accedere, la memoria di un altro task. Tale protezione è comunque soggetta alla proprietà del task e del sistema. Per esempio, esiste un meccanismo del kernel attraverso il quale un task T1 può accedere allo spazio d’indirizzo di un altro task

56 Anche se, nel Mac OS X, gli spazi d’indirizzo virtuale del kernel e dell’utente non sono suddivisioni di un singolo spazio d’indirizzo virtuale, le quantità di memoria virtuale utilizzabile con entrambi sono limitate in accordo con le mappature convenzionali. Per esempio, gli indirizzi kernel nello spazio d’indirizzo virtuale a32-bit del kernel giacciono tra 0x1000 e 0xDFFFFFFF (3.5GB). In modo simile, la quantità di memoria virtuale utilizzabile da un processo utente a 32-bit è significativamente inferiore ai 4GB, poiché varie librerie di sistema sono mappate per default in ogni spazio d’indirizzo utente. 61 Il sistema operativo Mac OS X

T2 se T1 è in esecuzione con i privilegi di root, o se T1 e T2 sono dello stesso utente. I tasks possono anche condividere esplicitamente la memoria con altri tasks.

• Lo spazio utente non può accedere direttamente all’hardware. È comunque possibile avere drivers user-space che accedano all’hardware dopo la mediazione del kernel. Poiché il kernel media l’accesso alle risorse fisiche, un programma utente deve scambiare informazioni con il kernel per avvalersi dei servizi del kernel. L’esecuzione tipica nello spazio utente richiede lo scambio sia di dati sia d’informazioni di controllo. In un tale scambio tra il task Mach ed il kernel, un thread interno al task effettua la transizione allo spazio kernel dallo spazio utente trasferendo il controllo al kernel. Dopo aver gestito la richiesta del thread utente, il kernel ritorna il controllo al thread, permettendogli di continuare la normale esecuzione. In altre occasioni il kernel può acquisire il controllo anche se il thread corrente non è coinvolto nel trasferimento – nei fatti il trasferimento è raramente richiesto dal programmatore. Ci riferiremo all’esecuzione nello spazio kernel e nello spazio utente come kernel mode e user mode rispettivamente. Tecnicamente anche il kernel mode del Mac OS X può essere visto come composto di due sottomodi. Il primo si riferisce all’ambiente in cui girano i threads propri del kernel, ossia il task kernel e le sue risorse. Il task kernel è un task Mach nel vero senso della parola (è il primo task Mach ad essere creato) che esegue diverse dozzine di threads in un tipico sistema. Il secondo modo si riferisce ai threads in esecuzione nel kernel dopo essere entrati nello spazio kernel attraverso una chiamata di sistema – cioè una threads trap dallo spazio utente nel kernel. I sottosistemi del kernel che devono essere informati dei due modi possono gestirli differentemente.

2.3.1 Tipi di trasferimento del controllo Anche se i trasferimenti del controllo sono tradizionalmente divisi in categorie basate sugli eventi che li causano, al livello del processore PowerPC tutte le categorie sono gestite dallo stesso meccanismo d’eccezione. Esempi d’eventi che causano il cambio di modo d’esecuzione del processore, sono i seguenti:

62 Il sistema operativo Mac OS X

• Segnali esterni, come quelli di un controllore d’interruzione hardware • Condizioni anormali incontrate durante l’esecuzione di un’istruzione • Eventi di sistema previsti, come il rescheduling ed i page faults • Eccezioni della traccia causate dalla deliberata abilitazione del single-stepping (bit SE del MSR) o del branch-tracing (bit BE del MSR)

• Condizioni interne al processore, come il rilevamento di un errore di parità nella L1 D-cache

• Esecuzione dell’istruzione di chiamata di sistema Tuttavia rimane utile dividere i trasferimenti del controllo in categorie basate sugli eventi che li generano. Andiamo ad esaminare alcune grandi categorie.

2.3.1.1 External Hardware Interrupts Un’interruzione hardware esterna è un trasferimento del controllo nel kernel tipicamente iniziata da una periferica hardware per indicare un evento. Tali interruzioni sono segnalate al processore dall’asserzione del segnale d’ingresso di interruzione esterna del processore, che causa un’eccezione d’interruzione esterna nel processore. Le interruzioni esterne sono asincrone e non sono generalmente correlate al thread correntemente in esecuzione. Notiamo che le interruzioni esterne possono essere mascherate. Un esempio di interruzione esterna è un controller di periferica di storage che causa un’interrupt per segnalare il completamento di una richiesta di I/O. In certi processori

un’eccezione termica è segnalata dall’asserzione del segnale d’ingresso di interruzione termica: in questo caso, anche se la condizione anormale è interna al processore, la sorgente dell’interruzione è esterna.

2.3.1.2 Processor Traps Una trappola di processore è un trasferimento del controllo nel kernel iniziata dal processore stesso a causa di qualche evento che necessita attenzione. Tali trappole possono essere sincrone o asincrone. Anche se le condizioni che causano una traps possono essere tutte definite anormali, in quanto tutte eccezionali, è utile sottoclassificarle come attese

63 Il sistema operativo Mac OS X

(expected) o inattese (unexpected). Alcuni esempi di cause di trappole sono l’errore di divisione per zero, completamento di un’istruzione di tracciamento, accesso illegale alla memoria o anche l’esecuzione di una istruzione illegale.

2.3.1.3 Software Traps Il kernel del Mac OS X implementa un meccanismo chiamato asynchronous system traps (ASTs), dove uno o più bits possono essere fissati dal software per un processore o un thread. Ognuno di tali bits rappresenta una particolare trappola software. Quando un processore sta per ritornare da un contesto di interruzione, incluso il ritorno da una chiamata di sistema, esso controlla questi bits e se ne trova uno attivo prende la trappola relativa. Quest’ultima operazione consiste nell’eseguire il corrispondente codice di gestione dell’interruzione. Un thread controlla spesso tali trappole quando sta per cambiare il suo stato di esecuzione, come ad esempio quando passa dallo stato suspended a quello running. Anche il clock interrupt handler del kernel controlla periodicamente le ASTs. Noi categorizziamo le ASTs nel Mac OS X come delle software traps poiché sono iniziate e gestite completamente dal software, anche se alcune implementazioni AST possono usare un supporto hardware.

2.3.1.4 System Calls L’istruzione di chiamata di sistema PowerPC è utilizzata per generare un’eccezione di chiamata di sistema, che causa la preparazione e l’esecuzione, da parte del processore, del system call handler nel kernel. La system call exception è sincrona. Un set di interfacce ben definito è composto da centinaia di system calls, che fungono da punti di ingresso nel kernel per i programmi utente. Un set di system calls standard è definito dal Portable Operating System Interface (POSIX), che definisce un’interfaccia e non la sua implementazione. Il Mac OS X fornisce un esteso subset dell’API POSIX.

64 Il sistema operativo Mac OS X

Capitolo 3 I Processi

In un tipico sistema operativo, un processo rappresenta un programma in esecuzione con le risorse di sistema associate, che possono essere fisiche (come i cicli di processore e la memoria) o astratte57 (come il numero di files che il processo può aprire). Il kernel fornisce un’illusione d’esecuzione concorrente attraverso lo scheduling delle risorse tra i processi ready-to-run. Su un sistema multiprocessore, o multicore, è possibile eseguire in modo realmente concorrente più processi. Il kernel del Mac OS X divide la tradizionale astrazione di processo in più astrazioni correlate. In questo capitolo andremo ad esaminare i dettagli del sottosistema di processo del Mac OS X, sia del livello kernel sia del livello utente. Nei primi sistemi UNIX, un processo poteva eseguire un programma utente o rappresentare uno o più flussi di controllo nel kernel. L’unico modo di creare un nuovo

processo in un tradizionale UNIX era attraverso la chiamata di sistema fork(), e l’unico modo di far girare un nuovo programma in un processo era attraverso la chiamata di

sistema (). Comparati ai moderni sistemi operativi, i primi UNIX avevano un’astrazione di processo molto semplice. Solo dopo essere stato riscritto in C, il kernel UNIX ha potuto avere più di un processo in memoria contemporaneamente. Oltre al testo del programma ed ai dati, ogni processo ha uno stack kernel-mode ed un’area dati (la struttura utente o u-area). Ricordiamo infine che può esserci un solo processo corrente.

57 Le risorse astratte sono spesso limitate, direttamente o indirettamente, dalle risorse fisiche. 65 Il sistema operativo Mac OS X

Come gli altri sistemi operativi moderni, anche il Mac OS X ha dei limiti soft e hard nel numero di processi consentiti. Il limite hard è maggiore o uguale al limite soft, è fissato al tempo di compilazione e non può essere cambiato. Il limite soft può variare mediante l’interfaccia sysctl fissando il valore della variabile kern.maxproc.

$ sysctl –a | grep proc // bsd/conf/param.c

kern.maxproc = 532 #define NPROC (20+16*MAXUSERS) kern.maxfilesperproc = 10240 #define HNPROC (20+64*MAXUSERS)

kern.maxprocperuid = 100 int maxproc = NPROC;

kern.aioprocmax = 16 /* hardcoded limit */ kern.proc_low_pri_io = 0 __private_extern__ int hard_maxproc = HNPROC;

… Il valore di MAXUSERS è definito in un file di configurazione nella porzione BSD del kernel, dalla tabella 3-1 possiamo ricavarne i valori. Un kernel Mac OS X standard è compilato in una configurazione medium, con MAXUSERS pari a 32. I corrispondenti valori di NPROC e HNPROC sono 532 e 2068 rispettivamente.

Tabella 3-1 Configurazione della dimensione del sistema Configurazione Descrizione MAXUSERS xlarge Scala extra-large 64 large Scala large 50 medium Scala medium 32 small Scala small 16 xsmall Scala extra-small 8 bsmall Scala extra-small speciale (come per i floppies di boot) 2

La differenza nel numero massimo di processi permessi tra i primi sistemi UNIX (tipicamente 50) ed il Mac OS X (dieci volte superiore) è insignificante rispetto alle differenze nelle rispettive composizioni dei loro sottosistemi di processo. Nella figura 3-1 possiamo vedere la composizione del sottosistema di processo del Mac OS X. Nonostante la presenza di numerose entità process-like, è una ed una sola astrazione ad essere eseguita in un processore: il thread Mach. Tutte le altre entità sono eventualmente stratificate sui threads Mach.

66 Il sistema operativo Mac OS X

Figura 3-1 Il sottosistema di processo del Mac OS X

3.1 Astrazioni, Strutture dati e APIs del Mach Esaminiamo alcune delle strutture dati del kernel, che giocano un ruolo importante nel sottosistema di processo del Mac OS X:

• struct processor_set [osfmk/kern/processor.h] – la struttura dell’insieme di processori.

• struct processor [osfmk/kern/processor.h] – la struttura del processore.

• struct task [osfmk/kern/task.h] – la struttura del task Mach. • struct thread [osfmk/kern/thread.h] – la struttura del thread Mach indipendente dalla macchina.

• struct machine_thread [osfmk/ppc/thread.h] – la struttura di stato del thread dipendente dalla macchina.

67 Il sistema operativo Mac OS X

• struct proc [bsd/sys/proc.h] – la struttura del processo BSD. • struct uthread [bsd/sys/user.h] – la struttura dell’utente per-thread BSD.

• struct run_queue [osfmk/kern/sched.h] – la struttura della coda di esecuzione utilizzata dallo scheduler. Il Mach raggruppa i processors in uno o più processor sets, ognuno dei quali con una propria run queue di threads eseguibili, e ogni processore ha la sua coda di esecuzione locale. Oltre alla coda, un insieme di processori mantiene anche una lista di tutti i threads nel set, insieme al task assegnato al set. Un task contiene una lista dei suoi threads ed un riferimento al relativo processor set. Uno stato del thread machine-dependent, incluso il

process control block (PCB), è “catturato” in una struttura machine_thread. Un processo BSD ha inoltre una struttura proc che si riferisce al task associato. Un processo multithread è implementato come un task Mach contenente threads Mach multipli. Ogni

thead in un processo BSD contiene un puntatore alla struttura uthread. La struttura proc, inoltre, contiene una lista di puntatori alle strutture uthreads, una per ogni thread del processo.

3.1.1 Processor sets Durante la fase d’avviamento del kernel, prima che lo scheduler possa partire, viene inizializzato il default processor set con tutti i processori del sistema58. Il primo task creato dal kernel è assegnato al set di default. La motivazione originale dietro gli insiemi di processori fu di raggruppare i processori per allocarli ad attività di sistema specifiche. Nelle prime versioni del Mac OS X i processor sets hanno avuto associate politiche di scheduling ed attributi, che fornivano un controllo uniforme delle funzioni di scheduling dei threads nel set. Politiche specifiche possono essere abilitate o disabilitate al livello dell’insieme di processori. Come risulta dalla figura 3-2, un oggetto ‘processor set’ ha due porte Mach che lo

58 Ne consegue che esiste sempre almeno un processor set (quello di default). Un processor set può anche essere vuoto, tranne quello di default che ne contiene almeno uno. Inoltre un processore appartiene al massimo ad un processor set alla volta. 68 Il sistema operativo Mac OS X

rappresentano: la porta name e la porte control. La porta nome è un identificatore, che può essere utilizzata solo per recuperare informazioni circa l’insieme di processori. La porta controllo rappresenta l’oggetto sottostante, che può essere utilizzato per eseguire operazioni di controllo (come ad esempio assegnare processori, tasks e threads al processor set). Questo scenario è rappresentativo dell’architettura Mach, dove le operazioni di controllo sui vari oggetti Mach sono effettuate spedendo gli appropriati messaggi alle porte di controllo dei rispettivi oggetti.

Figura 3-2 La struttura del processor set nel kernel XNU // osfmk/kern/processor.h

struct processor_set { queue_head_t idle_queue; // queue of idle processors int idle_count; // how many idle processors? queue_head_t active_queue; // queue of active processors queue_head_t processors; // queue of all processors int processor_count; // how many processors? decl_simple_lock_data(,sched_lock) // scheduling lock struct run_queue runq; // run queue for this set queue_head_t tasks; // tasks assigned to this set int task_count; // how many tasks assigned? queue_head_t threads; // threads in this set int thread_count; // how many threads assigned? int ref_count; // structure reference count int active; // is this set in use? ... struct ipc_port *pset_self; // control port (for operations) struct ipc_port *pset_name_self; // name port (for information) uint32_t run_count; // threads running uint32_t share_count; // timeshare threads running integer_t mach_factor; // the Mach factor integer_t load_average; // load average uint32_t pri_shift; // scheduler load average };

extern struct processor_set default_pset;

L’API del processor set del Mach fornisce delle routines che possono essere chiamate dallo spazio utente per richiedere e manipolare59 gli insiemi di processori. Riportiamo di seguito alcuni esempi di tali routines:

• host_processor_sets() ritorna una lista di send rights rappresentante le porte nome di tutti gli insiemi di processori presenti nell’host.

• host_processor_set_priv() traduce una porta nome in una porta controllo dell’insieme di processori.

• processor_set_default() ritorna la porta nome per il default processor set.

59 La manipolazione dei processor sets è un’operazione privilegiata. 69 Il sistema operativo Mac OS X

• processor_set_create() crea un nuovo processor set e ne ritorna le porte nome e controllo.

• processor_set_destroy() distrugge l’insieme di processori specificato e rilascia i suoi processori, task e threads al default processor set.

• processor_set_info() recupera varie informazioni riguardo l’insieme di processori specificato in base al parametro (flavour) passato, ad esempio

PROCESSOR_SET_BASIC_INFO fornisce il numero di processori assegnati e la politica di timeshare utilizzata per default (POLICY_TIMESHARE), restituiti attraverso una struttura processor_set_basic_info PROCESSOR_SET_TIMESHARE_DEFAULT fornisce gli attributi base per la politica di scheduling timeshare, restituiti attraverso una struttura

policy_timeshare_base PROCESSOR_SET_TIMESHARE_LIMITS fornisce i limiti sugli attributi permessi di politica timeshare, restituiti attraverso una struttura

policy_timeshare_limit. • processor_set_statistics() recupera le statistiche di scheduling per l’insieme di processori specificato in base al parametro passato, ad esempio

PROCESSOR_SET_LOAD_INFO fornisce le statistiche di caricamento, restituite attraverso una struttura processor_set_load_info. • processor_set_tasks() ritorna una lista di send rights alle porte del kernel contenente tutti i tasks assegnati correntemente all’insieme di processori specificato.

• processor_set_threads() ritorna una lista di send rights alle porte del kernel contenente tutti i threads assegnati correntemente all’insieme di processori specificato.

• processor_set_stack_usage() recupera informazioni sull’utilizzo dello stack del thread in un dato insieme di processori60. Notiamo che utilizzando la lista dei processor sets, tutti i task ed i threads nel sistema

60 Si tratta di una routine di debug che è abilitata solo se il kernel è stato compilato con l’opzione MACH_DEBUG. 70 Il sistema operativo Mac OS X

possono essere individuati. Se il kernel supporta solo un insieme di processori, ovviamente le chiamate alle routines di creazione e distruzione falliranno. Nel Mac OS X l’interfaccia del processor set è disapprovata e probabilmente verrà cambiata o addirittura fatta sparire. Il kernel XNU, infatti, supporta solo un singolo insieme di processori: le routines d’interfaccia operano sul default processor set.

3.1.2 Processors

La struttura processor è una descrizione indipendente dalla macchina di un processore fisico. Alcuni campi della struttura processor sono simili a quelli della struttura processor_set, ma con una portata locale al processore. Ad esempio il campo runq in questo caso si riferisce ai soli threads collegati al processore in questione.

Figura 3-3 La struttura del processor nel kernel XNU // osfmk/kern/processor.h

struct processor { queue_chain_t processor_queue; // idle, active, or action queue link int state; // processor state struct thread *active_thread; // thread running on processor struct thread *next_thread; // thread to run if dispatched struct thread *idle_thread; // this processor's idle thread processor_set_t processor_set; // the processor set that we belong to int current_pri; // current thread's priority timer_call_data_t quantum_timer; // timer for quantum expiration uint64_t quantum_end; // time when current quantum ends uint64_t last_dispatch; // time of last dispatch int timeslice; // quantum before timeslice ends int deadline; // current deadline struct run_queue runq; // local run queue for this processor queue_chain_t processors; // all processors in our processor set ... struct ipc_port *processor_self; // processor's control port processor_t processor_list; // all existing processors processor_data_t processor_data; // per-processor data };

... extern processor_t master_processor;

La figura 3-3 mostra un estratto della dichiarazione della struttura processor. I possibili stati in cui si può trovare un processore sono: PROCESSOR_OFF_LINE (non disponibile), PROCESSOR_RUNNING (in normale esecuzione), PROCESSOR_IDLE (in attesa), PROCESSOR_DISPATCHING (in transizione dall’attesa all’esecuzione), PROCESSOR_SHUTDOWN (in disattivazione), PROCESSOR_START (attivato).

71 Il sistema operativo Mac OS X

Figura 3-4 Un processor set contenente due processori

La figura 3-4 mostra come sono interconnesse le strutture processor set e processor in un sistema con un insieme di processori e due processori. Il campo processors in ogni processore, insieme con quello dell’insieme di processori, sono collegati insieme mediante una lista circolare. Tale campo è un elemento coda contenente solo due puntatori: prev (precedente) e next (successivo). In particolare, il puntatore next del campo processors relativo alla struttura processor_set punta

72 Il sistema operativo Mac OS X al primo processore (o master processor). In pratica è possibile attraversare la lista di tutti i processori in un processor set sia dall’insieme sia da un qualsiasi processore. In modo analogo si può attraversare la lista di tutti i processori attivi in un insieme, utilizzando il campo active_queue della struttura processor_set ed i campi processor_queue di ogni struttura processor dell’insieme. La figura 3-5 mostra le relazioni tra le strutture quando entrambi i processori (della figura 3-4) sono nella coda attiva dell’insieme di processori di default.

Figura 3-5 Un processor set con due processori nella sua coda attiva 73 Il sistema operativo Mac OS X

Riportiamo ora alcuni esempi di routines Mach relative ai processori:

• host_processors() ritorna un array di send rights rappresentante tutti i processori nel sistema61.

• processor_control() esegue operazioni di controllo dipendenti dalla macchina, o commands, sul processore specificato.

• processor_info() recupera informazioni relative al processore specificato in base al parametro (flavour) passato, ad esempio

PROCESSOR_BASIC_INFO fornisce il tipo, il sottotipo ed il numero di slot del processore, se è in esecuzione e se si tratta del master processor

PROCESSOR_CPU_LOAD_INFO fornisce il numero di tasks e threads assegnati al processore, il suo carico medio ed il suo fattore Mach.

• in un sistema multiprocessore, processor_start() fa partire il processore indicato se questi era offline (assegnandolo poi al default processor set), mentre

processor_exit() lo ferma (rimuovendolo dal processor set assegnato). • processor_get_assignement() ritorna la porta nome per l’insieme di processori a cui è attualmente assegnato il processore indicato.

• processor_assign() assegna un processore ad un insieme di processori. Poiché il kernel XNU supporta solo un insieme di processori, l’ultima routine fallisce

sempre, mentre la processor_get_assignement() ritorna sempre il default processor set. Molte routines Mach relative ai processori hanno un comportamento dipendente dalla macchina e, in particolare, le routines che riguardano il comportamento globale di un processore sono privilegiate. Esistono inoltre delle chiamate per fissare (set) oppure ottenere (get) la corrispondenza tra l’insieme di processori ed i tasks e threads. Ad

esempio task_assing() assegna un task, ed eventualmente tutti i suoi threads, ad un dato insieme di processori. A meno che tutti i threads siano inclusi, solo i nuovi threads

61 Notiamo che il chiamante nello spazio utente riceve l’array come out-of-line data in un messaggio IPC Mach. La memoria appare implicitamente allocata nello spazio di memoria virtuale del chiamante. In questo caso, il chiamante dovrà esplicitamente deallocare la memoria al termine dell’utilizzo mediante una delle routines Mach a disposizione, vm_deallocate() oppure mach_vm_deallocate(). 74 Il sistema operativo Mac OS X

creati saranno assegnati al nuovo processor set. Così come le altre chiamate collegate a

processor sets multipli, task_asign() ritornerà sempre un fallimento nel Mac OS X.

3.1.3 Tasks Un task Mach è un’astrazione, indipendente dalla macchina, dell’ambiente d’esecuzione dei threads. Come sappiamo un task è un contenitore di risorse, che incapsula accesso protetto ad uno spazio degli indirizzi virtuali sparso, uno spazio (di porte) IPC, risorse di processore, controllo di scheduling e, ovviamente, i threads che utilizzano tali risorse.

La figura 3-6 mostra un estratto della dichiarazione della struttura task del Mac OS X.

Figura 3-6 La struttura del task nel kernel XNU // osfmk/kern/task.h

struct task { ... vm_map_t map; // address space description queue_chain_t pset_tasks; // list of tasks in our processor set ... queue_head_t threads; // list of threads in this task int thread_count; // number of threads in this task ... integer_t priority; // base priority for threads integer_t max_priority; // maximum priority for threads ...

// IPC structures ... struct ipc_port *itk_sself; // a send right struct exception_action exc_actions[EXC_TYPES_COUNT]; // exception ports struct ipc_port *itk_host; // host port struct ipc_port *itk_bootstrap; // bootstrap port // "registered" ports -- these are inherited across task_create() struct ipc_port *itk_registered[TASK_PORT_REGISTER_MAX]; struct ipc_space *itk_space; // the IPC space ...

// locks and semaphores queue_head_t semaphore_list; // list of owned semaphores queue_head_t lock_set_list; // list of owned lock sets int semaphores_owned; // number of owned semaphores int lock_sets_owned; // number of owned locks ...

#ifdef MACH_BSD void *bsd_info; // pointer to BSD process structure #endif

struct shared_region_mapping *system_shared_region; struct tws_hash *dynamic_working_set; ... };

Riportiamo di seguito alcuni esempi delle routines relative al task Mach accessibili attraverso la libreria di sistema:

75 Il sistema operativo Mac OS X

• mach_task_self() è la “trappola d’identità” del task e ritorna un diritto di spedizione alla porta kernel del task chiamante.

• pid_for_task() recupera l’ID del processo BSD per il task specificato attraverso la porta data62.

• task_for_pid() recupera la porta per il task corrispondente allo specificato ID di processo BSD.

• task_info() recupera informazioni sul dato task a seconda del parametro (flavour) passato, ad esempio

TASK_BASIC_INFO fornisce la dimensione della memoria virtuale, la dimensione della memoria residente, il suspend count e così via

TASK_THREAD_TIMES_INFO fornisce il tempo totale dei threads vivi TASK_EVENT_INFO fornisce i page faults, le system calls, i context switch etc. • task_threads() ritorna un array di send rights alle porte kernel di tutti i threads del task specificato.

• task_create() crea un nuovo task Mach che può ereditare lo spazio degli indirizzi del task chiamante o essere creato con uno spazio degli indirizzi vuoto; il task chiamante ha pieno accesso alla porta kernel del task creato, che non contiene threads; notiamo che questa chiamata non crea un processo BSD e quindi non è usufruibile dallo spazio utente.

• task_suspend() incrementa il suspend count del task e ne ferma tutti i threads; i nuovi threads creati nel task non potranno essere eseguiti finché il suspend count relativo è positivo.

• task_resume() decrementa il suspend count del task e se ha raggiunto il valore zero, allora riprende l’esecuzione dei suoi threads63.

• task_terminate() elimina il task specificato e tutti i suoi threads, deallocandone tutte le risorse.

• task_get_exception_ports() recupera i send rights verso uno specifico

62 Notiamo che, mentre tutti i processi BSD hanno un corrispondente task Mach, è tecnicamente possibile avere un task Mach che non sia associato con un processo BSD. 63 Il suspend count di un task non può mai essere negativo, può essere nullo (runnable task) o positivo (suspended task). 76 Il sistema operativo Mac OS X

gruppo di exception ports per il task specificato64.

• task_set_exception_ports() definisce le porte d’eccezione del task. • task_swap_exception_ports() esegue le funzioni combinate delle due routines precedenti.

• task_get_special_port() recupera un send right per una specificata porta speciale in un task, ad esempio alcune porte speciali sono

TASK_KERNEL_PORT65 utilizzata per controllare il task TASK_BOOTSTRAP_PORT utilizzata nelle richieste per recuperare le porte che rappresentano i servizi di sistema

TASK_HOST_NAME_PORT66 utilizzata per recuperare informazioni sull’host.

• task_set_special_port() fissa una delle porte speciali del task al send right specificato.

• task_policy_get() recupera i parametri della politica di scheduling per il task specificato; può anche essere utilizzata per recuperare i valori dei parametri della default task policy.

• task_policy_set() è utilizzata per definire le informazioni della politica di scheduling per un task.

3.1.4 Threads Un thread Mach è un singolo flusso di controllo in un task Mach. In funzione della natura e dell’architettura di un’applicazione, l’utilizzo di threads multipli può portare a miglioramenti nelle prestazioni dell’applicazione stessa, come nei seguenti esempi:

• quando la computazione e l’I/O possono essere separati e sono mutuamente indipendenti, dei threads dedicati possono essere utilizzati per eseguire queste due tipologie di attività simultaneamente.

64 Ricordiamo che una exception port è una porta a cui il kernel manda messaggi quando si hanno uno o più tipi di eccezioni. Notiamo inoltre che i threads hanno le proprie exception ports, che sono prioritarie rispetto a quella del task. Solo se una porta d’eccezione di thread è impostata sulla porta nulla (IP_NULL), o ritorna con un fallimento, allora la porta d’eccezione del task entra in gioco. 65 La stessa porta ritornata dalla routine mach_task_self(). 66 La stessa porta ritornata dalla routine mach_host self(). 77 Il sistema operativo Mac OS X

• quando i contesti di esecuzione (threads o processi) devono essere creati e distrutti frequentemente, utilizzando i threads si ottengono prestazioni migliori poiché un thread è sostanzialmente meno oneroso da creare di un intero processo67.

• in un sistema multiprocessore, i vari threads all’interno dello stesso task possono realmente essere eseguiti in modo concorrente, il che aumenta le prestazioni (se il thread trae benefici dalla computazione concorrente).

Figura 3-7 La struttura del threads nel kernel XNU // osfmk/kern/thread.h

struct thread { queue_chain_t links; // run/wait queue links run_queue_t runq; // run queue thread is on wait_queue_t wait_queue; // wait queue we are currently on event64_t wait_event; // wait queue event ... thread_continue_t continuation; // continue here next dispatch void *parameter; // continuation parameter ... vm_offset_t kernel_stack; // current kernel stack vm_offset_t reserved_stack; // reserved kernel stack

int state; // state that thread is in

// scheduling information ... // various bits of stashed machine-independent state ... // IPC data structures ... // AST/halt data structures ... // processor set information ...

queue_chain_t task_threads; // threads in our task struct machine_thread machine; // machine-dependent state struct task *task; // containing task vm_map_t map; // containing task's address map ...

// mutex, suspend count, stop count, pending thread ASTs ... // other ... struct ipc_port *ith_sself; // a send right struct exception_action exc_actions[EXC_TYPES_COUNT]; // exception ports ...

#ifdef MACH_BSD void *uthread; // per-thread user structure #endif };

Un thread contiene varie informazioni, tra cui:

• la priorità e la politica di scheduling, insieme agli attributi relativi.

67 Il miglioramento delle prestazioni è percepibile solo se l’applicazione crea così tanti processi (o li crea in maniera tale) che l’overhead diventa un fattore limitante della performance dell’applicazione. 78 Il sistema operativo Mac OS X

• le statistiche sull’utilizzo del processore. • un numero ristretto di port rights specifici, inclusi la thread’s kernel port ed i thread-level exception ports (corrispondenti ai thread-level exception handlers).

• lo stato di macchina (attraverso una struttura thread-state dipendente dalla macchina), che cambia nel corso dell’esecuzione del thread.

Nella figura 3-7 possiamo vedere le costituenti principali della struttura thread, così com’è definita nel Mac OS X. Un programma utente controlla un thread Mach utilizzando la porta kernel del thread. Tale controllo avviene normalmente in modo indiretto, attraverso la libreria Pthreads che fa

parte delle librerie di sistema (libSystem.dylib) del Mac OS X. Esaminiamo alcune routines accessibili attraverso la libreria di sistema che compongono, nel loro insieme, l’API del thread:

• mach_thread_self() ritorna i send rights alla porta kernel del thread chiamante.

• thread_info() recupera informazioni riguardanti il thread specificato in base al parametro (flavour) passato, in particolare THREAD_BASIC_INFO che fornisce i run times d’utente e di sistema, la politica di scheduling in uso, il suspend count e così via68.

• thread_get_state() recupera lo stato d’esecuzione user-mode specifico della macchina per il relativo thread, che non deve essere il thread chiamante. A seconda del flavour passato, la routine restituisce uno stato contenente differenti set di valori di registro specifici della macchina. Alcuni esempi di tali flavours sono

PPC_THREAD_STATE, PPC_THREAD_STATE64, PPC_EXCEPTION_STATE, PPC_EXCEPTION_STATE64, PPC_VECTOR_STATE e PPC_FLOAT_STATE. • thread_set_state() esegue l’operazione inversa della precedente, ricordando che il thread chiamante non può modificare il proprio stato attraverso questa routine.

68 Esistono altri flavours, ormai obsoleti, come THREAD_SCHED_FIFO_INFO, THREAD_SCHED_RR_INFO e THREAD_SCHED_TIMESHARE_INFO che forniscono informazioni riguardo la politica di scheduling. 79 Il sistema operativo Mac OS X

• thread_create() crea un thread, all’interno del task indicato, con suspend count pari a 1 e senza uno stato di macchina69.

• thread_create_running() combina l’effetto delle routines thread_create(), thread_set_state() e thread_resume() creando così un thread running (con lo stato di macchina fornito) all’interno del task specificato.

• thread_suspend() incrementa il suspend count del thread; fin quando il suspend count resterà positivo, il thread non potrà eseguire nessuna istruzione a livello utente70.

• thread_resume() decrementa il suspend count del thread; se il valore diventa zero, il thread è ripreso (ma non obbligatoriamente rimesso in esecuzione)71.

• thread_terminate() distrugge il thread specificato e, se si tratta dell’ultimo thread da terminare in un task che corrisponde ad un processo BSD, il codice di terminazione del thread esegue anche un’uscita del processo BSD.

• thread_switch() istruisce lo scheduler di cambiare contesto direttamente verso un altro thread. Il chiamante può anche specificare un particolare thread come “suggerimento”. In questo caso lo scheduler tenterà di effettuare lo switch verso il thread specificato. Molte condizioni devono sussistere affinché lo switch suggerito avvenga con successo72. Nel caso non sia suggerito nessun thread, la routine forza una rischedulazione ed un nuovo thread da eseguire è scelto. L’esistente stack

kernel del chiamante è scartato e, quando questo eventualmente è ripreso, esegue la

funzione di continuazione thread_switch_continue() (contenuta in osfmk/kern/syscall_subr.c) in un nuovo stack kernel.

69 Lo stato di macchina del nuovo thread dovrà essere specificato esplicitamente mediante la chiamata alla routine thread_set_state() prima di poter essere ripreso mediante chiamata alla routine thread_resume(). 70 Nel caso il thread fosse in esecuzione nel kernel a causa di una trap (come una chiamata di sistema o un page fault), allora può bloccarsi in loco o proseguire fino quasi al termine della trap, a seconda della tipologia di trap. Tuttavia la trap ritornerà solo con la ripresa del thread. 71 Dobbiamo precisare che se il suspend count del task è maggiore di zero, un suo thread non può andare in esecuzione anche se il proprio suspend count è pari a zero. Analogamente a quello del task, il suspend count del thread non può essere negativo. 72 Si tratta di un esempio di handoff scheduling, infatti, il “quantum” del chiamante è passato (handed off) al nuovo thread. 80 Il sistema operativo Mac OS X

La routine thread_switch() può essere istruita a bloccare il thread chiamante per un tempo specificato, un’attesa che può essere cancellata solo attraverso la

chiamata a thread_abort(). Può anche abbassarne temporaneamente la priorità, modificando gli attributi di scheduling in modo che lo scheduler gli fornisca il servizio più basso possibile per il tempo specificato. Al termine di questo tempo, o quando il thread corrente sarà eseguito in seguito, la scheduling depression è abortita. Può esserci anche una terminazione esplicita mediante

chiamata a thread_abort() oppure thread_depress_abort(). • thread_wire() marca il thread come privilegiato in modo tale che possa consumare la memoria fisica dal pool riservato del kernel quando la memoria libera scarseggia73. Inoltre, se un simile thread deve essere inserito in una coda d’attesa di threads, sarà inserito in testa a tale coda.

• thread_abort() può essere usato da un thread per fermarne un altro. Tale routine termina una varietà di operazioni in corso nel thread designato, come ad esempio clock sleeps, scheduling depressions, page faults e altre chiamate di primitive Mach (come le system calls). Se il thread designato si trovava in kernel mode, una chiamata riuscita della routine comporta un ritorno dal kernel del thread. Nel caso di una chiamata di sistema, ad esempio, l’esecuzione del thread riprende nel codice di ritorno della system call (con un codice di ritorno di “chiamata di sistema interrotta”).

Notiamo che thread_abort() dovrebbe essere usata solo su un thread sospeso, in questo caso verrà interrotto quando riprenderà. Questo perché se un thread sta eseguendo una operazione non atomica nel momento in cui la routine è chiamata su di lui, l’operazione sarebbe terminata in un punto arbitrario e non si potrebbe farla

ripartire. La thread_abort() è stata pensata per fermare il target in modo pulito. Nel caso di chiamata alla thread_suspend(), se il target è in esecuzione nel kernel e lo stato del thread è modificato (attraverso

73 Questa routine è pensata per i threads direttamente coinvolti nel meccanismo di page-out, non dovrebbe mai essere invocata da un programma utente. 81 Il sistema operativo Mac OS X

thread_set_state()), allora lo stato può essere alterato in maniera imprevedibile come effetto collaterale della chiamata di sistema al momento della ripresa del thread.

• thread_abort_safely() è simile alla precedente, ma nel caso sia invocata durante una operazione non atomica ritorna un errore. In questo caso il thread dovrà essere ripreso ed una nuova chiamata della routine dovrà essere tentata.

• thread_get_exception_ports() recupera i send rights di una o più porte d’eccezione per un dato thread. I tipi d’eccezione per i quali recuperare le porte sono specificati attraverso una flag word.

• thread_set_exception_ports()definisce la porta data come porta d’eccezione per il tipo d’eccezione specificato.

• thread_get_speacial_port() restituisce un send right da una specifica porta speciale per un dato thread.

• thread_set_special_port() definisce una specifica porta speciale per un dato thread modificandola con il send right fornito dal chiamante. Il precedente send right è rilasciato dal kernel.

• thread_policy_get() recupera i parametri della politica di scheduling per il dato thread.

• thread_policy_set() definisce le informazioni della politica di scheduling per il dato thread. Un thread può mandare un port right ad un altro thread (anche se appartenente ad un altro task) attraverso l’IPC Mach. In particolare, se un thread spedisce la porta kernel del suo task ad un thread in un altro task, un thread nel task ricevente può controllare tutti i threads nel task mittente, poiché l’accesso alla porta kernel di un task comporta l’accesso alle porte kernel dei suoi threads.

3.1.4.1 Kernel Threads Un thread Mach che è la rappresentazione nel kernel di un thread dello spazio utente, può essere chiamato un kernel thread. Un’altra connotazione del termine kernel thread si

82 Il sistema operativo Mac OS X applica a quelli interni che il kernel esegue per le proprie funzionalità. Riportiamo una serie d’esempi di funzioni che il kernel esegue come threads dedicati per implementare le funzionalità del kernel come bootstrapping, scheduling, exception handling, networking e file system I/O.

• processor_start_thread() [osfmk/kern/startup.c] è il primo thread da eseguire su un processore.

• kernel_bootstrap_thread() [osfmk/kern/startup.c] avvia vari serivizi kernel durante l’avvio del sistema ed eventualmente diventa il daemon di

page-out, eseguendo vm_page() [osfmk/vm/vm_pageout.c]. Quest’ultima crea altri kernel threads per eseguire I/O e garbage collection.

• idle_thread() [osfmk/kern/sched_prim.c] è il thread di processore inattivo, che gira cercando altri threads da eseguire.

• sched_tick_thread() [osfmk/kern/sched_prim.c] esegue funzioni periodiche di contabilità relativa allo scheduler.

• thread_terminate_daemon() [osfmk/kern/thread.c] esegue la pulizia finale per i threads terminali.

• thread_stack_daemon() [osfmk/kern/thread.c] alloca gli stacks per i threads messi in coda per la stack allocation.

• serial_keyboard_poll() [osfmk/ppc/serial_io.c] sonda la porta seriale in cerca di input.

• Il meccanismo di callout del kernel gestisce le proprie funzioni come kernel threads.

• Le classi dell’I/O Kit IOWorkLoop e IOService usano IOCreateThread() [iokit/Kernel/IOLib.c] per creare kernel threads. • Il meccanismo dell’I/O asincrono del kernel (AIO) crea dei threads specifici per gestire le richieste di I/O.

• audit_worker() [bsd/kern/kern_audit.c] processa la coda delle annotazioni di verifica scrivendole nel logfile di verifica o altrimenti rimuovendole dalla coda.

83 Il sistema operativo Mac OS X

• mbuf_expand_thread() [bsd/kern/uipc_mbuf.c] aggiunge un cluster di mbufs per aggiungerne di nuovi liberi.

• ux_handler() [bsd/uxkern/ux_exception.c] è il gestore UNIX delle eccezioni che converte le eccezioni Mach in valori di segnali e di codice UNIX.

Figura 3-8 Funzioni per creare kernel threads

• nfs_bind_resv_thread() [bsd/nfs/nfs_socket.c] gestisce le richieste di legame sulle porte riservate da parte di processi non privilegiati.

• dlil_input_thread() [bsd/net/dlil.c] assiste le code di ingresso 84 Il sistema operativo Mac OS X

dell’mbuf delle interfacce di rete, compresa quella dell’interfaccia di loopback,

ingerendo i pacchetti di rete mediante dlil_input_packet() [bsd/net/dlil.c]. Inoltre chiama proto_input_run() per eseguire l’iniezione del pacchetto di livello protocollo.

• dlil_call_delayed_detach_thread() [bsd/net/dlil.c] esegue una separazione ritardata (sicura) di protocolli, filtri e filtri dell’interfaccia.

• bcleanbuf_thread() [bsd/vfs/vfs_bio.c] esegue un servizi di “lavanderia” di buffer del file system. Ripulisce i buffers presenti in una coda speciale (to-be-cleaned queue) scrivendoli sul disco.

• bufqscan_thread() [bsd/vfs/vfs_bio.c] bilancia una porzione delle code dei buffers, pubblicizzando la pulizia dei buffers e rilasciando quelli puliti alla coda dei buffers vuoti. La figura 3-8 mostra le funzioni kernel di alto livello coinvolti nella creazione di kernel

threads. Tra questi, kernel_thread() e IOCreateThread() devono essere utilizzate dal Mach e dall’I/O Kit rispettivamente.

3.1.5 Astrazioni relative al thread Andiamo ora ad esaminare un ristretto numero d’astrazioni rilevanti nel contesto dei threads Mach così come implementati nel kernel del Mac OS X. Discuteremo di remote procedure call (RPC), thread activation e shutlle, thread migration e continuations.

3.1.5.1 Chiamata a procedura remota Il Mach è un kernel orientato alla comunicazione, l’astrazione della chiamata a procedura remota è quindi fondamentale al funzionamento del Mach. Noi definiamo la RPC come l’astrazione di chiamata a procedura quando il chiamante ed il chiamato sono in tasks differenti; in questo caso, infatti, la procedura è remota dal punto di vista del chiamante. Anche se il Mac OS X utilizza solo RPC kernel-level tra tasks locali, quando i partecipanti all’RPC sono su macchine differenti il concetto è simile. In un tipico scenario RPC, l’esecuzione (ossia il controllo di flusso) è spostata temporaneamente in un’altra locazione

85 Il sistema operativo Mac OS X

(che corrisponde alla procedura remota) e, in seguito, riportata alla locazione originale così come nelle chiamate di sistema. Il chiamante (client) raduna tutti i parametri insieme in un messaggio che spedisce al fornitore di servizio (server). Quest’ultimo scompone il messaggio nei suoi pezzi originali e li processa come operazioni locali.

3.1.5.2 Attivazione e Shuttle Prima del ‘Mac OS X 10.4’, un kernel thread era diviso in due parti logiche: l’activation e lo shuttle. Il motivo di questa divisione era la necessità di avere una parte che fornisse un controllo esplicito sul thread (l’activation) ed un’altra parte che venisse utilizzata dallo scheduler (lo shuttle). La thread activation rappresenta il contesto di esecuzione del thread, rimane collegata al suo task (e quindi ha sempre un fissato puntatore a task valido) e, fino

a quando non è terminata, rimane nello stack di attivazione del task74. Il thread shuttle è l’entità schedulata corrispondente al thread, può operare (per un certo tempo75) all’interno di un’activation e può migrare durante una RPC a causa di una contesa di risorse. Uno shuttle contiene scheduling, accounting, informazioni di timing e supporto al messaging.

Figura 3-9 Le strutture dati shuttle e activation nel Mac OS X 10.0

74 Un thread, in quanto flusso logico di controllo, è rappresentato da uno stack d’attivazioni all’interno di un task. 75 Fino a che uno shuttle utilizza un’activation, mantiene un riferimento su questa. 86 Il sistema operativo Mac OS X

Notiamo che l’activation è vicina alla nozione popolare di thread, essendo la parte esternamente visibile di manipolazione del thread. In contrasto lo shuttle ne rappresenta la

parte più interna. All’interno del kernel, current_act() ritorna un puntatore alla corrente attivazione, laddove current_thread() restituisce un puntatore allo shuttle. L’astrazione duale shuttle/activation ha subito modifiche di implementazione nelle varie versioni di Mac OS X. Come possiamo vedere dalla figura 3-9, nella versione 10.0 l’implementazione di un thread consisteva di due strutture dati primarie: la

thread_shuttle e la thread_activation, con il tipo di dato thread (thread_t) che punta alla struttura thread_shutlle. L’activation può essere acceduta dallo shuttle, e quindi un thread_t rappresenta un thread nella sua interezza.

Figura 3-10 La struttura thread nel Mac OS X 10.3

Nelle versioni successive del Mac OS X, la struttura dati del thread ha inglobato, da un punto di vista sintattico, lo shuttle e l’activation in una singola struttura (vedi figura 3-10).

La figura 3-11 mostra l’implementazione di current_thread() e current_act() nel Mac OS X 10.3 e 10.476.

76 A partire dalla versione 10.4 la distinzione tra shuttle e activation non è più presente. 87 Il sistema operativo Mac OS X

Figura 3-11 Recupero del thread corrente (shuttle) e dell’activation corrente nelle versioni Mac OS X 10.3 e 10.4 // osfmk/ppc/cpu_data.h (Mac OS X 10.3) extern __inline__ thread_act_t current_act(void) { thread_act_t act; __asm__ volatile("mfsprg %0,1" : "=r" (act)); return act; }; ... #define current_thread() current_act()->thread

// osfmk/ppc/cpu_data.h (Mac OS X 10.4) extern __inline__ thread_t current_thread(void) { thread_t result; __asm__ volatile("mfsprg %0,1" : "=r" (result)); return (result); }

// osfmk/ppc/machine_routines_asm.s (Mac OS X 10.4) /* thread_t current_thread(void) * thread_t current_act(void) * Return the current thread for outside components. */ align 5 .globl EXT(current_thread) .globl EXT(current_act) LEXT(current_thread) LEXT(current_act) mfsprg r3,1 blr

Il modello originale dell’RPC nel Mach è basato su servizi per lo scambio di messaggi, dove threads distinti leggono messaggi e scrivono risposte. Abbiamo visto che anche i diritti d’accesso sono comunicati nel Mach attraverso messaggistica. I sistemi operativi hanno supportato l’IPC procedurale in diverse forme: gates nel Multics, lightweight RPC (LRPC) nel TaOS, doors nel Solaris ed event pairs nel Windows NT sono esempi di meccanismi di chiamata a procedura cross-domain. Il modello di thread nello Spring System della Sun ha il concetto di shuttle, la vera entità schedulabile dal kernel che supporta una catena di threads visibili dalle applicazioni (analoghi alle attivazioni).

3.1.5.3 Migrazione del thread Nella discussione sulla dualità shuttle/attivazione abbiamo accennato alla migrazione di un thread77. Il termine migration si riferisce al modo in cui è trasferito il controllo tra il client ed il server durante una RPC. La sequenza d’eventi successivi all’inizio della chiamata a procedura remota da parte del client, comportano molti cambi di contesto. Con il modello

77 Il modello dei migrating threads fu sviluppato all’University of Utah. 88 Il sistema operativo Mac OS X

a thread diviso, piuttosto che bloccare il thread client sulla sua RPC kernel, il kernel può farlo migrare in modo da riprendere l’esecuzione nel codice del server. Anche se è richiesto ancora qualche context switch (in particolare quello dell’address space, lo stack pointer e forse qualche subset del register state), non sono più due interi threads ad essere coinvolti. Dal punto di vista dello scheduler, tra l’altro, non c’è nessun cambio di contesto. Infine, il client utilizza il proprio tempo di processore durante la sua esecuzione all’interno del codice del server; in questo senso, la migrazione del thread è un meccanismo di ereditarietà prioritaria.78

3.1.5.4 Continuazioni Il kernel del Mac OS X utilizza un kernel stack di 16KB per ogni thread

(KERNEL_STACK_SIZE [osfmk/mach/ppc/vm_param.h]). All’aumentare del numero di threads in un sistema, la memoria consumata dai soli kernel stacks può diventare irragionevole, secondo le risorse disponibili. Alcuni sistemi operativi convogliano diversi threads utente in un unico kernel thread, anche se al costo della concorrenza (poiché questi user threads non possono essere schedulati indipendentemente). Ricordando che un Mach thread (o una attivazione) è legato al suo task per tutta la vita del thread e che ogni task (che abbia un significato) ha almeno un thread, possiamo affermare che in un sistema il numero di threads è almeno pari al numero dei tasks.

I sistemi operativi utilizzano uno dei due modelli storici per l’esecuzione kernel: il process model o l’interrupt model. Nel modello di processo, il kernel mantiene uno stack per ogni thread. Quando un thread è eseguito nel kernel (a causa di una chiamata di sistema o un’eccezione) il suo kernel stack dedicato, è utilizzato per tracciare il suo stato d’esecuzione. Se il thread si blocca nel kernel, non è richiesto un salvataggio esplicito dello stato, poiché lo stato è catturato nel kernel stack del thread. La semplificazione introdotta da questo approccio è

78 Il concetto di migrazione può essere comparato al modello UNIX della chiamata di sistema. Dove un thread utente (o processo) migra all’interno del kernel durante una system call, senza un cambio di contesto completo. 89 Il sistema operativo Mac OS X

controbilanciata da una richiesta maggiore di risorse, e dal fatto che lo stato macchina è più difficile da valutare se uno dovesse analizzarlo per ottimizzare il trasferimento del controllo tra threads. Nel modello di interruzione, il kernel tratta le chiamate di sistema e le eccezioni come se fossero interruzioni. Si utilizza un kernel stack per ogni processore durante tutta l’esecuzione kernel dei threads. Ciò richiede che i threads bloccati nel kernel devono salvare esplicitamente il loro stato d’esecuzione da qualche parte, in modo che il kernel possa utilizzare lo stato salvato al momento del resume del thread bloccato. Il tipico kernel UNIX ha usato il modello a processi, così come le prime versioni del Mach. Il concetto di continuations fu utilizzato nel ‘Mach 3’ come un approccio intermedio che permettesse ad un thread bloccante l’opzione di utilizzare il process model oppure l’interrupt model. Il Mac OS X continua ad utilizzare tale astrazione e di conseguenza un thread bloccante nel Mac OS X ha la possibilità di decidere come bloccarsi.

La funzione thread_block() [osfmk/kern/sched_prim.c] accetta un singolo argomento, che può essere una funzione di continuazione oppure il valore

THREAD_CONTINUE_NULL definito nell’header file osfmk/kern/kern_types.h. Tale funzione chiama thread_block_reason() [osfmk/kern/sched_prim.c] che a sua volta chiama thread_invoke() [osfmk/kern/sche_prim.c] per effettuare un cambio di contesto è iniziare l’esecuzione di un nuovo thread selezionato per il processore corrente. Questa funzione controlla se è stata specificata una continuazione valida e, in caso positivo, cerca79 di passare il kernel stack del vecchio thread al nuovo. Quindi, quando il vecchio thread si blocca il suo contesto viene scartato e quando riprende gli viene fornito un nuovo kernel stack, su cui verrà eseguita la funzione di continuazione.

La variante thread_block_parameter() [osfmk/kern/sched_prim.c] accetta un singolo parametro che thread_block_reason() memorizza in una struttura thread, dalla quale è recuperato e passato alla funzione di continuazione. Una funzione specificata come continuazione non può ritornare normalmente, ma solo

79 Un thread con una politica di scheduling real-time non rilascia il proprio stack. 90 Il sistema operativo Mac OS X

chiamare una funzione o un’altra continuazione. Un thread che usa una continuazione deve salvare ogni stato che potrebbe essere necessario dopo la ripresa. Tale salvataggio

deve avvenire in uno spazio dedicato che la struttura thread (o una struttura associata) deve avere, altrimenti il thread bloccante dovrà allocare memoria aggiuntiva per questo scopo. La funzione di continuazione dovrà conoscere in quale modo il thread bloccante provvede alla memorizzazione dello stato. Le continuazioni sono molto utili nei casi di thread bloccanti in cui non ci sia uno stato da salvare (o comunque sia piccolo). Esempi di uso delle continuazioni nel Mac OS X includono i threads di inattività del processore, il thread di tick dello scheduler, il thread di swap-in ed il daemon di page-out.

3.1.6 I threads del Mac OS X Il Mac OS X non è il frutto di un team di progettisti, ma è il risultato di un vasto conglomerato di tecnologie differenti provenienti da svariate sorgenti. In quanto sistema operativo commerciale, che si rivolge sia ad utenti esperti (che necessitano di strumenti potenti) sia a neofiti (che hanno bisogno di supporto), il Mac OS X include un’inusuale quantità di meccanismi ed interfacce. Il sottosistema di processo visibile all’utente è una buon esempio di tale fenomeno: il Mac OS X ha diversi flavours di processi e threads user-level, a seconda dell’ambiente dell’applicazione in uso. Alcuni esempi di tali flavours sono i seguenti:

• un thread Mach creato in un kernel task, per l’uso del kernel stesso

• un processo BSD a singolo thread creato mediante la chiamata di sistema fork() • un processo BSD multi-threaded creato originariamente dalla chiamata di sistema

fork(), seguita dalla creazione di uno o più threads aggiuntivi creati utilizzando l’API del Pthreads

• un’applicazione Java con vari threads Java creati utilizzando l’API di

java.lang.Thread • un sottoprocesso creato usando l’API di Cocoa NSTask • un thread creato usando l’API di Cocoa NSThread

91 Il sistema operativo Mac OS X

• un processo CPM (Cocoa Process Manager) creato chiamando l’API di Carbon

LaunchApplication() • tasks schedulati in modo preempitivo, creati in un’applicazione utilizzando Carbon Multiprocessing Service

• threads schedulati in modo cooperativo, creati in un’applicazione utilizzando Carbon Thread Manager

• un thread in esecuzione in un ambiente Carbon A questo punto, i seguenti punti generali possono essere riferiti ai threads del Mac OS X:

• Il kernel conosce un solo tipo di thread: il thread Mach. Quindi ogni entità in esecuzione visibile all’utente, gira come thread Mach, anche se una libreria utente che gestisce tali entità può stratificare queste su di un thread Mach, facendo girare una di queste alla volta.

• Un’entità simile ad un processo di prima classe visibile all’utente ha, tipicamente, un corrispondente processo BSD e, di conseguenza, un corrispondente task Mach.

• Un’entità simile ad un thread di prima classe visibile all’utente ha, tipicamente, un corrispondente pthread e, di conseguenza, un corrispondente thread Mach. Abbiamo detto tipicamente e non sempre, poiché è tecnicamente possibile creare un task Mach senza un processo BSD ed un thread Mach senza un pthread. Potrebbero esserci anche altre eccezioni, ad esempio l’intero ambiente Classic, insieme a tutti i suoi processi di Process Management, corrisponde ad un solo processo BSD.

3.2 Lo Scheduling Un sistema timesharing fornisce l’illusione che molti processi girino in modo concorrente interponendo la loro esecuzione, cambiando contesto da uno all’altro in base a varie condizioni. L’insieme di regole in base alle quali si determina l’ordine di esecuzione dei threads è chiamato scheduling policy. Una componente del sistema, chiamata scheduler, implementa la politica mediante strutture dati e algoritmi; l’implementazione permette allo scheduler di applicare la politica durante la selezione dei threads da eseguire tra quelli pronti.

92 Il sistema operativo Mac OS X

Anche se l’esecuzione concorrente ed il parallelismo sono obiettivi importanti per gli schedulers, specialmente con l’affermarsi dei sistemi multiprocessore, è abbastanza comune per un sistema operativo moderno supportare differenti politiche di scheduling, permettendo ai differenti carichi di lavoro di essere trattati in modo appropriato. In un’operazione tipica, lo scheduler del Mac OS X concede il processore ad ogni thread per un breve periodo di tempo, detto (timeslicing) quantum, dopo il quale prende in considerazione di passare ad un altro thread in attesa di esecuzione, con priorità maggiore o uguale. Notiamo che nel caso si presenti un thread con priorità maggiore, quello in esecuzione può essere interrotto prima dello scadere del proprio quantum per lasciare il processore al thread prioritario (preemptive scheduling).

3.2.1 Scheduler Operation Il Mac OS X è, fondamentalmente, un sistema a suddivisione di tempo e quindi i threads sono soggetti ad una politica di scheduling generalmente di tipo timeshare. In questo tipo di policy, il sistema tende a dare, senza garanzie, un equo tempo di processore ad ogni thread in competizione, dove per equità si intende fornire approssimativamente uguali quantità di risorse del processore in e per un tempo ragionevole. La figura 3-12 mostra un grafico (non esaustivo) delle chiamate, costituito da varie funzioni chiave che sono coinvolte nell’esecuzione e nello scheduling dei threads. I seguenti punti caratterizzano lo scheduling nel Mac OS X:

• Lo scheduler si occupa dei soli threads Mach. Di conseguenza non schedula nessuna altra entità di livello superiore.

• Lo scheduler non usa la conoscenza che due o più threads portino allo stesso task per selezionare tra loro. In teoria tale conoscenza potrebbe essere utilizzata per ottimizzare il cambio di contesto intra-task.

• Il Mach utilizza lo stesso scheduler sia per monoprocessore sia per multiprocessori. Nei fatti il Mach utilizza lo stesso kernel, in versione multiprocessore, senza tener conto del numero di processori presenti sulla macchina. Questo, ovviamente, crea dell’overhead su sistemi monoprocessore.

93 Il sistema operativo Mac OS X

Figura 3-12 Grafico delle chiamate di funzioni per l’esecuzione e lo scheduling di un thread

• Il kernel del Mac OS X supporta l’handoff scheduling, nel quale un thread può cedere direttamente il processore ad un altro thread senza coinvolgere completamente lo scheduler. Il meccanismo di scambio dei messaggi del kernel può utilizzare l’handoff scheduling durante il passaggio di un messaggio. Se un thread è in attesa di ricevere un messaggio, il thread mittente può cambiare

94 Il sistema operativo Mac OS X

direttamente sul thread ricevente. Il destinatario eredita effettivamente gli attributi di scheduling del mittente, incluso il residuo del quantum corrente del mittente. Una volta espirato il quantum, tuttavia, gli effetti di tale eredità scompaiono.

• Lo scheduler del Mac OS X supporta varie politiche di scheduling, inclusa una politica real-time “soft”, ma non fornisce una interfaccia per caricare policies personalizzate.

• Ogni processore ha il proprio, dedicato, idle thread che ricerca altri threads da eseguire.

Tabella 3-2 Priorità nello scheduler del Mac OS X Livelli Descrizione 0-10 Priorità più basse (aged, idle) e priorità abbassate (aged). La priorità minore (0) ha numerosi sinonimi, come MINPRI_USER, MINPRI, IDLEPRI (idle priority) e DEPRESSPRI (depress priority). 11-30 Priorità abbassate. 31 Priorità base di default (BASEPRI_DEFAULT) per i threads utente. La routine host_info() ritorna questo valore come user priority. 32-51 Priorità elevate, come quelle ottenibili attraverso task_policy_set(). Per esempio BASEPRI_BACKGROUND (46), BASEPRI_FOREGROUND (47) e BASEPRI_CONTROL (48) corrispondono alle priorità base dei tasks che sono stati designati come background, foreground e control rispettivamente. 52-63 Priorità elevate. Quando un nuovo task è creato la sua massima priorità è fissata da MAXPRI (63). 64-79 Priorità alte normalmente riservate al sistema. Gli estremi di tale range sono chiamati MINPRI_RESERVED (64) e MAXPRI_RESERVED (79). MINPRI_RESERVED è ritornato dalla routine host_info() come la server priority. 80-95 Priorità kernel-only. Le priorità 80, 81, 93 e 95 sono chiamate MINPRI_KERNEL, BASEPRI_KERNEL, BASEPRI_PREEMPT e MAXPRI_KERNEL rispettivamente. La routine host_info() ritorna MINPRI_KERNEL sia come kernel priority sia come system priority. 96-127 Priorità real-time ottenibili attraverso thread_policy_set(). Le priorità 96, 97 e 127 sono chiamate BASEPRI_REALTIME, BASEPRI_RTQUEUES e MAXPRI rispettivamente.

Lo scheduler del Mac OS X è basato sulla priorità, quindi la selezione dei threads da mettere in esecuzione tiene in conto della loro priorità. La tabella 3-2 mostra i vari ranges di priorità nel sottosistema di scheduling, dove tanto più alto è il valore numerico tanto

95 Il sistema operativo Mac OS X

maggiore è la priorità. Il flavor HOST_PRIORITY_INFO della routine Mach host_info() può essere utilizzato per recuperare i valori di varie priorità specifiche. Una struttura dati fondamentale mantenuta dallo scheduler è la run queue. Ognuna delle

strutture run_queue (figura 3-13) rappresenta una coda di priorità dei runnable threads e contiene un array di NRQS liste a doppio collegamento, una per ogni corrispondente

livello di priorità. Il membro highq è un hint che indica la probabile locazione del thread a priorità più alta. Ricordiamo inoltre che ogni processor set ha la propria run queue, ed ogni processore ha la propria local run queue.

Figura 3-13 La struttura run_queue // osfmk/kern/sched.h

#define NRQS 128 // 128 levels per run queue #define NRQBM (NRQS/32) // number of words per bitmap #define MAXPRI (NRQS-1) // maximum priorità possible #define MINPRI IDLEPRI // lowest legal priorità schedulable #define IDLEPRI 0 // idle thread priority #define DEPRESSPRI MINPRI // depress priority ... struct run_queue { int highq; // highest runnable queue int bitmap[NRQBM]; // run queue bitmap array int count; // number of threads total int urgency; // level of preemption urgency queue_head_t queues[NRQS]; // one for each priority };

Per bilanciare l’utilizzo del processore fra i vari threads, lo scheduler modifica le priorità dei threads tenendo conto del loro uso. Esistono svariate limitazioni e misure relative alle priorità associate ad ogni task e thread. Per comprenderle andiamo a riesaminare le

strutture task e thread ponendo l’attenzione alle parti riguardanti lo scheduling. Come mostrato nella figura 3-14, ogni thread ha una priorità base, tuttavia la scheduled priority è quella che lo scheduler esamina quando seleziona i threads da mandare in esecuzione80. La scheduled priority è calcolata sulla base priority con un offset derivato dal recente utilizzo del processore da parte del thread. La priorità base di default per i threads utente timesharing è 31, mentre la priorità minima del kernel è 80: di conseguenza i kernel threads sono sostanzialmente favoriti rispetto ai threads utente standard.

80 Questo è quanto accade ai threads in timesharing; quelli real-time sono trattati in maniera specifica dallo scheduler. 96 Il sistema operativo Mac OS X

Figura 3-14 Parti relative allo scheduling delle strutture task e thread // osfmk/kern/task.h

struct task { ... // task's role in the system // set to TASK_UNSPECIFIED during user task creation task_role_t role; // default base priority for threads created within this task // set to BASEPRI_DEFAULT during user task creation integer_t priority; // no thread in this task can have priority greater than this // set to MAXPRI_USER during user task creation integer_t max_priority; ... };

// osfmk/kern/thread.h struct thread { ... // scheduling mode bits include TH_MODE_REALTIME (time-constrained thread), // TH_MODE_TIMESHARE (uses standard timesharing scheduling), // TH_MODE_PREEMPT (can preempt kernel contexts), ... // // TH_MODE_TIMESHARE is set during user thread creation integer_t sched_mode; integer_t sched_pri; // scheduled (current) priority // base priority // set to parent_task->priority during user thread creation integer_t priority; // maximum base priority // set to parent_task->max_priority during user thread creation integer_t max_priority; // copy of parent task's base priority // set to parent_task->priority during user thread creation integer_t task_priority; // copy of task's base priority ... // task-relative importance // set to (self->priority - self->task_priority) during user thread creation integer_t importance; // parameters for time-constrained scheduling policy struct { ... } realtime; uint32_t current_quantum; // duration of current quantum ... // last scheduler tick // set to the global variable sched_tick during user thread creation natural_t sched_stamp; // timesharing processor usage // initialized to zero in the "template" thread natural_t sched_usage; // factor for converting usage to priority // set to the processor set's pri_shift value during user thread creation natural_t pri_shift; };

Man mano che un thread utilizza il processore, la sua priorità diminuisce e, poiché lo scheduler favorisce le priorità più alte, questo può condurre ad una situazione in cui un thread ha utilizzato talmente tanto tempo di processore che lo scheduler non è più disposto a concedergliene altro a causa della grande diminuzione di priorità. Lo schedulatore Mach si occupa di questo problema invecchiando (aging) l’uso del processore: in pratica

97 Il sistema operativo Mac OS X

“dimentica” esponenzialmente il passato utilizzo del processore da parte del thread andandone ad aumentare gradualmente la priorità. Tuttavia questo crea un altro problema: se il sistema ha un tale carico di lavoro che la maggior parte (o tutti) i threads ricevono un piccolo tempo di processore, allora la priorità di tutti questi threads aumenterà e la conseguente contesa deteriorerà la risposta del sistema. Per ovviare quest’altro problema, lo scheduler moltiplica l’utilizzo del processore da parte del thread per un fattore di conversione relativo al carico del sistema.

Figura 3-15 Calcolo della priorità timesharing di un thread // osfmk/kern/priority.c

#define do_priority_computation(thread, pri) \ do { \ (pri) = (thread->priority) /* start with base priority */ \ - ((thread)->sched_usage >> (thread)->pri_shift); \ if ((pri) < MINPRI_USER) \ (pri) = MINPRI_USER; \ else if ((pri) > MAXPRI_KERNEL) \ (pri) = MAXPRI_KERNEL; \ } while (FALSE);

In figura 3-15 vediamo com’è effettuato tale calcolo. Osserviamo che l’utilizzo di

processore del thread (thread→sched_usage), dopo essere stato ridotto da un fattore di conversione (thread→pri_shift), è sottratto alla sua priorità base (thread→priority) per restituire la scheduled priority. Andiamo ora ad esaminare com’è calcolato tale fattore di conversione81 e in che modo decade nel tempo l’utilizzo del processore. Il fattore di conversione consiste di due parti: una fissa basata sull’unità di tempo assoluto dipendente dalla macchina, e una dinamica

basata sul carico del sistema. La variabile globale sched_pri_shift contiene la parte fissa (computata nella fase di inizializzazione dello scheduler), mentre la parte dinamica è una entry di un array costante in cui l’indice indica il carico di sistema. In figura 3-16 vediamo un estratto di codice per il calcolo della parte dinamica del fattore di conversione.

81 La routine update_priority() [osfmk/kern/priority], invocata frequentemente nello scheduling, in determinate condizioni aggiorna il valore del fattore di conversione del thread ponendolo uguale a quello del processor set che contiene il thread. 98 Il sistema operativo Mac OS X

Figura 3-16 Calcolo del fattore di conversione utilizzo-priorità per priorità timeshare // osfmk/kern/sched_prim.c

int8_t sched_load_shifts[NRQS]; ... // called during scheduler initialization // initializes the array of load shift constants static void load_shift_init(void) { int8_t k, *p = sched_load_shifts; uint32_t i, j; *p++ = INT8_MIN; *p++ = 0; for (i = j = 2, k = 1; i < NRQS; ++k) { for (j <<= 1; i < j; ++i) *p++ = k; } }

// osfmk/kern/sched_average.c

void compute_averages(void) { ... register int nthreads, nshared; register uint32_t load_now = 0; ... if ((ncpus = pset->processor_count) > 0) { nthreads = pset->run_count - 1; // ignore current thread nshared = pset->share_count; // so many timeshared threads ... if (nshared > nthreads) nshared = nthreads; // current was timeshared! if (nshared > ncpus) { if (ncpus > 1) load_now = nshared / ncpus; else load_now = nshared; if (load_now > NRQS - 1) load_now = NRQS - 1; } pset->pri_shift = sched_pri_shift - sched_load_shifts[load_now]; } else { ... pset->pri_shift = INT8_MAX; // hardcoded to 127 ... } // compute other averages ... }

Lo scheduler invecchia l’uso del processore dei threads in maniera distribuita mediante svariate chiamate alla routine update_priority() [osfmk/kern/priority.c]. Tale routine comincia calcolando la differenza (ticks) tra lo scheduler tick corrente (sched_tick), che è incrementato periodicamente, e lo scheduler tick registrato del thread (thread->sched_stamp). Quest’ultimo è tenuto aggiornato aggiungendoci il valore ticks. In base a tale valore si possono verificare due situazioni: se ticks è maggiore o uguale rispetto a sched_decay_ticks (32), allora l’utilizzo del processore del thread è azzerato; se è minore, allora l’utilizzo è moltiplicato per (⅝)ticks. Ci sono fondamentalmente due ragioni per la scelta di ⅝ come fattore di decadimento esponenziale: fornisce un comportamento dello scheduling simile a quello di altri sistemi

99 Il sistema operativo Mac OS X

timesharing; la moltiplicazione con tale fattore può essere approssimata con operazioni binarie semplici. Consideriamo la moltiplicazione di un numero per ⅝, che può essere scritto come ½+⅛. La moltiplicazione di un numero per ½ si effettua con un shift a destra di 1, quella per ⅛ con uno shift a destra di 3 e sommando i risultati si ottiene il risultato cercato. Per facilitare il computo, il kernel ha un array statico con

sched_decay_ticks coppie di interi, dove la coppia di indice i contiene i valori shift per approssimare (⅝)i. Se il valore di ticks è nell’intervallo [0, 31], la coppia di indice ticks è usata in accordo alla seguente formula:

if (/* the pair’s second value is positive */) usage = (usage >> (first value)) + (usage >> abs(second value))); else usage = (usage >> (first value)) - (usage >> abs(second value))); Notiamo che non è sufficiente rendere un thread responsabile del decadimento dell’uso del processore. I threads con priorità basse potrebbero continuare a rimanere nella run queue senza avere possibilità di entrare in esecuzione a causa dei threads a priorità maggiore. In particolare, questi threads a bassa priorità sarebbero incapaci di aumentare la loro priorità abbattendo il loro processor usage. Di conseguenza lo scheduler utilizza un kernel thread

dedicato, il thread_update_scan(), per questo scopo. La routine consiste di due passi logici. Nel primo itera sulle run queues, comparando il valore sched_stamp dei threads timesharing con sched_tick. In questo passo sono raccolti in un array fino a THREAD_UPDATE_SIZE (128) threads candidati. Il secondo passo itera sugli elementi di tale array, chiamando update_priority() sui threads timesharing che soddisfano i seguenti criteri:

• Il thread non è sospeso e non ne è stato richiesto lo stop, cioè il bit TH_SUSP del suo stato non è settato.

• Il thread non è in coda di attesa, cioè il bit TH_WAIT del suo stato non è settato. • Il valore sched_stamp del thread non è ancora aggiornato con sched_tick.

3.2.2 Scheduling Policies Il Mac OS X supporta politiche di scheduling multiple denominate

THREAD_STANDARD_POLICY (timesharing), THREAD_EXTENDED_POLICY,

100 Il sistema operativo Mac OS X

THREAD_PRECEDENCE_POLICY e THREAD_TIME_CONSTRAINT_POLICY (realtime). Le routines Mach policy_get() e policy_set() possono essere utilizzate per recuperare e modificare, rispettivamente, la politica di scheduling di un

thread. Le equivalenti routines della Pthreads API sono pthread_getschedparam() e pthread_setschedparam(). Le informazioni sulla politica di scheduling posso anche essere specificate all’atto della creazione come attributi del pthread. Notiamo che la

Pthreads API utilizza diverse politiche denominate SCHED_FIFO (first in – first out), SCHED_RR (round robin) e SCHED_OTHER (system-specific policy)82. La THREAD_STANDARD_POLICY è la politica di scheduling standard ed è il default per i threads timesharing. Secondo tale politica, ai threads che eseguono delle computazioni long-running sono equamente assegnate le risorse di processore. Un conto dei threads timesharing è mantenuto per ogni processor set.

La THREAD_EXTENDED_POLICY è una versione estesa della politica standard. In questa politica, un hint booleano designa un thread come long-running (timesharing) o non-long- running (non-timesharing). Nel primo caso, la politica adottata è identica alla

THREAD_STANDARD_POLICY. Nel secondo caso il thread girerà ad una priorità fissata purché il suo utilizzo di processore non superi una soglia di sicurezza, nel qual caso lo scheduler lo retrocederà temporaneamente come thread timesharing attraverso un meccanismo a prova di errore.

La THREAD_PRECEDENCE_POLICY prevede un importance value (un signed integer) da associare ad un thread, permettendo così ai threads di un task di essere designati come più o meno importanti l’uno rispetto all’altro. A parità di tutti gli altri aspetti, il thread più importante del task è favorito rispetto a quelli meno importanti. Notiamo che questa politica può essere utilizzata in congiunzione con le altre.

La THREAD_TIME_CONSTRAINT_POLICY è la politica di scheduling real-time intesa per quei threads con costrizioni sulla propria esecuzione. Un thread specifica allo scheduler di avere bisogno di una certa frazione del tempo di processore, forse

82 Nel caso del Mac OS X la politica SCHED_OTHER è il timesharing. Notiamo inoltre che la Pthreads API non supporta specificamente una politica di scheduling realtime. 101 Il sistema operativo Mac OS X

periodicamente. Lo scheduler preferirà un thread real-time rispetto a tutti gli altri threads, a meno forse d’altri real-time. Tale policy può essere applicata ad un thread utilizzando la

routine thread_policy_set() con i seguenti parametri: tre interi espressi in unità di tempo assoluto (period, computation e constraint) e un booleano (preemptible). Un valore non nullo di period indica la periodicità nominale nella computazione, vale a dire il tempo tra due arrivi d’elaborazione consecutivi. Il valore di computation indica il tempo nominale necessario ad un periodo d’elaborazione. Il valore di constraint specifica il massimo quantitativo di tempo reale che può passare tra l’inizio di un periodo d’elaborazione alla fine della computazione. Notiamo che la differenza tra il valore di constraint e computation, che è sempre un valore non negativo, indica la latenza real-time. Infine, il parametro preemptible dichiara se la computazione può essere interrotta o no. Notiamo che la politica real-time non necessita privilegi speciali per essere usata, quindi è necessaria una certa prudenza poiché porta la priorità di un thread sopra quella di diversi kernel threads. Lo scheduler include un meccanismo fail-safe per i threads non-timesharing per i quali l’utilizzo del processore eccede una soglia pericolosa. Quando si esaurisce il quantum di un tale thread, esso viene retrocesso a thread timesharing e la sua priorità è impostata a

DEPRESSPRI. Tuttavia, nel caso di policy real-time, lo scheduler ricorda le precedenti necessità real-time del thread e, dopo un periodo di sicurezza per il rilascio, lo promuove

di nuovo a thread real-time e ne imposta la priorità a BASEPRI_RTQUEUES.83 Quando si utilizza la routine thread_policy_set() per cambiare la politica di scheduling, o per modificare i parametri della politica in uso, il kernel ricalcola la priorità del thread e i valori d’importanza, in conformità ai limiti di priorità minimi e massimi del thread (vedi figura seguente).

83 La massima unsafe computation è definita come il prodotto tra il quantum standard e la costante max_unsafe_quanta. Il valore di default di tale costante è MAX_UNSAFE_QUANTA, definito in osfmk/kern/sched_prim.c pari a 800. Un valore alternativo può essere ricavato attraverso l’argomento di boot- time unsafe. 102 Il sistema operativo Mac OS X

Figura 3-17 Ricalcolo della priorità di un thread in seguito al cambio di politica di scheduling // osfmk/kern/thread_policy.c

static void thread_recompute_priority(thread_t thread) { integer_t priority; if (thread->sched_mode & TH_MODE_REALTIME) priority = BASEPRI_RTQUEUES; // real-time else { if (thread->importance > MAXPRI) // very important thread priority = MAXPRI; else if (thread->importance < -MAXPRI) // very unimportant thread priority = -MAXPRI; else priority = thread->importance; priority += thread->task_priority; // add base priority if (priority > thread->max_priority) // clip to maximum allowed priority = thread->max_priority; else if (priority < MINPRI) // clip to minimum possible priority = MINPRI; } // set the base priority of the thread and reset its scheduled priority set_priority(thread, priority); }

Sappiamo che la routine task_policy_set() può essere utilizzata per definire la politica di scheduling associata ad un task. Un esempio di flavor di una task policy è

TASK_CATEGORY_POLICY, che informa il kernel del ruolo che il task ha all’interno del sistema operativo. Con tale flavor, la routine di cui sopra può essere utilizzata per designare il ruolo di un task. Esempi di ruoli di task nel Mac OS X sono i seguenti:

• TASK_UNSPECIFIED è il ruolo di default. • TASK_FOREGROUND_APPLICATION è progettato per una normale applicazione UI-based pensata per girare in foreground dal punto di vista dell’UI. L’assegnazione di questo ruolo ad un task fissa la sua priorità a

BASEPRI_FOREGROUND (vedi tabella 3-2). La priorità massima del task rimane invariata.

• TASK_BACKGROUND_APPLICATION è progettato per una normale applicazione UI-based pensata per girare in background dal punto di vista dell’UI. L’assegnazione di questo ruolo ad un task fissa la sua priorità a

BASEPRI_BACKGROUND (vedi tabella 3-2). La priorità massima del task rimane invariata.

• TASK_CONTROL_APPLICATION è un ruolo che può essere assegnato al più ad un task alla volta, su una base first-come first-serve. Esso designa il task come la

103 Il sistema operativo Mac OS X

applicazione di controllo UI-based. Il programma logwindow normalmente usa questa designazione. L’assegnazione di questo ruolo è un’azione privilegiata che

porta la priorità del task a BASEPRI_CONTROL senza modificare la massima priorità.

• TASK_GRAPHIC_SERVER può essere assegnato al programma WindowServer. Così come per TASK_CONTROL_APPLICATION può essere assegnato (con accesso privilegiato) ad un solo task per volta su base first-come first-serve. La

priorità del task è fissata a (MAXPRI_RESERVED-3) mentre la priorità massima a MAXPRI_RESERVED. L’uso di tale ruolo è opzionale per il sistema. Notiamo che i ruoli non sono ereditati attraverso i tasks, quindi ogni task nasce con il ruolo TASK_UNSPECIFIED.

104 Il sistema operativo Mac OS X

Capitolo 4 La memoria

La memoria, specificamente quella fisica, è una preziosa risorsa in un computer. Una caratteristica integrale dei moderni sistemi operativi è la memoria virtuale (VM, virtual memory). La tipica implementazione della VM fornisce l’illusione di un esteso e contiguo spazio d’indirizzi virtuali ad ogni programma, senza che il programmatore debba conoscerne i dettagli, come ad esempio quali parti del programma sono residenti nella memoria fisica (e dove) in ogni momento. La memoria virtuale è comunemente implementata attraverso il paging: uno spazio degli indirizzi è suddiviso in pagine (pages) di dimensione fissata. Quando è residente, ogni pagina virtuale è caricata in una particolare porzione, detta page frame, di memoria fisica. Oltre al nucleo del sottosistema VM del Mach, la gestione della memoria nel Mac OS X comprende diversi altri meccanismi, alcuni dei quali non strettamente facenti parte del

sottosistema della memoria virtuale ma in ogni caso strettamente correlati a questo. Nella figura 4-1 possiamo vedere tali componenti e le loro interazioni. Andiamo ad esaminarli brevemente, lasciando il loro approfondimento ai paragrafi successivi.

• Il sottosistema Mach VM è composto dal modulo pmap (physical map) dipendente dalla macchina, e da altri moduli indipendenti dalla macchina per la gestione delle strutture dati corrispondenti alle astrazioni come VM maps, VM objects, named entries e resident pages. Il kernel esporta diverse routine nello spazio utente come parte dell’API Mach VM.

105 Il sistema operativo Mac OS X

• Il kernel utilizza la struttura dati UPL (universal page list) per descrivere un ristretto gruppo di pagine fisiche. Le UPL contengono vari attributi delle pagine che descrivono. I sottosistemi del kernel (particolarmente il file system) utilizzano le UPLs durante la comunicazione con il sottosistema della memoria virtuale.

Figura 4-1 Il sottosistema della memoria del Mac OS X

• La UBC (unified buffer cache) è un insieme di pagine per il caching del contenuto di files e le porzioni anonime degli spazi di indirizzo dei task. L’esempio più comune di memoria anonima è quello della memoria allocata dinamicamente.

106 Il sistema operativo Mac OS X

• Il kernel include tre paginatori interni chiamati default (anonymous) pager, device pager e vnode84 pager. Questi si occupano delle operazioni di page-in e page-out nelle regioni di memoria. I pagers comunicano con il sottosistema Mach VM mediante le interfacce UPL e le interfacce derivate dal Mach pager. Il device pager, che gestisce la memoria delle periferiche, è implementato nella porzione I/O Kit del kernel. Nell’hardware a 64-bit, il device pager utilizza una parte del controllore della memoria, il DART (Device Address Resolution Table). Il DART, abilitato di default su hardware a 64-bit, mappa gli indirizzi dalla memoria a 64-bit allo spazio degli indirizzi (a 32-bit) delle periferiche PCI.

• Il page-out daemon è un gruppo di kernel threads che scrive porzioni dello spazio d’indirizzi del task nel disco come parte dell’operazione di paging nella memoria

virtuale. Esso esamina l’uso delle pagine residenti ed impiega uno schema LRU85- like per rimuovere le pagine che non sono state utilizzate per un certo tempo.

• Il programma utente dynamic_pager crea e distrugge i files di scambio per l’uso del kernel. Notiamo che questo programma non compie nessuna operazione di paging, come potrebbe invece far supporre il nome.

• Il daemon utente update invoca periodicamente la chiamata di sistema sync() per scaricare nel disco le caches del file system.

• Il sottosistema di rilevamento del TWS (task working set) mantiene i profili di comportamento di page-fault dei tasks in base all’applicazione. Quando

un’applicazione causa un page-fault, il kernel (o meglio il meccanismo di gestione del page-fault) consulta tale sottosistema per determinare per quali ulteriori pagine, se ce ne sono, deve essere fatto il page-in.

• Il kernel fornisce diversi meccanismi si allocazione di memoria, alcuni dei quali sono le sovrastrutture specifiche del sottosistema di altri. Tutti questi meccanismi infine utilizzano l’allocatore di pagine a livello kernel. Gli schemi di allocazione di

84 Un vnode (nodo virtuale) è un’astrazione di un oggetto filesystem indipendente dal file system. È analogo ad una classe base astratta da cui sono derivate le istanze specifiche dei vari file systems. Ogni directory o file attivo (dove “attivo” assumen connotazioni diverse a seconda del contesto) ha un vnode all’interno della memoria. 85 LRU sta per ‘least recently used’ ossia ‘usato meno di recente’. 107 Il sistema operativo Mac OS X

memoria dello spazio utente sono costruite sull’API del Mach VM.

• Il sottosistema Shared Memory Server è un servizio kernel che fornisce due regioni globali di memoria condivisa da 256MB: una read-only per il testo che parte

dall’indirizzo di memoria virtuale utente 0x9000_0000 ed è completamente condivisa tra i tasks; l’altra per i dati che parte dall’indirizzo di memoria virtuale

0xA000_0000 ed è condivisa copy-on-write. Il dynamic link editor (dyld) utilizza questo meccanismo per caricare le librerie condivise all’interno degli spazi d’indirizzo del task.

4.1 La memoria virtuale Andremo ora ad esaminare l’architettura della memoria virtuale del Mach, così com’è implementata nel kernel del Mac OS X. Il progetto della Mach VM comprende i seguenti aspetti notevoli:

• Una netta separazione tra la parte dipendente dalla macchina e quella indipendente. Solo quest’ultima ha informazioni relative alla memoria virtuale complete.

• Spazi d’indirizzo virtuale grandi e sparsi, uno per ogni task e completamente condiviso tra i threads del task.

• Integrazione tra gestione della memoria e comunicazione interprocesso. Il Mach fornisce delle interfacce basate su IPC per lavorare con gli spazi d’indirizzo del task. Queste interfacce sono specialmente flessibili nel permettere ad un task di

manipolare lo spazio d’indirizzo di un altro task.

• Operazioni di copia virtuale, ottimizzate attraverso gli algoritmi di copy-on-write (simmetrico o asimmetrico).

• Memoria flessibile che si ripartisce tra tasks collegati o indipendenti, con supporto

al COW, che è utile durante una fork() o durante i grandi trasferimenti IPC. • Files memory-mapped. • Vari tipi di backing store utilizzabili attraverso pagers multipli. Ogni spazio d’indirizzo di un task è rappresentato nel kernel mediante una mappa di indirizzi (VM map), che contiene una lista doubly linked di regioni di memoria ed una

108 Il sistema operativo Mac OS X struttura mappa fisica (pmap) machine-dependant. Quest’ultima gestisce le traduzioni degli indirizzi da virtuali a fisici. Ogni regione di memoria (VM map entry) rappresenta una serie contigua d’indirizzi di memoria, ognuno dei quali è attualmente mappato (valido) nel task. In ogni caso, ogni serie ha i propri attributi di protezione ed ereditarietà, quindi anche se un indirizzo è valido, il task potrebbe non essere capace di accederci per uno o più tipi di operazione. Le VM map entries, ordinate per indirizzo nella lista, hanno un VM object associato che contiene informazioni circa l’accesso alla memoria dalla sua sorgente.

Figura 4-2a Implementazione nel Mac OS X dell’architettura della Mach VM

109 Il sistema operativo Mac OS X

Un oggetto VM contiene una lista di pagine residenti (VM pages), ognuna delle quali identificata nediante il suo offset all’interno dell’oggetto. La memoria dei VM objects non è necessariamente residente nella memoria fisica. Un VM object è generalmente “sostenuto” (backed) da un memory object, che è semplicemente una porta Mach a cui il kernel può inviare messaggi al fine di recuperare i dati cercati. Il possessore di un oggetto di memoria è un memory manager (gestore di memoria) o pager (paginatore). La figura 4-2a mostra l’implementazione del Mac OS X dell’architettura Mach VM.

4.1.1 Task Address Spaces Ogni task ha uno spazio d’indirizzo virtuale che definisce un set di indirizzi virtuali a cui ogni thread interno al task può riferirsi. Un task a 32-bit ha uno spazio d’indirizzo virtuale di 4GB. Le ultime versioni del Mac OS X supportano task utente a 64-bit con spazio d’indirizzo virtuale da 51-bit, che corrisponde 2 PiB (Pebibyte) di memoria virtuale. Per un tipico task il suo virtual address space è “grande” nel senso che usa solo una parte della memoria virtuale disponibile: in ogni istante diversi subranges del spazio d’indirizzo del task può essere inutilizzato, portando alla tipica memoria virtuale sparsamente popolata. È comunque possibile che programmi con scopi specifici abbiano richieste di memoria virtuale eccedenti quella che uno spazio a 32-bit possa fornire.

4.1.2 VM Maps

Ogni virtual address space di task è descritto da una struttura dati VM map (struct vm_map [osfmk/vm/vm_map.h]). Il campo map della struttura task punta ad una struttura vm_map. La struttura task contiene anche informazioni utilizzate dai sottosistemi ‘task working set detection’ e ‘global shared memory’, che analizzeremo in seguito. Una VM map è una collezione di regioni di memoria dette VM entries; ognuna di queste è un set di pagine virtualmente contigue (un range virtuale) caratterizzato dalle stesse proprietà. La VM map punta ad una lista doubly linked ordinata di VM map entries. Ogni entry ha un indirizzo di inizio (start address) ed uno di fine (end address).

110 Il sistema operativo Mac OS X

Figura 4-2b Dettagli implementativi nel Mac OS X dell’architettura della Mach VM

4.1.3 VM Map Entries

Una VM map entry è rappresentata da una struttura dati struct vm_map_entry [osfmk/vm/vm_map.h]. Poiché ogni entry rappresenta un range di memoria virtuale attualmente mappata nel task, il kernel consulta la entry list svariate volte, particolarmente

durante l’allocazione di memoria. La funzione vm_map_lookup_entry() [osfmk/vm/vm_map.c] è utilizzata per cercare la VM map entry, se esistente, che

111 Il sistema operativo Mac OS X

contiene l’indirizzo specificato in una data una VM map86. Il kernel ha la possibilità, se necessario, di dividere (split) o riunire (merge) le pagine delle VM entries.

4.1.4 VM Objects La memoria di un task può avere diverse sorgenti. Per esempio, una libreria condivisa mappata lo spazio d’indirizzo del task rappresenta quella memoria la cui sorgente è il file della libreria condivisa. Abbiamo detto in precedenza che tutte le pagine di una singola

VM map entry ha la stessa sorgente: un VM object (struct vm_object [osfmk/vm/vm_object.h]) rappresenta tale sorgente, con la entry che rappresenta il ponte tra un object e una map. Un VM object è concettualmente un deposito contiguo di dati, alcuni dei quali possono essere tenuti (cached) nella memoria residente mentre il resto dei dati possono essere recuperati dalla corrispondente memoria secondaria (backing store). L’entità incaricata del trasferimento delle pagine tra memoria fisica e memoria

secondaria è chiamata pager, o in maniera più appropriata memory manager87. Un VM object contiene una lista delle sue pagine residenti, insieme alle informazioni per recuperare le pagine non residenti. Notiamo che le pagine residenti non sono condivise tra i vari oggetti, una data pagina esiste in esattamente un VM object. La lista in questione risulta particolarmente utile all’atto del rilascio di tutte le pagine associate ad un oggetto quando questo viene distrutto.

Una struttura dati vm_object contiene inoltre proprietà come le seguenti: • La dimensione dell’oggetto • Il numero di riferimenti all’oggetto • L’oggetto di memoria associato (pager) e l’offset nel pager • La porta di controllo dell’oggetto di memoria • Gli eventuali puntatori agli oggetti shadow e copy

86 L’algoritmo di ricerca è lineare e può iniziare o dalla testa della lista o da un punto specificato da un hint memorizzato durante un lookup precedente completato con successo. Questo hint è conservato all’interno della VM map, insieme ad un altro hint usato per determinare un indirizzo di spazio libero velocemente. Se l’indirizzo cercato non dovesse essere trovato, la funzione restituisce l’entry immediatamente precedente. 87 Anche se tendiamo ad usare i termini pager e memory manager come sinonimi, va ricordato che il secondo (oltre al paging) si occupa anche di mantenere la consistenza tra il contenuto della memoria secondaria e quello delle pagine residenti che corrispondono ad un oggetto di memoria virtuale. 112 Il sistema operativo Mac OS X

• La “strategia di copia” che il kernel dovrebbe utilizzare durante la copia dei dati del VM object

• Un flag indicante se l’oggetto è interno (e così è creato dal kernel e gestito dal pager di default)

• Un flag indicante se l’oggetto è temporaneo (e così non modificabile esternamente da un memory manager; le modifiche in memoria di un tale oggetto non sono riflesse indietro al memory manager)

• Un flag indicante se l’oggetto può persistere (in altre parole, se il kernel può mantenere in cache i dati dell’oggetto, insieme ai diritti all’oggetto di memoria associato) dopo che tutte le referenze di mappa d’indirizzo all’oggetto sono deallocato Come si può vedere nella figura 4-2b, un oggetto di memoria è implementato come una porta Mach della quale un pager detiene i diritti di ricezione. Quando il kernel ha bisogno che una pagina di un VM object sia passata dalla memoria secondaria a quella fisica, esso comunica con il pager associato attraverso la porta dell’oggetto di memoria. La porta di controllo dell’oggetto di memoria, di cui il kernel detiene i receive rights, è utilizzata per ricevere i dati dal pager. Con questa conoscenza, possiamo ridescrivere la figura completa come segue: una VM map “mappa” ogni regione valida di uno spazio d’indirizzo virtuale di un task in un offset all’interno di qualche oggetto di memoria. Per ogni oggetto di memoria usato in una VM map, il sottosistema della memoria virtuale mantiene un VM object. Un backing store è un luogo dove sono conservati i dati, quando non sono residenti. Può anche esserne la sorgente, ma non necessariamente. Nel caso di un file mappato in memoria, il backing store è il file stesso. Quando il kernel deve rimuovere dalla memoria fisica una pagina sostenuta da un file, può semplicemente scartare la pagina a meno che questa sia stata modificata mentre era residente: in questo caso la modifica può essere impegnata nel backing store. La memoria allocata dinamicamente è anonima in quanto non ha una named source con cui iniziare. Quando una pagina anonima è utilizzata per la prima volta, il Mach fornisce

113 Il sistema operativo Mac OS X

una pagina fisica riempita di zeri (da questo viene il nome alternativo di zero-filled memory). In particolare, non c’è backing store associato inizialmente alla memoria anonima. Quando il kernel deve rimuovere una tale pagina, utilizza lo spazio di scambio come backing store. La memoria anonima non persiste attraverso i riavvii di sistema. I corrispondenti VM objects, creati dal kernel, sono anche chiamati oggetti interni. Quando alloca memoria anonima, il kernel controlla se un’esistente VM entry può essere estesa in modo da non dover creare una nuova entry ed un nuovo oggetto.

4.1.5 Pagers Un pager gestisce, oltre alle pagine, anche i memory objects; di questi possiede la porta che è usata dai clients del pager come interfaccia. Un oggetto di memoria può essere visto come un incapsulamento object-oriented di memoria che implementa i metodi come lettura e scrittura. Un memory object rappresenta lo stato non-residente del sottostante backing storage, dove lo stato non-residente è essenzialmente la memoria secondaria che il kernel mette in cache nella memoria primaria (fisica). Come abbiamo visto nella figura 4-1, il Mac OS X prevede tre paginatori in-kernel:

• Il default pager, che trasferisce i dati tra la memoria fisica e lo spazio di swap • Il vnode pager, che trasferisce i dati tra la memoria fisica ed i files • Il device pager, che è usato per il mapping della memoria special-purpose Un paginatore può occuparsi di qualsiasi numero di memory objects, ognuno dei quali rappresenta un range di pagine che il pager gestisce. L’address space di un task può avere qualsiasi numero qualsiasi numero di pagers che gestiscono sue parti separate. Un paginatore non è direttamente coinvolto nelle policies di paginazione, non può modificare l’algoritmo di sostituzione delle pagine kernel (al di là della modifica degli attributi del memory object). Il termine external memory manager (o external pager) può essere interpretato in due modi. Nel primo caso, si riferisce a qualsiasi pager che non sia quello di default, nello specifico uno che gestisce la memoria la cui sorgente è esterna al kernel. La memoria anonima corrisponde a oggetti interni, mentre files memory-mapped corrispondono ad

114 Il sistema operativo Mac OS X oggetti esterni. In questo senso, il vnode pager può essere definito un paginatore esterno. Questa è l’accezione a cui noi faremo riferimento. L’altra interpretazione si riferisce a dove sono implementati i pager. Se definiamo un pager in-kernel come un paginatore interno, allora un paginatore esterno sarà implementato come un task utente specializzato. Considerato che un memory object rappresenta una sorgente di dati, il pager del memory object è il provider ed il manager di questi dati. Quando una porzione di memoria rappresentata da un oggetto di memoria è utilizzata da un task cliente, ci sono tre parti primarie coinvolte: il pager, il kernel ed il task cliente. Un task utilizza vm_map(), direttamente o indirettamente, per mappare una parte (o tutta) la memoria dei memory objects nell’address space. Per fare questo, il chiamante deve avere i send rights sulla porta Mach che rappresente l’oggetto di memoria. Il pager possiede tale porta e può quindi fornire i diritti ad altri. Un paginatore può pubblicizzare una porta di servizio alla quale i clients possono mandare messaggi per ottenere oggetti di memoria. I tre paginatori del Mac OS X hanno porte definite direttamente nel codice (hardcoded). Quando è necessario comunicare con un pager, si determina quale chiamare in base al valore del memory object passato, dato che il valore deve corrispondere ad uno dei pagers conosciuti. L’operazione di un pager nel kernel del Mac OS X utilizza una combinazione di: un subset dell’interfaccia del pager Mach originale, le UPLs (universal page lists) e l’UBC (unified buffer cache). Notiamo che il kernel fornisce implicitamente l’oggetto di memoria per un pager interno, il task chiamante non deve acquisire i send rights direttamente. Il paging nel Mach può essere sommariamente descritto come segue: un task cliente ottiene la porta di un oggetto di memoria direttamente o indirettamente da un gestore di memoria. Richiede il kernel chiamando vm_map() per mappare il memory object nel suo virtual address space. In seguito, quando il task cerca di accedere (in lettura o scrittura) per la prima volta ad una pagina nella memoria appena mappata, si verifica un errore di page- not-resident. Nella gestione del page fault, il kernel comunica con il gestore di memoria attraverso un messaggio di richiesta dei dati mancanti e quest’ultimo li va a prendere dal backing store che sta gestendo. Gli altri tipi di page faults sono gestiti appropriatamente

115 Il sistema operativo Mac OS X con il kernel che chiama il memory manager e questo che gli risponde in modo asincrono. Questo è il modo in cui il kernel usa la memoria fisica come una cache per i contenuti dei vari oggetti di memoria. Quando il kernel deve rimuovere delle pagine residenti, può (secondo la natura del mapping) spedire le pagine “sporche” (ossia quelle modificate mentre residenti) al memory manager. Quando un task cliente è fatto utilizzando un range di memoria mappato, esso può chiamare vm_deallocate() per rimuovere il range. Quando tutte le mappature di un oggetto di memoria sono andate, l’oggetto è terminato.

Figura 4-3 L’interfaccia del pager Mach nel Mac OS X

116 Il sistema operativo Mac OS X

La figura 4-3 mostra diversi messaggi88 che fanno parte del dialogo tra un gestore della memoria ed il kernel. Quando un oggetto di memoria è mappato per la prima volta, il kernel deve notificare al pager che sta utilizzando l’oggetto. Questa notifica avviene mediante la chiamata a kern_return_t memory_object_init(memory_object_t memory_object, memory_object_control_t memory_control, memory_object_cluster_size_t memory_object_page_size); L’argomento memory_object è la porta che rappresenta l’oggetto in questione. L’argomento memory_control è la porta, di cui il kernel detiene i send rights, che il pager usa per mandare messaggi al kernel (per questo è detta anche pager reply port). Consideriamo l’esempio specifico del paginatore vnode del Mac OS X. Quando la routine

memory_object_init() determina (usando la porta hardcoded del vnode pager) che l’oggetto di memoria passatogli corrisponde al vnode pager, richiama la funzione

vnode_pager_init() [osfmk/vm/bsd_vm.c]. Questa non impianta il vnode pager, che è già attivo dal momento della creazione del vnode, ma chiama a sua volta la

kern_return_t memory_object_change_atributes(memory_object_control_t control, memory_object_flavor_t flavor, memory_object_info_t attributes, mach_msg_type_number_t count); Il kernel conserva gli attributi degli oggetti mappati, in particolare cacheability e copy strategy. L’attributo cacheability specifica se il kernel può mettere in cache l’oggetto (a patto che ci sia abbastanza memoria) anche dopo che tutti gli utenti dell’oggetto sono andati. Se un oggetto è marcato come non cacheable, non sarà conservato quando non in uso: il kernel ritornerà le pagine sporche al pager, richiederà le pagine pulite e informerà il pager che l’oggetto non è più in uso. L’attributo copy strategy specifica in che modo le pagine dell’oggetto di memoria sono copiate. Alcuni strategie di copia valide sono:

• MEMORY_OBJECT_COPY_NONE – Le pagine del pager possono essere copiate immediatamente, senza ottimimizzazione COW dal kernel.

• MEMORY_OBJECT_COPY_CALL – Se il kernel deve copiare una qualsiasi pagina del pager, deve chiamare il pager.

• MEMORY_OBJECT_COPY_DELAY – Il pager promette di non modificare

88 Nel Mac OS X, i “messaggi” sono semplicemente delle chiamate di funzione, non dei messaggi IPC. 117 Il sistema operativo Mac OS X

esternamente alcun dato cached dal kernel, in modo che questo sia libero di utilizzare una strategia copy-on-write ottimizzata (asymmetric COW).

• MEMORY_OBJECT_COPY_TEMPORARY – Questa strategia funziona come la precedente, con l’aggiunta che il pager non è interessato a vedere alcun cambiamento dal kernel.

• MEMORY_OBJECT_COPY_SIMMETRIC – Questa strategia funziona come la precedente, con l’aggiunta che il memory object non è multi-mappato (symmetric COW). Gli attributi possono essere recuperati attraverso la routine

kern_return_t memory_object_get_attributes(memory_object_control_t control, memory_object_flavor_t flavor, memory_object_info_t attributes, mach_msg_type_number_t *count); Quando un task cliente accede ad una pagina di un memory object che non è residente, si verifica un errore di pagina. Il kernel localizza il VM object appropriato e richiama la

funzione memory_object_data_request(). Il pager fornisce tipicamente i dati prelevandoli dal backing store.

kern_return_t memory_object_data_request(memory_object_t memory_object, memory_object_offset_t offset, memory_object_cluster_size_t length, vm_prot_t desired_access); Nel Mach, il pager risponde alla richiesta dati mandando un risposta asincrona al kernel89.

Nel Mac OS X, la routine memory_object_data_request() chiama esplicitamente uno dei tre paginatori. Nel caso del vnode pager, il kernel chiama

vnode_pager_data_request() [osfmk/vm/bsd_vm.c], che invoca a sua volta vnode_page_cluster_read() [osfmk/vm/bsd_vm.c]. Quest’ultima causa la paginazione dei dati attraverso la chiamata alla routine vnode_pagein() [bsd/vm/vnode_pager.c], che infine chiama l’operazione di page-in specifica del

89 Il pager spedisce il messaggio memory_object_data_supply o memory_object_data_provided (a seconda della versione del Mach) alla porta di controllo del memory object. Il pager può anche rispondere con un messaggio memory_object_data_unavaible o memory_object_data_error. Il primo indica che, nonostante il range all’interno del memory object sia valido, non ci sono ancora dati. Questo messaggio notifica al kernel di ritornare pagine zero-filled per il range. Anche se il pager stesso potrebbe creare tali pagine (mediante memory_object_data_supply), il codice del kernel è probabilmente ottimizzato. Se un errore di paging (ad esempio un settore disco difettoso) causa un mancato recupero dei dati da parte del pager, questo risponde con un messaggio memory_object_data_error. 118 Il sistema operativo Mac OS X file system. Quando il kernel ha bisogno di reclamare memoria e ci sono pagine sporche per un oggetto di memoria, il kernel può mandare tali pagine al pager attraverso memory_object_data_return. Nel Mac OS X questo compito è svolto dal daemon di page-out del kernel.

kern_return_t memory_object_data_return(memory_object_t memory_object, memory_object_offset_t offset, vm_size_t size, memory_object_offset_t *resid_offset, int *io_error, boolean_t dirty, boolean_t kernel_copy, int upl_flags); Non c’è una risposta esplicita a questo messaggio, il pager semplicemente dealloca le pagine dal suo address space in modo che il kernel possa utilizzare la memoria fisica per altri scopi. Nel Mac OS X, per il vnode pager, la memory_object_data_return chiama vnode_pager_data_return() [osfmk/vm/bsd_vm.c], che a sua volta chiama vnode_page_cluster_write() [osfmk/vm/bsd_vm.c]. Quest’ultima causa il page-out dei dati attraverso la chiamata a vnode_pageout() [bsd/vm/vnode_pager.c], che infine chiama l’operazione di page-out specifica del file system.

Il paginatore usa memory_object_lock_request() per controllare l’uso dei dati (residenti) associati con l’oggetto di memoria. I dati sono specificati come il numero di bytes (argomento size) all’indirizzo relativo (argomento offset) all’interno del memory object. Tale funzione, dopo aver verificato la coerenza dei suoi parametri, chiama vm_object_update() [osfmk/vm/memory_object.c] sul VM object associato.

kern_return_t memory_object_lock_request(memory_object_control_t control, memory_object_offset_t offset, memory_object_size_t size, memory_object_offset_t *resid_offset, int *io_errno, memory_object_return_t should_return, int flags, vm_prot_t prot); L’argomento should_return è utilizzato per specificare i dati da ritornare, all’occorrenza, al gestore di memoria e può assumere i seguenti valori:

• MEMORY_OBJECT_RETURN_NONE – non ritorna nessuna pagina

119 Il sistema operativo Mac OS X

• MEMORY_OBJECT_RETURN_DIRTY – ritorna solo le pagine sporche • MEMORY_OBJECT_RETURN_ALL – ritorna sia le pagine sporche sia quelle preziose

• MEMORY_OBJECT_RETURN_EVERYTHING – ritorna tutte le pagine residenti L’argomento flags specifica l’eventuale operazione da eseguire sui dati. Le operazioni valide sono MEMORY_OBJECT_DATA_SYNC, MEMORY_OBJECT_COPY_SYNC, MEMORY_OBJECT_IO_SYNC o anche MEMORY_OBJECT_DATA_NO_CHANGE, MEMORY_OBJECT_DATA_FLUSH e MEMORY_OBJECT_DATA_PURGE. Notiamo che la combinazione di should_return e flags determina il destino dei dati: per esempio se should_return è MEMORY_OBJECT_RETURN_NONE e flags è MEMORY_OBJECT_DATA_FLUSH, allora le pagine residenti saranno scartate. L’argomento prot è utilizzato per restringere l’accesso alla memoria specificata. Il suo valore specifica l’accesso che deve essere disabilitato; il valore speciale

VM_PROT_NO_CHANGE indica che non si desidera nessuna modifica della protezione. Il kernel utilizza memory_object_terminate() per notificare al pager che l’oggetto non è più in uso. Il pager utilizza memory_object_destroy() per notificare al kernel di chiudere un oggetto di memoria anche se ci sono ancora riferimenti al VM object

associato. Questo comporta la chiamata a vm_object_destroy() [osfmk/vm/vm_object.c]. nel Mac OS X, tale funzione è chiamata a causa di vclean() [bsd/vfs/vfs_subr.c] che ripulisce un vnode quando richiesto.

kern_return_t memory_object_terminate(memory_object_t memory_object);

kern_return_t memory_object_destroy(memory_object_control_t control, kern_return_t reason);

4.1.6 Copy-on-Write La copy-on-write (COW) è una tecnica d’ottimizzazione della copia di pagine fisiche. Consiste nel mettere in condivisione le pagine fisiche tra le due parti coinvolte nella copia fino al momento in cui una di queste non cerca di scrivere su una di tali pagine. A questo punto viene creata una copia delle sole pagine modificate che viene assegnata al

120 Il sistema operativo Mac OS X

richiedente la scrittura. Riesamindo le figure 4-2, possiamo notare che ci sono due VM entries che puntano allo stesso VM object. Nella figura 4-4, invece, possiamo vedere lo schema di come il Mach implementa il symmetric COW90.

Figura 4-4 Copy-on-Write simmetrico con l’uso degli oggetti shadow

In un’operazione di COW simmetrico sia la sorgente sia la destinazione hanno il bit

needs_copy settato ed entrambe le entries puntano allo stesso oggetto VM che incrementa il proprio ref_count. In questo modo entrami i task accedono alle stesse

90 Lo schema è simmetrico, poiché il suo comportamento non dipende da quale task, sorgente o destinazione, modifica una pagina condivisa. 121 Il sistema operativo Mac OS X pagine fisiche durante le operazioni di lettura. Visto che tutte le pagine del VM object sono protette dalla scrittura, quando uno dei task prova a scrivere su una di queste avviene un errore di protezione. Il kernel non modifica il VM object originale, ma ne crea uno nuovo (uno shadow object contenente una copia delle faulting pages) dandolo al task che ha modificato le pagine. Le altre pagine, comprese le versioni originali delle pagine modificate, rimangono nel VM object originale, in cui il bit needs_copy rimane settato. Nella figura 4-4, quando il task destinazione accede ad una pagina (precendetemente condivisa mediante COW) che è stata già modificata, il kernel troverà tale pagina nell’oggetto shadow. Per trovare le altre pagine il kernel dovrà seguire il link all’oggetto originale dove sono memorizzate. L’utilizzo ripetuto delle operazioni COW possono portare alla copia shadow di un oggetto shadow, creando così una shadow chain. È importante notare che quando si crea uno shadow object durante una COW simmetrica, nessun gestore di memoria è coinvolto. Il kernel utilizza lo spazio di swap come backing store, ed il paginatore di default come memory manager, quando ha bisogno di effettuare il page-out di memoria anonima. Rimane comunque un problema quando un gestore di memoria esterno, come ad esempio il vnode pager nel caso di un file memory-mapped, restituisce il VM object originale. Il kernel non può modificare il VM object, poiché questo disconnetterebbe il file mapping. Siccome la modifica in una COW simmetrica è visibile solo dagli oggetti shadow, l’oggetto originale (che è collegato al memory manager) non vedrà mai tali modifiche. Il Mach risolve tale problema mediante l’uso dell’algoritmo asymmetric copy-on-write, in cui la sorgente mantiene il VM object originale ed il kernel crea un nuovo oggetto per la destinazione. L’algoritmo asimmetrico (vedi figura 4-5) funziona nel seguente modo:

• Quando si esegue un’operazione di copia, crea un nuovo oggetto (copy object) per la destinazione.

• Fa puntare il campo shadow del copy object all’oggetto originale. • Fa puntare il campo copy dell’oggetto originale al copy object.

122 Il sistema operativo Mac OS X

• Marca il copy object come copy-on-write (l’oggetto originale non lo è). • Ogni volta che una pagina sta per essere modificata nel source mapping, la copia prima in una nuova pagina e poi la inserisce nel copy object.

Figura 4-5 Copy-on-Write asimmetrico con l’uso degli oggetti copy

4.1.7 The Physical Map (Pmap)

Una VM map punta anche ad una struttura dati pmap (struct pmap [osfmk/ppc/pmap.h]), che descrive la traduzione degli indirizzi da virtuali a fisici definita per l’hardware. Lo strato pmap del Mach incapsula il codice VM dipendente dalla macchina (in particolare quello per la gestione dell’MMU e delle caches) ed esporta le funzioni generiche per l’utilizzo dello strato indipendente dalla macchina.

Il kernel del Mac OS X contiene altro codice (osfmk/ppc/mappings.c) oltre al modulo pmap per il mappings virtual-to-physical nel PowerPC. Tale codice funge da ponte tra lo strato pmap e l’hardware sottostante, che è contrario all’incapsulazione tradizionale del Mach. Per comprendere quale ruolo gioca lo strato pmap nel sistema, analizziamo qualche

esempio di funzioni dell’interfaccia pmap. La funzione pmap_map(), richiamata durante la fase di bootstrapping, mappa il virtual address range che parte da va nel physical address range spa attraverso epa, con il valore di protezione prot indipendente dalla

123 Il sistema operativo Mac OS X macchina.

vm_offset_t pmap_map(vm_offset_t va, vm_offset_t spa, vm_offset_t epa, vm_prot_t prot); La funzione pmap_create() crea e restituisce una mappa fisica, recuperandola dalla lista delle pmaps libere o allocandola da zero. Tale funzione è chiamata durante la fase di creazione del task, indipendentemente dal fatto che il figlio stia ereditando la memoria del padre o meno. Se non c’è eredità di memoria, viene creato un nuovo spazio d’indirizzo per il task figlio; altrimenti ogni singola VM entry nel task padre è esaminata per vedere se deve essere condivisa, copiata o non ereditata affatto. Oltre alla lista delle pmap libere

(free_pmap_list), il kernel mantiene anche le seguenti strutture dati: • Una lista delle pmaps in uso (ancorata da kernel_pmap). • Una lista degli indirizzi fisici delle pmaps in uso (ancorata da

kernel_pmap_phys). • Un puntatore ad un cursore pmap (cursor_pmap), che il kernel usa come punto di partenza durante la ricerca di pmaps libere.

pmap_t pmap_create(vm_map_size_t size); La funzione pmap_destroy(), chiamata quando viene distrutta una VM map, rimuove un riferimento alla pmap indicata. Quando il conteggio dei riferimenti arriva a zero, la pmap è aggiunta alla lista delle pmaps libere. La funzione pmap_reference(), invece, incrementa di uno il conteggio dei riferimenti della pmap indicata.

void pmap_destroy(pmap_t pmap);

void pmap_reference(pmap_t pmap); La funzione pmap_enter() crea una traslazione per l’indirizzo virtuale va alla pagina fisica numero pa nella pmap specificata con la protezione prot. L’argomento flags può essere usato per specificare attributi particolari per il mapping.

void pmap_enter(pmap_t pmap, vm_map_offset_t va, ppnum_t pa, vm_prot_t prot, unsigned int flags, __unused boolean_t wired); La funzione pmap_remove() demappa tutti gli indirizzi virtuali nel virtual address range determinato dalla pmap specificata e l’intervallo [sva, eva).

void pmap_remove(pmap_t pmap, addr64_t sva, addr64_t eva);

124 Il sistema operativo Mac OS X

La funzione pmap_page_protect() riduce i permessi di tutte le mappature per una data pagina. In particolare, se prot è VM_PROT_NONE, questa funzione rimuove tutte le mappature alla pagina. void pmap_page_protect(ppnum_t pa, vm_prot_t prot);

La funzione pmap_protect() cambia la protezione su tutti gli indirizzi virtuali del virtual

address range determinato dalla pmap specificata e l’intervallo [sva, eva). Se prot è VM_PROT_NONE, la funzione pmap_remove() è chiamata sul virtual address range.

void pmap_protect(pmap_t pmap, vm_map_offset_t sva, vm_map_offset_t eva, vm_prot_t prot); La funzione pmap_switch() commuta ad una nuova pmap, ossia cambia ad un nuovo spazio d’indirizzo. È chiamata durante il cambio di contesto di thread, a meno che i due threads non appartengano allo stesso task e quindi condividano lo stesso spazio d’indirizzo.

void pmap_switch(pmap_t pmap); La funzione pmap_clear_modify() azzera il dirty bit per una pagina indipendente dalla macchina che parte dall’indirizzo fisico indicato. La funzione

pmap_is_modified() controlla se la pagina fisica specificata è stata modificata dall’ultima chiamata a pmap_clear_modify(). Le funzioni pmap_clear_reference() e pmap_is referenced() funzionano in modo simile sul referenced bit.

void pmap_clear_modifiy(ppnum_t pa); boolean_t pmap_is_modified(register ppnum_t pa); void pmap_clear_reference(ppnum_t pa); boolean_t pmap_is_referenced(ppnum_t pa);

4.2 La memoria residente Il Mach divide uno spazio d’indirizzi in pagine, con la dimensione della pagina generalmente pari alla dimensione della pagina hardware nativa91. Il progetto del Mach

91 È possibile che l’hardware nativo supporti diverse dimensioni di pagina: ad esempio, il PowerPC 970FX supporta pagine da 4KB e 16MB. La variabile kernel vm_page_shift contiene il numero di bits da shiftare a destra per convertire un byte address in un numero di pagina. La variabile di librearia vm_page_size contiene la dimensione di pagina usata dal Mach. 125 Il sistema operativo Mac OS X

permette comunque l’utilizzo di una pagina virtuale di dimensioni maggiori, utilizzando più pagine hardware fisicamente contigue. Visto che la memoria visibile al programmatore è indirizzabile per byte, le primitive Mach per la memoria virtuale operano solo su pagine. In effetti il Mach allinea internamente gli offset di memoria nelle pagine ed arrotonda la dimensione dei memory range al più vicino limite di pagina. Inoltre l’applicazione da parte del kernl della protezione della memoria è a livello di pagina. Le porzioni valide di un address space corrispondono a pagine virtuali valide. In funzione dello schema d’utilizzo della memoria utilizzato nel programma (insieme con altri fattori), la memoria virtuale può essere cached nella memoria fisica attraverso le pagine residenti92.

Figura 4-6 La struttura di una pagina residente

Una struttura di pagina residente (struct vm_page [osfmk/vm/vm_page.h]) corrisponde ad una pagina di memoria fisica, e viceversa. La struttura contiene un puntatore al VM object associato /e l’offset in esso) insieme alle informazioni indicanti se la pagina è riferita, se è stata modificata, se è criptata e così via. La figura 4-6 mostra una

visione d’insieme di come la struttura vm_page è connessa alle altre strutture dati. Notiamo che la struttura risiede in svariate liste contemporaneamente. Il kernel mantiene una tabella hash delle pagine residenti, detta VP table (virtual-to-

92 Il trasferimento della memoria virtuale può essere completo, parziale o nullo. 126 Il sistema operativo Mac OS X

physical), utilizzata per la ricerca di una pagina residente data la coppia {VM object, offset}. Riportiamo di seguito qualcuna delle funzioni usate per manipolare la VP table.

vm_page_t vm_page_lookup(vm_object_t object, vm_object_offset_t offset); void vm_page_insert(vm_page_t mem, vm_object_t object, vm_object_offset_t offset); void vm_page_remove(vm_page_t mem); Notiamo che la funzione di ricerca utilizza un hint93 memorizzato nel campo memq_hint del VM object. Prima di esaminare la tabella di hash per la data coppia oggetto/offset, la funzione vm_page_lookup() [osfmk/vm/vm_resident.c] esamina la pagina residente specificata dall’hint e, se necessario, anche la precedente e la successiva. Il kernel mantiene un contatore che è incrementato per ogni ricerca (basata sull’hint) che abbia esito positivo. Una pagina residente paginabile risiede in una delle seguenti tre code di paging mediante

il campo pageq della struttura vm_page. • La free queue (vm_page_queue_free) contiene le pagine libere immediatamente disponibili all’allocazione. Una pagina di tale coda non ha mappature e non contiene dati utili. Quando il kernel ha bisogno di pagine vuote, ad esempio durante un page fault o l’allocazione di memoria del kernel, le prende da questa coda.

• La inactive queue (vm_page_queue_inactive) contiene le pagine che non sono riferite in nessuna pmap ma hanno ancora una mappatura di pagina oggetto/offset. Una pagina di tale coda può essere sporca. Quando il kernel deve liberare della memoria, rimuove delle pagine dal questa lista. Esiste una coda della

memoria inattiva separata per la memoria anonima (vm_page_queue_zf), che permette al daemon di page-out di assegnare una maggiore affinità alle pagine di memoria anonima. Si tratta di una lista FIFO (first-in first-out).

• La active queue (vm_page_queue_active) contiene le pagine che sono riferite in almeno una pmap. Anche questa è una lista FIFO ed ha un ordinamento simile all’LRU. Poiché la memoria fisica è una risorsa limitata, il kernel deve continuamente decidere

93 Esiste anche una versione della funzione lookup che non utilizza l’hint del VM object: tale versione è utilizzata dal sottosistema task-working-set-detection. 127 Il sistema operativo Mac OS X

quali pagine devono rimanere residenti e quali devono essere rimosse. La politica di rimpiazzo utilizzata è chiamata FIFO with Second Chance, che approssima il comportamento dell’LRU. Un obiettivo specifico del page replacement è di mantenere l’equilibrio tra le liste active e inactive. La lista active dovrebbe idealmente contenere solo i working set di tutti i programmi. Il kernel gestisce le tre code fondamentali summenzionate con una serie di parametri di page-out che specificano i confini di paging ed altri vincoli. La gestione della coda di pagine include le seguenti operazioni specifiche:

• Spostare le pagine dall’inizio dell’active queue all’inactive queue. • Pulire le pagine sporche dell’inactive queue. • Spostare le pagine pulite dall’inactive queue alla free queue. Poiché l’active queue è gestita in modalità FIFO, la pagina più vecchia è la prima ad essere rimossa. Se una pagina inattiva è riferita viene riportata nella coda attiva, perciò le pagine nella coda inattiva sono idonee ad una seconda opportunità di essere riferite94. La pulitura delle pagine sporche è effettuata dal deamon di page-out, che consiste dei seguenti kernel threads:

• vm_pageout_iothread_internal() [osfmk/vm/vm_pageout.c] • vm_pageout_iothread_external() [osfmk/vm/vm_pageout.c] • vm_pageout_garbage_collect() [osfmk/vm/vm_pageout.c] Entrambi i threads ‘internal’ ed ‘external’ richiamano la stessa continuazione

(vm_pageout_iothread_continue() [osfmk/vm/vm_pageout.c]), ma usano differenti code (“laundry”) di page-out: vm_pageout_queue_internal e vm_pageout_queue_external rispettivamente. La funzione di continuazione compie il lavaggio della laundry queue specificata, richiamando se necessario la routine

memory_object_data_return() per mandare i dati all’appropriato pager. Il thread vm_pageout_garbage_collect() libera i kernel stacks in eccesso e possibilmente attiva la garbage collection nel Mach. Il daemon di page-out controlla inoltre il rateo al

94 Se una pagina è riferita abbastanza frequentemente, gli sarà impedito lo spostamento nella coda libera in modo da non doverla reclamare. 128 Il sistema operativo Mac OS X

quale le pagine sono spedite ai paginatori; in particolare, la costante VM_PAGE_LAUNDRY_MAX (16) limita il numero massimo di pag-out in sospeso per il default pager. Quando il laundry count supera tale limite, il daemon di page-out si sospende per permettere al default pager di raggiungerlo.

4.2.1 Page Faults Un page fault è il risultato del tentativo di un task di accedere a dati contenuti in una pagina che necessita l’intervento del kernel prima di poter essere utilizzata dal task. Ci possono essere svariate cause per una page fault, tra le quali:

• invalid access – l’indirizzo non è mappato nell’address space del task. Questo

comporta una EXC_BAD_ACCES eccezione Mach con il codice d’eccezione specifico KERN_INVALID_ADDRESS. Tale exception è normalmente tradotta dal kernel nel segnale SIGSEGV. • nonresident page – la pagina virtuale non è inserita nella pmap del task. Se la pagina non è effettivamente nella memoria fisica ed i dati devono essere letti (paged in) dalla memoria secondaria, si dice che si è verificato un hard page fault. Il kernel contatta il pager che gestisce la pagina richiesta che a sua volta accede al backing store associato. Se, incede, i dati sono presenti nella cache si ha un soft page fault. In questo caso, la pagina deve trovarsi ancora in memoria e le appropriate traduzioni di pagina devono ancora essere allestite.

• protection violation – la pagina ha diritti d’accesso oltre la possibilità del task. Se la violazione della protezione è correggibile, il kernel gestirà l’errore in modo trasparente; altrimenti, l’eccezione sarà comunicata al task (normalmente mediante

un segnale SIGBUS). Un esempio di violazione correggibile è quando un task prova ad accedere in scrittura ad una pagina marcata read-only in seguito ad una operazione copy-on-write.

Il gestore di page-fault è implementato in osfmk/vm/vm_fault.c, con vm_fault() come punto d’ingresso principale. Andiamo ad esaminare i passi che compongono la gestione di un tipico page-fault.

129 Il sistema operativo Mac OS X

Un page fault sul PowerPC corrisponde ad una data access exception. Per gestire

l’eccezione, il kernel chiama trap() [osfmk/ppc/trap.c] con il codice d’interruzione impostato su T_DATA_ACCESS; in funzione delle condizioni in cui si è verificata l’eccezione95, la trap() può affrontarla in diversi modi. In generale però questa chiamerà vm_fault() per risolvere il page-fault: quest’ultima prima di tutto cerca la data VM map attraverso il dato indirizzo virtuale. Se la ricerca ha successo, cerca un VM object, l’offset nell’oggetto e il valore di protezione associato. In seguito di deve assicurare che la pagina sia residente: o la pagina si trova nella memoria fisica mediante la ricerca nella VP table, oppure una nuova pagina residente è allocata per la data coppia oggetto/offset e inserita nella tabella (in questo caso la pagina deve anche essere riempita di dati). Se il VM object ha un paginatore, il kernel chiamerà la funzione

memory_object_data_request() per richiedere al pager di recuperare i dati; alternativamente, se il VM object ha una shadow, il kernel attraversa la shadow chain alla ricerca della pagina. Le nuove pagine corrispondenti ai VM objects interni (memoria anonima) saranno zero-filled. Oltretutto, se il VM object ha un copy object associato e la pagina è scritta, sarà inserita nel copy object se non già fatto. Infine, il gestore del page-

fault inserirà la pagina nella pmap del task attraverso la chiamata a pmap_enter(). Dopo questo, la pagina è finalmente disponibile al task.

4.3 Universal Page Lists (UPLs) Il kernel fornisce un’astrazione chiamata universal page list (UPL) che descrive un set di pagine fisiche associate ad un range di indirizzi di un VM object. In particolare, una UPL fornisce un’istantanea di varie proprietà delle sue pagine, come ad esempio se non mappate, sporche, criptate , occupate o corrispondenti a memoria di I/O.

Una UPL è creata internamente da upl_create() [osfmk/vm/vm_pageout.c], che alloca ed inizializza la struttura struct upl [osfmk/vm/vm_pageout.h]. Se la UPL è creata con il control flag UPL_SET_INTERNAL, tutte le informazioni sono

95 Ad esempio se si è verificata nel kernel o nello spazio utente, se il debugger del kernel era attivo o meno, se la struttura thread del faulting thread contiene un puntatore valido ad un funzione di “recupero” e così via. 130 Il sistema operativo Mac OS X

contenute in un singolo oggetto di memoria, permettendo un trasporto conveniente

dell’UPL nel kernel. Nel caso di un UPL interno, la funzione upl_create() alloca ulteriore memoria per memorizzare delle strutture upl_page_info [osfmk/mach/memory_object_types.h], una per ogni pagina dell’UPL. Il numero massimo delle pagine che possono essere gestite da un UPL è

MAX_UPL_TRANSFER (256). I clients principali dell’API UPL include i pagers, lo strato del file system e l’UBC

(unified buffer cache). Questi clients non chiamano direttamente upl_create() quando devono creare una UPL da un VM object, ma utilizzano funzioni ad alto livello

come vm_object_upl_request(), vm_object_iopl_request() e vm_map_get_upl(). L’ultima è particolarmente utile quando non si ha il VM object in questione, poiché ricerca l’oggetto sottostante dato un range di indirizzi in una VM map. Una volta modificata una UPL, le modifiche possono essere confermate o scartate

mediante le funzioni upl_commit() e upl_abort(), rispettivamente. Queste funzioni operano su tutta la struttura UPL, per agire su una parte specifica esistono le

funzioni upl_commit_range() e upl_abort_range(). Le funzioni UBC ubc_upl_commit_range() e ubc_upl_abort_range() sono delle estensioni delle precedenti, in quanto deallocano l’UPL dell’associato VM object se non ha più pagine residenti dopo la conferma o lo scarto delle modifiche, rispettivamente.

4.4 Unified Buffer Cache (UBC) Storicamente, lo UNIX alloca una porzione di memoria fisica per essere utilizzata come buffer cache, con l’obiettivo di migliorare le prestazioni attraverso il caching di blocchi disco in memoria, evitando quindi di dover accedere al disco durante la lettura o scrittura dei dati. Prima dell’avvento dell’UBC, un buffer cached era identificato mediante un numero di periferica ed un numero di blocco. I sistemi operativi moderni, compreso il Mac OS X, utilizza un approccio unificato in cui il contenuto in memoria dei files risiede nello stesso namespace come memoria regolare.

131 Il sistema operativo Mac OS X

L’UBC esiste concettualmente nella porzione BSD del kernel. Ogni vnode corrispondente ad un file regolare contiene un riferimento ad una struttura ubc_info, che assume il ruolo di ponte tra i vnodes ed i rispettivi VM objects. Notiamo che l’informazione UBC non è valida per i vnodes di sistema (marcati VSYSTEM), neanche se il vnode è al contrario regolare. Quando un vnode è creato, a seguito di una chiama di sistema open(), una struttura ubc_info viene allocata ed inizializzata.

// bsd/sys/ubc_internal.h struct ubc_info { memory_object_t ui_pager; // for example, the vnode pager memory_object_control_t ui_control; // pager control port long ui_flags; struct vnode *ui_vnode; // our vnode struct ucred *ui_cred; // credentials for NFS paging off_t ui_size; // file size for vnode struct cl_readahead *cl_rahead; // cluster read-ahead context struct cl_writebehind *cl_wbehind; // cluster write-behind context };

// bsd/sys/vnode_internal.h struct vnode { ... union { struct mount *vu_mountedhere; // pointer to mounted vfs (VDIR) struct socket *vu_socket; // Unix IPC (VSOCK) struct specinfo *vu_specinfo; // device (VCHR, VBLK) struct fifoinfo *vu_fifoinfo; // fifo (VFIFO) struct ubc_info *vu_ubcinfo; // regular file (VREG) } v_un; ... }; Il lavoro dell’UBC è di effettuare il cahing nella memoria fisica con un approccio avido: cerca di consumare tutta la memoria fisica disponibile. Questa è particolarmente rilevante per i processi a 32-bit su macchine a 64-bit con oltre 4GB di memoria fisica. Anche se nessun singolo processo a 32.bit può indirizzare più di 4GB di memoria virtuale, la dimensione maggiore della memoria fisica avvantaggia tutti i processi così come una maggiore buffer cache. L’UBC esporta diverse routines ad uso dei file systems (figura 4-7). Ad esempio ubc_setsize(), che informa l’UBC che un file ha cambiato dimensione, può essere chiamata quando una routine di scrittura del file system estende un file. L’UBC fornisce, inoltre, delle routine per lavorare con le UPLs:

• ubc_create_upl() – crea una UPL dati un vnode, l’offset e la dimensione. • ubc_upl_map() – mappa una intera UPL in un address space (la ubc_upl_unmap() effettua l’operazione inversa).

132 Il sistema operativo Mac OS X

• ubc_upl_commit(), ubc_upl_commit_range(), ubc_upl_abort() e ubc_upl_abort_range() – sono le estensioni delle corrispondenti funzioni UPL per confermare o scartare le modifiche.

Figura 4-7 Esempi di routines UBC esportate // convert logical block number to file offset off_t ubc_blktooff(vnode_t vp, daddr64_t blkno);

// convert file offset to logical block number daddr64_t ubc_offtoblk(vnode_t vp, off_t offset);

// retrieve the file size off_t ubc_getsize(vnode_t vp);

// file size has changed int ubc_setsize(vnode_t vp, off_t new_size);

// get credentials from the ubc_info structure struct ucred *ubc_getcred(vnode_t vp);

// set credentials in the ubc_info structure, but only if no credentials // are currently set int ubc_setcred(vnode_t vp, struct proc *p);

// perform the clean/invalidate operation(s) specified by flags on the range // specified by (start, end) in the memory object that backs this vnode errno_t ubc_msync(vnode_t vp, off_t start, off_t end, off_t *resid, int flags);

// ask the memory object that backs this vnode if any pages are resident int ubc_pages_resident(vnode_t vp);

Non tutti i tipi di cache di sistema sono unificate, ed alcune non possono esserlo. Per esempio, i metadati del file system (che dal punto di vista dell’utente non sono parte del file) devono essere messi in una cache indipendente. D’altra parte, in alcune circostanze può essere vantaggioso (per le prestazioni) creare una buffer cache privata. Questo è il caso dell’implementazione della NFS nel kernel del Mac OS X96, che utilizza una buffer

cache privata con una struttura buffer specifica per l’NFS (struct nfsbuf [bsd/nfs/nfsnode.h]). La versione 3 dell’NFS fornisce una nuova operazione di COMMIT che permette ad un client di richiedere al server una unstable write, nella quale i dati sono scritti al server ma a questi non è richiesta la verifica che tali dati siano stati impegnati in una memoria stabile. In questo modo il server può rispondere immediatamente al client e, di

conseguenza, quest’ultimo mandare una richiesta COMMIT per impegnare i dati in una memoria stabile. La NFSv3, inoltre, fornisce un meccanismo che permette al client di

96 Le versioni del Mac OS X precedenti la 10.3 non utilizzavano una buffer cache separata per l’NFS. 133 Il sistema operativo Mac OS X scrivere di nuovo i dati uncommitted al server nel caso di perdita, ad esempio a causa di un reboot del server.

int nfs_doio(struct nfsbuf *bp, kauth_cred_t cr, proc_t p){ ... if (ISSET(bp->nb_flags, NB_WRITE)) { // we are doing a write ... if (/* a dirty range needs to be written out */) { ... error = nfs_writerpc(...); // let this be an unstable write ... if (!error && iomode == NFSV3WRITE_UNSTABLE) { ... SET(bp->nb_flags, NB_NEEDCOMMIT); ... } ... } ... } ... } Il buffer cache regolare ed il meccanismo del cluster I/O non sono a conoscenza del meccanismo dell’unstable write, tipico dell’NFS. In particolare, una volta che il client ha completato una unstable write, i corrispondenti buffers nella NFS buffer cache sono etichettati come NB_NEEDCOMMIT. L’NFS utilizza il proprio daemon di I/O asincrono (nfsiod). Il regular buffer laundry thread, ossia bcleanbuf_thread() [bsd/vfs/vfs_bio.c], è anch’esso all’oscuro delle scritture instabili. Durante la pulizia dei buffers NFS sporchi, il laundry thread non può aiutare il codice del client NFS a fondere le richieste di COMMIT corrispondenti a multipli buffers NB_NEEDCOMMIT, ma può rimuovere un buffer alla volta dalla laundry queue e rilasciargli l’I/O. di conseguenza l’NFS dovrebbe spedire richieste di COMMIT singole, cosa che intaccherebbe le prestazioni e incrementerebbe il traffico di rete. Un’altra differenza tra NFS ed il regular buffer cache è che il primo supporta esplicitamente i buffers con pagine multiple. Il secondo prevede un singolo bit

(B_WASDIRTY) nella struttura buf per marcare una pagina trovata sporca nella cache. La struttura nfsbuf prevede fino a 32 pagine che possono essere marcate individualmente come pulite o sporche. Buffers NFS di dimensioni maggiori aiutano il miglioramento delle prestazioni dell’I/O NFS.

134 Il sistema operativo Mac OS X

// bsd/nfs/nfsnode.h struct nfsbuf { ... u_int32_t nb_valid; // valid pages in the buffer u_int32_t nb_dirty; // dirty pages in the buffer ... };

#define NBPGVALID(BP,P) (((BP)->nb_valid >> (P)) & 0x1) #define NBPGDIRTY(BP,P) (((BP)->nb_dirty >> (P)) & 0x1) #define NBPGVALID_SET(BP,P) ((BP)->nb_valid |= (1 << (P))) #define NBPGDIRTY_SET(BP,P) ((BP)->nb_dirty |= (1 << (P)))

4.5 Il programma Dynamic Pager

Il programma dynamic pager (/sbin/dynamic_pager) è un processo utente che crea e cancella files (di swap) nella directory /var/vm designata. Malgrado il nome, dynamic_pager non è un paginatore Mach e non è coinvolto in nessuna operazione di paging: si tratta solo di un gestore dello spazio di swap utilizzato dal kernel. Il Mac OS X utilizza di default dei paging files invece di partizioni di swap dedicate. I paging files hanno dimensioni variabili e sono creati dinamicamente; il kernel scrive i dati in tali files in gruppi di pagine (cluster).

Nel suo modo di funzionamento tipico, il dynamic_pager opera con due limiti di byte: l’high-water mark ed il lo-water mark. Quando ci sono meno bytes liberi negli swap files

di quanti ne permette l’hig-water mark, il dynamic_pager crea un nuovo file e lo aggiunge allo swap pool notificandolo al kernel. Quando ci sono più bytes liberi nei files di paging rispetto a quanti previsti dal low-water mark, il kernel manda una notifica al

dynamic_pager per attivare la cancellazione di uno swap file97. Quando è avviato, il dynamic_pager determina i due marks ed altri limiti sulla base degli argomenti della linea di comando, lo spazio libero del file system, la memoria fisica istallata e gli hard limits incorporati. Nello specifico, stabilisce i seguenti limiti e regole:

• La dimensione minima assoluta (64MB) e la dimensione massima assoluta (1GB) di uno swap file.

• La dimensione massima di un file di scambio non può essere maggiore né del 12.5% dello spazio disponibile sul volume che contiene il file, né del totale della

97 Ad ogni singola notifica corrisponde la cancellazione di un solo file. Notiamo che il file da cancellare è probabile che abbia qualche pagina paged-out. Tali pagine sono portate nel kernel ed infine paged-out in un altro swap file. 135 Il sistema operativo Mac OS X

memoria fisica istallata nel sistema.

• Possono essere creati al massimo otto files di scambio. • I primi due files di scambio hanno la stessa dimensione (quella minima di 64MB). I files successivi raddoppiano la loro dimensione fino a quella massima.

• L’high-water mark di default è di 40˙000˙000 bytes (circa 38MB). Il Mac OS X 10.4 supporta il criptaggio dei dati scritti dal kernel negli swap files. Il kernel utilizza a questo scopo una variante dell’algoritmo di criptaggio AES. Anche senza il criptaggio, il kernel impedisce ai programmi utente di leggere direttamente i dati di

scambio mediante l’implementazione di una speciale chiamata di sistema read().

4.6 L’Update Daemon

L’update daemon (/usr/sbin/update) ripulisce98 periodicamente i buffers sporchi del file system invocando la chiamata di sistema sync(). Il periodo di deafult è di 30 secondi, ma può essere modificato mediante linea di comando. Esiste, inoltre, un ulteriore intervallo di power-save che può essere specificato: quando il sistema è alimentato a batteria ed il disco è in sleep, l’intervallo di power-save è utilizzato al posto di quello normale.

La sync() itera la lista dei file systems montati, invocando sync_callback() [bsd/vfs/vfs_syscalls.c] su ogni file system. Ad ognuna segue VFS_SYNC() [bsd/vfs/kpi_vfs.c], che chiama la funzione di sincronizzazione specifica del file system mediante una tabella mantenuta dallo strato VFS.

4.7 La memoria condivisa di sistema Il kernel fornisce un meccanismo di condivisione della memoria, il sottosistema Shared Memory Server, mediante il quale sia il kernel che i programmi utente possono condividere codice e dati tra tutti i task del sistema. È anche possibile fornire ad uno o più tasks delle versioni private della memoria condivisa.

98 Con ripulitura non s’intende la scrittura immediata dei dati nel disco, ma solo l’accodamento per la scrittura. La scrittura effettiva su disco avviene tipicamente in breve tempo. La chiamata di sistema fcntl(), mediante il controllo F_FULLSYNC realizza la vera pulizia di un file a disco. 136 Il sistema operativo Mac OS X

L’implementazione del sottosistema Shared Memory Server può essere divisa in un BSD

front-end (implementato in bsd/vm/vm_unix.c) ed un Mach back-end (implementato in osfmk/vm/vm_shared_memory_server.c). Il primo fornisce un set di system calls private dell’Apple che è utilizzato dal dynamic link editor (dyld). Il secondo nasconde i dettagli della memoria virtuale del Mach e fornisce funzionalità di basso livello per la memoria condivisa che sono utilizzate dal front-end. Nonostante tutte le applicazioni utente tipiche beneficino dei servizi forniti dallo Shared Memory Server, le APIs corrispondenti sono riservate esclusivamente alle applicazioni

fornite dalla Apple, con il dyld come unico client. L’utilizzo di queste APIs possono interessare tutte le applicazioni del sistema, anche in modo avverso: per questo motivo i programmi di terze parti non devono utilizzarle.

4.8 Task Working Set Il kernel utilizza la memoria fisica come cache per la memoria virtuale. Quando delle nuove pagine devono essere caricate a causa dei page faults, il kernel deve decidere quali pagine reclamare fra quelle correntemente nella memoria fisica. Dal punto di vista di un’applicazione, il kernel dovrebbe idealmente mantenere in memoria quelle pagine che saranno presto necessarie. In un sistema operativo ideale, il kernel saprebbe prima del tempo quali pagine saranno riferite da una applicazione durante la propria esecuzione. Sono stati studiati diversi algoritmi per approssimare una sostituzione di pagina così

ottimale. Un altro approccio usa Principio di Località, su cui si basa il Working Set Model. Come descritto nel documento intitolato “Virtual Memory”99, la località può essere intesa informalmente come l’affinità di un programma per un sottogruppo delle sue pagine, in cui i membri cambiano lentamente. Da questo prende forma il concetto di working set, definito informalmente come il set delle pagine “più utili” per un programma. Il Working Set Principle stabilisce la regola che un programma può girare se e solo se il suo working set è in memoria, ed una pagina non può essere rimossa se fa parte del working set di un programma in esecuzione. Degli studi hanno dimostrato che mantenere

99 “Virtual Memory” di Peter J. Denning (ACM Computing Surveys 2:3, Settembre 1970, pp 153-189). 137 Il sistema operativo Mac OS X il working set residente nella memoria fisica permette al programma relativo di girare con delle prestazioni accettabili, vale a dire senza causare un numero inaccettabile di page faults. Il kernel del Mac OS X include un meccanismo che, per ogni coppia {applicazione, utente}, costruisce i profili working set, salva le pagine corrispondenti in una directory designata e cerca di caricarle quando l’applicazione è eseguita dall’utente. Questo meccanismo, chiamato TWS (da task working set), è integrato nel meccanismo di gestione del page-fault del kernel. La prima volta che una applicazione è lanciata in un dato contesto utente, il TWS cattura il working set iniziale e lo memorizza in un file nella directory ‘var/vm/app_profile/’. Diversi aspetti dello schema TWS contribuiscono alle prestazioni.

• Le informazioni del profilo sono utilizzate durante la gestione del page-fault per determinare se qualche pagina vicina debba essere portata dentro. Prendere più pagine di quelle corrispondenti all’immediato page-fault porta ad una singola grande richiesta al pager, evitando le richieste multiple successive che dovrebbero al contrario essere fatte per portare le pagine che si pensa siano necessarie nell'immediato futuro. Questo vale solo per le pagine non sequenziali, tuttavia, poiché le pagine sequenziali sono comunque paged-in per il cluster I/O.

• Il TWS cattura e memorizza su disco il working set iniziale di un’applicazione la prima volta che questa viene lanciata da un particolare utente. Questa informazione è utilizzata per il “preriscaldamento” del working set quando l’applicazione è lanciata di nuovo nello stesso contesto utente. In questo modo, il profilo dell’applicazione è costruito nel tempo.

• La località dei riferimenti di memoria sono spesso messe su disco, poiché i files su disco hanno tipicamente una buona località sui volumi HFS Plus. Normalmente i working sets possono essere letti da disco con dei piccoli overheads di ricerca. Il TWS memorizza i profili delle applicazioni per un dato user in due files nella directory

‘var/vm/app_profile/’. Questi files si chiamano #U_names e #U_data, dove #U è la rappresentazione esadecimale dello user ID. Il file names è un semplice database che

138 Il sistema operativo Mac OS X contiene un header seguito dagli elementi del profilo, mentre il file data contiene il working set attuale. Gli elementi del profilo nel file names puntano ai working sets nel file data.

// bsd/vm/vm_unix.c

// header for the "names" file struct profile_names_header { unsigned int number_of_profiles; unsigned int user_id; unsigned int version; off_t element_array; unsigned int spare1; unsigned int spare2; unsigned int spare3; };

// elements in the "names" file struct profile_element { off_t addr; vm_size_t size; unsigned int mod_date; unsigned int inode; char name[12]; }; Il kernel mantiene una struttura dati contenente un array di profili globali, ognuno dei quali le informazioni per un utente.

// bsd/vm/vm_unix.c

// meta information for one user's profile struct global_profile { struct vnode *names_vp; struct vnode *data_vp; vm_offset_t buf_ptr; unsigned int user; unsigned int age; unsigned int busy; };

struct global_profile_cache { int max_ele; unsigned int age; struct global_profile profiles[3]; // up to 3 concurrent users };

...

struct global_profile_cache global_user_profile_cache = { 3, 0, { NULL, NULL, 0, 0, 0, 0 }, { NULL, NULL, 0, 0, 0, 0 }, { NULL, NULL, 0, 0, 0, 0 } }; Molte delle funzionalità del TWS sono implementate in ‘bsd/vm_vm_unix.c’, con il file ‘osfmk/vm/task_working_set.c’ che ne utilizza le funzioni. • prepare_profile_database() crea un’unico pathname assoluto per i files names e data per il dato user ID. È chiamata da setuid() per preparare questi files per un nuovo utente.

139 Il sistema operativo Mac OS X

• bsd_search_page_cache_data_base() ricerca un profilo d’applicazione in un dato file names.

• bsd_open_page_cache_files() cerca di aprire (se presenti entrambi) o creare (se assenti entrambi) i files names e data. Nel caso solo uno fosse presente, il tentativo fallisce.

• bsd_close_page_cache_files() decrementa i riferimenti nei files names e data per il profilo utente specificato.

• bsd_read_page_cache_file() per prima cosa chiama bsd_open_page_cache_files(), dopodichè cerca il profilo d’applicazione dato nel file names usando bsd_search_page_cache_data_base(). Se il profilo è trovato, la funzione ne legge i dati dal file data nel buffer specificato.

• bsd_write_page_cache_file() scrive nei files names e data.

4.9 Memory-mapped files

Il Mac OS X fornisce la chiamata di sistema mmap() per la mappatura (mapping) di files, dispositivi a carattere e descrittori POSIX di memoria condivisa nell’address space del chiamante. Inoltre, la memoria anonima può essere mappata impostando l’argomento

flags di mmap() su MAP_ANON.

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset); Quando mmap() è usata per un file regolare o memoria anonima, la mappatura è backed da un oggetto su disco come segue:

• La memoria anonima è sempre backed dallo spazio di swap. • La mappatura di un file regolare è backed dal file stesso se la chiamata ha

l’argomento flags settato a MAP_SHARED. Questo significa che ogni modifica fatta alla mappatura è scritta nel file originale quando la corrispondente pagina è rimossa.

• La mappatura di un file regolare è backed dallo spazio di swap se la chiamata ha

l’argomento flags settato a MAP_PRIVATE. Questo significa che ogni modifica fatta alla mappatura è privata.

140 Il sistema operativo Mac OS X

Andiamo ora ad analizzare l’implementazione di mmap() osservando la sequenza di operazioni che hanno luogo quando un programma mappa un file regolare. Per prima cosa il programma deve acquisire il file descriptor del file in questione. La figura 4-8 mostra le attività rilevanti che avvengono in conseguenza di una chiamata di sistema open(). In questo caso, un preesistente file regolare residente in un volume HFS+ viene aperto per la prima volta.

Figura 4-8 Configurazione del vnode pager durante la chiamata di sistema open()

141 Il sistema operativo Mac OS X

La struttura vnode (struct vnode [bsd/sys/vnode_internal.h]) corrispondente ad un file regolare contiene un puntatore ad una struttura informazioni

UBC (struct ubc_info [bsd/sys/ubc_internal.h]). Questa contiene un puntatore al pager, in questo caso il vnode pager, come rappresentato da una struttura vnode_pager [osfmk/vm/bsd_vm.c]. La figura 4-9 mostra come queste strutture sonno connesse durante la creazione di un vnode.

Figura 4-9 Configurazione del vnode pager per la creazione di un nuovo vnode

Supponiamo che un programma utente chiami mmap() per mappare il file descriptor ottenuto in figura 4-8. La figura 4-10 mostra la conseguente attività del kernel. La funzione mmap() chiama mach_vm_map() [osfmk/vm/vm_user.c] che, nel caso di un file regolare, chiamerà vm_object_enter() [osfmk/vm/vm_object.c]. Siccome nessun VM object è ancora stato assegnato al dato pager, quest’ultima funzione ne creerà uno nuovo. Inoltre inizializzerà il pager, il che comporta anche l’allocazione di una porta di controllo che verrà passata come argomento a memory_object_init(). Infine la chiamata a vm_map_enter() [osfmk/vm/vm_map.c] comporterà l’allocazione di un virtual address range nello spazio d’indirizzo virtuale del task.

142 Il sistema operativo Mac OS X

Figura 4-10 Elaborazione della chiamata di sistema mmap() da parte del kernel

Quando un programma cerca di accedere ad un indirizzo della memoria mappata per leggerlo, causa un’attività di page-in se la pagina corrispondente non è ancora residente.

Dato che il programma ha mappato il file con PROT_READ|PROT_WRITE come valori di protezione e MAP_SHARED specificato come flags, causerà l’attività di page-out se modifica la memoria mappata.

143 Il sistema operativo Mac OS X

Le figure 4-11a e 4-11b mostrano i passi di un’operazione di page-in, con la seconda che specifica i dettagli del paging in da un vnode. Le figure 4-12a e 4-12b mostrano gli step analoghi per l’operazione di page-out-

Figura 4-11a Una visione d’insieme dell’operazione di page-in

144 Il sistema operativo Mac OS X

Figura 4-11b Il paging in da un vnode

145 Il sistema operativo Mac OS X

Figura 4-12a Una visione d’insieme dell’operazione di page-out

146 Il sistema operativo Mac OS X

Figura 4-12b Il paging out da un vnode

147 Il sistema operativo Mac OS X

Capitolo 5 Interprocess Communication

I programmi complessi, anche solo moderatamente, sono di solito suddivisi in parti logicamente e funzionalmente separate. Questo permette un più semplice sviluppo, una migliore manutenzione e flessibilità ed un’agevolata compresione del software. Anche se tale suddivisione può essere fatta in diversi modi, alcuni dei quali formali e standardizzati, c’è un risultato generale: in un tipico sistema operativo possono esserci diverse entità coinvolte in un’operazione. Queste entità spesso necessitano di condividere informazioni, sincronizzarsi e comunicare l’una con l’altra. Facendo girare il più banale programma in C sul Mac OS X comporta l’invocazione di dozzine di system calls. Programmi non banali possono comprendere threads multipli (o forse anche processi multipli) che debbano comunicare l’uno con l’altro in modi arbitrari. Spesso, processi non coinvolti nello stesso programma devono comunque comunicare

l’uno con l’altro. Vale la pena chiedersi cosa si qualifica come comunicazione. In alcuni casi, la linea tra ‘comunicazione’ e ‘condivisione dell’informazione’ può essere poco chiara. Per questo motivo intenderemo l’interprocess communication (IPC) come un meccanismo ben definito, provvisto di un’interfaccia di programmazione, per il trasferimento d’informazioni tra due o più entità. Le entità comunicanti erano, storicamente, i processi (da questo viene il termine ‘comunicazione interprocesso’). Tali entità possono attualmente essere anche dei threads di uno stesso task, dei threads di diversi tasks o

148 Il sistema operativo Mac OS X

threads nel kernel. Fin dai primi giorni dei sistemi timesharing, una varietà di risorse di computazione sono state associate con i processi: IPC è anche il mezzo di condivisione di tali risorse.100 A seconda del tipo di IPC, le parti possono richiedere una qualche forma di sincronizzazione per permettere al meccanismo di funzionare correttamente. In generale l’IPC può richiedere e può consistere di una o più delle seguenti operazioni:

• Condivisione dei dati • Trasferimento dei dati • Condivisione delle risorse • Sincronizzazione tra i partecipanti all’IPC • Notifiche sincrone ed asincrone • Operazioni di controllo I primi meccanismi IPC usavano dei files come mezzo di comunicazione. In seguito ci fu l’approccio della memoria condivisa, dove i processi utilizzavano regioni di memoria comunemente accessibili per implementare ad hoc l’IPC e gli schemi di sincronizzazione. Infine il meccanismo dell’IPC divenne un’astrazione fornita dallo stesso sistema operativo. Il Mac OS X fornisce un gran numero di meccanismi IPC, alcuni con interfacce disponibili a più strati del sistema. I seguenti sono esempi di meccanismi/interfacce di IPC nel Mac OS X:

• ‘Mach IPC’ • Eccezioni Mach • Segnali Unix • Pipes senza nome • Pipes con nome (fifos) • ‘XSI/System V IPC’ • ‘POSIX IPC’ • Oggetti distribuiti

100 Il termine IPC è spesso usato come sinonimo di message passing, che può essere pensato come uno specifico (e piuttosto popolare) meccanismo IPC. 149 Il sistema operativo Mac OS X

• ‘Apple Events’ • Varie interfacce per spedire e ricevere notifiche • Meccanismo ‘Core Foundation IPC’ Ognuno di questi meccanismi ha i propri benefici, difetti e rischi. Un programmatore può aver bisogno di usare un particolare meccanismo, o anche diversi, in base alle richieste del programma e lo strato di sistema per cui è progettato.

5.1 L’IPC del Mach Il Mach fornisce una IPC orientata ai messaggi che rappresenta un’evoluzione degli approcci utilizzati dai suoi precursori Accent e RIG. L’implementazione utilizza il sottosistema VM per trasferire efficientemente grandi quantità di dati mediante le

ottimizzazioni copy-on-write101. Il kernel Mac OS X usa la primitiva generale del messaggio, fornita dall’interfaccia IPC del Mach, come mattone fondamentale. In

particolare, le chiamate Mach mach_msg() e mach_msg_overwrite() possono essere utilizzate per spedire e ricevere (rispettivamente) i messaggi, permettendo un’interazione simile alla RPC come caso speciale dell’IPC. Questo tipo di RPC è utilizzata per implementare diversi servizi di sistema nel Mac OS X. L’IPC del Mach si basa su due astrazioni kernel di base: le porte ed i messaggi. Una porta è un’entità poliedrica, mentre un messaggio è una collezione, di dimensione arbitraria, di data objects.

5.1.1 Le porte Mach Una porta è fondamentalmente un canale di comunicazione, ossia una coda di lunghezza finita di messaggi gestita (e protetta) dal kernel. Le operazioni di base su una porta sono la spedizione e la ricezione di messaggi: spedire ad una porta permette ad un task di mettere i messaggi nella coda sottostante, ricevere su una porta permette al task di recuperare i messaggi dalla coda. Quando la coda corrispondente ad una porta è piena o vuota, i

101 L’integrazione dell’IPC con la memoria virtuale permette la mappatura (copy-on-write, se possibile ed appropriato) dei messaggi nell’address space del task ricevente. In teoria, un messaggio potrebbe essere grande quanto la dimesione dell’address space del task. 150 Il sistema operativo Mac OS X

senders ed i receivers sono (rispettivamente) bloccati, in generale. Per poter accedere ad una porta, un task deve avere un port right (send right o receive right). Questo meccanismo serve ad impedire ad i tasks di accedere in modo arbitrario sulle porte, limitando il set di operazioni che ogni task può compiere su ogni porta. Le porte sono anche utilizzate per rappresentare risorse, servizi e mezzi che forniscono così un accesso object-style a queste astrazioni. Ad esempio, il Mach usa le porte per rappresentare astrazioni come hosts, tasks, threads, memory objects, clocks, timers, processori e processor set. Le operazioni su un tale oggetto sono effettuate attraverso la spedizione di messaggi alla porta che lo rappresenta. Nel suo ruolo di canale di comunicazione, una porta Mach potrebbe sembrare una socket BSD, ma ci sono differenze importanti:

• La progettazione dell’IPC Mach è integrata con il sottosistema della memoria virtuale.

• Laddove i sockets sono usate principalmente per le comunicazioni remote, l’IPC Mach è utilizzata (ed ottimizzata) per la comunicazione intra-macchina102.

• I messaggi dell’IPC Mach possono trasportare dati tipizzati. • In generale, le interfacce dell’IPC Mach sono più potenti e flessibili di quelle del socket. Quando si parla di un messaggio spedito ad un task, s’intende che il messaggio è spedito alla porta di cui il task ricevente possiede i diritti di ricezione. Il messaggio viene poi

rimosso dalla coda da un thread interno al task ricevente. Il Mac OS X definisce i seguenti tipi specifici di port rights:

• MACH_PORT_RIGHT_SEND – un diritto di spedizione ad una porta implica che il possessore del diritto può spedire messaggi a quella porta. Se un thread acquisisce un send right che il proprio task possedeva, il reference count relativo è incrementato. Analogamente, se un thread rilascia il diritto viene decrementato il conteggio. Il task perde il proprio diritto di spedizione solo quando il reference

102 Il progetto dell’IPC Mach può essere esteso in modo trasparente alla rete, mediante dei tasks esterni (ossia utente) chiamati Network Servers, che agiscono come dei proxies per i tasks remoti . Anche se il kernel XNU mantiene la maggior parte delle semantiche dell’IPC Mach, il Mac OS X non utilizza quest’estensione a livello di rete. 151 Il sistema operativo Mac OS X

count relativo si azzera.

• MACH_PORT_RIGHT_RECEIVE – un diritto di ricezione su una porta implica che il possessore del diritto può rimuovere i messaggi dalla coda di quella porta. Una porta puà avere qualsiasi numero di senders, ma un solo receiver; inoltre se un task ha il diritto di ricezione, ha automaticamente anche quello di spedizione.

• MACH_PORT_RIGHT_SEND_ONCE – un diritto di “unica spedizione” permette al possessore di spedire un solo messaggio, dopo del quale il diritto viene cancellato. I send-once rights sono usati come porte di risposta: un client può includere un diritto di send-once in una richiesta ed il server può usare tale diritto per spedire una risposta. Un send-once right comporta sempre la spedizione di uno ed un solo messaggio.

• MACH_PORT_RIGHT_PORT_SET – un nome port set può essere considerato come un diritto di ricezione che comprende porte multiple. Un port set rappresenta un gruppo di porte di cui il task possiede il receive right. Questo diritto permette al task di ricevere un messaggio, il primo ottenibile, da una delle porte del set. Il messaggio identifica la porta sul quale è stato ricevuto.

• MACH_PORT_RIGHT_DEAD_NAME – un dead name non è realmente un diritto, ma rappresenta un send (o send-once) right che è diventato invalido in seguito alla distruzione della porta corrispondente. Quando il send right si trasforma in dead name, il suo reference count rimanda al dead name. Cercare di spedire un messaggio ad un dead name produce un errore, che permette ai senders di realizzare che la porta è distrutta103. Andiamo ora ad analizzare alcuni aspetti degni di nota che riguardano i port rights.

• I diritti sono posseduti a livello di task. Per esempio, sebbene il codice per creare una porta è eseguito in un thread, i diritti associati sono concessi al suo task. Di conseguenza, ogni altro thread interno a quel task può usare o manipolare tali diritti.

103 Un porta è considerata distrutta quando i suoi receive rights sono deallocato. Anche se i send (o send-once) rights esistenti si trasformano in dead names quando avviene la distruzione, i messaggi esistenti nella coda della porta vengono distrutti ed ogni memoria out-of-line associata viene liberata. 152 Il sistema operativo Mac OS X

• Lo spazio dei nomi per le porte è privato al task. In altre parole, un dato nome di porta è valido solo all’interno dello spazio IPC di un task, in maniera analoga allo spazio d’indirizzo virtuale di un task.

• Se un task possiede entrambi i diritti (di spedizione e ricezione) per una porta, allora i diritti hanno lo stesso nome.

• Due send-once rights detenuti da uno stesso task non possono avere lo stesso nome. • I diritti possono essere trasferiti mediante lo scambio di messaggi. In particolare, l’operazione (frequente) di ottenere l’accesso ad una porta coinvolge la ricezione di un messaggio contenente un port right.

• Dopo che un task ha spedito un messaggio contenente uno o più port rights, e prima che il messaggio venga rimosso dalla coda dal ricevente, i diritti sono mantenuti dal kernel. Poiché il diritto di ricezione può essere detenuto da un solo task alla volta, c’è la possibilità di messaggi spediti ad una porta il cui receive right è in trasferimento. In tale caso, il kernel accoderà i messaggi fino a che il task riceverà i diritti e rimuoverà i messaggi dalla coda.

5.1.2 I messaggi IPC del Mach I messaggi IPC Mach possono essere spediti e ricevuti attraverso la famiglia di funzioni

mach_msg. La chiamata di sistema IPC fondamentale del Mac OS X è una trap chiamata mach_msg_overwrite_trap() [osfmk/ipc/mach_msg.c], che può essere usata per spedire un messaggio, per ricevere un messaggio o per spedire e ricevere in una singola chiamata.

// osfmk/ipc/mach_msg.c mach_msg_return_t mach_msg_overwrite_trap( mach_msg_header_t *snd_msg, // message buffer to be sent mach_msg_option_t option, // bitwise OR of commands and modifiers mach_msg_size_t send_size, // size of outgoing message buffer mach_msg_size_t rcv_size, // maximum size of receive buffer (rcv_msg) mach_port_name_t rcv_name, // port or port set to receive on mach_msg_timeout_t timeout, // timeout in milliseconds mach_port_name_t notify, // receive right for a notify port mach_msg_header_t *rcv_msg, // message buffer for receiving mach_msg_size_t scatterlist_sz); // size of scatter list control info Il comportamento di tale funzione è controllato dall’argomento option: i suoi bits determinano cosa deve fare la chiamata e come lo deve fare.

153 Il sistema operativo Mac OS X

L’anatomia di un messaggio Mach si è evoluta nel tempo, ma la struttura di base consistente in un header di dimensione fissa seguito da dati di dimensioni variabili è rimasta immutata. I messaggi Mach nel Mac OS X contiene le seguenti parti:

• Un header di dimensione fissata (mach_msg_header_t). • Un body di dimensione variabile (eventualmente vuoto) contenente dati utente e del

kernel (mach_msg_body_t). • Un trailer di dimensione variabile contenente degli attributi del messaggio, apposti

dal kernel (mach_msg_trailer_t).

Figura 5-1 La struttura di un messaggio Mach complesso

154 Il sistema operativo Mac OS X

Un messaggio può essere semplice o complesso. Un messaggio semplice contiene un header immediatamente seguito da dati senza tipo, mentre un messaggio complesso contiene anche un corpo del messaggio strutturato.

5.2 Mach IPC: implementazione nel Mac OS X Il nucleo del sottosistema IPC è implementato mediante i files della directory

osfmk/ipc/ nel kernel source tree. Inoltre, il gruppo di files osfmk/kern/ipc_* implementano le funzioni di supporto all’IPC per gli oggetti kernel come i tasks ed i threads. La figura 5-2 mostra una visione di insieme dell’implementazione del sistema IPC Mach all’interno del Mac OS X.

Figura 5-2 Implementazione dell’IPC Mach nel Mac OS X

5.2.1 Gli spazi IPC Ogni task ha un IPC space privato (un namespace per le porte), rappresentato dalla

struttura dati ipc_space nel kernel, che ne definisce le capacità IPC. Lo spazio IPC incapsula la conoscenza necessaria a tradurre i nomi di porta locale (task-specific) in

155 Il sistema operativo Mac OS X

strutture dati di porta globali (kernel-wide). Questa traduzione è implementata usando le translation entries per le port capabilities. Ogni capability è registrata nel kernel mediante

una struttura dati ipc_entry. Uno spazio IPC contiene sempre una tabella di IPC entries puntata dal campo is_table della struttura ipc_space. Essa può contenere anche uno splay tree104 delle IPC entries, nel qual caso il campo is_tree risulta non-NULL. Notiamo che entrambe le strutture dati sono per-task.

Figura 5-3 Struttura dati dello spazio IPC di un task // osfmk/ipc/ipc_space.h typedef natural_t ipc_space_refs_t; struct ipc_space { decl_mutex_data(,is_ref_lock_data) ipc_space_refs_t is_references; decl_mutex_data(,is_lock_data) boolean_t is_active; // is the space active? boolean_t is_growing; // is the space growing? ipc_entry_t is_table; // table (array) of IPC entries ipc_entry_num_t is_table_size; // current table size struct ipc_table_size *is_table_next; // information for larger table struct ipc_splay_tree is_tree; // splay tree of IPC entries (can be NULL) ipc_entry_num_t is_tree_total; // number of entries in the tree ipc_entry_num_t is_tree_small; // number of "small" entries in the tree ipc_entry_num_t is_tree_hash; // number of hashed entries in the tree boolean_t is_fast; // for is_fast_space() };

In generale i nomi di port right si adattano bene in una tabella perchè il numero di porte che usa un task è tipicamente ridotto. Il Mach permette di rinominare una porta, ed in particolare permette che sia allocata utilizzando un nome specificato dal chiamante. Questo significa che un nome di porta può essere un indice che non rientra nei limiti della tabella del task. Tali rights possono essere allogiati mediante overflow dello splay tree del task. Per minimizzare il consumo di memoria, il kernel aggiusta dinamicamente il limite entro cui le entries sono mantenute nello splay tree. In realtà, la tabella può anche crescere in dimensioni. Quando il kernel accresce la tabella, la espande ad una nuova dimensione

(espressa in numero di table entries) specificata nel campo is_table_next della struttura ipc_space. Il kernel mantiene un array chiamato ipc_table_entries di tali strutture. Questo array, popolato durante l’inizializzazione del sottosistema IPC, è semplicemente una sequenza predefinita di dimensioni della tabella. Uno spazio IPC fast è un caso speciale di IPC space che non usa uno splay tree. Può essere

104 Uno splay tree è un albero di ricerca binario particolarmente efficiente. 156 Il sistema operativo Mac OS X

utilizzato solo se esiste la garanzia che i nomi di porta rientrino nei limiti della tabella. Quando un port right la cui entry è in tabella viene cancellato, l’entry è piazzata in un lista delle unused entries. La lista è mantenuta nella tabella stessa mediante il concatenamento

dei suoi elementi, attraverso i loro campi ie_next. Quando il prossimo port right è allocato, si prende l’ultima entry liberata. Il campo ie_index implementa una tabella hash ordinata per la traduzione dalla coppia {IPC space, IPC object} al nome. Come mostrato in figura 5-2, una entry dello splay tree consiste in una struttura

ipc_entry con i seguenti campi addizionali: nome, spazio IPC e i puntatori ai figli destro e sinistro. Il campo ite_next implementa una open hash table globale utilizzata per la traduzione di una coppia {IPC space, IPC object} in una coppia {name, IPC entry}.

5.2.2 Anatomia di una porta Mach

Una porta Mach è rappresentata nel kernel da un puntatore ad una struttura ipc_port. Il campo ipc_object della struttura IPC entry punta ad una struttura ipc_object, che è una logicamente sovrimpressa su una struttura ipc_port.

Figura 5-4 La struttura interna di una porta Mach 157 Il sistema operativo Mac OS X

I campi di una struttura ipc_port includono un puntatore allo spazio IPC del task che detiene il diritto di ricezione, uno al kernel object che la porta rappresenta e vari reference counts (come make-send count, il numero dei send rights e quello dei send-once rights).

È importante comprendere la differenza tra mach_port_t e mach_port_name_t: questi due tipi sono trattati allo stesso modo nello user space, ma non nel kernel. Un nome

di porta è rilevante solo in un particolare namespace, quindi mach_port_name_t rappresenta l’identità locale di una porta, senza implicare nessun diritto associato. Un

mach_port_t rappresenta un riferimento aggiunto o cancellato ad un port right. Nel kernel i port rights sono rappresentati passando un puntatore (ipc_port_t) alla struttura dati port appropriata.

Se un programma utente riceve un mach_port_name_t dal kernel, significa che il kernel non ha mappato nessun port rights associato: il nome è semplicemente la rappresentazione intera della porta105. Quando, invece, il kernel ritorna un

mach_port_t, mappa i port rights associati al destinatario del messaggio. In entrambi i casi, il programma utente vede lo stesso intero anche se con differenti semantiche sottostanti. La stessa porta può esistere con nomi differenti in diversi task e, inversamente, uno stesso nome di porta può rappresentare differenti porte in differenti task. In un dato namespace, se esistono più diritti per una particolare porta, i nomi posso fondersi in uno solo: in altre parole, un singolo nome può denotare diversi diritti (tranne nel caso dei send-once rights).

Il campo ie_bits della struttura ipc_entry mantiene i tipi di diritti che il dato nome rappresenta. Questo bitmap è quello che permette ad un singolo nome in uno spazio IPC di rappresentare diritti multipli.

// osfmk/mach/mach_port.h typedef natural_t mach_port_right_t; #define MACH_PORT_RIGHT_SEND ((mach_port_right_t) 0) #define MACH_PORT_RIGHT_RECEIVE ((mach_port_right_t) 1) #define MACH_PORT_RIGHT_SEND_ONCE ((mach_port_right_t) 2) #define MACH_PORT_RIGHT_PORT_SET ((mach_port_right_t) 3) #define MACH_PORT_RIGHT_DEAD_NAME ((mach_port_right_t) 4)

105 Il tipo intero usato per rappresentare un nome di porta è storicamente il tipo intero nativo della macchina. Questo tipo si chiama natural_t e vi si accede includendo (che a sua volta vi accede da o , a seconda della macchina). Con l’introduzione dell’ABI Darwin a 64-bit, diversi tipi di dati Mach (come vm_offset_t e vm_size_t) sono stati scalati per coincidere con le dimensioni di un puntatore, tuttavia natural_t è rimasto di 32-bits. 158 Il sistema operativo Mac OS X

#define MACH_PORT_RIGHT_NUMBER ((mach_port_right_t) 5) typedef natural_t mach_port_type_t; typedef mach_port_type_t *mach_port_type_array_t; #define MACH_PORT_TYPE(right) \ ((mach_port_type_t)(((mach_port_type_t) 1) \ << ((right) + ((mach_port_right_t) 16)))) #define MACH_PORT_TYPE_NONE ((mach_port_type_t) 0L) #define MACH_PORT_TYPE_SEND MACH_PORT_TYPE(MACH_PORT_RIGHT_SEND) #define MACH_PORT_TYPE_RECEIVE MACH_PORT_TYPE(MACH_PORT_RIGHT_RECEIVE) #define MACH_PORT_TYPE_SEND_ONCE MACH_PORT_TYPE(MACH_PORT_RIGHT_SEND_ONCE) #define MACH_PORT_TYPE_PORT_SET MACH_PORT_TYPE(MACH_PORT_RIGHT_PORT_SET) #define MACH_PORT_TYPE_DEAD_NAME MACH_PORT_TYPE(MACH_PORT_RIGHT_DEAD_NAME) Anche se i nomi di porta sono di solito assegnati dal kernel, un programma utente può creare un port right con un nome specifico mediante la routine

mach_port_allocate_name(). Un valore mach_port_name_t assegnato dal kernel ha due componenti: index e generation number.

// osfmk/mach/port.h #define MACH_PORT_INDEX(name) ((name) >> 8) #define MACH_PORT_GEN(name) (((name) & 0xff) << 24) #define MACH_PORT_MAKE(index, gen) (((index) << 8) | (gen) >> 24) Se un programma utente deve usare nomi di porta per mapparli nei dati utente, deve usare solo la parte index del port name, che è il layout visibile allo user space del

mach_port_name_t. Il kernel definisce il valore ‘0’ come nome della porta null (MACH_PORT_NULL). La porta null è un valore legale di porta che può essere trasportato nei messaggi per indicare

l’assenza di qualunque porta o port rights. Una porta dead (MACH_PORT_DEAD) indica che un port right era presente ma ora non lo è più. Il suo valore numerico è un natural_t con tutti i bit settati ed è un valore legale di porta che può comparire nei messaggi. Ad ogni modo, questi due valori non rappresentano delle porte valide, mentre tutti gli altri valori lo sono. Il codice di gestione delle IPC entries fornisce delle interfacce per ricercare un IPC object dato il suo nome in un IPC space (utile per spedire un messaggio) e, inversamente, per ricercare il nome di un IPC object in un dato IPC space (utile nella ricezione di un messaggio).

5.2.3 Tasks e IPC I tasks ed i threads Mach cominciano entrambi la loro vita con dei sets di porte Mach standard. Il set di porte standard del task includono le seguenti:

159 Il sistema operativo Mac OS X

• Una self port (conosciuta anche col nome di kernel port del task) rappresenta il task stesso. Il kernel mantiene i receive rights di questa porta.

• Un set di exception ports che include una porta per ogni tipo di eccezione supportata dal kernel. Il kernel spedisce un messaggio alla exception port appropriata quando avviene un’eccezione in uno dei threads del task. Notiamo che le porte d’eccezione esistono anche a livello di thread (più specifiche di quelle a livello task) ed a livello host (meno specifiche). Il kernel cerca sempre di mandare i messaggi d’eccezione alla porta più specifica. Le porte d’eccezione sono utilizzate per implementare sia i meccanismi di gestione degli errori che il debugging.

• Una host port che rappresenta l’host sul quale il task sta girando. • Una bootstrap port usata per spedire messaggi al Bootstrap Server, che è essenzialmente un server dei nomi locale per i servizi accessibili attraverso le porte Mach. I programmi possono contattare il Bootstrap Server richiedendo il ritorno delle altre porte di system service.

• Un set di note porte di sistema (al massimo TASK_PORT_REGISTER_MAX) sono registrate per un task e sono usate dal sistema di runtime per inizializare il task. La

routine mach_ports_register() può essere usata per registrare un array di send rights, con ogni diritto in uno slot dell’array itk_registered contenuto nella struttura task. Quando viene creato un task, una nuova porta è allocata nello spazio IPC del kernel. Il

campo itk_self della struttura task è impostato sul nome di questa porta, mentre il membro itk_sself contiene il send right a tale porta. Il campo itk_space contiene il riferimento al nuovo spazio IPC creato per il task. Il nuovo task eredita le porte registered, exception, host e bootstrap del padre. Il kernel crea dei diritti di spedizione naked106 per il figlio per ognuna di queste porte, a partire dagli esistenti naked rights del padre. Come sappiamo, a parte queste, le porte Mach non sono ereditate durante la creazione di task.

106 Un diritto naked esiste solo nel contesto del kernel task. È chiamato in questo modo perché un tale diritto non è inserito nel port namespace del kernel task. 160 Il sistema operativo Mac OS X

Le figure 5-5 e 5-6 mostrano le strutture dati relative all’IPC associate al task ed al thread, rispettivamente.

Figura 5-5 Strutture dati relative all’IPC associate ad un task Mach // osfmk/mach/ppc/exception.h #define EXC_TYPES_COUNT 10

// osfmk/mach/mach_param.h #define TASK_PORT_REGISTER_MAX 3 // number of "registered" ports

// osfmk/kern/task.h struct task { // task's lock decl_mutex_data(,lock) ... // IPC lock decl_mutex_data(,itk_lock_data) // not a right -- ipc_receiver does not hold a reference for the space // used for representing a kernel object of type IKOT_TASK struct ipc_port *itk_self; // "self" port -- a "naked" send right made from itk_self // this is the task's kernel port (TASK_KERNEL_PORT) struct ipc_port *itk_sself; // "exception" ports -- a send right for each valid element struct exception_action exc_actions[EXC_TYPES_COUNT]; // "host" port -- a send right struct ipc_port *itk_host; // "bootstrap" port -- a send right struct ipc_port *itk_bootstrap; // "registered" port -- a send right for each element struct ipc_port *itk_registered[TASK_PORT_REGISTER_MAX]; // task's IPC space struct ipc_space *itk_space; ... };

Figura 5-6 Strutture dati relative all’IPC associate ad un thread Mach // osfmk/kern/thread.h struct thread { ... struct ipc_kmsg_queue ith_messages; // reply port -- for kernel RPCs mach_port_t ith_rpc_reply; ... // not a right -- ip_receiver does not hold a reference for the space // used for representing a kernel object of type IKOT_THREAD struct ipc_port *ith_self; // "self" port -- a "naked" send right made from ith_self // this is the thread's kernel port (THREAD_KERNEL_PORT) struct ipc_port *ith_sself; // "exception" ports -- a send right for each valid element struct exception_action exc_actions[EXC_TYPES_COUNT]; ... };

5.2.4 Threads e IPC Come un task, anche il thread contiene una self port ed un set di excption port usate per la gestione degli errori. Mentre le nuove porte d’eccezione di un task sono ereditate dal parent, ognuna delle porte d’eccezione del thread sono inizializzate alla porta null al momento della creazione del thread. Le exception ports, sia del task che del thread, 161 Il sistema operativo Mac OS X

possono essere cambiate in seguito in modo programmatico. Se una thread exception port per un tipo specifico d’eccezione è la porta null, il kernel usa la successiva porta più specifica: la corrispondente porta d’eccezione a livello task.

Il campo ith_rpc_reply della struttura thread indica la reply port per le RPC del kernel. Quando il kernel deve spedire un messaggio al thread e ricevere una risposta,

alloca una reply port sel il valore corrente di ith_rpc_reply è IP_NULL.

5.2.5 Allocazione di una porta

Anche se mach_port_allocate() è la routine tipicamente utilizzata per allocare un port right, esistono diverse varianti flessibili come mach_port_allocate_name() e mach_port_allocate_qos() che permettono di specificare maggiori proprietà del nuovo port right.

Tutte queste routines sono casi speciali di mach_port_allocate_full(), che rimane disponibile allo spazio utente.

typedef struct mach_port_qos { boolean_t name:1; // caller-specified port name boolean_t prealloc:1; // preallocate a message buffer boolean_t pad1:30; natural_t len; // length of preallocated message buffer } mach_port_qos_t; kern_return_t mach_port_allocate_full( ipc_space_t space, // target IPC space mach_port_right_t right, // type of right to be created mach_port_t proto, // subsystem (unused) mach_port_qos_t *qosp, // quality of service mach_port_name_t *namep); // new port right's name in target IPC space La routine mach_port_allocate_full() crea uno dei tre tipi di port rights in base al valore passato nell’argomento right: • Un receive right (MACH_PORT_RIGHT_RECEIVE), che è il tipo di diritto più comune che viene creato da questa funzione

• Un empty right (MACH_PORT_RIGHT_PORT_SET) • Un dead name (MACH_PORT_RIGHT_DEAD_NAME) con un riferimento utente È possibile creare un port right con un nome specificato dal chiamante, a patto che il nome non sia già in uso nello spazio IPC e che quest’ultimo non sia ‘fast’. Il chiamante può

specificare il nome passandone un puntatore nell’argomento namep e settando il bit-field

162 Il sistema operativo Mac OS X name nella struttura QoS (Quality of Service) passata. La dimensione del buffer è specificata nel campo len della stessa struttura. Il kernel utilizza, se disponibile, un buffer preallocato della porta quando manda messaggi dal kernel; in questo modo un mittente di messaggi critici può evitare di rimanere bloccato sull’allocazione di memoria.

Figura 5-7 Allocazione di un port right

L’allocazione di un oggetto IPC include i seguenti passi:

• Alloca una struct ipc_object [osfmk/ipc/ipc_object.h] dall’appropriata zona per il tipo di IPC object. Il puntatore a questa struttura è la

163 Il sistema operativo Mac OS X

rappresentazione nel kernel della porta (struct ipc_port [osfmk/ipc/ipc_entry.h]). • Inizializza il mutex nella struttura IPC object.

• Alloca una struct ipc_entry [osfmk/ipc/ipc_entry]. Questa operazione prova in primo luogo di trovare una entry libera, nella tabella dell’IPC space indicato, utilizzando l’hint “first free”. Se non ci dovessero essere entries libere nella tabella, la tabella viene accresciuta.

La routine mach_port_names() può essere utilizzata per recuperare una lista delle porte, con i loro tipi, in uno specifico spazio IPC.

5.2.6 Implementazione del messaging Andiamo ora ad esaminare in che modo il kernel gestisce lo scambio di messaggi. Dato il fatto che la IPC soggiace a molte delle funzionalità del Mach, è ovvio che il messaging sia un’operazione molto frequente nei sistemi basati sul Mach. Non sorprende neanche che una implementazione Mach sia pesantemente ottimizzata, specie quella usata in un prodotto commerciale come il Mac OS X. La funzione kernel fondamentale del messaging

è la mach_msg_overwrite_trap() [osfmk/ipc/mach_msg.c], che contiene numerosi casi speciali che cercano di migliorare le prestazioni nelle varie situazioni. Una delle ottimizzazioni utilizzate è l’handoff scheduling, che consiste nel trasferimento diretto del controllo del processore da un thread ad un altro. Un handoff può essere

eseguito sia dai mittenti che dai destinatari di una RPC. Per esempio, se un server thread è attualmente bloccato in una receive call, un client thread può “passare la palla” al server thread e bloccare se stesso in attesa della risposta. Similmente, quando il server è pronto a mandare una risposta al client, può passare il controllo al client thread e bloccarsi nell’attesa della prossima richiesta. In questo modo è anche possibile evitare di accodare e rimuovere dalla coda i messaggi, visto che un messaggio può essere trasferito direttamente al destinatario. Nel Mach il passaggio del messaggio è assicurato e preserva l’ordine: un messaggio non può essere perso ed è sempre ricevuto nell’ordine con il quale è stato spedito. Tuttavia, il

164 Il sistema operativo Mac OS X kernel consegna i messaggi spediti con i send-once rights fuori ordine e senza assicurarsi della disponibilità nella coda della porta ricevente. La figura 5-8 mostra una visione d’insieme semplificata (ossia priva di casi particolari) dell’elaborazione da parte del kernel riguardante la spedizione di un messaggio. La figura 5-9 mostra l’analogo per la ricezione di un messaggio.

Figura 5-8 Spedizione di un messaggio IPC del Mach

165 Il sistema operativo Mac OS X

Figura 5-9 Ricezione di un messaggio IPC del Mach

5.3 Lo standard POSIX La Single UNIX Specification definisce un set di interfacce IPC come parte delle estensioni X/Open System Interface (XSI). Le interfacce XSI IPC sono essenzialmente le stesse delle precedenti interfacce IPC del ‘System V’, che sono state supportate da un vasto numeri di sistemi UNIX (anche quelli non standardizzati). Il Mac OS X fornisce delle chiamate di sistema per i meccanismi IPC del ‘System V’ riguardanti le code di messaggi, i semafori e la memoria condivisa. Tali specifiche sono un superset delle caratteristiche richieste per lo standard ‘POSIX.1’.

166 Il sistema operativo Mac OS X

Lo standard POSIX 1003.1b-1993 (POSIX93) ha introdotto una serie di interfacce IPC come parte delle estensioni realtime POSIX. Collettivamente conosciute come POSIX IPC, queste interfacce definiscono le funzioni per le code di messaggi, i semafori e la memoria condivisa107. In contrasto con la XSI IPC che utilizza delle chiavi come identificatori IPC, il POSIX usa nomi di stringa per gli oggetti IPC. La Single UNIX Specification regolamenta diversi aspetti dei nomi IPC ma lascia molti parametri liberi e quindi permette una libera implementazione.

• Non è specificato se un nome IPC compare nel file system. Il Mac OS X non richiede che il nome sia presente nel file system e, se lo fosse, la sua presenza non modificherebbe il comportamento delle chiamate POSIX IPC.

• Il Mac OS X permette ad un IPC name di essere al massimo di 31 caratteri

(compreso il carattere di terminazione NUL). • Se un nome IPC inizia con il carattere slash (/), ogni chiamante di una funzione di

apertura (come ad esempio sem_open() o shm_open()) con lo stesso nome si riferisce allo stesso oggetto IPC, fino a che il nome non viene rimosso.

• Se un nome IPC non inizia con il carattere slash, l’effetto dipende dall’implementazione. Il Mac OS X tratta questo caso in modo identico a quello in cui il nome comincia con lo slash.

• L’interpretazione dei caratteri slash, a parte quello iniziale, all’interno dei nomi è definito dall’implementazione. Il Mac OS X tratta un carattere slash in un nome IPC come se fosse un carattere qualsiasi.

Un semaforo POSIX (con nome108) è creato usando sem_open() e distrutto mediante sem_unlink(). La prima è anche utilizzata per connettere il processo chiamante con un semaforo esistente, mentre la sem_close() chiude un semaforo aperto109. I semafori

107 Il Mac OS X 10.4 supportava solo i semafori e la memoria condivisa dello standard POSIX. Con l’avvento del Mac OS X 10.5 “Leopard”, il sistema è stato dichiarato completamente compatibile con POSIX. 108 Oltre ai semafori POSIX con nome, esistono anche semafori POSIX senza nome. Questi possono essere inizializzati e distrutti chiamando sem_init() e sem_destroy(). Queste due chiamate di sistema non sono supportate dal ‘Mac OS X 10.4’. 109 Queste funzioni sono analoghe alle open(), unlink() e close() utilizzate con i files. Infatti, così come open(), anche la funzione sem_open() accetta i flags O_CREAT e O_EXCL per determinare se l’oggetto con nome 167 Il sistema operativo Mac OS X

POSIX sono contatori: un’operazione di lock su un semaforo decrementa il suo valore di uno, mentre una unlock lo incrementa. Nel senso più semplice, un semaforo POSIX è una

variabile intera a cui si accede attraverso due operazioni atomiche: sem_wait() e sem_post(). Dato un semaforo aperto le due precedenti funzioni effettuano, rispettivamente, le operazioni di lock e unlock. Se il valore del semaforo è zero quando è

chiamata la sem_wait(), il chiamante si blocca. Tale blocco può essere interrotto da un segnale. Un segmento di memoria condivisa POSIX con nome può essere creata (o aperta se

preesistente) utilizzando shm_open() e cancellata con shm_unlink(), in modo analogo ai semafori. La prima ritorna un descrittore di file che può essere mappato in

memoria attraverso la chiamata di sistema mmap(). Nel momento in cui l’accesso alla memoria non dovesse essere più necessario, esso può essere unmapped attraverso

munmap() ed il descrittore può essere chiuso con la funzione close(). Per questo gli oggetti di memoria condivisa POSIX assomigliano i files mappati in memoria. Notiamo che la dimensione iniziale di un nuovo segmento è zero. Una dimensione specifica può

essere impostata usando ftruncate(), mentre le informazioni riguardanti un segmento esistente possono essere recuperate chiamando stat() sul descrittore di file ottenuto da shm_open().

deve essere creato o solo acceduto. Ad ogni modo, invece di descrittori di file interi, le funzioni dei semafori trattano puntatori a strutture sem_t. 168 Il sistema operativo Mac OS X

Capitolo 6 Il file system HFS+

Il formato di volume utilizzato per default nel Mac OS X è lo Plus (HFS+). Questo ha sostituito il file system HFS (utilizzato fino al Mac OS 8.1) che a sua volta sosituì il (MFS) dei primi sistemi operativi Macintosh. Noto anche col nome di Mac OS X Extended, mantiene un’architettura simile all’HFS ma fornisce diversi benefici rispetto al suo predecessore.

6.1 Concetti fondamentali Andiamo ora ad analizzare la terminologia e le strutture fondamentali inerenti l’HFS+. Se definiamo un file system come uno schema per organizzare i dati su uno storage medium, il volume rappresenta una istanza di un file system. Un volume HFS+ può occupare un intero disco o usarne solo una porzione, cioè una slice o partition. I volumi HFS+ possono anche abbracciare diversi dischi o partizioni, anche se tale spanning è a livello di device e non specifico dell’HFS+, che continua a vedere un singolo volume logico. In un volume HFS+ lo spazio è allocato nei files in unità fondamentali chiamate allocation blocks. Per ogni volume, la dimensione del blocco di allocazione è un multiplo della

sector size del supporto di memorizzazione110. I blocchi sono concettualmente numerati sequenzialmente. L’implementazione del file system indirizzi i contenuti del volume

110 Delle dimensioni comuni sono 512 bytes per i disk drives e 2KB per gli optical drives. La dimensione di default dell’allocation block è di 4KB, valore ottimale per l’implementazione HFS+ del Mac OS X. 169 Il sistema operativo Mac OS X

utilizzando delle quantità a 32-bit rappresentate dal tipo u_int32_t nel kernel. La dimensione dell’allocation block è una proprietà fondamentale di ogni volume. È possibile scegliere una diversa dimensione al momento della creazione di uno nuovo file system

HFS+, mediante il programma newfs_hfs, a patto di rispettare le seguenti regole: • Deve essere una potenza del 2. • Deve essere un multiplo della sector size dello storage device.

• Deve essere minore di MAXBSIZE, definito in sys/param.h ad 1MB111. È possibile che la capacità di un volume non sia un multiplo della dimensione del suo blocco di allocazione. In tal caso ci sarà uno spazio finale del volume che non sarà coperto da nessun allocation block.

Figura 6-1 Un disco contenente due volumi HFS+

111 Non si tratta di una limitazione del file system HFS+, ma del programma newfs_hfs. Per superare questo limite, se davvero necessario, si dovrà utilizzare una versione modificata del programma (o un programma sostitutivo) per creare il file system. 170 Il sistema operativo Mac OS X

Un blocco di allocazione non può essere condiviso tra due files e neanche tra due forks dello stesso file112. Un extent è una sequenza contigua di allocation blocks ed è rappresentata dell’HFS+ da

una struttura dati extent descriptor (struct HFSPlusExtentDescriptor [bsd/hfs/hfs_format.h]). Tale descrittore contiene una coppia di numeri: il numero del blocco da cui parte l’extent ed il numero di blocchi contenuti in esso.

struct HFSPlusExtentDescriptor { u_int32_t startBlock; // first allocation block in the extent u_int32_t blockCount; // number of allocation blocks in the extent }; typedef struct HFSPlusExtentDescriptor HFSPlusExtentDescriptor;

typedef HFSPlusExtentDescriptor HFSPlusExtentRecord[8]; Un array di otto extent descriptors costituisce un extent record. L’HFS+ usa l’extent record come lista d’extent inline per i contenuti di un file, cioè fino ad i primi otto extents di un file sono memorizzati come parte dei metadata di base del file. Per un file che contenga più di otto extents, l’HFS+ mantiene uno o più extent records aggiuntivi, ma non inline nei metadata. Un file è tradizionalmente equivalente ad un singolo stream di bytes. L’HFS+ supporta byte-streams multipli per un file, con due streams speciali sempre presenti (anche se uno o entrambi possono essere vuoti): il data fork ed il . Ogni fork è una parte distinta del file e può essere percepita come un file a se stante. Una fork è rappresentata

nell’HFS+ dalla struttura HFSPlusForkData [bsd/hfs/hfs_format.h].

struct HFSPlusForkData { u_int64_t logicalSize; // fork's logical size in bytes u_int32_t clumpSize; // fork's clump size in bytes u_int32_t totalBlocks; // total blocks used by this fork HFSPlusExtentRecord extents; // initial set of extents }; typedef struct HFSPlusForkData HFSPlusForkData; Entrambe le forks hanno le proprie strutture HFSPlusForkData (e quindi l’extent records) che sono memorizzate insieme ai metadata standard del file. La visione tradizionale di un file riguarda la sua data fork. Molti files nell’istallazione tipica del Mac OS X hanno solo questa, in quanto la loro resource fork è vuota. Un altro aspetto degno di nota è che i nomi delle forks data e resource non possono essere

112 L’UFS del BSD (compreso quello implementato nel Mac OS X) impiega un'altra unitò di allocazione oltre al blocco: il fragment. Il frammento è una frazione del blocco che permette la condivisione del block tra diversi files. 171 Il sistema operativo Mac OS X

cambiati. Ogni byte-stream aggiuntiva creata ha un proprio nome Unicode. Queste streams con nome possono avere contenuti arbitrari, anche se un’implementazione HFS+ può limitare il quantitativo di dati che possono contenere. Così come un allocation block è un gruppo a dimensione fissa (per un dato volume) di settori contigui, un clump è un gruppo a dimensione fissa di allocation blocks contigui. Anche se ogni clump è un extent, non è vero che ogni extent è un clump. Quando si alloca spazio per una fork, un’implementazione HFS+ può farlo in termini di clumps, invece di allocation blocks individuali, per evitare la frammentazione esterna.

6.1.1 B-Trees L’HFS+ utilizza i B-Trees per implementare le strutture dati critiche per l’indexing, che permettono di localizzare sia il contenuto del file che i metadata residenti in un volume. Un B-Tree è una generalizzazione di un albero di ricerca binario bilanciato. Mentre un albero binario ha un fattore di ramificazione di due, un B-Tree può avere un fattore di ramificazione arbitrariamente grande. Questo è possibile grazie all’uso di nodi molto grandi: un nodo B-Tree può essere pensato come una incapsulazione di vari livelli di un albero binario. Il fatto di avere dei fattori di ramificazioni molto grandi, comporta una loro altezza molto ridotta e quindi sono l’ideale nel caso di utilizzo in periferiche ad accesso costoso (come un disk drive). Nello specifico, l’HFS+ utilizza una variante dei B+ Trees, che a loro volta sono una

variante dei B-Trees. In un B+ Tree tutti i dati risiedono nei nodi foglia (esterni), con i nodi indice (interni) contenenti solo chiavi e puntatori ai sottoalberi. Di conseguenza i nodi foglia (leaf) ed indice (index) possono avere differenti formati e dimensioni. I nodi foglia, che giacciono tutti sullo stesso livello in un albero bilanciato, sono concatenati insieme in una lista per formare un sequence set. La implementazione dei B+ Trees nell’HFS+ differisce da quella standard nella struttura dei nodi indice. In un B+ Tree

standard, un index node I contenente N chiavi ha N+1 puntatori (uno per ognuno dei suoi N+1 figli). In particolare il primo (leftmost) puntatore punta al figlio (subtree) contenente le chiavi minori della prima chiave del nodo I. Nell’implementazione HFS+ tale puntatore

172 Il sistema operativo Mac OS X non esiste, poiché non esiste il sottoalbero contenente chiavi minori di I. Nonostante utilizzi diversi B-Trees con chiavi e formati differenti, l’implementazione HFS+ utilizza la stessa struttura di base in tutti gli alberi. Di conseguenza la maggior parte delle operazioni sui diversi alberi possono essere eseguite con lo stesso set di funzioni. Andiamo ora ad analizzare alcune delle caratteristiche comuni ai vari B-Trees:

• Ogni B-Tree è implementato come un file speciale che non è mai visibile all’utente ne accessibile attraverso interfacce standard. Diversamente dai files regolari che hanno due forks predefinite, un file speciale ha una sola fork. Fa eccezione il B- Tree Hot File Clustering che nonostante sia un file di sistema, è implementato come file regolare visibile all’utente e con due forks predefinite (anche se la fork resource è vuota).

• Lo spazio totale in un file B-Tree è concettualmente diviso in nodes della stessa dimensione. Ogni B-Tree ha una dimensione fissata del nodo che deve essere una potenza del 2 (dai 512 ai 32˙768 bytes). La dimensione è determinata al momento della creazione del volume e non può essere modificata senza riformattarlo.

• I nodi sono numerati sequenzialmente a partire da zero. L’offset di un nodo N in un dato B-Tree è ottenuto moltiplicando N per la dimensione del nodo. Il numero di nodo è rappresentato da un unsigned integre a 32-bit.

• Ogni B-Tree ha un singolo header node (di tipo kBTHeaderNode) che è il primo nodo dell’albero.

• Ogni B-Tree ha zero o più map nodes (di tipo kBTMapNode) che sono essenzialmente dei bitmaps di allocazione usati per tracciare quale nodo è in uso e quale è libero. La prima parte del bitmap di allocazione risiede nell’header node, quindi ne sono necessari uno o più map nodes solo se l’intero bitmap non rientra nello spazio dell’header.

• Ogni B-Tree ha zero o più index nodes (di tipo kBTIndexNode) che contengono keyed pointer records che portano ad altri nodi indice o foglia. Gli index nodes sono anche chiamati internal nodes.

• Ogni B-Tree ha uno o più leaf nodes (di tipo kBTLeafNode) che contiene keyed

173 Il sistema operativo Mac OS X

records contenenti i dati associati alle chiavi.

• Tutti i tipi di nodo posono contenere records di lunghezza variabile.

Figura 6-2a La struttura di un nodo di un B-Tree HFS+

La struttura del nodo mostrata in figura 6-2a è condivisa da tutti i tipi di nodo. C’è un descrittore di nodo struct BTNodeDescriptor [bsd/hfs/hfs_format.h] all’inizio di ogni nodo. I campi bLink e fLink della struttura concatenano insieme nodi di tipo particolare (indicato dal campo kind). Subito dopo il descrittore c’è il records segment che contiene i records del nodo. Poiché tali records possono essere di dimensioni differenti, la parte finale del nodo contiene una lista di offsets a 16-bit, ognuno dei quali è

174 Il sistema operativo Mac OS X l’offset di un record a partire dall’inizio del nodo. L’ultima vode della lista è l’offset dello spazio non utilizzato (se non ci dovesse essere spazio inutilizzato, tale elemento persisterebbe e punterebbe a se stesso).

Figura 6-2b La struttura di un nodo header di un B-Tree HFS+

La figura 6-2b mostra la struttura di un header node. In un B-Tree l’header node è formato da tre records:

• L’header record contenente informazioni generali sul B-Tree, come la dimensione del nodo, la sua profondità (o altezza… dipende dai punti di vista), il numero del root node (se esistente), il numero di records foglia ed il numero totale di nodi.

175 Il sistema operativo Mac OS X

• Lo user data record che fornisce 128 bytes di spazio per memorizzare informazioni arbitrarie associate all’albero. Di tutti i B-Trees HFS+, solo l’Hot File Clustering usa questa area.

• Il map record contenente una bitmap che indica, mediante i suoi bit, se un nodo nell’albero è in uso oppure no. Un albero può avere più nodi di quelli rappresentati dal map record dell’header node. La somma delle dimensioni del descrittore di nodo (14 bytes), l’header record (106 bytes), lo user data record (128 bytes) e le offset entries (4*2 bytes) è 256 bytes. Lo spazio restante rimane per il map record. Se dovesse essere necessario ulteriore spazio, l’albero usa i map nodes per ospitare le estensioni del bitmap. Se un albero ha uno o più map nodes, il campo fLink dell’header node conterrà il numero del map node successivo o zero se è l’ultimo della lista. I campi bLink di tutti i map nodes, e quello dell’header node, sono sempre settati a zero. Mentre l’header ed i map nodes di un B-Tree contengono informazioni amministrative, i nodi indice e foglia contengono informazioni del file system (dipendenti dallo specifico B- Tree in questione). I records nei nodi indice e foglia hanno, inoltre, la stessa struttura generale (vedi figura 6-2c).

Figura 6-2c La struttura di un record di un B-Tree HFS+

All’inizio della struttura del record c’è la lunghezza della chiave (keyLength), memorizzata con uno o due bytes in funzione del bit kBTBigKeysMask nel campo attributes dell’header (bit clear=1 byte, bit set=2 bytes). Immediatamente dopo la

176 Il sistema operativo Mac OS X lunghezza c’è la chiave attuale. Il fatto che la key length rappresenti o meno la lunghezza della chiave attuale è determinato sulle seguenti basi:

• Nel caso di un nodo foglia, keyLength rappresenta la actual key length. • Nel caso di un nodo indice, keyLength rappresenta la actual key length se il bit kBTVariableIndexKeyMask è settato nel campo attributes del nodo header.

• Se il bit kBTVariableIndexKeyMask non è settato nel campo attributes del nodo header, la actual key length è il valore costante contenuto nel campo

maxKeyLength del nodo header. I nodi indice e foglia contengono solo records indice e foglia, rispettivamente. Poiché si tratta di B+ Trees, i dati attuali sono memorizzati solo nei nodi foglia. I dati di un record indice si riducono ad un semplice numero di nodo (un puntatore ad un altro nodo indice o foglia). In altre parole, i nodi index costituiscono nel loro insieme un indice per effettuare ricerche arbitrarie tra i dati memorizzati nei nodi foglia. Una operazione fondamentale riguardante l’accesso e la manipolazione di un B-Tree è la key comparison. Un’importante proprietà di un nodo B-Tree è che tutti i suoi records sono memorizzati secondo l’ordine crescente delle loro chiavi. Per le chiavi semplici (come ad esempio degli interi) la comparazione si riduce ad una semplice comparazione numerica. Nel caso di chiavi più complesse, come quelle utilizzate nei B-Trees dell’HFS+, la presenza di svariate componenti comporta operazioni di comparazione più complicate. I vari componenti delle chiavi complesse hanno, tipicamente, dei valori di precedenza assegnati. Quando due chiavi complesse (ovviamente dello stesso tipo) sono comparate, i componenti individuali sono comparati in ordine decrescente di precedenza: se una comparazione individuale produce una identità, la comparazione globale prosegue con la componente successiva; questo procedimento prosegue fino a che non si verifica una disequità o fino all’esaurimento delle componenti, caso in cui le chiavi sono considerate uguali.

177 Il sistema operativo Mac OS X

Figura 6-3 Il contenuto di un ipotetico B-Tree HFS+

La figura 6-3 mostra un B-Tree ipotetico che usa delle chiavi integrali a lunghezza fissa, con altezza pari a 3: in generale i nodi foglia, ognuno dei quali allo stesso livello e quindi alla stessa altezza, hanno un’altezza assegnata di 1; un nodo indice immediatamente sopra le foglie ha 2 come altezza e così via.

Il nodo indice con l’altezza massima è il nodo radice, riferito dal campo rootNode dell’header record. L’altezza di ogni nodo è memorizzata nel campo height del proprio descrittore: nell’header il campo height è posto a zero mentre il campo treeDepth

178 Il sistema operativo Mac OS X

contiene l’altezza dell’albero (ossia quella del root node). In un albero vuoto non c’è il nodo radice. Un nodo radice non deve essere necessariamente un nodo indice: se tutti i records sono contenuti all’interno di un singolo nodo, questo sarà sia il nodo radice che l’unico nodo foglia.

6.2 La struttura di un volume HFS+ La figura 6-4 mostra la struttura di un volume HFS+. A parte i files regolari e le directories, un volume HFS+ può contenere le seguenti entità:

• Reserved areas presente all’inizio ed alla fine del volume. • Il volume header contenente una varietà di informazioni sul volume, inclusa la posizione delle altre strutture chiave del volume. Alla fine del volume è presente una copia di queste informazioni denominata alternate volume header.

• Il Catalog B-Tree che immagazzina i metadata di base per files e directories, incluso il primo extent record per ogni file. La struttura gerarchica del file system è inoltre catturata nel Catalog B-Tree attraverso dei records che memorizzano le relazioni padre-figlio intercorrenti tra gli oggetti del file system.

• L’Extent Overflow B-Tree che immagazzina gli extent records eccedenti per quei files che hanno più di otto extents.

• L’Attribute B-Tree che immagazzina gli attributi estesi per files e directories. • L’Allocation file che è una bitmap contenente un bit per ogni allocation block, indicante se il blocco è in uso o meno.

• Il private metadata folder utilizzato per implementare gli hard links e per memorizzare i files che sono stati cancellati mentre erano aperti

(/\xC0\x80\xC0\x80\xC0\x80\xC0\x80HFS+ Private Data). • L’Hot Files B-Tree utilizzato dal meccanismo di ottimizzazione Hot File Clustering per registrare informazioni riguardo ai files acceduti frequentemente

(/.hotfiles.btree). • Lo Startup file concepito per contenere informazioni arbitrarie che un sistema operativo può utilizzare per eseguire il boot da un volume HFS+.

179 Il sistema operativo Mac OS X

• I Journal files utilizzati per mantenere informazioni riguardo il file system journal

(/.journal_info_block) ed il contenuto del journal stesso (/.journal). • I Quota files utilizzati per mantenere informazioni concernenti le user quotas

(/.quota.user) e le group quotas (/.quota.group) a livello del volume.

Figura 6-4 La struttura di un volume HFS+

6.2.1 Frammentazione La frammentazione in un file system è sempre stata un importante freno alle prestazioni, anche se i moderni file systems sono generalmente meno inclini a frammentarsi rispetto ai loro predecessori. Numerosi algoritmi e schemi sono stati incorporati all’interno dei file

180 Il sistema operativo Mac OS X systems per ridurre la frammentazione, sia nella sua formazione che rimuovendo quella esistente. In un tipico scenario, un sistema operativo usa un disk drive in modo tale che lo spazio di memorizzazione del drive appaia come una sequenza di blocchi logicamente contigua. Le prestazioni dei moderni driver sono migliori quando le richieste di I/O hanno dimensioni maggiori. Una maggiore contiguità nell’allocazione del file permette maggiori richieste di I/O (oltre a ridurre ogni CPU overheads), comportando quindi migliori prestazioni di I/O sequenziale. Appare quindi chiaro che la contiguità dei dati in un disco è desiderabile. La frammentazione esiste in diverse forme, dipendenti dal contesto:

• User-level data fragmentation – anche se un file è contiguo sul disco, potrebbe contenere informazioni che non sono contigue a livello utente. Per esempio un documento word processor può essere contiguo sul disco ma non nel modo in cui il word processor lo legge. Quantificare o gestire tale frammentazione è un’operazione complessa, ma degna di nota, poiché dipende dall’applicazione in questione, il formato del file ed altri fattori difficili da controllare.

• Internal fragmentation – in un volume con blocchi di allocazione da 4KB, un file di 1 byte andrebbe ad “utilizzare” 4096 bytes su disco: di conseguenza i restanti 4095 bytes andrebbero sprecati (a meno di una crescita successiva delle dimensioni del file). Questo spreco di spazio è quello che comunemente viene indicato come frammentazione interna.

• External fragmentation – la frammentazione esterna è quella a cui tutti si riferiscono quando parlano di frammentazione. Un file è esternamente frammentato se non tutto il suo contenuto risiede in blocchi contigui al livello del volume. Possiamo considerare un frammento come sinonimo di un extent HFS+. In altre parole, un file non frammentato ha esattamente un extent: ogni extent addizionale introduce una discontinuità nel file.

181 Il sistema operativo Mac OS X

6.3 Particolarità dell’HFS+ Di default, l’HFS+ è un file system case-preserving ma case-insensitive, nonostante il fatto che i file systems tradizionali UNIX siano case-sensitive. Per codificare i nomi dei files, delle directories e gli attributi estesi utilizza l’Unicode. I nomi dei files e delle directories

sono rappresentati dalla struttura HFSUnitStr255, che consiste di 16-bit seguiti da fino a 255 caratteri (di due bytes) Unicode.

6.3.1 Permessi Il sistema dei permessi del file system HFS+ è nello stile UNIX. Entrambe le strutture

HFSPlusCatalogFile e HFSPlusCatalogFolder includono una struttura HFSPlusBSDInfo che incapsula le informazioni relative al possesso, i permessi ed il tipo del file.

struct HFSPlusBSDInfo { u_int32_t ownerID; // owner ID 99 ("unknown") is treated as the user ID // of the calling process (substituted on the fly) u_int32_t groupID; // group ID 99 ("unknown") is treated as the owner ID // of the calling process (substituted on the fly) u_int8_t adminFlags; // superuser-changeable BSD flags, see chflags(2) u_int8_t ownerFlags; // owner-changeable BSD flags, see chflags(2) u_int16_t fileMode; // file type and permission bits union { u_int32_t iNodeNum; // indirect inode number for hard links u_int32_t linkCount; // links that refer to this indirect node u_int32_t rawDevice; // device number for block/character devices } special; }; Mentre i permessi sono obbligatori in un volume root, possono essere deattivati su un volume HFS+ nonroot. Disabilitare i permessi assegna essenzialmente la proprietà dei files e dei folders del volume ad un singolo user ID, il cosiddetto “replacement user ID”113.

Questo può essere esplicitamente indicato, altrimenti il kernel userà UNKNOWUID, lo user ID sconosciuto (99). La proprietà speciale di quest’ultimo è che verifica tutti gli user ID quando comparato per la determinazione dei diritti di possesso. La figura 6-5 mostra come l’implementazione HFS+ del Mac OS X determina se un dato processo ha i diritti di possesso di un oggetto del file system.

113 La sostituzione è puramente comportamentale. Ogni oggetto del file system mantiene il proprio owner ID originale. La struttura hfsmount mantiene in memoria il replacement ID. 182 Il sistema operativo Mac OS X

Figura 6-5 Algoritmo per la determinazione degli ownership rights su un file system object

L’installer del Mac OS X usa un bill of materials (bom) per ogni pacchetto istallato. Si

tratta di un file contenente una lista di tutti i files nella directory ed i metadata per ogni file. In particolare, il file bom contiene i permessi UNIX di ogni file.

6.3.2 Journaling L’HFS+ supporta il journaling dei metadata, incluse le strutture dati del volume, in cui le modifiche del file system sono registrate in un file di log (il journal) che è implementato

come un buffer circolare su disco114. Lo scopo primario di un journal è di assicurare la

114 Siccome il meccanismo di journaling scrive le modifiche prima nel file journal e poi nei blocchi di destinazione (tipicamente in un buffer cache), si dice che compia un write-ahead journaling. 183 Il sistema operativo Mac OS X consistenza del file system in caso di guasto. Alcune operazioni del file system semanticamente atomiche possono avere una quantità di I/O considerevole. Se si dovesse verificare un guasto prima che tutte le modifiche siano riportate sulla memoria fisica, il file system si troverebbe in uno stato di inconsistenza. Il journaling permette di raggruppare le modifche tra loro correlate in transactions registrate nel file journal. Il journalling è stato inserito nell’HFS+ introducendo nel kernel uno strato di journaling al livello del VFS [bsd/vfs/vfs_journal.c]. Questo strato esporta una interfaccia che può essere usata da qualsiasi file system per incorporare il journal.

Figura 6-6 Interfaccia di journaling dello strato VFS nel Mac OS X

La figura 6-6 mostra una visione di insieme dell’interfaccia di journaling ed il suo utilizzo da parte dell’HFS+. Notiamo che dal punto di vista del journal, le modifiche sono eseguite in unità di journal block size, che deve essere specificato al momento della creazione del journal. L’HFS+ usa la dimensione del blocco fisico (tipicamente 512 bytes per i dischi) come dimensione del blocco journal. Quando l’HFS+ ha bisogno di modificare uno o più blocchi come parte di un’operazione, inizia una journal transaction per incapsulare le

184 Il sistema operativo Mac OS X modifiche relative; quando tutti i blocchi nella transazione sono stati modificati, il file system termina la transazione. Il volume header in un HFS+ journaled contiene la posizione di una struttura dati chiamata journal info block, che a sua volta contiene la locazione e la dimensione del journal appropriato consistente in un header ed un buffer circolare. Sia l’info block che il journal sono memorizzati come files contigui (occupanti esattamente un extent ciascuno), precisamente in .journal_info_block e .journal rispettivamente. Questi files sono normalmente invisibili e non direttamente accessibili attraverso le APIs del file system. La figura 6-7 mostra la struttura, indipendente dallo specifico file system, dei files di journal.

Figura 6-7 Journal dell’HFS+

185 Il sistema operativo Mac OS X

6.3.3 Deframmentazione on-the-fly Quando un file utente viene aperto in un volume HFS+, il kernel controlla se il file è qualificato per la deframmentazione “al volo”. Per essere definito eleggibile un file deve soddisfare le seguenti condizioni:

• Il file system non è a sola lettura. • Il file system è journaled. • Il file è un file regolare. • Il file non è già aperto. • La file fork acceduta è nonzero e non supera i 20MB di dimensione. • La fork è frammentata in otto o più extents. • Il sistema è stato attivo per più di tre minuti (per assicurasi che il bootstrp sia terminato). Se tutte queste condizioni sono verificate, il file è rilocato dalla chiamata

hfs_relocate() [bsd/hfs/hfs_readwrite.c], che cerca di trovare blocchi di allocazione contigui per il file. Un rilocazione riuscita comporta un file deframmentato.

6.3.4 Zona metadata L’implementazione HFS+ del Mac OS X 10.3 introdusse una politica di allocazione che riserva spazio per diverse strutture metadata del volume, piazzandole una vicina all’altra, se possibile, in un’area (la metadata allocation zone) nella parte iniziale del volume. Fino a che lo spazio non scarseggia, l’allocatore HFS+ non consuma lo spazio della zona metadata per l’allocazione dei files normali. In modo simile, fino a che non la zona metadata non si esaurisce, l’HFS+ allocherà spazio per i metadata nella zona. Quindi vari tipi di metadata sono probabilmente adiacenti fisicamente ed hanno, in generale, un’alta contiguità: la conseguente riduzione dei seek times andranno a migliorare le prestazioni di sistema. La policy è abilitata per un volume (journaled di almeno 10GB) al runtime

quando il volume è montato. La struttura hfsmount memorizza dettagli runtime della zona metadata.

186 Il sistema operativo Mac OS X

Figura 6-8 Layout della zona metadata dell’HFS+

6.3.5 Hot File Clustering L’Hot File Clustering (HFC) è uno schema adattativo di clustering multistage basato sulla premessa che i files acceduti di frequente siano ridotti sia in numero che in dimensione. Tali files sono chiamati hot files nel contesto dell’HFC. Come parte dell’HFC, l’implementazione Apple dell’HFS+ esegue le seguenti operazioni per migliorare le prestazioni durante l’accesso agli hot files:

• Mantiene traccia dei blocchi letti dalle file forks per identificare gli hot files candidati.

• Dopo un periodo predeterminato di registrazione, analizza la lista dei candidati per determinare quali debbano essere spostati nell’area Hot File nell’ultima parte della zona metadata (figura 6-8).

• Se necessario, toglie il files esistenti dall’area Hot File per far spazio a files più nuovi e più caldi.

• Sposta gli hot files selezionati nell’area Hot File, allocando spazio contiguo all’atto del loro trasferimento. L’HFC memorizza la temperature di gni file, che è definita come il rapporto del numero di bytes letti dal file durante il periodo di registrazione e la dimensione del file: di

187 Il sistema operativo Mac OS X conseguenza maggiore è la frequenza con cui il file è acceduto, maggiore è la sua temperatura. L’HFS+ utilizza il campo clumpSize della struttura HFSPlusForkData per memorizzare la quantità di dati letti da una fork.

Figura 6-9 Schema delle trasizioni tra le varie fasi dell’Hot File Clustering

In ogni momento, l’HFC su un volume si trova in uno delle seguenti fasi:

• HFC_DISABLED – l’HFC è attualmente disabilitato, generalmente perché non si tratta di un volume root oppure se il volume è stato smontato mentra l’HFC era nella fase di registrazione.

188 Il sistema operativo Mac OS X

• HFC_IDLE – l’HFC è in attesa di iniziare la registrazione. Si entra in questa fase dopo che l’HFC è stato inizializzato durante il montaggio.

• HFC_BUSY – è una fase temporanea in cui si trova l’HFC mentra lavora sulla transizione da una fase all’altra.

• HFC_RECORDING – l’HFC sta registrando le temperature dei files. • HFC_EVALUATION – l’HFC ha finito di registrare le temperature e sta elaborando la nuova lista degli hot files.

• HFC_EVICTION – l’HFC sta rilocando i files più freddi e vecchi per recuperare dello spazio nell’area Hot File.

• HFC_ADOPTION – l’HFC sta rilocando i files più caldi e nuovi nell’area Hot File. Se la fase corrente è adoption, eviction, idle o recording, la transizione alla fase successiva è innescata come effetto collaterale di un’operazione di sincronizzazione sul volume. L’HFC utilizza un B-Tree, chiamato Hot Files B-Tree, per tenere traccia degli hot files o, specificatamente, le file forks. A differenza degli altri files speciali dell’HFS+, le extents di quest’albero non sono memorizzate nel volume header: si tratta di un file su disco a cui il kernel accede attraverso il suo pathname ‘/.hotfiles.btree’. l’Hot Files B-Tree è simile al Catalog B-Tree nel fatto che ogni fork è tracciata come un Thread Record ed un Hot File Record.

189 Il sistema operativo Mac OS X

Figura 6-10 Ricerca nell’ Hot Files B-Tree

Figura 6-11 Restrizioni adottate dall’Hot File Clustering Name Value Notes HFC_BLKSPERSYNC 300 The maximum number of allocation blocks that can be movedwhether for eviction or adoptionduring a single sync-triggered HFC operation. Adoption does not move only parts of a file; therefore, this effectively limits the size of a hot file to 1.2MB for the default allocation block size of 4KB. HFC_FILESPERSYNC 50 The maximum number of files that can be moved during adoption or eviction. HFC_DEFAULT_DURATION 60 h The default temperature-recording duration. HFC_DEFAULT_FILE_COUNT 1000 The default number of hot file sto track. HFC_MAXIMUM_FILE_COUNT 5000 The upper limit on the number of hot files to track. HFC_MAXIMUM_FILESIZE 10 MB The upper limit on the size of file sto track during recording. Files larger than this will not be tracked. HFC_MINIMUM_TEMPERATURE 24 The threshold temperature for residency in the Hot File area.

190

Bibliografia

[1] The Apple Museum 2006 “History”

URL: www.theapplemuseum.com/index.php?id=53 [2] Amit Singh 2006 “Mac OS X Internals: A System Approach” Ed. Addison Wesley URL: www.osxbook.com [3] Apple Developer Connection 2005-2007 “Guides – Mac OS X” URL: developer.apple.com/documentation/MacOSX/index.html

191