UTILIZZO E RICOMPILAZIONE DI BFQ COME MODULO E' estremamente più pratico utilizzare e ricompilare BFQ come modulo. In particolare, ci concentriamo su bfq-mq. Se bfq-mq non è compilato come modulo, modificate il config del kernel, utilizzando ad esempio "make menuconfig", dopodiché ricompilate, installate e riavviate il kernel. Se bfq-sq o bfq-mq è compilato come modulo, per vedere lo scheduler, dovete caricare il rispettivo modulo (vi faccio gli esempi solo con bfq-mq): sudo modprobe bfq_mq_iosched Per toglierlo: sudo modprobe -r bfq_mq_iosched Per selezionare bfq-mq per un dato dispositivo: eseguire, da root: echo bfq-mq > /sys/block//queue/scheduler oppure, se non si è root echo bfq-mq | sudo tee /sys/block//queue/scheduler Per controllare: cat /sys/block//queue/scheduler Una volta selezionato bfq-mq, avete i suoi parametri nella cartella /sys/block//queue/iosched NOTA: anche se il modulo non è caricato, e quindi bfq-mq non appare nella lista degli scheduler, potete comunque selezionare bfq-mq. Questo comporterà il caricamento automatico del modulo bfq-mq-iosched. Il grande vantaggio di avere bfq-mq caricato come modulo si evidenzia nel caso in cui lo si modifichi. In tal caso, per poter utilizzare la versione modificata, non è più necessario ricompilare e reinstallare tutto il kernel. Basta solo ricompilare e reinstallare i moduli. I comandi sono make -j O= modules sudo make O= modules_install Fatto questo, 1. assicuratevi che bfq-mq non sia più selezionato come I/O scheduler per alcun dispositivo (altrimenti il seguente comando fallisce). 2. Rimuovete il modulo bfq-mq-iosched sudo modprobe -r bfq-mq-iosched 3. Ricaricate il modulo bfq-mq-iosched, oppure semplicemente selezionate direttamente bfq-mq come scheduler per il vostro dispositivo di interesse ---------------------------------- TEST DI COMPILAZIONE VELOCE make M= Esempio: make -j 3 O=~/linux-build M=block La compilazione con opzione M= è talmente riduttiva che non esegue neanche la generazione di eventuali file generali, che potrebbero essere necessari per compilare correttamente il contenuto della cartella scelta. In tal caso la compilazione fallisce. Inoltre non tiene conto di eventuali cambiamenti del file .config. Per ottenere una compilazione coerente, potete usare quest'altra forma, più lenta ma più generale: make Una volta generati i file generali, si può utilizzare la forma M= senza problemi. ----------------------------------- FINTA COMPILAZIONE Se si cambia branch, git aggiorna i timestamp di tutti i file che cambiano, usando come nuovo timestamp l'ora corrente. Questo implica che, anche se ci si sposta da un branch con file recenti, ad un branch con file meno recenti, e si ha una cartella di build già contenente i file oggetto per la versione meno recente dei file sorgente (tale cartella potrebbe anche coincidere con la cartella stessa dei sorgenti, nel caso non si usi una cartella di build distinta), sarà comunque necessario ricompilare tutti i file che sono cambiati. In questo caso, se si è sicuri che il contenuto della cartella di build sia a posto, si può usare il seguente trucco make -t O= Poi, per le cartelle contenenti file che invece si sa che potrebbero essere più recenti dei file oggetto nella cartella di build, si può fare il make (vero) a parte make O= Questo trucco purtroppo non funziona se CONFIG_STACK_VALIDATION è abilitata, perché in tal caso parte il tool objtool, che è incompatibile con questo trucco. ----------------------------------- ACCESSO REMOTO AD UN GUEST Vi consiglio vivamente di interagire con le vostre macchine virtuali attraverso ssh. Per farlo, dovete avere il l'emulatore di macchine virtuali con rete interna emulata, ed il pacchetto openssh-server installato sul guest. Per esempio, su VirtualBox, dovete 1) Andare su Preferenze->Rete->Reti solo host ed aggiungere una scheda virtuale 2) Nella impostazioni della macchina virtuale, andare su Rete ed aggiungere una seconda scheda di tipo "solo host". Vi verrà proposta proprio la scheda virtuale che avete creato nel passaggio precedente Poi dovete ovviamente scoprire l'indirizzo di rete del guest, all'interno della rete emulata nell'host. Di norma basta un ifconfig da dentro il guest. Supponiamo che ad esempio sia: 10.0.2.15, dall'host fate: ssh nome_utente@10.0.2.15 Vi può tornare molto utile usare ssh-copy-id, così da non dover poi dare la passwd ogni volta. Se ssh-copy-id non va perché non avete una chiave nell'host, allora generatela invocando semplicemente ssh-keygen. ------------------------------------ KERNEL LOG Il kernel scrive i propri messaggi per l'utente all'interno di un array circolare di messaggi, chiamato kernel log. Potete visualizzare il contenuto del kernel log con il comando dmesg Ogni messaggio riporta anche l'istante in cui è stato generato, a partire dall'accensione del sistema. Ogni messaggio inserito nel kernel log è anche stampato automaticamente su una delle console. Si può configurare il kernel per spedire questi messaggi anche su console seriale, ossia sulla porta seriale. Per esempio, se si usa grub per il boot, editate il file /etc/default/grub e modificate il seguente parametro come segue: GRUB_CMDLINE_LINUX=" console=ttyS0,115200n8 ignore_loglevel" quindi aggiornare grub: sudo update-grub e riavviare. Questo è particolarmente utile se il kernel è eseguito in una macchina virtuale, perché la macchina virtuale presenta al guest delle interfacce seriali virtuali. Si può configurare la macchina virtuale affinché quello che il kernel spedisce su una di tali interfacce seriali virtuali, viene scritto all'interno di un file. Ad esempio, con VirtualBox, basta andare su Porte, abilitare una delle porte seriali, scegliere la modalità raw per tale porta, e scrivere il percorso del file in cui si vuole che finisca il log del kernel. ---------------------------- DEBUGGING OOPS Per ritrovare la riga di codice in cui accade un fallimento, a partire da un OPS, si può utilizzare, ad esempio, gdb gdb -> NON PASSATE TUTTO ESEGUIBILE KERNEL, perché altrimenti funziona solo se quella riga è relativa ad un componente compilato con built-in. Per esempio, se il problema è in una funzione definita dentro bfq-mq-iosched.c, allora gdb /block/bfq-mq-iosched.o Poi, da dentro gdb: list *(nome_funzione+offset) ------------------------------- CONTROLLI IN KERNEL Il kernel è in grado di effettuare dei controlli automatici di consistenza e correttezza di operazioni e dati. Vi può essere molto utile attivare alcuni o molti di questi controlli, a seconda del tipo di modifiche che fate nel kernel. Eccovi quelli che uso più di frequente. Trovate tutti questi controlli dentro "Kernel Hacking" nel menuconfig. Memory Debugging -> Kernel Memory Leak detector Settate poi "Maximum kmemleak early log entries" ad almeno 3000, perché altrimenti, se gli si riempie il buffer, si disattiva al boot, e poi non segnala alcun errore, anche se ce ne sono. Per controllare se tutto è andato bene al boot, cercate kmemleak in dmesg. Memory Debugging -> KASan: runtime memory debugger (questo può' essere pesante per le prestazioni, ho il sospetto che rallenti anche il tempo di compilazione e faccia aumentare di molto le dimensioni dei file oggetto) Debug Lockups and Hangs -> Detect Hung Tasks Lock Debugging (spinlocks, mutexes, etc...) -> Lock debugging: prove locking correctness Debug linked list manipulation Trigger a BUG when data corruption is detected ------------------------------- COMANDI UTILI QUANDO SI LAVORA SU DUE BRANCH DISTINTI Posizionarsi nel branch nel quale si vogliono effettuare le modifiche git checkout Per controllare i commit nell'altro branch, con anche le diff: git log -p Per vedere il contenuto dei file nell'altro branch: git show : Per vedere le diff tra il branch di lavoro e l'altro: git diff ------------------------------- KERNEL PROFILING CON PERF Se state eseguendo un kernel custom, è poco probabile che la versione i perf che avete eventualmente già installata nel sistema sia compatibile con tale kernel. Vi tocca pertanto compilarvi perf per il vostro kernel. Trovate i sorgenti di perf di cui avete bisogno direttamente nell'albero del kernel. Per poter compilare perf, dovete avere i seguenti pacchetti installati sudo apt install flex bison elfutils libdw-dev Le istruzioni per compilare perf sono: cd /tools/perf make [O=] sudo make [O=] install Le regole di installazione metteranno l'eseguibile di perf qui: ~/bin/perf Se necessario, abilitare le opzioni relative a perf nel kernel. Un kernel stock dovrebbe già avere tutte le opzioni necessarie abilitate. Quindi, se avete generato il config con lo script streamline, dovreste ritrovarvi tutte le opzioni abilitate. Possono essere molto convenienti i flamegraph, generabili con questo tool: https://github.com/brendangregg/FlameGraph Istruzioni su come generare un report ed il corrispondente flamegraph qui: http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html Può non essere banale far funzionare correttamente la catena di istruzioni necessaria per arrivare ad un flamegraph. Ecco una sequenza che dovrebbe funzionare, per prendere tutti gli eventi che stanno accadendo nel sistema per N secondi: sudo /home/paolo/bin/perf record -a -g --call-graph dwarf -- sleep N sudo /home/paolo/bin/perf script | ../FlameGraph/stackcollapse-perf.pl > out.perf-folder sudo ../FlameGraph/flamegraph.pl out.perf-folded > perf-kernel.svg Se volete isolare la sola attività del kernel: sudo /home/paolo/bin/perf record -e cycles:k -a -g --call-graph dwarf -- sleep 5 Ecco un esempio di riga di comando che fa un po' tutto: sudo rm *perf* ; sudo /home/paolo/bin/perf record -e cycles:k -a -g --call-graph dwarf -- sleep 5 && sudo /home/paolo/bin/perf script | ../FlameGraph/stackcollapse-perf.pl > out.perf-folded && sudo ../FlameGraph/flamegraph.pl out.perf-folded > perf-kernel.svg ----------------------- KERNEL PROFILING CON eBPF/BCC E' disponibile molta documentazione, ben fatta, su questi strumenti. Quindi, anziché ripetere dettagli già disponibili, passo a fornivi una micro guida su quando utilizzare questi strumenti generali (incluso perf stesso), e quando passare a tecniche più precise. ------------------- STRUMENTAZIONE DEL CODICE Tool come perf o eBPF/BCC sono di norma molto utili nella fase di 'avvicinamento' al problema che si vuole risolvere. Una volta individuato chiaramente il problema, conviene quasi sempre passare alla strumentazione del codice, in special modo al tracing: https://en.wikipedia.org/wiki/Instrumentation_(computer_programming) Un trucco particolarmente efficace con l'I/O è il tracing attraverso le funzioni trace_printk: i messaggi finiranno direttamente nella traccia generata dal blk tracer, permettendo di correlare l'I/O con le informazioni aggiuntive che si vogliono monitorare. ------------------- STRUMENTAZIONE DEL CODICE RELATIVO ALL'I/O A PARTIRE DA LIVELLI SUPERIORI ALLO SCHEDULER Nel caso di processi il cui carico di I/O è dato principalmente da molte read o write esplicite, si può usare perf per scoprire di quali operazioni si tratta esattamente, per poi andare a lavorare direttamente su tali funzioni. Queste ultime dovrebbero trovarsi principalmente in fs/read_write.c Nel caso di processi il cui carico di I/O è dato principalmente da dei page fault su file memory-mapped (carico tipico di una applicazione che parte), è __do_page_cache_readahead. Un'altra funzione spesso invocata è filemap_fault in mm/filemap.c, che a sua volta può invocare __do_page_cache_readahead. Scendendo invece più in basso, troviamo la submit_bio, in block/blk-core.c, che a sua volta fa I/O attraverso generic_make_request. In merito, il tempo di esecuzione della generic_make_request è una misura molto efficace delle latenze di I/O presenti nel kernel, sopra il block layer; ossia delle latenze non dovute direttamente al vero e proprio I/O fisico col dispositivo di storage. -------------------- VISUALIZZAZIONE DI FILE E RICERCHE NEI SORGENTI LINUX Uno dei metodi più efficaci e veloci è utilizzare: http://elixir.free-electrons.com/linux/latest/source -------------------- RICERCA FUNZIONI EFFETTIVAMENTE INVOCATE IN PRESENZA DI PUNTATORI A FUNZIONE Spesso nel kernel si ritrova il polimorfismo implementando attraverso puntatori a funzione. Ad esempio: ret = mapping->a_ops->readpages(filp, mapping, pages, nr_pages) Qual è la funzione effettivamente invocata? I classici strumenti per la visualizzazione e la ricerca nel codice non aiutano. Se si cerca readpages, difficilmente si troverà qualcosa. Ecco invece un trucco che spesso funziona: cercare tutti i punti in cui i campi di nome readpages vengono inizializzati. Per farlo, si può utilizzare il comando: egrep -r "readpages.*=" * Oppure, se si vuole essere più selettivi: egrep -r "\.readpages.*=" * oppure egrep -r "\->readpages.*=" * (una delle due ricerche avrà quasi certamente successo) Il passo successivo è capire quale possa essere l'inizializzazione appropriata per il proprio sistema, da cui deduciamo qual è la funzione effettivamente invocata. In caso di dubbio, del semplice tracing può aiutare nuovamente. ---------------------- SCOPERTA DELLA CATENA DI CHIAMATE CHE PORTA AD UNA FUNZIONE Un trucco semplice per scoprire tale catena, è inserire una WARN_ONCE nella funzione. Si tratta di una macro così definita: #define WARN_ONCE(condition, format...) Se condition è vera, stampa quanto riportato nei parametri successivi, con la sintassi e semantica di una printf. Quindi, ad esempio, WARN_ONCE(true, "incorrect cpu parameter %d", cpu); Dopo aver stampato il messaggio una volta, la WARN_ONCE non stamperà più nulla, anche se dovesse essere eseguita di nuovo (con la condizione vera). Questo è comodo per funzioni che sono invocate ad altissima frequenza. Se invece si teme che la funzione potrebbe essere raggiunta da diversi percorsi, allora la WARN_ONE non va ovviamente più bene, perché ne mostrerà al più uno. In tale caso, potreste usare la WARN: #define WARN(condition, format...) che stamperà il messaggio ogni volta che verrà invocata con condition vera. Per evitare il problema di una esplosione di messaggi, in questo caso potreste utilizzare un trucco di questo tipo: funzione_sotto_esame(...) { static unsigned long last_warn; ... if (time_is_before_jiffies(last_warn + HZ/10); // stampa al più ogni decimo di secondo { WARN(true, "messaggio", ...); last_warn = jiffies; } ... } ---------------------- SCOPERTA DELLA CAUSA DEL RITARDO INSOLITO DI UNA QUALCHE OPERAZIONE Partire dal misurare il tempo di esecuzione del codice che precede tale operazione, all'interno della funzione che contiene tale operazione. Per farlo, strumentare la funzione come segue: funzione_contente_operazione() { u64 start_ns, duration; ... start_ns = ktime_get_ns(); duration = ktime_get_ns() - start_ns; if (duration >= 10000) // o qualsiasi soglia oltre la quale la durata è eccessiva trace_printk("duration: %llu\n", duration); ... } Eseguire con le tracce accese, e controllare la traccia. Il vantaggio dell'uso di trace_printk è che si ottengono le informazioni del tempo di esecuzione combinate con i log del componente sotto esame (ad esempio lo scheduler BFQ). Se invece, per qualsiasi motivo, è necessario che i tempi siano inseriti nel kernel log, sostituire trace_printk con pr_crit. Se il codice che precede l'operazione impiega effettivamente un tempo molto alto, allora, o si riesce a capire perché dal codice stesso, oppure, per ciascuna funzione invocata da tale codice, bisogna misurarne la durata, con la stessa tecnica vista sopra. Ossia, per ciascuna funzione sospetta, si può modificare la strumentazione come segue: funzione_contente_operazione() { ... start_ns = ktime_get_ns(); duration = ktime_get_ns() - start_ns; if (duration >= 10000) // o qualsiasi soglia oltre la quale la durata è eccessiva trace_printk("duration: %llu\n", duration); ... } Questo passo ci permette necessariamente di individuare la funzione colpevole, dopodiché ripetiamo gli stessi passi di sopra, ma dentro la funzione colpevole, fino a trovare ulteriori funzioni colpevoli lungo la catena di invocazioni; e così via fino a trovare necessariamente il punto del codice che causa il ritardo. Se, invece, il codice che precede l'operazione NON impiega un tempo eccessivo, allora la causa del ritardo dell'operazione è che la funzione stessa viene invocata con ritardo. Per trovare la causa del problema in questo caso, bisogna procedere nel verso opposto, ossia risalendo lungo le funzioni invocanti. Per scoprire quali sono tali funzioni, si può utilizzare la tecnica descritta nella precedente nota (o precedere manualmente, tramite, per esempio, lxr). Per ciascuna delle funzioni invocanti, ripetere la procedura di strumentazione, per cercare il pezzo di codice che contiene il ritardo. A differenza del caso in cui si cerca nelle funzioni invocate, può succedere che nessuna delle funzioni invocanti contenga tale ritardo. Per scoprirlo rapidamente, senza dover controllare ciascuna funzione, vi basta strumentare direttamente la funzione all'inizio della catena di invocazioni. Se effettivamente il ritardo non è nella catena di invocazioni, allora la causa del problema è che il processo si è addormentato prima di tale catena di invocazioni, e si è svegliato con molto ritardo. In tale caso, più complesso, bisogna capire la causa dell'addormentamento, e risalire al punto (fuori dalla catena di invocazioni sotto esame) in cui il processo si è addormentato. Ma di questo caso più complesso vi parlerò, forse, in un'altra puntata. ----------------------------- For an actual running kernel, one way to get the config file this is to cat /proc/config.gz | gunzip > running.config or, zcat /proc/config.gz > running.config Then running.config will contain the configuration of the running linux kernel. However this is only possible if your running linux kernel was configured to have /proc/config.gz. The configuration for this is found in General setup [*] Kernel .config support [*] Enable access to .config through /proc/config.gz Se ancora non si vede il file /proc/config.gz, allora sudo modprobe configs