Operační systémy
Obsah Soubory
Soubory a zařízení

VFS a připojování

Aktualizováno 9 min čtení 1 660 slov

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 - tady jde o vrstvu nad ním. Předpokládá to systémová volání a základní představu o procesech.

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, 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é.

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. Nový program o přesměrování neví, píše na jedničku.

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.

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é.

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ř.

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. 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. 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í 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.

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. Praktický dopad je jeden: když přípojný bod vidí jeden proces a druhý ne, neopravuj mount, ale porovnej jejich /proc/PID/mountinfo.

Diagnostika

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