UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO...

52
UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIA Dipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea in Informatica Analisi dei problemi di latenza legati all’I/O nei sistemi Android, e prime soluzioni. Relatore: Candidato: Dott. Paolo Valente Samuele Zecchini Anno Accademico 2014/2015

Transcript of UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO...

Page 1: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

UNIVERSITÁ DEGLI STUDI DI

MODENA E REGGIO EMILIA

Dipartimento di Scienze Fisiche,Informatiche e

Matematiche

Corso di Laurea in Informatica

Analisi dei problemi di latenza legati all’I/O nei

sistemi Android, e prime soluzioni.

Relatore: Candidato: Dott. Paolo Valente Samuele Zecchini

Anno Accademico 2014/2015

Page 2: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

2

Indice: RINGRAZIAMENTI ................................................................................................................................................ 3

1. INTRODUZIONE ................................................................................................................................................. 4

2. IL SISTEMA ANDROID ....................................................................................................................................... 5

2.1 LIVELLO APPLICATIVO .................................................................................................................... 5

2.2 ANDROID RUNTIME ........................................................................................................................... 8

2.3 ANDROID KERNEL ............................................................................................................................10

3. BUDGET FAIR QUEUEING (BFQ) ....................................................................................................................12

4. PROBLEMA: LE APP SPERIMENTANO ALTA LATENZA ............................................................................14

5. PICCOLA SUITE PER RIPRODURRE IL PROBLEMA IN MODO SISTEMATICO E CONTROLLABILE

..................................................................................................................................................................................16

5.1 LO SCRIPT ...........................................................................................................................................16

5.1.1 INTRODUZIONE ALLE OPZIONI ....................................................................................16

5.1.2 GESTIONE TRACCE KERNEL .........................................................................................19

5.1.3 BACKGROUND WORKLOAD ...........................................................................................20

5.1.4 APPLICAZIONI OGGETTO DI STUDIO ..........................................................................21

5.2 LPROBE ...............................................................................................................................................23

6. CAUSE DEL PROBLEMA ..................................................................................................................................26

6.1 CARATTERISTICHE DEL DEVICE UTILIZZATO NEGLI ESPERIMENTI ................................. 27

6.2 PRIMO INSIEME DI CAUSE ................................................................................................28

6.2.1 JOURNALING .....................................................................................................................28

6.2.2 DIRTY PAGE BALANCING E THROTTLING .................................................................31

6.2.3 RACE CONDITION SEMAFORO INODE ........................................................................34

6.3 SOLUZIONI E RISULTATI SPERIMENTALI ................................................................................... 37

6.3.1 SOLUZIONI PROPOSTE .................................................................................................... 37

6.3.2 RISULTATI SPERIMENTALI .............................................................................................41

6.4 SECONDO INSIEME DI CAUSE ........................................................................................................43

6.4.1 JOURNALING II .................................................................................................................43

7. CONFRONTO TRA SCHEDULER ..................................................................................................................... 47

8. CONCLUSIONI E FUTURI SVILUPPI ..............................................................................................................49

BIBLIOGRAFIA…………………………………………………………………………………..50

Page 3: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

3

Ringraziamenti

Ringrazio Sara Arioli per avermi accompagnato e sostenuto in questo lungo e tortuoso cammino.

Ringrazio la mia famiglia per avermi permesso di intraprendere l’università.

Ringrazio Micaela Verucchi per essere stata un’amica fedele oltre che la compagna di tutti progetti

universitari.

Ringrazio tutto il team di Algodev per essere stato come un seconda famiglia. In particolare

ringrazio Paolo Valente e Arianna Avanzini per l’instancabile sostegno e aiuto.

Page 4: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

4

1. Introduzione

In questo elaborato mi propongo di analizzare, studiare e proporre una prima soluzione ai problemi

di alte latenze che si manifestano in un dispositivo Android in presenza di I/O in background. La

