# Souborový systém: i-uzly, adresáře a odkazy

Smažeš `rm` dvougigabajtový log, `df -h` ukáže pořád stejně volného místa a `nginx`, který ho měl otevřený, do něj zapisuje dál. Nic nehlásí chybu.

Nebo obráceně: jeden soubor je vidět ve dvou adresářích naráz, v každém pod jiným jménem. Jeden z nich smažeš a druhý funguje dál i s daty.

Ani jedno není chyba a ani jedno není zvláštní režim. Obojí plyne z jedné věci, kterou většina lidí o souborech neví, protože v běžné práci ji nikdy neuvidí.

Stránka je o tom, jak jsou soubory na disku doopravdy zařízené: co je i-uzel, proč je adresář jen tabulka, jak se z i-uzlu dostaneš k datům a proč se ext4 chová jinak než FAT. Neřeší, co přežije výpadek proudu uprostřed zápisu - to je [žurnálování a konzistence](Zurnalovani-a-konzistence) - ani jak se souborový systém zapojuje do stromu adresářů, což je [VFS a montování](VFS-a-montovani). Předpokládá, že víš, co je [systémové volání](Systemova-volani).

## Jméno souboru není soubor

Tohle je nejdůležitější věta na celé stránce: **soubor je i-uzel, jméno je jen záznam v adresáři, který na něj ukazuje.** I-uzel zmizí teprve tehdy, když na něj neukazuje žádné jméno **a zároveň** žádný otevřený deskriptor.

Zbytek stránky je rozvedení téhle věty. `rm` se ve skutečnosti jmenuje `unlink` a dělá přesně to: odpojí jméno. Kolik jmen na i-uzel ukazuje, si počítá sám i-uzel, otevřené deskriptory k tomu připočítává jádro. Nula a nula znamená uvolnit bloky.

### Co v i-uzlu je a co v něm není

I-uzel (inode) je záznam pevné velikosti, u ext4 obvykle 256 bajtů. Drží typ souboru a práva, UID vlastníka a GID skupiny, velikost v bajtech, počet pevných odkazů, ukazatele na datové bloky a tři časy. Ty časy si lidi pletou nejčastěji:

- **mtime** - kdy se změnil **obsah**. Tohle chceš skoro vždycky.
- **ctime** - kdy se změnil **i-uzel**: práva, vlastník, počet odkazů. Změna obsahu ho mění taky. Nedá se nastavit dozadu, což je při vyšetřování průšvihu jediné, co ti zbyde.
- **atime** - kdy se naposledy **četlo**. Od jádra 2.6.30 (2009) se montuje s `relatime` a atime se přepíše, jen když je starší než mtime nebo starší než den. Jinak by každé čtení znamenalo zápis.

Co v i-uzlu **není**: jméno souboru. Pusť si `stat soubor.txt`:

```
  Size: 10          Blocks: 8       IO Block: 4096   regular file
Device: 259,2   Inode: 1837042      Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/  martin)   Gid: ( 1000/  martin)
Modify: 2026-08-06 21:03:55 +0200
Change: 2026-08-07 08:59:11 +0200
```

Jméno v tom výpisu chybí - `stat` ho tiskne jen proto, žes mu ho sám napsal na příkazovou řádku. Zajímavá jsou dvě čísla: `Inode: 1837042` je identita souboru v rámci oddílu a `Blocks: 8` je osm půlkilobajtových jednotek, čili jeden 4KiB blok. Deset bajtů zabírá 4 096.

### Adresář je taky jen soubor

Adresář je soubor, jehož obsahem jsou dvojice **jméno → číslo i-uzlu**. Nic víc v něm není a zapisovat do něj smí jen jádro.

Odtud plyne věc, která vypadá jako díra a není: **abys mohl smazat soubor, potřebuješ právo zápisu do adresáře, ne k souboru.**

```bash
ls -l cizi.txt        # -rw------- 1 root root 42 Aug  7 09:12 cizi.txt
rm cizi.txt           # zeptá se, ale projde - adresář je tvůj
```

Tohle je nejlepší možný důkaz té ústřední věty. A je to důvod, proč má `/tmp` sticky bit: zapisovat tam smí každý, takže bez něj by ti kdokoli mohl smazat cokoli. Detaily jsou na stránce [práva a uživatelé](Prava-a-uzivatele).

## `ln` a `ln -s` nejsou dvě varianty téhož

