Salta al contenuto principale

ulimit: i limiti di risorse di una sessione

Inviato da tuxsa il

Un servizio sotto carico smette di accettare connessioni e nei registri compare Too many open files. Un programma impazzito riempie la memoria. Un file di prova cresce senza controllo. Linux ha una difesa per tutti questi casi: ogni processo ha dei limiti sulle risorse che può usare. ulimit permette di vederli e di cambiarli.

Cos'è

ulimit ("limite d'uso") non è un programma a sé, ma un comando interno della shell (bash): non si trova in /usr/bin, e type ulimit risponde ulimit is a shell builtin. Il motivo è tecnico e importante: i limiti sono una proprietà del processo, e un processo figlio li eredita dal padre. Per cambiare i limiti di una sessione bisogna quindi cambiarli nella shell che la gestisce, e valgono poi per tutto ciò che si avvia da lì.

Cosa fa

Mostra i limiti attuali (quanti file si possono aprire, quanti processi, quanta memoria, quanto tempo di processore, quanto può essere grande un file) e permette di abbassarli, o di alzarli entro un massimo. Ogni limite ha due valori: un limite morbido (soft, quello in vigore) e un limite rigido (hard, il tetto fino al quale il morbido può essere alzato).

Come si usa

$ ulimit [opzioni] [valore]

Gli esempi sono stati provati in un sandbox; i valori mostrati negli elenchi sono quelli tipici di un server Debian, e sul tuo possono essere diversi.

Esempio 1: vedere tutti i limiti, con -a

$ ulimit -a
core file size              (blocks, -c) 0
data seg size               (kbytes, -d) unlimited
file size                   (blocks, -f) unlimited
pending signals                     (-i) 31523
max locked memory           (kbytes, -l) 8192
max memory size             (kbytes, -m) unlimited
open files                          (-n) 1024
pipe size                (512 bytes, -p) 8
POSIX message queues         (bytes, -q) 819200
stack size                  (kbytes, -s) 8192
cpu time                   (seconds, -t) unlimited
max user processes                  (-u) 31523
virtual memory              (kbytes, -v) unlimited
file locks                          (-x) unlimited

L'elenco mostra, per ogni risorsa, l'unità (blocchi, kibibyte, secondi) e la lettera dell'opzione da usare per leggerlo o cambiarlo. Le voci che interessano più spesso: open files (-n), i file che un processo può avere aperti insieme (compresi i collegamenti di rete: vedi l'articolo su lsof); max user processes (-u), quanti processi può avere un utente; core file size (-c), la dimensione degli "scarichi di memoria" lasciati da un programma che si blocca; file size (-f), la dimensione massima di un file; cpu time (-t), i secondi di processore; virtual memory (-v), la memoria. unlimited significa "nessun limite". Attenzione: un limite non è una garanzia, ma solo un tetto (vedi l'articolo su free per la memoria realmente disponibile).

Esempio 2: limite morbido e rigido

$ ulimit -n
1024
$ ulimit -Sn
1024
$ ulimit -Hn
1048576
$ ulimit -Sa | head -n 3
$ ulimit -Ha | head -n 3

Con -S (soft) si legge il limite morbido, con -H (hard) quello rigido; senza nessuna delle due, in lettura, ulimit mostra il morbido. Il primo è quello che i programmi trovano quando partono, il secondo è il tetto: un utente normale può portare il morbido fino al rigido, ma non oltre. Qui il morbido è 1024 e il rigido più di un milione: significa che puoi alzare il limite dei file aperti molto in alto senza essere amministratore. Le opzioni -Sa e -Ha mostrano l'elenco completo dei due tipi.

Esempio 3: cambiare un limite

$ ulimit -Sn 4096
$ ulimit -n
4096
$ ulimit -Sn 2000000
bash: ulimit: open files: cannot modify limit: Invalid argument
$ ( ulimit -Sn 8192; ./mio-servizio )
$ ulimit -n
4096

Con un valore, ulimit imposta il limite. Il cambiamento vale per la shell attuale e per tutto ciò che si avvia da qui in poi; non per le altre finestre, non per i programmi già in corso. Un valore morbido superiore al rigido è rifiutato (Invalid argument). Racchiudendo il comando tra parentesi tonde si crea una sottoshell temporanea: il limite cambia solo per ./mio-servizio e la shell principale resta com'era (l'ultimo ulimit -n mostra ancora 4096). È il modo più pulito di dare un limite diverso a un singolo comando.