latenza ai nostri scopi si può definire distinguere in due classi: quella relativa ai task real-time (non

oggetto di studio nell'elaborato) e quella relativa ai task interattivi, in cui possiamo vederla come la

responsiveness di un'applicazione o dell'interno sistema, se riusciamo a garantire una bassa latenza

possiamo garantire anche una alta responsiveness. Questo problema ha raggiunto un elevata

importanza ed è stato trattato in conferenze internazionali come quella svoltasi al FAST 2015

(conferenza internazionali sullo storage) [1].

Tutti attualmente abbiamo uno smartphone, un tablet o altri dispositivi che sono diventati parte

integrante della nostra quotidianetà. Ognuno di noi in base all'utilizzo che ne fa, più o meno intenso,

ha vissuto la situazione in cui il telefono raggiunge un punto di stallo o non risponde più ai comandi

oppure ha avuto la percezione di tempi di attesa per l'avvio di un'applicazione molto lunghi. Tutte

queste situazioni, in cui si nota appunto un alta latenza del sistema, possono essere dovute a un

grande carico di I/O che alza inesorabilmente i tempi di risposta del dispositivo.

Ad oggi Android è il sistema operativo più utilizzato su device quali smartphone, tablet, smart glass,

inoltre tale sistema si base sul kernel linux che, essendo open source, può essere scaricato e

modificato. I codici sorgenti di Android sono liberamente scaricabili e modificabili tramite

AOSP(Android Open Source Project) fornito da Google. Per queste ragioni si è scelto di lavorare su

questo tipo di sistema.

Il lavoro di tesi è stato svolto interamente all'Università degli studi di Modena e Reggio Emilia sotto

la supervisione del dottor Paolo Valente e con la collaborazione di Riccardo Pizzetti.

Nell'elaborato dopo una breve introduzione del sistema Android e dello scheduler che meglio

sembra affrontare i problemi di latenza, illustrerò la lista delle cause di alta latenza che sono riuscito

a mettere in risalto durante il tirocinio, nonché verranno esposte le soluzioni proposte ed esaminati i

problemi ancora aperti.

Infine ci sarà un confronto dei risultati ottenuti con vari scheduler del disco, basati su

un’applicazione ultra semplificata che rispecchia solo parzialmente una situazione reale.

Page 5: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

5

2. Il sistema Android

Analizziamo l'architettura del sistema Android [2].

2.1 Livello applicativo

Iniziamo dal primo strato, il livello applicativo.

Un'applicazione (app) consiste in un insieme di componenti tra loro correlati. I componenti di un

applicazione possono comunicare con quelli di altre applicazioni.

I componenti fondamentali di un app sono:

Activities:

esse sono i blocchi principali che costituiscono una app: gestiscono tutto quel che riguarda la

GUI e l'interattività con l'utente. Un activity prende sempre l'intero schermo, non può essere

massimizzata o minimizzata. Un'applicazione può avviare più activity che vengono impilate

in uno stack una sopra l'altra, permettendo all'utente se lo desidera di tornare alla activity

precedente proprio come in un browser con le pagine web.

Illustrazione 1: Architettura android

Page 6: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

6

L'activity stack contiene tutte le activity attive correntemente e viene chiamato TASK.

Services:

I servizi sono l'analogo Android dei demoni presenti nel mondo Unix.

Essenzialmente un servizio è attivo se un altro componente gli richiede di svolgere una

particolare azione, esso rimane attivo per la durata richiesta dal chiamante.

Broadcast receivers:

Sono simili agli interrupt handlers. Quando occorre un evento, un broadcast receiver è attivo

e lo gestisce per conto dell'applicazione.

Content providers:

Sono essenzialmente database. Una app includerà un content provider se necessita di rendere

i suoi dati accessibili ad altre applicazioni.

Illustrazione 3: Interazione attività-servizi

Illustrazione 2: Activities stack

Page 7: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

7

Un'applicazione non possiede un singolo “entry point”, un main da cui le applicazione partono. Al

suo posto ci sono un insieme di eventi chiamati Intent. Gli intent sono messaggi asincroni che

permettono di legare i componenti individuali insieme a runtime, può essere visto come un

messaggero che richiede agli altri componenti di compiere un'azione. Ad esempio quando

cerchiamo di avviare un'applicazione il sistema genera un intent il quale comunica al componente

activity dell'applicazione che deve attivarsi. Una volta attivata, l'activity chiama il metodo

onCreate() e visualizzando così la GUI dell'app. Gli intent sono uno dei concetti più importanti

nello sviluppo di applicazioni, permettono ai componenti di interagire tra loro, sono definiti come

una descrizione astratta di una operazione che deve essere eseguita. Di notevole importanza è il

fatto che un intent inviato da un componente può essere ricevuto solo da un componente dello

stesso tipo.

L'utente utilizzando i dispositivi Android si aspetta di aprire tutte le applicazioni che desidera e

passare da una all'altra liberamente. Può quindi creare nuovi task oppure voler accedere ad uno

vecchio creato in precedenza. Come conseguenza di questa scelta di design del sistema,

rapidamente le applicazioni consumano sempre più risorse, il sistema inizia a reclamare le risorse

dei componenti usati meno di recente o di quelli senza priorità per far spazio a nuovi task e

componenti. Questo meccanismo di riciclo delle risorse è completamente trasparente all'utente.

Questo comportamento può essere critico su un smartphone per le caratteristiche hardware che,

nella stragrande maggioranza dei casi, sono di basso livello. I dispositivi dispongono di un

quantitativo di memoria principale limitata, i dispositivi di storage hanno anch'essi dimensioni

limitate e con velocità di lettura e scrittura ridotte come le cpu.

Inoltre all'interno del sistema Android è stato definito uno “standard lifecycle” dei componenti. Lo

sviluppatore di app deve preoccuparsi di gestire una serie di callback che saranno attivate quando si

verificherà un evento inerente al lifecycle di un componente. Esempio: quando un’applicazione

passa da foreground a background viene attivata la callback onPause();

Page 8: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

8

2.2 Android Runtime

Un altro livello molto importante nell'architettura è quello di Runtime, composto principalmente da

due parti:

Dalvik:

è la macchina virtuale che esegue le applicazioni scritte per Android. I programmi sono

comunemente scritti in java e all'atto della compilazione vengono trasformati in un bytecode

apposito per la dalvik e salvato in un file .dex (Dalvik Executable). Questo ulteriore livello

di trasformazione è stato pensato appositamente per far fronte hai grossi vincoli di memoria

e velocità di tali dispositivi infatti è più efficiente e portabile.

La dalvik è una macchina virtuale register-based, a differenza di quella di java che è stack-

based e ciò richiede sicuramente istruzioni più complicate per la prima ma in compenso in

numero inferiore rispetto a una stack-based. Inoltre la dalvik al suo interno possiede un J

IT(Just-in-time compiler) che permette di tradurre parti mirate di codice in codice macchina

specifico per l'architettura in modo da avere uno speed-up in termini di esecuzione. In

particolare android suppporta un TraceJIT che interpreta il codice profilandolo per capire

quali sono i percorsi di esecuzione “caldi” con il vantaggio che l'impatto sulla memoria è

minimo perchè solo i percorsi più frequenti vengono compilati in codice macchina[3]. La

dalvik contiene un garbage collector che si occupa di recuperare memoria, come nelle

comuni macchine virtuali. Dalvik è stata sostituita nelle ultime versione di Android dalla

ART per cercare di far fronte alle grosse limitazioni di tale macchina virtuale tra cui i cicli di

Illustrazione 4: Ciclo di vita app android

Page 9: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

9

garbage collector molto lunghi. In particolare, nella Dalvik era implementato l'algoritmo di

mark and sweep, sostituito nella ART da un più efficiente tri-color marking.

Zygote:

è un “demone” che viene eseguito in una virtual machine con lo scopo di avviare le

applicazioni. È alla base del sistema android. È uno dei primi processi che vengono creati

allo start-up. All'atto della sua creazione Zygote carica e inizializza tutte le classi che si

suppone siano utilizzate molto spesso dalle applicazioni. Questo consente un notevole

vantaggio in termini prestazionali perchè, dato che tali classi sono “read-only”, il

caricamento in memoria da parte di Zygote permette di sfruttare la COW(copy on write)

garantendo che le stesse pagine di memoria non siano duplicate per tutte le app, con

conseguente notevole risparmio in termini di risorse [4].

All'atto dell'apertura di un'applicazione l'activity manager comunica al demone tramite

un'opportuna chiamata di sistema (startViaZygote()) che deve essere avviata una nuova app.

Zygote si prende l'incarico e tramite una chiamata alla syscall fork() si duplica e crea la

nuova applicazione.

Effettuando la chiamata alla funziona fork() (per la precisione forkAndSpecialize()) Zygote

crea una nuova macchina virtuale che viene inizializzata attraverso la chiamata

dvmInitAfterZygote() per diventare la Dalvik virtual machine sulla quale l'applicazione

verrà eseguita [5].

Illustrazione 5: Funzionamento zygote

Page 10: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

10

2.3 Android Kernel

L'ultimo livello è invece quello inerente al kernel linux. Si nota lavorando con android un kernel

“Android-like” abbastanza diverso rispetto a “vanilla” scaricabile da http://kernel.org.

Al tal proposito elenchiamo le modifiche principali che sono state fatte ai kernel classici per poter

funzionare meglio sui device Android.

Wakelocks:

Per capire l'utilità dei wakelock occorre prima discutere come la gestione della batteria è utilizzata

all'interno dei sistemi Linux. Lo “use case” più comune all'interno di un sistema desktop Linux è

quello dei laptop in cui quando lo schermo è chiuso il sistema va in stato di “sleep” o

“sospensione”. In questa modalità lo stato del sistema è preservato interamente in RAM e gli altri

componenti sono spenti. Così facendo il pc usa meno batteria possibile. Quando l'utente alza lo

schermo, il laptop “si sveglia” e il pc ritorna rapidamente riutilizzabile. Questo modus operandi non

è però funzionale per un device Android. È stato quindi scelto dal team di sviluppo Android di

cambiare leggermente le regole per gestire meglio gli use case dei device. Vista la scarsa durata

della batteria all'interno di questi dispositivi è stato deciso di mettere in stato di “sleep” il sistema il

più spesso possibile, e di mantenerlo sveglio solo per operazioni importanti, come ad esempio

l'attesa dell'input di un utente. I wakelocks consentono appunto un meccanismo per mantenere

“sveglio” il sistema.

A livello applicativo questo non è necessario in quanto l'astrazione fornita dalle API Android

automaticamente si occupa di prendere questi lock. A più basso livello, come può essere lo sviluppo

di driver per il dispositivo, lo sviluppatore deve preoccuparsi di prendere esplicitamente tali lock

prima di effettuare un'operazione potenzialmente critica [2]. I wakelocks sono un argomento molto

discusso all'interno della comunità Linux e la loro inclusione in mainline ha richiesto quasi

2000 email e ancora oggi rimangono solo parzialmente adottati [6].

Low-Memory Killer:

Visto che alla base del sistema c'è una situazione costante di memoria libera limitata, il

comportamento durante un out-of-memory è di vitale importanza. Per questa ragione il team di

Android ha deciso di aggiungere, oltre l'OOM killer standard di linux anche un low-memory killer

che permette di uccidere i processi per liberare risorse.

A differenza dell'OOM killer, il low-memory killer non attende che il sistema non abbia più

memoria, ma superata una certa soglia inizia ad uccidere processi seguendo delle specifiche

euristiche differenti da quelli dell'OOM killer [2].

Page 11: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

11

Binder:

È un meccanismo di RPC(remote procedure call)/IPC(inter process comunication).

Questo strumento cerca di creare una capacità di invocazione di oggetti remoti su un sistema

operativo classico. Con i termini “invocazione di oggetti” intendiamo un tentativo degli sviluppatori

Android di creare dei servizi del sistema operativo object-oriented, vista la nuova struttura dei

servizi si è reso necessario creare un nuovo sistema di IPC in grado di comunicare e ragionare ad

oggetti [7]. Un processo in Android può invocare una routine di un altro processo Android

utilizzando il binder per identificare il metodo invocato e passare gli argomenti tra processi.

Tutto ciò rende molto facile per lo sviluppatore estendere le funzionalità del sistema aggiungendo

l'invocazione di oggetti remoti invece di implementare un nuovo demone che svolga tale servizio.

Ad esempio supponiamo di creare un'applicazione alla quale serve sfruttare la fotocamera, può non

essere necessario re-implementare tale funzionalità in quanto se c'è un servizio che già la

implementa si può sfruttare la sua mettendosi in comunicazione attraverso il binder con tale

servizio.

Per invocare un oggetto remoto basta avere la definizione della sua interfaccia e un suo riferimento

[2].

Illustrazione 6: Binder

Page 12: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

12

3. Budget Fair Queueing (BFQ)

BFQ è un algoritmo di scheduling dei dispositivi di storage che punta a raggiungere un alta

responsiveness, ed in generale basse latenze a livello applicativo e di sistema, anche sotto un

intenso workload, garantendo allo stesso tempo un throughtput elevato. I risultati delle euristiche

permettono di garantire, anche in presenza di un alto workload di input/output in background, un

tempo di start-up delle applicazioni interattive basso come se il dispositivo fosse in stato di idle[8].

All'interno di tale algoritmo c'è una coda di richieste per applicazione, le richieste sono inserite

invocando l'interfaccia add_request(). L'accesso allo storage è garantito ad una sola applicazione

alla volta, chiamata applicazione in servizio. Ad ogni applicazione è associato un budget che misura

il numero di settori del disco su cui può operare. Quando un’applicazione diventa attiva è servita

fino a quando non esaurisce il suo budget o finché non smette di fare richieste. BFQ sceglie la

prossima applicazione attiva tramite il fair-queueing scheduler interno chiamato B-WF2Q. Una

richiesta è spedita se il dispositivo invoca l'interfaccia dispatch().

All'interno di BFQ ad ogni applicazione è inoltre associato un peso (weight) che determina la

frazione di throughput garantita all'applicazione: all'applicazione è garantita una frazione di

throughput pari al rapporto tra il suo peso ed il peso delle altre applicazioni che competono per il

dispostivo.

Illustrazione 7: Schema logico BFQ

Page 13: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

13

Tale scheduler contiene una serie di euristiche che ne migliorano le performance e permettono di

raggiungere obiettivi di notevole importanza. Tra le euristiche troviamo la “low latency heuristic”

dedicata a garantire una bassa latenza ai processi interattivi.

Quando un'applicazione viene creata il peso è moltiplicato per un coefficiente di weight-raising.

Questo garantisce al processo una grande frazione di througthput iniziale permettendo di ottenere

un tempo di start-up basso in quanto vengono rapidamente lette le parti di eseguibile e le librerie

necessarie per avviare l'applicativo. L'euristica lascia che il peso sia costantemente uguale a

orig_weight*Crais per l'intera durata del weight-raising period. Tale periodo è calcolato

automaticamente in base al tipo e alla velocità del dispositivo e in relazione al tempo di caricamento

di una grande applicazione su quel dispositivo, con le cache “fredde” e senza workload in

background. Due concetti fondamentali sono: le richieste sincrone, una richieste di I/O se il

processo che ha effettuato la richiesta deve aspettare il suo completamento per effettuare la

successiva, e device idling cioè il momento in cui il dispositivo di storage non riceve richieste di

I/O. BFQ sfrutta appunto un device-idling timeout (un periodo di tempo in cui si lascia lo storage in

idle e si aspetta) elevato per una coda in weight raising in modo da ridurre la probabilità che

un'applicazione sia tolta dal servizio in caso effettui richieste sincrone e le richieste successive non

arrivino in tempo. Ridurre questa probabilità è fondamentale per essere sicuri che l'applicazione

riceva la frazione di throughput desiderata.

Page 14: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

14

4. Problema: le app sperimentano alta

latenza

Il problema della latenza del sistema in questi dispositivi si vive con grande frequenza. Essi sono

molto limitati da un punto di vista hardware. Dispongono infatti di processori con ridotta velocità,

di poca memoria, di capacità di storage limitate e di una durata della batteria decisamente

contenuta. Tutte queste caratteristiche hardware combinate con l'efficacia del software incidono

notevolmente sul sistema.

Supponiamo di voler ricreare un'esperienza di alta latenza di una app o dell'intero sistema, così

come la sperimenterebbe un normale utente. Iniziamo aprendo alcune applicazioni (tra cui giochi

oppure utility di sistema) sul nostro dispositivo senza necessariamente chiuderle, questo genera un

buon carico di lavoro e magari si può avvertire qualche rallentamento nell'operazione di “switch”

tra un'applicazione ed un altra, ma niente di significativo e allarmante. Aggiungiamo ora del carico

al nostro esperimento. Supponiamo che il carico sia rappresentato da una serie di download partiti

in automatico dal play store. Succede ad esempio quando sono disponibili degli aggiornamenti o

quando decidiamo di installare una nuova applicazione. Tale operazione genera un notevole lavoro

per quanto riguarda il dispositivo di storage. Più o meno incuranti della presenza di questi download

iniziamo a riaprire applicazioni una dopo l'altra e già verso la seconda/terza applicazione si inizia a

percepire un ritardo considerevole nel caricamento. Allo stesso modo anche il comando per

spostarci tra una applicazione ed un'altra e il pulsante “home” iniziano a non rispondere. Appena

terminato il download le applicazioni lentamente riprendono a funzionare correttamente, i tempi di

start-up tornano contenuti e il sistema sembra rispondere di nuovo egregiamente.

Esasperiamo ulteriormente il problema. Ad oggi 2 milioni circa di persone (dati presi dal play store

android) hanno scaricato l'applicazione per scaricare file torrent sul proprio smartphone o tablet. Ai

nostri scopi dimostrativi tale applicazione è perfetta. Il download di un torrent genera un carico di

lavoro sul sistema costante e duraturo nel tempo, producendo un consumo di memoria principale

non eccessivo ma generando continue write per scrivere i dati ricevuti sul disco. Supponendo che il

carico di lavoro fornito da tale applicazione sia il nostro background, il tentativo di aprire svariate

applicazioni fallisce rapidamente. Il sistema già verso la seconda/terza app collassa. I tempi di start-

up si dilatano in maniera esponenziale. Il carico così generato può essere talmente eccessivo da

portare, non solo al crash della applicazione che stiamo aprendo, ma anche un una uccisione totale

della UI del sistema da parte del low memory killer nel tentativo disperato di liberare risorse.

Page 15: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

15

Come si può vedere da questi piccoli esperimenti, da noi svolti ad inizio lavoro, è molto semplice

generare un carico di I/O tale da rendere la latenza insopportabile per un normale utente, il quale già

dopo qualche secondo in più di attesa la percepisce sgradevole e fastidiosa.

È chiaro come non tutti i problemi della latenza del sistema siano legati all'I/O, a volte anche

semplicemente cicli di garbage collection o persino l'azione del low memory killer possono portare

ad una diminuzione della responsiveness sia della singola app che dell'intero sistema Android.

Il nostro obbiettivo è quindi quello di migliorare la latenza del sistema in presenza di forte I/O in

background cercando di sfruttare le proprietà fornite da BFQ nel migliorare la responsiveness delle

applicazioni interattive.

Per aiutarci e semplificare lo studio di ciò che avviene internamente al sistema nel momento in cui

si genera un carico di lavoro abbiamo deciso di creare una piccola suite descritta in maniera

approfondita nel prossimo capitolo.

Page 16: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

16

5. Piccola suite per riprodurre il

problema in modo sistematico e

controllabile

Tale suite, composta da una applicazione e uno script bash, è stata creata per cercare di riprodurre in

maniera sistematica il problema della alta latenza all'avvio di un'applicazione. È stato scelto di

creare un'applicazione ad-hoc in quanto con le applicazioni attualmente disponibili era difficile

riuscire a gestire, ma soprattutto calcolare i tempi di start-up e capire fino in fondo quali operazioni

venissero svolte all'avvio.

5.1 Lo script

Lo script è il programma principale della suite che permette di decidere quale applicazione far

partire tra le 2 usate per i nostri esperimenti. Permette di settare il carico in background cioè il

numero di processi che effettuano delle write e il numero di processi che effettuano delle read,

permette di cambiare scheduler dello storage ed altre funzionalità illustrate successivamente.

Esso è composto a sua volta da altri script bash che permettono di eseguire specifiche funzionalità.

È stato scelto questo approccio nel tentativo di seguire una modularizzazione del codice che potesse

portare a una più semplice gestione delle funzionalità e degli eventuali errori.

Nel parte iniziale dello script vengono inizializzate alcune variabili tra cui: lo scheduler dello

storage di default (BFQ), un tunable di bfq (per la precisione la device-idle), il numero di processi

reader e writer in background fissato inizialmente a 1, ecc.

5.1.1 Introduzione alle opzioni

Una delle prime function significative è quella che ci mostra un messaggio di errore nell'avvio dello

script in caso il comando con cui si avviata contenesse un errore.

function help {

echo "USAGE: sh StarDemoClean [...]"

echo "[-c] -> Demo doesn't soil the ram"

echo "[-l] -> Demo takes the log of thread in execution"

echo "[-sched][bfq/cfq/noop/deadline] -> Set the scheduler"

Page 17: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

17

echo "[-sched][bfq][number] -> Set the value of bfq's slice idle"i

echo "[-t] -> Set tracer"

echo "[-tl] -> Available tracer"

echo "[-r [NumRead]] -> Stat 'NumRead' read (default 1)"

echo "[-w [NumWrite]] -> Start 'NumWrite' write (default 1)"

echo "[-noprio] -> Doesn't set IO prio io candycrushsaga"

echo "[-NOC] -> Start another app"

echo "[-start [num]] -> set time elapsed before start of traces"

echo "[-stop [num]] -> set the duration of the traces"

return 0

}

Tale funzione da subito una visione di quali sono le possibilità dello script. Esso viene lanciato da

una bash con una o più di queste opzioni, permettendoci di modificare liberamente la maggior parte

dei parametri necessari durante gli esperimenti. Il comando -c fa partire lo script in modalità

“clean”. L'esistenza di questa opzione presuppone una spiegazione preliminare. L'idea iniziale era

quella di misurare lo start-up di una applicazione in una casistica reale. Si avviava in primis

l'applicazione oggetto di studio, successivamente si aprivano una serie di applicazioni che

riempissero la memoria e attivassero il low memory killer il quale uccideva il processo oggetto di

studio (al tempo candy crush saga). Il processo veniva poi riaperto e oltre al carico di memoria

notevole gli veniva contrapposto un workload in background fornito dai processi writer e reader,

così facendo si sperimentava la latenza nello start-up in un caso sfortunato ma realistico. A lungo

andare tale scelta è stata parzialmente abbandonata per la difficoltà nel capire cosa avvenisse

all'interno del sistema durante quelle operazioni. L’opzione serve quindi per saltare questa parte che

è rimasta però all'interno dello script in caso sviluppi futuri permettano di studiare il

comportamento. L’opzione -l prende i log del sistema; Android ha un suo sistema specifico di log

di alcuni eventi generati dalle app e dal sistema nel suo complesso. Il meccanismo di log è molto

sofisticato e permette di debuggare il sistema o una applicazione [9]. È implementato tramite un

buffer circolare e può essere visualizzato o filtrato tramite il comando logcat. Questo comando

permette di scegliere tra diversi buffer di output:

radio: visualizza il buffer contenente i messaggi relativi alle funzioni radio/telefono;

events: visualizza il buffer contenente i messaggi legati agli eventi del sistema;

system: visualizza il buffer relativo ai messaggi di sistema;

main: visualizza il log buffer principale (è quello di default di logcat ed è anche quello da

noi utilizzato);

Page 18: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

18

tutti questi buffer sono salvati nella directory /dev/log/. [2]

Le opzioni t e -tl permettono rispettivamente di scegliere quale tracer usare a livello kernel e di

visualizzare l'elenco dei tracer disponibili. Il kernel linux supporta un sistema di tracing noto con il

nome di Ftrace (Function tracer). Ftrace è un sottosistema del kernel linux, creato per aiutare gli

sviluppatori a capire cosa stesse succedendo nel sistema, attraverso output esaustivi e mirati. Ftrace

è un framework molto più ampio di quello che il nome fa pensare, utilizzandolo non solo si possono

tracciare le funzioni invocate nel kernel ma anche tracciare quello che avviene nel block layer

fornendo informazioni su chi stia facendo richieste di I/O, quali funzioni dello scheduler sono

invocate, se la richiesta è sincrona o asincrona ecc.. L'output fornito da Ftrace, e tutte le sue

funzionalità personalizzabili si trovano all'interno della directory /sys/kernel/debug/. I tracer

disponibili variano in base alle impostazioni con cui è stato compilato il kernel custom, ma in

generale sono quelli disponibili su qualunque versione del kernel linux [10].

L’opzione -NOC (no open candy) permette di aprire applicazioni diverse da candy crush saga. Tale

applicazione è stata utilizzata come riferimento nei primi esperimenti e si era pensato di studiarne il

comportamento. Viste però le complicazioni che sorgono nel capire tutte le operazioni svolte nella

fase di start-up è stato scelto di spostarsi su altre applicazioni ma mantenere tale possibilità nello

script per eventuali sviluppi futuri. Le ultime due opzioni sono -start e -stop seguite da un numero

che rappresenta i secondi da aspettare nel primo caso o la durata delle tracce nel secondo. Tali

opzioni sono molto utili per decidere quando iniziare e quando smettere di prendere le tracce. Le

Illustrazione 8: Android log framework

Page 19: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

19

tracce sono implementate tramite buffer circolari, ciò spesso porta le operazioni svolte nell'arco di

tempo dell'intero script ad essere troppe e il buffer parzialmente sovrascritto portando a perdite di

informazioni significative. Cosi facendo, attraverso il numero posto a fianco le opzioni, scegliamo

un lasso di tempo preciso in cui prendere le tracce in modo che il buffer circolare non saturi nella

speranza di trovare più informazioni utili.

5.1.2 Gestione tracce kernel

Le tracce a livello kernel sono inizializzate e gestite da due funzioni specifiche, la prima

function init_tracing {

if [ "$TRACE" == "1" ] ; then

if [ ! -d /sys/kernel/debug/tracing ] ; then

mount -t debugfs none /sys/kernel/debug

fi

echo nop > /sys/kernel/debug/tracing/current_tracer

echo 50000 > /sys/kernel/debug/tracing/buffer_size_kb

echo buffer_size: $(cat /sys/kernel/debug/tracing/buffer_size_kb)

if [[ "$tracer" == "blk" ]]

then

echo "${SCHED}*" "__${SCHED}*" >\

/sys/kernel/debug/tracing/set_ftrace_filter

else

echo .

fi

echo $tracer > /sys/kernel/debug/tracing/current_tracer

echo -n "Selected tracer:"

cat /sys/kernel/debug/tracing/current_tracer

fi

}

Se la variabile TRACE è settata ad 1 viene montata la partizione debugfs (in caso non sia già

montata). In tale partizione è presente l'ambiente delle tracce. Lo script inizializza il tracer di default

a nop (no operation) e decide la grandezza del buffer. La grandezza del buffer delle tracce può

essere prefissata dall'utente. Se il tracer è blk e quindi riguarda il block layer aggiunge dei filtri alle

Page 20: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

20

tracce, di modo che vengano selezionate solo le operazioni svolte da BFQ. L'ultima operazione è

quella di reale settaggio del tracer scelto all'avvio dello script.

La seconda funzione, che attiva e disattiva le tracce, è molto semplice e quindi non viene riportata.

Essenzialmente se invocata passandogli valore 1 attiva le tracce attraverso il comando:

echo $1 > /sys/block/$HD/trace/enable

Se passata con valore 0 le tracce vengono disattivate.

5.1.3 Background workload

Le ultime due function importanti di tale script sono quelle che si occupano di gestire il carico in

background. Il workload nello script è rappresentato da dei dd che effettuano read e write. L'utilizzo

dei dd simula in maniera semplice l'I/O che può essere generato durante un download o durante l’

installazione di un'applicazione.

#Reader creator

function dd_read {

while [[ "$(cat EndFile)" == "false" ]]

do

echo dd if=/data/tmp/BIGFILE_READ$1 of=/dev/null bs=1048576 count=512

dd if=/data/tmp/BIGFILE_READ$1 of=/dev/null bs=1048576 count=512 &

echo BIGFILE_READ$1 $!

wait

done

}

#Writers creator

function dd_write {

while [[ "$(cat EndFile)" == "false" ]]

do

dd if=/dev/zero of=/data/tmp/BIGFILE_WRITE$1 bs=1048576 count=512 &

echo BIGFILE_WRITE$1 $!

wait

done

}

Page 21: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

21

Queste due function fanno, da un punto di vista logico, la stessa cosa. L'unica differenza è che la

prima crea dei dd in lettura mentre la seconda dei dd in scrittura. Descriveremo quindi solo la

prima. La funzione rimane in un ciclo while fino a quando non legge da un file di controllo un

determinato valore che rappresenta la terminazione della demo (questa soluzione è molto semplice e

permette di evitare l'invio di un segnale allo script. I segnali sono comunque utilizzati all'interno

dello script in quanto è gestito un eventuale CTRL+C che chiude in maniera pulita l'esecuzione e fa

terminare i processi reader e writer; questa soluzione è utilizzata di norma solo se lo script si

blocca). All'interno del ciclo crea un dd in lettura/scrittura. Se un dd termina prima della fine dello

script ne viene creato un altro al suo posto di modo che l'I/O continui fino al termine dello script. È

stato scelto di utilizzare questo sistema per controllare il più possibile anche il carico di lavoro

presente in background. Cosi facendo i comportamenti riscontrati sono oltremodo significativi e ci

danno una stima reale di quanto possa essere elevata la latenza con un carico di I/O inteso e

costante. Non utilizzando questo meccanismo l'applicazione, appena terminato il carico di I/O,

viene caricata all'instante generando quindi un valore ingannevole per i tempi di start-up misurati.

Un altro punto dello script che necessita di spiegazioni è un semplice comando:

echo 3 > /proc/sys/vm/drop_caches

Con questo comando andiamo a liberare la pagecache, la dentries e inodes cache, liberando così un

notevole quantitativo di memoria. Questo comando ha due principali funzionalità per i nostri scopi:

la prima ci permette di partire da un situazione omogenea e “standardizzata” ogni volta che

ripetiamo lo script; il secondo è quello di garantirci che tutte le operazioni siano sempre fatte

leggendo informazioni dal dispositivo di storage. Se questo comando non ci fosse potremmo trovare

in memoria parti del programma che stiamo avviando, ciò falserebbe non poco i risultati in quanto

la lettura da memoria principale è estremamente più rapida rispetto a quella dal disco e ciò

abbasserebbe inesorabilmente i tempi di start-up anche in presenza di I/O.

5.1.4 Applicazioni oggetto di studio

Gli ultimi statement analizzati riguardano il caricamento delle applicazioni scelte. Come già detto

sopra con il comando -NOC (No Open Candy) si può scegliere se aprire l'applicazione Lprobe,

costruita per semplificare lo studio delle latenze, o l'applicazione candy crush saga (il gioco scelto

per cercare di studiare le latenze in una caso reale, più vicino all'esperienza dell'utente).