### Pevný odkaz je druhé jméno

```bash
ln soubor.txt kopie.txt        # nový záznam v adresáři na TENTÝŽ i-uzel
ls -li soubor.txt kopie.txt    # -i vypíše číslo i-uzlu jako první sloupec
```

Obě jména mají stejné číslo i-uzlu a počet odkazů `2`. Není mezi nimi originál a kopie, jsou rovnocenná. Smažeš jedno, počet klesne na jedna a data se nikam nehnou.

### Symbolický odkaz je soubor s cestou uvnitř

`ln -s` vyrobí **nový i-uzel** vlastního typu, jehož obsahem je textová cesta k cíli. ext4 cestu do 59 bajtů uloží rovnou do i-uzlu, takže symbolický odkaz obvykle nezabere ani blok.

| Vlastnost | Pevný (`ln`) | Symbolický (`ln -s`) |
|---|---|---|
| Vlastní i-uzel | ne, sdílí s cílem | ano |
| Přes hranici oddílu | **ne** | ano |
| Na adresář | **ne** | ano |
| Po smazání cíle | funguje dál | ukazuje do prázdna |
| Po přejmenování cíle | funguje dál | rozbije se |
| `ls -l` ukáže | obyčejný soubor | `l` a `-> cesta` |

Přes hranici oddílu pevný odkaz neprojde proto, že v adresáři je uložené **číslo** i-uzlu a to platí jen uvnitř jednoho souborového systému. **Když nevíš, ber `ln -s`** - je vidět a dá se opravit, kdežto pevný odkaz vypadá jako obyčejný soubor a člověk, který ho najde za rok, netuší, že úpravou jednoho mění i druhý.

### Proč pevný odkaz na adresář nejde

Vznikl by cyklus. Adresáře tvoří strom právě proto, že do každého vede jediná cesta shora. Smyčkou by `find`, `du` ani `rm -r` neprošly - nemají jak poznat, že už tam byly - a počítání odkazů by u cyklu nikdy nekleslo na nulu.

Výjimkou jsou `.` a `..`, které si zakládá souborový systém sám. Odtud plyne účetnictví, které mate: **prázdný adresář má počet odkazů 2** - jednou jeho jméno v rodiči, podruhé jeho vlastní `.`. Každý podadresář přidá další, protože jeho `..` ukazuje zpátky. S počtem souborů uvnitř to nesouvisí.

## Jak se i-uzel dostane k datům

### Přímé a nepřímé bloky

Klasické unixové schéma má v i-uzlu patnáct ukazatelů: dvanáct **přímých** na datové bloky, jeden **jednoúrovňový nepřímý** (ukazuje na blok plný ukazatelů), jeden **dvouúrovňový** a jeden **trojúrovňový**. Při bloku 4 KiB a čtyřbajtovém ukazateli se do jednoho bloku vejde 1 024 ukazatelů:

| Úroveň | Bloků | Dat |
|---|---|---|
| 12 přímých | 12 | 48 KiB |
| jednoúrovňový | 1 024 | 4 MiB |
| dvouúrovňový | 1 048 576 | 4 GiB |
| trojúrovňový | 1 073 741 824 | 4 TiB |

Malý soubor je dostupný okamžitě, na konec velkého se prokoušeš až po třech dalších čteních. To je záměr: většina souborů v systému je malá.

### Extenty, čili co s tím udělal ext4

Souvislý 1GiB soubor potřebuje ve starém schématu čtvrt milionu ukazatelů, přestože jde o jeden jediný souvislý rozsah.

ext4 (výchozí od roku 2008) to nahradil **extenty**. Extent je trojice „logický blok, fyzický blok, délka" a pokryje až 32 768 bloků, čili 128 MiB. Do i-uzlu se vejdou čtyři, na víc se rozvine strom. Ten samý 1GiB soubor je pak popsaný osmi extenty. Kromě místa to šetří hlavně čtení metadat a dovoluje alokovat velké kusy naráz.

## Kam se bloky ukládají a proč se Linux „nedefragmentuje"

Bloky se dají rozdat třemi způsoby. **Souvislá alokace** je nejrychlejší na čtení a nepoužitelná, protože soubor nemůže růst. **Seznamová** dá do každého bloku odkaz na další a tím zabije náhodný přístup - přesně to dělá FAT. **Indexová** drží seznam bloků stranou v i-uzlu, a to je Unix.