Esempio 4: l'errore "Too many open files"

$ ( ulimit -n 20; for i in $(seq 1 30); do exec {fd}<>/dev/null || break; done )
bash: /dev/null: Too many open files
$ python3 -c "import resource; print(resource.getrlimit(resource.RLIMIT_NOFILE))"
(1024, 1048576)

È l'errore più frequente legato ai limiti: un programma ha raggiunto il numero massimo di file aperti, e ogni nuova apertura (di un file, di una connessione di rete, di un canale) fallisce con Too many open files ([Errno 24] in Python). Il primo esempio lo provoca di proposito con un limite di 20: exec {fd}<>/dev/null apre ripetutamente un file finché il limite non è raggiunto. Succede ai server web e ai database con molte connessioni, e ai programmi che aprono file senza richiuderli. Il secondo comando mostra come un programma Python vede i suoi limiti (morbido e rigido).

Esempio 5: i limiti di un processo già in corso

$ cat /proc/1034/limits
Limit                     Soft Limit           Hard Limit           Units
Max cpu time              unlimited            unlimited            seconds
Max file size             unlimited            unlimited            bytes
Max core file size        0                    unlimited            bytes
Max processes             31523                31523                processes
Max open files            1024                 524288               files
...
$ ls /proc/1034/fd | wc -l
312
$ sudo prlimit --pid 1034 --nofile
RESOURCE DESCRIPTION               SOFT   HARD UNITS
NOFILE   max number of open files  1024 524288 files
$ sudo prlimit --pid 1034 --nofile=65535:65535