Page 22: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

22

if [[ $NoOpenCandy == "false" ]]; then

sh Open_app2.sh 1 d 1000 $set_prio 54000 &

cc=$!

echo cc $cc

else

LProbe

fi

Lo script Open_app2.sh è un modulo dello script che contiene un’euristica che cerca di determinare

quando realmente un'applicazione ha finito di effettuare lo start-up. Questa euristica diventa

necessaria perchè alcune applicazioni vengono visualizzate ma poi effettuano una fase di “loading”

in cui vengono caricate e lette dallo storage le informazioni necessarie. Tale fase di caricamento

deve essere considerata anch'essa nei tempi di start-up perchè, finchè tale fase non è terminata, l'app

risulta inutilizzabile dall'utente anche se già visualizzata sul monitor. L'euristica funziona misurando

quanta memoria il programma ha già occupato. La memoria occupata è misura sfruttando il

comando dumpsys e filtrando il suo output [11]. Viene ora riportata la parte principale dell'euristica

presente nel file Open_app2.sh.

time while [[ $diff -gt $max_diff_mem ]]

do mem_used=$(dumpsys meminfo | grep com.king.candycrushsaga| head -c 9 | tail -c 5)

if [[ $mem_used>$5 ]]

then

diff=$(($mem_used - $previous_mem_used))