Vnitřní fragmentaci neřeší žádná: blok je nejmenší jednotka, takže soubor o deseti bajtech zabere 4 KiB a 4 086 bajtů se zahodí. Na dvou stech tisících drobných souborech je to skoro gigabajt.

**Linux se opravdu nedefragmentuje a není to mýtus.** ext4 používá **zpožděnou alokaci**: data drží v [page cache](Vstup-a-vystup) a fyzické bloky vybere až při zápisu na disk, kdy zná celkovou velikost. K tomu přidává víceblokovou alokaci a rezervaci místa za koncem rostoucího souboru. Běžný soubor tak vznikne jako jeden nebo dva extenty a nemá se co rozsekat.

**Kdy to přestává platit: nad zhruba devadesát procent zaplnění.** Volné místo přestane být souvislé, alokátor bere, co zbylo, a soubory vznikají rozsekané na desítky kusů. Rady na defragmentaci, které v návodech pořád visí, řeší následek jiného problému: správná odpověď je uvolnit místo, ne pouštět `e4defrag`.

## FAT jako protiklad

Na začátku oddílu je **tabulka**, ve které má každý cluster položku s číslem clusteru následujícího - spojový seznam. Adresářový záznam nese jméno, velikost, atributy a číslo prvního clusteru. Žádný i-uzel neexistuje: identita souboru a jeho jméno jsou jedna a ta samá věc.

Z toho plyne, co FAT nemá: vlastníka, práva, pevné ani symbolické odkazy, žurnál. Systém na něm provozovat nejde už proto, že by nebyl rozdíl mezi rootem a kýmkoli jiným.

Přesto ho používáš denně - USB klíčenka, kterou přečte i patnáct let staré rádio, a **EFI oddíl**, kde FAT vyžaduje přímo specifikace UEFI, protože firmware musí načíst zavaděč bez ovladačů (viz [start systému](Start-systemu)).

| Systém | Největší soubor | Práva | Konzistence | Kdy ho chceš |
|---|---|---|---|---|
| FAT32 | 4 GiB - 1 B | ne | žádná | EFI oddíl, staré přístroje |
| exFAT | prakticky bez limitu | ne | žádná | velké SD karty sdílené s Windows |
| **ext4** | 16 TiB | ano | žurnál | **výchozí volba na systém i data** |
| btrfs | 16 EiB | ano | copy-on-write | snapshoty, kontrolní součty, více disků |
| XFS | 8 EiB | ano | žurnál | velké soubory, hodně paralelních zápisů |

Když nevíš, ber ext4. btrfs si vezmi, když opravdu chceš snapshoty nebo detekci tichého poškození dat, XFS na server s videem.

## Pět procent, na která nesmíš

ext4 si při vytvoření rezervuje **5 procent bloků pro roota**. Když se disk zaplní, systémové služby mají ještě kam zapsat a stroj se dá zachránit, a alokátor navíc drží rezervu, aby při plném disku nezačal hned rozsekávat soubory.

Na kořenovém oddíle to nech být. Na dvouterabajtovém datovém disku je to sto gigabajtů, které nikdy nikdo nepoužije:

```bash
tune2fs -l /dev/nvme0n1p2 | grep -i "reserved block"   # kolik je vyhrazeno teď
tune2fs -m 1 /dev/nvme0n1p2                            # sniž na 1 %, jen u datového oddílu
```

## Když si `du` a `df` odporují

`ls -l` čte velikost z i-uzlu, `du` sčítá **skutečně přidělené bloky**. Obě čísla bývají jiná a obě jsou správně.

**Řídký soubor (sparse)** má díry, kterým nikdy nebyly přiděleny bloky. Čtení z díry vrátí nuly, na disku ale není nic:

```bash
truncate -s 1G prazdny.img
ls -lh prazdny.img     # 1,0G
du -h prazdny.img      # 0
```

Takhle fungují obrazy disků virtuálních strojů. Pozor při přenosu: `rsync` bez `-S` a `tar` bez `-S` díry vyplní nulami a z jednogigabajtového souboru je rázem gigabajt na disku.

Druhý případ je ten z úvodu. Smazaný, ale stále otevřený soubor už nemá jméno, takže ho `du` při procházení stromu nenajde. Jeho bloky pořád existují, takže je `df` počítá jako obsazené. Rozdíl mezi `du -sh /` a `df -h /` je přesně to, co drží běžící procesy.

