Nauč se Python > Kurzy > Linuxová administrace > Procesy & deskriptory souborů > Soubory a deskriptory

Soubory a deskriptory

Co je to soubor?

Lehce naivní ale přesto užitečná definice je: soubor je něco, z čeho můžeme číst a/nebo do čeho můžeme zapisovat.

Se soubory se dají dělat i další operace než jen čtení a zápis, ale v tomto kurzu se jimi většinou nebudeme zabývat.

Pravděpodobně máš největší zkušenosti se soubory, které jsou uložené na disku pod nějakým jménem. Třeba následující příkaz zapisuje do souboru vystup.txt:

$ ps -Af > vystup.txt

Tento příkaz znamená: Bashi, spusť ps -Af a řekni mu aby psal do souboru vystup.txt. ps píše na svůj standardní výstup, což je soubor – v příkladu výše je to soubor vystup.txt.

Kdybys výstup nepřesměrovala, uviděla bys ho v terminálu. Pro program ps se nic nemění: pořád píše na svůj standardní výstup, což je soubor.

Je to soubor, který reprezentuje terminál – něco, do čeho může proces zapisovat (a co se pak objeví na obrazovce) a z čeho může číst (když uživatel něco zadá). Obsah tohoto souboru není uložen na disku, ale přesto jde o soubor.

Podívej se na tenhle příkaz:

$ ps -Af | grep -w ps

Příkaz ps -Af stále píše na svůj standardní výstup, což je soubor.

tomhle případě je to soubor, který reprezentuje „rouru“ – něco, do čeho může jeden proces zapisovat a druhý to pak může číst. Ani obsah roury není uložen na disku – „proudí“ přímo z jednoho procesu do druhého – ale přesto jde o soubor.

Známe tedy už tři druhy souborů:

  • normální souboru uložené na disku,
  • terminál,
  • rouru.

Otevřené soubory procesu

Každý proces si může otevírat další soubory - to už znáš z funkce open v Pythonu. Podíváme se, jak zjistit seznam takto používaných souborů. Existuje na to program lsof (z angl. list open files). Většinou se spouští s přepínačem -p <číslo procesu>, aby ukázal jen otevřené soubory jednoho procesu.

Použijme PID Bashe, které se jednoduše zjišťuje – je v proměnné $$:

$ lsof -p $$
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
bash    5236 user  cwd    DIR    8,1     4096 1310722 /home/user
bash    5236 user  rtd    DIR    8,1     4096       2 /
bash    5236 user  txt    REG    8,1  1113504  524393 /bin/bash
bash    5236 user  mem    REG    8,1    47568 7344943 /lib/x86_64-linux-gnu/libnss_files-2.27.so
bash    5236 user  mem    REG    8,1    26376 6032184 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
(...)
bash    5236 user    0u   CHR  136,0      0t0       3 /dev/pts/0
bash    5236 user    1u   CHR  136,0      0t0       3 /dev/pts/0
bash    5236 user    2u   CHR  136,0      0t0       3 /dev/pts/0
bash    5236 user  255u   CHR  136,0      0t0       3 /dev/pts/0

Jak vidíš, je to poměrně velká tabulka.

  • COMMAND je jméno programu, který otvírá daný soubor (bez -p vypisuje lsof soubory pro víc programů najednou).
  • PID je číslo procesu, náš starý známý.
  • USER je uživatel, jehož jménem proces běží.
  • FD je zkratka pro deskriptor souboru (angl. file descriptor), což je číslo otevřeného souboru. Toto číslo nás bude dnes nejvíc zajímat.

    • cwd je speciální hodnota pro aktuální adresář (current working directory) – právě tahle hodnota se dá v Bashi změnit pomocí cd
    • rtd je kořenový adresář (root directory) - mělo by to být /
    • txt je kód samotného programu. (Každý program musí být uložený někde na disku. Když ho pustíš jako proces, systém soubor načte a začne provádět příkazy v něm uložené.)
    • mem jsou soubory načtené v paměti, většinou další součásti programu.
    • 0 a dál jsou konečně normální otevřené soubory. Písmenka označují mód, např.:
      • 1r - otevřeno pro čtení (angl. read)
      • 1w - otevřeno pro zápis (angl. write)
      • 1u - otevřeno „univerzálně“, pro čtení i zápis
  • TYPE může být např.
    • DIR - adresář
    • REG - normální soubor
    • CHR - speciální soubor, např. terminál
  • DEVICE - číslo zařízení (disku), na kterém soubor je
  • SIZE/OFF - velikost / pozice v souboru
  • NODE - číslo souboru (unikátní v rámci DEVICE)
  • NAME - jméno souboru

