# VFS a připojování souborových systémů

Pusť `cat` na pět různých věcí: na soubor na NVMe disku, na soubor na USB klíčence naformátované na FAT, na soubor na síťovém disku na druhém konci republiky, na `/proc/cpuinfo` a na `/dev/urandom`. Pokaždé to funguje a pokaždé je to tentýž program s tímtéž kódem.

Pod tím přitom leží pět úplně různých mechanismů: extenty na ext4, tabulka alokačních jednotek na FAT, síťový protokol s časovými limity, funkce jádra, která odpověď sestaví až ve chvíli čtení, a generátor, který neukládá vůbec nic. `cat` o žádném z nich neví a nikdy vědět nebude. Zavolá `open`, `read` a `close`, víc v něm není.

Mezi ním a těmi pěti věcmi stojí jedna vrstva jádra, která předstírá, že jsou všechny stejné. Říká se jí VFS (Virtual File System) a je to nejlepší příklad abstrakce v celém systému.

Tahle stránka je o tom, jak ta vrstva vypadá zevnitř, jak se do stromu připojují další souborové systémy a co se na tom rozbíjí. Uspořádání dat na disku má vlastní stránku [Souborový systém](Souborovy-system) - tady jde o vrstvu nad ním. Předpokládá to [systémová volání](Systemova-volani) a základní představu o [procesech](Procesy).

## Jedna sada operací pro všechno

Jádro definuje pevnou sadu operací nad souborem: otevři, čti, zapiš, posuň se, zavři a několik desítek dalších. Technicky je to struktura ukazatelů na funkce a pro celý systém je jedna jediná.

Každý souborový systém a každý ovladač do ní dosadí své vlastní funkce. ext4 dosadí čtení, které umí extenty, FAT čtení přes alokační tabulku a `/dev/urandom` čtení, které nikam nesáhne a bajty vyrobí.

Tohle je nejdůležitější věc na celé stránce: **v Linuxu je souborem všechno, protože jádro má jednu jedinou sadu operací a každý souborový systém i každé zařízení k ní jen dodá své funkce.** Program zavolá `read`, VFS se podívá, o jaký objekt jde, a zavolá to, co tam kdo zaregistroval. Zbytek stránky je jen rozvedení téhle věty.

## Čtyři objekty, na kterých VFS stojí

| Objekt | Co představuje | Vznikne, když |
|---|---|---|
| superblok | připojený souborový systém jako celek | připojíš svazek |
| i-uzel | soubor sám o sobě - práva, velikost, umístění dat | sáhneš na soubor |
| dentry | jméno v adresáři a jeho vazba na i-uzel | přeloží se cesta |
| soubor | otevřená instance včetně pozice a příznaků | zavoláš `open` |

I-uzel je tentýž pojem, který popisuje [Souborový systém](Souborovy-system), jen držený v paměti. Nemá v sobě jméno - jméno je až dentry, a proto může jeden i-uzel viset pod několika jmény. Objekt „soubor“ naopak vzniká na každé otevření zvlášť: dva procesy čtoucí týž soubor mají dva objekty s vlastní pozicí, ale jeden společný i-uzel.

### Proč existuje mezipaměť dentry

Překlad cesty `/home/martin/data/soubor.txt` není jedna operace. Je to čtyři vyhledání za sebou: `home` v kořeni, `martin` v `home`, `data` v `martin` a teprve pak soubor. Bez mezipaměti by se ten řetěz i s načtením adresářových bloků odehrál znovu při každém otevření.

Dentry cache si proto pamatuje provedené překlady, a to i **negativní výsledky** - že se pod tímhle jménem nic nenachází. Marné hledání knihovny v deseti adresářích je díky tomu levné.

```bash
sudo slabtop -o | head -5    # kolik paměti drží mezipaměť dentry a i-uzlů
```

### Deskriptor souboru: číslo, ne soubor

Deskriptor je malé nezáporné celé číslo a nic jiného v něm není: index do tabulky otevřených souborů daného procesu. Jádro přiděluje vždycky nejnižší volné. Čísla 0, 1 a 2 jsou standardní vstup, výstup a chybový výstup - není to pravidlo jádra, ale dohoda, kterou drží shell a libc.

**Deskriptory přežijí `fork` a ve výchozím stavu i `exec`.** Potomek dostane kopie ukazující na tytéž otevřené soubory a při `exec` zůstávají otevřené, pokud u nich není příznak `FD_CLOEXEC`. Přesně na tom stojí přesměrování v shellu: shell udělá `fork`, v potomkovi otevře soubor a přehodí ho na deskriptor 1, a teprve pak zavolá `exec` - mechanika obojího je na stránce [Procesy](Procesy). Nový program o přesměrování neví, píše na jedničku.

```bash
ls -l /proc/self/fd                     # čím má otevřeno proces, který to volá
ls -l /proc/$(pgrep -n nginx)/fd | head # a čím poslední spuštěný nginx
ulimit -n                               # měkký strop, na Debianu často 1024
```