```bash
lsof +L1               # soubory s počtem odkazů 0, které někdo drží otevřené
: > /proc/1234/fd/7    # uvolní místo bez restartu procesu
```

Druhý příkaz zkrátí soubor přes deskriptor. Spolehlivě funguje u procesu, který zapisuje v režimu append - jinak bude psát dál na svůj starý offset a vyrobí ti řídký soubor.

## Toy Story 2, rok 1998

Během výroby filmu někdo pustil na sdíleném svazku Pixaru `rm -rf` na špatném místě. Soubory mizely za běhu a animátoři sledovali, jak se ztrácejí celé adresáře s postavami a scénami. Než stačili stroj odpojit, byla pryč většina filmu - podle pozdějších vyjádření technického ředitele zhruba devadesát procent.

Záloha existovala. Jenže zálohovací systém tiše selhával a nikdo si toho nevšiml, protože obnovu nikdy nikdo nezkusil. Film zachránila kopie, kterou měla doma technická ředitelka Galyn Susmanová - pracovala z domova po narození dítěte a data si k sobě synchronizovala.

**Poučení: záloha, kterou jsi nikdy neobnovil, není záloha.** Je to jen složka, o které si to myslíš.

## Diagnostika

```bash
df -h /var                 # volné bajty
df -i /var                 # volné i-uzly - jiný zdroj, dochází dřív
lsof +L1                   # smazané a stále otevřené soubory
ls -li                     # čísla i-uzlů a počty odkazů
stat soubor.txt            # všechno o jednom i-uzlu
filefrag -v velky.img      # na kolik extentů je soubor rozsekaný
du -sh --apparent-size .   # velikost proti skutečně obsazenému místu
```

| Příznak | Kde je problém |
|---|---|
| „No space left on device" a `df -h` ukazuje volno | došly i-uzly, ověř `df -i` |
| Smazal jsi logy, místo nepřibylo | proces drží soubor otevřený, `lsof +L1` |
| `du -sh /` a `df -h /` si odporují | totéž, smazané a otevřené soubory |
| `du` hlásí míň než `ls -l` | řídký soubor, díry nezabírají bloky |
| Soubor nejde smazat, i když je tvůj | chybí zápis do **adresáře**, sticky bit nebo `chattr +i` |
| Uživateli došlo místo, rootovi ne | 5 procent vyhrazených bloků |
| Kopírování 5GB souboru na klíčenku selže | FAT32, limit 4 GiB - 1 B |

Počet i-uzlů se u ext4 stanoví při `mkfs` a **později se nedá zvětšit** - to je klasika serverů s poštou nebo cache. Jediné řešení je přeformátovat s `mkfs.ext4 -i 8192` a obnovit ze zálohy. Víc nástrojů je na stránce [nástroje a diagnostika](Nastroje-a-diagnostika).

## Co si odnést

**Soubor je i-uzel, jméno je ukazatel na něj.** Všechno divné chování kolem mazání plyne z téhle věty.

**Mazání je odebrání jména.** Data zmizí, až když nezbyde jméno ani otevřený deskriptor.

**Právo mazat je právo zápisu do adresáře.** K samotnému souboru nepotřebuješ nic.

**Pevný odkaz je rovnocenné jméno, symbolický je soubor s cestou uvnitř.** První neprojde přes oddíl, druhý se rozbije přejmenováním cíle.

**Extenty popisují rozsahy, ne jednotlivé bloky.** Osm položek místo čtvrt milionu ukazatelů.

**Zaplnění nad devadesát procent láme alokátor.** Do té doby se ext4 fragmentuje zanedbatelně.

**Místo a i-uzly jsou dva nezávislé zdroje.** Každý dochází zvlášť a `df -h` o tom druhém mlčí.

## Kam dál

- **[Žurnálování a konzistence](Zurnalovani-a-konzistence)** - co z těchhle struktur přežije výpadek proudu
- **[VFS a montování](VFS-a-montovani)** - jak se ext4, tmpfs a FAT tváří pro programy stejně
- **[Práva a uživatelé](Prava-a-uzivatele)** - rwx bity, setuid a sticky bit, které sedí v i-uzlu
- **[Vstup a výstup](Vstup-a-vystup)** - page cache a proč se zápis nekoná hned
- **[Nástroje a diagnostika](Nastroje-a-diagnostika)** - `lsof`, `strace` a `/proc` na živém systému