Terminál jako soubor

Soubor, který má Bash otevřený jako 0u, 1u i 2u, je za normálních okolností terminál, ve kterém Bash běží. Zjisti z výstupu výše, který to je. V našem příkladu je to /dev/pts/0, u tebe může být jméno jiné.

Jak už víš, terminál je soubor, do kterého můžeš zapisovat. Otevři si další okno terminálu a zadej následující příkaz. (Za /dev/pts/0 doplň „svoje“ jméno terminálu):

$ echo abc > /dev/pts/0

Všimni si, že se abc objeví ve druhém terminálovém okénku!

Když znáš jméno souboru a víš, jak se do něj dostat (znáš cestu), můžeš do něj zapisovat. Soubor pro terminál není zas tolik zvláštní: jakmile znáš jméno, můžeš do něj zapisovat jako do jakéhokoli jiného souboru.

K čemu to je dobré?

Svého času takhle administrátoři posílali zprávy dalším lidem, co zrovna pracovali na tom samém stroji. Ale dnes se to už tolik nepoužívá. Jen to ukazuje jak věci fungují – „všechno je soubor“.

Procvičování v Pythonu

Procvičme si to trochu v Pythonu. Budeš potřebovat textový editor a dva terminály. V tomto textu jim budu říkat A a B.

Budeme používat Python, který je zabudovaný přímo v systému. Nevytvářej/neaktivuj si virtuální prostředí – pracuješ na virtuálním stroji, to úplně stačí.

V obou terminálech se přepni do adresáře, do kterého ukládáš soubory pro dnešní lekci. Jestli takový nemáš, vytvoř si ho.

Otevři si textový editor a následující kód si ulož do souboru soubory.py:

# soubory.py
# modul, kde jsou zpřístupněné služby operačního systému
import os
import time

# číslo právě běžícího procesu (ne Bashe, ale Pythonu)
# Pokaždé, když spustíš soubory.py v příkazové řádce, se toto číslo změní
print(os.getpid())

# Nechme program 600 vteřin (10 minut) čekat.
# To bude dost času na to, abychom mohli např. analyzovat otevřené soubory.
time.sleep(600)

A v terminálu A program spusť.

$ python soubory.py
17342

Mělo by se ti vypsat číslo procesu (jiné, než v našem příkladu).

V terminálu B pusť příkaz:

$ lsof -p <číslo procesu z terminálu A>
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
python  6503 user  cwd    DIR    8,1     4096 1310722 /home/user
python  6503 user  rtd    DIR    8,1     4096       2 /
python  6503 user  txt    REG    8,1  4873376 5773466 /usr/bin/python3.7
python  6503 user  mem    REG    8,1 11531024 5773943 /usr/lib/locale/locale-archive
python  6503 user  mem    REG    8,1  1700792 7344916 /lib/x86_64-linux-gnu/libm-2.27.so
python  6503 user  mem    REG    8,1   116960 7345025 /lib/x86_64-linux-gnu/libz.so.1.2.11
(...)
python  6503 user    0u   CHR  136,0      0t0       3 /dev/pts/0
python  6503 user    1u   CHR  136,0      0t0       3 /dev/pts/0
python  6503 user    2u   CHR  136,0      0t0       3 /dev/pts/0

Zkontroluj, jestli pod txt vidíš nějako verzi Pythonu. Vidíš? Skvěle, pojďme o kousek dál.

soubory.py změň poslední sekci – dopiš kus kódu, který otevře soubor:

# soubory.py
# modul, kde jsou zpřístupněné služby operačního systému
import os   
import time

# číslo právě běžícího procesu (ne Bashe, ale Pythonu)
# pokaždé, když spustíš soubory.py v příkazové řádce, se toto číslo změní
print(os.getpid())

# Tentokrát čekáme s otevřeným souborem:
with open('soubory.py', encoding='utf-8') as soubor:
    time.sleep(600)

Po uložení souboru ukonči program v terminálu A (CTRL+C) a spusť ho znovu. Vypíše se jiné číslo procesu. Do terminálu B zadej aktualizovaný příkaz lsof -p <číslo procesu>. Podívej se na poslední řádek výpisu. Vidíš tam něco nového?

$ lsof -p 6604
(...)
python  6604 user    3r   REG    8,1      333 1314091 /home/user/bash/03/soubory.py

Měl by přibýt záznam o nově otevřeném souboru (soubory.py) s číslem 3r. Tři je další zatím nevyužité číslo (po 0, 1, a 2); r indikuje soubor otevřený pro čtení.

