Cosa succede davvero tra premi il tasto di accensione e la tua shell risponde al prompt. Boot, moduli, scheduler, memoria virtuale, VFS, namespace e cgroup: lo strato che tutti usano e quasi nessuno guarda.
"Linux è solo il kernel" — frase vera, ripetuta da almeno un pedante in ogni thread su Internet dal 1991, e comunque il punto di partenza giusto per capire cosa stai davvero eseguendo.
Non è il "sistema operativo" nel senso in cui lo intende chi installa Ubuntu. È il programma che parla direttamente con l'hardware e decide chi può fare cosa.
Immagina un palazzo di uffici (l'hardware: CPU, RAM, dischi, rete). Gli inquilini (i tuoi programmi: Firefox, bash, nginx) non possono girare liberamente per i piani tecnici, staccare i cavi o riconfigurare l'ascensore. Tutto passa dalla portineria: se vuoi un pacco (leggere un file), usare il telefono (aprire un socket) o cambiare stanza (allocare memoria), fai una richiesta al portiere. Il portiere è il kernel: l'unico che ha le chiavi di tutto, e ogni richiesta che riceve si chiama system call. Gli inquilini vivono in user space, il portiere lavora in kernel space, e il confine tra i due è sorvegliato dalla CPU stessa (i "ring" o "exception level" del processore).
Ogni volta che un programma stampa a schermo, legge un file o apre una connessione di rete, in un modo o nell'altro finisce per chiedere il permesso al kernel.
| Modello | Idea | Esempi | Pro / contro |
|---|---|---|---|
| Monolitico | Tutto (scheduler, filesystem, driver, rete) gira in un unico spazio kernel, un'unica "immagine". | Linux, i BSD | Veloce (nessun context switch tra sottosistemi), ma un bug in un driver può far cadere tutto il kernel |
| Microkernel | Il kernel fa solo lo stretto indispensabile (IPC, scheduling base); driver e filesystem girano come processi user space separati. | Minix 3, QNX, seL4 | Un driver che crash non porta giù il sistema, ma ogni operazione passa da più hop di messaggi → più overhead |
| Ibrido | Kernel piccolo "puro" ma con moduli che girano comunque in kernel space per performance. | Windows NT, macOS/XNU | Compromesso: prova a prendere il meglio di entrambi |
.ko, cap. 4) invece di essere sempre presenti. Non è un microkernel — niente processi separati per i driver — ma non è nemmeno il monolite rigido degli anni '70. È per questo che la stessa immagine di kernel gira su un Raspberry Pi e su un mainframe con 512 core: i moduli giusti si caricano solo dove servono.
La CPU stessa impone il confine: su x86 esistono 4 "ring" (0=massimo privilegio, 3=minimo), Linux usa solo ring 0 (kernel) e ring 3 (user, ARM chiama gli stessi concetti EL1/EL0). Un processo user space non può leggere memoria di un altro processo, non può parlare direttamente all'hardware, non può disabilitare gli interrupt. Se ci prova, la CPU genera un'eccezione e il kernel decide cosa fare (di solito: SIGSEGV, "Segmentation fault").
Senza isolamento, un bug (o un malware) in un browser potrebbe leggere la password che stai digitando in un terminale accanto, o scrivere direttamente sul disco bypassando i permessi del filesystem. Il kernel space è l'unico posto dove "tutto è permesso": per questo ogni riga di codice che ci gira dentro è potenzialmente un bug di sicurezza globale, e ogni driver che carichi (anche closed-source, es. NVIDIA) gira con la stessa fiducia del resto del kernel.
Tra "premi il pulsante" e "vedi il prompt" succedono cinque o sei passaggi distinti, ognuno con un colpevole diverso quando qualcosa non parte.
switch_root verso il disco realePer montare il vero filesystem di root, il kernel potrebbe avere bisogno di driver che lui stesso non ha ancora caricato: il modulo per il controller NVMe, quello per l'array RAID software, quello per sbloccare un volume LUKS cifrato con una passphrase. È un problema di uovo e gallina: serve un driver per montare il disco, ma il driver di solito sta... sul disco. La soluzione è l'initramfs: un mini-filesystem compresso, caricato tutto in RAM dal bootloader insieme al kernel, che contiene giusto i moduli e gli strumenti (mdadm, lvm2, cryptsetup) necessari per assemblare la root reale. Una volta trovata e montata, il kernel fa switch_root e getta via l'initramfs: da quel momento vivi sul disco vero.
# Il ring buffer del kernel: tutti i messaggi dal boot in poi
dmesg -T | less
dmesg -T | grep -iE "error|fail|warn"
# Stessa cosa via systemd, filtrata per il boot corrente (-b) o uno precedente (-b -1)
journalctl -k -b
journalctl -k -b -1 # il boot PRIMA di questo (utile dopo un crash)
# Quanto ci ha messo ogni fase, e chi ha rallentato l'avvio
systemd-analyze
systemd-analyze blame
systemd-analyze critical-chain
dmesg legge lo stesso ring buffer del kernel che journalctl -k mostra via systemd-journald, con la differenza che il journal lo persiste su disco tra un boot e l'altro (se Storage=persistent in journald.conf). Se il sistema è crashato ieri notte, journalctl -k -b -1 è spesso l'unico modo per vedere cosa è successo.
/boot/vmlinuz-$(uname -r) — l'immagine del kernel, compressa/boot/initrd.img-$(uname -r) — l'initramfs, generato con mkinitramfs/dracut/boot/grub/grub.cfg — la config generata di GRUB (non toccarla a mano, si rigenera)/etc/default/grub — qui si personalizzano i parametri kernel, poi update-grub/proc/cmdline — i parametri effettivamente passati al kernel in questo bootcat /proc/cmdline
# esempi che troverai spesso in GRUB_CMDLINE_LINUX_DEFAULT
quiet splash # boot silenzioso con logo
nomodeset # disabilita KMS, utile con GPU rotte
systemd.unit=rescue.target # boot in modalità rescue
init=/bin/bash # bypassa init: recovery estremo
root=, o un aggiornamento del kernel che non ha rigenerato l'initramfs. Il rimedio quasi sempre è avviare da un kernel precedente nel menu GRUB e rigenerare l'initramfs (update-initramfs -u su Debian/Ubuntu, dracut -f su Fedora/RHEL).
Il kernel è un progetto open source enorme, sviluppato su git, con un ritmo di rilascio incredibilmente regolare per essere gestito da migliaia di contributor sparsi nel mondo.
Il kernel usa versioning X.Y (es. 6.11), senza un vero significato semantico stile semver: un bump di X non implica breaking change. Ogni ciclo dura circa 9-10 settimane: due settimane di "merge window" (si accettano nuove feature), poi 7-8 "release candidate" (rc1...rc7) solo per stabilizzare e correggere bug, poi il rilascio finale.
| Ramo | Cosa contiene | Quanto dura |
|---|---|---|
| Mainline | L'ultima versione, sviluppo attivo di Linus Torvalds | ~10 settimane per release |
| Stable | Backport di fix e sicurezza su una release mainline | Fino alla prossima mainline |
| Longterm (LTS) | Stessa idea ma mantenuta per anni, usata dalle distro enterprise | Tipicamente 2-6 anni (es. 6.1, 6.6, 6.12 LTS) |
| Distro kernel | Un LTS "congelato" + patch backportate a mano dalla distro | Il ciclo di vita della distro (es. Ubuntu HWE, RHEL) |
uname -r mostra spesso un numero tipo 6.8.0-45-generic: 6.8.0 è la base LTS, 45 è il numero della build della distro.
| Directory | Cosa c'è dentro |
|---|---|
arch/ | Codice specifico per CPU: arch/x86, arch/arm64, arch/riscv... |
kernel/ | Il cuore: scheduler, gestione processi, timer, moduli |
mm/ | Memory management: paging, allocatori, swap, OOM killer |
fs/ | VFS e i filesystem: fs/ext4, fs/xfs, fs/proc... |
net/ | Lo stack di rete: net/ipv4, net/netfilter, socket |
drivers/ | La fetta più grande in assoluto: driver per ogni hardware esistente |
include/ | Header condivisi, incluse le definizioni delle syscall |
security/ | I Linux Security Module: SELinux, AppArmor, Smack (cap. 12) |
Documentation/ | Documentazione in testo semplice, sorprendentemente buona |
drivers/ da sola è oltre la metà delle righe di codice dell'intero kernel. La ragione per cui l'immagine che scarichi è "solo" qualche decina di MB compressi è che al build entra solo il codice per il tuo hardware: il resto sta lì per compilare kernel diversi da immagini diverse.
git clone --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
# Parti dalla config del kernel che gia' gira (comodo, eredita i driver giusti)
cp /boot/config-$(uname -r) .config
make olddefconfig # aggiorna la config alle nuove opzioni
make menuconfig # o nconfig: interfaccia a menu per toccare le opzioni
# Build vera e propria: usa tutti i core disponibili
make -j$(nproc)
# Installa moduli e immagine (poi serve rigenerare initramfs e grub.cfg)
sudo make modules_install
sudo make install
Il motivo per cui non serve ricompilare il kernel ogni volta che colleghi una webcam nuova: i Loadable Kernel Module.
Un modulo (.ko, "kernel object") è codice compilato apposta per essere caricato e scaricato dal kernel a runtime, senza riavviare. È come installare un'app sul telefono invece di dover flashare tutto il firmware: il driver per la tua webcam USB non serve finché non la colleghi, quindi resta su disco come modulo e si carica solo quando udev se ne accorge.
# Cosa e' caricato ADESSO (letto da /proc/modules)
lsmod
# Info su un modulo: autore, parametri, dipendenze, licenza
modinfo nvidia
modinfo e1000e
# Carica un modulo (modprobe risolve le dipendenze da solo, meglio di insmod)
sudo modprobe vfat
sudo insmod /lib/modules/$(uname -r)/kernel/fs/fat/vfat.ko # basso livello, no dipendenze automatiche
# Scaricalo (fallisce se qualcosa lo sta ancora usando)
sudo modprobe -r vfat
sudo rmmod vfat
# Ricostruisci l'indice delle dipendenze dopo aver aggiunto moduli
sudo depmod -a
modprobe è quasi sempre lo strumento giusto: legge /lib/modules/$(uname -r)/modules.dep (generato da depmod) e carica in automatico anche le dipendenze. insmod è il livello sotto, usato principalmente da modprobe stesso o per test mirati.
udev carica i moduli in automatico quando rileva hardware nuovo (in base a vendor/device ID)/etc/modules-load.d/*.conf — moduli da caricare sempre al boot/etc/modprobe.d/blacklist.conf — moduli da non caricare mai (es. driver conflittuali, come nouveau vs nvidia)/etc/modprobe.d/*.conf — opzioni da passare a un modulo al caricamentoAlcuni moduli (tipicamente driver proprietari come quello NVIDIA) non sono sotto licenza GPL: caricarli marca il kernel come tainted ("contaminato"). Non è un errore fatale, ma i manutentori del kernel possono rifiutarsi di aiutarti a debuggare un crash se il kernel è tainted — ragionevole: non hanno il sorgente del modulo che potrebbe essere il colpevole.
cat /proc/sys/kernel/tainted # 0 = pulito
dmesg | grep -i taint
Come il kernel decide, migliaia di volte al secondo, quale dei tanti processi in coda ottiene la CPU adesso.
Ogni processo (e ogni thread) è rappresentato nel kernel da una struttura dati C chiamata task_struct: PID, stato, priorità, spazio di indirizzamento, file aperti, credenziali, e molto altro. Un thread per il kernel Linux è semplicemente un task_struct che condivide lo spazio di memoria con altri (creato con clone() e i flag giusti, es. CLONE_VM): non esiste una distinzione "processo vs thread" a basso livello come in altri sistemi, esiste solo "quanto condividono due task tra loro".
| Stato (ps/top) | Sigla | Significato |
|---|---|---|
| Running / Runnable | R | Sta girando sulla CPU o è pronto e in coda |
| Sleeping (interruptible) | S | In attesa di un evento (I/O, timer); può essere svegliato da un segnale |
| Uninterruptible sleep | D | In attesa di I/O che non può essere interrotta — se troppi processi restano qui, spesso è un disco/NFS malato |
| Stopped | T | Fermato da SIGSTOP o da un debugger |
| Zombie | Z | Terminato ma il genitore non ha ancora letto il suo exit status con wait() |
Z è già morto: non consuma CPU né memoria, resta solo la sua entry nella process table finché il genitore non fa wait(). kill -9 su uno zombie non fa nulla (non c'è più nessun processo da uccidere). Se ne hai a centinaia, il colpevole è il genitore che non raccoglie mai i figli: va riavviato o corretto lui.
Dal 2007 al 2023 Linux ha usato il Completely Fair Scheduler: modella il tempo CPU come una risorsa da dividere "equamente" usando un red-black tree ordinato per "virtual runtime" (chi ha girato meno, va prima). Dal kernel 6.6 lo scheduler di default è EEVDF (Earliest Eligible Virtual Deadline First): stessa filosofia di equità, ma modella meglio la latenza per i task interattivi, dando priorità a chi "ha diritto" di girare prima secondo una scadenza virtuale.
Il nice value (-20 a +19) è un suggerimento di priorità per lo scheduling "normale" (CFS/EEVDF): più basso = più CPU relativa. Sopra a tutto questo esistono le classi real-time (SCHED_FIFO, SCHED_RR): un task RT gira sempre prima di qualsiasi task normale, a costo di poter "affamare" il resto del sistema se scritto male — per questo servono privilegi speciali per usarle.
# Vista con priorita' e nice value
ps -eo pid,ppid,ni,pri,stat,comm --sort=-pri | head
# Avvia un processo con priorita' bassa (buono citizen, non rallenta gli altri)
nice -n 15 ./backup-notturno.sh
# Cambia il nice di un processo gia' in corsa
sudo renice -n -5 -p 4821
# Real-time scheduling (richiede CAP_SYS_NICE): usalo solo se sai cosa fai
sudo chrt -f -p 50 4821 # SCHED_FIFO, priorita' 50
chrt -p 4821 # mostra la policy attuale
# Dettagli grezzi di un processo, letti da procfs
cat /proc/4821/status | grep -E "State|Threads"
Ogni processo crede di avere tutta la RAM per sé. È una bugia elaborata, e il kernel è quello che la mantiene credibile.
Ogni processo vede uno spazio di indirizzi tutto suo, tipicamente da 0 a qualche exabyte teorico, come se fosse solo. In realtà quello spazio è virtuale: la MMU (Memory Management Unit, hardware nella CPU) traduce ogni indirizzo virtuale in un indirizzo fisico reale usando delle tabelle delle pagine che il kernel mantiene per ogni processo. Due processi possono usare lo stesso indirizzo virtuale 0x400000 senza mai pestarsi i piedi, perché puntano a pagine fisiche diverse. È anche così che l'isolamento tra processi è possibile: nessuno può nemmeno "vedere" la memoria fisica di un altro, semplicemente perché non ha le tabelle per tradurla.
La memoria è divisa in pagine (tipicamente 4 KiB, o "huge page" da 2 MiB/1 GiB per ridurre l'overhead di traduzione su carichi pesanti). Ogni pagina virtuale mappa una pagina fisica, oppure è marcata "non presente": se un processo ci accede, scatta un page fault e il kernel decide cosa fare (caricarla da disco, allocarla on-demand, o terminare il processo se l'accesso è illegale).
Quando la RAM fisica scarseggia, il kernel può spostare pagine usate raramente su disco (swap), liberando RAM per chi ne ha bisogno adesso. Su hardware moderno con tanta RAM lo swap serve più come "valvola di sicurezza" che come uso quotidiano; su sistemi con poca RAM (o desktop con hibernation) resta fondamentale. Il parametro vm.swappiness (0-200) regola quanto aggressivamente il kernel preferisce swappare rispetto a liberare cache.
Linux di default fa overcommit della memoria: permette di allocare più memoria virtuale di quanta RAM+swap esista fisicamente, scommettendo che non tutti i processi useranno davvero tutto quello che hanno chiesto. Quando la scommessa va male e la memoria finisce per davvero, entra in scena l'OOM killer (Out-Of-Memory killer): sceglie un processo "colpevole" (in base a memoria usata, tempo di vita, e uno score regolabile) e lo termina con SIGKILL per salvare il resto del sistema.
# Cerca "Out of memory: Killed process" nei log
dmesg | grep -i "killed process"
journalctl -k | grep -i oom
# oom_score: piu' alto = piu' probabile candidato alla morte (0-1000)
cat /proc/4821/oom_score
# oom_score_adj: -1000 = mai ucciso, +1000 = prima scelta
echo -500 | sudo tee /proc/4821/oom_score_adj
dmesg.
free -h # totale, usata, cache/buffer, disponibile
vmstat 1 5 # snapshot ogni secondo, 5 volte: si/so = swap in/out
cat /proc/meminfo | head -20 # il dettaglio grezzo che free riassume
sysctl vm.swappiness # quanto e' propenso a swappare (0-200)
free -h è quella che conta davvero, non "free": Linux usa la RAM libera come cache del filesystem in modo aggressivo (buona cosa!), quindi "free" bassa non significa "stai per finire la memoria". "Available" già considera quanta cache può essere liberata all'istante se serve.
Come lo stesso comando read() funziona identico su un file ext4, un socket di rete e uno pseudo-file in /proc.
Il Virtual File System è uno strato di astrazione tra le syscall generiche (open(), read(), write(), close()) e i filesystem reali che implementano quelle operazioni ognuno a modo suo. Come una presa multipla universale che accetta la spina di qualsiasi paese: il programma chiama sempre read(fd, buf, size), e il VFS instrada la chiamata verso ext4, XFS, NFS, o un file virtuale in /proc a seconda di dove sta quel file descriptor. L'applicazione non sa (e non le importa) quale filesystem c'è sotto.
È il motivo per cui un file può avere più nomi (hard link): sono dentry diverse che puntano allo stesso inode. Cancellare un nome (unlink(), quello che fa rm) decrementa solo un contatore di riferimenti nell'inode; i dati spariscono davvero solo quando l'ultimo link sparisce e nessun processo tiene ancora il file aperto — per questo su Linux puoi cancellare un file di log mentre un servizio ci sta ancora scrivendo, senza errori, e lo spazio si libera solo quando il servizio lo chiude o riavvia.
| Filesystem virtuale | Dove monta | Cosa espone |
|---|---|---|
procfs | /proc | Processi e stato del kernel come file (cap. 11) |
sysfs | /sys | Il device model: hardware, driver, moduli (cap. 8) |
tmpfs | /tmp, /run | Filesystem che vive solo in RAM, sparisce al reboot |
devtmpfs | /dev | Nodi dei device, popolati automaticamente dal kernel + udev |
overlayfs | usato da Docker | Unisce più directory in una vista sola (layer immutabili + uno scrivibile) |
cgroupfs | /sys/fs/cgroup | Controllo risorse per gruppi di processi (cap. 10) |
Come un pezzo di hardware collegato diventa un file in /dev che qualsiasi programma può usare con open() e read().
Flusso di byte sequenziale: seriali, tastiere, /dev/null, /dev/urandom
Accesso ad blocchi indicizzati: dischi, partizioni, /dev/sda
Niente file in /dev: gestiti via socket, es. eth0, wlan0
Ogni device char o block ha un numero major (identifica il driver) e minor (identifica l'istanza specifica). Quando apri /dev/sda1, il kernel guarda major/minor associati a quel nodo, trova il driver giusto tramite major, e passa il minor al driver per sapere "quale" disco/partizione specifica.
ls -l /dev/sda /dev/null
# brw-rw---- 1 root disk 8, 0 ... /dev/sda ← major 8, minor 0
# crw-rw-rw- 1 root root 1, 3 ... /dev/null ← major 1, minor 3
cat /proc/devices # tutti i major registrati, char e block
Il kernel notifica gli eventi hardware (collegato/scollegato) via uevent; udevd li ascolta e crea/rimuove i nodi in /dev con nomi persistenti e simbolici, oltre a caricare i moduli giusti e far girare regole custom (permessi, alias).
udevadm info --query=all --name=/dev/sda
udevadm monitor # eventi live: collega una chiavetta
lsblk -o NAME,MAJ:MIN,MODEL
/sys/devices rappresenta la topologia fisica reale del sistema (bus PCI, USB, piattaforma), con /sys/class e /sys/block come "viste" più comode organizzate per tipo. Sono link simbolici che puntano tutti alla stessa struttura interna del kernel: il device model unificato introdotto con il kernel 2.6.
ls /sys/class/net/ # interfacce di rete
cat /sys/block/sda/queue/rotational # 0=SSD, 1=HDD
lspci -k # device PCI + driver in uso
Dal socket che apre il tuo programma fino al pacchetto che esce dalla scheda di rete, passando per i punti in cui firewall e NAT si infilano nel percorso.
Un'applicazione non "parla TCP" direttamente: chiama socket(), bind(), listen(), accept(), send()/recv(), tutte syscall che il VFS tratta quasi come file (i socket hanno persino un file descriptor). Sotto, il kernel gestisce l'intero stack: segmentazione TCP, routing IP, ARP, il driver della scheda fisica.
Il framework netfilter inserisce dei "punti di aggancio" (hook) lungo il percorso che un pacchetto fa dentro il kernel, dove le regole di firewall/NAT possono ispezionare, modificare o scartare il traffico. Sono la base su cui sono costruiti sia il vecchio iptables che il moderno nftables (la guida Networking copre in dettaglio come scriverle).
# Socket in ascolto/connessi, con processo
ss -tulpn
# Tabella di routing del kernel
ip route show
# Parametri kernel per il networking: forwarding, buffer, tuning TCP
sysctl net.ipv4.ip_forward # 0/1: la macchina fa da router?
sysctl net.core.somaxconn # backlog massimo per listen()
# Regole netfilter attive (nftables moderno)
sudo nft list ruleset
Docker, Podman e Kubernetes non hanno inventato un modo nuovo di isolare processi: hanno impacchettato due feature del kernel che esistono da anni.
Un container ha bisogno di due cose: non vedere quello che non gli compete (altri processi, altra rete, altri mount) e non consumare più risorse di quelle assegnate. Il primo problema lo risolvono i namespace (isolamento della vista), il secondo i cgroup (limitazione delle risorse). Un container Docker è, in sostanza, un processo Linux normale lanciato dentro namespace dedicati e sotto un cgroup con dei limiti — non c'è nessuna virtualizzazione hardware coinvolta, per questo i container partono in millisecondi e le VM in secondi.
| Namespace | Isola |
|---|---|
PID | Numerazione dei processi: dentro il container, il primo processo è visto come PID 1 |
NET | Interfacce di rete, tabelle di routing, porte: ogni container ha il suo stack di rete |
MNT | Il punto di vista sui mount: la root del container è una directory qualsiasi dell'host |
UTS | Hostname e nome di dominio |
IPC | Coda messaggi, semafori e memoria condivisa System V |
USER | Mappatura UID/GID: root dentro il namespace può essere un utente non privilegiato fuori |
CGROUP | La vista sulla propria gerarchia di cgroup |
TIME | Offset dell'orologio monotonico/boot, per far credere a un container di essere avviato in un altro momento |
I control group raggruppano processi e impongono limiti su CPU, memoria, I/O e altro. La v1 aveva una gerarchia separata per ogni tipo di risorsa (confusionario); la v2 (ormai lo standard, usata da systemd e Docker moderni) usa un'unica gerarchia unificata sotto /sys/fs/cgroup, più semplice da ragionare e da assegnare.
# Crea un nuovo namespace PID+mount ed esegui una shell dentro (un "container" primitivo)
sudo unshare --pid --mount --fork --mount-proc bash
# Elenca i namespace attivi sul sistema
lsns
# La vista ad albero dei cgroup che systemd mantiene
systemd-cgls
# Limite di memoria di un servizio (cgroup v2, via systemd)
systemctl set-property nginx.service MemoryMax=512M
cat /sys/fs/cgroup/system.slice/nginx.service/memory.max
Il kernel espone quasi tutto il suo stato interno come semplici file di testo. Non serve un debugger per capire cosa sta facendo: serve sapere dove guardare.
/proc/[pid]/ — tutto su un processo: status, cmdline, fd/, maps, environ/proc/cpuinfo, /proc/meminfo — hardware e memoria/proc/sys/ — gli stessi parametri regolabili con sysctl, esposti come file/proc/version, /proc/cmdline — con cosa è stato avviato il kernelsysctl -a | grep swappiness
sudo sysctl -w vm.swappiness=10 # applica subito, non persiste
# persistente: /etc/sysctl.d/99-custom.conf
sudo sysctl --system # ricarica tutti i file sysctl.d
| Strumento | A cosa serve |
|---|---|
strace | Traccia le syscall di un processo: il primo strumento quando "non si capisce cosa fa" |
ltrace | Come strace ma per le chiamate a librerie condivise (user space) |
perf | Profiler basato sui contatori hardware della CPU: dove va davvero il tempo |
ftrace | Tracer integrato nel kernel: latenza, funzioni chiamate, scheduling |
eBPF / bpftrace | Programmi sandboxed iniettati nel kernel a runtime: tracing e networking programmabili senza moduli custom |
strace -f -e trace=open,openat -p 4821 # quali file apre un processo in corsa
sudo perf top # cosa consuma CPU, in tempo reale
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s\n", comm); }'
Il kernel gira con la fiducia massima possibile sul sistema. Ridurre cosa "root" può davvero fare è una battaglia continua, non un interruttore.
Storicamente su Unix esisteva solo "root" (può tutto) e "non-root" (può poco). Le capabilities spezzano i privilegi di root in ~40 permessi granulari: CAP_NET_BIND_SERVICE (aprire porte <1024), CAP_SYS_ADMIN (il "quasi tutto" rimasto, da evitare), CAP_NET_RAW (socket raw, es. ping). Un processo può avere solo la capability che gli serve, senza essere root vero.
getcap /usr/bin/ping
sudo setcap cap_net_bind_service=+ep /usr/bin/miod
Un framework di hook sparsi in tutto il kernel dove moduli di sicurezza possono decidere se permettere o negare un'operazione, oltre ai normali permessi Unix. SELinux (label-based, RHEL/Fedora) e AppArmor (path-based, Debian/Ubuntu/SUSE) sono i due più diffusi: la guida AppArmor copre in dettaglio come si scrivono i profili.
Seccomp-bpf permette a un processo di dichiarare "da ora in poi posso chiamare solo queste 30 syscall, tutte le altre uccidimi o rifiutale": un runtime di container come Docker applica un profilo seccomp di default che blocca decine di syscall pericolose o inutili (es. caricare moduli kernel, montare filesystem) senza che l'applicazione dentro se ne accorga.
kernel.dmesg_restrict=1 — nasconde il ring buffer a utenti non privilegiatikernel.kptr_restrict=2 — nasconde indirizzi di memoria del kernel nei logkernel.unprivileged_bpf_disabled=1 se non ti serve eBPF non privilegiatoCAP_SYS_ADMIN quando serve una capability specifica più ristretta--privileged) per pigrizia invece di capire quale capability manca davveroTutto quello che serve, su una pagina. Bookmark questa sezione e dimentica il resto.
uname -a
cat /proc/version
cat /proc/cmdline
dmesg -T | less
journalctl -k -b
systemd-analyze blame
lsmod
modinfo nome_modulo
sudo modprobe nome_modulo
sudo modprobe -r nome_modulo
sudo depmod -a
ps -eo pid,ni,pri,stat,comm
sudo renice -n -5 -p PID
free -h
cat /proc/PID/status
dmesg | grep -i "killed process"
udevadm info --query=all --name=/dev/sda
lspci -k
ss -tulpn
sudo nft list ruleset
lsns
systemd-cgls
sysctl -a | grep swappiness
sudo sysctl -w vm.swappiness=10
strace -f -p PID
sudo perf top
cat /proc/sys/kernel/tainted
getcap /usr/bin/ping
/dev in base agli eventi hardwaredrivers/ per amministrare Linux bene. Ti serve sapere che dmesg è il primo posto dove guardare quando qualcosa si comporta in modo strano, che l'OOM killer esiste e lascia tracce, e che namespace/cgroup sono la ragione per cui i container non sono magia. Il resto lo cerchi quando ti serve davvero, con questa guida come mappa di dove guardare.