Salta al contenuto principale

lsof: quali file sono aperti, e da chi

Inviato da tuxsa il

Non riesci a smontare una chiavetta ("target is busy"). Il disco è pieno, ma du non trova i file che lo riempiono. Una porta risulta occupata e non sai da quale programma. Tre problemi diversi con la stessa domanda: chi sta usando questo file? lsof risponde a questa domanda, e in Linux "file" vuol dire quasi tutto.

Cos'è

lsof (list open files, "elenca i file aperti") è un programma storico dell'amministrazione dei sistemi Unix. Su Debian e Ubuntu va spesso installato a parte (sudo apt install lsof, vedi l'articolo su apt). Il suo punto di forza deriva da un principio di Unix: "tutto è un file". Per un processo sono file aperti non solo i documenti e i registri, ma anche le cartelle in cui si trova, le librerie caricate, i terminali, le connessioni di rete e le porte in ascolto. lsof li elenca tutti, in una sola tabella.

Cosa fa

Elenca i file aperti dai processi in esecuzione, con il processo e l'utente che li tengono aperti. Si può chiedere "tutto quello che ha aperto questo processo" oppure, al contrario, "chi ha aperto questo file, questa cartella o questa porta". È il complemento di ps (che dice quali processi esistono) e di ss (che dice quali connessioni ci sono): lsof li collega ai file.

Come si usa

$ sudo lsof [opzioni] [file o cartella]

Senza sudo si vedono solo i processi dell'utente; per vedere quelli degli altri servono i privilegi di amministratore (vedi l'articolo su sudo). Senza opzioni, lsof elenca tutti i file aperti dal sistema, migliaia di righe: nella pratica si usa sempre con un filtro. Gli esempi usano il server web01 degli articoli precedenti.

Esempio 1: cosa ha aperto un processo

$ sudo lsof -p 925
COMMAND PID USER  FD TYPE DEVICE SIZE/OFF   NODE NAME
nginx   925 root cwd DIR   254,0     4096      2 /
nginx   925 root txt REG   254,0  1318416 262411 /usr/sbin/nginx
nginx   925 root  2w REG   254,0     8650 262977 /var/log/nginx/error.log
nginx   925 root  6u IPv4  15033      0t0    TCP *:80 (LISTEN)
nginx   925 root  8w REG   254,0  4521338 262978 /var/log/nginx/access.log