Řekla jsi Pythonu, aby otevřel pro čtení soubor s vlastním programem. Protože příkaz lsof vypíše otevřené soubory procesu, vidíš ve výsledcích nově výsledek této operace.

Zatím všechno v pohodě? Tak otevři jeden soubor pro čtení a nějaký jiný pro zápis. Změn svůj program na příklad níže.

# soubory.py
# modul, kde jsou zpřístupněné služby operačního systému
import os   
import time

# číslo právě běžícího procesu (ne Bashe, ale Pythonu)
# pokaždé, když spustíš soubory.py v příkazové řádce, se toto číslo změní
print(os.getpid())

with open('soubory.py', encoding='utf-8') as soubor:
    with open('jiny.txt', mode='w', encoding='utf-8') as soubor2:
        print(soubor.fileno(), soubor2.fileno())
        time.sleep(600)

Kromě PID se teď vypíší výsledky metody fileno. Jsou to deskriptory souborů – čísla která pro otevřené soubory používá systém, a která budou vidět i ve výstupu lsof.

Ukonči běh předchozího programu v terminálu A (CTRL+C) a spusť soubor ještě jednou. V terminálu B spusť aktualizovaný příkaz lsof.

$ lsof -p 6904
python  6904 user    0u   CHR  136,0      0t0       3 /dev/pts/0
python  6904 user    1u   CHR  136,0      0t0       3 /dev/pts/0
python  6904 user    2u   CHR  136,0      0t0       3 /dev/pts/0
python  6904 user    3r   REG    8,1      373 1314108 /home/user/bash/03/soubory.py
python  6904 user    4w   REG    8,1        0 1314053 /home/user/bash/03/jiny.txt

Výsledek doufám nepřekvapí.

Deskriptory

Python nám v mnohém pomáhá, ale taky vzdaluje od systémové vrstvy. Pythonní objekty které vrací funkce open nejsou přesně totéž jako způsob, jakým soubory zpracovává operační systém. Dělají spoustu věcí navíc.

My se chceme podívat, jak funguje vevnitř operační systém, nikoliv Pythonní zlepšováky. Proto si otevři tyto soubory ještě jednou pomocí modulu os, který umožňuje dělat věci trošku víc „přímo“.

Zaměň celý blok s with za tento kus kódu:

fd1 = os.open("soubory.py", os.O_RDONLY)
fd2 = os.open("jiny.txt", os.O_WRONLY)
print(fd1, fd2)
time.sleep(600)

Takto upravený program spusť v terminálu A. Kromě nového čísla procesu bys měl(a) vidět na dalším řádku dvě čísla. U nás jsou to 3, 4.

$ python soubory.py
19257
3, 4

V terminálu B spusť opět příkaz lsof s novým číslem procesu a podívej se na dva poslední řádky. Deskriptory otevřených souborů by měly opět odpovídat číslům z terminálu A.

Poznámka pro zvídavé: pod složitým os.O_RDONLY a os.O_WRONLY se skrývají jenom číselné konstanty 0 a 1. Systémové operace (např. open) používají číselné konstant, kterým byla pro lepší čitelnost přiřazena krátká jména.

Pokud otevřeš pomocí pythonní funkce open neexistující soubor pro zápis, funkce soubor vytvoří a otevře ho. Oproti tomu systémová funkce os.open() ho nevytvoří, ale zahlásí chybu. Pokud se ti to stane, můžeš společně s os.O_WRONLY použít os.O_CREAT. Kombinují se pomocí operátoru |:

os.open('jiny.txt', os.O_WRONLY | os.O_CREAT)

Anebo můžeš soubor vytvořit v terminálu pomocítouch jiny.txt.

Přidej do pythonního souboru pod řádky, kde soubory otevíráš, tento řádek:

print(os.read(fd1, 10))

Tento řádek načte prvních 10 bajtů ze souboru soubory.py a vypíše je do terminálu. V terminálu A spusť soubory.py. V našem případě výstup vypadá takto:

$ python soubory.py
20019
b'# soubory.'
3, 4

Do souboru můžeš i něco napsat. Pozor, to co zapisuješ (a čteš) není Pythonní řetězec (text), ale bajty. Když se ale omezíš na anglickou abecedu, hlavní rozdíl mezi nimi je zápis s b na začátku.

Přidej za předchozí řádek tento kód, který do jiny.txt zapíše 4 písmenka.

os.write(fd2, b'abcd\n')

Nezapomeň na konec dát \n, nový řádek, aby se pak text hezky vypisoval. Když teď spustíš program v terminálu A a v terminálu B vypíšeš obsah souboru jiny.txt pomocí programu cat, měl by se ti zobrazit text "abcd".

$ cat jiny.txt 
abcd

