Prima di leggere: due domande sulle lezioni precedenti
Nella prima lezione hai scoperto che /bin è un collegamento simbolico verso
usr/bin. Prima domanda: se qualcuno cancellasse /usr/bin/bash, che cosa
resterebbe di /bin/bash? Seconda domanda: quanto spazio su disco occupa un collegamento
simbolico a un file da quattro megabyte? Rispondi a entrambe prima di andare avanti.
Le due domande sembrano scollegate e misurano la stessa cosa: se hai capito che un collegamento simbolico non contiene il file, ma solo il suo indirizzo. La differenza fra i due tipi di collegamento che Linux offre si gioca tutta su questo punto, e sbagliarla costa un backup inutilizzabile o uno spazio disco che non torna mai.
Il problema: due nomi per lo stesso file, in due modi diversi
Ti serve che lo stesso contenuto sia raggiungibile da due percorsi. Il caso classico è una configurazione condivisa fra due servizi, oppure la versione attiva di un programma puntata da un nome stabile. Linux offre due meccanismi che sembrano fare la stessa cosa, hanno comandi quasi identici, e si rompono in modi opposti. Chi li confonde scopre la differenza nel momento peggiore: durante un ripristino.
Nella seconda metà della lezione c'è il problema gemello: una volta che i file sono tanti, trovarne uno non è più un gesto ma una ricerca, e il comando che la fa ha una sintassi che non somiglia a nessun altro comando Unix.
Il nome non è il file
Su un filesystem come ext4 il file vero è una struttura chiamata inode, che
contiene i permessi, le date, e i puntatori ai blocchi di dati. Il nome che vedi in una cartella è
soltanto una voce che associa una stringa a un numero di inode. Quindi un file non ha un nome: ha
uno o più nomi, e l'inode conta quanti sono.
Un collegamento fisico, che si crea con ln, è un secondo nome per lo stesso inode. Un
collegamento simbolico, che si crea con ln -s, è un file a sé, con un inode proprio, il
cui contenuto è il percorso di destinazione scritto come testo. Da questa differenza di struttura
discendono tutti i comportamenti, e si vedono in una sola riga di output.
$ echo "originale" > dato.txt
$ ln dato.txt hard.txt
$ ln -s dato.txt soft.txt
$ ls -li dato.txt hard.txt soft.txt
81179682 -rw-rw-r-- 2 federicocalo federicocalo 10 Oct 30 17:43 dato.txt
81179682 -rw-rw-r-- 2 federicocalo federicocalo 10 Oct 30 17:43 hard.txt
81179683 lrwxrwxrwx 1 federicocalo federicocalo 8 Oct 30 17:43 soft.txt -> dato.txt
La prima colonna è il numero di inode, che l'opzione -i aggiunge. dato.txt
e hard.txt hanno lo stesso numero: sono lo stesso file. soft.txt ha un
numero diverso. La colonna successiva ai permessi è il conteggio dei nomi: 2 per i
primi due, 1 per il collegamento simbolico. E la dimensione di soft.txt è
8, che sono esattamente le lettere di dato.txt: il contenuto del
collegamento è la stringa del percorso.
Il confronto che decide quale usare
La prova che separa i due meccanismi è cancellare l'originale. Se il collegamento fisico fosse una scorciatoia, morirebbe con l'originale. Non muore, perché non esiste un originale: esistono due nomi pari fra loro, e cancellarne uno abbassa il conteggio da due a uno.
$ rm dato.txt
$ cat hard.txt
originale
$ cat soft.txt
cat: soft.txt: No such file or directory
$ ls -l soft.txt
lrwxrwxrwx 1 federicocalo federicocalo 8 Oct 30 17:43 soft.txt -> dato.txt
Il collegamento fisico ha il contenuto. Il collegamento simbolico esiste ancora come file, lo vedi in
ls, ma punta a un nome che non c'è più: è un collegamento rotto, e si trova con
find . -xtype l, che sulla mia prova ha risposto ./soft.txt.
Le differenze che contano sono quattro, e vanno sapute prima di scegliere. Un collegamento fisico non può attraversare filesystem diversi, perché il numero di inode ha senso solo dentro un filesystem, mentre il simbolico può puntare dove vuole, anche a un percorso che non esiste ancora. Un collegamento fisico non può puntare a una cartella, il simbolico sì. Il simbolico si rompe se la destinazione si sposta, il fisico no. E solo il simbolico ti dice, guardandolo, dov'è il contenuto.
L'errore di classificazione plausibile riguarda lo spazio su disco, e si vede con
du. Due nomi dello stesso inode occupano i blocchi una volta sola, e
du se ne accorge se gli passi entrambi i nomi nella stessa invocazione, ma non se glieli
passi separatamente:
$ du -ch reale.bin copia-hard.bin
4,0M reale.bin
4,0M total
$ du -h reale.bin
4,0M reale.bin
$ du -h copia-hard.bin
4,0M copia-hard.bin
Nella prima invocazione copia-hard.bin non compare affatto e il totale è quattro
megabyte, perché du riconosce l'inode già contato. Nelle due invocazioni separate
ciascun nome dichiara quattro megabyte, e sommandole a mano otterresti otto. La virgola decimale
nell'output dipende dalla lingua del mio sistema: con LC_ALL=C davanti al comando
ottieni il punto, e questo conta quando l'output lo legge uno script.
Esempio svolto: trovare i log vecchi senza cancellare quelli nuovi
Il problema: una cartella di log è cresciuta e devi individuare i file più vecchi di una settimana per archiviarli. Il requisito implicito, quello che fa danno se lo ignori, è non toccare i file recenti e non toccare le cartelle.
L'informazione disponibile: find ha un criterio sull'età in giorni,
-mtime, e un criterio sul tipo, -type. Il ragionamento sulla sintassi è il
passaggio in cui si sbaglia: find non prende opzioni come gli altri comandi, prende
un'espressione di condizioni valutate in ordine, e il risultato della valutazione decide se il file
entra nell'elenco. Quindi l'ordine delle condizioni non è decorativo.
$ find . -name '*.log'
./log/vecchio.log
./log/app.log
./log/zero.log
$ find . -type f -name '*.log' -mtime +7
./log/vecchio.log
$ find . -empty
./vuota
./log/zero.log
La prima ricerca trova tutti i log. La seconda aggiunge due condizioni e isola l'unico file più
vecchio di sette giorni. La terza mostra una cosa che vale la pena notare: -empty trova
anche la cartella vuota, perché senza -type f il criterio si applica a
qualunque voce.
La decisione che prendo: le virgolette attorno a '*.log' ci stanno sempre.
L'alternativa, scriverlo nudo, funziona finché la cartella da cui lanci il comando non contiene
file che corrispondono al modello: in quel caso la shell espande l'asterisco prima
che find lo veda, e find riceve dei nomi concreti al posto del modello. Il
risultato è una ricerca che cerca la cosa sbagliata, senza nessun messaggio di errore.
La verifica: cambia cartella e rilancia lo stesso comando. Se il risultato dipende da dove ti trovi
quando non dovrebbe, le virgolette mancano. L'errore che avrei potuto commettere e che ho visto fare
è passare direttamente a -delete per fare in fretta: -delete implica
-depth e cambia l'ordine di visita, quindi la stessa espressione può selezionare un
insieme diverso. Prima si elenca, si guarda l'elenco, e solo poi si cancella.
Il caso negativo: tre comandi che sembrano sinonimi
Qualcuno ti dice che ip sta in /usr/bin. Tu verifichi e trovi due risposte
diverse dai due comandi che dovrebbero rispondere alla stessa domanda. Prova a dire quale delle due
è utile e perché, prima di leggere il seguito.
$ which ip
/usr/sbin/ip
$ whereis ip
ip: /usr/bin/ip /usr/sbin/ip /usr/share/man/man8/ip.8.gz /usr/share/man/man7/ip.7.gz
Le due risposte non si contraddicono: rispondono a due domande diverse. which dice quale
file verrebbe eseguito adesso, con il tuo PATH, e la risposta è
/usr/sbin/ip. whereis cerca in posizioni standard e nelle pagine di
manuale, e trova tutte le copie esistenti, incluse quelle che il tuo PATH non raggiungerebbe. Se
stai diagnosticando «perché viene eseguito il programma sbagliato», la risposta utile è la prima.
Il terzo comando è quello che nessuno usa e che spesso serve: type -a, che è interno
alla shell e vede anche ciò che un file non è. Su questo sistema type -a cd risponde
che cd è un comando interno di shell, cioè non esiste nessun file da trovare, e per
questo which cd non è la domanda giusta. Il messaggio compare nella lingua del sistema,
quindi sulla tua macchina potrebbe essere in inglese.
Sul quarto strumento che il programma del modulo nomina, locate, una precisazione
misurata: su questa installazione di Ubuntu 26.04 non è presente. Il comando
command -v locate non restituisce nulla, e apt-cache policy plocate
dichiara Installato: (nessuno) con candidato 1.1.23-1ubuntu3. Quindi
locate va installato prima di usarlo, e la sua velocità viene da un indice costruito
periodicamente: risponde in fretta e risponde sul passato, mentre find è lento e
risponde sul presente.
Esercizio 1, con un passaggio mancante
Devi trovare, sotto la tua cartella personale, tutti i file più grandi di dieci megabyte modificati negli ultimi due giorni, e stampare per ciascuno il percorso e la dimensione. Due condizioni sono scritte, la terza e il formato di stampa li scrivi tu.
$ find "$HOME" -type f -size +10M ??????? -printf '???'
Esercizio 2, autonomo: un collegamento che non si rompe
Hai due versioni di un programma in /opt/app-1.2 e /opt/app-1.3, e vuoi un
percorso stabile /opt/app-attuale da cui i servizi leggono, che tu possa spostare da
una versione all'altra in un gesto e riportare indietro se qualcosa va male. Decidi quale dei due
tipi di collegamento usare, costruiscilo in una cartella di prova, e poi provoca il
guasto: rinomina la cartella di destinazione e osserva che cosa succede al percorso stabile.
Criterio di successo: sai dire quale dei due tipi hai scelto, perché l'altro non andava bene, e che
cosa vede un servizio nell'istante in cui la destinazione non c'è.
Soluzioni commentate
Esercizio 1. La condizione mancante è -mtime -2 e il formato è
-printf '%p %s\n'. Il ragionamento sul segno è la parte che conta: con
-mtime il meno significa «meno di», quindi -2 sono i file modificati negli
ultimi due giorni, mentre +2 sono quelli più vecchi di due giorni. Scrivere
-mtime 2 senza segno significa «esattamente nel giorno numero due», che è un insieme
quasi sempre diverso da quello che intendevi e quasi sempre vuoto.
Sul formato, %p è il percorso e %s la dimensione in byte. Perché non
-exec ls -l {} \;: funzionerebbe, ma avvia un processo per ogni file trovato, e su
diecimila file la differenza si misura in minuti. -printf stampa dal processo che sta
già girando. Perché non -ls: stampa un formato fisso che include dati che non ti
servono, e se l'output lo legge uno script le colonne in più sono un problema.
Una cautela sulla portabilità: -printf è un'estensione di GNU findutils, e su questa
macchina ho verificato la versione con find --version, che risponde
find (GNU findutils) 4.10.0. Su un sistema BSD o in un container minimale
-printf può non esserci, e in quel caso la strada portabile è
-exec stat -c '%n %s' {} +, dove il più finale raggruppa i file in poche invocazioni
invece di una per file.
Esercizio 2. Serve un collegamento simbolico, e le ragioni per cui
il fisico non va sono due, entrambe dirimenti. La prima: un collegamento fisico non può puntare a una
cartella, quindi il requisito è tecnicamente impossibile. La seconda, che conta anche se lavorassi su
un singolo file: un collegamento fisico punta all'inode, non al nome, quindi non lo puoi
«ripuntare» altrove. Il gesto che ti serve è ln -sfn /opt/app-1.3 /opt/app-attuale, dove
-f sostituisce il collegamento esistente e -n evita che il collegamento
venga creato dentro la cartella già puntata, che è l'errore più comune di questo comando.
Sul guasto provocato: nell'istante in cui la destinazione non c'è, il servizio non riceve un
collegamento vuoto, riceve No such file or directory sul percorso completo, come hai
visto nella prova con cat soft.txt. È un errore buono, perché è immediato e nomina il
file. La cosa da cui diffidare è il contrario: se usassi una copia invece di un collegamento, il
servizio continuerebbe a leggere la versione vecchia senza nessun segnale, e scopriresti il problema
quando un comportamento atteso non si manifesta.
Domande per fissare il concetto
Perché un collegamento fisico non può attraversare due filesystem diversi, se parti dal fatto che è una voce di cartella che punta a un numero di inode? Che cosa succede allo spazio su disco se cancelli un file che ha tre nomi, e in quale momento i blocchi vengono liberati? E se un backup copiasse i collegamenti fisici come file distinti, quanto spazio occuperebbe il ripristino rispetto all'originale?
Che cosa adesso sai fare
Sai distinguere un collegamento fisico da uno simbolico leggendo due colonne di ls -li,
il numero di inode e il conteggio dei nomi, e sai prevedere quale dei due sopravvive alla
cancellazione della destinazione. Sai costruire un percorso stabile verso una versione, e sai
spostarlo con un comando solo. Sai scrivere una ricerca con find mettendo le virgolette
dove servono, e sai che il segno davanti a -mtime cambia l'insieme selezionato.
La verifica concreta: cerca i collegamenti rotti sotto la tua cartella personale con
find "$HOME" -xtype l. Ognuno di quelli è un pezzo di sistema che punta a qualcosa che
non c'è più, e adesso sai leggere che cosa si è spostato.