if [[ $diff<0 ]]

then

diff=$(($diff*-1))

fi

fi

echo $mem_used $previous_mem_used $diff

previous_mem_used=$mem_used

done

L'euristica misura attraverso il comando time la durata del ciclo while.

Il comando dumpsys è il fulcro dell'euristica [11] e permette di stabilire quanta memoria il processo

interessato, in questo caso candy crush saga, ha già occupato; questo è il primo passo e il valore è

Page 23: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

23

salvato nella variabile “mem_used”. Successivamente si sfrutta un valore passato in ingresso al

modulo Open_app2.sh. Tale valore è stato deciso in maniera sperimentale attorno ai 54 megabyte,

quando l'applicazione si è quasi completamente caricata. Se la memoria usata supera questa soglia

allora si calcola la differenza tra la memoria usata(mem_used) e la memoria utilizzata

precedentemente(previous_mem_used vari abile il cui valore è inizializzato a 0 prima del ciclo

while) e la si assegna alla variabile “diff”; se la differenza è negativa si cambia il segno

moltiplicandola per -1. Successivamente si setta “previous_mem_used” al valore di “mem_used” e

si controlla la condizione di terminazione. Il ciclo termina se la diff è maggiore della massima

differenza consentita (max_diff_mem) e tale valore viene passato in ingresso al modulo che è stato

settato a 1 megabyte. Il metodo si è rivelato molto efficace e nella maggior parte dei casi il tempo

calcolato è identico all'effettivo tempo di start-up dell'applicazione.

Il ramo else invece invoca la funzione LProbe che a sua volta invoca lo script Lprob.sh che avvia

l'applicazione ad-hoc Lprobe.

Entrambi gli script Lprobe.sh e Open_app2.sh possono essere utilizzati come programmi a se stanti

e oltre alla funzionalità descritte svolgono entrambi un altro compito importantissimo agli scopi

della ricerca settando l'ioprio del processo che stiamo avviando.

L'ioprio è modificato tramite il comando “ionice” che permette di settare la classe di scheduling

dell'I/O e la priorità di uno specifico programma [12]. Le classi di scheduling possibili sono tre:

idle: un programma con ioprio idle viene servito solo se nessun altro programma fa I/O per

un determinato periodo di tempo;

best effort: questa è la classe di default di tutti i processi. Ha una priorità che va da 0 a 7 con

i numeri più piccoli che indicano una priorità più elevata;

real time: un processo in classe RT viene servito prima rispetto agli altri presenti nel sistema.

Come la classe best effort prevede 8 livelli di priorità la cui importanza va in ordine inverso

rispetto alla grandezza numerica (0 priorità più alta -7 priorità più bassa).

L'ioprio è di fondamentale importanza ai nostri scopi in quanto BFQ è stato modificato in modo da

garantire un weight raising persistente a quei processi che hanno priorità 0, questa modifica è stata

fatta ad-hoc per gli scopi della tesi. Così facendo è possibile garantire una frazione di throughtput

molto elevata alle applicazioni oggetto di studio le quali si trovano notevolmente avvantaggiate

rispetto a concorrenti in background. Inoltre il weight raising persistente è sfruttato anche nelle

soluzioni ai problemi descritti nel capitolo 6.

5.2 Lprobe

Lprobe (latency probe) è un'applicazione Android scritta per mettere in maggior evidenza e studiare

Page 24: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

24

meglio i problemi di latenza dovuti alla presenza di intenso I/O. La scelta di creare un'applicazione

“ad-hoc” è scaturita dalle notevoli difficoltà riscontrate nel cercare di capire cosa avvenisse

all'avvio di candy crush saga. Non potendo disporre del codice non eravamo sicuri di quali fossero

le operazioni svolte, disponendo solo delle tracce fornite da Ftrace e da eventuali messaggi ricavati

dal logcat di Android. Era quasi impossibile riuscire a districare la grande matassa che tale

applicazione generava interagendo con molti servizi di sistema, facendo richieste di I/O ad un

database SQLite e creando diversi thread per svolgere operazioni in parallelo. Pertanto per avere la

certezza di quello che l'applicazione facesse e riuscire così a carpire cosa avvenisse a basso livello

con chiarezza è stata creata l'applicazione Lprobe.

L'applicazione è molto semplice: all'atto della chiamata della funzione onCreate(), invocata per

effettuare lo start-up, apre un file in lettura (BIGFILE_READ1) e uno in scrittura (FILE_WRITE).

Successivamente crea una matrice di byte in cui ogni riga rappresenta una linea letta dal file, e

effettua un ciclo che va da 0 a dim*4 e riempie la matrice con il valore di ritorno della funzione

Lantecy_probe.

int dim = 13200;

byte[][] lines = new byte[dim][];

for(int j = 0;j<dim*4;j++) {

lines[j%dim]=Latency_probe(input_file,j,outputStream,txt);

}

Tale funzione è il cuore dell'applicazione. Gli sono passati in ingresso come parametri il

FileInputStream necessario per leggere dal file, il FileOutputStream necessario per scrivere sul file

e una TextView.

long t1,t2;

t1=System.currentTimeMillis();

input_file.read(line);

t2=System.currentTimeMillis();

if ((t2-t1)>300) {

txt.append("\nRead Latency " + (t2 - t1));

Log.d("Read Latency", "" + (t2 - t1));

}

if(j % 10 == 0) {

t1 = System.currentTimeMillis();

outputStream.write(line);

Page 25: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

25

t2=System.currentTimeMillis();

if ((t2-t1)>300) {

txt.append("\nWrite Latency " + (t2 - t1));

Log.d("Write Latency", "" + (t2 - t1));

}

}

La textview passata alla funzione serve per visualizzare la durata delle singole write e delle singole

read che superano i 300 millisecondi di durata. È stata scelta questa soglia in quanto dopo 300

millisecondi il ritardo inizia ad essere importante ed è facile accorgersene anche visivamente (oltre

questa soglia anche il normale utente percepisce la latenza all'avvio dell'applicazione). La funzione

prepara la linea da leggere allocando un vettore di byte di grandezza 4096 (scelto della grandezza di

una pagina di memoria). Successivamente si va a leggere una linea dall'inputStream e la si inserisce

nel vettore. Effettuata l'operazione di read si valuta se è, o meno, necessario effettuare anche una

write. Una write è effettuata ogni 10 read per non aumentare troppo il carico dell'applicazione stessa

nella fase di avvio. La funzione, dopo aver gestito tutti i casi di errore all'interno di costrutti try

catch, ritorna la linea letta (e scritta se necessario) dal file. All'interno del programma sono inseriti

altri due insiemi di statement che svolgono un ruolo fondamentale. Il primo è:

try {

Thread.sleep(5000); //1000 milliseconds is one second.

} catch(InterruptedException ex) {

Thread.currentThread().interrupt();

}

Questo sleep è svolto prima di qualsiasi altra cosa. È stato introdotto perchè, utilizzando ionice per

privilegiare l'applicazione all'interno dello scheduler, c'era un problema di sincronizzazione

(approfondito successivamente).

Il secondo insieme di statament invece è una banale scrittura di alcune righe della matrice, creata in

precedenza, sui file di log del sistema. Questa operazione è svolta al fine di evitare che il garbage

collector consideri le nostre linee non utilizzate e quindi le de-allochi alla fine di ogni ciclo for. Così

facendo si alloca molta memoria simulando maggiormente il caricamento di un'applicazione reale.

Page 26: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

26

6. Cause del problema

Le cause del problema possono essere notevoli e variegate, oggetto di analisi saranno solo i

principali problemi che siamo riusciti a rilevare a livello kernel durante il tirocinio.

Per studiare tali problemi abbiamo prima di tutto preso e studiato delle traccie a livello di block

layer del programma di esempio Lprobe. Tali tracce diedero però risultati non indicativi a proposito

delle cause scatenanti l'alta latenza, anzi esse inserivano ancora più incognite in quanto si vedeva

uno strano comportamento del processo. Il processo in questione iniziava a fare richieste di I/O e

BFQ lo metteva in servizio prontamente. Si vedeva però, nella finestra di durata dell'applicativo,

che all'interno delle tracce c’erano dei macro “buchi” di 3/4 secondi, che si ripetevano anche più

volte nelle tracce, in cui il processo smetteva di fare richieste di I/O. In tali intervalli di tempo il

dispositivo di storage veniva monopolizzato prima dal DD in lettura poi dal flusher che si occupa di

effettuare le write asincrone per conto dei processi. Di conseguenza la responsiveness del nostro

programma subiva un grosso degrado.

Viene riportato un piccolo trafiletto di traccia del blk in cui si nota il “buco” appena descritto.

<idle>-0 [002] 92.340216: 179,0 m N bfq2151 put_queue: d58a8580 2

LProbe-2151 [001] 95.975945: 179,0 m N bfq2151 add_request 1 1

Si vede subito come i timestamp (terza colonna) siano molto distanti tra loro e la differenza è

davvero notevole in tale esempio (ben 3 secondi). La prima riga riportata rappresenta il momento in

cui BFQ de-attiva la coda di quel processo in quanto non ha più richieste. La successiva invece

mostra il momento in cui BFQ riceve la prossima richiesta da parte del processo studiato. Il tempo

che intercorre è elevatissimo se si pensa alla frequenza con la quale la nostra applicazione fa

operazioni di read e operazioni di write, per la precisione effettua una write ogni dieci read.

I risultati forniti da queste tracce non forniscono ulteriori informazioni utili. Nella sostanza

mostrano solamente che il problema non risiede a livello di block layer o scheduler ma bensì risiede

ad un livello più alto nelle operazioni svolte all'interno del kernel durante una read o una write.

Allo scopo di capire in quale parte del kernel il processo si bloccasse per così tanto tempo, sono

state utilizzate le tracce fornite dal function tracer. Questo tracer è particolarmente espressivo e

fornisce moltissime informazioni. Essenzialmente permette di vedere le varie chiamate a funzioni

effettuate da un processo all'interno del kernel linux [10].

Anche sfruttando queste tracce si nota come siano presenti dei “buchi” analoghi a quelli nel block

layer in cui il nostro processo non effettua alcuna operazione. Questa volta grazie al “function

Page 27: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

27

tracer” di Ftrace è stato possibile capire, almeno parzialmente, quale percorso di codice portasse il

processo ad addormentarsi. Infatti partendo dal momento in cui il processo si addormentava è stato

relativamente semplice capire che l'ultima funzione invocata fosse la schedule; è però stato più

complesso andare oltre tale funzione in quanto molte funzioni inline non vengono tracciate dal

tracer rendendo molto complesso seguire i percorsi e capirli alla perfezioni (il buffer di Ftrace è

circolare e abbastanza limitato quindi informazioni importanti molto spesso andavano perdute).

Per capire in maniera definita dove fosse il problema si è sfruttato l'output di Lprobe che stampa la

durata delle write e delle read superiori ai 300ms. Si è così immediatamente visto come il problema

fossero le write, fatte dal processo, che richiedono moltissimo tempo. Inoltre per trovare i percorsi

causa della latenza, all’interno delle operazioni di write, sono state strumentate ad una ad una le

funzioni del kernel facendo stampare al dmesg le funzioni la cui durata superava i 300ms. Ad

esempio:

unsigned long time_t1 = jiffies;

status = a_ops->write_begin(file, mapping, pos, bytes, flags,&page, &fsdata);

if(jiffies - time_t1 >= msecs_to_jiffies(300))

printk(KERN_DEBUG "write_begin %u PID %u ioprio %u wrais

%u",jiffies_to_msecs(jiffies-time_t1),

current->pid,current->io_context ? current->io_context->ioprio:-1,wrais);

Come si può vedere codice riportato tale meccanismo è stato sfruttato anche per fare debugging e