Chybu „Too many open files“ dostaneš, když ten strop vyčerpáš. Většinou to není málo nastavený limit, ale program, který deskriptory otevírá a nezavírá. Serveru, který jich opravdu potřebuje víc, zvedni `LimitNOFILE=` v jednotce systemd, ne `ulimit` ve spouštěcím skriptu.

## Připojování a přípojné body

Připojení napojí kořen jednoho souborového systému na existující adresář jiného. Od té chvíle vede cesta přes ten adresář jinam.

**Obsah překrytého adresáře nezmizí, jen není vidět.** Po odpojení je zpátky. Odtud pochází častý úlek: napíšeš gigabajty do `/mnt/data`, pak tam připojíš disk a soubory jsou pryč. Nejsou, jsou pod přípojným bodem.

```bash
sudo mount /dev/nvme0n1p2 /mnt/data   # ručně, restart to nepřežije
findmnt /mnt/data                     # co je tam doopravdy připojené
```

Aby připojení přežilo restart, patří do `/etc/fstab`. Řádek má šest polí: zařízení, přípojný bod, typ, volby, příznak pro `dump` a pořadí kontroly při startu.

```
UUID=8f1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8  /mnt/data  ext4  defaults,noatime  0  2
```

**Zařízení se píše jako `UUID=`, nikdy jako `/dev/sda1`.** Názvy zařízení přiděluje jádro v pořadí, ve kterém je najde, a to se mezi starty mění - stačí zapomenutá USB klíčenka a z `/dev/sdb1` je `/dev/sdc1`. UUID je zapsané v souborovém systému samotném a vypíše ho `lsblk -f`.

Místo `mount` bez argumentů používej `findmnt`: vypíše strom místo hromady řádků a s `findmnt -T /mnt/data/projekt` řekne, pod jaký přípojný bod daná cesta spadá.

### Volby, které stojí za to nastavit

**Na svazku, kde jsou jenom data, nastav `nosuid,nodev,noexec`.** `nosuid` zruší účinek bitu setuid, `nodev` zakáže soubory zařízení, `noexec` spouštění. Co setuid dělá a proč je citlivý, rozebírají [Práva a uživatelé](Prava-a-uzivatele).

Kde to přestává platit: `noexec` není bezpečnostní hranice. Zabrání spuštění binárky, ale `bash /mnt/data/skript.sh` projde, protože interpret soubor jenom čte. Je to obrana proti nedopatření, ne proti útočníkovi.

Připojení jen pro čtení (`ro`) použij vždycky, když z disku zachraňuješ data.

### Když se svazek nedá odpojit

`umount: target is busy` znamená, že do toho stromu má někdo otevřenou cestu - obvykle proces s otevřeným deskriptorem nebo s pracovním adresářem uvnitř.

```bash
fuser -vm /mnt/data    # kdo tam sahá a čím: otevřený soubor, cwd, běžící binárka
```

**`umount -l` je poslední možnost, ne první.** Odpojí přípojný bod ze stromu okamžitě, ale souborový systém žije dál, dokud ho poslední proces nezavře. Procesy tedy dál zapisují do něčeho, co už nikdo nevidí.

**Reálný případ, 31. ledna 2017.** Inženýr GitLabu při potížích s replikací smazal datový adresář databáze na produkčním stroji místo na záložním. Přišlo se zhruba o šest hodin dat a ukázalo se, že pět různých záložních postupů nefunguje tak, jak se čekalo. Poučení sem: **cesta vypadá na obou strojích stejně, identitu jí dává až to, co je pod ní připojené.**

## Souborové systémy, které nejsou na disku

| Cesta | Typ | Co tam najdeš |
|---|---|---|
| `/proc` | procfs | adresář na každý proces, plus stav jádra (`/proc/meminfo`) |
| `/proc/PID/` | procfs | `fd`, `maps`, `status`, `cmdline`, `cwd`, `exe` |
| `/sys` | sysfs | zařízení, ovladače, sběrnice, přepínače jádra |
| `/dev` | devtmpfs | uzly zařízení, které jádro vytvoří samo |
| `/run` | tmpfs | běhové soubory služeb, PID soubory, sokety |
| `/dev/shm` | tmpfs | sdílená paměť mezi procesy |
| `/sys/fs/cgroup` | cgroup2 | limity a účtování zdrojů |

Ani jeden z nich nemá na disku jediný bajt. Čtení z `/proc/cpuinfo` spustí funkci v jádře, která text vyrobí až v tu chvíli - proto `ls -l /proc/cpuinfo` hlásí nulovou velikost, i když z něj vypadne dvacet řádků.

**`/proc` jsou procesy jako adresáře, `/sys` zařízení jako adresáře.** Sysfs navíc drží pravidlo jedna hodnota na soubor, takže se čte i nastavuje obyčejným `cat` a `echo`.