ulimit parla solo della tua shell. Per un programma già avviato (come il database con il PID 1034, o nginx) i limiti si leggono in /proc/PID/limits, e il numero di file che ha aperti in questo momento si conta con ls /proc/PID/fd | wc -l (vedi l'articolo su wc): qui 312 su 1024, quindi il programma sta usando un terzo del suo limite. Il comando prlimit (del pacchetto util-linux, come lsblk e mount) legge e cambia i limiti di un processo in corso, senza riavviarlo; per i processi di altri utenti serve sudo. La forma --nofile=morbido:rigido imposta i due valori. Un rimedio d'emergenza: vale finché il processo vive, poi si deve sistemare la configurazione (esempio 8).

Esempio 6: limiti per i programmi che si comportano male

$ ( ulimit -f 100; dd if=/dev/zero of=grande bs=1k count=500 )
File size limit exceeded
$ ls -l grande | awk '{print $5}'
102400
$ ( ulimit -v 100000; python3 -c "x = bytearray(300*1024*1024)" )
MemoryError
$ ( ulimit -t 1; while :; do :; done )
Killed

I limiti sono utili anche per proteggersi da errori. Con -f (qui in blocchi da 1024 byte) un comando non può creare file più grandi del limite: dd si ferma a 102400 byte (100 blocchi) invece di scrivere i 500 richiesti, ricevendo il segnale File size limit exceeded. Con -v (kibibyte) un programma non può usare più memoria virtuale: il programma Python di prova, che ne chiede 300 MiB con un tetto di circa 98 MiB, riceve un errore MemoryError. Con -t (secondi di processore) un ciclo infinito viene ucciso dopo un secondo di lavoro. Sono esempi pensati per essere sicuri: puoi provarli senza rischi, perché ogni limite vale solo dentro le parentesi.

Esempio 7: i file di scarico (core dump)

$ ulimit -c
0
$ ulimit -c unlimited
$ ./programma-che-si-blocca
Segmentation fault (core dumped)
$ coredumpctl list | tail -n 2
$ ulimit -c 0

Quando un programma si blocca per un errore grave, il sistema può salvare un "core dump", una copia della memoria, utile agli sviluppatori per capire il guasto. Normalmente ulimit -c vale 0 (niente scarichi), perché i file possono essere molto grandi e contenere dati riservati, come password. Per diagnosticare un problema si alza a unlimited solo per la sessione di prova, e poi si rimette a 0. Dove vengono scritti dipende dal sistema: sulle installazioni con systemd-coredump si consultano con coredumpctl.

Esempio 8: rendere i limiti permanenti

$ sudo nano /etc/security/limits.d/90-enzo.conf
enzo   soft   nofile   8192
enzo   hard   nofile   65536
*      soft   core     0
$ sudo systemctl edit nginx
[Service]
LimitNOFILE=65535
$ sudo systemctl restart nginx
$ systemctl show nginx -p LimitNOFILE
LimitNOFILE=65535
$ grep 'open files' /proc/$(systemctl show -p MainPID --value nginx)/limits

I limiti cambiati con ulimit spariscono alla chiusura della sessione. Per renderli permanenti ci sono due strade diverse, a seconda di chi deve rispettarli. Per le sessioni degli utenti (l'accesso con ssh, il terminale) si scrive in /etc/security/limits.conf o in un file sotto /etc/security/limits.d/: le colonne sono utente (o * per tutti), tipo (soft o hard), risorsa (nofile per i file aperti, nproc, core…) e valore; valgono dal prossimo accesso, tramite il modulo pam_limits. Per i servizi avviati da systemd (come nginx, mariadb), invece, limits.conf non conta: si usa la direttiva LimitNOFILE= (e simili: LimitNPROC=, LimitCORE=) in un file di personalizzazione, creato con systemctl edit (vedi l'articolo su systemctl) e seguito da un riavvio. Con systemctl show si verifica il valore, e con /proc/PID/limits si controlla che il processo l'abbia davvero ricevuto. I nomi dei file e i valori sono esempi.

Esempio 9: uso negli script

#!/bin/bash
# Alza il limite morbido dei file aperti al massimo consentito
ulimit -Sn "$(ulimit -Hn)"
echo "Limite file aperti: $(ulimit -n)"
exec /usr/local/bin/mio-servizio

# Controllo: quanti file ha aperti un processo rispetto al suo limite?
PID=$1
LIMITE=$(awk '/Max open files/ {print $4}' /proc/"$PID"/limits)
USATI=$(ls /proc/"$PID"/fd | wc -l)
echo "File aperti: $USATI su $LIMITE"

Il primo blocco è uno "script di avvio": porta il limite morbido al valore del rigido (ulimit -Sn "$(ulimit -Hn)") e poi lancia il servizio con exec, che lo sostituisce allo script, ereditando il limite. È un modo semplice di risolvere "Too many open files" per un programma lanciato a mano. Il secondo blocco è uno strumento di controllo: legge il limite dal file in /proc (con awk: vedi l'articolo relativo, $4 è il valore morbido) e lo confronta con i file aperti. L'ho provato sulla shell stessa (5 file aperti su 20000). Programmato con crontab, e con un avviso quando il rapporto supera l'80%, è un buon allarme.

Opzioni utili

  • -a: tutti i limiti; -S / -H: limite morbido / rigido (con -a per l'elenco).
  • -n: file aperti; -u: processi dell'utente; -c: dimensione dei core dump; -f: dimensione massima dei file.
  • -t: secondi di processore; -v: memoria virtuale; -s: dimensione dello stack; -l: memoria bloccata.
  • unlimited: nessun limite; ( ulimit ...; comando ): limite solo per un comando.
  • cat /proc/PID/limits, prlimit --pid PID: limiti di un processo in corso (prlimit li può anche cambiare).
  • /etc/security/limits.conf, /etc/security/limits.d/: limiti permanenti per le sessioni; LimitNOFILE= e simili: per i servizi systemd.
  • sysctl fs.file-max, /proc/sys/fs/file-nr: limiti e uso dei file aperti di tutto il sistema.

Attenzione a

  • Senza -S o -H, scrivendo un valore si cambiano entrambi i limiti: ulimit -n 50 imposta sia il morbido che il rigido a 50, e poi non puoi più rialzarlo (si ottiene Operation not permitted), se non chiudendo la shell. Per abbassare o alzare solo il morbido usa sempre -S: ulimit -Sn 4096.
  • Il rigido non si alza: un utente normale può solo abbassarlo. Per alzarlo serve un accesso privilegiato o la configurazione (esempio 8). Se il morbido è più alto del nuovo rigido, ulimit risponde Invalid argument.
  • Vale per la sessione e i suoi figli, non per il sistema: gli altri accessi, i programmi già avviati e i servizi di systemd non cambiano. Per un servizio in funzione, usa prlimit (temporaneo) o LimitNOFILE= (permanente).
  • limits.conf non vale per i servizi: è la confusione più comune. Modifichi il file, riavvii il servizio, e il limite non cambia: quel file lo leggono solo le sessioni che passano da pam_limits (accesso, su, sudo), non i processi avviati direttamente da systemd.
  • Le unità non sono uniformi: -f è in blocchi (da 1024 byte in bash), -v e -d in kibibyte, -t in secondi, -n e -u sono semplici numeri. Controlla l'unità nell'elenco di ulimit -a prima di scrivere un valore.
  • Un limite troppo basso rompe i programmi in modo oscuro: un tetto di memoria o di processi troppo stretto produce errori strani (Cannot allocate memory, Resource temporarily unavailable) e crash. Prima di applicarlo a un servizio, provalo in una sottoshell.
  • Alzare "a unlimited" non è una buona cura: nasconde una perdita di file o di memoria del programma, che prima o poi riempirà comunque il sistema. Se i file aperti crescono sempre, il difetto è nel programma: controlla con lsof e /proc/PID/fd.
  • Il limite dei processi è per utente: -u conta tutti i processi dell'utente, non quelli della sessione; è una difesa contro i programmi che si moltiplicano senza controllo, e può bloccare i nuovi comandi dello stesso utente (fork: Resource temporarily unavailable). L'amministratore è di solito esentato.
  • Esistono anche limiti di tutto il sistema: il numero massimo di file aperti dal sistema intero (fs.file-max) è un'altra cosa, e si vede con cat /proc/sys/fs/file-nr. I limiti di ulimit sono per processo e restano sotto quello.
  • Nei container i limiti si impostano in altro modo (con Docker, --ulimit nofile=65535:65535) e spesso non sono quelli della macchina ospite.
  • Non sostituisce un controllo vero delle risorse: per limitare con precisione memoria e processore di un servizio, systemd offre MemoryMax= e CPUQuota=, che sono più adatti di ulimit.

Riepilogo

ComandoCosa fa
ulimit -aMostra tutti i limiti della sessione
ulimit -Sn / ulimit -HnLimite morbido / rigido dei file aperti
ulimit -Sn 4096Alza (o abbassa) il limite morbido, solo per questa sessione
( ulimit -Sn 8192; comando )Limite diverso per un solo comando
ulimit -c unlimitedAbilita i core dump (per la diagnosi)
ulimit -f 100File non più grandi di 100 blocchi da 1 KiB
ulimit -t 60Massimo 60 secondi di processore
cat /proc/PID/limitsLimiti di un processo in corso
ls /proc/PID/fd | wc -lFile che il processo ha aperti ora
sudo prlimit --pid PID --nofile=N:NCambia i limiti di un processo in corso
/etc/security/limits.d/*.confLimiti permanenti per le sessioni degli utenti
LimitNOFILE= (systemctl edit)Limite permanente per un servizio systemd