tracing più approfondito del kernel, facendo stampare altre informazioni utili per la ricerca. Questo

meccanismo genera un stack di chiamate a funzioni molto dettagliato che consente di vedere

l’intero percorso che genera latenza all’interno del kernel.

Confrontando tali risultati con quelli forniti anche dal function tracer sono state individuate le prime

funzioni responsabili del problema.

Tali funzioni erano legate sia al journaling del filesystem sia a funzioni come

“balance_dirty_pages” e “vm_throttle_writeout” che riguardano la gestione della memoria virtuale.

6.1 Caratteristiche del device utilizzato negli

esperimenti Il device utilizzato per gli esperimenti è un Nexus 7 versione del 2012 con le seguenti caratteristiche

hardware [27]:

Page 28: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

28

Android KitKat 4.4.4

Kernel versione 3.1.0

32 GB di storage

1 GB di RAM

Processore quad-core NVIDIA tegra 3.

6.2 Primo insieme di cause Nel primo insieme di cause vengono illustrati nello specifico i problemi riscontrati a livello kernel,

approfondendo quelli già citati sopra e descrivendo gli ulteriori problemi risolti.

6.2.1 Journaling Sfruttando l'output fornito dal nostro programma, che traccia la durata delle singole write e delle

singole read, si nota subito come il problema della latenza risieda nella elevata durata delle write.

Modificando il codice del kernel e tracciando le chiamate a funzione che hanno latenza più elevata

abbiamo trovato il seguente percorso critico di invocazioni:

1)sys_write()

2)vfs_write()

3)file->f_op->write → new_sync_write()

4)filp->f_op->write_iter → ext4_file_write_iter()

5)__generic_file_write_iter()

6)file_update_time()

7)update_time()

9)__mark_inode_dirty()

10)sb->s_op->dirty_inode → ext4_dirty_inode()

11)ext4_journal_start() → __ext4_journal_start_sb()

12)jbd2__journal_start()

13)start_this_handle()

14)add_transaction_credits()

15)wait_transaction_locked()

Tale percorso riguarda l'aggiornamento del tempo di modifica del file, che coinvolge il journal in

quanto in tal modo vengono modificati i metadati del file.

Page 29: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

29

Il dispositivo Android fornito per gli esperimenti è basato sul filesystem ext4 il quale, come l'ext3,

prevede l'utilizzo del journal per proteggere il filesystem stesso da eventuali corruzioni in caso di

crash del sistema. Per ragioni di performance è stato deciso che ext4 attraverso il journal scrivesse,

di default, solo metadati dei file.

Il journaling avviene con un meccanismo a transazioni ovvero per un certo periodo di tempo il

sistema accumula modifiche da registrare sul journal. L'insieme di queste modifiche costituisce una

transazione. Ad un certo punto un kernel thread adibito al journaling (e chiamato, nel caso di ext4,

jbd2) prende il lock sul journal, svuota le modifiche su disco (commit della transazione), e rilascia il

lock sul journal. Quando nel contesto di un processo si effettua journaling, il processo richiede

un handle, ovvero un aggancio, alla transazione corrente. Se la transazione è ancora aperta, il

processo può inserirsi in quella. Altrimenti, normalmente, se la transazione è in fase di commit

(quindi il lock è preso), il processo si addormenta su una waitqueue per aspettare che ci sia di nuovo

una finestra utile per agganciarsi.

Le transazioni non sono però strutture dati monolitiche. Esse sono invece suddivise in unità più

piccole: gli handle già menzionati ed i log record [13].

I log record sono la più piccola unità che può essere trattata dal journal. Essi rappresentano le

singole modifiche ai metadati di un file, svolte da un determinato processo, che devono essere

registrate dal journal. Ognuno di essi rappresenta un aggiornamento individuale a un blocco. Ad

ogni log record è associata una struttura dati chiamata buffer_head (vedi capitolo 6.3).

Gli handle come già detto posso essere definiti come un aggancio alla transazione e raggruppano

insieme svariati log record. Ad esempio se si invoca una write usando la sua syscall, tutti i log

record coinvolti in tale operazione sono raggruppati in un handle. Ogni handle è sempre associato

Illustrazione 9: Struttura transazione

Page 30: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

30

ad uno specifico processo, tale associazione è assicurata dal campo journal_info presente nella

task_struct che il JBD2 converte in un puntatore ad un handle_t. La struttura dati handle_t è un

tipo di dato opaco infatti è un typedef della più specifica jbd2_journal_handle [14] la quale è

definita nel file /include/linux/jbd2.h alla riga 419 [15] che contiene due campi di notevole

importanza:

transaction_t *h_transaction;

int h_buffer_credits;

Il primo è il puntatore alla transazione al quale l’handle appartiene, mentre il secondo è il numero di

buffer liberi ancora disponibili per le operazioni di journaling.

Ogni transazione raggruppa più handle per assicurare migliori perfomance. Se così non fosse e il

journal facesse le operazione sui singoli handle, ci sarebbe un grosso overhead dovuto alle piccole

ma continue richieste del journal. Essendo ogni handle associato ad un processo specifico il numero

di handle presenti nel sistema può essere notevole generando un carico di lavoro eccessivo se

fossero considerati singolarmente. La transazione è definita dal tipo di dato opaco transaction_t il

quale è un typedef della struttura dati transaction_s definita [16]. Tale struttura contiene un

puntatore alla struttura journal_t la quale rappresenta il journal per questa transazione. Contiene una

serie di stati in cui la transazione si può trovare che sono:

T_RUNNING: in questo stato gli handle possono essere aggiunti alla transazione;

T_FLUSH: indica che il journal sta scrivendo i log record;

T_COMMIT: indica una fase in cui i metadati devono ancora essere processati;

T_FINISHED: tutti i dati sono stati scritti sul disco correttamente;

T_LOCKED: la transazione è bloccata per la scrittura sul disco

Il resto sono una serie di liste doppiamente concatenate utilizzate per distinguere i buffer della

transazione in svariate casistiche [16].

Dato che il percorso di codice che entra nel journal, nel contesto di una write, prevede transazioni

normali, e dato l'elevato carico indotto sul journaling dai writer di background durante il test, lprobe

si trova spesso ad aspettare perché la transazione attuale è già in fase di commit. Il processo si

blocca nella funzione start_this_handle() [26]:

if (transaction->t_state == T_LOCKED) {

DEFINE_WAIT(wait);

prepare_to_wait(&journal->j_wait_transaction_locked,&wait,

TASK_UNINTERRUPTIBLE);

Page 31: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

31

read_unlock(&journal->j_state_lock);

schedule();

finish_wait(&journal->j_wait_transaction_locked, &wait);

goto repeat;

}

Se la transazione è nello stato T_LOCKED non è più possibile aggiungere la handle all’attuale

transazione così il processo viene de schedulato e si “addormenta” in attesa che finisca il commit

sullo storage. Inoltre, come si può notare dalle tracce prese con blktrace, l'attesa può diventare

davvero di grandi dimensioni in quanto jbd2 effettua write asincrone le quali sono penalizzate da

BFQ perchè lo scheduler non da weight raising alle write asincrone che quindi si trovano ad avere

un frazione di throughput molto limitata. In questo modo i tempi di completamento della richiesta

del jbd2 si dilatano e si ripercuotono per lo stesso tempo sul programma Lprobe che si blocca,

aumentando così notevolmente la latenza dell'applicazione.

6.2.2 Dirty page balancing e throttling Con Balance e throttle intendiamo due funzioni nel percorso delle write che possono portare al

blocco dell'applicazione LProbe. Entrambe fanno parte della gestione della memoria all'interno del

kernel. La prima balance_dirty_pages() è una funzione presente nel file mm/page-writeback.c [17].

Questa funzione è invocata da balance_dirty_pages_ratelimited_nr [17] passandogli un

address_space _t e un long int ratelimit, valore che è calcolato dalla funzione

sync_writeback_pages() che fornisce un numero di quante pagine si cercheranno di scrivere in caso

si decida di effettuare il writeback non in background [17].

Tale funzione deve essere invocata dal processo il quale ha generato i dati dirty. La funzione guarda

il numero di pagine sporche nel sistema e forzerà ad effettuare il writeback se si sta superando la

vm_dirty_ratio. Per maggiore precisione il valore di vm_dirty_ratio è il quantitativo massimo di

memoria che può essere riempito con pagine sporche prima che tutto venga scritto sul disco.

Quando il sistema raggiunge questa soglia ogni nuova richiesta di I/O deve essere scritta sullo

storage direttamente, diventando così bloccante. Questo è spesso causa di lunghe latenze dovute

all'I/O ma, in compenso, consente una maggiore sicurezza dei dati evitando un eccessivo caching.

Tornando alla funzione se invece si supera la “background_thresh” viene svegliato il thread che si

occupa di effettuare il writeback e l'attuale processo si “addormenta” all'atto della deschedulazione.

Quando scatta il timeout, lo scheduler della cpu rimette il processo in esecuzione, ma se persiste la

situazione di superamento della soglia da parte del sistema il processo torna a riaddomentarsi, e

Page 32: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

32

così via. Tale ciclo di risvegli e riaddormentamenti immediati può durare per svariati secondi

aumentando la latenza. Viene ora riportata e spiegata la parte di codice principale della funzione.

for (;;) {

nr_reclaimable = global_page_state(NR_FILE_DIRTY) +

global_page_state(NR_UNSTABLE_NFS);

nr_dirty = nr_reclaimable + global_page_state(NR_WRITEBACK);

global_dirty_limits(&background_thresh, &dirty_thresh);

if (nr_dirty <= (background_thresh + dirty_thresh) / 2)

break;

….

__set_current_state(TASK_UNINTERRUPTIBLE);

io_schedule_timeout(pause);

….

}

Notiamo subito anche in tale funzione il ciclo for, che viene eseguito all'infinito, all'interno del

quale vengono subito calcolati la background_thresh e la dirty_thresh tramite la funzione

global_dirty_limits(...). Viene poi calcolato il numero di pagine sporche come somma tra le pagine

reclamabili e le pagine di cui fare il writeback.

nr_dirty = nr_reclaimable + global_page_state(NR_WRITEBACK);

Successivamente si va a valutare se il numero nr_dirty è inferiore a (backgroung_thresh +

dirty_tresh)/2; se si, si effettua un break che rompe il ciclo infinito altrimenti si prosegue col ciclo.

Gli altri casi di break riguardano specifiche soglie del bdi. Dopo questi possibili casi di break si

vedono le due istruzioni che addormentano il processo corrente, tra cui troviamo le function

__set_current_state(TASK_UNINTERRUPTIBLE) con la quale si setta lo stato del processo

corrente a task non interrompibile e io_schedule_timeout(...) che de schedula il processo.

Tale funzione prevede un meccanismo per effettuare il writeback direttamente in caso si superi la

sogli di pagine sporche del bdi. Come si può vedere da questi statement presenti nel ciclo for:

if (bdi_nr_reclaimable > task_bdi_thresh) {

pages_written += writeback_inodes_wb(&bdi->wb,write_chunk);

trace_balance_dirty_written(bdi, pages_written);

if (pages_written >= write_chunk)

Page 33: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

33

break;

}

la funzione writeback_inodes_wb() [32] permette di fare il writeback sullo storage delle pagine di

memoria. Tale operazioni è di notevole importanza in quanto in tal caso è il processo stesso a fare

direttamente il writeback mentre di norma tale operazione è effettuata da un demone di sistema.

Il problema del balance_dirty_pages() è uno dei più frequenti. Si può giustificare tale

comportamento anche dalle scelte di design fatte alla base del sistema Android. Esso fa un utilizzo

esteso e intensivo della memoria, quasi sempre mantenendola satura, per attuare un meccanismo di

caching delle applicazioni di modo da rendere più reattivo il loro avvio e utilizzo. Questa scelta non

è però supportata a livello kernel in quanto, in situazioni in cui la disponibilità di memoria è bassa,

si attivano percorsi critici che possono portare ad altissime latenze e a perdere tutto il beneficio di

un eventuale caching.