**tmpfs žije v paměti a započítává se do ní.** Soubory v `/run` a `/dev/shm` zmizí při restartu a dokud existují, zabírají paměť. Odložit se dají, ale zahodit ne, protože nemají kam - aplikace, která zaplní `/dev/shm`, se tak umí dostat až k zásahu OOM killeru, jak popisují [Výměnné algoritmy](Vymenne-algoritmy). Výchozí strop `/dev/shm` je polovina paměti, tedy 8 GiB na stroji se 16 GB.

Co se dnes už nedělá: statické `/dev` s tisícovkami předem vytvořených uzlů a `mknod` v návodech na instalaci. Uzly vytváří jádro samo přes devtmpfs, jména a práva jim doladí udev.

## Vrstvené a uživatelské souborové systémy

**overlayfs skládá adresáře na sebe.** Spodních vrstev může být několik a jsou jen pro čtení, horní je zapisovatelná. Čtení propadává odshora dolů, první zápis do souboru ho celý zkopíruje nahoru. Na tomhle stojí kontejnery: vrstvy obrazu jsou spodek, zapisovatelná vrstva kontejneru vršek a se smazáním kontejneru zmizí, jak rozebírají [Virtualizace a kontejnery](Virtualizace-a-kontejnery). Důsledek: zápis jednoho bajtu do dvougigabajtového souboru zkopíruje dva gigabajty.

**FUSE přesouvá souborový systém do uživatelského prostoru.** Každá operace jde z aplikace do jádra, z jádra do procesu s obsluhou, zpátky do jádra a teprve pak do aplikace. Dvě přepnutí navíc na jednu operaci, a při ceně přechodu popsané u [systémových volání](Systemova-volani) je to znát hlavně u mnoha malých souborů.

Přesto ho používej. Pro sshfs, připojený cloud přes rclone nebo šifrované úložiště je to jediná rozumná cesta, protože alternativou je ovladač v jádře, kde chyba položí celý stroj. Jen po něm nechtěj propustnost lokálního disku.

## Každý proces může vidět jiný strom

Přípojné body nejsou globální. Jmenný prostor přípojných bodů je kopie stromu, kterou proces mění, aniž by o tom zbytek systému věděl.

```bash
sudo unshare -m bash    # nový shell s vlastním stromem přípojných bodů
```

Co v takovém shellu připojíš, nikdo jiný neuvidí. Na tomhle stojí kontejnery a celý výklad je na stránce [Virtualizace a kontejnery](Virtualizace-a-kontejnery). Praktický dopad je jeden: když přípojný bod vidí jeden proces a druhý ne, neopravuj `mount`, ale porovnej jejich `/proc/PID/mountinfo`.

## Diagnostika

```bash
findmnt              # strom přípojných bodů, čitelnější než holé mount
lsblk -f             # disky, oddíly, typy, UUID a kam jsou připojené
df -hT               # volné místo včetně typu souborového systému
mount | column -t    # rychlý pohled na volby připojení
lsof /mnt/data       # kdo tam má otevřeno
cat /proc/mounts     # totéž očima jádra
```

| Příznak | Kde je problém |
|---|---|
| `umount: target is busy` | otevřený deskriptor nebo cwd uvnitř - `fuser -vm` |
| Po restartu svazek chybí | není v `/etc/fstab` nebo se změnil název zařízení |
| Zapisuju a soubory po připojení zmizí | psalo se do adresáře pod přípojným bodem |
| `df` hlásí plno, `du` ukazuje málo | smazaný soubor drží otevřený proces - `lsof +L1` |
| `/dev/shm` plné, systém začne odkládat | tmpfs se počítá do paměti |
| „Too many open files“ | strop `ulimit -n`, obvykle neuzavírané deskriptory |
| Přípojný bod vidí jen některé procesy | jiný jmenný prostor - `/proc/PID/mountinfo` |

## Co si odnést

**VFS je jedna sada operací, do které si každý dodá své funkce.** Odtud plyne „souborem je všechno“, ne naopak.

**Čtyři objekty: superblok, i-uzel, dentry, soubor.** Svazek, soubor, jméno, otevření.

**Deskriptor je číslo do tabulky procesu**, přežije `fork` a bez `FD_CLOEXEC` i `exec`.

**Překrytý adresář se neztratil, jen ho nevidíš.** Odpoj a je zpátky.

**V `fstab` patří `UUID=`**, protože `/dev/sdX` se mezi starty mění.

**`/proc`, `/sys` a `tmpfs` nemají na disku nic**, ale tmpfs ti ubírá paměť.

**`umount -l` problém neřeší, jen ho schová.** Napřed `fuser -vm`.

## Kam dál

- **[Souborový systém](Souborovy-system)** - co je pod VFS: i-uzly, adresáře, alokace bloků
- **[Žurnálování a konzistence](Zurnalovani-a-konzistence)** - proč `umount` není formalita
- **[Vstup a výstup](Vstup-a-vystup)** - zařízení a mezipaměť stránek pod tím vším
- **[Virtualizace a kontejnery](Virtualizace-a-kontejnery)** - jmenné prostory a overlayfs v plné šíři
- **[Nástroje a diagnostika](Nastroje-a-diagnostika)** - `lsof`, `strace` a `/proc` jako každodenní nářadí