Jak víš z kurzu Pythonu, otevřené soubory je dobré vždy na konci manipulace zavřít. Tak to pojď udělat: na konec, před time.sleep, dopiš:

os.close(fd1)
os.close(fd2)

Standardní deskriptory

Celou dobu pracuješ se soubory otevřenými pythonním programem, tedy 3 a 4. Určitě tě zajímá, co jsou vlastně soubory označení 0, 1, 2.

V textovém editoru smaž time.sleep a místo toho dopiš řádky:

os.write(1, b'Tohle jde do souboru 1\n')
os.write(2, b'Tohle jde do souboru 2\n')

Kam se to vypíše?

Řešení

Funguje to? Tak zkus přesměrovat výstup soubory.py do jiny.txt:

$ python soubory.py > jiny.txt
Tohle jde do souboru 2
$ cat jiny.txt 
Tohle jde do souboru 1
7167
3 4
b'## soubory'

Když se podíváš do souboru jiny.txt, najdeš v něm text Tohle jde do souboru 1.

Pokud to tam máš, připiš do programu ještě poslední řádek.

print(os.read(0, 10))

Spusť ho v terminálu A bez přesměrování.

$ python soubory.py

Tdyž teď něco napíšeš do terminálu, Python vypíše prvních 10 bajtů tvého textu.

Co se tady děje?

Tyto tři soubory, 0, 1 a 2, má každý proces otevřené.

0 je náš starý známý standardní vstup. Když nepřesměrováváš, je to terminál: ťe se to co napíšeš na klávesnici. Ale může to být i jiný soubor – třeba následující grep má pod číslem 0 otevřený soubor soubory.py:

grep print < soubory.py

1 je standardní výstup – místo, kam program píše informace, které chce předat světu.

Chybový výstup

2 je ale nové: je to standardní chybový výstup (angl. standard error stream, stderr), místo, kam programy píší chybové hlášky. Často to bývá stejný terminál jako stdout, ale přesměrovává se samostatně.

Zkus si třeba zkopírovat neexistující soubor pomocí cp. Dostaneš chybovou hlášku:

$ cp a b
cp: cannot stat a no such file or dir

Když přesměruješ standardní výstup do souboru, chybová hláška se přesto objeví v terminálu. Zkus:

$ cp a b > jiny.txt
cp: cannot stat a no such file or dir

Zobáček > přesměrovává pouze standardní výstup, zatímco chybový výstup nechá nepřesměrovaný.

A k čemu je to užitečné?

Na standardní výstup programy většinou píšou výsledky své práce v nějakém formátu, který se pak dá automaticky zpracovat. Když nastane chyba, program ji ohlásí, ale ohlásí ji na jiném místě, než kam vypíše výstup, aby nekazil další zpracování.

Když zadáš ps -A | grep ps, tak grep zpracovává výstup z příkazu ps:

$  ps -A | grep ps
    776 ?        00:00:00 psimon
    840 ?        00:00:00 cupsd
   5431 pts/1    00:00:00 ps

Když ale uděláš chybu a vynecháš pomlčku před A, tak se chyba vypíše na terminál a grep zpracuje jen prázdný soubor:

$ ps -A | grep top
[petr@fedora ~]$ ps A | grep top
error: unsupported option (BSD syntax)
(...)

Přesměrovat všechno

Vraťme se na chvíli k Pythonu.

V terminálu A spusť aktuální program.

$ python soubory.py
7305
3 4
b'## soubory'
Tohle jde do souboru 1
Tohle jde do souboru 2

Měly by se ti vypsat všechny tyto informace na terminál.

A co se stane, když přesměruješ výstup programu a pak zmáčkneš Ctrl+C?

$ python soubory.py > jiny.txt
Tohle jde do souboru 2
Traceback (most recent call last):
  File "soubory.py", line 21, in <module>
    print(os.read(0, 10))
KeyboardInterrupt

Výstup – všechno kromě chybového výstupu – je v souboru jiny.txt. Chybová hláška ale jde do chybového výstupu, tedy stále do terminálu.

Co kdybys ale přece jen chtěla přesměrovat ten druhý, chybový, výstup? Existuje na to speciální operátor 2> - tedy přesměrování souboru číslo 2.

$ python soubor.py 2> jiny.txt
7388
3 4
b'## soubory'
Tohle jde do souboru 1
^C
$ cat jiny.txt 
Tohle jde do souboru 2
Traceback (most recent call last):
  File "soubory.py", line 21, in <module>
    print(os.read(0, 10))
KeyboardInterrupt

Přesměrování obojího

Můžeš přesměrovat i oba výstupy:

$ python soubory.py > vystup.txt 2> chyby.txt
^C$ cat vystup.txt 
Tohle jde do souboru 1
7423
3 4
b'## soubory'
$ cat chyby.txt 
Tohle jde do souboru 2
Traceback (most recent call last):
  File "soubory.py", line 21, in <module>
    print(os.read(0, 10))
KeyboardInterrupt

Když přesměrováváš do různých souborů, tak nezáleží na pořadí > a 2>.

Když ale použiješ dvakrát stejný soubor (např. > jiny.txt 2> jiny.txt), narazíš na problém: v souboru se většinou objeví jen jeden z výsledků. Když je jeden soubor otevřený pro čtení dvakrát, a zapisuje se do obou deskriptorů zároveň, obvykle „vyhraje“ jen jeden.

Bash má na řešení této situace speciální operátor:

$python soubory.py > jiny.txt 2>&1
#                               ^--- chybový směruje tam, kam první

Kdyby v příkazu nebyl &, výstup se přesměruje do souboru s názvem 1. &1 ale „odkazuje“ na deskriptor 1, tedy jiny.txt.

Tady už záleží v jakém pořadí se skládají příkazy, protože tohle fungovat nebude:

#                  ,-------- 1. přesměruje stderr na stdout, tedy na terminál
#                  |    ,--- 2. přesměruje stdout do souboru
#                  ↓    ↓
$python soubory.py 2>&1 > jiny

Proto stderr půjde na terminál a stdout do souboru.

Přesměrování vstupu

Pro úplnost si ukážeme i přesměrování standardnho vstupu.

$ python soubory.py < jiny.txt
7481
3 4
b'## soubory'
Tohle jde do souboru 1
Tohle jde do souboru 2
b'abcd\n jde '

Poslední řádek je vstup načtený ze souboru jiny.txt. Podívej se na soubor 0 ve výpisu lsof – standardní vstup je nastaven na „opravdový“ soubor na disku.

$ lsof -p 7481
...
python  7481 user 0r REG 253,0 150 299463294 /home/user/bash/03/jiny.txt
...

Když data „přitečou“ rourou, bude standardní vstup vypadat ještě trochu jinak.

$ cat jiny.txt | python soubory.py
7485
3 4
b'## soubory'
Tohle jde do souboru 1
Tohle jde do souboru 2
b'abcd\n jde '

Podívej se na výpis lsof, na soubor číslo 0:

$ lsof -p 7485
...
python  7485 user 0r FIFO 0,12 0t0 1352745 pipe
...

Ono pipe znamená roura. Stejně jako terminál (např. /dev/pts/0) není obsah roury uložený na disku (data „proudí“ přímo z jednoho procesu do druhého). Na rozdíl od terminálu ale roura ani nemá jméno: nemůžeš udělat echo Ahoj > /dev/pts/0 jako u terminálu. S rourou může pracovat jen proces, který už ji má k dispozici. V tomto případě rouru vytvořil Bash, jeden její konec dal procesu cat a druhý procesu python.

Odbočka pro zvídavé

Přesměrování funguje i s jinými čísly než 2, a to i se vstupem. Například:

console $ python soubory.py 8< jiny.txt

Soubor jiny.txt se takto předá Pythonu na zpracování jako 8r (když se podíváš do lsof). Dá se pak přečíst např. pomocí os.read(8, 10). Jen čísla 0, 1, 2 mají určený význam, ostatní můžeš používat dle libosti. Jen pozor že open nebo os.open si nějaké číslo „zamluví“ pro sebe.

Zahození výstupu

Zvláštní případ použití je přesměrování některého výstupu do speciálního souboru, který si nic z toho co se do něj píše, neukládá. Jmenuje se /dev/null.

$ python soubory.py 2> /dev/null

Celý chybový výstup se tak „vyhodí“.

/dev/null není ani normální soubor, ani terminál, ani roura. Je to další druh speciálního souboru.

Jiný příklad - jsou programy, které píšou do terminálu spoustu výstupu. Třeba find. Pokud chceš vidět jen problémy, ale ne nalezené soubory, můžeš přesměrovat standardní výstup do /dev/null a v terminálu uvidíš jen chybová hlášení – tady o tom, že k některým souborům nemáš přístup:

$ find /var/cache > /dev/null
find: … Permision denied

Nebo naopak můžeš mít tolik souborů k nimž nemáš přístup, že se ti ani nebude chtít o tom číst; chceš jen dostat ty ke kterým přístup máš. Pak si do /dev/null přesměruješ chybový výstup:

$ find /var/cache 2> /dev/null

Toto je stránka lekce z kurzu, který probíhá nebo proběhl naživo s instruktorem.