La seconda throttle_vm_writeout() è una funzione presente nel file mm/page-writeback.c [17]

invocata anch'essa in uno dei possibili percorsi di una write.

Della funzione throttle_vm_writeout, descritta parzialmente sopra, viene riportata la parte

principale:

for ( ; ; ) {

global_dirty_limits(&background_thresh, &dirty_thresh);

dirty_thresh += dirty_thresh / 10; /* wheeee... */

if (global_page_state(NR_UNSTABLE_NFS) +

global_page_state(NR_WRITEBACK) <= dirty_thresh)

break;

congestion_wait(BLK_RW_ASYNC, HZ/10);

}

In questo piccolo blocco di codice si vede chiaramente il funzionamento descritto sopra. Un ciclo

infinito all'interno del quale viene subito invocata la funzione global_dirty_limits() che calcola la

dirty thresholds basandosi non soltanto su vm_dirty_ratio ma anche su altri parametri ritornandoli

all'interno delle variabili passate in ingresso alla funzione. Alla dirty_thresh così calcolata viene

aggiunto un decimo del suo valore; l'obiettivo è quello di aumentare la soglia cercando di prevenire

che dei writers intensi provochino un “denial of service”. Successivamente si va a controllare se il

numero di pagine sporche presenti in memoria è inferiore alla soglia; se si, avviene il break e si esce

Page 34: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

34

dal ciclo for (siamo nel caso in cui si stata già liberata abbastanza memoria) altrimenti si invoca la

funzione congestion_wait(). La congestion_wait prepara una waitqueue e invoca

io_schedule_timeout per de schedulare correttamente l'attuale processo. Da questo momento il

processo si “addormenta”. Anche in questo caso, come nel precedente, quando scatta il timeout si

risveglia il processo e si controlla se le condizioni di uscita sono valide altrimenti si riaddormenta.

Queste due funzioni sembrano svolgere compiti analoghi in realtà sono molto differenti tra loro. La

balance_dirty_pages può essere invocata potenzialmente sempre nel percorso della write: infatti la

funzione generic_perform_write() [18] alla riga 2424 invoca balance_dirty_pages_ratelimited() la

quale invoca a sua volta balance_dirty_pages_ratelimited_nr() che come già detto invoca la

funzione interessata, in quanto si deve occupare di bilanciare le pagine sporche presenti in memoria.

La throttle_vm_writeout invece presuppone una situazione più critica nella quale si ha urgenza di

liberare memoria. Tale funzione è invocata dalla shrink_zone la quale a sua volta è invocata da

shrink_zones che viene invocata da do_try_to_free_pages() [19]. Questo è il principale entry point

per reclamare direttamente le pagine. Se tale funzione nel reclamare pagine fallisse il sistema si

troverebbe “out of memory” e qualche processo verrebbe ucciso. In questo secondo caso ci

troviamo quindi in una situazione più critica rispetto a quella della balance_dirty_pages() e, cosa

ancora più importante, in questa funziona il processo non si occupa di effettuare il writeback

personalmente come nella balance_dirty_pages(), qui viene semplicemente de schedulato per

lasciare spazio al demone di sistema che lo fa per lui.

6.2.3 Race condition semaforo inode L'ultima causa qui illustrata riguarda un percorso caldo e molto importante durante l'esecuzione di

una write. Infatti il percorso critico di invocazioni prima del blocco è:

down_read 5050ms

ext4_map_blocks: 5050ms

get_block: 5050ms

for cicle inside __block_write_begin: 5050ms

__block_write_begin: 5060ms

ext4_da_write_begin: 5060ms

write_begin 5060ms

generic_perform_write: 5060ms

generic_file_buffered_write_inside_else: 5060ms

__generic_file_aio_write 5060ms

Page 35: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

35

blk,generic,unlock 5060ms

generic_file_aio_write 5060ms

filp->f_op_aio_write 5060ms

f_op->write duration: 5060ms

La latenza in realtà è generata dalla funzione down_read() [20].

Come si nota dal codice della down_read, la causa delle alte latenze in questo caso è un corsa critica

su un semaforo, questa race condition è tra l’applicazione LProbe e il flusher (il cui ruolo verrà

spiegato successivamente):

void __sched down_read(struct rw_semaphore *sem) {

might_sleep();

rwsem_acquire_read(&sem->dep_map, 0, 0, _RET_IP_);

LOCK_CONTENDED(sem, __down_read_trylock, __down_read);

}

Tale funzione non da molte informazioni riguardo a cosa sia la causa della corsa critica. Essa è

infatti solamente un front-end alla vera funzione che si occupa di gestire il semaforo che è la

__down_read [21], che è invocata dalla macro LOCK_CONTENDED. È proprio all'interno di tale

funzione che il processo si addormenta.

Analizzandola nello specifico si nota come la funzione prenda in ingresso un puntatore ad una

struttura rw_semaphore:

__down_read(struct rw_semaphore *sem)

Tale semaforo è passato dal down_read che a sua volta lo riceve dalla funzione ext4_map_blocks()

[22].

Brevemente, tale funzione si occupa di vedere se un blocco di memoria richiesta è già mappato; in

caso non lo sia si occupa di prendere un write lock della struttura i_data_sem, di allocare il blocco,

salvarlo in un buffer e marcarlo come mappato.

Di questa funzione interessano quattro invocazioni a funzione:

down_read();

up_read();

down_write();

up_write();

Tutte queste quattro invocazioni sono tra loro correlate in quanto gestiscono appunto i semafori in

Page 36: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

36

questione. Tutte prendono in ingresso lo stesso attributo, i_data_sem menzionato sopra.

Tale parametro è un rw_semaphore appartenente alla struct ext4_inode [23] che si ricava partendo

dallo struct inode. Come meccanismo di mutua esclusione è stato scelto il semaforo in quanto

permette di addormentare il processo senza che la CPU rimanga in un'attesa attiva. Inoltre questo

semaforo permette di gestire in maniera più flessibile chi debba usufruire dell'inode. Ad esempio la

funzione __up_write() invoca la funzione __rwsem_do_wake() [24] la quale gestisce in maniera

diversa il risveglio dei processi. Può scegliere se svegliare un processo che sta aspettando nella

condizione “RWSEM_WAITING_FOR_WRITE” e quindi svegliare chi si trova bloccato nella

__down_write() oppure decidere se svegliare tutti quei processi bloccati nella __down_read() nella

condizione “RWSEM_WAITING_FOR_READ”. Alla funzione __rwsem_do_wake() è passato un

parametro wakewriter, il quale passando per la __up_write() assume sempre valore 1. Quindi questo

if nella funzione:

if (!wakewrite) {

if (waiter->flags & RWSEM_WAITING_FOR_WRITE)

goto out;

goto dont_wake_writers;

}

non viene mai eseguito tramite questo percorso, ma se il primo nella lista del semaforo è bloccato

sulla __down_write(), allora viene risvegliato dagli statement successivi:

if (waiter->flags & RWSEM_WAITING_FOR_WRITE) {

sem->activity = -1;

list_del(&waiter->list);

tsk = waiter->task;

smp_mb();

waiter->task = NULL;

wake_up_process(tsk);

put_task_struct(tsk);

goto out;

}

Un oggetto inode rappresenta tutte le informazioni necessarie al kernel per manipolare un file o una

directory. Questa struttura è la rappresentazione di ogni file all'interno del file system. A differenza

Page 37: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

37

dei file però gli inode sono costruiti solo in memoria quando il file interessato viene acceduto [25].

Fatta questa piccola spiegazione su cosa sia un inode si vede come la causa della latenza, essendo il

semaforo i_data_sem legato proprio all'inode di un file, possa essere dovuta alla corsa critica sul

semaforo quando due processi distinti stanno lavorando sullo stesso inode. Per trovare il processo

che provoca questa corsa critica sono state utilizzate molte printk in punti specifici delle quattro

funzioni elencate sopra, all'interno delle quali è stato fatto stampare il pid del processo, l'indirizzo

del semaforo, il nome del processo e dove possibile l'inode. Dopo svariati test si è notato come il

processo sia in competizione con il flusher. Il flusher è un demone, presente anche nei sistemi linux

classici, che si occupa di effettuare il writeback delle pagine di memoria sullo storage. Durante le

operazioni di writeback il flusher prende il semaforo dell'inode. Il flusher effettuando write

asincrone viene molto penalizzato da BFQ. Tale penalizzazione comporta però lunghe latenze in

quanto finchè la sua write non è stata completata mantiene il semaforo occupato. Come già spiegato

precedentemente per il caso del jorunaling, BFQ non da weight raising alle write asincrone

garantendogli così una frazione di throughput ridotta che ne dilata i tempi di completamento.

6.3 Soluzioni e risultati sperimentali

Vengono ora elencate le soluzioni alle tre cause descritte sopra.

6.3.1 Soluzioni proposte Prima di spiegare come i vari problemi delineati sopra sono stati risolti è necessario spiegare come è

stato risolto un problema preliminare cioè quello dell'identificare uno specifico processo all'interno

del kernel risolve questo problema è un punto focale delle soluzioni proposte in quanto permette di

variare il comportamento del codice in base a chi lo sta eseguendo. A tale scopo si è scelto di

utilizzare una caratteristica di BFQ utilizzando il weight raising [8], identificare l’applicazione in

questo particolare stato significa scoprire se chi sta eseguendo una particolare funzione è da

privilegiare o meno creando percorsi di codice che risolvano la latenza. Durante gli esperimenti

abbiamo utilizzato l'ionice per modificare l'ioprio e far si che un certo processo fosse in weight

raising persistente. È stato così aggiunto un nuovo header file all'interno della parte di interface di

linux nel quale sono state definite delle funzioni che permettono di vedere se un processo

(identificato dalla sua task struct) sia in weight raising anche al di fuori della parte dedicata allo

scheduler. Così, in qualsiasi punto del kernel, è possibile sapere se il processo che sta eseguendo

attualmente quella funzione (identificato dal current) è, o meno, in weight raising distinguendolo da

tutti gli altri.

In tutte le soluzioni è presente la variabile booleana wrais la quale ha valore vero se l'attuale

processo in esecuzione è in weight raising, falso altrimenti. Tale valore è ricavato da due particolari

Page 38: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

38

funzioni:

wrais = bfq_ioc_is_wraised(get_bfq_cic(bdi,current));

se nella funzione interessata abbiamo il bdi oppure:

wrais = bfq_is_wraised(current);

se non disponiamo del bdi. Entrambe le funzioni le sono definite nell’header file descritto sopra.

Per quanto riguarda la prima causa delle latenze (journaling) è stata risolta inserendo una piccola

modifica alla funzione start_this_handle() [26];

/*

* If the current transaction is locked down for commit, wait for the

* lock to be released.

*/

if (transaction->t_state == T_LOCKED && !wrais) {

DEFINE_WAIT(wait);

prepare_to_wait(&journal->j_wait_transaction_locked,

&wait, TASK_UNINTERRUPTIBLE);

read_unlock(&journal->j_state_lock);

schedule();

finish_wait(&journal->j_wait_transaction_locked, &wait);

goto repeat;

}

La condizione dell'if, dove il processo oggetto degli esperimenti si andava poi addormentando, è

stata modificata aggiungendo in and la condizione !wrais.

Così facendo forziamo l'handle ad attaccarsi ad una transazione in commit. Il meccanismo di

aggiunta delle handle è gestito da un sistema di crediti, se l’attuale transazione ha ancora crediti

allora vuol dire che ha spazio libero e le handle possono essere aggiunte. Si è notato come molto

spesso la transazione non esaurisca i crediti ciò permette alla nostra soluzione di risolvere il

problema forzando l’aggiunta. Questa soluzione si potrebbe pensare porti a problemi in caso la

transazione originata sia troppo grande o non ci sia più spazio per la handle. In realtà anche tale

problema sembra essere mitigato dal codice della funzione in quanto, se la transazione è troppo

grande, il processo viene “addormentato”, si inizia subito il commit della transction e la handle in

più è attaccata alla transazione successiva.

Page 39: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

39

La seconda causa, quella riguardante le funzioni balance_dirty_pages() [17] e