Con -p si indica il numero di un processo (il PID, che si trova con ps: vedi l'articolo su ps e kill). L'elenco reale è più lungo (qui ne ho mostrato solo alcune righe). Le colonne:

  • COMMAND, PID, USER: il programma, il suo numero e l'utente con cui gira;
  • FD (file descriptor): come il file è aperto. cwd è la cartella corrente, txt il programma stesso, mem le librerie caricate in memoria; un numero come 2 o 8 è un file aperto dal programma, seguito da r (lettura), w (scrittura) o u (entrambe). I numeri 0, 1 e 2 sono gli ingressi e le uscite standard (vedi gli articoli su echo e tee);
  • TYPE: REG file normale, DIR cartella, CHR dispositivo (come /dev/null), IPv4 e IPv6 connessioni di rete, unix comunicazioni interne tra programmi, FIFO canali (pipe);
  • DEVICE, SIZE/OFF, NODE: il disco, la dimensione (o la posizione di lettura) e il numero del file nel file system: servono di rado;
  • NAME: il nome del file, o l'indirizzo e la porta per le connessioni.

Esempio 2: chi sta usando questo file o questa cartella?

$ sudo lsof /var/log/nginx/access.log
COMMAND PID     USER FD TYPE DEVICE SIZE/OFF   NODE NAME
nginx   925     root  8w REG  254,0  4521338 262978 /var/log/nginx/access.log
nginx   931 www-data  8w REG  254,0  4521338 262978 /var/log/nginx/access.log
$ sudo lsof /mnt/usb
COMMAND  PID USER  FD TYPE DEVICE SIZE/OFF NODE NAME
bash    4102 enzo cwd DIR    8,33     4096    2 /mnt/usb
$ sudo lsof +D /var/www/html

Se passi il nome di un file, lsof mostra chi lo tiene aperto. Se è un file system montato (come /mnt/usb), mostra chi lo sta usando: è la risposta al problema "target is busy" dell'articolo su mount: qui una shell di enzo ha la cartella come cartella corrente (cwd). Con +D si cerca ricorsivamente in tutta una cartella: utile ma lento, perché esamina tutti i file sottostanti.

Esempio 3: filtrare per utente, programma o processo

$ sudo lsof -u www-data
$ sudo lsof -u ^root
$ sudo lsof -c nginx
$ sudo lsof -a -u enzo -c bash
$ sudo lsof -p 925,931

-u filtra per utente (con ^ davanti si escludono: tutti tranne root), -c per il nome del programma (anche una parte iniziale), -p per i numeri di processo (più di uno, separati da virgole). Attenzione: più filtri si sommano (lsof -u enzo -c nginx mostra i file di enzo oppure quelli di nginx). Per richiedere che valgano tutti insieme si aggiunge -a (and), come nel quarto comando: solo i file di enzo aperti da bash.

Esempio 4: le porte e le connessioni di rete

$ sudo lsof -i :80
$ sudo lsof -iTCP -sTCP:LISTEN -P -n
COMMAND   PID  USER FD TYPE DEVICE SIZE/OFF NODE NAME
sshd      812  root  3u IPv4  14520      0t0  TCP *:22 (LISTEN)
nginx     925  root  6u IPv4  15033      0t0  TCP *:80 (LISTEN)
nginx     925  root  7u IPv4  15034      0t0  TCP *:443 (LISTEN)
mariadbd 1034 mysql 21u IPv4  16410      0t0  TCP 127.0.0.1:3306 (LISTEN)

L'opzione -i (internet) seleziona le connessioni di rete. Da sola le mostra tutte; con :80 solo quelle sulla porta 80; con TCP o UDP solo quel protocollo; con @192.168.1.20 solo quelle verso quell'indirizzo. -sTCP:LISTEN limita ai programmi in ascolto, cioè alle porte aperte. Il comando risponde a "quale programma ha aperto questa porta?", ed è utile quando un servizio non parte perché la porta è già occupata. Le opzioni -P e -n evitano di tradurre i numeri in nomi (la porta 80 in http, l'indirizzo in un nome di computer): l'output è più chiaro e il comando molto più veloce. Lo stesso lavoro lo fa ss -tlnp (vedi l'articolo su ss); lsof è utile perché usa lo stesso formato per file e connessioni.

Esempio 5: le connessioni attive, riassunte

$ sudo lsof -nP -i TCP:443 -sTCP:ESTABLISHED | awk 'NR>1 {print $1, $2, $9, $10}'
nginx 931 192.168.1.10:443->203.0.113.45:51822 (ESTABLISHED)
nginx 931 192.168.1.10:443->198.51.100.7:40310 (ESTABLISHED)

Le connessioni in corso (ESTABLISHED) mostrano l'indirizzo locale e quello remoto, separati da una freccia. Qui i due indirizzi remoti sono quelli di due visitatori della porta 443 (i numeri sono inventati). Con awk (vedi l'articolo relativo) si tengono solo i campi che servono: NR>1 salta l'intestazione. Utile per scoprire da dove arrivano i visitatori o per riconoscere un indirizzo sospetto.

Esempio 6: il disco è pieno, ma i file non si trovano

$ df -h /var
File system     Dim. Usati Dispon. Uso% Montato su
/dev/sda2       226G  219G    1,2G  99% /
$ sudo du -sh /var/log
1,1G    /var/log
$ sudo lsof +L1
COMMAND  PID USER     FD TYPE DEVICE   SIZE/OFF NLINK   NODE NAME
php-fpm 1120 www-data 5w REG   254,0 3221225472     0 262900 /var/log/app.log (deleted)
$ sudo truncate -s 0 /proc/1120/fd/5

Un caso classico: df dice che il disco è al 99%, ma du non trova abbastanza file. La spiegazione è che qualcuno ha cancellato un file con rm mentre un programma lo teneva ancora aperto: il nome scompare, ma lo spazio resta occupato finché il programma non lo chiude. lsof +L1 elenca i file con meno di un collegamento (NLINK a 0), cioè cancellati ma ancora aperti, e li riconosci dalla scritta (deleted) in NAME. Qui un registro da 3 GB, tenuto aperto da php-fpm. Per liberare lo spazio senza interrompere il servizio si svuota il file tramite il suo "percorso nascosto" sotto /proc (/proc/PID/fd/NUMERO, con il PID e il numero di FD letti dalla tabella); oppure si riavvia il servizio (vedi l'articolo su systemctl). Capita spesso quando si cancellano a mano i registri di un servizio acceso, invece di svuotarli o di lasciar fare a logrotate. L'output è illustrativo, ma il meccanismo l'ho provato.

Esempio 7: i numeri dei processi per gli script

$ sudo lsof -t -i :8080
4410
$ sudo lsof -ti :8080 | xargs -r kill
$ if lsof -i :8080 -sTCP:LISTEN -t >/dev/null; then
>     echo "porta occupata"
> fi

L'opzione -t (terse, "conciso") stampa soltanto i numeri dei processi, uno per riga: è fatta per comporre comandi. Il secondo comando usa xargs (vedi l'articolo su tee e xargs) per terminare con kill (vedi l'articolo su ps e kill) il programma che occupa la porta 8080; l'opzione -r di xargs evita di eseguire kill senza argomenti se non c'è niente. Prima di usarlo, guarda sempre chi è con la forma completa. Il terzo comando usa il codice di uscita di lsof, che è 0 se ha trovato qualcosa e 1 se non ha trovato niente: perfetto per un if.

Esempio 8: ripetere e osservare

$ sudo lsof -r 2 -i :80
$ sudo lsof -p $$
$ sudo lsof -n -w | awk 'NR>1 {print $1}' | sort | uniq -c | sort -nr | head -n 3
    272 claude
    252 environme
    143 rclone-fi

Con -r 2 (repeat) lsof ripete la richiesta ogni 2 secondi fino a Ctrl+C: comodo per vedere comparire o sparire una connessione. $$ è il numero della shell in cui stai lavorando: lsof -p $$ mostra i file aperti da lei. L'ultimo comando conta quante righe ha ciascun programma (con awk, sort e uniq: vedi gli articoli relativi) per scoprire chi tiene più file aperti; i nomi e i numeri dell'esempio vengono dal sistema usato per provare il comando, e sul tuo server saranno altri. Con -w si nascondono gli avvisi.

Esempio 9: troppi file aperti

$ ulimit -n
1024
$ grep 'open files' /proc/1034/limits
Max open files            1048576              1048576              files
$ sudo lsof -p 1034 | wc -l
312

Ogni programma ha un limite al numero di file che può aprire insieme: ulimit -n mostra quello della tua shell, e /proc/PID/limits quello di un processo in corso. Quando un programma lo supera, compare l'errore Too many open files: un servizio sotto carico smette di accettare connessioni, e il motivo non è evidente. Con lsof -p PID | wc -l (wc: vedi l'articolo relativo) si conta quanti file ha aperti: la riga di intestazione e le librerie contano come file, per cui il numero è approssimato. Se il conteggio sale nel tempo senza scendere (un programma che apre file e non li chiude), il difetto è nel programma; se è semplicemente vicino al limite, si alza il limite con la configurazione del servizio (per systemd con LimitNOFILE).

Esempio 10: uso negli script

#!/bin/bash
PORTA=8080

PID=$(lsof -t -i :"$PORTA" -sTCP:LISTEN | head -n 1)
if [ -n "$PID" ]; then
    NOME=$(ps -p "$PID" -o comm=)
    echo "La porta $PORTA è già usata da $NOME (PID $PID)" >&2
    exit 1
fi

echo "Porta $PORTA libera: avvio il servizio"

Prima di avviare un servizio, lo script controlla se la porta è già occupata e, se lo è, dice da chi (con ps -p PID -o comm= si ottiene il nome del programma, senza intestazione), invece di lasciare che il servizio fallisca con un messaggio oscuro. L'ho provato con un server finto sulla porta, e lsof restituisce nessuna riga quando la porta è libera.

Opzioni utili

  • -p PID: file aperti da un processo; -c nome: da un programma; -u utente: da un utente (^utente per escluderlo).
  • file o cartella: chi la usa; +D cartella: tutta la cartella, ricorsivamente (lento).
  • -i: connessioni di rete; -i :porta, -iTCP, -i @indirizzo, -sTCP:LISTEN: filtri per porta, protocollo, indirizzo e stato.
  • -a: i filtri valgono tutti insieme (altrimenti basta uno); -t: solo i numeri dei processi; -r N: ripete ogni N secondi.
  • -n, -P: nessuna traduzione di indirizzi e porte (più veloce e più chiaro); -w: nasconde gli avvisi.
  • +L1: file cancellati ma ancora aperti; -d cwd: solo le cartelle correnti; -F: formato per i programmi.
  • fuser -vm: chi usa un file system (più semplice); ss -tlnp: porte in ascolto; ps: informazioni sul processo.

Attenzione a

  • Serve sudo per vedere tutto: da utente normale lsof mostra solo i processi dell'utente, e un "non trovato" può significare solo "non puoi vederlo". Anche con sudo alcune righe di processi speciali possono riportare errori di permesso: sono normali, e si nascondono con -w.
  • Senza filtri è inutilizzabile: sono migliaia di righe, e sono dominate dalle librerie caricate (mem). Parti sempre da una domanda precisa (un file, una porta, un processo).
  • I filtri multipli si sommano: è l'errore più comune. lsof -u enzo -i mostra i file di enzo e tutte le connessioni di rete di chiunque. Aggiungi -a per l'intersezione.
  • Usa -n e -P per le connessioni di rete: senza, lsof interroga il DNS per ogni indirizzo e può metterci minuti, o restare fermo. Con -n -P è immediato.
  • +D è lento: esamina tutti i file sotto una cartella e, su alberi grandi, può impiegare molto tempo e carico. Per una sola cartella è preferibile indicarla senza +D.
  • Non bloccarti sui dischi di rete: se un montaggio di rete (NFS) non risponde, lsof può restare bloccato aspettando. In quel caso, -b evita le chiamate che potrebbero bloccarsi (con meno dettagli) e -w nasconde gli avvisi.
  • Prima di fermare un processo, verifica chi è: lsof -ti :porta | xargs kill è comodo, ma chiude qualunque programma tenga la porta, anche quello sbagliato. Controlla con lsof -i :porta o con ps prima di agire (vedi l'articolo su kill).
  • Un file cancellato occupa ancora spazio: è la causa tipica di un disco "pieno senza file". Svuotare il file tramite /proc/PID/fd/N libera lo spazio ma il programma continua a scriverci dentro: non è una soluzione al problema, solo un rimedio d'emergenza. La soluzione vera è la rotazione dei registri.
  • Dentro un container si vedono solo i processi del container (e per vedere quelli dell'ospite servono permessi speciali). Controlla da quale parte stai guardando.
  • Non è un monitor in tempo reale: è una fotografia istantanea. Un file aperto per pochi millisecondi può non comparire. Per osservare una situazione che cambia, usa -r, oppure strumenti dedicati come strace.
  • Su altri sistemi cambia: lsof esiste anche su macOS e FreeBSD, ma alcune opzioni e le colonne possono differire. In uno script portabile, prova prima il comando.

Riepilogo

ComandoCosa fa
sudo lsof -p 925File aperti da un processo
sudo lsof fileChi tiene aperto un file
sudo lsof /mnt/usbChi sta usando un file system montato
sudo lsof +D cartellaFile aperti in tutta una cartella (lento)
sudo lsof -u utenteFile aperti da un utente
sudo lsof -c nginxFile aperti da un programma
sudo lsof -a -u enzo -c bashUnisce i filtri (tutti insieme)
sudo lsof -i :80Chi usa la porta 80
sudo lsof -iTCP -sTCP:LISTEN -P -nProgrammi in ascolto, senza traduzioni
sudo lsof +L1File cancellati ma ancora aperti
sudo lsof -t -i :8080Solo il numero del processo (per gli script)
sudo lsof -r 2 -i :80Ripete ogni 2 secondi