요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. 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.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
GPU별 ioctl에서 얻은 ABI 교훈
1-28첫 줄은 이탈리아어 번역 문서의 공통 면책 고지를 포함하고, `:Original:` metadata는 같은 v6.18.37의 `Documentation/process/botching-up-ioctls.rst`를 정확히 가리킵니다. Daniel Vetter가 drm/i915에서 얻은 교훈을 바탕으로 GPU별 전용 ioctl이 늘어날수록 ABI 설계를 더 엄격하게 해야 한다고 설명합니다.
Daniel Vetter가 drm/i915 개발 경험을 바탕으로 작성한 2013년 문서다. 서로 완전히 다른 GPU의 execution unit과 memory를 하나의 통합 interface로 관리하려는 시도는 실용적이지 않았다. 그래서 driver마다 memory allocation과 GPU work submission용 ioctl을 제공하게 되었다.
한 번만 쓰이는 가짜 generic interface를 피할 수 있지만 driver별 ABI를 잘못 설계할 가능성도 늘어난다. 이 문서는 command submission의 큰 설계보다 반복해서 실수하기 쉬운 기술 세부 사항을 다룬다.
.. 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.
32-bit compat를 피하기 위한 layout 전제
29-58- Fixed-size integer만 사용한다. Userspace typedef 충돌을 피하도록 __u32, __s64 같은 kernel ABI type을 쓴다.
- 모든 field를 natural size에 align하고 explicit padding을 넣는다. 32-bit platform은 64-bit value를 64-bit boundary에 align하지 않을 수 있지만 64-bit platform은 그렇게 하므로 layout을 명시해야 한다.
- 64-bit type이 있는 structure 전체 크기를 64 bit 배수로 padding한다. 그렇지 않으면 32-bit와 64-bit에서 sizeof가 달라져 structure array 전달과 drm core의 size check가 깨진다.
- Pointer는 __u64로 표현한다. Userspace에서는 uintptr_t와 변환하고 kernel에서는 void __user *와 즉시 변환한다. Raw __u64 pointer를 내부까지 끌고 가지 않아야 sparse 검사를 살릴 수 있다. Kernel에서는 u64_to_user_ptr()로 size warning을 피할 수 있다.
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.
Forward와 backward compatibility
59-95- Userspace가 새 ioctl 또는 extension 지원 여부를 분명히 알아낼 방법을 제공한다. 옛 kernel이 새 flag와 mode를 확실히 거부하지 않는다면 driver feature flag나 revision number가 필요하다.
- Structure 끝에 새 flag나 field를 추가할 확장 계획을 마련한다. drm core의 size check와 zero-extension은 도움이 되지만 새 userspace가 옛 kernel에서 추가 field가 무시된 사실을 알지 못하므로 feature flag가 여전히 필요하다.
- Unused field, flag, padding이 모두 0인지 검사하고 아니면 ioctl을 거부한다. Random stack garbage를 받아들이면 그 값도 ABI 일부가 되어 field를 미래에 사용할 수 없다. Compiler가 만든 implicit padding에도 garbage가 들어갈 수 있으므로 explicit padding이 필요하다.
- 위의 모든 동작에 간단한 testcase를 둔다.
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.
Input validation과 error path test
96-119- Array size overflow와 일반 integer over/underflow, clamping 문제를 모두 검사한다. 예를 들어 12-bit hardware sprite position에 unclamped display-server 값을 직접 쓰면 cursor가 screen을 wrap할 수 있다.
- 각 input validation failure에 별도 testcase를 만들고 예상 error code를 확인한다.
- Subtest 하나에서는 오직 한 error path만 검사한다. 나머지 input은 완전히 valid하게 만들어 앞선 check가 먼저 거부하며 목표 path의 bug와 regression을 가리지 않게 한다.
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.
Restartable ioctl과 killable hang
120-142모든 ioctl을 restartable하게 만든다. X server는 signal을 많이 사용하므로 test suite에 계속 signal을 보내는 것만으로 error handling path 대부분을 시험할 수 있다. Restart semantic은 userspace library helper까지 일관되게 유지한다. i915 set_tiling ioctl의 불일치처럼 한번 공개한 이상한 semantic은 kernel과 userspace에서 영구히 유지해야 할 수 있다.
Restartable하게 만들 수 없는 path라도 stuck task는 kill 가능해야 한다. GPU failure 때문에 unkillable X process가 system 전체를 멈추게 해서는 안 된다. State recovery가 어렵다면 timeout이나 hangcheck를 마지막 safety net으로 둔다.
Hangcheck code와 waiter 사이 deadlock처럼 까다로운 recovery corner case에도 testcase를 작성한다.
DRM userspace library의 작은 `drmIoctl` helper처럼 ioctl 재시작 규칙을 공통 경로에 모아 일관되게 적용해야 합니다. i915의 `set_tiling` ioctl은 이 규칙을 어겨 kernel과 userspace가 특이한 semantics를 계속 유지하게 된 사례입니다.
* 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.
Clock domain과 time 표현
143-169GPU operation은 대부분 asynchronous라 시간 측정과 outstanding work 대기가 필요하지만 어렵다.
- Reference time은 항상 CLOCK_MONOTONIC을 사용한다. ALSA, DRM, V4L도 이를 기본으로 쓴다.
- Kernel system clock과 독립 hardware counter처럼 다른 clock domain에서 나온 timestamp는 userspace에 명시한다. Clock mismatch를 없앨 수 없어도 performance tool이 보정할 수 있다.
- Command stream performance counter sampling처럼 raw clock value에 접근할 수 있다면 노출을 고려한다.
- Time은 __s64 seconds와 __u64 nanoseconds 조합으로 표현한다.
- Input time이 normalize되었는지 검사하고 아니면 거부한다. Native struct ktime은 seconds와 nanoseconds 모두 signed integer이므로 주의한다.
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.
Absolute timeout과 asynchronous event
170-189Timeout은 absolute time으로 표현한다. Restartable ioctl에서 relative timeout을 쓰면 restart마다 rounding되어 전체 wait가 기한 없이 늘어날 수 있다. Display frame counter처럼 느린 reference clock이면 animation stutter로 나타날 수 있다.
Timeout이 있는 synchronous wait ioctl 대신 poll 가능한 file descriptor로 asynchronous event를 전달하는 방식도 고려한다. Event-driven application main loop에 더 자연스럽다.
이미 완료된 event, 성공한 wait, timeout된 wait의 return value가 모두 일관되고 목적에 맞는지 corner-case testcase로 확인한다.
* 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.
Resource lifetime과 namespace
190-224완전한 DRM driver는 특정 GPU를 위한 작은 운영체제처럼 많은 object와 resource handle을 userspace에 노출한다.
- 동적으로 생성한 resource lifetime을 항상 file descriptor lifetime에 연결한다. Process 사이 공유가 필요하면 1:1 mapping과 Unix-domain socket의 fd passing을 고려해 userspace lifetime 관리도 단순화한다.
- 항상 O_CLOEXEC를 지원한다.
- Client 사이를 충분히 격리한다. 기본은 sharing을 명시적으로 수행하게 하는 private per-fd namespace다. Object가 진정 device-unique할 때만 global per-device namespace를 사용한다.
- DRM modeset의 connector 같은 per-device object와 대부분 공유하지 않는 framebuffer object가 같은 namespace를 쓰는 것은 반례다. Framebuffer는 기본 private인 별도 namespace가 더 적합했다.
- Userspace handle의 uniqueness 요구를 설계한다. Shareable object를 다른 process에서 import했을 때 이미 본 object인지 구분해야 한다. Shared fd의 inode number를 unique identifier로 쓰는 방안이 있지만 kernel에 full virtual filesystem 구현이 필요하다.
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.
모든 문제에 새 ioctl이 필요한 것은 아니다
225-243Driver-private interface는 generic solution 논의보다 빨리 만들 수 있고 새 개념을 먼저 시험할 때 필요할 수도 있다. 하지만 이후 generic interface가 생기면 두 interface를 기한 없이 함께 유지해야 한다.
Per-device setting이나 lifetime이 비교적 고정된 child object에는 sysfs attribute가 더 적합할 수 있다. Test suite만 필요한 interface라면 stable ABI를 약속하지 않는 debugfs가 낫다.
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.
첫 ABI를 사실상 영구히 유지해야 한다
244-249Driver가 널리 쓰이고 hardware platform이 오래 유지되면 공개한 ioctl ABI는 사실상 영구히 유지해야 한다. 새 hardware generation에서 나쁜 ioctl을 deprecate해도 수년이 걸리고, 마지막 사용자가 regression을 보고할 수 없게 될 때까지 또 수년이 필요하다. 그래서 첫 설계를 올바르게 만드는 것이 핵심이다.
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.
요약·해설
botching-up-ioctls.rst:1-249GPU driver의 userspace ABI를 처음부터 장기간 유지할 수 있게 설계하는 실전 점검표입니다. Fixed-size integer, explicit padding과 pointer 변환부터 feature flag, zero 검증, overflow와 error-path test까지 다룹니다.
Restartable ioctl, absolute timeout과 pollable asynchronous event, file descriptor에 묶인 resource lifetime, private namespace 및 sysfs·debugfs 대안까지 연결해 ioctl 하나가 갖는 호환성과 운영 책임을 설명합니다.