throttle_vm_writeout() [17], è stata risolta molto semplicemente forzando il break dei cicli infiniti

presenti in entrambe le funzioni se un certo processo è in weight raising.

All'interno del ciclo è stato quindi aggiunta tale modifica:

for (;;) {

…...

if (wrais)

break;

…..

}

Anche in questo caso la piccola modifica apportata risolve efficacemente il problema e non si

notano più problemi di latenza all'interno di queste funzioni. Tali funzioni inoltre sono percorsi

preventivi che il sistema operativo attiva se si supera una certa soglia di memoria il che non implica

che essa sia completamente satura. La soluzione sicuramente spreme ulteriormente la memoria ma

essendo solo un processo a farlo, se la sua impronta in memoria è minima, non si dovrebbe arrivare

a condizioni di saturazione completa della memoria centrale, anche se il rischio permane.

L'ultima causa, relativa alla corsa critica sul semaforo dell'inode, è stata risolta cercando di mitigare

l'effetto del flusher che tiene occupato il semaforo per molto tempo. Non potendo decidere come

gestire il semaforo a vantaggio del programma è stato scelto di agire sulle operazioni di writeback,

forzando il flusher a saltare le pagine relative all'inode interessato dall'applicazione Lprobe.

Per fare questo è stato aggiunto un campo alla struttura inode [25]:

atomic_t counter;

tale valore può assumere valori 0 o 1 e funge quindi come una sorta di variabile booleana. È stato

scelto di utilizzare una variabile atomic_t per essere sicuri che le operazioni di lettura e modifica

della variabile fossero svolte in maniera atomica e sincronizzate, evitando accessi multipli.

La variabile è settata a 1 nella funzione generic_perform_write(), il più in basso possibile nello

stack di funzioni di una write, attraverso la funzione atomic_set(). È importante notare come tale

variabile sia settata dal processo in weight raising ciò permette di legare il processo agli inode che

sta utilizzando distinguendo così gli inode del processo in wrais dagli altri esistenti:

if(wrais){

Page 40: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

40

atomic_set(&inode->counter,1);

}

Per riuscire ad effettuare lo “skip” delle pagine da parte del flusher è stata modificata la funzione

write_cache_pages() all'interno del file mm/page-writeback.c [17]. Tale funzione già parzialmente

prevede un meccanismo attraverso il quale sia possibile saltare della pagine senza creare problemi

al resto del sistema. La funzione è strutturata con un ciclo “while(!done && (index <= end))”

principale che continua o fino a quando non viene settata la variabile “done” a vero oppure fino a

quando, utilizzando gli offset delle pagina, non si è finito di scorrere le pagine delle quali occorre

fare il writeback. All'interno del ciclo, come già detto, è stato utilizzato il meccanismo preesistente

modificandone le condizioni:

if (unlikely(page->mapping != mapping) || atomic_read(&inode->counter)) {

continue_unlock:

unlock_page(page);

if(atomic_read(&inode->counter)){

break;

}

continue;

}

è stata aggiunta la condizione in or logico dove viene letto il valore nella variabile atomica

dell'inode e in caso sia già stata settata a 1 per quell’inode allora la sua pagina viene saltata e

sbloccata tramite l'unlock_page(..) (all'interno della quale vengono modificati degli specifici flag

relativi alla pagina), facendo inoltre un break del ciclo while.

Il contatore viene resettato a zero quando l'applicazione ha finito di lavorare sul file e decide di

chiuderlo nella funzione ext4_release_file(..) dove attraverso le istruzioni:

if(atomic_read(&inode->counter) > 0){

atomic_set(&inode->counter,0);

}

si setta il counter dell'inode a zero.

Tale soluzione è efficace e permette di saltare le pagine mantenendo comunque consistente il

sistema, in tal modo non si verifica più il blocco del processo sul semaforo e la latenza in tale

Page 41: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

41

funzione ritorna bassa. L'unico possibile svantaggio di questa funzione è un overhead dovuto al

break prematuro del ciclo while perchè così facendo possono essere necessarie più invocazioni del

flusher per fare il writeback della pagine di memoria invece che una sola che le scorre dall'inizio

alla fine e le scrive sul dispositivo di storage.

Tutte le soluzioni sopra proposte hanno però un problema di fondo non banale e ancora

parzialmente irrisolto. Come spiegato a inizio capitolo all'interno degli esperimenti è stato utilizzato

il comando ionice per privilegiare il processo in questione durante l'I/O e poi grazie a tale privilegio

distinguerlo all'interno delle altre parti del kernel. Il problema di questo meccanismo risiede a

monte nell'utilizzo di ionice, tale tool può richiedere notevole tempo non solo per partire ma anche

per essere recepito a livello dello scheduler e apportare tutte le modifiche necessarie. Questo

comporta inevitabilmente che se il processo fa richieste prima che l'ionice arrivi, il suo destino sarà

quello di bloccarsi all'interno di uno dei percorsi precedentemente descritti, provocando un'alta

latenza su quelle richieste rimaste fuori. Come già spiegato nel capitolo 5 è stato aggiunto

all'applicazione Lprobe uno sleep iniziale nel tentativo di sincronizzare il programma e il tool ma

comunque capita che alcune richieste iniziali rimangano escluse anche se la frequenza è molto

ridotta. Non è stata investigata oltre l’esistenza di altri comandi che potessero avviare direttamente

il processo con ionice decisa a priori. Inoltre, eventualmente, si potrebbe modificare BFQ in modo

che sia più reattivo ai cambiamenti di ioprio.

6.3.2 Risultati sperimentali Riportiamo ora i risultati ottenuti inserendo le quattro soluzioni proposte nei capitoli precedenti. Si

effettuerà un confronto tra i risultati ottenuti con un kernel senza le modifiche (sempre contenente

BFQ) e il kernel con le modifiche apportate. Verranno confrontati anche vari carichi di lavoro:

prima il confronto con carico solo reader (rispettivamente prima uno poi due) poi il carico di lavoro

con più reader e writer in contemporanea mettendo anche in risalto il tempo impiegato quando il

dispositivo di storage è in idle. I valori misurano lo start-up time medio di LProbe su un campione

di 10 tentativi per ognuna delle tipologie di background workload.

Page 42: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

42

Confrontando lo start-up time ottenuto con una versione di un kernel contente BFQ standard e un

kernel contenente le modifiche apportate sopra si vede come i tempi di BFQ-mod siano, nei primi

due casi di workload, molto vicini ai tempi in idle rappresentati dalla linea tratteggiata rossa il cui

valore è 6,28 secondi. Mentre nell'ultimo caso BFQ-mod ha un start-up circa doppio rispetto a

quello in idle ma comunque incredibilmente migliore di quello di BFQ-no mod, il tempo così

elevato della versione BFQ-mod è quasi sicuramente dovuto al secondo insieme di cause, descritto

precedentemente. È possibile però che tale latenza sia dovuta anche ad alcune richieste effettuate

prima della modifica dell’ioprio. Il valore 0.00 presente nel caso 1r1w rappresenta concettualmente

“l'infinito”, in quanto nella versione di BFQ-no mod lo start-up dell'applicazione continua fino a

quando il workload non finisce. Il termine è stato stabilito attorno ai 50/60 secondi, un tempo

infinitamente grande per il sistema.

In conclusione in confronto alle versioni di BFQ precedenti questa ottiene ottimi risultati in termini

di start-up di Lprobe sotto vari tipi di workload.

Per quanto riguarda invece l’applicazione candy crush saga attualmente non si vendono

miglioramenti, questo può essere dovuto alla presenza di operazioni su un database che esercitano

notevole pressione sul journal aumentando così la probabilità che il secondo insieme di cause si

verifichi. Tali condizioni necessitano uno studio più approfondito di ciò che avviene a livello kernel

quando si interagisce con un database.

1r 1w 1r1w

0,00

2,00

4,00

6,00

8,00

10,00

12,00

14,00

9,94

11,59

0,00

6,67 6,31

12,50

Confronto tra versioni

BFQ-no mod

BFQ-mod

Se

co

nd

i

Illustrazione 10: Confronto risultati ottenuti prima e dopo le modifiche al kernel

Page 43: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

43

6.4 Secondo insieme di cause

Viene ora riportata l'ultima causa delle latenze nelle operazioni di write. Tale problema è descritto in

un capitolo differente, agli altri esposti sopra, in quanto di esso non è ancora stata trovata una

soluzione definitiva.

6.4.1 Journaling II Anche questa causa come la prima descritta è parzialmente colpa del journaling. Osservando lo

stack di invocazioni che passa per la write end creando la latenza:

infinite_loop_inside_do_get_write_access 11380

do_get_write_acces 11380

jdb2_journal_get_write_access 11380

ext4_journal_get_write_access 11380

ext4_reserve_inode_write 11380

ext4_mark_inode_dirty 11380

ext4_dirty_inode 11380

sb->s_op->dirty_inode 11380

mark_inode_dirty 11380

generic_write_end: 11380

ext4_da_write_end: 11380

write_end 11380

…..(Il resto dello stack è identico al percorso già inserito nella write begin)

si nota immediatamente come ci sia la presenza di alcune funzioni del journal. In questo percorso è

proprio il processo stesso a cercare di avviare la fase di journaling per effettuare il commit di alcuni

metadati sul disco. Concettualmente, in questa causa di latenza, il problema si annida nel tentativo

da parte del processo di avviare il journaling su un certo buffer che però è bloccato in quanto già

oggetto di un commit precedente non ancora terminato, il processo si trova quindi ad attendere la

terminazione del commit sullo storage con conseguente aumento della latenza per i motivi già citati

nelle altre casistiche.

La funzione do_get_write_access() [28] si occupa di verificare se il buffer (identificato dalla

struttura buffer_head illustrata in seguito) è già parte della transazione corrente del journal; se lo è

non si fa nulla; altrimenti ci sono una serie di casistiche in cui ci possiamo trovare che possono

portare o ad effettuare il copy-out del buffer oppure ad un “corner case” che è proprio quello in cui

Page 44: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

44

Lprobe si addormenta. Preliminarmente illustriamo cos'è la struttura dati buffer_head e il

meccanismo del copy-out.

La buffer_head è una struttura dati utilizzata per mappare un blocco all'interno di una pagina. Il

buffer di una pagina di memoria viene fatto mappare col blocco corrispondente [29]. Il buffer della

pagina rappresenta il suo contenuto mentre il blocco rappresenta un insieme di settori contigui sullo

storage. Tale struttura è stata l'unità di I/O tra il filesystem e il block layer anche se attualmente è

stata sostituita dalle struct bio. La struttura buffer_head rimane comunque utilizzata per estrarre la

mappatura dei blocchi, per tracciare lo stato delle pagine e per retrocompatibilità dopo

l'introduzione delle bio.

La struttura buffer_head è composta da diversi campi:

struct buffer_head {

unsigned long b_state;

struct buffer_head *b_this_page;

struct page *b_page;

sector_t b_blocknr;

size_t b_size;

char *b_data;

struct block_device *b_bdev;

bh_end_io_t *b_end_io;

void *b_private;

struct list_head b_assoc_buffers;

struct address_space *b_assoc_map;

atomic_t b_count;

};

Tra i campi più importanti della struttura dati troviamo il puntatore alla buffer_head b_this_page,

utilizzato per implementare una lista circolare dei buffer delle pagine; troviamo il puntatore b_page

il quale punta alla pagina cui è riferita la buffer_head e troviamo un puntatore a char b_data che

individua i dati all'interno della pagina.

Page 45: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

45

Il copy-out è invece uno specifico meccanismo messo in atto dal journal attraverso il quale si

effettua una copia del buffer allo scopo di evitare che, in alcuni casi molto specifici, la copia

originale che sta andando sullo storage venga modificata. Effettuando del tracing a livello kernel si

vede come tale meccanismo venga utilizzato davvero di rado, ma è interessante in quanto può

essere di spunto per un'eventuale soluzione.

Fatte queste brevi ma dovute spiegazioni preliminari analizziamo la funzione

do_get_write_access(). Tale funzione prende in ingresso tre parametri: il primo è un puntatore ad

