blob: 91732cdf808a3a4e7f16b15f3a1e8716ffbb2369 [file] [log] [blame]
.. include:: ../disclaimer-ita.rst
:Original: Documentation/process/botching-up-ioctls.rst
==========================================
(Come evitare di) Raffazzonare delle ioctl
==========================================
Preso da: https://blog.ffwll.ch/2013/11/botching-up-ioctls.html
Scritto da : Daniel Vetter, Copyright © 2013 Intel Corporation
Una cosa che gli sviluppatori del sottosistema grafico del kernel Linux hanno
imparato negli ultimi anni è l'inutilità di cercare di creare un'interfaccia
unificata per gestire la memoria e le unità esecutive di diverse GPU. Dunque,
oggigiorno ogni driver ha il suo insieme di ioctl per allocare memoria ed
inviare dei programmi alla GPU. Il che è va bene dato che non c'è più un insano
sistema che finge di essere generico, ma al suo posto ci sono interfacce
dedicate. Ma al tempo stesso è più facile incasinare le cose.
Per evitare di ripetere gli stessi errori ho preso nota delle lezioni imparate
mentre raffazzonavo il driver drm/i915. La maggior parte di queste lezioni si
focalizzano sui tecnicismi e non sulla visione d'insieme, come le discussioni
riguardo al modo migliore per implementare una ioctl per inviare compiti alla
GPU. Probabilmente, ogni sviluppatore di driver per GPU dovrebbe imparare queste
lezioni in autonomia.
Prerequisiti
------------
Prima i prerequisiti. Seguite i seguenti suggerimenti se non volete fallire in
partenza e ritrovarvi ad aggiungere un livello di compatibilità a 32-bit.
* Usate solamente interi a lunghezza fissa. Per evitare i conflitti coi tipi
definiti nello spazio utente, il kernel definisce alcuni tipi speciali, come:
``__u32``, ``__s64``. Usateli.
* Allineate tutto alla lunghezza naturale delle piattaforma in uso e riempite
esplicitamente i vuoti. Non necessariamente le piattaforme a 32-bit allineano
i valori a 64-bit rispettandone l'allineamento, ma le piattaforme a 64-bit lo
fanno. Dunque, per farlo correttamente in entrambe i casi dobbiamo sempre
riempire i vuoti.
* Se una struttura dati contiene valori a 64-bit, allora fate si che la sua
dimensione sia allineata a 64-bit, altrimenti la sua dimensione varierà su
sistemi a 32-bit e 64-bit. Avere una dimensione differente causa problemi
quando si passano vettori di strutture dati al kernel, o quando il kernel
effettua verifiche sulla dimensione (per esempio il sistema drm lo fa).
* I puntatori sono di tipo ``__u64``, con un *cast* da/a ``uintptr_t`` da lato
spazio utente e da/a ``void __user *`` nello spazio kernel. Sforzatevi il più
possibile per non ritardare la conversione, o peggio maneggiare ``__u64`` nel
vostro codice perché questo riduce le verifiche che strumenti come sparse
possono effettuare. La macro u64_to_user_ptr() può essere usata nel kernel
per evitare avvisi riguardo interi e puntatori di dimensioni differenti.
Le Basi
-------
Con la gioia d'aver evitato un livello di compatibilità, possiamo ora dare uno
sguardo alle basi. Trascurare questi punti renderà difficile la gestione della
compatibilità all'indietro ed in avanti. E dato che sbagliare al primo colpo è
garantito, dovrete rivisitare il codice o estenderlo per ogni interfaccia.
* Abbiate un modo chiaro per capire dallo spazio utente se una nuova ioctl, o
l'estensione di una esistente, sia supportata dal kernel in esecuzione. Se non
potete fidarvi del fatto che un vecchio kernel possa rifiutare correttamente
un nuovo *flag*, modalità, o ioctl, (probabilmente perché avevate raffazzonato
qualcosa nel passato) allora dovrete implementare nel driver un meccanismo per
notificare quali funzionalità sono supportate, o in alternativa un numero di
versione.
* Abbiate un piano per estendere le ioctl con nuovi *flag* o campi alla fine di
una struttura dati. Il sistema drm verifica la dimensione di ogni ioctl in
arrivo, ed estende con zeri ogni incongruenza fra kernel e spazio utente.
Questo aiuta, ma non è una soluzione completa dato che uno spazio utente nuovo
su un kernel vecchio non noterebbe che i campi nuovi alla fine della struttura
vengono ignorati. Dunque, anche questo avrà bisogno di essere notificato dal
driver allo spazio utente.
* Verificate tutti i campi e *flag* inutilizzati ed i riempimenti siano a 0,
altrimenti rifiutare la ioctl. Se non lo fate il vostro bel piano per
estendere le ioctl andrà a rotoli dato che qualcuno userà delle ioctl con
strutture dati con valori casuali dallo stack nei campi inutilizzati. Il che
si traduce nell'avere questi campi nell'ABI, e la cui unica utilità sarà
quella di contenere spazzatura. Per questo dovrete esplicitamente riempire i
vuoti di tutte le vostre strutture dati, anche se non le userete in un
vettore. Il riempimento fatto dal compilatore potrebbe contenere valori
casuali.
* Abbiate un semplice codice di test per ognuno dei casi sopracitati.
Divertirsi coi percorsi d'errore
--------------------------------
Oggigiorno non ci sono più scuse rimaste per permettere ai driver drm di essere
sfruttati per diventare root. Questo significa che dobbiamo avere una completa
validazione degli input e gestire in modo robusto i percorsi - tanto le GPU
moriranno comunque nel più strano dei casi particolari:
* Le ioctl devono verificare l'overflow dei vettori. Inoltre, per i valori
interi si devono verificare *overflow*, *underflow*, e *clamping*. Il
classico esempio è l'inserimento direttamente nell'hardware di valori di
posizionamento di un'immagine *sprite* quando l'hardware supporta giusto 12
bit, o qualcosa del genere. Tutto funzionerà finché qualche strano *display
server* non decide di preoccuparsi lui stesso del *clamping* e il cursore
farà il giro dello schermo.
* Avere un test semplice per ogni possibile fallimento della vostra ioctl.
Verificate che il codice di errore rispetti le aspettative. Ed infine,
assicuratevi che verifichiate un solo percorso sbagliato per ogni sotto-test
inviando comunque dati corretti. Senza questo, verifiche precedenti
potrebbero rigettare la ioctl troppo presto, impedendo l'esecuzione del
codice che si voleva effettivamente verificare, rischiando quindi di
mascherare bachi e regressioni.
* Fate si che tutte le vostre ioctl siano rieseguibili. Prima di tutto X adora
i segnali; secondo questo vi permetterà di verificare il 90% dei percorsi
d'errore interrompendo i vostri test con dei segnali. Grazie all'amore di X
per i segnali, otterrete gratuitamente un eccellente copertura di base per
tutti i vostri percorsi d'errore. Inoltre, siate consistenti sul modo di
gestire la riesecuzione delle ioctl - per esempio, drm ha una piccola
funzione di supporto `drmIoctl` nella sua librerie in spazio utente. Il
driver i915 l'abbozza con l'ioctl `set_tiling`, ed ora siamo inchiodati per
sempre con una semantica arcana sia nel kernel che nello spazio utente.
* Se non potete rendere un pezzo di codice rieseguibile, almeno rendete
possibile la sua interruzione. Le GPU moriranno e i vostri utenti non vi
apprezzeranno affatto se tenete in ostaggio il loro scatolotto (mediante un
processo X insopprimibile). Se anche recuperare lo stato è troppo complicato,
allora implementate una scadenza oppure come ultima spiaggia una rete di
sicurezza per rilevare situazioni di stallo quando l'hardware da di matto.
* Preparate dei test riguardo ai casi particolarmente estremi nel codice di
recupero del sistema - è troppo facile create uno stallo fra il vostro codice
anti-stallo e un processo scrittore.
Tempi, attese e mancate scadenze
--------------------------------
Le GPU fanno quasi tutto in modo asincrono, dunque dobbiamo regolare le
operazioni ed attendere quelle in sospeso. Questo è davvero difficile; al
momento nessuna delle ioctl supportante dal driver drm/i915 riesce a farlo
perfettamente, il che significa che qui ci sono ancora una valanga di lezioni da
apprendere.
* Per fare riferimento al tempo usate sempre ``CLOCK_MONOTONIC``. Oggigiorno
questo è quello che viene usato di base da alsa, drm, e v4l. Tuttavia,
lasciate allo spazio utente la possibilità di capire quali *timestamp*
derivano da domini temporali diversi come il vostro orologio di sistema
(fornito dal kernel) oppure un contatore hardware indipendente da qualche
parte. Gli orologi divergeranno, ma con questa informazione gli strumenti di
analisi delle prestazioni possono compensare il problema. Se il vostro spazio
utente può ottenere i valori grezzi degli orologi, allora considerate di
esporre anch'essi.
* Per descrivere il tempo, usate ``__s64`` per i secondi e ``__u64`` per i
nanosecondi. Non è il modo migliore per specificare il tempo, ma è
praticamente uno standard.
* Verificate che gli input di valori temporali siano normalizzati, e se non lo
sono scartateli. Fate attenzione perché la struttura dati ``struct ktime``
del kernel usa interi con segni sia per i secondi che per i nanosecondi.
* Per le scadenze (*timeout*) usate valori temporali assoluti. Se siete dei
bravi ragazzi e avete reso la vostra ioctl rieseguibile, allora i tempi
relativi tendono ad essere troppo grossolani e a causa degli arrotondamenti
potrebbero estendere in modo indefinito i tempi di attesa ad ogni
riesecuzione. Particolarmente vero se il vostro orologio di riferimento è
qualcosa di molto lento come il contatore di *frame*. Con la giacca da
avvocato delle specifiche diremmo che questo non è un baco perché tutte le
scadenze potrebbero essere estese - ma sicuramente gli utenti vi odieranno
quando le animazioni singhiozzano.
* Considerate l'idea di eliminare tutte le ioctl sincrone con scadenze, e di
sostituirle con una versione asincrona il cui stato può essere consultato
attraverso il descrittore di file mediante ``poll``. Questo approccio si
sposa meglio in un applicazione guidata dagli eventi.
* Sviluppate dei test per i casi estremi, specialmente verificate che i valori
di ritorno per gli eventi già completati, le attese terminate con successo, e
le attese scadute abbiano senso e servano ai vostri scopi.
Non perdere risorse
-------------------
Nel suo piccolo il driver drm implementa un sistema operativo specializzato per
certe GPU. Questo significa che il driver deve esporre verso lo spazio
utente tonnellate di agganci per accedere ad oggetti e altre risorse. Farlo
correttamente porterà con se alcune insidie:
* Collegate sempre la vita di una risorsa creata dinamicamente, a quella del
descrittore di file. Considerate una mappatura 1:1 se la vostra risorsa
dev'essere condivisa fra processi - passarsi descrittori di file sul socket
unix semplifica la gestione anche per lo spazio utente.
* Dev'esserci sempre Il supporto ``O_CLOEXEC``.
* Assicuratevi di avere abbastanza isolamento fra utenti diversi. Di base
impostate uno spazio dei nomi riservato per ogni descrittore di file, il che
forzerà ogni condivisione ad essere esplicita. Usate uno spazio più globale
per dispositivo solo se gli oggetti sono effettivamente unici per quel
dispositivo. Un controesempio viene dall'interfaccia drm modeset, dove
oggetti specifici di dispositivo, come i connettori, condividono uno spazio
dei nomi con oggetti per il *framebuffer*, ma questi non sono per niente
condivisi. Uno spazio separato, privato di base, per i *framebuffer* sarebbe
stato meglio.
* Pensate all'identificazione univoca degli agganci verso lo spazio utente. Per
esempio, per la maggior parte dei driver drm, si considera fallace la doppia
sottomissione di un oggetto allo stesso comando ioctl. Ma per evitarlo, se
gli oggetti sono condivisibili, lo spazio utente ha bisogno di sapere se il
driver ha importato un oggetto da un altro processo. Non l'ho ancora provato,
ma considerate l'idea di usare il numero di inode come identificatore per i
descrittori di file condivisi - che poi è come si distinguono i veri file.
Sfortunatamente, questo richiederebbe lo sviluppo di un vero e proprio
filesystem virtuale nel kernel.
Ultimo, ma non meno importante
------------------------------
Non tutti i problemi si risolvono con una nuova ioctl:
* Pensateci su due o tre volte prima di implementare un'interfaccia privata per
un driver. Ovviamente è molto più veloce seguire questa via piuttosto che
buttarsi in lunghe discussioni alla ricerca di una soluzione più generica. Ed
a volte un'interfaccia privata è quello che serve per sviluppare un nuovo
concetto. Ma alla fine, una volta che c'è un'interfaccia generica a
disposizione finirete per mantenere due interfacce. Per sempre.
* Considerate interfacce alternative alle ioctl. Gli attributi sysfs sono molto
meglio per impostazioni che sono specifiche di un dispositivo, o per
sotto-oggetti con una vita piuttosto statica (come le uscite dei connettori in
drm con tutti gli attributi per la sovrascrittura delle rilevazioni). O magari
solo il vostro sistema di test ha bisogno di una certa interfaccia, e allora
debugfs (che non ha un'interfaccia stabile) sarà la soluzione migliore.
Per concludere. Questo gioco consiste nel fare le cose giuste fin da subito,
dato che se il vostro driver diventa popolare e la piattaforma hardware longeva
finirete per mantenere le vostre ioctl per sempre. Potrete tentare di deprecare
alcune orribili ioctl, ma ci vorranno anni per riuscirci effettivamente. E
ancora, altri anni prima che sparisca l'ultimo utente capace di lamentarsi per
una regressione.