una handle_t (handle_t *handle), il secondo un puntatore alla struct journal_head(struct

journal_head *jh) e il terzo un intero (force_copy) il quale permette di decidere se forzare il copy-

out o meno. La struttura dati journal_head è definita nel file /include/linux/journal-head.h [30] e

contiene svariati campi tra cui, degni di un'analisi approfondita, troviamo: un puntatore ad una

struct buffer_head, un unsigned b_jlist, un char pointer b_frozen_data all'interno del quale viene

salvata la copia del buffer fatta durante i copy-out, due puntatori alla stuct journal_head b_next e

b_prev utilizzati per implementare una lista doppiamente concatenata.

Il campo b_jlist viene settato nella funzione __jbd2_journal_file_buffer() [31] e può assumere sette

diversi valori: BJ_None, BJ_Metadata, BJ_Forget, BJ_IO, BJ_Shadow, BJ_LogCtl, BJ_Reserved.

Tutti questi possibili valori rappresentano una diversa lista presente nella struttura transaction_s

[16].

Analizzando il punto in cui il Lprobe si “addormenta” all'interno della do_get_write_access():

if (jh->b_jlist == BJ_Shadow) {

DEFINE_WAIT_BIT(wait, &bh->b_state, BH_Unshadow);

Illustrazione 11: Struttura dati buffer_head

Page 46: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

46

wait_queue_head_t *wqh;

wqh = bit_waitqueue(&bh->b_state, BH_Unshadow);

JBUFFER_TRACE(jh, "on shadow: sleep");

jbd_unlock_bh_state(bh);

/* commit wakes up all shadow buffers after IO */

for ( ; ; ) {

prepare_to_wait(wqh, &wait.wait,

TASK_UNINTERRUPTIBLE);

if (jh->b_jlist != BJ_Shadow)

break;

schedule();

}

goto repeat;

}

vediamo subito come se entra nell'if viene preparata una waitqueue e poi si fa entrare il processo in

un ciclo infinito in cui controlla se la condizione di uscita è valida altrimenti viene de schedulato.

Tale casistica all'interno della do_get_write_access() è molto delicata. Se la transazione che sta

effettuando il commit sta scrivendo il buffer sul dispositivo di storage, e non è stato

precedentemente fatto il copy-out, allora in questo momento non può essere modificato il contenuto

del buffer. L'essenza del copy-out è quella di avere una copia extra, distinta da quella originale della

quale si fa il journaling. Se la copia principale sta già andando verso lo storage il copy-out non può

essere effettuato, quindi il processo deve attendere che il commit sia completato ma, come già

spiegato, il jdb2 fa richieste asincrone che sono molto penalizzate da BFQ e quindi causano un

lungo stallo dell'applicativo.

A tale problema non è ancora stata trovata una soluzione definitiva. Sono state provate svariate

modifiche che non hanno però dato i risultati sperati. In linea di massimo stiamo cercando di forzare

il meccanismo del copy-out, da parte del journal thread, per quei buffer sui quali il processo si

blocca. Al momento tale soluzione è ancora in fase embrionale e non viene riportata nell’elaborato.

Page 47: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

47

7. Confronto tra scheduler

Vengono ora confrontati i risultati ottenuti con le modifiche apportate al kernel, aggiungendo le

soluzioni proposte nel capitolo 6, e i risultati ottenuti utilizzando gli scheduler di default presenti in

qualsiasi sistema. Anche in questo caso la linea tratteggiata rossa nel grafico rappresenta il tempo di

start-up quando lo storage è in idle, mentre il valore 0,00 indica che il processo termina lo start-up

quando termina il workload in background, quindi potenzialmente un tempo infinito. I valori

rappresentano il tempo di start-up medio su un campione di 10 test dell’applicazione LProbe. Sono

stati utilizzati i valori medi anche se il rapporto tra varianza e valori medi era elevato. In tutti questi

test è stata assegnata ioprio 0 e classe real time indistintamente dallo scheduler utilizzato, ciò

avvantaggia CFQ, mentre gli scheduler non risentono della modifica. Successivamente vengono

riportati anche i dati reali relativi a CFQ senza ioprio modificata.

1r

0,00

5,00

10,00

15,00

20,00

25,00

30,00

6,678,48

23,82

12,72

Start-up time con 1 reader

BFQ-mod

CFQ

DEADLINE

NOOP

Se

co

nd

i

Illustrazione 12: Tempo di start-up LProbe con un reader come workload

Page 48: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

48

1w

0,00

5,00

10,00

15,00

20,00

25,00

6,31 6,84

21,99

0,00

Start-up time con 1 writer

BFQ-mod

CFQ

DEADLINE

NOOP

se

co

nd

i

Illustrazione 13: Tempo di start-up LProbe con un writer come workload

1r1w

0,00

5,00

10,00

15,00

20,00

25,00

12,50

21,08

0,00 0,00

Start-up time con 1 reader e 1 writer

BFQ-mod

CFQ

DEADLINE

NOOP

se

co

nd

i

Illustrazione 14: Tempo di start-up LProbe con workload un reader e un writer

Page 49: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

49

Come si può vedere dai grafici riportati, BFQ con le modifiche al kernel ha performance

decisamente migliori degli altri scheduler disponibili. In caso di background workload cospicui,

come 1 reader e 1 writer, permangono però i problemi legati al journal illustrati nel capitolo 6.3 che

dilatano i tempi notevolmente e allontanano BFQ dai tempi di start-up ottenuti con il dispositivo di

storage in idle.

Vengono ora riportati i dati “reali” in rapporto ai differenti workload. Con reali intendiamo senza

ioprio modificata per lo scheduler CFQ. DEADLINE e NOOP restano uguali in quanto non

utilizzano il concetto di ioprio. Questi dati ci permetto di capire la reale validità delle soluzioni da

noi proposte confrontandole con l’utilizzo degli scheduler disponibili senza avvantaggiare

l’applicazione.

1r 1w 1r1w

0,00

5,00

10,00

15,00

20,00

25,00

30,00

6,67 6,31

12,50

8,486,84

21,08

23,82

21,99

0,00

12,72

0,00 0,00

Start-up time LProbe

BFQ-mod

CFQ

DEADLINE

NOOP

se

co

nd

i

Illustrazione 15: Grafico riassunto Start-up LProbe con diversi workload

Page 50: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

50

come si può vedere togliendo ioprio da CFQ le sue prestazioni peggiorano notevolmente tanto che

nel caso di background workload composto da un reader e un writer lo start-up time medio è più del

doppio rispetto a quello di BFQ modificato e quattro volte il tempo con il dispositivo di storage in

idle.

1r 1w 1r1w

0

5

10

15

20

25

30

6,67 6,31

12,513,09

7,03

27,21

23,8221,99

0,00

12,72

0,00 0,00

Start-up time LProbe senza ionice CFQ

BFQ-mod

CFQ

DEADLINE

NOOP

Se

co

nd

i

Illustrazione 16: Start-up LProbe senza ionice CFQ

Page 51: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

51

8. Conclusioni e futuri sviluppi In conclusione il lavoro di tesi svolto all'Università degli studi di Modena e Reggio Emilia ha dato

risultati soddisfacenti. Come mostrato nel capitolo 7 le modifiche apportate al kernel hanno portato

un ottimo miglioramento delle prestazioni tracciando una strada da seguire in futuro. Il problema

della latenza delle applicazioni in presenza di I/O è molto dibattuto non solo per quanto riguarda

sistemi Android ma anche nei sistemi desktop. È un problema ampio e importante che, come

dimostrato nell'elaborato, esula dal solo scheduler dello storage e si annida in parti diverse del

kernel, come la gestione della memoria virtuale e il journaling. Le soluzioni fornite non hanno lo

scopo di risolvere il problema nella sua completezza ma l'obbiettivo è quello di mitigare la latenza il

più possibile rendendola accettabile per l'utente. L'elaborato ha anche l'obbiettivo di aiutare e

fornire le informazioni basilari a chi decida di continuare il lavoro di ricerca qui iniziato portando

avanti la battaglia contro la latenza.

Nell'immediato futuro ci sono molte strade aperte. Il primo passo da svolgere sarebbe quello di

sistemare in maniera efficace la causa journaling per riuscire a portare lo start-up time vicino ai

tempi ottenuti quando il dispositivo di storage è in idle, riuscendo a mantenere il filesystem in un

stato consistente. Successivamente occorrerà iniziare a testare le soluzioni su applicazioni diverse

da Lprobe e vedere se anche in quel caso l'improvement alla responsivness dell'applicazione è

davvero così elevato. Il codice andrà rivisto, sistemato per distribuirlo alle comunità internazionali e

dovranno essere svolti controlli di non regressione per verificare che le nuove modifiche introdotte

non porti alla formazione di bug in alcune parti del sistema.

L'argomento è davvero complesso ma spero, con questo elaborato, di stimolare altri giovani

universitari a intraprendere questa strada e portare avanti un lavoro di notevole importanza che ha

bisogno di continue nuove forze per poter essere risolto definitivamente.

Page 52: UNIVERSITÁ DEGLI STUDI DI MODENA E REGGIO EMILIAalgo.ing.unimo.it/algodev/theses/tesi_triennale_zecchini.pdfDipartimento di Scienze Fisiche,Informatiche e Matematiche Corso di Laurea

52

Bibliografia

[1] https://www.usenix.org/conference/fast15/technical-sessions/presentation/jeong

[2] Karim Yaghmour (2013) - Embedeed Android

[3] https://dl.google.com/googleio/2010/android-jit-compiler-androids-dalvik-vm.pdf

[4] https://os.itec.kit.edu/downloads/sa_2010_braehler-stefan_android-architecture.pdf

[5] Wen Hu, Yanli Zhao - Analysis on Process Code schedule of Android Davil Virtual Machine

[6] http://lwn.net/Articles/318611/

[7] http://events.linuxfoundation.org/images/stories/slides/abs2013_gargentas.pdf

[8] Paolo Valente, Arianna Avanzini (2015) - Evolution of BFQ storage-I/O scheduler

[9] http://developer.android.com/tools/debugging/debugging-log.html

[10] https://www.kernel.org/doc/Documentation/trace/ftrace.txt

[11] https://developer.android.com/tools/debugging/debugging-memory.html#ViewingAllocations

[12] http://linux.die.net/man/1/ionice

[13] Wolfagang Mauerer (2008) - Professional linux kernel architecture

[14] http://androidxref.com/kernel_3.0/xref/include/linux/jbd2.h#97

[15] http://androidxref.com/kernel_3.0/xref/include/linux/jbd2.h#419

[16] http://androidxref.com/kernel_3.0/xref/include/linux/jbd2.h#513

[17] http://androidxref.com/kernel_3.0/xref/mm/page-writeback.c

[18] http://androidxref.com/kernel_3.0/xref/mm/filemap.c#2342

[19] http://androidxref.com/kernel_3.0/xref/mm/vmscan.c#2085

[20] http://androidxref.com/kernel_3.0/xref/kernel/rwsem.c#19

[21] http://androidxref.com/kernel_3.0/xref/lib/rwsem-spinlock.c#142

[22] http://androidxref.com/kernel_3.0/xref/fs/ext4/inode.c#1279

[23] http://androidxref.com/kernel_3.0/xref/fs/ext4/ext4.h#578

[24] http://androidxref.com/kernel_3.0/xref/lib/rwsem-spinlock.c#302

[25] Robert Love - Linux kernel Development third edition

[26] http://androidxref.com/kernel_3.0/xref/fs/jbd2/transaction.c#117

[27]https://support.google.com/nexus/answer/2841846?hl=it&ref_topic=2841129&vid=1-

635775064708029292-4080615270

[28] http://androidxref.com/kernel_3.0/xref/fs/jbd2/transaction.c#583

[29] http://androidxref.com/kernel_3.0/xref/include/linux/buffer_head.h#59

[30] http://androidxref.com/kernel_3.0/xref/include/linux/journal-head.h#19

[31] http://androidxref.com/kernel_3.0/xref/fs/jbd2/transaction.c#1962

[32] http://androidxref.com/kernel_3.0/xref/fs/fs-writeback.c#583