: title : PB152 Operační systémy (cvičení) : authors : Petr Ročkai, František Bráblík, Adam Matoušek, Tomáš Pavlík : doctype : lnotes : typing : plain : toc : yes : lang : cs # A. Pravidla a organizace Tento dokument je sbírkou cvičení a komentovaných příkladů zdrojového kódu. Každá kapitola odpovídá jednomu týdnu semestru a tedy jednomu cvičení. Cvičení v prvním týdnu semestru („nulté“) je určeno k seznámení se s výukovým prostředím, studijními materiály a základními nástroji ekosystému. Každá část sbírky (zejména tedy všechny ukázky a příklady) jsou také k dispozici jako samostatné soubory, které můžete upravovat a spouštět. Této rozdělené verzi sbírky říkáme «zdrojový balík». Aktuální verzi¹ (ve všech variantách) můžete získat dvěma způsoby: 1. Ve «studijních materiálech²» předmětu v ISu – soubory PDF ve složce ‹text›, zdrojový balík ve složkách ‹00› (organizační informace), ‹01› až ‹12› (jednotlivé kapitoly = týdny semestru), dále ‹s1› až ‹s3› (sady úloh) a konečně ve složce ‹sol› vzorová řešení. Doporučujeme soubory stahovat dávkově pomocí volby „stáhnout jako ZIP“. 2. Po přihlášení na studentský server ‹aisa› (buď za pomoci ‹ssh› nebo ‹putty›) zadáním příkazu ‹pb152 update›. Všechny výše uvedené složky pak naleznete ve složce ‹~/pb152›. Tato kapitola (složka) dále obsahuje «závazná» pravidla a organizační pokyny. Než budete pokračovat, pozorně si je prosím přečtěte. Pro komunikaci s organizátory kurzu slouží «diskusní fórum» v ISu (více informací naleznete v části T.1). Nepište prosím organizátorům ani cvičícím maily ohledně předmětu, nejste-li k tomu specificky vyzváni. S žádostmi o výjimky ze studijních povinností, omluvenkami, atp., se obracejte vždy na studijní oddělení. ¹ Studijní materiály budeme tento semestr doplňovat průběžně, protože kurz prochází zásadní reorganizací. Než začnete pracovat na přípravách nebo příkladech ze sady, vždy se prosím ujistěte, že máte jejich aktuální verzi. Zadání příprav lze považovat za finální počínaje půlnocí na pondělí odpovídajícího týdne, sady podobně půlnocí na první pondělí odpovídajícího bloku. Pro první týden tedy 18.9.2023 0:00 a první sadu 25.9.2023 0:00. ² ‹https://is.muni.cz/auth/el/fi/podzim2023/PB152cv/um/› ## Přehled Tento předmět sestává z cvičení, sad domácích úloh a závěrečného praktického testu (kolokvia). Protože se jedná o „programovací“ předmět, většina práce v předmětu – a tedy i jeho hodnocení – se bude zaměřovat na praktické programování. Je důležité, abyste programovali co možná nejvíce, ideálně každý den, ale minimálně několikrát každý týden. K tomu Vám budou sloužit příklady v této sbírce (typicky se bude jednat o velmi malé programy v rozsahu jednotek až desítek řádků, kterých byste měli být v průměru schopni vyřešit několik za hodinu) a domácí úlohy, kterých budou za semestr 3 sady, a budou znatelně většího rozsahu (maximálně malé stovky řádků). V obou případech bude v průběhu semestru stoupat náročnost – je tedy důležité, abyste drželi krok a práci neodkládali na poslední chvíli. Protože programování je těžké, bude i tento kurz těžký – je zcela nezbytné vložit do něj odpovídající úsilí. Doufáme, že kurz úspěšně absolvujete, a co je důležitější, že se v něm toho naučíte co nejvíce. Je ale nutno podotknout, že i přes svou náročnost je tento kurz jen malým krokem na dlouhé cestě. ### Probíraná témata Předmět je rozdělen do 4 bloků (čtvrtý blok patří do zkouškového období). Do každého bloku v semestru patří 4 kapitoly (témata) a jim odpovídající 4 cvičení. │ bl. │ │ téma │ ├─────│────▻┼◅──────────────────────────────────────│ │ 1 │ 1. │ základy práce se soubory │ │ │ 2. │ sockety │ │ │ 3. │ čekání na událost │ │ │ 4. │ mapování souborů do paměti │ │┄┄┄┄┄│┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│ │ 2 │ 5. │ složky (adresáře) │ │ │ 6. │ síť │ │ │ 7. │ spustitelné soubory │ │ │ 8. │ správa popisovačů souborů │ │┄┄┄┄┄│┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄│ │ 3 │ 9. │ procesy │ │ │ 10. │ vlákna │ │ │ 11. │ synchronizace │ │ │ 12. │ opakování │ ### Organizace sbírky V následujících sekcích naleznete detailnější informace a «závazná» pravidla kurzu: doporučujeme Vám, abyste se s nimi důkladně seznámili.¹ Zbytek sbírky je pak rozdělen na části, které odpovídají jednotlivým týdnům semestru. «Důležité:» během prvního týdne semestru už budete řešit přípravy z první kapitoly, přestože první cvičení je ve až v týdnu druhém. Nulté cvičení je volitelné a není nijak hodnoceno. Kapitoly jsou číslovány podle témat z předchozí tabulky: ve druhém týdnu semestru se tedy «ve cvičení» budeme zabývat tématy, ke kterým jste v prvním týdnu vypracovali a odevzdali přípravy. ¹ Pravidla jsou velmi podobná těm v kurzu IB111, ale přesto si je pozorně přečtěte. ### Plán semestru Tento kurz vyžaduje značnou aktivitu během semestru. V této sekci naleznete přehled důležitých událostí formou kalendáře. Jednotlivé události jsou značeny takto (bližší informace ke každé naleznete v následujících odstavcích tohoto úvodu): • „#X“ – číslo týdne v semestru, • „cv0“ – tento týden běží „nulté“ cvičení (kapitola B), • „cv1“ – tento týden probíhají cvičení ke kapitole 1, • „X/v“ – mezivýsledek verity testů příprav ke kapitole X, • „X/p“ – poslední termín odevzdání příprav ke kapitole X, • „sX/Y“ – Yté kolo verity testů k sadě X, • „sX/z₁“ – první kolo známek za kvalitu kódu sady X, • „sX/op“ – termín pro opravná odevzdání sady X, • „sX/z₂“ – finální známky za kvalitu kódu sady X, • „test“ – termín programovacího testu. Nejdůležitější události jsou zvýrazněny: termíny odevzdání příprav a poslední termín odevzdání úloh ze sad (obojí vždy o 23:59 uvedeného dne). │ září │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │     │   Po  │   Út  │   St  │   Čt  │   Pá  │   So  │   Ne  │ ├────▻│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│ │ #1 │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ │ cv0 │   │   │   │ 01/v │   │«01/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #2 │ 25 │ 26 │ 27 │ 28 sv │ 29 │ 30 │   │ │ cv1 │ s1/1 │   │ s1/2 │ 02/v │ s1/3 │«02/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ říjen │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │     │   Po  │   Út  │   St  │   Čt  │   Pá  │   So  │   Ne  │ ├────▻│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│ │ #2 │   │   │   │   │   │   │ 1 │ │ │   │   │   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #3 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ │ │ s1/4 │   │ s1/5 │ 03/v │ s1/6 │«03/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #4 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │ 15 │ │ │ s1/7 │   │ s1/8 │ 04/v │ s1/9 │«04/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #5 │ 16 │ 17 │ 18 │ 19 │ 20 │ 21 │ 22 │ │ │ s1/10 │   │ s1/11 │ 05/v │«s1/12»│«05/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #6 │ 23 │ 24 │ 25 │ 26 │ 27 │ 28 sv │ 29 │ │ │ s2/1 │   │ s2/2 │ 06/v │ s2/3 │«06/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #7 │ 30 │ 31 │   │   │   │   │   │ │ │ s2/4 │ s1/z₁ │   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ listopad │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │     │   Po  │   Út  │   St  │   Čt  │   Pá  │   So  │   Ne  │ ├────▻│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│ │ #7 │   │   │ 1 │ 2 │ 3 │ 4 │ 5 │ │ │   │   │ s2/5 │ 07/v │ s2/6 │«07/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #8 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │ │ │ s2/7 │«s1/op»│ s2/8 │ 08/v │ s2/9 │«08/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #9 │ 13 │ 14 │ 15 │ 16 │ 17 sv │ 18 │ 19 │ │ │ s2/10 │ s1/z₂ │ s2/11 │ 09/v │«s2/12»│«09/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #10 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │ 26 │ │ │ s3/1 │   │ s3/2 │ 10/v │ s3/3 │«10/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #11 │ 27 │ 28 │ 29 │ 30 │   │   │   │ │ │ s3/4 │ s2/z₁ │ s3/5 │ 11/v │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ prosinec │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │     │   Po  │   Út  │   St  │   Čt  │   Pá  │   So  │   Ne  │ ├────▻│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│ │ #11 │   │   │   │   │ 1 │ 2 │ 3 │ │ │   │   │   │   │ s3/6 │«11/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #12 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ │ │ s3/7 │«s2/op»│ s3/8 │ 12/v │ s3/9 │«12/p» │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ #13 │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │ │ │ s3/10 │ s2/z₂ │ s3/11 │   │«s3/12»│   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ │ │   │   │   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │ │   │   │   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ leden │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │     │   Po  │   Út  │   St  │   Čt  │   Pá  │   So  │   Ne  │ ├────▻│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│ │ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ │ │   │ s3/z₁ │   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │ │ │   │«s3/op»│   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 15 │ 16 │ 17 │ 18 │ 19 │ 20 │ 21 │ │ │   │ s3/z₂ │ test │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 22 │ 23 │ 24 │ 25 │ 26 │ 27 │ 28 │ │ │   │   │ test │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 29 │ 30 │ 31 │   │   │   │   │ │ │   │   │ test │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ únor │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │     │   Po  │   Út  │   St  │   Čt  │   Pá  │   So  │   Ne  │ ├────▻│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│◅──────│ │ │   │   │   │ 1 │ 2 │ 3 │ 4 │ │ │   │   │   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ │ │   │   │ test │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ │ │ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │ │ │   │   │   │   │   │   │   │ │┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄│ ## Hodnocení Abyste předmět úspěšně ukončili, musíte v «každém bloku¹» získat «60 bodů». Žádné další požadavky nemáme. Body lze získat mnoha různými způsoby (přesnější podmínky naleznete v následujících sekcích této kapitoly). V blocích 1-3 (probíhají během semestru) jsou to: • za každou úspěšně odevzdanou přípravu «1» bod (max. 6 bodů každý týden, nebo «24/blok»), • za každou přípravu, která projde „verity“ testy navíc «0,5» bodu (max. 3 body každý týden, nebo «12/blok»), • za účast³ na cvičení získáte 3 body (max. tedy «12/blok»), • za aktivitu ve cvičení 3 body (max. tedy «12/blok»). Za přípravy a cvičení lze tedy získat teoretické maximum «60» bodů. Dále můžete získat: • «10» bodů za úspěšně vyřešený příklad ze sady domácích úloh (celkem vždy «60/blok»). V blocích 2-4 navíc můžete získat body za kvalitu řešení příkladů ze sady úloh předchozího bloku: • za kvalitu kódu max. «5» bodů za příklad (celkem «30/blok»). Konečně blok 4, který patří do zkouškového období, nemá ani cvičení ani sadu domácích úloh. Krom bodů za kvalitu kódu ze třetí sady lze získat: • «15» bodů za každý zkouškový příklad (celkem «90/blok»). Celkově tedy potřebujete: • blok 1: «60/120» bodů, • blok 2: «60/150» bodů, • blok 3: «60/150» bodů, • blok 4: «60/120» bodů (neplatí pro ukončení zápočtem). ¹ Máte-li předmět ukončen zápočtem, čtvrtý blok a tedy ani závěrečný test pro Vás není relevantní. Platí požadavek na 3×60 bodů z bloků v semestru. ³ V případě, že jste «řádně omluveni» v ISu, nebo Vaše cvičení «odpadlo» (např. padlo na státní svátek), můžete body za účast získat buď náhradou v jiné skupině (pro státní svátky dostanete instrukce mailem, individuální případy si domluvte s cvičícími obou dotčených skupin). Nemůžete-li účast nahradit takto, «domluvte se» se svým cvičícím (v tomto případě lze i mailem) na vypracování 3 rozšířených příkladů ze sbírky (přesné detaily Vám sdělí cvičící podle konkrétní situace). Neomluvenou neúčast lze nahrazovat «pouze» v jiné skupině a to max. 1–2× za semestr. ## Přípravy Jak již bylo zmíněno, chcete-li se naučit programovat, musíte programování věnovat nemalé množství času, a navíc musí být tento čas rozložen do delších období – semestr nelze v žádném případě doběhnout tím, že budete týden programovat 12 hodin denně, i když to možná pokryje potřebný počet hodin. Proto od Vás budeme chtít, abyste každý týden odevzdali několik vyřešených příkladů z této sbírky. Tento požadavek má ještě jeden důvod: chceme, abyste vždy v době cvičení už měli látku každý samostatně nastudovanou, abychom mohli řešit zajímavé problémy, nikoliv opakovat základní pojmy. Také Vás prosíme, abyste příklady, které plánujete odevzdat, řešili vždy samostatně: případnou zakázanou spolupráci budeme trestat (viz také konec této kapitoly). ### Odevzdání Každý příklad obsahuje základní sadu testů. To, že Vám tyto testy prochází, je jediné kritérium pro zisk základních bodů za odevzdání příprav. Poté, co příklady odevzdáte, budou «tytéž testy» na Vašem řešení automaticky spuštěny, a jejich výsledek Vám bude zapsán do poznámkového bloku. Smyslem tohoto opatření je zamezit případům, kdy omylem odevzdáte nesprávné, nebo jinak nevyhovující řešení, aniž byste o tom věděli. Velmi silně Vám proto doporučujeme odevzdávat s určitým předstihem, abyste případné nesrovnalosti měli ještě čas vyřešit. Krom základních („sanity“) testů pak ve čtvrtek o 23:59 a znovu v sobotu o 23:59 (těsně po konci odevzdávání) spustíme «rozšířenou» sadu testů („verity“). Za každý odevzdaný příklad, který splnil «základní» („sanity“) testy získáváte jeden bod. Za příklad, který navíc splnil «rozšířené» testy získáte dalšího 0,5 bodu (tzn. celkem 1,5 bodu). Výsledky testů naleznete v «poznámkovém bloku» v informačním systému. Příklady můžete odevzdávat: 1. do «odevzdávárny» s názvem ‹NN› v ISu (např. ‹01›), 2. příkazem ‹pb152 submit› ve složce ‹~/pb152/NN›. Podrobnější instrukce naleznete v kapitole T (technické informace, soubory ‹00/t*›). ### Harmonogram Termíny pro odevzdání příprav k jednotlivým kapitolám jsou shrnuty v následující tabulce: │ blok │ kapitola │ verity │ termín │ ├──────│──────────│─────────│─────────│ │ 1 │ 1 │ 21.9. │ 23.9. │ │ │ 2 │ 28.9. │ 30.9. │ │ │ 3 │ 5.10. │ 7.10. │ │ │ 4 │ 12.10. │ 14.10. │ │┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│ │ 2 │ 5 │ 19.10. │ 21.10. │ │ │ 6 │ 26.10. │ 28.10. │ │ │ 7 │ 2.11. │ 4.11. │ │ │ 8 │ 9.11. │ 11.11. │ │┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│ │ 3 │ 9 │ 16.11. │ 18.11. │ │ │ 10 │ 23.11. │ 25.11. │ │ │ 11 │ 30.11. │ 2.12. │ │ │ 12 │ 7.12. │ 9.12. │ ## Cvičení Těžiště tohoto předmětu je jednoznačně v samostatné domácí práci – učit se programovat znamená zejména hodně programovat. Společná cvičení sice nemohou tuto práci nahradit, mohou Vám ale přesto v lecčem pomoct. Smyslem cvičení je: 1. analyzovat problémy, na které jste při samostatné domácí práci narazili, a zejména prodiskutovat, jak je vyřešit, 2. řešit programátorské problémy společně (s cvičícím, ve dvojici, ve skupině) – nahlédnout jak o programech a programování uvažují ostatní a užitečné prvky si osvojit. Cvičení je rozděleno na dva podobně dlouhé segmenty, které odpovídají těmto bodům. První část probíhá přibližně takto: • cvičící vybere ty z Vámi odevzdaných příprav, které se mu zdají něčím zajímavé – ať už v pozitivním, nebo negativním smyslu, ◦ řešení bude «anonymně» promítat na plátno a u každého otevře diskusi o tom, čím je zajímavé; ◦ Vaším úkolem je aktivně se do této diskuse zapojit (můžete se například ptát proč je daná věc dobře nebo špatně a jak by se udělala lépe, vyjádřit svůj názor, odpovídat na dotazy cvičícího), ◦ k promítnutému řešení se můžete přihlásit a ostatním přiblížit, proč je napsané tak jak je, nebo klidně i rozporovat případnou kritiku (není to ale vůbec nutné), • dále podobným způsobem vybere vzájemné (peer) recenze, které jste v předchozím týdnu psali, a stručně je s Vámi prodiskutuje (celkovou strukturu recenze, proč je který komentář dobrý nebo nikoliv, jestli nějaký komentář chybí, atp.) – opět se můžete (resp. byste se měli) zapojovat, • na Vaši žádost lze ve cvičení analogicky probrat «neúšpěšná» řešení příkladů (a to jak příprav, tak příkladů z uzavřených sad). Druhá část cvičení je variabilnější, ale bude se vždy točit kolem bodů za aktivitu (každý týden můžete za aktivitu získat maximálně 3 body). Ve čtvrtém, osmém a dvanáctém týdnu proběhnou „vnitrosemestrálky“ kde budete řešit samostatně jeden příklad ze sbírky, bez možnosti hledat na internetu – tak, jak to bude na závěrečném testu; každé úspěšné řešení (tzn. takové, které splní verity testy) získá ony 3 body za aktivitu pro daný týden. V ostatních týdnech budete ve druhém segmentu kombinovat různé aktivity, které budou postavené na příkladech typu ‹r› z aktuální kapitoly (které konkrétní příklady budete ve cvičení řešit vybere cvičící, může ale samozřejmě vzít v potaz Vaše preference): 1. Můžete se přihlásit k řešení příkladu na plátně, kdy primárně vymýšlíte řešení Vy, ale zbytek třídy Vám bude podle potřeby radit, nebo se ptát co/jak/proč se v řešení děje. U jednodušších příkladů se od Vás bude také očekávat, že jako součást řešení doplníte testy. 2. Cvičící Vám může zadat práci ve dvojicích – první dvojice, která se dopracuje k funkčnímu řešení získá možnost své řešení předvést zbytku třídy – vysvětlit jak a proč funguje, odpovědět na případné dotazy, opravit chyby, které v řešení publikum najde, atp. – a získat tak body za aktivitu. Získané 3 body budou rozděleny rovným dílem mezi vítězné řešitele. 3. příklad můžete také řešit společně jako skupina – takto vymyšlený kód bude zapisovat cvičící (body za aktivitu se v tomto případě neudělují). ## Sady domácích úloh Ke každému bloku patří sada 6 domácích úloh, které tvoří významnou část hodnocení předmětu. Na úspěšné odevzdání každé domácí úlohy budete mít 12 pokusů rozložených do 4 týdnů odpovídajícího bloku cvičení. Odevzdávání bude otevřeno vždy v 0:00 prvního dne bloku (tzn. 24h před prvním spuštěním verity testů). Termíny odevzdání (vyhodnocení verity testů) jsou vždy v pondělí, středu a pátek v 23:59, dle následujícího harmonogramu: │ sada │ týden │ pondělí │ středa │ pátek │ ├──────│───────│────────▻┼────────▻┼────────▻┼ │ 1 │ 1 │ 25.9. │ 27.9. │ 29.9. │ │ │ 2 │ 2.10. │ 4.10. │ 6.10. │ │ │ 3 │ 9.10. │ 11.10. │ 13.10. │ │ │ 4 │ 16.10. │ 18.10. │ 20.10. │ │┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│ │ 2 │ 1 │ 23.10. │ 25.10. │ 27.10. │ │ │ 2 │ 30.10. │ 1.11. │ 3.11. │ │ │ 3 │ 6.11. │ 8.11. │ 10.11. │ │ │ 4 │ 13.11. │ 15.11. │ 17.11. │ │┄┄┄┄┄┄│┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│┄┄┄┄┄┄┄┄┄│ │ 3 │ 1 │ 20.11. │ 22.11. │ 24.11. │ │ │ 2 │ 27.11. │ 29.11. │ 1.12. │ │ │ 3 │ 4.12. │ 6.12. │ 8.12. │ │ │ 4 │ 11.12. │ 13.12. │ 15.12. │ ### Odevzdávání Součástí každého zadání je jeden zdrojový soubor (kostra), do kterého své řešení vepíšete. Vypracované příklady lze pak odevzdávat stejně jako přípravy: 1. do «odevzdávárny» s názvem ‹sN_úkol› v ISu (např. ‹s1_a_queens›), 2. příkazem ‹pb152 submit sN_úkol› ve složce ‹~/pb152/sN›, např. ‹pb152 submit s1_a_queens›. Podrobnější instrukce naleznete opět v kapitole T. ### Vyhodnocení Vyhodnocení Vašich řešení probíhá ve třech fázích, a s každou z nich je spjata sada automatických testů. Tyto sady jsou: • „syntax“ – kontroluje, že odevzdaný program je syntakticky správně, lze jej přeložit a prochází základními statickými kontrolami, • „sanity“ – kontroluje, že odevzdaný program se chová „rozumně“ na jednoduchých případech vstupu; tyto testy jsou rozsahem a stylem podobné těm, které máte přiložené k příkladům ve cvičení, • „verity“ – důkladně kontrolují správnost řešení, včetně složitých vstupů a okrajových případů a kontroly paměťových chyb. Fáze na sebe navazují v tom smyslu, že nesplníte-li testy v některé fázi, žádná další se už (pro dané odevzdání) nespustí. Pro splnění domácí úlohy je klíčová fáze „verity“, za kterou jsou Vám uděleny body. Časový plán vyhodnocení fází je následovný: • kontrola „syntax“ se provede obratem (do cca 5 minut od odevzdání), • kontrola „sanity“ každých 6 hodin počínaje půlnocí (tzn. 0:00, 6:00, 12:00, 18:00), • kontrola „verity“ se provede v pondělí, středu a pátek ve 23:59 (dle tabulky uvedené výše). Vyhodnoceno je vždy pouze nejnovější odevzdání, a každé odevzdání je vyhodnoceno v každé fázi nejvýše jednou. Výsledky naleznete v poznámkových blocích v ISu (každá úloha v samostatném bloku), případně je získáte příkazem ‹pb152 status›. ### Bodování Za každý domácí úkol, ve kterém Vaše odevzdání v příslušném termínu splní testy „verity“, získáte 10 bodů. Za stejný úkol máte dále možnost získat body za kvalitu kódu, a to vždy v hodnotě max. 5 bodů. Body za kvalitu se počítají v bloku, «ve kterém byly uděleny», tzn. body za kvalitu ze «sady 1» se započtou do «bloku 2». Maximální bodový zisk za jednotlivé sady: • sada 1: 60 za funkčnost v bloku 1 + 30 za kvalitu v bloku 2, • sada 2: 60 za funkčnost v bloku 2 + 30 za kvalitu v bloku 3, • sada 3: 60 za funkčnost v bloku 3 + 30 za kvalitu v bloku 4 («zkouškovém»). ### Hodnocení kvality kódu Automatické testy ověřují «správnost» vašich programů (do takové míry, jak je to praktické – ani nejpřísnější testy nemůžou zaručit, že máte program zcela správně). Správnost ale není jediné kritérium, podle kterého lze programy hodnotit: podobně důležité je, aby byl program «čitelný». Programy totiž mimo jiné slouží ke komunikaci myšlenek lidem – dobře napsaný a správně okomentovaný kód by měl čtenáři sdělit, jaký řeší problém, jak toto řešení funguje a u obojího objasnit «proč». Je Vám asi jasné, že čitelnost programu člověkem může hodnotit pouze člověk: proto si každý Váš «úspěšně» vyřešený domácí úkol přečte opravující a své postřehy Vám sdělí. Přitom zároveň Váš kód oznámkuje podle kritérií podrobněji rozepsaných v kapitole Z. Tato kritéria aplikujeme při známkování takto: • hodnocení A dostane takové řešení, které jasně popisuje řešení zadaného problému, je správně dekomponované na podproblémy, je zapsáno bez zbytečného opakování, a používá správné abstrakce, algoritmy a datové struktury, • hodnocení B dostane program, který má výrazné nedostatky v jedné, nebo nezanedbatelné nedostatky ve dvou oblastech výše zmíněných, například: ◦ je relativně dobře dekomponovaný a zbytečně se neopakuje, ale používá nevhodný algoritmus nebo datovou strukturu a není zapsán příliš přehledně, ◦ používá optimální algoritmus a datové struktury a je dobře dekomponovaný, ale lokálně opakuje tentýž kód s drobnými obměnami, a občas používá zavádějící nebo jinak nevhodná jména podprogramů, proměnných atp., ◦ jinak dobrý program, který používá zcela nevhodný algoritmus, «nebo» velmi špatně pojmenované proměnné, «nebo» je zapsaný na dvě obrazovky úplně bez dekompozice, • hodnocení X dostanou programy, u kterých jste se dobrovolně vzdali hodnocení (a to jasně formulovaným komentářem «na začátku souboru», např. „Vzdávám se hodnocení.“), • hodnocení C dostanou všechny ostatní programy, zejména ty, které kombinují dvě a více výrazné chyby zmiňované výše. Známky Vám budou zapsány druhé úterý následujícího bloku. Dostanete-li známku B nebo C, budete mít možnost svoje řešení ještě zlepšit, odevzdat znovu, a známku si tak opravit: • na opravu budete mít týden, • na opraveném programu nesmí selhat verity testy, • testy budou nadále probíhat se stejnou kadencí jako během řádné doby k vypracování (pondělí, středa, pátek o 23:59). Bude-li opravující s vylepšeným programem spokojen, výslednou známku Vám upraví. │ sada │ řádný termín │ známka │ opravný termín │ známka │ ├──────│─────────────▻│───────▻│───────────────▻│───────▻│ │ 1 │ «20.10.» │ 31.10. │ «7.11.» │ 14.11. │ │ 2 │ «17.11.» │ 28.11. │ «5.12.» │ 12.12. │ │ 3 │ «15.12.» │ 2.1. │ «9.1.» │ 16.1. │ Jednotlivé «výsledné» známky se promítnou do bodového hodnocení úkolu následovně: • známka «A» Vám vynese «5 bodů», • známka «B» pak «2 body», • známka «X» žádné body neskýtá, • známka «C» je hodnocena «-1 bodem». Samotné body za funkcionalitu se při opravě kvality již nijak nemění. ### Neúspěšná řešení Příklady, které se Vám nepodaří vyřešit kompletně (tzn. tak, aby na nich uspěla kontrola „verity“) nebudeme hodnotit. Nicméně může nastat situace, kdy byste potřebovali na „téměř hotové“ řešení zpětnou vazbu, např. proto, že se Vám nepodařilo zjistit, proč nefunguje. Taková řešení můžou být předmětem společné analýzy ve cvičení, v podobném duchu jako probíhá rozprava kolem odevzdaných příprav (samozřejmě až poté, co pro danou sadu skončí odevzdávání). Máte-li zájem takto rozebrat své řešení, domluvte se, ideálně s předstihem, se svým cvičícím. To, že jste autorem, zůstává mezi cvičícím a Vámi – Vaši spolužáci to nemusí vědět (ke kódu se samozřejmě můžete v rámci debaty přihlásit, uznáte-li to za vhodné). Stejná pravidla platí také pro nedořešené přípravy (musíte je ale odevzdat). Tento mechanismus je omezen prostorem ve cvičení – nemůžeme zaručit, že v případě velkého zájmu dojde na všechny (v takovém případě cvičící vybere ta řešení, která bude považovat za přínosnější pro skupinu – je tedy možné, že i když se na Vaše konkrétní řešení nedostane, budete ve cvičení analyzovat podobný problém v řešení někoho jiného). ## Vzájemné recenze Jednou z možností, jak získat body za aktivitu, jsou vzájemné (peer) recenze. Smyslem této aktivity je získat praxi ve čtení a hodnocení cizího kódu. Možnost psát tyto recenze se váže na vlastní úspěšné vypracování téhož příkladu. Příklad: odevzdáte-li ve druhém týdnu 4 přípravy, z toho u třech splníte testy „verity“ (řekněme ‹p1›, ‹p2›, ‹p5›), ve třetím týdnu dostanete po jednom řešení těchto příkladů (tzn. budete mít možnost recenzovat po jedné instanci ‹02/p1›, ‹02/p2› a ‹02/p5›). Termín pro odevzdání recenzí na přípravy z druhé kapitoly je shodný s termínem pro odevzdání příprav třetí kapitoly (tzn. sobotní půlnoc). Vypracování těchto recenzí je dobrovolné. Za každou vypracovanou recenzi získáte jeden bod za aktivitu, počítaný v týdnu, kdy jste recenze psali (v uvedeném příkladu by to tedy bylo ve třetím týdnu semestru, tedy do stejné „kolonky“ jako body za příklady ‹02/r›). Udělení bodů je podmíněno smysluplným obsahem – «nestačí» napsat „nemám co dodat“ nebo „není zde co komentovat“. Je-li řešení dobré, napište «proč» je dobré (viz též níže). Vámi odevzdané recenze si přečte Váš cvičící a některé z nich může vybrat k diskusi ve cvičení (v dalším týdnu), v podobném duchu jako přípravy samotné. «Pozor», v jednom týdnu lze získat maximálně «3 body» za aktivitu, bez ohledu na jejich zdroj (recenze, vypracování příkladu u tabule, atp.). Toto omezení není dotčeno ani v případě, kdy dostanete k vypracování více než 3 příklady (můžete si ale vybrat, které z nich chcete recenzovat). ### Jak recenze psát Jak recenze vyzvednout a odevzdat je blíže popsáno v kapitole T. Své komentáře vkládejte přímo do vyzvednutých zdrojových souborů. Komentáře můžete psát česky (slovensky) nebo anglicky, volba je na Vás. Komentáře by měly být stručné, ale užitečné – Vaším hlavním cílem by mělo být pomoct adresátovi naučit se lépe programovat. Snažte se aplikovat kritéria a doporučení z předchozí sekce (nejlépe na ně přímo odkázat, např. „tuto proměnnou by šlo jistě pojmenovat lépe (viz doporučení 2.b)“). Nebojte se ani vyzvednout pozitiva (můžete zde také odkázat doporučení, máte-li například za to, že je obzvlášť pěkně uplatněné) nebo poznamenat, když jste se při čtení kódu sami něco naučili. Komentáře vkládejte vždy «před» komentovaný celek, a držte se podle možnosti tohoto vzoru (použití ‹**› pomáhá odlišit původní komentáře autora od poznámek recenzenta): /** A short, one-line remark. **/ U víceřádkových komentářů: /** A longer comment, which should be wrapped to 80 columns or ** less, and where each line should start with the ** marker. ** It is okay to end the comment on the last line of text like ** this. **/ Při vkládání komentářů «neměňte» existující řádky (zejména se ujistěte, že máte vypnuté automatické formátování, editujete-li zdrojový kód v nějakém IDE). Jediné povolená operace jsou: • vložení nových řádků (prázdných nebo s komentářem), nebo • doplnění komentáře na stávající «prázdný» řádek. ## Závěrečný programovací test Zkouškové období tvoří pomyslný 4. blok a platí zde stejné kritérium jako pro všechny ostatní bloky: musíte získat alespoň 60 bodů. Závěrečný test: • proběhne v počítačové učebně bez přístupu k internetu nebo vlastním materiálům, • k dispozici bude tato sbírka (bez vzorových řešení příkladů typu ‹e› a ‹r›), skripta PB152 a manuálové stránky (příkaz ‹man›), • budete moct používat textový editor nebo vývojové prostředí VS Code, překladač jazyka C (‹gcc› a/nebo ‹clang›) a nástroje ‹valgrind› a ‹gdb›. Na vypracování praktické části budete mít 4 hodiny čistého času, a bude sestávat ze šesti příkladů, které budou hodnoceny automatickými testy, s maximálním ziskem 90 bodů. Příklady jsou hodnoceny binárně (tzn. příklad je uznán za plný počet bodů, nebo uznán není). Kvalita kódu hodnocena nebude. Příklady budou na stejné úrovni obtížnosti jako příklady typu ‹p›/‹r›/‹v› ze sbírky. Během zkoušky můžete kdykoliv odevzdat (na počet odevzdání není žádný konkrétní limit) a vždy dostanete zpět výsledek testů syntaxe a sanity. Součástí zadání bude navíc soubor ‹tokens.txt›, kde naleznete 4 kódy. Každý z nich lze použít nejvýše jednou (vložením do komentáře do jednoho z příkladů), a každé použití kódu odhalí výsledek verity testu pro ten soubor, do kterého byl vložen. Toto se projeví pouze při prvním odevzdání s vloženým kódem, v dalších odevzdáních bude tento kód ignorován (bez ohledu na soubor, do kterého bude vložen). Zkouška proběhne až po vyhodnocení recenzí za třetí blok (tzn. ve druhé polovině zkouškového období). Plánované termíny¹ jsou tyto: • středa 24.1. 9:00–13:00, 14:00–18:00, • středa 31.1. 9:00–13:00, 14:00–18:00, • středa 7.2. 9:00–13:00, 14:00–18:00, • středa 14.2. 9:00–13:00, 14:00–18:00. ### Vnitrosemestrálky V posledním týdnu každého bloku, tedy • týden 4 (16.-20. října), • týdnu 8 (13.–17. listopadu), • týden 12 (11.-15. prosince), proběhne v rámci cvičení programovací test na 40 minut. Tyto testy budou probíhat za stejných podmínek, jako výše popsaný závěrečný test (slouží tedy mimo jiné jako příprava na něj). Řešit budete vždy ale pouze jeden příklad, za který můžete získat 3 body, které se počítají jako body za aktivitu v tomto cvičení. ¹ Může se stát, že termíny budeme z technických nebo organizačních důvodů posunout na jiný den nebo hodinu. V takovém případě Vám samozřejmě změnu s dostatečným předstihem oznámíme. ## Opisování Na všech zadaných problémech pracujte prosím zcela samostatně – toto se týká jak příkladů ze sbírky, které budete odevzdávat, tak domácích úloh ze sad. To samozřejmě neznamená, že Vám zakazujeme společně studovat a vzájemně si pomáhat látku pochopit: k tomuto účelu můžete využít všechny zbývající příklady ve sbírce (tedy ty, které nebude ani jeden z Vás odevzdávat), a samozřejmě nepřeberné množství příkladů a cvičení, které jsou k dispozici online. Příklady, které odevzdáváte, slouží ke kontrole, že látce skutečně rozumíte, a že dokážete nastudované principy prakticky aplikovat. Tato kontrola je pro Váš pokrok naprosto klíčová – je velice snadné získat pasivním studiem (čtením, posloucháním přednášek, studiem již vypracovaných příkladů) pocit, že něčemu rozumíte. Dokud ale sami nenapíšete na dané téma několik programů, jedná se pravděpodobně skutečně pouze o pocit. Abyste nebyli ve zbytečném pokušení kontroly obcházet, nedovolenou spolupráci budeme relativně přísně trestat. Za každý prohřešek Vám bude strženo «v každé instanci» (jeden týden příprav se počítá jako jedna instance, příklady ze sad se počítají každý samostatně): • 1/2 bodů získaných (ze všech příprav v dotčeném týdnu, nebo za jednotlivý příklad ze sady), • 10 bodů z hodnocení bloku, do kterého opsaný příklad patří, • 10 bodů (navíc k předchozím 10) z celkového hodnocení. Opíšete-li tedy například 2 přípravy ve druhém týdnu a: • Váš celkový zisk za přípravy v tomto týdnu je 4,5 bodu, • Váš celkový zisk za první blok je 65 bodů, jste «automaticky hodnoceni známkou X» (65 - 2,25 - 10 je méně než potřebných 60 bodů). Podobně s příkladem z první sady (65 - 5 - 10), atd. Máte-li v bloku bodů dostatek (např. 80 - 5 - 10 > 60), ve studiu předmětu pokračujete, ale započte se Vám ještě navíc penalizace 10 bodů do celkové známky. Přestává pro Vás proto platit pravidlo, že 4 splněné bloky jsou automaticky E nebo lepší. V situaci, kdy: • za bloky máte před penalizací 77, 62, 61, 64, • v prvním bloku jste opsali domácí úkol, budete penalizováni: • v prvním bloku 10 + 5, tzn. bodové zisky za bloky budou efektivně 62, 62, 61, 64, • v celkovém hodnocení 10, tzn. celkový zisk 62 + 62 + 61 + 64 - 10 = 239, a budete tedy hodnoceni známkou «F». To, jestli jste příklad řešili společně, nebo jej někdo vyřešil samostatně, a poté poskytl své řešení někomu dalšímu, není pro účely kontroly opisování důležité. Všechny „verze“ řešení odvozené ze společného základu budou penalizovány stejně. Taktéž «zveřejnění řešení» budeme chápat jako pokus o podvod, a budeme jej trestat, bez ohledu na to, jestli někdo stejné řešení odevzdá, nebo nikoliv. Podotýkáme ještě, že kontrola opisování «nespadá» do desetidenní lhůty pro hodnocení průběžných kontrol. Budeme se sice snažit opisování kontrolovat co nejdříve, ale odevzdáte-li opsaný příklad, můžete být bodově penalizováni kdykoliv (tedy i dodatečně, a to až do konce zkouškového období). # B. Úvod Tato kapitola je předmětem cvičení v prvním týdnu semestru. V tomto cvičení se seznámíte s organizací cvičení, se studijními materiály (tedy zejména touto sbírkou) a také si připomenete základy práce v prostředí POSIX a v jazyce C. Detailněji se pak budeme zabývat základní strukturou příkladů (definice, hlavičkové soubory, procedura ‹main›, pomocné testovací funkce, atp.), vztahem tohoto předmětu ke standardu POSIX a v neposlední řadě obecnými principy ošetření různých chybových stavů v programech. ## Normy Kurz je celý veden v programovacím jazyce C99, specifikovaném normou ISO. Krom samotného jazyka C budeme používat rozhraní operačního systému v podobě, kterou specifikuje norma POSIX, a několik málo vybraných rozšíření (zejména právě v souvislosti s ošetřením chyb – drobné odchýlení od standardu nám totiž ušetří značné množství psaní). Není-li daná konstrukce nebo knihovní funkce specifikována normou «ISO 9899:1999¹» (ISO C99; «HTML verze²»), normou «IEEE Std 1003.1-2017³» (POSIX), ani explicitně zmíněna v této sbírce jako podporovaná nad rámec těchto standardů, «nepoužívejte ji». Řešení, která na takové konstrukce spoléhají, se nemusí po odevzdání přeložit, nebo se v horším případě mohou chovat jinak, než na Vašem systému (nebo i jinak, než na serveru ‹aisa›). Nevíte-li jistě, zda je v tomto směru Vaše řešení v pořádku, odevzdávejte vždy s dostatečným předstihem. Nevíte-li, je-li nějaká funkcionalita, kterou byste chtěli použít, součástí zmiňovaných standardů, a jejich čtení je nad Vaše síly, zeptejte se v diskusním fóru, nebo na cvičení. Řešení příkladů z této sbírky nebude nikdy vyžadovat funkce, konstrukce, atp., které se neobjevují ani v ukázkách, ani v úvodech kapitol, ani nejsou explicitně zmíněné v zadání daného příkladu. ¹ ‹http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf› ² ‹http://port70.net/~nsz/c/c99/n1256.html› ³ ‹https://pubs.opengroup.org/onlinepubs/9699919799/› ## Manuálové stránky Každá knihovní funkce z norem POSIX a ISO C99 je zdokumentovaná systémem ‹man›, který je nainstalovaný na většině POSIX-kompatibilních systémů (server ‹aisa› nevyjímaje). Příkaz ‹man› lze použít těmito způsoby: • ‹man 2 read› zobrazí dokumentaci «lokální» verze systémového volání ‹read› (za ‹read› lze dosadit libovolné systémové volání) – pozor, tato lokální verze může mít oproti POSIX-u nepřenositelná rozšíření (většinou Vás na to ale manuálová stránka explicitně upozorní), • ‹man 3 dprintf› zobrazí dokumentaci «lokální» verze knihovní funkce ‹dprintf› (opět lze dosadit libovolnou jinou knihovní funkci, např. ‹memcmp›), • ‹man 3p read› zobrazí příslušnou část normy POSIX⁴ – na zde popsané chování se můžete spolehnout. Samozřejmě krom příkazu ‹man› můžete použít výše odkazované normy samotné. Dalším poměrně spolehlivým zdrojem informací o jazyku C je online referenční příručka «cppreference⁵». ⁴ Norma POSIX zahrnuje jako svou část popis knihovních funkcí specifikovaných normou ISO «C89» (nikoliv novější «C99» která je závazná pro tento kurz). Autorům této sbírky není známo, že by mezi těmito normami existoval (v částech sdílených s normou POSIX) pro tento předmět relevantní konflikt. Můžete se tedy odvolávat na normu POSIX i u funkcí, které patří do průniku s ISO C. Narazíte-li ovšem při studiu literatury na nějaké relevantní rozdíly, budeme rádi, když se s námi o tento nález podělíte v diskusním fóru. ⁵ ‹https://en.cppreference.com/w/c› ## Struktura programu Každý příklad, který budete v tomto předmětu řešit, je tvořen jedním zdrojovým souborem v jazyce C. Součástí kostry je text zadání, případný pomocný kód, který by Vám měl řešení ulehčit, procedura ‹main›, která obsahuje základní sadu testů a případné pomocné testovací funkce. Vaším úkolem je do této kostry doplnit kód tak, aby výsledný celek splňoval text zadání. Hlavním úkolem přiložených testů je usnadnit Vám dosažení tohoto cíle. O hlavních částech programu se více dovíte v ukázkách a samotných příkladech. Programy, které budete psát budou dvou hlavních typů – může se jednat o znovupoužitelné („knihovní“) funkce se zadaným rozhraním, nebo o de-facto kompletní programy. Z povahy zadaného programu bude obvykle zřejmé, o který typ úlohy se jedná (je-li např. vstupem popisovač otevřeného souboru, nebo nějaká datová struktura, těžko se může jednat o kompletní program). Trochu atypicky, i v případech, kdy píšete de-facto kompletní program, vstupní bod tohoto programu bude nějaká námi zadaná procedura (např. v přípravě ‹02/p5_echod› to je procedura s názvem ‹echod›), nikoliv speciální procedura ‹main›. Tuto formu jsme zvolili zejména pro jednoduchost testování – dodané testy vytvoří pro Váš program nový proces (použitím systémového volání ‹fork›) a chování tohoto programu pak vyhodnotí „zvenčí“ z rodičovského procesu. Vám tím zejména odpadne nutnost spouštět různé pomocné skripty, které by hotový program testovaly, nebo provádět testovací scénáře „ručně“. Spuštěním přeloženého programu bez parametrů se tento přímo otestuje. U tohoto typu programů ale dodaná procedura ‹main› umožní i jejich přímé spuštění, pro případy, kdy si je budete chtít vyzkoušet obvyklým způsobem (jak tohoto dosáhnout zjistíte v komentáři na začátku dodané procedury ‹main›). ## Ošetření chyb Od typu programu nebo podprogramu se bude typicky odvíjet i způsob, jakým se vypořádáme s chybovými stavy. Základní pravidlo systémového programování je, že «každé systémové volání může selhat» a totéž platí pro každý podprogram (ať už knihovní nebo Váš vlastní), který vnitřně nějaké systémové volání používá (ať už přímo, nebo nepřímo). Chyby obecně dělíme na «fatální» a «opravitelné». Toto dělení je ovšem silně kontextově závislé – tatáž chyba může být v některých případech fatální, ale v jiných opravitelná. Fatální chyba je taková, po které nemá smysl pokračovat ve vykonávání programu a tento je lepší rovnou ukončit. Je samozřejmě žádoucí uživateli tuto chybu ještě před ukončením programu co nejpřesněji popsat. Opravitelné chyby jsou pak takové, kdy může program ve své činnosti pokračovat. Zde do hry vstupuje onen rozdíl mezi kompletním programem a neúplným (znovupoužitelným) podprogramem. Máme-li pod kontrolou celý program, máme automaticky k dispozici mnohem přesnější informaci o tom, v jakém kontextu daná chyba nastala, a můžeme se k ní tedy mnohem snadněji postavit jako k chybě fatální. Ve znovupoužitelném podprogramu je situace komplikovanější – ukončíme-li při nějaké chybě celý program, omezíme tím možnosti použití tohoto podprogramu v situaci, kdy je tato reakce nežádoucí. Na druhé straně správně se vypořádat s opravitelnou chybou je mnohem náročnější – program jako celek se musí z chyby zotavit. Dotčený podprogram obvykle nemůže svůj úkol úspěšně dokončit; musí ale: 1. vrátit jakékoliv už provedené efekty, které by mohly mít negativní dopad na další fungování programu – zejména musí uvolnit veškeré již alokované zdroje a zajistit platnost relevantních invariantů, 2. chybu ohlásit volajícímu podprogramu – nikoliv přímo uživateli – způsobem, který umožní zbytku programu na chybu vhodně reagovat. Při dekompozici programu na podprogramy musíme vždy zvážit, které chyby považovat za fatální a které nikoliv. Od tohoto rozhodnutí se pak odvíjí jak jednoduchost zápisu (považovat chybu za fatální je jednodušší) tak i praktická znovupoužitelnost navrženého podprogramu (podprogram, který při sebemenším problému ukončí program, není příliš znovupoužitelný). ## d. Demonstrace (ukázky) ### 1. [‹empty›] Tento program nic nedělá, slouží pouze jako ukázka základní struktury. Ve všech programech v tomto předmětu budeme deklarovat, že jsou napsané s ohledem na normu POSIX.1–2017. K tomu slouží následovná direktiva preprocesoru: #define _POSIX_C_SOURCE 200809L /* C */ Relevantní informace z kapitoly 2 standardu POSIX: > A POSIX-conforming application shall ensure that the feature > test macro _POSIX_C_SOURCE is defined before inclusion of any > header. > > When an application includes a header described by > POSIX.1-2017, and when this feature test macro is defined to > have the value ‹200809L›: All symbols required by POSIX.1-2017 > to appear when the header is included shall be made visible. Výše uvedená direktiva ‹#define› není jediný způsob, jak tomuto požadavku dostát, ale je pro naše účely nejjednodušší. Musí vždy stát před jakoukoliv direktivou ‹#include›. Obvykle následují direktivy ‹#include›, které odkazují tzv. systémové hlavičkové soubory. V těchto souborech jsou deklarovány podprogramy, které poskytuje implementace jazyka C a operační systém. Které hlavičkové soubory je pro použití daného podprogramu vložit je vždy popsáno v odpovídající manuálové stránce (a také v příslušných normách). V ukázkách a kostrách budeme u každého ‹#include› v komentáři uvádět podprogramy, případně konstanty, které z dané hlavičky hodláme využívat. #include /* exit */ /* C */ Na tomto místě bude ve většině příkladů popsaný podprogram, napsání kterého je Vaším úkolem. V této ukázce zde není nic. Konečně bude součástí kostry procedura ‹main›, která představuje vstupní bod výsledného programu. Jejím obvyklým úkolem bude otestovat podprogram, který jste výše naprogramovali. V této ukázce nedělá nic, pouze explicitně ukončí program. int main() /* demo */ /* C */ { Procedura ‹exit› ukončí vykonávání programu. Má stejný efekt, jako návrat z procedury ‹main›, ale lze ji použít na libovolném místě. V tomto kurzu ji prakticky nikdy nebudete mít důvod použít (může se ale objevit v testech, proto je dobré ji znát). exit( 0 ); /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹err›] V této ukázce se podíváme na to, jak se lze vypořádat s fatálními chybami, zejména těmi, které jsou zaviněny selháním systémového volání. Pro tento účel budeme používat «nestandardní» rozšíření v podobě hlavičkového souboru ‹err.h›. Tento kompromis volíme ze dvou důvodů: 1. jedná se o velmi široce podporované rozšíření a není jednoduché nalézt POSIX-kompatibilní operační systém, který by je neposkytoval, 2. definice funkcí ‹err›, ‹errx›, ‹warn› a ‹warnx›, které budeme v tomto předmětu používat, se vejdou na cca 50 řádků, takže v případě potřeby je není problém do programů doplnit (zajímají-li Vás možné definice, naleznete je v poslední ukázce této kapitoly). #include /* err, errx, warn, warnx */ /* C */ #include /* dprintf */ #include /* STDOUT_FILENO */ Tento program nedělá nic jiného, než že na standardní výstup zapíše řetězec. Podle toho, jak ho spustíme, může i tato velmi jednoduchá operace selhat. Podrobněji se základy vstupu a výstupu budeme zabývat v první kapitole. int main( void ) /* demo */ /* C */ { Pro vypořádání se s chybou, která není fatální, ale je žádoucí na ni upozornit uživatele, budeme obvykle používat proceduru ‹warn›, která vypíše chybové hlášení ale pokračuje ve vykonávání programu. Spustíte-li si tento program např. příkazem, dostanete výpis, který krom samotné chyby vypíše: 1. jméno programu (toto je užitečné zejména ve chvíli, kdy uživatel spustil několik programů najednou, třeba spojených rourou) 2. popis poslední «systémové» chyby – výčet možných chyb naleznete např. v manuálové stránce ‹man 7p errno›. Příklad: $ ./d3_err > /dev/full d3_err: write to stdout failed: No space left on device d3_err: write to stdout failed: No space left on device $ echo $? 1 Všimněte si zejména poslední části chybového výpisu, kde je uvedeno jakým způsobem poslední systémové volání selhalo. Je ovšem naším úkolem dodat uživateli dostatečný kontext, aby mohl tuto část chybového výpisu interpretovat (je například dobré v první části chyby zmínit s jakým souborem se pracovalo). if ( dprintf( STDOUT_FILENO, "foo\n" ) == -1 ) /* C */ warn( "write to stdout failed" ); Procedura ‹err› pracuje podobně, ale program zároveň ukončí, a to s návratovým kódem, který jí byl předán v prvním parametru. Varianty ‹warnx› a ‹errx› se liší tím, že nevypisují poslední část, tzn. popis systémové chyby. Můžeme je tedy použít v situaci, kdy chyba nepochází ze systémového volání. if ( dprintf( STDOUT_FILENO, "bar\n" ) == -1 ) /* C */ err( 1, "write to stdout failed" ); return 0; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 4. [‹fail›] Zde si ukážeme několik technik, které nám pomohou vypořádat se s opravitelnými chybami. Připomínáme, že za opravitelné považujeme chyby, po kterých může program jako celek smysluplně pokračovat ve své činnosti. V tomto předmětu to bude zejména v případech, kdy budeme psát znovupoužitelné podprogramy. Nastane-li opravitelná chyba, snažíme se dotčený podprogram ukončit tak, aby: 1. nedošlo k úniku zdrojů, tzn. každý zdroj, který podprogram vyžádal, musí být vrácen, nebo se musí stát součástí stavu programu (a tedy bude vrácen později, v rámci standardního – nechybového – úklidu), 2. byly zachovány veškeré požadované invarianty, na které mohl mít podprogram vliv – v ideálním případě je chování podprogramu „všechno anebo nic“, tzn. pokud podprogram neuspěl, nebude mít žádný pozorovatelný efekt,¹ 3. volající byl o tomto selhání co možná nejpodrobněji informován. ¹ Toto je samozřejmě v praxi těžké stoprocentně zajistit, nicméně čím blíže se tomuto ideálu dokážeme přiblížit, tím snazší bude daný podprogram použít v nějakém větším celku. #define _POSIX_C_SOURCE 200809L /* C */ #include /* read, write */ #include /* openat */ #include /* warn */ #include /* errno */ V této ukázce trochu předběhneme učivo a naprogramujeme si jednoduchou proceduru, která vytvoří kopii souboru v souborovém systému. Podrobněji se budeme systémovými voláními ‹openat›, ‹read› a ‹write› zabývat v první kapitole. int copy_1( int from_dir, const char *from_name, /* C */ int to_dir, const char *to_name ) { int fd_in, fd_out; Voláním ‹openat› otevřeme soubor s názvem ‹from_name› ve složce ‹from_dir›. Nyní jsou důležité dvě věci: 1. systémové volání ‹openat› může selhat, například proto, že požadovaný soubor neexistuje, a s touto situací se budeme muset vypořádat, 2. je-li soubor úspěšně otevřen, popisovač souboru, který získáme jako návratovou hodnotu, reprezentuje «zdroj» který je nutné později opět uvolnit (systémovým voláním ‹close›). fd_in = openat( from_dir, from_name, O_RDONLY ); /* C */ Neúspěch volání ‹openat› poznáme tak, že ‹fd_in› obsahuje hodnotu -1. Protože v tomto kontextu se jedná o opravitelnou chybu, podprogram ukončíme. Navíc jsme dosud neprovedli žádné „viditelné“ akce, tak nemusíme řešit jejich vrácení. if ( fd_in == -1 ) /* C */ return 1; Abychom mohli kopírovat data, musíme otevřít (a případně vytvořit) i cílový soubor. Opět k tomu použijeme volání ‹openat›. fd_out = openat( to_dir, to_name, O_WRONLY | O_CREAT, 0666 ); /* C */ Opakuje se zde situace, kdy volání ‹openat› může selhat, ale nyní máme nový problém – první volání ‹openat› uspělo a tedy ‹fd_in› odkazuje na «alokovaný zdroj», totiž popisovač souboru. Abychom dodrželi zásadu, že podprogram, který selhal, by neměl mít žádný efekt, musíme tento popisovač před návratem uzavřít. if ( fd_out == -1 ) /* C */ { Dostáváme se nyní do trochu nešťastné situace, kdy vrácení zdroje může také selhat. V takové situaci nám nezbývá, než uživatele varovat a pokračovat ve výpočtu (alternativně bychom mohli tuto chybu považovat za fatální a program ukončit). Volání ‹close› na tomto místě přináší ještě jeden problém – hodnota ‹errno›, která popisuje z jakého důvodu selhalo volání ‹openat›, bude při volání ‹close› přepsána, a volající tedy nebude moct hodnotu ‹errno› použít. Měli bychom tedy tuto hodnotu uložit a před návratem z podprogramu ‹copy_1› obsah ‹errno› obnovit. Toto platí o jakémkoliv systémovém volání, nebo podprogramu, který systémová volání vnitřně používá. Použijeme-li např. proceduru ‹warn›, tato může ‹errno› také přepsat. int saved_errno = errno; /* C */ if ( close( fd_in ) == -1 ) /* C */ warn( "failed to close file %s", from_name ); errno = saved_errno; /* C */ return 1; } Máme nyní otevřeny oba dotčené soubory. Nyní budeme potřebovat místo v paměti, do kterého nejprve načteme blok dat ze vstupního souboru a tento pak obratem zapíšeme do souboru výstupního. Opět platí, že každá z těchto operací může selhat a tuto situaci musíme řešit. const int nbytes = 1024; /* C */ char buffer[ nbytes ]; int bytes_read, bytes_written; do { /* C */ bytes_read = read( fd_in, buffer, nbytes ); if ( bytes_read == -1 ) /* C */ { Výsledek -1 znamená, že čtení selhalo. Operaci musíme ukončit a opět musíme zároveň vrátit vše, co můžeme, do původního stavu. To už v tuto chvíli nebude možné zcela splnit, protože volání ‹openat› pro výstupní soubor mohlo tento soubor vytvořit, ale nemůžeme zde tento soubor odstranit, protože není zaručeno, že pod jménem ‹to_name› ve složce ‹from_dir› se stále nachází soubor, který jsme o několik řádků dříve vytvořili. Omezíme se tedy na uvolnění zdrojů. int saved_errno = errno; /* C */ if ( close( fd_in ) == -1 ) /* C */ warn( "failed to close file %s", from_name ); if ( close( fd_out ) == -1 ) warn( "failed to close file %s", to_name ); errno = saved_errno; /* C */ return 2; } bytes_written = write( fd_out, buffer, bytes_read ); /* C */ if ( bytes_written == -1 ) /* C */ { A ještě jednou totéž. int saved_errno = errno; /* C */ if ( close( fd_in ) == -1 ) /* C */ warn( "failed to close file %s", from_name ); if ( close( fd_out ) == -1 ) warn( "failed to close file %s", to_name ); errno = saved_errno; /* C */ return 2; } } while ( bytes_read != 0 ); Data jsou úspěšně zkopírována, můžeme tedy oba soubory zavřít a oznámit volajícímu úspěch. if ( close( fd_in ) == -1 ) /* C */ warn( "failed to close file %s", from_name ); if ( close( fd_out ) == -1 ) warn( "failed to close file %s", to_name ); return 0; /* C */ } Jistě jste si všimli, že procedura ‹copy_1› obsahuje velké množství redundantního kódu (a jistě také víte, že to není dobře). Máme dva základní prostředky, které nám pomohou se s tímto vypořádat. Jedním jsou pomocné podprogramy. Jako „nízko visící ovoce“ se nabízí procedura ‹close_or_warn›, která nám zjednoduší zavírání souborů, aniž bychom museli přijmout tzv. tichá selhání. Jako dodatečné vylepšení navíc budeme vstupní popisovač s hodnotou -1 ignorovat – žádný platný popisovač nemůže nikdy mít tuto hodnotu. Jedná se o podobný „trik“ jako používá knihovní procedura ‹free›, kterou lze bez rizika zavolat na nulový ukazatel. void close_or_warn( int fd, const char *name ) /* C */ { int saved_errno = errno; if ( fd != -1 && close( fd ) == -1 ) /* C */ warn( "failed to close file %s", name ); errno = saved_errno; /* C */ } Druhým prostředkem je poněkud kontroverzní příkaz ‹goto›. To, že budeme někdy ‹goto› používat pro ošetření chyb neznamená, že můžete ‹goto› „beztrestně“ použít na cokoliv. Problém, který zde ‹goto› řeší je, že odkazy na zdroje alokované podprogramem obvykle ukládáme do lokálních proměnných, které bychom případnému pomocnému „úklidovému“ podprogramu museli vždy všechny předat – to je zápisově značně nepraktické.² int copy_2( int from_dir, const char *from_name, /* C */ int to_dir, const char *to_name ) { int fd_in = -1, fd_out = -1; int rv = 1; const int nbytes = 1024; /* C */ char buffer[ nbytes ]; int bytes_read, bytes_written; if ( ( fd_in = openat( from_dir, from_name, O_RDONLY ) ) == -1 ) /* C */ goto error; if ( ( fd_out = openat( to_dir, to_name, O_WRONLY | O_CREAT, /* C */ 0666 ) ) == -1 ) goto error; rv = 2; /* C */ do { /* C */ if ( ( bytes_read = read( fd_in, buffer, nbytes ) ) == -1 ) /* C */ goto error; if ( ( bytes_written = write( fd_out, buffer, /* C */ bytes_read ) ) == -1 ) goto error; Za povšimnutí stojí, že neřešíme situaci, kdy je ‹bytes_written› nezáporné, ale zároveň menší než ‹bytes_read›. Rozpravu na toto téma si necháme na později. } while ( bytes_read != 0 ); /* C */ rv = 0; /* C */ error: /* C */ close_or_warn( fd_in, from_name ); close_or_warn( fd_out, to_name ); return rv; } int main( void ) /* demo */ /* C */ { int wd = openat( AT_FDCWD, ".", O_DIRECTORY ); if ( wd == -1 ) /* C */ err( 1, "could not open working directory" ); const char *src = "a0_intro.txt"; /* C */ const char *dest = "zt.intro.txt"; if ( copy_1( wd, src, wd, dest ) != 0 ) /* C */ warn( "copying %s to %s failed", src, dest ); if ( copy_2( wd, src, wd, dest ) != 0 ) /* C */ warn( "copying %s to %s failed", src, dest ); if ( unlinkat( wd, dest, 0 ) != 0 ) /* C */ warn( "removing %s failed", dest ); return 0; /* C */ } ² Oddělení úklidu do pomocného podprogramu má i jiné problémy. Můžete si zkusit takový úklidový podprogram napsat, abyste zjistili, jaké kompromisy to obnáší. # Základy práce se soubory Soubor je základní jednotkou práce s perzistentními daty. Rozhraní pro práci se soubory je ve většině operačních systémů velmi důležité, a často pokrývá mnohem víc, než jen obyčejné soubory. Více o souborech a jejich organizaci se dovíte ve třetí kapitole skript. Z pohledu programátora je důležité rozhraní, které práci se soubory zprostředkuje – centrálním prvkem je zde «popisovač otevřeného souboru». Jedná se o hodnotu (číslo), která je pevně svázaná s «otevřeným souborem».¹ Samotný popisovač nemá žádnou vnitřní strukturu, kterou bychom mohli zkoumat, je definován pouze operacemi, které je nad ním možné provádět (jedná se v tomto smyslu o abstraktní datový typ). Těmi hlavními jsou čtení (‹read›) a zápis (‹write›) bajtů. Pozor, na jeden otevřený soubor může odkazovat více než jeden popisovač (s různými číselnými hodnotami). Zároveň může být stejný soubor otevřen více než jednou. Některé typy «otevřených souborů» (zejména obyčejné soubory) si pamatují «pozici» – místo, na kterém proběhne další operace ‹read› nebo ‹write›. Konečně se v této kapitole budeme zabývat tím, že podprogramy, které zprostředkují služby operačního systému, mohou v naprosté většině případů «selhat». Tuto skutečnost obvykle indikují speciální «návratovou hodnotou» a konkrétní problém upřesňuje proměnná ‹errno›.³ Ukázky: 1. ‹copy› – čtení a zápis do obyčejného souboru, 2. ‹filter› – kopírujeme pouze některé záznamy, 3. ‹hello› – standardní vstup a výstup. Přípravy: 1. ‹count› – počítání zadaného bajtu na vstupu, 2. ‹bwconv› – převod obrázku ze stupňů šedi na černobílý, 3. ‹catfd› – použití ‹read› a ‹write› 4. ‹pick› – vykopíruje zadaný rozsah bajtů z každého záznamu, 5. ‹align› – zarovnaný zápis „zubaté“ matice, 6. ‹split› – rozdělení záznamů ze vstupu na dva výstupy. Rozšířené: 1. ‹min› – výpis záznamu s nejmenším jednobajtovým klíčem, 2. ‹bcount› – počítání různých bajtů které se objeví na vstupu, 2. ‹buniq› – výpis každého bajtu nejvýše jednou, 3. ‹otp› – „one time pad“ na dvou vstupech, 5. ‹grep› – přepis záznamů, které obsahují zadanou hodnotu, 6. ‹togray› – † konverze barevného obrázku na černobílý. ¹ Nikoliv s cestou, nebo jiným nepřímým pojmenováním nebo označením. Identita již otevřeného souboru se nám nemůže „změnit pod rukama“. ² Tuto proměnnou nelze striktně vzato označit za globální, protože každé vlákno má vlastní verzi. Implementace takové proměnné je komplikovaná a jde mimo rámec tohoto kurzu. ## Systémová volání Pro čtení a zápis bloků dat (bajtů) slouží operace ‹read› a ‹write›. Pro textový (tzv. formátovaný) zápis je pak určena knihovní procedura ‹dprintf›¹ – vnitřně používá pro výstup na zadaný popisovač volání ‹write›. Bude se nám hodit také volání ‹lseek›. ### ‹write› Protože výstup je jednodušší než vstup, začneme systémovým voláním ‹write›. Má tři parametry: • ‹fd› – popisovač, do kterého hodláme zapisovat, • ‹buf› – ukazatel na paměť, kde jsou uložena data určená k zápisu, • ‹nbytes› – počet bajtů, které si přejeme zapsat. Volání ‹write› pak provede zápis bajtů uložených v polouzavřeném rozsahu adres od ‹buf› po ‹buf + nbytes› (všechny tyto adresy musí být platné, a je velice žádoucí, aby na nich uložené bajty byly inicializované). Návratová hodnota volání ‹write› je počet skutečně zapsaných bajtů. Pro «blokující popisovače» (to budou všechny, které v prvních třech kapitolách potkáme) může být výsledek: • -1 značí chybu zápisu, • kladné číslo menší než ‹nbytes› – pro kompletní zápis nebyl dostatek místa a další volání ‹write› selže s výsledkem -1, • přesně ‹nbytes› – zápis proběhl bez problémů, • konečně 0 může nastat pouze pro nulové ‹nbytes›. První dva případy bychom měli rozlišovat – pro nekompletní zápis totiž není nastaveno ‹errno› a případný výpis důvodu selhání (např. pomocí ‹err› nebo ‹warn›) by byl přinejlepším matoucí – to se týká i situace, kdy ukončujeme podprogram (volající bude očekávat platné ‹errno›). Nejrobustnějším řešením je pokusit se v takovém zápisu pokračovat, nicméně pro účely tohoto kurzu to není nutné a vystačíme si s indikací chyby bez nastavené hodnoty ‹errno›. ### ‹read› TBD. Vstup ze souborů je zprostředkován systémovým voláním ‹read›. ¹ Podobá se funkci ‹printf›, kterou již možná znáte z předchozího programování v jazyce C. Prozatím ‹dprintf› nepotřebujeme k řešení příkladů (a samozřejmě vždy se lze bez ‹dprintf› obejít, máme-li ‹write›), ale může se hodit pro ladicí výpisy atp. ### ‹lseek› TBD. ## Knihovní podprogramy • ‹memcpy› • ‹memcmp› • ‹memchr› ## d. Demonstrace (ukázky) ### 1. [‹read›] Tento jednoduchý program demonstruje POSIXové rozhraní pro práci s obyčejným souborem. Jako obvykle začneme direktivami ‹#define _POSIX_C_SOURCE› a ‹#include› – budeme potřebovat hlavičku ‹unistd.h›, která deklaruje většinu systémových volání (nás bude v tuto chvíli zajímat pouze ‹read›), a také hlavičku ‹fcntl.h›, která deklaruje ‹openat› (tato jako jedna z mála není deklarována v ‹unistd.h›). #define _POSIX_C_SOURCE 200809L /* C */ #include /* read, write */ /* C */ #include /* openat */ #include /* memcmp */ #include /* err */ Protože se jedná o velmi jednoduchý program, bude obsahovat pouze proceduru ‹main›. Jak si jistě pamatujete, jedná se o vstupní bod programu (spustíme-li přeložený program, začne se vykonávat odsud). int main( void ) /* demo */ /* C */ { Začneme tím, že otevřeme soubor pro čtení. Z úvodu víme, že k tomu slouží systémové volání ‹openat›, které má v tomto případě 3 parametry. Prozatím se omezíme na soubory v «pracovní složce» spuštěného programu, jako první parametr tedy předáme ‹AT_FDCWD› (aby program správně pracoval, musíte jej spustit přímo ve složce ‹01›). Dalším parametrem je «název souboru» který chceme otevřít, formou řetězce (ukončeného nulou). Krom samotného názvu zde může stát i cesta (relativní nebo absolutní), ale pro tuto chvíli se opět omezíme na práci s jedinou složkou (tou pracovní). Konečně příznak ‹O_RDONLY› specifikuje, že ze souboru hodláme pouze číst (‹O› od ‹open›, ‹RDONLY› od ‹read only›). const char * const filename = "zz.foo.txt"; /* C */ int fd = openat( AT_FDCWD, filename, O_RDONLY ); /* C */ ‹fd› je tradiční název proměnné, která uchovává popisovač otevřeného souboru (z angl. «f»ile «d»escriptor; samozřejmě tento název lze použít pouze v situaci, kdy pracujeme s jediným popisovačem). Za povšimnutí stojí typ této proměnné – POSIX specifikuje, že popisovače souborů jsou typu ‹int›. Než budeme pokračovat, musíme (jako u prakticky každého systémového volání) ověřit, že otevření souboru proběhlo v pořádku. Manuálová stránka pro systémové volání ‹open› (otevřete ji příkazem ‹man 2 open›) v sekci „return value“ píše: > If successful, ‹open()› returns a non-negative integer, > termed a file descriptor. Otherwise, a value of -1 is > returned and ‹errno› is set to indicate the error. if ( fd == -1 ) /* this would indicate an error */ /* C */ Protože se jedná o kompletní program (nikoliv samostatnou funkci), lze tento typ chyby chápat jako fatální a program s odpovídající chybovou hláškou ukončit. K tomu použijeme proceduru ‹err›, kterou již známe z kapitoly B. err( 1, "opening file %s", filename ); /* C */ Protože procedura ‹err› ukončí program, dostaneme-li se do tohoto místa, víme, že volání ‹open› uspělo a ‹fd› obsahuje platný popisovač, ze kterého lze číst. Čtení provedeme voláním ‹read›, kterému musíme krom popisovače předat ukazatel na paměť, do které data přečtená ze souboru uloží (angl. obvykle označované jako ‹buffer›). Konečně poslední parametr určuje kolik «nejvýše» bajtů má být přečteno. Tomuto parametru musíme věnovat zvláštní pozornost: 1. Bajtů může být přečteno méně, než jsme žádali – kolik jich bylo skutečně přečteno zjistíme až z návratové hodnoty. 2. Vedlejším efektem volání ‹read› je, že do paměti určené adresou ‹buffer› bude zapsáno až ‹nbytes› bajtů (volání tedy přepíše hodnoty na adresách ‹buffer + 0›, ‹buffer + 1›, …, ‹buffer + nbytes - 1›). Abychom si omylem nezničili nějaká nesouvisející data, musíme systémovému volání ‹read› předat adresu, od které máme vyhrazeno alespoň ‹nbytes› bajtů. Jednoduchý způsob, jak vyhradit pevný počet bajtů v paměti, je deklarací lokálního pole. Počet bajtů, které hodláme načíst, si uložíme do pojmenované konstanty ‹nbytes›. const int nbytes = 16; /* C */ const int expect = 4; char buffer[ nbytes ]; /* C */ ssize_t bytes_read = read( fd, buffer, nbytes ); Jako každé systémové volání může ‹read› selhat. Podobně jako u volání ‹open› tuto skutečnost indikuje návratová hodnota -1.¹ Pozor, návratová hodnota ‹0› «není chybou» (říká nám pouze, že žádné další bajty přečíst nelze, protože jsme narazili na konec souboru). if ( bytes_read == -1 ) /* C */ err( 1, "error reading from %s", filename ); Dále ověříme, že jsme načetli data, která jsme očekávali. Protože se v těchto případech nejedná o systémové chyby, použijeme místo procedury ‹err› proceduru ‹errx›, která nevypisuje chybu uloženou v proměnné ‹errno›. if ( bytes_read < expect ) /* C */ errx( 1, "file %s was shorter than expected, only %zd bytes", filename, bytes_read ); if ( memcmp( buffer, "foo\n", expect ) ) /* C */ errx( 1, "file %s has unexpected content", filename ); Po dokončení práce se souborem tento uzavřeme. Všimněte si, že v případě chyby čtení jsme popisovač neuzavřeli – to si můžeme dovolit pouze v situaci, kdy zároveň ukončujeme celý program (tím jsou veškeré zdroje automaticky uvolněny). Systémové volání ‹close› může opět selhat, nicméně situace, kdy se můžeme s takovou chybou smysluplně vypořádat jsou relativně vzácné. Měli bychom ale v každém případě o takovém selhání uživatele informovat, protože tento typ chyby může znamenat ztrátu dat, která program do souboru zapisoval. To, co naopak udělat «nesmíme», je pokusit se soubor zavřít podruhé – v závislosti na systému a okolnostech mohl být popisovač uzavřen i přesto, že volání ‹close› selhalo (a ani nemáme jak zjistit, jestli k tomu došlo nebo nikoliv). if ( close( fd ) != 0 ) /* C */ warn( "error closing %s", filename ); Návratová hodnota 0 značí, že program bez chyby doběhl. return 0; /* C */ } ¹ Všimněte si, že proměnnou ‹bytes› jsme deklarovali s typem ‹ssize_t› – jedná se o «znaménkový» typ, na rozdíl od podobně pojmenovaného typu ‹size_t›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹hello›] Krom obyčejných souborů lze popisovače využít k práci s řadou dalších podobných zdrojů. V tomto programu si ukážeme, jak pracovat s takzvaným standardním vstupem a výstupem (angl. standard input/output, nebo také stdio). Tento se v systémech POSIX skládá ze tří částí, reprezentovaných třemi popisovači:¹ • standardní vstup, na popisovači číslo 0, symbolicky ‹STDIN_FILENO›, který je implicitně „připojený ke klávesnici“ (čtením z tohoto popisovače získáme bajty, které uživatel zadal do terminálu po spuštění programu), • standardní výstup, na popisovači číslo 1, symbolicky ‹STDOUT_FILENO›, který je implicitně „připojený k obrazovce“ (zápisem do tohoto popisovače zobrazujeme data na obrazovku terminálu), • standardní «chybový» výstup, na popisovači číslo 2, symbolicky ‹STDERR_FILENO›, který je implicitně spojen s předchozím a tedy odesílán na obrazovku. Přesto, že se na pohled chovají zaměnitelně, je mezi standardním výstupem a standardním chybovým výstupem klíčový «sémantický» rozdíl – výstup běžného neinteraktivního programu lze pomyslně rozdělit do dvou oddělených proudů: • užitný výstup, který je hlavním výsledkem výpočtu a tzv. • diagnostický výstup, který obsahuje informace o průběhu výpočtu, zejména o chybách. Nikoho zřejmě nepřekvapí, že tyto dva typy výstupů patří každý na odpovídající typ standardního výstupu. Důležitým efektem tohoto rozdělení je, že když standardní výstup «přesměrujeme» (např. do souboru, nebo na vstup dalšího programu), nebudou se do dat určených k dalšímu zpracování míchat diagnostické výstupy – naopak, uživateli budou nadále přístupné na obrazovce.² #include /* write, STDIN_FILENO, … */ /* C */ #include /* dprintf */ #include /* err */ int main( void ) /* demo */ /* C */ { Veškerý výstup do popisovače otevřeného souboru je realizován systémovým voláním ‹write›.³ Zejména veškeré sofistikované knihovní funkce pro výstup (např. ‹dprintf›, ale třeba i procedury z rodiny ‹err›) nakonec výstup realizují voláním ‹write›. Systémové volání ‹write› je ve svém principu velmi jednoduché: předáme mu relevantní popisovač, ukazatel na paměť a počet bajtů, které má vypsat. const int nbytes = 5; /* C */ int bytes_written = write( STDOUT_FILENO, "hello", nbytes ); Uspěje-li volání ‹write›, náš program právě na standardní výstup zapsal 5 bajtů, 0x68, 0x65, 0x6c, 0x6c a konečně 0x6f (viz také ‹man ascii›). Úspěch ale ani u takto na první pohled jednoduché operace není zaručen. Výstup mohl být například uživatelem přesměrován do souboru nebo do roury. Za určitých okolností může také zápis uspět částečně, ale touto situací se prozatím nebudeme zabývat.⁴ Protože standardní výstup a standardní chybový výstup nemusí být tentýž objekt, má i v případě selhání zápisu na standardní výstup smysl zapsat na chybový výstup hlášení o chybě. Je již na uživateli, aby chybový výstup směroval na podle možnosti spolehlivá zařízení. Krom ukončení programu s chybovým kódem nemáme žádnou možnost, jak na chybu při výpisu chybové hlášky reagovat. if ( bytes_written == -1 ) /* C */ err( 1, "write to stdout failed" ); Krom přímého použití systémového volání ‹write› budeme k zápisu používat knihovní proceduru ‹dprintf›, která nám umožní jednodušeji sestavovat textové zprávy. Protože ‹dprintf› vnitřně používá systémová volání, která mohou (opět) selhat, musí se s případnou chybou nějak vypořádat. Protože se jedná o knihovní podprogram, který by měl být použitelný v mnoha různých situacích, prakticky jedinou rozumnou možností je informaci o selhání předat volajícímu. Podobně jako samotná systémová volání k tomu používá návratovou hodnotu. Rychlý pohled do ‹man dprintf› nám sdělí, že výsledkem funkce je počet zapsaných bajtů, resp. -1 pokud nastala nějaká chyba. if ( dprintf( STDOUT_FILENO, " world\n" ) == -1 ) /* C */ err( 1, "write to stdout failed" ); Konečně vypíšeme nějaký text i na chybový výstup. Protože selhání výstupu v tomto případě nemáme jak dále diagnostikovat, program pouze ukončíme s chybou. if ( dprintf( STDERR_FILENO, "hello stderr\n" ) == -1 ) /* C */ return 2; Abyste si ověřili, že rozumíte tomu, jak se tento program chová, zkuste si jej spustit následovnými způsoby, a ujistěte se, že jste tento výsledek očekávali (soubor ‹/dev/full› simuluje situaci, kdy dojde místo v souborovém systému – není standardizován, ale na stroji ‹aisa› je k dispozici): $ ./d2_hello $ ./d2_hello > /dev/null $ ./d2_hello > /dev/full $ ./d2_hello 2> /dev/null $ ./d2_hello 2> /dev/full Návratový kód programu si můžete bezprostředně po jeho ukončení vypsat příkazem ‹echo $?› (platí interprety příkazů, které se drží normy POSIX). return 0; /* C */ } ¹ Součástí jazyka C (nezávisle na standardu POSIX) jsou knihovní funkce pro práci standardním vstupem a výstupem, které možná znáte. Patří sem např. procedury ‹printf›, ‹fprintf›, ‹puts›, atp. – tyto můžete v principu používat jako ladící pomůcky, ale v hotových programech se jim raději vyhněte. Potřebujete-li alternativu k ‹fprintf›, použijte ‹dprintf›. «Pozor»! Programy, které míchají vstupně-výstupní prostředky jazyka C s těmi POSIX-ovými, jsou náchylné na těžko odhalitelné chyby (zejména při nesprávném nebo chybějícím použití procedury ‹fflush›). ² Mohli byste mít pocit, že výstup Vašeho programu nebude nikdo dále automaticky zpracovávat a tedy na toto rozdělení nemusíte dbát. Uvědomte si ale, že i běžné použití programu ‹grep› pro vyhledání relevantních řádků ve výstupu na toto rozdělení spoléhá. Uvažte situaci, kdy ‹./program | grep system› nic nevypíše – je to proto, že program selhal, ale ‹grep› chybovou hlášku z výstupu odstranil, nebo proto, že žádný řádek ve výpisu neobsahoval řetězec ‹system›? ³ Nebo některou jeho variantou – ‹pwrite›, ‹writev›, ‹pwritev›, které jdou ale nad rámec tohoto předmětu. ⁴ Situace s částečnými zápisy je poněkud komplikovaná – pro blokující popisovač (případ, kdy by byl standardní vstup nebo výstup nastaven do neblokujícího režimu, nebudeme řešit) může být zápis (nebo analogicky i čtení) přerušeno signálem. Protože naše programy nebudou signály obsluhovat, doručení signálu zároveň ukončí program, a tedy částečné zápisy nebo čtení nemusíme řešit, není-li pro to speciální důvod (např. použití neblokujících popisovačů – ‹O_NONBLOCK›). ## p. Přípravy ### 1. [‹lines›] POSIX definuje řádek jako posloupnost libovolných znaků zakončenou znakem nového řádku ‹\n› (U+000A, označovaný ‹line feed› nebo též ‹newline›). Implementujte podprogram ‹count_lines›, který spočítá řádky na vstupu daném popisovačem ‹fd› a ověří, zda vstup neobsahuje žádné nekompletní řádky. Počet (kompletních) řádků vrátí skrze ukazatel ‹count›. Vstup zpracovávejte postupně po malých částech (množství paměti potřebné pro spuštění programu by nemělo záviset na velikosti vstupu). Návratová hodnota bude: • ‹0› proběhlo-li vše v pořádku, • ‹1› obsahuje-li soubor nekompletní řádek, • ‹2› v případě selhání čtení nebo jiné systémové chyby (v tomto případě navíc není určeno, jaká hodnota bude zapsána do výstupního parametru ‹count›). int count_lines( int fd, int *count ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹bwconv›] Úkolem je naprogramovat proceduru ‹bwconv›, která převede obrázek ve formátu BMP ze stupňů šedi do černé a bílé. Výsledný obrázek tak bude obsahovat pixely pouze těchto dvou barev. Procedura přijímá parametry: • ‹fd_in› – popisovač pro vstupní bitmapová data; • ‹w› – šířka obrázku; • ‹h› – výška obrázku; • ‹fd_out› – výstupní popisovač; • ‹threshold› – mez rozdělující bílou a černou (hodnota 0–255). Na vstupu může procedura očekávat pouze bitmapová data (tedy o hlavičku je postaráno se jinde) a stejně tak bude zapisovat pouze výsledná bitmapová data bez hlavičky. Data budou takového formátu, že jeden bajt = jeden pixel (tedy 8 bitů na pixel). Formát BMP navíc definuje, že každý řádek musí být uložen tak, aby jeho délka v bajtech byla dělitelná 4, tedy po přečtení ‹w› bajtů budou na řádku další 0 až 3 bajty, které výsledný obrázek nijak neovlivní. Můžete předpokládat, že na vstupu bude hodnota těchto bajtů 0. Pro každý bajt určující barvu zapište na výstup černou (hodnotu 0) je-li vstupní barva «menší nebo rovna» hodnotě ‹threshold› a bílou (‹255›) jinak. «Pozor!» Když budete srovnávat vstupní bajty s hodnotou ‹threshold›, dejte si pozor na implicitní konverze. Ujistěte se, že konverze mezi použitými číselnými typy proběhne tak, jak očekáváte. Návratová hodnota: ‹0› – úspěch; ‹-1› – systémová chyba. Při testování může přijít vhod příkaz ‹od -t x1› na prohlížení jednotlivých bajtů obrázku. int bwconv( int fd_in, int w, int h, int fd_out, int threshold ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹catfd›] V tomto příkladu bude úkolem implementovat proceduru, jejíž chování se podobá standardnímu programu ‹cat›. Procedura ‹catfd› přijímá 3 parametry: • ‹fds› – ukazatel na pole popisovačů, • ‹count› – počet popisovačů zde uložený, • ‹out_fd› – výstupní popisovač. Účelem této procedury bude přečíst veškerá data z každého popisovače (v zadaném pořadí) a ta zapsat do výstupního popisovače. Pokud vše proběhne bez chyby, vrátí 0, jinak skončí při první chybě a vrátí -1. int catfd( int *fds, int count, int out_fd ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 4. [‹cols›] Vaším úkolem je implementovat proceduru ‹write_aligned›, jejímž účelem bude na výstupní popisovač vypsat tabulku v níže popsaném formátu, avšak doplněnou o mezery tak, aby byl každý sloupec zarovnaný vpravo. Zároveň by měl být použit minimální nutný počet takových mezer. CSV je velmi jednoduchý formát na ukládání tabulek. Jelikož však neexistuje¹ jediný standard, který by byl všeobecně dodržován, je možné se setkat s množstvím rozdílných variant. Pro toto zadání uvažme následující: • Hodnoty jsou ukládány «oddělené» znakem čárky ‹,› – U+002C. • Počet hodnot na každém řádku odpovídá počtu sloupců. Všechny řádky tak musí obsahovat stejný počet hodnot, jinak by se nejednalo o validní tabulku. • Každý řádek je «zakončen» znakem ‹\n› (U+000A, označovaný ‹line feed› nebo též ‹newline›). Protože budeme beztak hodnoty zarovnávat mezerami, pro lepší čitelnost budeme také za každou oddělovací čárkou zapisovat alespoň jednu mezeru. Procedura akceptuje čtyři parametry: • ‹fd› – «výstupní» popisovač, na který má být zapsána vpravo zarovnaná tabulka; • ‹values› – ukazatel na pole celočíselných hodnot o velikosti alespoň ‹cols × rows›; • ‹cols› – počet sloupců; • ‹rows› – počet řádků. Návratová hodnota nechť je v případě úspěchu 0 a jinak -1 (například selže-li zápis na zadaný popisovač). «Příklad:» Pro hodnoty ‹123, 456, 789, 1, 2, 3, 12, 3, 456› a velikost tabulky 3 × 3 očekáváme, že na výstup bude vypsán řetězec: "123, 456, 789\n" " 1, 2, 3\n" " 12, 3, 456\n" Nápověda: jistě Vám přijde vhod procedura ‹dprintf› – doporučujeme podívat se zejména co znamená znak ‹*› ve formátovacím řetězci. Rovněž může být užitečná funkce ‹snprintf› s omezením na nulovou délku. ¹ Existuje definice dle RFC 4180, nicméně tato se nezdá být širší komunitou považována za závaznou. int write_aligned( int fd, const int *values, int cols, int rows ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 5. [‹cut›] Implementujte proceduru ‹cut›, která na popisovač ‹out› vypíše sloupec zadaný parametrem ‹field› ze souboru ‹file› (sloupce číslujeme od 1).¹ Každý řádek na vstupu i výstupu bude zakončen znakem ‹'\n'› a sloupce jsou na vstupu odděleny bajtem ‹delim›. Soubor ‹file› hledejte ve složce určené popisovačem ‹dir›. Návratová hodnota: 0 – úspěch; -1 – systémová chyba. ¹ Toto chování je podobné standardnímu příkazu ‹cut -d delim -f field file›. int cut( int dir, const char* file, char delim, int field, int out ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 6. [‹cat›] Naprogramujte proceduru ‹cat›, která obdrží tyto 3 parametry: • ‹dir_fd› – popisovač adresáře, ve kterém bude hledat všechny níže zmíněné soubory, • ‹list› – jméno souboru se jmény souborů ke čtení, • ‹out_fd› – «výstupní» popisovač. Soubor ‹list› bude obsahovat na každém řádku jméno souboru. Procedura ‹cat› zapíše obsahy všech těchto souborů (v zadaném pořadí) do popisovače ‹out›. Stejně jako v předešlých příkladech za řádek považujeme posloupnost znaků «zakončenou» ‹'\n'› (nikoliv tedy ‹"\r\n"› nebo ‹'\r'›). Pro zjednodušení navíc zavedeme limit, kdy délka každého řádku smí být nejvýše ‹name_max› bajtů (nepočítaje znak konce řádku). static const int name_max = 256; /* C */ Návratová hodnota 0 označuje úspěch. Pokud je některý ze řádků souboru ‹list› delší než ‹name_max›, vraťte -2; nastane-li systémová chyba, hodnotu -1. Nápověda: na popisovači k souboru ‹list› je možné používat volání ‹lseek›. Jeho použitím si můžete usnadnit implementaci. int cat( int dir_fd, const char *list, int out_fd ); /* C */ ## r. Řešené úlohy ### 1. [‹wcount›] Za slovo budeme považovat posloupnost „nebílých“ znaků, po které následují jeden či více „bílých“ znaků, nebo konec vstupu. Bílé znaky uvažujeme ve smyslu standardní funkce ‹isspace› deklarované v hlavičce . Podprogram ‹count_words› zpracuje soubor o zadané cestě ‹file› a výsledek vrátí skrze ukazatel ‹count›. Nastane-li systémová chyba, podprogram vrátí -1 (přitom hodnota na adrese ‹count› není určena). V opačném případě vrátí 0. int count_words( int dir_fd, const char *file, int *count ); /* C */ ### 2. [‹cgrep›] Implementujte podprogram ‹cgrep›, která vypíše všechny řádky ze vstupu ‹fd_in›, které obsahují znak ‹c›. Tyto řádky vypište na popisovač ‹fd_out›. Pro tuto úlohy není stanoven žádný limit na maximální délku řádku. Smíte ovšem předpokládat, že ve vstupním souboru se lze posouvat voláním ‹lseek›. Návratová hodnota: 0 – úspěch; 1 – systémová chyba. int cgrep( int fd_in, char c, int fd_out ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹flake›] V této úloze budeme programovat funkci ‹path›, jejímž účelem bude ověřit, zda zadaný soubor splňuje určitou vlastnost. Soubor musí obsahovat řádky nejvýše dané délky (ta je funkci předána jako druhý parametr ‹len›). Každý řádek je zakončen znakem '\n' a obsahuje pouze symboly ' ' a '*'. Na každém řádku se musí vyskytovat právě jednou znak '*', který označuje pozici pomyslného robota. Nevadí, pokud řádek obsahuje mezery mezi tímto znakem a koncem řádku. Vaším úkolem je rozhodnout, zda mezi dvěma řádky vždy platí, že robot změnil pozici nejvýše o jedna. Příklad: Uvažme následující řádky. 1: " * \n" 2: " *\n" 3: " *\n" Mezi řádky 1 a 2 se robot posunul o jedno pozici doprava, to je v pořádku. Mezi řádky 2 a 3 se však robot posunul o dvě doleva, tedy soubor celkově není validní. Návratová hodnota: • 0 – čtení úspěšné a soubor je validní; • 1 – čtení úspěšné, ale robot se posunul o více pozic; • 2 – čtení úspěšné, ale soubor je špatného formátu; • 3 – čtení neúspěšné. int path( const char* file, int len ); /* C */ void main( void ) /* C */ { return 0; } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 4. [‹bcount›] Podprogram ‹count_distinct› spočítá počet různých bajtů v souboru. Tento počet v případě úspěchu vrátí, jinak vrátí hodnotu -1. int count_distinct( int dir_fd, const char *file ); /* C */ ### 5. [‹linked›] Implementujte funkci ‹linked›, která přečte «první řádek» zadaného souboru. Tento text se interpretuje jako název souboru, kterým se má dále pokračovat. Nejprve ho vypíše (na popisovač ‹out›) a potom soubor otevře a provede pro něj totéž. Tento proces se opakuje, dokud nenarazíme na prázdný soubor, který zřetězenou sekvenci ukončí. Abychom nemuseli komplikovaně načítat neomezeně dlouhé názvy, zavedeme zde limit pro délku názvu souboru 256 bajtů. Je-li první řádek v některém vstupním souboru delší, funkce ‹linked› skončí s návratovou hodnotou -2 (znak konce řádku se do tohoto limitu nepočítá). Nastane-li systémová chyba, funkce nechť vrátí hodnotu -1, jinak pak počet souborů, které otevřela. const int name_max = 256; /* C */ int linked( const char* file, int out ); /* C */ ### 6. [‹otp›] V této úloze si naprogramujeme jednoduchou techniku z kryptografie označovanou jako „one-time pad.“ Funkci ‹otp› jsou zadány dva názvy souborů a výstupní popisovač. Úkolem funkce je na tento popisovač vypsat ‹obsah prvního souboru XOR obsah druhého souboru›. «Druhý» soubor zde považujeme za klíč a pro bezpečnost této kryptografické techniky je nezbytné, aby jeho délka byla «alespoň» taková jako je délka prvního. V případě, že tento požadavek není splněn, funkce nechť vrátí -1. Jestliže nastane systémová chyba (např. problém ve čtení některého souboru), vraťte -2. Pokud je vše úspěšné, vraťte 0. Doporučení: při testování se může hodit shellový příkaz ‹od -t x1› na prohlížení zapsaných bajtů do souboru (případně ‹od -t x1 -t c›, pokud zároveň chceme zobrazit ASCII znaky). int otp( int dir, const char* file, const char* key_file, int out ); /* C */ # Datagramy V této kapitole budeme pracovat s novým typem objektu, který se může skrývat za popisovačem otevřeného souboru. Zdaleka nejdůležitější aplikací socketů je síťová komunikace – na úrovni softwaru je naprostá většina provozu na internetu realizovaná skrze sockety.¹ S obyčejnými soubory mají sockety dvě klíčové společné vlastnosti: • jedná se o objekt operačního systému, který má identitu, a na který se může odkazovat popisovač otevřeného souboru, • ze socketu můžeme číst data (bajty), a/nebo do něj data zapisovat. Ukázky: 1. ‹xxx› 2. ‹xxx› Přípravy: 1. ‹min› – velmi jednoduchý datagramový „server“, 2. ‹plog› – ukládání datagramů do souboru, 3. ‹otpd› – šifrování datagramů (keystream ze souboru), 4. ‹newsc› – jednoduchý klient pro stažení novinek, 5. ‹newsd› – server pro předchozí, 6. ‹tftpw› – odeslání souboru – první interaktivní protokol. Řešené příklady: 1. ‹agree› – domluvení společné podmnožiny 2. ‹xxx› 3. ‹filter› – dva sockety, přeposílá obousměrně podle podmínky 4. ‹xxx› 5. ‹xxx› 6. ‹tftpr› – RRQ, uloží data do souboru ¹ Sockety jsou klíčová součást implementace webových serverů (apache, nginx), aplikačních serverů (node.js, django, rails), databází, ale i klientského software (prohlížečů, mobilních aplikací). ## Systémová volání V této kapitole přidáme volání ‹send› a ‹recv›, které jsou analogické k ‹write› a ‹read›. Pro práci s datagramovými sockety budeme upřednostňovat volání ‹send› a ‹recv›, zejména abychom čtenáře upozornili, že pracujeme s datagramy. Funkční rozdíl mezi ‹send› a ‹write›, resp. ‹recv› a ‹read›, prozatím žádný neuvidíme.¹ ### ‹send› TBD. ### ‹recv› TBD. ¹ To se změní ve druhém bloku, kdy budeme pracovat s adresami a volání ‹sendto› a ‹recvfrom›. ## d. Demonstrace (ukázky) ### 3. [‹datagram›] V této ukázce budeme pracovat s «datagramovými» sockety – ty se od spojovaných liší ve dvou ohledech: 1. není potřeba (ani není možné) navazovat spojení – s tím je spojena i větší symetrie mezi komunikujícími stranami, např. v tom, že obě strany musí mít reprezentovatelnou adresu, 2. datagram je doručen vždy jako celek,¹ a to i na úrovni volání ‹sendto›/‹recvfrom› – není tedy potřeba řešit situace, kdy ‹read› skončí „uprostřed věci“ a my musíme data skládat na straně programu. Program bude mít dva režimy, v jednom bude čekat na zprávu, na kterou obratem odpoví, v tom druhém odešle zprávu a vyčká na odpověď. Spustíme-li program dvakrát v různých režimech, uvidíme průběh komunikace. Protože budeme ‹bind› potřebovat na několika místech, vytvoříme si pomocnou proceduru, která daný socket sváže se zadanou adresou (cestou). Samotné volání ‹bind› se od případu spojovaných socketů nijak neliší. void setup_unix( struct sockaddr_un *sa, const char *path ) /* C */ { if ( strlen( path ) >= sizeof sa->sun_path - 1 ) errx( 1, "socket address %s too long, maximum is %zu", path, sizeof sa->sun_path ); snprintf( sa->sun_path, sizeof sa->sun_path, "%s", path ); /* C */ } void bind_unix( int sock_fd, const char *addr ) /* C */ { struct sockaddr_un sa = { .sun_family = AF_UNIX }; setup_unix( &sa, addr ); /* C */ if ( unlink( sa.sun_path ) == -1 && errno != ENOENT ) /* C */ err( 1, "unlinking %s", sa.sun_path ); if ( bind( sock_fd, ( struct sockaddr * ) &sa, sizeof sa ) ) /* C */ err( 1, "binding a unix socket to %s", sa.sun_path ); } Poslouchající program potřebuje pouze adresu, na které bude poslouchat – odpověď odešle zpátky na adresu, ze které původní zpráva přišla. void listener( int sock_fd, const char *addr ) /* C */ { bind_unix( sock_fd, addr ); Pro datagramový socket je tímto nastavení hotovo – pro komunikaci budeme, podobně jako na klientské straně spojované komunikace, používat přímo ‹sock_fd›. Pro získání jednoho datagramu od libovolného klienta použijeme volání ‹recvfrom›, které jednak získá odeslaná data, ale také «adresu» odesílatele, kterou budeme potřebovat, abychom mohli odeslat odpověď. struct sockaddr_un sun; /* C */ struct sockaddr *sa = ( struct sockaddr * ) &sun; socklen_t sa_size = sizeof sun; char buffer[ 512 ]; /* C */ int bytes_recvd; if ( ( bytes_recvd = recvfrom( sock_fd, buffer, 512, 0, /* C */ sa, &sa_size ) ) == -1 ) err( 1, "recvfrom on %s", addr ); Podobně jako ‹read›, ‹recvfrom› je implicitně blokující – návrat z volání znamená, že jsme obdrželi zprávu. Zprávu ohlásíme na standardní výstup a zároveň odešleme odpověď. dprintf( STDOUT_FILENO, "listener received: \"%.*s\" from %s\n", /* C */ bytes_recvd, buffer, sun.sun_path ); if ( sendto( sock_fd, "pong", 4, 0, sa, sa_size ) == -1 ) /* C */ err( 1, "sending pong on %s", sun.sun_path ); } /* C */ Odesílatel bude potřebovat adresy dvě – adresu, ze které bude odesílat (a na které obdrží případné odpovědi) a adresu na kterou má počáteční zprávu odeslat. void sender( int sock_fd, /* C */ const char *addr_us, const char *addr_dest ) { bind_unix( sock_fd, addr_us ); struct sockaddr_un sun_dest = { .sun_family = AF_UNIX }; /* C */ struct sockaddr *sa_dest = ( struct sockaddr * ) &sun_dest; socklen_t sa_size = sizeof sun_dest; setup_unix( &sun_dest, addr_dest ); /* C */ Počáteční zprávu odešleme voláním ‹sendto›, kterému předáme jak zprávu, tak adresu, na kterou si ji přejeme doručit. V tomto případě jsme cílovou adresu získali z příkazové řádky. if ( sendto( sock_fd, "ping", 4, 0, sa_dest, sa_size ) == -1 ) /* C */ err( 1, "sending ping to %s", addr_dest ); A nyní počkáme na odpověď. Pozor! Volání ‹recv› přijme zprávu od kteréhokoliv odesílatele, a nemáme jak určit, od kterého. Pro tento jednoduchý program bude ‹recv› stačit, aniž bychom učinili nějaká další opatření. Abychom se ujistili, že zpráva je skutečně odpovědí na náš „ping“, mohli bychom použít volání ‹connect› – tím sice nenavážeme u datagramového socketu žádné spojení, ale nastavíme dvě adresy, které si operační systém k socketu uloží: • implicitní cílovou adresu, kterou použije pro volání ‹send› (u kterého, na rozdíl od ‹sendto›, adresu uvést nemůžeme) – tuto adresu můžeme vždy „přebít“ použitím volání ‹sendto›, • adresu, ze které hodláme zprávy přijímat – operační systém bude zprávy od jiných odesílatelů odmítat. Protože ‹connect› na datagramovém socketu pouze nastavuje asociované adresy, ale nevytváří žádné spojení, lze ‹connect› na jednom takovém socketu volat opakovaně a tím asociaci měnit. char buffer[ 512 ]; /* C */ int bytes_recvd; if ( ( bytes_recvd = recv( sock_fd, buffer, 512, 0 ) ) == -1 ) /* C */ err( 1, "recv on %s", addr_us ); Zprávu, kterou jsme obdrželi, nakonec vypíšeme uživateli. dprintf( STDOUT_FILENO, "sender received: \"%.*s\"\n", /* C */ bytes_recvd, buffer ); } int main( int argc, const char **argv ) /* C */ { if ( argc != 3 && argc != 4 ) errx( 0, "expected arguments: recv_socket [send_socket]" ); V obou případech budeme ke komunikaci potřebovat socket: int sock_fd = socket( AF_UNIX, SOCK_DGRAM, 0 ); /* C */ if ( sock_fd == -1 ) /* C */ err( 1, "creating a unix socket" ); Dále pouze určíme režim, a spustíme příslušnou proceduru. Pro vyzkoušení použijte tyto příkazy (v tomto pořadí, ale každý v jiném terminálu, samozřejmě na stejném počítači a ve stejné pracovní složce): $ ./d3_datagram recv zt.foo $ ./d3_datagram send zt.bar zt.foo if ( strcmp( argv[ 1 ], "recv" ) == 0 ) /* C */ { listener( sock_fd, argv[ 2 ] ); } else if ( strcmp( argv[ 1 ], "send" ) == 0 ) { if ( argc != 4 ) errx( 1, "expected arguments: send recv_socket send_socket" ); sender( sock_fd, argv[ 2 ], argv[ 3 ] ); /* C */ } else errx( 1, "expected 'send' or 'recv' as first argument" ); return 0; /* C */ } ¹ Je-li doručen. Typickým datagramovým protokolem je UDP, které doručení datagramu nezaručuje. Podobně není zaručeno pořadí doručení jednotlivých datagramů mezi sebou. Ani jeden z těchto problémů nás v této chvíli nemusí trápit, protože sockety v doméně ‹AF_UNIX› datagramy doručují spolehlivě a v pořadí odeslání. ## p. Přípravy ### 1. [‹echoc›] V této úloze je Vaším úkolem naprogramovat klient pro velmi jednoduchý protokol echo, konkrétně jeho proudově orientovanou verzi.¹ Server tohoto protokolu od klienta přijímá data a obratem je odesílá nezměněná zpět. Implementujte proceduru ‹check_echo›, která obdrží adresu unxiového socketu, data která má odeslat a jejich délku, a následně pomocí této zprávy ověří, že na předané adrese běží korektní server² protokolu echo. Můžete předpokládat, že testovací zpráva, která bude proceduře ‹check_echo› předána, je menší, než implicitní kapacita vyrovnávací paměti socketu. Návratová hodnota nechť je: • 0 je-li ověření úspěšné, • 1 je-li zjištěno, že server se nechová korektně, • 2 dojde-li k systémové chybě. ¹ Protokol echo je přes svoji jednoduchost standardizovaný, konkrétně v RFC 862. ² Server považujeme za korektní i v případě, že odpoví pomocí křišťálové koule – Vámi odeslaná data nemusí vůbec přečíst, bude-li jeho odpověď i tak správná. int check_echo( const char* sock_path, const char* msg, int size ); /* C */ Přesto, že implementujete znovupoužitelný podprogram, může být užitečné jej testovat interaktivně – tento program můžete spustit jako ‹./p1_echoc cesta_k_socketu zpráva› – v takovém případě se přiložené testy přeskočí. Viz též začátek procedury ‹main›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹lmtpc›] V tomto cvičení bude Vaším úkolem naprogramovat klient protokolu LMTP (Local Mail Transfer Protocol, RFC 2033). Implementujte proceduru ‹lmtp_send›, která pomocí LMTP odešle e-mail, a která bude mít tyto parametry: • ‹addr› – adresa unixového socketu na které běží server, • ‹sender› – nulou ukončený řetězec, který obsahuje adresu odesílatele, • ‹recip› – podobně, ale s adresou adresáta, • ‹data› – obsah zprávy (včetně hlaviček; nebude obsahovat řádek se samostatně stojící tečkou). Návratová hodnota bude: • 0 – vše proběhlo v pořádku a mail byl doručen, • 1 – adresát neexistuje, • 2 – došlo k jiné chybě doručení, • -1 – došlo k systémové chybě na straně klienta.¹ LMTP je jeden z mnoha řádkově orientovaných internetových protokolů. Na rozdíl od konvence POSIX-ových systémů používají protokoly specifikované v RFC jako ukončení řádku sekvenci bajtů ‹0x0d 0x0a› (CRLF). Takto ukončené řádky musíte jak odesílat, tak akceptovat v odpovědích serveru. Základní příkazy protokolu LMTP jsou tyto: • ‹LHLO jméno› – pozdrav; jako jméno použijte ‹pb152cv›, • ‹MAIL FROM:› – oznámí serveru, že hodláme odesílat mail a zároveň adresu jeho odesílatele, • ‹RCPT TO:› – předá serveru adresu, na kterou má zprávu doručit (lze použít vícekrát, ale my budeme odesílat zprávy vždy pouze jednomu adresátovi), • ‹DATA› – iniciuje přenos samotné zprávy, která je ukončena řádkem, který obsahuje pouze tečku, • ‹QUIT› – ukončí sezení, server příkaz potvrdí a uzavře spojení. Na každý klientem odeslaný příkaz dostanete jednořádkovou² odpověď, která začíná číselným kódem. Kód v rozsahu 200–299 znamená úspěšné provedení příkazu. Na navázání spojení reaguje server kódem 220, na který musíte vyčkat než odešlete příkaz ‹LHLO›. Chybové kódy jsou v rozsahu 400–599 (4xx pro dočasné a 5xx pro permanentní chyby). ¹ Zahrnuje mimo jiné chybnou adresu serveru. Při výsledku -1 nechť je ‹errno› nastavené na odpovídající hodnotu. ² Protokol umožňuje serveru odeslat více odpovědí na jeden příkaz, tuto situaci ale nemusíte řešit, protože normálně nastává pouze pro zprávy s více než jedním adresátem. int lmtp_send( const char *addr, const char *sender, /* C */ const char *recip, const char *message ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹tftpc›] Vaším úkolem je tentokrát naprogramovat klient protokolu TFTP (Trivial File Transfer Protocol, RFC 1350). Jedná se o paketově (datagramově) orientovaný protokol pro přenos souborů. Procedura ‹tftp_put› pomocí protokolu TFTP na server uloží dodaný soubor. Parametry: • ‹addr› – adresa unixového socketu na které běží server, • ‹name› – nulou ukončený název souboru, • ‹data› – ukazatel na data souboru, který má být uložen, • ‹size› – velikost souboru. Nahrání souboru probíhá v protokolu TFTP takto: 1. klient odešle paket ‹WRQ›, který obsahuje: ◦ kód příkazu – bajty ‹0x00 0x02›, ◦ název souboru ukončený nulovým bajtem, ◦ řetězec ‹"octet"›, ukončen dalším nulovým bajtem, 2. server odpoví paketem ‹ACK› nebo ‹ERROR› (viz níže), 3. je-li odpověď ‹ACK›, klient pokračuje odesláním paketu typu ‹DATA›, který obsahuje: ◦ kód příkazu – bajty ‹0x00 0x03›, ◦ dvoubajtové sekvenční číslo (první ‹DATA› paket má číslo 1, každý další pak o jedna větší než předchozí), ◦ 0–512 bajtů dat (každý paket krom posledního má 512 bajtů dat, poslední paket zbytek – může být i nula), 4. server potvrdí příjem datového paketu opět paketem ‹ACK›, který zároveň slouží jako výzva k odeslání dalšího datového paketu, 5. odpověď ‹ACK› na poslední ‹DATA› paket signalizuje, že přenos skončil a klient může spojení uzavřít. Pakety, které odesílá server, vypadají takto: • ‹ACK› – začíná bajty ‹0x00 0x04›, další dva bajty určují pořadové číslo paketu, na který navazují (0 pro paket ‹WRQ›, 1 a výše pro pakety ‹DATA›), • ‹ERROR› – začíná bajty ‹0x00 0x05›, dále dvoubajtový kód chyby, řetězec popisující chybu a nulový bajt. Všechny dvoubajtové číselné hodnoty jsou odesílány v pořadí vyšší bajt, nižší bajt (tzv. „big endian“). Procedura ‹tftp_put› vrátí: • 0 – přenos byl úspěšný, • kladné číslo – přenos skončil chybou na straně serveru, návratový kód odpovídá kódu chyby z paketu ‹ERROR›, • -3 – server ukončil přenos s chybou 0 (paketem ‹ERROR›), • -2 – neočekávaný paket od serveru (špatný kód příkazu, špatné sekvenční číslo v paketu typu ‹ACK›, špatná velikost paketu), • -1 – nastala systémová chyba na straně klienta. int tfpt_put( const char *addr, const char *name, /* C */ const void *data, size_t size ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 4. [‹newsc›] V této přípravě je Vaším úkolem implementovat klient, který bude synchronizovat data ze serveru, a to tak, že přenese vždy jen ta data, která ještě nemá uložena lokálně. Server má uloženu sekvenci bajtů, která se může postupně prodlužovat, nemůže se ale ani zkrátit, ani se již uložené bajty nemohou měnit. Po navázání spojení klient odešle číslo ‹n› (zakódované do 4 bajtů, nejvýznamnější bajt první – tzn. v pořadí big endian). Server obratem odešle všechny své uložené bajty počínaje tím na indexu ‹n› a ukončí spojení. Hodnota ‹0xffff'ffff› je vyhrazená pro nahrávání dat na server (klient v tomto příkladu tuto hodnotu používat nebude). Naprogramujte proceduru ‹news_update›, která bude mít tyto dva parametry: • ‹state_fd› – popisovač souboru, který obsahuje již známá data, a do kterého budou nová data připsána, tzn. po ukončení ‹news_update› bude soubor obsahovat přesnou kopii dat ze serveru, • ‹addr› – adresa unixového socketu, na které server poslouchá. O popisovači ‹state_fd› nepředpokládejte nic jiného, než že se jedná o obyčejný soubor otevřený pro zápis (zejména může ukazovat na libovolnou pozici). Návratová hodnota 0 indikuje, že soubor ‹state› byl úspěšně doplněn. Nastane-li libovolná chyba, procedura vrátí hodnotu -1 a obsah souboru ‹state› nezmění. int news_update( int state_fd, const char *addr ); /* C */ Přestože implementujete znovupoužitelný podprogram, může být užitečné jej testovat interaktivně – tento program můžete spustit jako ‹./p4_newsc cesta_k_souboru_s_daty cesta_k_socketu› – v takovém případě se přiložené testy přeskočí. Viz též začátek procedury ‹main›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 5. [‹echod›] V této přípravě je Vaším úkolem naprogramovat jednoduchý server implementující protokol echo v proudově orientované verzi. Tento server od klienta přijme data a stejná mu odpoví. Bude tak protějškem ke klientu z ‹p1_echoc›. Naprogramujte proceduru ‹echo_server›, která přijímá jediný parametr: adresu unxiového socketu, který má vytvořit a na němž má poslouchat. Procedura ‹echo_server› vytváří server, který by měl běžet, dokud nebude zvenčí indikováno, že má skončit. Toho zde docílíme zabitím procesu (např. signálem jako v testech níže). Pro zjednodušení «není» třeba tento případ v kódu zachycovat (např. zpracováním signálů), jelikož to je nad rámec tohoto kurzu. Na proceduru ‹echo_server› tedy pohlížíme jako na kompletní program a po zavolání neočekáváme, že se z ní program vrátí. Jestliže nastane chyba při vytváření serveru, ukončí se «program» s návratovým kódem ‹2›. Pokud vše proběhne v pořádku, server poslouchá na zadaném socketu a zpracovává spojení. Nastane-li chyba během komunikace s některým klientem, ukončí se toto spojení a program pokračuje čekáním na dalšího klienta (můžete v tomto případě vypsat nějaké varování na standardní chybový výstup). Není-li možné pokračovat, program skončí s chybou (rovněž návratový kód ‹2›). void echo_server( const char *sock_path ); /* C */ Jelikož implementujete kompletní server, může být užitečné jej testovat interaktivně – můžete ho spustit jako ./p5_echod cesta_k_socketu v takovém případě se přiložené testy přeskočí. Viz též začátek procedury ‹main›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 6. [‹newsd›] Tato příprava doplňuje předchozí ‹p4_newsc› – Vaším úkolem bude tentokrát naprogramovat odpovídající server. Krom tam popsaného protokolu implementujte také nahrávání dat – jsou-li první 4 bajty odeslané klientem ‹0xff 0xff 0xff 0xff›, následuje libovolný počet nenulových bajtů, které server připojí na konec své sekvence. První nulový bajt ukončí zpracování, načež server uzavře spojení. Procedura ‹news_server› bude mít jediný parametr – adresu unixového socketu, na které má poslouchat. Protože se jedná o uzavřený program, procedura ‹news_server› se nikdy nevrátí. Nastane-li chyba během komunikace s klientem, ukončí spojení a vyčká na dalšího klienta. Není-li toto možné, program ukončí s chybou. void news_server( const char *addr ); /* C */ Přestože implementujete znovupoužitelný podprogram, může být užitečné jej testovat interaktivně – tento program můžete spustit jako ‹./p6_newsd cesta_k_socketu› – v takovém případě se přiložené testy přeskočí. Viz též začátek procedury ‹main›. ## r. Řešené úlohy ### 1. [‹agree›] socket, recv, send, connect */ #include /* struct sockaddr_un */ #include /* close, unlink, fork, alarm */ #include /* waitpid */ #include /* kill, SIGTERM */ #include /* errno, ENOENT, ESRCH */ #include /* memcmp, memcpy, memset, strncpy */ #include /* err, warn */ Naprogramujte proceduru ‹agree›, která obdrží: • ‹sock_fd›, popisovač datagramového socketu, • ‹our_set›, ukazatel na pole, které popisuje podmnožinu ⟦S⟧ množiny ⟦{ k ∈ ℤ | 0 ≤ k < 512 }⟧, a to tak, že číslo ⟦i⟧ je přítomno právě když je v poli nastavený ⟦i⟧-tý bit,¹ • ‹intersection› – ukazatel na neinicializované pole stejné velikosti a stejného významu, do kterého uloží výsledek. Procedura (server) přijme od klienta datagram, který bude obsahovat množinu čísel ve stejné podobě, jakou má parametr ‹our_set›, provede průnik s ‹our_set› a tuto odešle jako odpověď klientovi. Klient výslednou množinu potvrdí nebo zamítne zasláním jednobajtového datagramu s hodnotou 1 (potvrzení) nebo 0 (zamítnutí). Můžete předpokládat, že všechny příchozí datagramy mají téhož odesílatele. TBD Předaný socket má nastavenou implicitní cílovou adresu. Návratová hodnota ‹agree› bude: • ‹-3› v případě, že se domluva nezdařila – klient vybranou množinu zamítl, • ‹-2› při chybě protokolu (klient poslal data v nečekaném tvaru), • ‹-1› v případě, že nastala systémová chyba na straně serveru, • konečně ‹0› byla-li podmnožina úspěšně domluvena, pak zároveň do ‹intersection› tuto uloží (v žádném jiném případě hodnoty v ‹intersection› měnit nebude). ¹ Bity počítáme od nejméně významného bitu prvního bajtu až po nejvýznamnější bit posledního bajtu v poli. int agree( int sock_fd, const char our_set[ 64 ], /* C */ char intersection[ 64 ] ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹filter›] Naprogramujte proceduru ‹filter›, která obdrží: • ‹fd_in›, popisovač datagramového socketu, na kterém bude očekávat příchozí pakety, • ‹fd_out› na který bude některé pakety přeposílat – tento socket má nastavenu implicitní adresu (je „připojený“), • ‹offset› – pořadové číslo 32bitového slova, které budeme kontrolovat, • ‹threshold› – 32bitové číslo bez znaménka, které určí, zda se má daný datagram přeposlat nebo zahodit. Procedura ‹filter› pro každý obdržený datagram rozhodne, zda se má přeposlat, takto: 1. neobsahuje-li datagram celé 32bitové slovo, které by začínalo na indexu ‹offset›, datagram je zamítnut (zahozen), 2. je-li slovo přítomno a jeho hodnota je alespoň ‹threshold›, datagram je přeposlán. Předpokládejte, že jednotlivý datagram není větší než 4KiB. void filter( int fd_in, int fd_out, /* C */ int offset, uint32_t threshold ); ### 6. [‹tftpr›] Naprogramujte proceduru ‹tftp_get›, která ze serveru protokolu TFTP¹ stáhne zadaný soubor a zapíše ho do předaného popisovače obyčejného souboru. Socket, který je proceduře ‹tftp_get› předán má nastavenu implicitní adresu příjemce (je „připojený“). Výsledkem je: • ‹-2› server ohlásil chybu, • ‹-1› nastala systémová chyba na straně klientu, • ‹0› soubor byl úspěšně převzat a uložen. ¹ TODO popsat RRQ část protokolu. int tftp_get( const unsigned char *id, int id_len, /* C */ int sock_fd, int out_fd ); # Blokující vstup a výstup, čekání na událost Ukázky: 1. ‹count› – počítání přijatých bajtů 2. ‹poll› – souběžná komunikace na dvou popisovačích Přípravy: 1. ‹min› – jednoduchý datagramový server, 2. ‹read› – read loop pro pevně velký záznam 3. ‹ready› – nalezení popisovače připraveného ke čtení 4. ‹collect› – čtení dat ze skupiny popisovačů 5. ‹fget› – blokový protokol (posílá bloky, čte potvrzení) 6. ‹fput› – podobně, ale přijímá bloky, posílá potvrzení ## Komunikace a blokování V této kapitole se budeme zabývat situací, kdy čtení a zápis můžou «blokovat» (čekat, angl. «block»). Podobně jako odeslání nebo přijetí datagramu může čtení a zápis představovat komunikaci.¹ Rozdílem zde je, že ‹read› a ‹write› používáme s rourami nebo spojovanými sockety – pracují tedy s «proudem dat», který nemá žádné jasně určené hranice, jako tomu bylo u datagramů. Zejména nemůžeme předpokládat, že logické celky protokolu (to, co bychom intuitivně považovali za zprávu) budou přečteny jako jeden celek. Operace ‹write› v blokujícím režimu (tak jak ji tento týden budeme používat) je poměrně jednoduchá – i v případě, kdy dochází ke komunikaci, provede ‹write› vždy kompletní zápis.² Z pohledu zapisujícího se tedy jedná o operaci s daty «pevné velikosti» (velikost určuje třetí parametr). Operace ‹read› je složitější, zejména proto, že nemůžeme spolehlivě předvídat, kolik druhá strana odešle dat. Čtení z proudu dat je proto nutně operací s daty «proměnné velikosti»³ – se všemi komplikacemi, které se s tím pojí. Jedno pravidlo zůstává v platnosti – vrátí-li ‹read› nulu, žádná další data již nebude možné přečíst – v tomto případě to znamená, že protistrana uzavřela komunikační kanál (u souborů to značilo konec souboru). ¹ V první kapitole jsme operace ‹read› a ‹write› používali pouze na pasivní objekty – soubory – a měli jsme tak zaručeno, že data budou k dispozici ihned. ² Zápis menšího počtu bajtů, než kolik bylo vyžádáno, může nastat pouze na «neblokujícím» popisovači – tímto se budeme zabývat v příští kapitole – nebo při «obsluze» signálu (pozor, nikoliv při jeho doručení) – téma, které jde mimo rámec tohoto předmětu. Naopak situace, kdy pro zápis není dostatek místa v souborovém systému, při komunikaci nastat nemůže. ³ Mohlo by se zdát, že by operace ‹read› mohla pracovat podobně jako u souborů, totiž jednoduše vyčkat, až bude k dispozici dostatek dat, aby se naplnil dodaný buffer. Rozmyslete si, že takové řešení by automaticky vedlo k uváznutí, kdykoliv protistrana čeká na odpověď (zavolá blokující ‹read›) po odeslání kratší zprávy, než jakou očekáváme. ## Zpracování událostí Klíčovým stavebním prvkem interaktivních aplikací je tzv. «event loop» – konstrukce, která umožňuje přejít od klasického sekvenčního programu, jaký dobře znáte z předchozích kurzů programování, k tzv. «reaktivním» programům (jsou známé také pod názvem programy řízené událostmi).¹ Standardní «event loop» je postaven na službě operačního systému ‹poll›, nebo některé její variantě/rozšíření. Její základní myšlenkou je pozastavit vykonávání programu do chvíle, než je splněna nějaká podmínka.² Tento princip známe již z předešlé kapitoly, kde funkce ‹recv› nebo ‹send› mohou «blokovat» – program je «uvnitř» systémového volání uspán a je mu umožněno pokračovat ve výpočtu až ve chvíli, kdy je operaci možné dokončit. Z pohledu toku řízení programu se program na delší dobu „zasekne“ ve volání ‹recv›. Připravenost pokračovat ve výpočtu je pak programu „oznámena“ prostým návratem z volání ‹recv›. Princip volání ‹poll›³ je stejný, umožňuje nám ale najednou uvést větší množství podmínek pro probuzení (= pokračování ve výpočtu = návrat z volání ‹poll›). Je-li tedy potřeba reagovat na jednu z vyjmenovaných situací, program je operačním systémem probuzen – volání ‹poll› se vrátí. Jeho výsledkem je pak popis těch podmínek, které vedly k probuzení programu. ¹ Opět se jedná o klíčovou součást implementace řady známých aplikací. Známé implementace můžete potkat například v ‹node.js› (pro programy v jazyce C je tato dostupná pod názvem ‹libuv›), v interpretu jazyka Python (součást modulu ‹asyncio›), ve většině knihoven pro tvorbu nativních aplikací (gtk, qt), atd. Většina nativních serverových programů obsahuje vlastní event loop. ² Existují zde dvě základní filozofie – tzv. level-triggered (rozhodují se na základě splněných podmínek) a edge-triggered (rozhodují se podle nastalých událostí). Funkce ‹poll›, která je součástí standardu POSIX, a kterou se zde budeme zabývat, patří do rodiny level-triggered funkcí. Rozšíření systému Linux ‹epoll› nabízí také edge-triggered režim. ³ Krom samotného systémového volání ‹poll› existuje obdobné pod názvem ‹select› – pracuje na stejném principu, liší se pouze způsobem předání parametrů a návratové hodnoty. Protože řada existujících programů používá toto starší rozhraní, přikládáme i ukázku použití volání ‹select›. ## Systémová volání Jedinou novinkou je v této kapitole systémové volání ‹poll›. ## Knihovní podprogramy • ‹memmove› ## d. Demonstrace (ukázky) ### 1. [‹count›] V této ukázce navrhneme jednoduchý podprogram, který přečte veškerá data z bajtově orientovaného socketu (případně roury) a vrátí celkový počet přečtených bajtů. int count( int fd ) /* C */ { const int nbytes = 256; int bytes_read, total = 0; char buffer[ nbytes ]; Jak bylo zmíněno již v úvodu, «krátké čtení» je situace, kterou musíme při bajtově orientované (proudové) komunikaci řešit prakticky vždy. Zkuste si cyklus, ve kterém čtení probíhá, nahradit jediným voláním ‹read›. Srovnejte chování původního a upraveného programu. Srovnejte také výstup z programu ‹strace -f›. while ( 1 ) /* C */ { if ( ( bytes_read = read( fd, buffer, nbytes ) ) == -1 ) return -1; Nulová návratová hodnota značí, že protistrana spojení uzavřela, a žádná další data už doručena nebudou. if ( bytes_read == 0 ) /* C */ return total; total += bytes_read; /* C */ } } static void close_or_warn( int fd, const char *name ) /* C */ { if ( close( fd ) == -1 ) warn( "closing %s", name ); } int main( void ) /* demo */ /* C */ { int fds[ 2 ]; int bytes, status; Všimněte si, že používáme spojovaný, proudový socket – ‹SOCK_STREAM›. if ( socketpair( AF_UNIX, SOCK_STREAM, 0, fds ) == -1 ) /* C */ err( 1, "socketpair" ); Voláním ‹fork› vytvoříme nový proces, který bude na socket posílat data. V hlavním procesu budeme počítat bajty použitím podprogramu ‹count›. pid_t pid = fork(); /* C */ alarm( 5 ); /* die after 5 seconds if we get stuck */ if ( pid == -1 ) /* C */ err( 1, "fork" ); Potomek bude postupně zapisovat data do socketu. if ( pid == 0 ) /* child */ /* C */ { close_or_warn( fds[ 1 ], "receiver side of a socketpair" ); if ( write( fds[ 0 ], "hel", 3 ) == -1 || /* C */ write( fds[ 0 ], "lo ", 3 ) == -1 || write( fds[ 0 ], "world", 5 ) == -1 ) { err( 1, "writing to the socket" ); } close_or_warn( fds[ 0 ], "sender side of a socketpair" ); /* C */ return 0; } close_or_warn( fds[ 0 ], "sender side of a socketpair" ); /* C */ Výše uvedený podprogram bude běžet až do chvíle, než protistrana neuzavře spojení. Všimněte si, že přesto, že se celá zpráva pohodlně vejde if ( ( bytes = count( fds[ 1 ] ) ) == -1 ) /* C */ err( 1, "counting bytes" ); Ověříme, že počet přečtených bajtů odpovídá počtu odeslaných bajtů. Při analýze programem ‹strace› si můžete všimnout, že v tomto scénáři se ‹read› v podprogramu ‹count› zavolá celkem 4× – jednou pro každý ‹write› výše a jednou po ukončení komunikace, s nulovou návratovou hodnotou. «Pozor!» Toto chování je sice obvyklé, ale není ničím a nijak zaručeno. Navíc nemůžete předpokládat, že protistrana provedla nějaký konkrétní zápis v jedné operaci – zprávy mohou být z různých důvodů odesílatelem děleny na celky, které nijak neodpovídají logické struktuře protokolu. assert( bytes == 3 + 3 + 5 ); /* C */ Vyčkáme na ukončení procesu–zapisovatele, ověříme, že skončil bez chyb a následně celý program ukončíme. if ( waitpid( pid, &status, 0 ) == -1 ) /* C */ err( 1, "waiting for child %d", pid ); assert( WIFEXITED( status ) ); /* C */ assert( WEXITSTATUS( status ) == 0 ); close_or_warn( fds[ 1 ], "receiver side of a socketpair" ); /* C */ return 0; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹poll›] V této ukázce se budeme zabývat systémovým voláním ‹poll› – uvažme situaci, kdy máme dva popisovače, které oba slouží ke komunikaci (pro účely ukázky použijeme datagramové sockety, ale ‹poll› lze stejným způsobem používat i s jinými typy popisovačů). Podprogram ‹pong› bude odpovídat na zprávy na libovolném z obou popisovačů, aniž by docházelo k prodlevám při odpovědi v situaci, kdy probíhá komunikace pouze na jednom z obou popisovačů. Samotnou komunikaci s protistranou delegujeme na pomocný podprogram, ‹recv_and_reply›, který implementujeme níže. void recv_and_reply( int fd ); /* C */ void pong( int fd_1, int fd_2 ) /* C */ { Pro použití systémového volání ‹poll› budeme potřebovat pole struktur typu ‹pollfd›, které popisují, na jaké podmínky hodláme čekat. Volání ‹poll› bude ukončeno, jakmile nastane libovolná z nich. Které podmínky nastaly pak přečteme ze stejného pole – volání ‹poll› před návratem upraví předané položky ‹revents›. struct pollfd pfds[ 2 ]; /* C */ Každá instance ‹pollfd› se váže k jednomu popisovači: položka ‹fd› určí tento popisovač, zatímco položka ‹events› určí, jaké podmínky na tomto popisovači nás zajímají. To, jestli podmínka platila ještě před voláním ‹poll› nebo začala platit až později nerozhoduje (jak bylo zmíněno v úvodu, ‹poll› poskytuje tzv. level-triggered rozhraní). pfds[ 0 ].fd = fd_1; /* C */ pfds[ 1 ].fd = fd_2; Podmínky nastavujeme položkou ‹events› – pro nás v tuto chvíli nejdůležitější je ‹POLLIN›, která indikuje, že z deskriptoru je možné «bez blokování» získat data, tzn. «jedno» volání ‹recv› (nebo ‹read›) na tomto popisovači se vrátí ihned a předá programu nějaká data (kolik dat to bude nám volání ‹poll› nesdělí). pfds[ 0 ].events = POLLIN; /* C */ pfds[ 1 ].events = POLLIN; Položku ‹revents› inicializovat nemusíme – každé volání ‹poll› ji přepíše aktuálním stavem popisovače. Tím je nastavení parametrů pro ‹poll› u konce, můžeme tedy přistoupit k hlavnímu cyklu: while ( 1 ) /* C */ { Bezpodmínečně zavolat ‹recv› na některém z popisovačů by v tuto chvíli mohlo zablokovat program (případně i vést k uváznutí). Proto musíme začít voláním ‹poll›. Krom ukazatele na pole struktur ‹pollfd› mu předáme počet položek tohoto pole a maximální dobu (počet milisekund), po kterou hodláme čekat. V tomto předmětu to bude prakticky vždy -1 – budeme čekat libovolně dlouho. if ( poll( pfds, 2, -1 ) == -1 ) /* C */ err( 1, "poll" ); Návratové hodnoty jiné než -1 nás nemusí v tuto chvíli zajímat – veškeré informace, které potřebujeme, jsou obsaženy v poli ‹pfds› v položkách ‹revents›. Samotnou komunikaci provede podprogram ‹recv_and_reply› (který má zaručeno, že předaný popisovač je připraven ke čtení). for ( int i = 0; i < 2; ++i ) /* C */ if ( pfds[ i ].revents & POLLIN ) recv_and_reply( pfds[ i ].fd ); Tím je procedura ‹pong› hotova – bude čekat a odpovídat na obou popisovačích souběžně, i přesto, že se jedná o na první pohled zcela sekvenční program. Níže si tuto funkčnost otestujeme. } /* C */ } void recv_and_reply( int fd ) /* C */ { V tomto místě máme zajištěno, že ‹recv› na popisovači ‹fd› nám vrátí nějakou zprávu. Nebudeme s ní dělat nic jiného, než že protistraně pošleme velikost zprávy, kterou jsme obdrželi, jako čtyřbajtové číslo. Omezíme se na 4096 bajtů – pro větší zprávy odpovíme 4096. uint8_t message[ 4096 ]; /* C */ int bytes = recv( fd, message, 4096, 0 ); if ( bytes == -1 ) /* C */ err( 1, "recv" ); Jak je v komunikačních protokolech zvykem, slovo (číslo) odešleme v pořadí nejvýznamnější bajt první (big endian). Využijeme k tomu knihovní funkci ‹htonl› («h»ost «to» «n»etwork, «l»ong). uint32_t reply = htonl( bytes ); /* C */ Nezbývá, než zprávu odeslat. Zde budeme trochu podvádět, a spolehneme se na to, že odeslání proběhne okamžitě – předpoklad, který není zcela neopodstatněný, protože odesíláme malé zprávy a lze očekávat, že ve vyrovnávací paměti operačního systému je pro zprávu dostatek místa. Ve skutečném programu bychom ale na tomto místě měli použít neblokující operaci v součinnosti s voláním ‹poll› – tím se budeme ale zabývat až příště. if ( send( fd, &reply, 4, 0 ) == -1 ) /* C */ err( 1, "send" ); Protože odesíláme datagram, situace, že bude odesláno méně bajtů než bylo požadováno nastat nemůže (jednalo by se o chybu a výsledek by byl -1). Samozřejmě, můžete si tuto skutečnost pojistit (jestli voláním ‹assert› nebo ‹errx› je otázka osobních preferencí). } /* C */ static void close_or_warn( int fd, const char *name ) /* C */ { if ( close( fd ) == -1 ) warn( "closing %s", name ); } int main( void ) /* demo */ /* C */ { int fds_1[ 2 ], fds_2[ 2 ]; if ( socketpair( AF_UNIX, SOCK_DGRAM, 0, fds_1 ) == -1 || /* C */ socketpair( AF_UNIX, SOCK_DGRAM, 0, fds_2 ) == -1 ) { err( 1, "socketpair" ); } Voláním ‹fork› vytvoříme nový proces, který bude sloužit jako testovací server – spustíme v něm proceduru ‹pong›. Hlavní proces bude tento server testovat posíláním zpráv. pid_t pid = fork(); /* C */ alarm( 5 ); /* die after 5 seconds if we get stuck */ if ( pid == -1 ) /* C */ err( 1, "fork" ); if ( pid == 0 ) /* child */ /* C */ { close_or_warn( fds_1[ 1 ], "client side of a socketpair" ); close_or_warn( fds_2[ 1 ], "client side of a socketpair" ); pong( fds_1[ 0 ], fds_2[ 0 ] ); exit( 1 ); /* should never return */ } close_or_warn( fds_1[ 0 ], "server side of a socketpair" ); /* C */ close_or_warn( fds_2[ 0 ], "server side of a socketpair" ); int fd_1 = fds_1[ 1 ], /* C */ fd_2 = fds_2[ 1 ]; uint32_t reply_1, reply_2; /* C */ Zde zvolené (nebo analogické) pořadí ‹send›/‹recv› by v případě, že ‹pong› nepracuje správně, vedlo k uváznutí. Zkuste si zakomentovat volání ‹poll› výše a program spustit v této verzi. Ověřte si také, že v pořadí send 1, send 2, recv 1, recv 2 program pracuje i bez volání ‹poll›. Rozmyslete si, proč tomu tak je. if ( send( fd_1, "hello", 5, 0 ) == -1 ) /* C */ err( 1, "sending hello" ); if ( send( fd_2, "bye", 3, 0 ) == -1 ) err( 1, "sending hello" ); if ( recv( fd_2, &reply_2, 4, 0 ) == -1 ) /* C */ err( 1, "recv on fd_2" ); if ( recv( fd_1, &reply_1, 4, 0 ) == -1 ) err( 1, "recv on fd_1" ); assert( ntohl( reply_1 ) == 5 ); /* C */ assert( ntohl( reply_2 ) == 3 ); int status; /* C */ if ( kill( pid, SIGTERM ) == -1 || /* C */ waitpid( pid, &status, 0 ) == -1 ) err( 1, "terminating child process" ); assert( WIFSIGNALED( status ) ); /* C */ assert( WTERMSIG( status ) == SIGTERM ); close_or_warn( fd_1, "client side of a socketpair" ); /* C */ close_or_warn( fd_2, "client side of a socketpair" ); return 0; /* C */ } ## p. Přípravy ### 1. [‹min›] V této přípravě je Vaším úkolem naprogramovat datagramový server, který od každého z ⟦N⟧ klientů obdrží jedno číslo, nalezne mezi nimi minimum a toto vrátí. Čísla v rozsahu 0 až 2³¹ - 1 budou klienty posílána jako čtyřbajtové pakety, nejvýznamnější bajt první (tzn. big endian). Naprogramujte proceduru ‹uniqmin_server›, která jako parametry dostane adresu unixového socketu, na kterém má poslouchat, a kladné číslo ‹n›. Po ukončení protokolu (tzn. potom, co obdrží všech ‹n› čísel od klientů) uklidí případné lokálně alokované zdroje a výsledné minimum vrátí volajícímu. Nastane-li chyba protokolu (některý klient pošle paket v nesprávném formátu), vrátí -2, ale až po přijetí paketů od všech klientů. Nastane-li nějaká systémová chyba, vrátí -1 a nastaví hodnotu ‹errno› odpovídajícím způsobem. int uniqmin_server( int fd, int n ); /* C */ ### 2. [‹read›] Při použití rour nebo spojovaných socketů musíme počítat s tím, že systémové volání ‹read› načte menší počet bajtů, než kolik jsme vyžádali, nebo než kolik by se nám hodilo. Naprogramujte proceduru ‹read_buffer›, která z popisovače načte právě zadaný počet bajtů ‹nbytes›. Procedura bude blokovat tak dlouho, než se příslušný počet bajtů podaří přečíst, nebo načte všechny bajty, které druhá strana zapsala před ukončením komunikace. Výsledkem nechť je počet skutečně přečtených bajtů, nebo -1 v případě, že při čtení došlo k systémové chybě. «Pozor!» Tuto proceduru je možné bezpečně použít pouze v situaci, kdy je zaručeno, že druhá strana alespoň ‹nbytes› bajtů skutečně zapíše, nebo komunikaci ukončí. Odešle-li druhá strana menší počet bajtů a pak bude čekat na odpověď, povede použití procedury ‹read_buffer› k uváznutí.¹ ¹ Jak krátké čtení řešit u protokolů s proměnnou délkou zprávy si ukážeme v páté kapitole. int read_buffer( int fd, char *buffer, int nbytes ); /* C */ ### 3. [‹ready›] Vaším úkolem je implementovat podprogram ‹readyfd›, který obdrží tyto dva parametry: • ‹bits› je ukazatel na bitové pole, • ‹count› je velikost takto předaného pole v bitech. Každý nenulový bit v předaném bitovém poli označuje popisovač, na který se volající dotazuje. Funkce vrátí hodnotu nejnižšího z dotazovaných popisovačů, z něhož je možné «číst». Není-li možné číst z žádného z nich, program zablokuje do doby, než to možné bude. Bitového pole je předáno v ⌈count/8⌉ bajtech, přitom jednotlivé bity jsou indexovány následovně: • bajt s nejnižší adresou odpovídá popisovačům 0–7, • následující bajt popisovače 8–15, atd., • uvnitř každého bajtu odpovídá nejméně významný bit popisovači s nejnižším číslem. Návratová hodnota je číslo popisovače připraveného ke čtení, nebo -1 nastane-li chyba. Krom systémových chyb ošetřete i případ, kdy je vstupní množina popisovačů prázdná – v takové situaci nastavte ‹errno› na ‹EDEADLK›. int readyfd( unsigned char* bits, int count ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 4. [‹collect›] Uvažme situaci, kdy potřebujeme číst data z několika zdrojů zároveň, přitom tato data přichází různou rychlostí. Navrhněte a naprogramujte podprogram ‹collect›, který obdrží: • ‹count› – počet popisovačů, se kterými bude pracovat, • ‹fds› – ukazatel na pole popisovačů, • ‹buffers› – ukazatel na pole ukazatelů, kde každý ukazatel určuje paměť, do které se načtou data z odpovídajícího popisovače, • ‹sizes› – ukazatel na pole čísel, které určí, kolik nejvýše bajtů se má z odpovídajícího popisovače načíst, • ‹pivot› – index popisovače (význam viz níže). Uživatel bude podprogram volat opakovaně, vždy když bude připraven zpracovat další data. Při každém volání podprogram ‹collect›: • načte data z jednoho popisovače, • posune odpovídající ukazatel v poli ‹buffers› za konec právě načtených dat, • sníží odpovídající hodnotu v poli ‹sizes› o počet načtených bajtů, • vrátí index dotčeného popisovače. Není-li žádný popisovač ke čtení připraven, funkce blokuje, než se situace změní. Je-li připraven více než jeden popisovač, použije se ten s nejnižším indexem větším nebo rovným hodnotě ‹pivot›. Jsou-li všechny připravené popisovače na indexech menších než ‹pivot›, použije se nejnižší z nich. Dojde-li k systémové chybě, podprogram vrátí hodnotu -1 a zařídí, aby se budoucí volání se stejnými parametry pokusilo neprovedené akce zopakovat. int collect( int count, int *fds, char **buffers, /* C */ int *sizes, int pivot ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 5. [‹bget›] V této přípravě je Vaším úkolem naprogramovat klient pro jednoduchý spojovaný protokol pro přenos bloků dat. Po připojení klienta začne server ihned odesílat jednotlivé 512-bajtové bloky. Po odeslání každého bloku vyčká server na jednobajtovou potvrzovací zprávu od klienta, že tento data přijal a bezpečně uložil (aby bylo možné data na serveru smazat). Odpověď klienta bude ‹0x01› proběhlo-li uložení bloku v pořádku a ‹0x02› v případě opačném. V reakci na odpověď ‹0x01› server: • pošle další blok, je-li nějaký k dispozici, • jinak zavře spojení. V reakci na odpověď ‹0x02› server ukončí spojení (již bez další komunikace). Procedura ‹bget› obdrží popisovač spojovaného socketu a popisovač souboru, do kterého bude data od serveru zapisovat. Po zapsání každého bloku použije na popisovač systémové volání ‹fdatasync› – úspěch tohoto volání pak potvrdí serveru odesláním bajtu ‹0x01›. Návratová hodnota ‹bget› bude: • 0 v případě úspěchu (všechna data, která server odeslal, byla uložena), • -2 v případě, že byla komunikace ukončena kvůli selhání zápisu dat do souboru a • -1 v případě, že nastala chyba komunikace. Do výstupního parametru ‹block_count› pak uloží počet bloků, které byly úspěšně uloženy do souboru, bez ohledu na celkový výsledek. int bget( int sock_fd, int file_fd, int *block_count ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 6. [‹bput›] V této přípravě je Vaším úkolem naprogramovat klient pro jednoduchý spojovaný protokol pro přenos bloků dat. Po připojení klienta server potvrdí připravenost přijímat data jednobajtovou zprávou ‹0x01›. Klient tak může odeslat první 512bajtový blok, načež vyčká na potvrzení od serveru, který odpoví opět jednobajtovou zprávou – ‹0x01› v případě úspěchu a ‹0x02› v případě, že data nemohla být zapsána. Klient vždy před odesláním dalšího bloku vyčká na potvrzení. Konec přenosu klient oznámí zavřením spojení. Procedura ‹bput› obdrží popisovač spojovaného socketu a popisovač souboru, ze kterého bude data načítat. Není-li velikost předaného souboru dělitelná 512, procedura se ihned vrátí s chybou -1 (aniž by jakkoliv komunikovala s protistranou). Aby mohla být data ze souboru efektivně odstraněna, budeme bloky odesílat od konce souboru a každý blok, kterého příjem byl protistranou potvrzen, ze souboru odstraníme voláním ‹ftruncate›. Návratová hodnota ‹bput› bude: • 0 v případě úspěchu – server potvrdil všechny zprávy a soubor s daty k odeslání je prázdný, • -3 je-li předán nesprávný datový soubor, • -2 v případě, že byla komunikace ukončena kvůli selhání na straně serveru (server indikoval, že data nebyl schopen uložit), • -1 v případě, že nastala chyba komunikace nebo systémová chyba na straně klienta. Do výstupního parametru ‹block_count› pak uloží počet bloků, které byly úspěšně odeslány a potvrzeny, bez ohledu na celkový výsledek. Popisovač spojení vždy před návratem uzavře, popisovač souboru nikoliv. int bput( int sock_fd, int file_fd, int *block_count ); /* C */ ## r. Řešené úlohy ### 1. [‹clone›] Naprogramujte proceduru ‹clone_stream›, která dostane jako parametry 3 popisovače připojených proudových socketů. Jejím úkolem bude přeposílat data příchozí na první z nich ‹in_fd› do dvou dalších (‹out1_fd›, ‹out2_fd›). Klonování bude probíhat synchronně – nestíhá-li protistrana na některém výstupním popisovači (‹out1_fd›, ‹out2_fd›) data dostatečně rychle zpracovávat, ‹clone_stream› nebude další data (z ‹in_fd›) načítat, dokud se situace nezlepší (a tím případně regulovat produkci dat na zdroji). Procedura ‹clone_stream› skončí, je-li ukončeno spojení na ‹in_fd›, nebo na obou výstupech (jinak řečeno, přeposílání bude pokračovat tak dlouho, dokud je připojen odesílatel a alespoň jeden z příjemců). Návratová hodnota nechť je 0, došlo-li ke korektnímu ukončení spojení, nebo -1 při fatální chybě. int clone_stream( int in_fd, int out1_fd, int out2_fd ); /* C */ ### 2. [‹bridge›] Naprogramujte podprogram ‹bridge›, který obdrží dva popisovače, přičemž data, která přichází na jeden z nich, přepošle na ten opačný bez zbytečné prodlevy – komunikaci může blokovat pouze situace, kdy některá strana nestíhá data přijímat. Pro účely tohoto příkladu je ale v pořádku, budou-li v této situaci blokovány oba směry.¹ Návratová hodnota budiž -1 při systémové chybě a 0, pokud komunikace úspěšně skončí uzavřením spojení na některém z popisovačů. int bridge( int fd_1, int fd_2 ); /* C */ ¹ Můžete si samozřejmě zkusit naprogramovat řešení, které bude schopno data ve volném směru přeposílat i když je ten druhý zablokovaný. Budete k tomu potřebovat ‹POLLOUT› a ‹O_NONBLOCK› – prostředky, o kterých se víc dozvíte v další kapitole. ### 3. [‹slip›] Naprogramujte proceduru ‹decode_slip›, která dostane dva popisovače – ‹in_fd›, který odkazuje na připojený proudový socket, a ‹out_fd›, který odkazuje na datagramový socket s nastaveným implicitním příjemcem. Z ‹in_fd› bude číst velmi jednoduchý zapouzdřovací protokol: každý datagram je zakódován do jednoho rámce, přitom jednotlivé rámce jsou v proudu dat odděleny bajtem ‹0xc0›. Oddělovač se v takto vymezených rámcích (a tedy ani odpovídajících datagramech) nesmí objevit.¹ Můžete předpokládat, že velikost jednoho rámce nepřesáhne 512 bajtů. Procedura ‹decode_slip› konečně z každého rámce vytvoří jeden datagram, který odešle na ‹out_fd›. int decode_slip( int in_fd, int out_fd ); /* C */ ¹ Jako možné rozšíření můžete naprogramovat verzi, kde budou bajty ‹0xc0› v datagramech povoleny – v rámcích jsou pak nahrazeny sekvencí ‹0xdb 0xdc› a bajty ‹0xdb› z původních datagramů jsou nahrazeny sekvencí ‹0xdb 0xdd›. ### 4. [‹seq›] V tomto cvičení budeme realizovat opačný převod, než v tom předchozím, zvolíme ale jiný typ zapouzdření – každému rámci předchází dva bajty kódující jeho délku (významnější bajt první). Procedura ‹packet_seq› obdrží popisovač ‹in_fd› na kterém bude přijímat datagramy, které zakóduje výše uvedeným způsobem do proudu, který bude zapisovat do popisovače ‹out_fd›, který odkazuje na připojený proudový socket. Je-li přijatý datagram příliš velký, přebytečné bajty ignorujte. Uzavře-li protistrana výstupního popisovače spojení, proceduru ‹packet_seq› považujeme za úspěšně dokončenou (s návratovou hodnotou 0). V případě selhání nechť vrátí hodnotu -1. int packet_seq( int in_fd, int out_fd ); /* C */ ### 5. [‹log›] Vaším úkolem je naprogramovat proceduru ‹log_stream›, která bude přeposílat data z jednoho připojeného proudového socketu na druhý, a přitom bude v souboru udržovat záznam o takto přeposlaných datech. Parametry: • ‹in_fd› – popisovač, ze kterého bude data číst, • ‹out_fd› – popisovač, do kterého bude data přeposílat, • ‹log_fd› – popisovač souboru, ve kterém bude udržovat záznamy, • ‹limit› – limit na počet bajtů uložených v souboru ‹log_fd›. Dojde-li během přenosu kapacita logovacího souboru, tento bude zkrácen na polovinu limitu smazáním nejstarších dat. Návratová hodnota ‹-1› indikuje chybu, jinak procedura vrátí počet bajtů, které byly přeposlány, ale ze souboru ‹log_fd› byly pro nedostatek místa smazány. Poznámka: Vhodnou volbou velikosti čtení z popisovače ‹in_fd› se může řešení značně zjednodušit (zejména ji můžete přizpůsobit aktuálně volnému místu v logovacím souboru). int log_stream( int in_fd, int out_fd, int log_fd, int limit ); /* C */ ### 6. [‹pubsub›] Procedura ‹pubsub› (od „publish–subscribe“) obdrží pole připojených proudových popisovačů. Na každém z nich mohou přicházet dva typy zpráv, které rozlišíme podle prvního bajtu: • zpráva, která začíná nulovým bajtem, obsahuje pouze jeden další bajt, který označuje nenulový kód kategorie – jedná se o „subscribe“ část protokolu, tzn. odesílatel této zprávy si přeje dostávat zprávy z dané kategorie, • ve zprávě, která začíná nenulovým bajtem, tento označuje kód kategorie; tato zpráva dále obsahuje bajt, který značí jak dlouhý je zbytek zprávy – tento typ zprávy tvoří „publish“ část. Každá příchozí „publish“ zpráva bude přeposlána právě těm připojeným protistranám, které si předtím vyžádaly (zprávou „subscribe“) odběr příslušné kategorie zpráv. Zprávy ani jednoho typu není potřeba odesílateli potvrzovat. Je-li odesílatel odběratelem dané kategorie, zpráva je mu odeslána stejně, jako všem ostatním. Procedura ‹pubsub› skončí, jakmile se odpojí poslední z protistran. # Mapování do paměti, neblokující zápis V této kapitole se podíváme na dvě pokročilejší techniky vstupu a výstupu. První z nich se týká především obyčejných souborů, kterými jsme se zabývali v první kapitole: ukážeme si alternativu ke klasickým voláním ‹read› a ‹write› v podobě mapování souborů do paměti. Druhá část kapitoly pak uzavírá téma komunikace pomocí rour a proudových socketů z předchozí kapitoly. Budeme se zejména zabývat otázkou krátkého zápisu – situací analogickou ke krátkému čtení z předchozí kapitoly. Přípravy: 1. ‹sum› – sumarizace tabulky s pevně velkými záznamy 2. ‹list› – kontrola struktury záznamů 3. ‹flood› – in-situ flood fill v bitmapovém souboru 4. ‹write› – zápis bloku dat do neblokujícího popisovače 5. ‹hose› – souběžný zápis do několika popisovačů 6. ‹tee› ‡ – souběžný zápis s vyrovnávací pamětí Řešené: TBD. ## Mapování souborů do paměti Abychom mohli s daty uloženými v souboru pracovat, musíme je načíst do operační paměti – k tomu nám doposud sloužilo systémové volání ‹read›. Protože operační paměť má řádově menší kapacitu (a je to obecně mnohem vzácnější zdroj, než perzistentní úložiště), jsme často nuceni pracovat se soubory po částech.¹ Správa paměti spojená se vstupem a výstupem je díky tomu relativně nepohodlná. Mapování souborů do paměti nám umožňuje předstírat, že máme celý soubor načtený v operační paměti a tím řadu úkolů zjednodušit. Zároveň dává operačnímu systému kontrolu nad tím, která data budou skutečně v operační paměti uložena, a která se načtou až ve chvíli, kdy je program skutečně použije.² Pro interakci s tímto mechanismem nabízí POSIX tři systémová volání: • ‹mmap› vytvoří mapování – vstupem je popisovač obyčejného souboru, vlastnosti mapování (přístupová práva, sdílený vs soukromý přístup) a jeho rozsah, • ‹msync› vyžádá zápis změn, které program provedl v mapované paměti, zpátky do souboru na disku, a konečně • ‹munmap› již nepotřebné mapování uvolní a zároveň provede případné nedokončené zápisy do souboru. Mapování může být dvou základních typů: • «sdílené» (‹MAP_SHARED›) – změny provedené skrz toto mapování se projeví v souboru, do kterého takto mapované adresy odkazují, • «soukromé» (‹MAP_PRIVATE›) – změny provedené v mapované paměti zůstávají viditelné pouze v procesu, který mapování vlastní. «Pozor!» Není zaručeno, že v «soukromém» mapování se nebudou projevovat vnější změny souboru – přesné chování závisí na konkrétním operačním systému. Zároveň není zaručeno, že změny provedené ve «sdíleném» mapování budou viditelné pro ostatní vstupně-výstupní operace ihned, a to ani ze stejného procesu – operace ‹read› «může» vrátit verzi dat, kde se zápis skrze mapování ještě neprojevil. Přenos upraveného obsahu paměti do souboru je nutné vyžádat voláním ‹msync› nebo ‹munmap›. ¹ Hlavní problém představují velmi velké soubory. Ne každý program musí být schopen s velkými soubory pracovat, ale u řady programů očekáváme, že budou smysluplně fungovat i se soubory, které jsou mnohem větší než je dostupná operační paměť. A i v situaci, kdy je operační paměti dostatek, obvykle pro ni existuje lepší využití. ² Více informací o tom, jak tento mechanismus funguje na úrovni operačního systému, naleznete v sekcích 1.4 a 3.2 skript. ## Správa zdrojů V této kapitole se poprvé setkáváte se situací, kdy je Vámi vytvořený podprogram odpovědný za nějaký zdroj (konkrétně mapování vytvořené službou ‹mmap›). Platí zde několik jednoduchých, ale velmi důležitých pravidel: 1. každý zdroj, který byl vyžádán (alokován), musí být také vrácen (uvolněn), 2. odpovědnost za vrácení zdroje musí být jasně určena, 3. obě pravidla platí i v situaci, kdy nějaká operace selže. Nejjednodušší forma správy zdroje je spojena s «lexikálním rozsahem platnosti». Je zde zřejmá analogie s «lokální proměnnou», pro kterou je při vstupu do funkce alokována na zásobníku paměť a při opuštění funkce je takto vyhrazená paměť automaticky uvolněna. Mluvíme o «lokálním» (někdy také dočasném) zdroji. Odpovědnost za jeho uvolnění nese «vždy» podprogram (např. ‹subr›), který jej alokoval. Takto alokovaný zdroj může být předán dalšímu podprogramu, odpovědnost za uvolnění zdroje ale i tak zůstává na ‹subr›. Podprogramy, kterým je předán (propůjčen) zdroj spravovaný nějakou vnější entitou, a které tento zdroj pouze využívají, již dobře znáte. Složitější jsou podprogramy, které alokovaný zdroj předávají volajícímu (v návratové hodnotě, výstupním parametru, nebo jako součást nějaké složitější struktury). Těmito se zatím zabývat nebudeme – v této kapitole si vystačíte s lokálními zdroji. ## Neblokující zápis • ‹O_NONBLOCK› ## d. Demonstrace (ukázky) ### 1. [‹mode›] V této ukázce budeme pracovat se systémovým voláním ‹mmap› v režimu pouze pro čtení (zápis si předvedeme v dalším programu). Procedura ‹mode›, kterou budeme programovat níže, nalezne mód – nejčastější hodnotu – v datovém souboru, který obsahuje záznamy pevné délky. Abychom mohli počítat výskyty jednotlivých hodnot, budeme potřebovat slovník – nejjednodušší implementace slovníku je v tomto případě hašovací tabulka. Pro jednoduchost použijeme pevně velkou tabulku a konfliktní záznamy budeme udržovat v zřetězeném seznamu. Každý uzel bude obsahovat počítanou hodnotu ‹value›, která představuje klíč tabulky, a počítadlo výskytů ‹counter›. struct hash_node /* C */ { uint32_t value; int counter; struct hash_node *next; }; Samotná tabulka je pak pouze pole ukazatelů na hlavy takovýchto zřetězených seznamů. struct hash_table /* C */ { struct hash_node *buckets[ 65536 ]; }; Podprogram ‹hash_get› nalezne v tabulce uzel, který odpovídá klíči ‹value›. Není-li takový uzel v tabulce přítomný, do tabulky jej přidá. Selže-li alokace paměti, vrátí nulový ukazatel. struct hash_node *hash_get( struct hash_table *ht, uint32_t value ) /* C */ { uint32_t mux = value * 1077630871; uint16_t hash = ( mux & 0xffff ) ^ ( mux >> 16 ); struct hash_node *node; for ( node = ht->buckets[ hash ]; node != NULL ; node = node->next ) /* C */ if ( node->value == value ) break; if ( !node ) /* C */ { if ( !( node = malloc( sizeof( struct hash_node ) ) ) ) return NULL; node->next = ht->buckets[ hash ]; node->value = value; node->counter = 0; ht->buckets[ hash ] = node; } return node; /* C */ } Podprogram ‹hash_free› uvolní paměť alokovanou pro hašovací tabulku. void hash_free( struct hash_table *ht ) /* C */ { struct hash_node *n, *next; for ( int i = 0; i < 65536; ++i ) /* C */ for ( n = ht->buckets[ i ]; n != NULL; n = next ) { next = n->next; free( n ); } } Podprogram ‹mode› bude mít 4 parametry: • popisovač vstupního souboru, • počet čtyřbajtových slov v jednom záznamu ‹record_size›, • pořadové číslo čtyřbajtového slova v záznamu, které budeme zpracovávat ‹value_index›, • výstupní parametr ‹result›. Hodnoty, které budeme zpracovávat, budou čtyřbajtové, uložené od nejvýznamnějšího bajtu. int mode( int fd, int record_size, int value_index, /* C */ uint32_t *result ) { Nejprve zjistíme velikost souboru. Není-li tato dělitelná velikostí záznamu, jedná se o chybu a podprogram ukončíme. ssize_t size = lseek( fd, 0, SEEK_END ); /* C */ if ( size == -1 || size % record_size != 0 ) /* C */ return -1; Dále vytvoříme mapování souboru do paměti. První parametr systémové služby ‹mmap› bude vždy nulový ukazatel¹, dále jí předáme: • velikost mapování (v tomto případě velikost souboru), • přístupový mód (v tomto případě „pouze pro čtení“), • režim sdílení (‹MAP_PRIVATE› protože do souboru nehodláme zapisovat), • popisovač souboru, kterého obsah chceme do paměti mapovat, • místo, od kterého chceme obsah souboru mapovat (v bajtech, v tomto případě mapujeme celý soubor od prvního bajtu). uint32_t *base = mmap( NULL, size, PROT_READ, MAP_PRIVATE, fd, 0 ); /* C */ int retcode = -1; Výsledkem volání ‹mmap› je ukazatel na místo v paměti, kde mapování začíná. Jako každé systémové volání ale může ‹mmap› skončit chybou – trochu netradičně pro volání, které vrací ukazatel, vrátí ‹mmap› v případě chyby hodnotu -1 (přetypovanou na ukazatel). if ( base == ( void * ) -1 ) /* C */ return -1; Vytvoříme prázdnou hašovací tabulku a iterujeme všemi záznamy v souboru, přitom u každého si poznačíme výskyt příslušné hodnoty do tabulky. struct hash_table ht = { NULL }; /* C */ struct hash_node *node; for ( ssize_t offset = 0; offset < size / 4; offset += record_size ) /* C */ { uint32_t value = ntohl( base[ offset + value_index ] ); if ( !( node = hash_get( &ht, value ) ) ) goto out; node->counter ++; } Nyní nezbývá, než nalézt hodnotu s nejvyšším počtem výskytů. Je-li takových víc, vrátíme nejmenší z nich. *result = UINT32_MAX; /* C */ int max = 0; for ( int i = 0; i < 65536; ++i ) /* C */ for ( node = ht.buckets[ i ]; node != NULL; node = node->next ) if ( node->counter > max || ( node->counter == max && node->value < *result ) ) { *result = node->value; max = node->counter; } retcode = 0; /* C */ out: Nezapomeneme uvolnit zdroje – jak paměť alokovanou pro hašovací tabulku, tak mapování souboru do paměti. hash_free( &ht ); /* C */ if ( munmap( base, size ) == -1 ) warn( "unmapping region %p", base ); return retcode; } int main() /* demo */ /* C */ { const char *name = "zt.d1_data.bin"; int fd = open( name, O_CREAT | O_TRUNC | O_RDWR, 0666 ); if ( fd == -1 ) /* C */ err( 1, "creating file %s", name ); uint32_t buffer[ 65536 ]; /* C */ for ( int i = 0; i < 65536 ; ++i ) /* C */ switch ( i % 32 ) { case 0: buffer[ i ] = htonl( 7 ); break; case 1: buffer[ i ] = htonl( 1 + ( ( i - 1 ) % 64 == 0 ) ); break; default: buffer[ i ] = i; } for ( int i = 0; i < 256; ++i ) /* C */ if ( write( fd, buffer, sizeof buffer ) == -1 ) err( 1, "writing data into %s", name ); uint32_t result; /* C */ assert( mode( fd, 8, 0, &result ) == 0 ); /* C */ assert( result == 7 ); assert( mode( fd, 8, 1, &result ) == 0 ); assert( result == 1 ); if ( close( fd ) == -1 ) /* C */ warn( "closing %s", name ); return 0; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹iota›] V této ukázce navrhneme jednoduchý podprogram, který bude do dvojice rour zapisovat sekvenci postupně se zvyšujících čísel, až do nějakého předem zadaného limitu. Budeme ale požadovat, aby podprogram dodával data do každého popisovače tak rychle, jak jej protistrana čte. Zejména nás budou zajímat situace, kdy jedna strana čte mnohem pomaleji, než ta druhá (tento případ budeme níže i testovat). Jak je již dobrým zvykem, čísla budeme zapisovat nejvýznamnějším bajtem napřed. Aby byl zápis rozumně efektivní, nebudeme zapisovat každé čtyřbajtové číslo samostatně – místo toho předvyplníme vhodně velký buffer stoupající posloupností čísel, který pak odešleme najednou. Podprogram ‹iota_update› „posune“ sekvenci v takto nachystaném bufferu. Všimněte si, že vynulovaný buffer vyplní začátkem sekvence (od jedničky). Návratová hodnota určuje počet bajtů, které je potřeba zapsat. int iota_update( uint32_t *buffer, int count, uint32_t max ) /* C */ { uint32_t last = ntohl( buffer[ count - 1 ] ) + 1; int i = 0; for ( ; i < count && last + i <= max; ++i ) /* C */ buffer[ i ] = htonl( last + i ); return i * 4; /* C */ } Stav generátoru zapouzdříme do jednoduché struktury. Budeme potřebovat buffer pro odesílání dat a informaci o počtu bajtů, které je ještě potřeba ve stávajícím bufferu odeslat. Poznačíme si také samotný popisovač – podprogram ‹iota_pipe› tak bude jednodušší zapsat. Samotný zápis bude provádět pomocný podprogram ‹iota_write›, kterého implementaci naleznete níže. struct iota_state /* C */ { int fd; int nbytes; int offset; uint32_t buffer[ 64 ]; }; int iota_write( struct iota_state *state, /* C */ int buf_size, uint32_t max ); Vstupem pro ‹iota_pipe› budou jednak potřebné popisovače, jednak maximum do kterého má podprogram čísla generovat. Popisovače budou mít volajícím nastaveny příznak ‹O_NONBLOCK› (viz ‹main›) – znamená to, že výsledný zápis může být krátký (zapíše se méně bajtů, než bylo vyžádáno), a zároveň, že takové volání ‹write› nemůže program zablokovat. int iota_pipe( int fd_1, int fd_2, uint32_t max ) /* C */ { Protože zápis může probíhat různě rychle, budeme pro každý popisovač udržovat stav odděleně. Popisovač, pro který byl již zápis ukončen, uzavřeme, a do příslušné proměnné uložíme hodnotu -1. struct iota_state state[ 2 ] = /* C */ { { .fd = fd_1 }, { .fd = fd_2 } }; Dalším nutným prvkem efektivního řešení je systémové volání ‹poll›, které nám umožní čekat než bude některý popisovač připraven k zápisu. Jsou-li oba popisovače zablokované, opakované pokusy o zápis nikam nevedou, a pouze zatěžují systém zbytečnou prací. Připravenost k zápisu indikuje volání ‹poll› příznakem ‹POLLOUT›. Čísla popisovačů v poli ‹pfds› vyplníme až uvnitř hlavního cyklu, protože se mohou uvnitř podprogramu ‹iota_write› změnit. struct pollfd pfds[ 2 ]; /* C */ for ( int i = 0; i < 2; ++i ) /* C */ { state[ i ].offset = 0; state[ i ].nbytes = iota_update( state[ i ].buffer, 64, max ); pfds[ i ].events = POLLOUT; } Nyní je vše připraveno pro hlavní cyklus. while ( state[ 0 ].fd >= 0 || state[ 1 ].fd >= 0 ) /* C */ { for ( int i = 0; i < 2; ++i ) pfds[ i ].fd = state[ i ].fd; if ( poll( pfds, 2, -1 ) == -1 ) /* C */ return -1; for ( int i = 0; i < 2; ++i ) /* C */ if ( pfds[ i ].revents & POLLOUT ) if ( iota_write( state + i, 64, max ) == -1 ) return -1; } return 0; /* C */ } int iota_write( struct iota_state *state, /* C */ int buf_size, uint32_t max ) { Protože není zaručeno, že počet skutečně odeslaných bajtů bude dělitelný 4, všechny zarážky udržujeme v bajtech (nikoliv v položkách). Abychom ukazatel na místo v poli ‹buffer›, odkud chceme zapisovat, spočítali správně, musíme použít „bajtový“ ukazatel (vzpomeňte si, jak funguje ukazatelová aritmetika). uint8_t *data = ( uint8_t * ) state->buffer; /* C */ int written = write( state->fd, data + state->offset, /* C */ state->nbytes ); Při vstupu do podprogramu ‹iota_write› víme, že popisovač ‹state->fd› byl připraven k zápisu. Máme tedy jistotu, že i neblokující zápis nějaká data odešle – nevíme ale kolik jich bude. Proto musíme krom selhání řešit také krátký zápis. if ( written == -1 ) /* C */ return -1; state->offset += written; /* C */ state->nbytes -= written; Ověříme, zda v poli ‹buffer› zbývají nějaká data k zápisu. Pokud ne, vyplníme jej novými hodnotami a odpovídajícím způsobem přenastavíme zarážky ‹offset› a ‹nbytes›. if ( state->nbytes == 0 ) /* C */ { state->nbytes = iota_update( state->buffer, buf_size, max ); state->offset = 0; } Je-li stále počet bajtů k zápisu nulový, znamená to, že jsme vygenerovali a odeslali všechna požadovaná čísla. Popisovač uzavřeme a nastavíme mu hodnotu -1. Volání ‹poll› tím oznamujeme, že příslušná položka je nevyužitá (popisovače pro ‹poll› se přenastavují v podprogramu ‹iota_pipe› výše). if ( state->nbytes == 0 ) /* C */ { close( state->fd ); state->fd = -1; } return 0; /* C */ } static void close_or_warn( int fd, const char *name ) /* C */ { if ( close( fd ) == -1 ) warn( "closing %s", name ); } int main( void ) /* demo */ /* C */ { int fds_1[ 2 ], fds_2[ 2 ]; if ( socketpair( AF_UNIX, SOCK_STREAM, 0, fds_1 ) == -1 || /* C */ socketpair( AF_UNIX, SOCK_STREAM, 0, fds_2 ) == -1 ) { err( 1, "socketpair" ); } Voláním ‹fork› vytvoříme nový proces, který bude sloužit jako testovací generátor – spustíme v něm proceduru ‹iota_pipe›. Hlavní proces pak bude generátor testovat střídavým čtením z popisovačů. pid_t pid = fork(); /* C */ alarm( 120 ); /* die if we get stuck */ if ( pid == -1 ) /* C */ err( 1, "fork" ); if ( pid == 0 ) /* child */ /* C */ { close_or_warn( fds_1[ 1 ], "consumer side of a socketpair" ); close_or_warn( fds_2[ 1 ], "consumer side of a socketpair" ); Popisovače nastavíme do neblokujícího režimu systémovým voláním ‹fcntl›. Pro nastavení příznaků slouží režim ‹F_SETFL›. if ( fcntl( fds_1[ 0 ], F_SETFL, O_NONBLOCK ) == -1 || /* C */ fcntl( fds_2[ 0 ], F_SETFL, O_NONBLOCK ) == -1 ) err( 1, "setting O_NONBLOCK on generator sockets" ); if ( iota_pipe( fds_1[ 0 ], fds_2[ 0 ], 1 << 22 ) == -1 ) /* C */ err( 1, "iota_pipe unexpectedly failed" ); else exit( 0 ); /* success */ } close_or_warn( fds_1[ 0 ], "producer side of a socketpair" ); /* C */ close_or_warn( fds_2[ 0 ], "producer side of a socketpair" ); int fd_1 = fds_1[ 1 ], /* C */ fd_2 = fds_2[ 1 ]; uint32_t reply_1, reply_2; /* C */ Pro každé číslo, které přečteme z popisovače ‹fd_1› přečteme z popisovače ‹fd_2› čísel 8. Rozmyslete si, že kdyby generátor zapisoval data synchronně, pomalejší spojení by muselo na konci cyklu ve vyrovnávací paměti udržovat 7/8 všech vygenerovaných čísel. Kapacita této paměti je ale omezená, a počet čísel je zvolený tak, aby jistě na tolik hodnot nestačila. for ( uint32_t i = 1; i <= 1 << 22; ++i ) /* C */ { if ( i % 8 == 0 ) { assert( read( fd_1, &reply_1, 4 ) == 4 ); assert( ntohl( reply_1 ) == i / 8 ); } assert( read( fd_2, &reply_2, 4 ) == 4 ); /* C */ assert( ntohl( reply_2 ) == i ); } Ověříme, že generátor po zapsání všech čísel zavřel spojení. Zároveň druhé spojení zůstává v provozu – přečteme zbývající čísla a ověříme, že ‹iota_pipe› bez chyb skončilo. assert( read( fd_2, &reply_2, 4 ) == 0 ); /* C */ for ( uint32_t i = ( 1 << 19 ) + 1; i <= 1 << 22; ++i ) /* C */ { assert( read( fd_1, &reply_1, 4 ) == 4 ); assert( ntohl( reply_1 ) == i ); } assert( read( fd_1, &reply_1, 4 ) == 0 ); /* C */ int status; /* C */ if ( waitpid( pid, &status, 0 ) == -1 ) /* C */ err( 1, "awaiting child process" ); assert( WIFEXITED( status ) ); /* C */ assert( WEXITSTATUS( status ) == 0 ); close_or_warn( fd_1, "consumer side of a socketpair" ); /* C */ close_or_warn( fd_2, "consumer side of a socketpair" ); return 0; /* C */ } ## p. Přípravy ### 1. [‹sum›] V tomto cvičení je Vaším úkolem zpracovat soubor, ve kterém je uložena tabulka se záznamy pevně daných velikostí (v bitech). Podprogram ‹sum› obdrží: • ‹fd› – popisovač souboru, se kterým bude pracovat (můžete předpokládat, že objekt, se kterým je svázaný, lze mapovat do paměti), • ‹cols› – počet sloupců zde uložené tabulky, • ‹sizes› – pole čísel (pro každý sloupec jedno), které určuje kolik bitů daný sloupec obsahuje (1–64), • ‹results› – pole ‹cols› 64bitových čísel, kam uloží součty jednotlivých sloupců. Hodnoty jsou uloženy od nejvýznamnějšího bajtu po nejméně významný, vždy v nejmenším možném počtu bajtů. Není-li počet bitů sloupce dělitelný 8, zbývající nejvyšší bity nejvyššího bajtu jsou nevyužity. Tyto bity ignorujte – ve vstupním souboru mohou mít libovolné hodnoty. Hodnoty ve sloupcích jsou bezznaménkové. Nedošlo-li při sčítání k chybě, podprogram vrátí hodnotu 0. V případě systémové chyby vrátí -1 a pokud soubor obsahuje neúplný řádek, vrátí -2, aniž by změnil pole ‹results›. Podprogram musí pracovat efektivně i v situaci, kdy soubor obsahuje velmi velký počet krátkých řádků. int sum( int fd, int cols, int *sizes, uint64_t *results ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹list›] V tomto cvičení je Vaším úkolem zpracovat soubor, ve kterém jsou uloženy záznamy pevné velikosti. Každý záznam obsahuje odkaz na následující záznam, který je: • uložen v posledních 4 bajtech, • formou celého čísla se znaménkem (v dvojkovém doplňkovém kódu, nejvýznamnější bajt první), • které určuje o kolik bajtů dále od začátku soubor začíná odkazovaný záznam oproti tomu aktuálnímu. Podprogram ‹check_list› ověří, že soubor má správnou strukturu: • soubor neobsahuje nic než záznamy, • každý záznam vyjma posledního odkazuje na začátek jiného záznamu, • poslední záznam odkazuje sám na sebe (tj. odkaz je nulový), • na první záznam žádný jiný neodkazuje, • na každý další záznam existuje právě jeden odkaz. Vstupem pro ‹check_list› je popisovač souboru (o kterém můžete předpokládat, že odkazuje objekt, který lze mapovat do paměti) a velikost jednoho záznamu. Návratová hodnota 0 znamená, že soubor je korektní, hodnota 1, že nikoliv a -1 že při zpracování souboru nastala systémová chyba. Podprogram musí pracovat efektivně i v situaci, kdy je v souboru velké množství malých záznamů. Bez ohledu na obsah souboru musí výpočet skončit (podprogram se nesmí zacyklit). int check_list( int fd, int rec_size ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹flood›] Implementujte podprogram ‹flood_fill› s těmito parametry: • ‹fd› je popisovač souboru ve formátu BMP, se kterým bude pracovat (a který lze mapovat do paměti), • ‹offset› index prvního pixelu v souboru, • ‹w›, ‹h› je šířka a výška obrázku v pixelech, • ‹x›, ‹y› jsou souřadnice pixelu, od kterého začne vyplňovat jednobarevnou plochu, • ‹color› je barva, na kterou plochu přebarví. Obrázek používá formát, kde jeden pixel je kódovaný jedním bajtem a různé hodnoty představují různé barvy. Pixely jsou uloženy po řádcích a každý řádek je zarovnán na celé 4 bajty doleva (tzn. nepoužité bajty jsou na konci), Podprogram vrátí nulu proběhne-li vše úspěšně, -2 není-li vstup v očekávaném formátu (nesouhlasí první dva bajty nebo je soubor příliš krátký), nebo -1 nastane-li při zpracování systémová chyba. int flood_fill( int fd, int offset, int w, int h, /* C */ int x, int y, int color ); ### 4. [‹write›] Na rozdíl od čtení, při blokujícím zápisu do socketu nebo roury nemusíme řešit situaci, kdy by se operace provedla pouze částečně (s nenulovým počtem bajtů, který je ale menší než požadovaný) – blokující zápisy jsou v běžných situacích „všechno nebo nic“. V neblokujícím režimu je ale situace jiná – máme-li z nějakého důvodu neblokující popisovač, do kterého potřebujeme odeslat nějaké pevné množství dat (a zároveň jej z nějakého důvodu nemůžeme ani dočasně přepnout do blokujícího režimu), musíme se se situací vypořádat podobně, jako tomu bylo u operace ‹read›.¹ Vaším úkolem je tedy naprogramovat proceduru ‹write_buffer›, která zapíše do «neblokujícího» popisovače proudového socketu (nebo roury) zadaný počet bajtů. Zamyslete se, jak efektivně vyřešit situaci, kdy operace ‹write› skončí s výsledkem ‹EAGAIN› – nemá totiž smysl ji okamžitě zkoušet znovu. Šance, že se mezi iteracemi uvolní zdroje, je velmi malá, a takto napsaný program by zcela zbytečně využíval neúměrné množství procesorového času. Výsledkem nechť je počet skutečně přečtených bajtů, nebo -1 v případě, že při zápisu došlo k fatální systémové chybě. int write_buffer( int fd, const char *buffer, int nbytes ); /* C */ ¹ Na rozdíl od analogické konstrukce pro operaci ‹read› zde nevzniká přímé riziko uváznutí. To ale neznamená, že tento de-facto blokující zápis není bez rizik. ### 5. [‹hose›] Uvažme situaci analogickou k přípravě ‹p4_collect›, ale se zápisem – naprogramujte podprogram ‹hose›, který obdrží: • ‹count› – počet popisovačů, se kterými se bude pracovat, • ‹fds› – ukazatel na pole popisovačů,¹ • ‹buffers› – ukazatel na pole ukazatelů, kde každý určuje data, která se mají zapsat do příslušného popisovače, • ‹sizes› – ukazatel na pole čísel, která určují, kolik dat se má do kterého popisovače zapsat. Není-li možné provést žádný zápis, podprogram ‹hose› bude blokovat, než se situace změní. Jinak provede všechny zápisy, které provést lze, aniž by volání blokovalo, a odpovídajícím způsobem upraví ukazatele v ‹buffers› a velikosti v ‹sizes›. Návratová hodnota: • 0 – veškeré požadované zápisy byly provedeny, • kladné číslo – počet popisovačů, které nejsou připravené k zápisu, ale jsou pro ně přichystaná data, • -1 – došlo k systémové chybě – opětovné volání se stejnými parametry se pokusí akci zopakovat. ¹ Je zaručeno, že všechny vstupní popisovače budou mít nastavený příznak ‹O_NONBLOCK›. int hose( int count, int* fds, char** buffers, int* sizes ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 6. [‹tee›] ‡ Uvažme situaci, kdy chceme stejná data zapisovat do několika komunikačních kanálů, které ale nejsou tato data schopna zpracovávat stejně rychle. Zároveň nechceme program blokovat v situaci, kdy některý komunikační kanál není schopen data ihned přijmout. Navrhněte sadu podprogramů, která bude tento problém řešit: • ‹tee_init› obdrží pole popisovačů, do kterých si přejeme data kopírovat, a vrátí ukazatel ‹handle› (nebo nulový ukazatel v případě selhání), • ‹tee_write› obdrží ukazatel ‹handle›, ukazatel na data a velikost dat, které si přejeme rozeslat, • ‹tee_fini› zapíše všechna zbývající data a uvolní veškeré zdroje přidružené k předanému ukazateli ‹handle›. Podprogram ‹tee_write› nebude za žádných okolností blokovat. Zároveň zabezpečí, že veškerá data která odeslat lze budou odeslána ihned, a že data, která už byla odeslána do všech kanálů nebudou zabírat paměť. Podprogram ‹tee_write› lze volat s nulovou velikostí, v takovém případě pouze provede případné odložené zápisy. Návratová hodnota ‹tee_write› je: • 0 – veškerá data byla odeslána všem příjemcům, • kladné číslo – počet popisovačů, které jsou „pozadu“, • -1 – nastala chyba, která znemožnila zpracování předaných dat (data lze bezpečně do ‹tee_write› předat znovu, aniž by hrozilo dvojité odeslání). Nastane-li chyba při zápisu na některý popisovač, ‹tee_write› tento popisovač přeskočí a data se pokusí při dalším volání opět odeslat. Konečně ‹tee_fini› dokončí veškeré zápisy. Dojde-li při některém z nich k chybě, vrátí ‹-1›, ale až potom, co dokončí všechny zápisy, které provést lze, a uvolní zdroje spojené s ukazatelem ‹handle› (včetně uzavření všech přidružených popisovačů). Očekává se, že tee_fini zavře všechny popisovače předané tee_ini void *tee_init( int fd_count, int *fds ); /* C */ int tee_write( void *handle, const char *data, int nbytes ); int tee_fini( void *handle ); # S.1. Vstup a výstup K prvnímu bloku náleží tyto úkoly: A. ‹csv› B. ‹merge› C. ‹lmdb› D. ‹httpc› E. ‹httpd› F. ‹logd› ## a. ‹csv› Soubory typu CSV (Comma-Separated Values) existují v řadě variant, které se liší mimo jiné použitým oddělovačem. Ostatní vlastnosti budeme pro účely tohoto příkladu uvažovat pevné: • obsahuje-li v sobě některá hodnota právě používaný oddělovač, musí být uzavřena v uvozovkách (jinak jsou uvozovky kolem hodnot volitelné), • obsahuje-li hodnota uzavřená v uvozovkách znaky ‹"› nebo ‹\›, tyto musí být uvozeny znakem ‹\›, • znak nového řádku v hodnotě nepřipouštíme, • oddělovač, uvozovka, zpětné lomítko a znak nového řádku jsou každý kódovány jedním bajtem. Vaším úkolem bude naprogramovat proceduru, která na vstupu (zadaném pomocí popisovače) obdrží soubor v tomto formátu a na výstup (opět popisovač) zapíše novou verzi, která bude: • používat zadaný oddělovač (může být jiný nebo stejný jako byl na vstupu), • právě ty hodnoty, které obsahují nový oddělovač, uzavře do uvozovek. int reformat_csv( int fd_in, char old_delim, /* C */ char new_delim, int fd_out ); ## b. ‹merge› Uvažme formát souborů, které obsahují záznamy pevné délky uložené těsně za sebou. Tyto záznamy jsou vždy vzestupně lexikograficky uspořádané podle nějakého klíče, který je zadaný jako rozsah bajtů od-do (zleva uzavřený, zprava otevřený interval). Vaším úkolem je načíst dva soubory tohoto typu a sloučit je do jednoho: • vstupní soubory mají stejnou velikost záznamu, • soubory mohou být mnohem větší, než je dostupná paměť, • vstupy mohou být roury nebo jiné objekty, které neumožňují opakované čtení (podobně výstup), • výstup nebude obsahovat dva záznamy se stejným klíčem, a to ani v situaci, kdy je obsahoval některý soubor na vstupu (použije se první záznam z prvního souboru, který ho obsahuje). Návratová hodnota 0 značí úspěch, -1 systémovou chybu, -2 nekompletní záznam v některém vstupním souboru. int merge( int in1_fd, int in2_fd, int record_size, /* C */ int key_begin, int key_end, int out_fd ); ## c. ‹lmdb› Knihovna LMDB (Lightning Memory-mapped Database Library) je určená k efektivnímu ukládání slovníků (dvojic klíč–hodnota). Data ukládá do souborů, přitom samotná data jsou uspořádána do B+stromů. Vaším úkolem bude naprogramovat funkci, která zjistí, je-li zadaný klíč přítomen v databázi. Databáze je funkci předána formou popisovače otevřeného datového souboru. Návratová hodnota 0 znamená úspěch, -1 systémovou chybu, -2 chybu ve formátu souboru. V případě úspěchu je přítomnost klíče poznačena na adresu ‹found›. Předpokládáme, že databázi souběžně nepoužívá žádná jiná aplikace. Také předpokládáme, že klíče jsou řazeny lexikograficky, a že databáze používá stejné pořadí bajtů ve slově („endianitu“) jako počítač, na kterém tento program běží. int lmdb_has_key( int fd, const char *key, int key_len, /* C */ int *found ); Databázové soubory systému LMDB jsou strukturovány do stránek, které odpovídají stránkám paměti operačního systému.¹ Každá stránka je rozdělena na 3 části: metadatovou, prázdnou a datovou. Metadatová část začíná šestnáctibajtovou hlavičkou (v hranatých závorkách je uvedena počáteční adresa dané položky, šestnáctkově): • [00] 64bitové pořadové číslo stránky v souboru, • [08] nevyužité 16bitové číslo,² • [0a] 16bitové pole příznaků, • [0c] 16bitová celková velikost metadatové části, • [0e] 16bitové číslo prvního bajtu datové části. První dvě stránky datového souboru popisují databázi jako celek a po výše uvedené hlavičce v každé z nich následuje: • [10] 32bitové identifikační číslo (vždy 0xbeefc0de), • [14] 32 + 64 + 64 bitů, které přeskočíme, • [28] 6 × 64 bitů metadat o prázdném místě, • [58] informace o hlavním B+stromě: ◦ [58] 32 nevyužitých bitů, ◦ [5c] 16 bitů příznaků, ◦ [5e] 16bitová hloubka stromu, ◦ [60] 3 × 64 bitů statistických informací, ◦ [78] 64bitový počet klíčů, ◦ [80] číslo kořenové stránky, • [88] číslo poslední stránky, • [90] pořadové číslo transakce, která tuto stránku zapsala.³ Samotné B+stromy jsou uloženy po stránkách – jedna stránka obsahuje jeden uzel. V metadatové části jsou uloženy 16bitové odkazy na začátky jednotlivých klíčů – odkaz je index prvního bajtu daného klíče, počítáno od začátku stránky.⁴ Jedná-li se o vnitřní uzel nebo o list se pozná z příznaků v metadatové hlavičce stránky – vnitřní uzly mají nastavený nejnižší bit (‹0x01›), listy mají nastavený druhý nejnižší bit (‹0x02›). Formát klíče se mírně liší podle toho, jestli se jedná o vnitřní uzel nebo o list. Vnitřní uzly používají tuto strukturu: • 48bitové číslo stránky (uzlu), na kterou tento klíč odkazuje, • 16bitová velikost klíče, • samotný klíč. Protože ke každému klíči je připojen jeden odkaz, musí vnitřní uzly obsahovat jeden prázdný klíč navíc – je to ten nejvíce vlevo a má nastavenu délku klíče na nulu.⁵ Listy pak používají tuto strukturu dat: • 32bitová velikost hodnoty náležící tomuto klíči, • 16 bitů příznaků (uvažujeme nulové), • 16bitová velikost klíče, • samotný klíč, • hodnota náležící klíči. Poznámky na závěr: • pro práci s čísly pevné bitové šířky se budou hodit typy ‹uint16_t›, ‹uint64_t›, ‹int16_t›, atd., definované hlavičkou ‹stdint.h›, • při testování se může hodit modul ‹lmdb› pro Python: ◦ získáte jej příkazem ‹pip install lmdb›, ◦ ‹env = lmdb.open('test')› vytvoříte prázdnou databázi, ◦ ‹txn = env.begin(write=True)› otevřete transakci, ◦ ‹txn.put(b'key', b'value')› vložíte dvojici klíč/hodnota, ◦ ‹txn.commit()› transakci ukončíte, ◦ datový soubor se bude jmenovat ‹test/data.mdb›. ¹ S databázovými soubory lze pracovat i na systémech, které mají jinou velikost stránky, než ten, který databázi vytvořil. Touto situací se zde ale zabývat nebudeme – pro účely této úlohy můžete předpokládat, že soubor používá nativní velikost stránky, a že tato je 4KiB. ² Používá se pouze pro typy stránek, které zde nebudeme uvažovat. ³ Protože nás zajímá pouze nejnovější transakce, budeme metadatovou stránku s nižším číslem transakce ignorovat. ⁴ Tyto odkazy jsou seřazené podle klíče, na který odkazují, aby mezi nimi bylo možné vyhledávat půlením intervalu. Samotné klíče jsou uloženy v datové části již v libovolném pořadí kvůli efektivnějšímu vkládání. ⁵ B+stromy zde používané mají ještě jednu odlišnost od těch, které znáte z IB002 – uzly neobsahují pevný počet klíčů, protože je pevná velikost stránky ve které je uzel uložen a zároveň jsou uvnitř uzlu uloženy klíče proměnné délky. Uzel má tedy tolik potomků, kolik se do něj vejde klíčů (velikost klíče je omezena tak, aby se do každého uzlu vešly alespoň dva). ## d. ‹httpc› Vaším úkolem bude naprogramovat klient protokolu HTTP 1.0. Omezíme se na základní funkcionalitu – sestavení a odeslání požadavku a přijetí a zpracování odpovědi. Pro komunikaci budeme používat unixové sockety.¹ Řádky požadavku oddělujte sekvencí ‹\r\n› a můžete předpokládat, že server bude dodržovat totéž. Jak požadavek tak odpověď má tři části: 1. řádek požadavku (resp. stavový řádek pro odpověď), ◦ řádek požadavku má formu ‹METODA cesta HTTP/1.0›, ◦ stavový řádek má formu ‹HTTP/1.0 číselný_kód popis›, 2. hlavičky – každé pole začíná jménem, následované dvojtečkou a textovou hodnotou, která pokračuje až do konce řádku,² 3. tělo, oddělené od hlaviček prázdným řádkem. Obsah těla může být libovolný, nebudeme jej nijak interpretovat. Pozor, může se jednat o binární data (tzn. tělo nemusí být nutně textové). Všechny zde popsané podprogramy s návratovou hodnotou typu ‹int› vrací v případě úspěchu nulu a v případě systémové chyby -1, není-li uvedeno jinak. Jednotlivá pole hlavičky protokolu HTTP budeme reprezentovat typem ‹http_header› a celou hlavičku pak typem ‹http_header_list›. Jedná se o jednoduše zřetězený seznam dvojic klíč-hodnota. Hodnota ‹value› nebude ukončena znakem nového řádku (tzn. bude obsahovat pouze samotnou hodnotu příslušného pole). struct http_header /* C */ { char *name, *value; }; struct http_header_list /* C */ { struct http_header header; struct http_header_list *next; }; (Velmi zjednodušený) požadavek protokolu HTTP budeme reprezentovat strukturou ‹http_request› – přitom budeme podporovat pouze dvě metody, totiž ‹GET› a ‹HEAD›. Tělo požadavku bude v obou případech prázdné. Prázdný seznam hlaviček je reprezentovaný nulovým ukazatelem ‹headers›. enum http_method { HTTP_GET = 1, /* C */ HTTP_HEAD }; struct http_request /* C */ { enum http_method method; char *path; struct http_header_list *headers; }; Pro zjednodušení tvorby požadavku implementujte následující dvě funkce. Veškerá paměť spojená s požadavkem je vlastnictvím požadavku – požadavek musí být platný i v situaci, kdy uživatel do funkce předané ‹path› atp. později přepíše nebo uvolní. Protože na pořadí hlaviček nezáleží, zvolte takové pořadí, aby byla implementace efektivní. int http_request_set_path( struct http_request *request, /* C */ const char *path ); int http_request_add_header( struct http_request *request, const char *field, const char *value ); Následující funkce nechť požadavek zapíše do otevřeného popisovače souboru. Tato funkce se Vám může hodit také v implementaci procedury ‹http_request› níže. int http_request_write( struct http_request *request, int fd ); /* C */ Konečně procedura ‹http_request_free› uvolní veškerou paměť spojenou s požadavkem. Opětovné volání ‹http_request_free› na stejný objekt nechť nemá žádný efekt. void http_request_free( struct http_request *request ); /* C */ Pro reprezentaci odpovědi serveru použijeme strukturu ‹http_response›, která bude obsahovat kód odpovědi, hlavičky a tělo. Podobně jako u předchozích typů, hodnota typu ‹http_response› bude vlastnit veškerou potřebnou paměť. V seznamu ‹headers› budou hlavičky seřazeny v pořadí, ve kterém je server odeslal (dejte si pozor na efektivitu!). struct http_response /* C */ { int code; struct http_header_list *headers; size_t body_length; char *body; }; Procedura ‹http_response_read› přečte odpověď protokolu HTTP ze zadaného popisovače a uloží ji do předané struktury ‹http_response›. Výsledkem bude 0 proběhlo-li vše v pořádku, -1 při systémové chybě a -2 je-li odpověď špatně sestavená. int http_response_read( struct http_response *response, int fd_in ); /* C */ Pro uvolnění veškeré paměti spojené s požadavkem slouží následující procedura. Předaná hodnota ‹http_response› bude uvedena do takového stavu, aby opětovné volání ‹http_response_free› na stejné hodnotě neprovedlo žádnou akci. void http_response_free( struct http_response *response ); /* C */ Konečně procedura ‹http_request› provede požadavek podle parametru ‹request› na unixovém socketu ‹address› a odpověď vyplní do předané hodnoty typu ‹response›. Návratová hodnota bude 0 proběhlo-li vše v pořádku, -1 v případě systémové chyby a -2 v případě chybné odpovědi ze strany serveru. Není-li výsledek 0, předaná hodnota ‹response› zůstane nedotčena. int http_request( const char *address, /* C */ struct http_request *request, struct http_response *response ); ¹ Jako testovací server můžete použít například knihovnu ‹aiohttp› pro Python – umí pracovat s UNIXovými sockety (příklad serveru naleznete v ‹zz.httpd.py›). Úkol je ale rozvržen tak, že většinu funkcionality lze testovat i bez skutečného serveru – požadavky a odpovědi lze ukládat pro účely testování do obyčejných souborů. ² Víceřádkové hlavičky pro zjednodušení nebudeme uvažovat. ## e. ‹httpd› Základní popis protokolu HTTP 1.0 naleznete v předchozí úloze. Nyní bude Vaším úkolem naprogramovat jednoduchý server tohoto protokolu. Můžete předpokládat, že vyřízení jednoho požadavku bude dostatečně rychlé a nemusíte tedy řešit souběžnou komunikaci s více klienty. Je-li klientem vyžádaná cesta přítomna v seznamu souborů, odpovídejte kódem 200 OK, jinak kódem 404 Not Found. Na neznámé metody reagujte kódem 501 Not Implemented. struct file /* C */ { const char *path; const char *content_type; const char *data; size_t data_size; }; struct file_list /* C */ { struct file file; struct file_list *next; }; Tato úloha tvoří samostatný program, vstupním bodem bude podprogram ‹httpd›, který spustí server na zadané adrese a bude poskytovat soubory popsané druhým parametrem. Hledání souboru můžete realizovat lineárním průchodem seznamu. Při fatální chybě ukončete program (s odpovídající chybovou hláškou), jinak chybu zapište na chybový výstup a pokračujte (je-li to potřeba, můžete přerušit komunikaci s aktuálním klientem). Pro testování můžete použít např. příkazy: $ ./e_httpd zt.httpd $ curl -v --unix-socket zt.httpd http://foo/hello.txt void httpd( const char *address, struct file_list *files ); /* C */ static void unlink_if_exists( const char* file ) /* C */ { if ( unlink( file ) == -1 && errno != ENOENT ) err( 2, "unlinking %s", file ); } int main( int argc, const char **argv ) /* C */ { if ( argc == 1 ) return 0; /* no automated tests */ if ( argc > 2 ) /* C */ errx( 1, "expected arguments: socket_path" ); struct file_list hello, bye; /* C */ hello.file.path = "/hello.txt"; /* C */ hello.file.content_type = "text/plain"; hello.file.data = "hello world\n"; hello.file.data_size = strlen( hello.file.data ); hello.next = &bye; bye.file.path = "/bye.txt"; /* C */ bye.file.content_type = "text/plain"; bye.file.data = "bye world\n"; bye.file.data_size = strlen( bye.file.data ); bye.next = NULL; unlink_if_exists( argv[ 1 ] ); /* C */ httpd( argv[ 1 ], &hello ); return 1; /* httpd should never return */ /* C */ } ## f. ‹logd› V této úloze bude Vaším úkolem implementovat jednoduchý logovací server. K serveru se může připojit libovolný počet klientů, přitom každý klient bude posílat zprávy pro uložení na serveru (každá jednotlivá zpráva je ukončena znakem konce řádku). Server je pak bude v pořadí, ve kterém je obdržel, ukládat do dvou souborů – jeden, který je specifický pro daného klienta, a jeden společný (hlavní). Po připojení klient pošle řádek ‹log id\n›, kde ‹id› je libovolný řetězec, který je platným názvem souboru. Server odpoví: • ‹ok, logging as id\n› čím potvrdí, že požadavek přijal a je připraven zaznamenávat přijaté zprávy, • nebo ‹error \n› narazí-li na nějaký problém (např. se nepodařilo otevřít logovací soubor). Připojí-li se víc klientů se stejným ‹id›, nejedná se o chybu, logovací soubor budou sdílet. Když soubor se záznamy již existuje, existující obsah nechť je zachován – nové záznamy bude server přidávat na konec. Proběhne-li počáteční výměna v pořádku, server bez zbytečné prodlevy uloží každou další zprávu, kterou od klienta obdrží, do obou souborů. Hlavní soubor bude proceduře ‹logd› předán jako popisovač otevřeného souboru ‹main_log_fd›, vedlejší soubory bude podle potřeby vytvářet ve složce předané popisovačem otevřené složky ‹log_dir_fd›. Soubory se budou jmenovat ‹id.log› kde ‹id› je identifikátor odeslaný klientem. Narazí-li ‹logd› na fatální chybu, ukončí program s chybovým kódem. Není-li server schopen zprávy některého klienta ukládat, ukončí s tímto klientem spojení. void logd( const char *addr, int main_log_fd, int log_dir_fd ); /* C */ int main( int argc, const char **argv ) /* C */ { if ( argc == 1 ) return 0; /* no automated tests */ if ( argc < 4 ) /* C */ errx( 1, "arguments expected: " "socket_path main_log log_dir" ); int log_fd, dir_fd; /* C */ const char *sock = argv[ 1 ], *log = argv[ 2 ], *dir = argv[ 3 ]; if ( unlink( sock ) == -1 && errno != ENOENT ) /* C */ err( 1, "removing %s", sock ); if ( mkdir( dir, 0777 ) == -1 && errno != EEXIST ) /* C */ err( 1, "creating directory %s", dir ); if ( ( log_fd = open( log, /* C */ O_CREAT | O_TRUNC | O_RDWR, 0666 ) ) == -1 ) err( 1, "creating %s", log ); if ( ( dir_fd = open( dir, O_DIRECTORY ) ) == -1 ) /* C */ err( 1, "opening %s", dir ); logd( sock, log_fd, dir_fd ); /* C */ return 1; /* logd should never return */ /* C */ } # Řetězce, cesty, ‹openat› V této kapitole budeme pracovat se záznamy proměnné délky. Důležitým speciálním případem pak bude práce s řetězci a jejich kódováním.¹ Ukázky: 1. ‹strings› – základy práce s řetězci a textem, 2. ‹dirs› – nalezení / otevření obyčejného souboru, 3. ‹acrostic› – řetězec prvních písmen každého řádku. Přípravy: 1. ‹long› – nalezení nejdelšího řádku, 2. ‹read› – čtení záznamů oddělených specifickým bajtem, 3. ‹cat› – načtení souboru se jmény souborů pro spojení, 4. ‹kvsd› – slovníkový server s úložištěm v souborovém systému, 5. ‹kvchk› – ověření přítomnosti klíče a asociované hodnoty, 6. ‹kvget› – dávkové stažení (linked list klíčů na vstupu). ## Systémová volání V této kapitole přidáme systémové volání ‹openat›, které nám umožní získat popisovač pro existující obyčejný soubor, nebo takový soubor i vytvořit. Jinak budeme používat hlavně již dobře známá volání ‹read› a ‹write› (jak ke komunikaci, tak k zápisu a čtení dat z obyčejných souborů). Také budeme samozřejmě potřebovat voláni ‹close›, kterým popisovač uzavřeme (a uvolníme tím alokované zdroje). ### ‹openat› Pro vytvoření popisovače existuje několik systémových volání, ze kterých si tento týden ukážeme to nejdůležitější. Má 3 pevné a 1 volitelný parametr: 1. ‹int fd› je již existující popisovač, který je svázán «se složkou», vůči které budeme uvádět «cestu» – lze použít symbolickou hodnotu ‹AT_FDCWD›, která označuje tzv. pracovní složku, 2. ‹const char *path› je řetězec (ukončený nulou), který udává název, případně cestu (oddělovačem je znak ‹/›) k souboru, se kterým hodláme pracovat, 3. ‹int flags› je bitová kombinace příznaků – budou nás pro tuto chvíli zajímat symbolické hodnoty: ◦ ‹O_RDONLY›, ‹O_WRONLY› a ‹O_RDWR›, které určí jaké operace plánujeme později se souborem provádět, ◦ ‹O_CREAT› která nám umožní vytvořit nový obyčejný soubor, 4. ‹int mode› dodáváme pouze tehdy, když může operací vzniknout nový soubor (zejména obsahuje-li parametr ‹flags› příznak ‹O_CREAT›). ## Knihovní funkce • ‹malloc›, ‹realloc›, ‹free› ## d. Demonstrace (ukázky) ### 1. [‹strings›] Operační systém a také většina programů při své činnosti komunikuje s uživatelem. Tato komunikace je obvykle postavena na textu, a proto musí mít program a operační systém společný způsob, kterým text reprezentuje v paměti. I kdybychom si odmysleli klasické výpisy na obrazovku, tak základní věc jako soubor má «jméno», které je samozřejmě také kusem textu. Normy ISO C a POSIX specifikují některé základní charakteristiky kódování textu: 1. každé písmeno¹ je kódováno nějakou posloupností bajtů, a fragment textu (tzv. «řetězec») je v paměti uložen tak, že kódování jednotlivých znaků (písmen) jsou uložena za sebou (na postupně se zvyšujících adresách), 2. nulový bajt je vyhrazen pro speciální účely (pro řadu knihovních a systémových funkcí označuje konec řetězce, tzn. bajt uložený bezprostředně před nulovým bajtem je poslední bajt kódující daný řetězec), 3. vybranou množinu znaků, nazývanou Portable Character Set, musí být systém schopen kódovat, a to navíc tak, že každému znaku odpovídá jeden bajt (jedná se o většinu znaků obsažených v ASCII²), 4. konkrétní číselné hodnoty, kterými jsou tyto znaky kódované, nejsou pevně určeny (s výjimkou nulového znaku, který musí být kódován nulovým bajtem), ale v praxi každý systém, který potkáte, bude používat kódování ASCII, a naprostá většina UTF-8 (které je nadmnožinou ASCII). #define _POSIX_C_SOURCE 200809L /* C */ #include /* strlen, strcmp */ #include /* dprintf, snprintf */ #include /* STDOUT_FILENO */ #include int main( void ) /* demo */ /* C */ { Nejjednodušší způsob, jak v programu získat kódování nějakého textu je pomocí tzv. «řetězcového literálu». Zdrojový kód je samozřejmě také text, je tedy zcela logické, že programovací jazyky nám umožňují část textu označit za data. Zapíšeme-li v jazyce C do programu řetězcový literál, překladač v paměti programu vyhradí potřebné místo a uloží do tohoto místa kódování uvozeného textu, ukončeno nulovým bajtem. Protože řetězcový literál je výraz, má hodnotu – tato hodnota je (opět platí pro jazyk C) «ukazatel» na takto vyhrazenou paměť. Je obvyklé, že tato paměť je označena «pouze pro čtení», do paměti řetězcového literálu tedy «není dovoleno» zapsat jiná data. const char * const string = "hello"; /* C */ Pojmenovaná konstanta ‹string› nyní obsahuje ukazatel na paměť, kde je uloženo kódování řetězce ‹hello› ukončené nulovým bajtem. Pro práci s takto uloženými řetězci poskytuje jazyk C sadu základních funkcí. První z nich je ‹strlen›, která zjistí počet «bajtů» (nikoliv znaků), kterými je řetězec zakódovaný. Např.: assert( strlen( string ) == 5 ); /* C */ assert( strlen( "věc" ) == 4 ); Pozor, ‹strlen› prochází paměť od předaného ukazatele po bajtech, až dokud nenajde nulový bajt. Není-li na předané adrese uložen nulou ukončený řetězec, může se stát celkem cokoliv (včetně chyby ochrany paměti). Další užitečnou funkcí je ‹strcmp›, která po bajtech srovnává dvě oblasti paměti, až dokud nenarazí na rozdílný nebo na nulový bajt. Návratová hodnota je 0, jsou-li na zadaných adresách uloženy stejné bajty³, záporná je-li levá strana lexikograficky (po bajtech) menší a kladná jinak. assert( strcmp( string, "hello" ) == 0 ); /* C */ Protože řetězce jsou v paměti zakódované jako posloupnost bajtů, můžeme samozřejmě tuto posloupnost bajtů do paměti uložit přímo.⁴ const char thing_1[] = { 0x76, 0xc4, 0x9b, 0x63, 0 }; /* C */ const char thing_2[] = { 0x76, 0x65, 0xcc, 0x8c, 0x63, 0 }; assert( strcmp( "věc", thing_1 ) == 0 ); /* C */ assert( strcmp( "věc", thing_2 ) != 0 ); Pro tzv. formátovaný výpis můžeme využít knihovní funkce ‹dprintf›. Podrobný popis formátovacího řetězce (druhý parametr) naleznete v její manuálové stránce (‹man dprintf›). Nám v tuto chvíli postačí, že za každou ‹%›-sekvenci se při výpisu dosadí postupně další parametry, a že daná ‹%›-sekvence popisuje tzv. «konverzi», která určuje, jak se má daný parametr vypsat. Konverze ‹%s› interpretuje příslušný parametr jako ukazatel na paměť, která kóduje nulou ukončený řetězec. dprintf( STDOUT_FILENO, "%s - %s\n", thing_1, thing_2 ); /* C */ Pro výpis číselných hodnot typicky použijeme konverze ‹%d› (desítková) nebo ‹%x› (šestnáctková), případně jsou-li předané hodnoty typu ‹long›, použijeme ‹%ld› nebo ‹%lx›. Pro desítkový výpis bez znaménka použijeme ‹%u› nebo ‹%lu›. dprintf( STDOUT_FILENO, "%d %u %x\n", thing_1[ 0 ], /* C */ (unsigned char) thing_1[ 1 ], (unsigned char) thing_1[ 2 ] ); Krom zápisu do souboru (resp. na standardní výstup) můžeme někdy potřebovat pomocí formátování nachystat řetězec v paměti. K tomu lze použít funkci ‹snprintf›, které předáme ukazatel na přichystané místo v paměti, kam bude uloženo výsledné kódování, počet vyhrazených bajtů, formátovací řetězec a případné další parametry. Tato funkce zapíše tolik bajtů výsledného řetězce, kolik umožní vyhrazené místo. Návratová hodnota pak indikuje, kolik bajtů bylo potřeba (bez ohledu na to, jestli se do vyhrazené paměti vešly nebo nikoliv). Poslední zapsaný bajt je vždy nulový, a to i v situaci, kdy zapsaný řetězec není kompletní. char buffer[ 5 ]; /* C */ assert( snprintf( buffer, 5, "123456789" ) == 9 ); assert( strcmp( buffer, "1234" ) == 0 ); return 0; /* C */ // cflags: -Wno-format-truncation /* C */ } ¹ Co přesně je písmeno je mnohem komplikovanější otázka, než by se mohlo zdát. Zájemce o obecně uznávanou definici odkazujeme na normu Unicode. To, co tady zjednodušeně nazýváme písmenem, je správně tzv. kódový bod. ² ASCII je norma, která popisuje kódování základní 127-znakové abecedy, používaná původně ve Spojených státech (proto název „American Standard Code for Information Interchange“), nyní ale celosvětově, zejména jako podmnožina kódování UTF-8 standardu Unicode. ³ Rádi bychom zde řekli, že funkce srovnává řetězce, to by ale bylo mírně zavádějící. Některé řetězce lze kódovat více než jedním ekvivalentním způsobem, např. ‹věc› lze v UTF-8 zakódovat jako ‹76 c4 9b 63› nebo jako ‹76 65 cc 8c 63›. Tyto sekvence reprezentují tentýž «text», ale protože jsou v paměti uloženy jako různé posloupnosti bajtů, funkce ‹strcmp› je bude považovat za odlišné. ⁴ Toto bude samozřejmě fungovat pouze tehdy, kdy známe konkrétní kódování používané daným systémem. Spustíte-li tento program na systému, který nepoužívá UTF-8, výsledky se mohou lišit. Zejména výstup na obrazovce nebude odpovídat tomu, co bychom očekávali. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹dirs›] V této ukázce si trochu více přiblížíme systémové volání ‹openat› a podíváme se na základy práce se složkami (adresáři). Jak víte z třetí přednášky PB152, složka asociuje jména¹ se soubory (v obecném smyslu, tzn. nejen obyčejnými). Složku lze otevřít stejně jako jiné typy souborů – takto získaný popisovač tuto složku jednoznačně identifikuje, a to i v situaci, kdy se změní odkazy na tuto složku, a tedy její jméno v nadřazené složce nemusí platit, nebo se může přesunout v adresářové struktuře na úplně jiné místo.² #include /* dprintf */ /* C */ #include /* read, write */ #include #include Nejprve si nachystáme několik jednoduchých pomocných podprogramů, které nám zjednoduší zápis zbytku programu. Protože se jedná o malý uzavřený program, můžeme si dovolit považovat chyby při otevírání souboru za fatální. int open_or_die( int dir_fd, const char *path, int flags ) /* C */ { int fd = openat( dir_fd, path, flags ); if ( fd == -1 ) /* C */ err( 1, "error opening %s", path ); return fd; /* C */ } void close_or_warn( int fd, const char *name ) /* C */ { if ( close( fd ) == -1 ) warn( "error closing descriptor %d for file %s", fd, name ); } int main( int argc, const char **argv ) /* demo */ /* C */ { Tento program bude postupně otevírat soubory, kterých jména jsou uložené v následujícím poli ukazatelů, a z každého vypíše prvních pár bajtů na standardní výstup. Nulový ukazatel označuje konec pole. const char * const filenames[] = /* C */ { "a0_intro.txt", "a1_overview.txt", "a2_grading.txt", NULL }; Aby se nám s programem lépe experimentovalo, spustíme-li jej s parametrem, tento parametr se použije jako cesta ke složce, ve které budeme hledat složku ‹00› (a v ní pak výše uvedené soubory). Doporučujeme zkusit si tento program spustit s různě přichystanými složkami, např. takovou, která podsložku ‹00› vůbec neobsahuje, s takovou, která neobsahuje všechny očekávané soubory, atp. const char *top_path = argc > 1 ? argv[ 1 ] : ".."; /* C */ int top_fd = open_or_die( AT_FDCWD, top_path, O_DIRECTORY ); int dir_fd = open_or_die( top_fd, "00", O_DIRECTORY ); Následující cyklus každý soubor otevře – všimněte si, že používáme pouze «jména» souborů, protože rodičovská složka je do podprogramu ‹open_or_die› předána pomocí popisovače. Tento přístup má dvě zásadní výhody: 1. nemusíme složitě konstruovat «cesty», které by k souborům vedly – něco jako ‹top_path + "/00/" + name› – zápis, který v jazyce C samozřejmě nemůžeme použít; navíc 2. takto sestavené cesty mohou v různých iteracích ukazovat na soubory v různých složkách – souborový systém je «sdílený» a každá operace je potenciálním «hazardem souběhu».³ for ( int i = 0; filenames[ i ]; ++i ) /* C */ { const char *name = filenames[ i ]; int file_fd = open_or_die( dir_fd, name, O_RDONLY ); const int nbytes = 10; /* C */ char buffer[ nbytes ]; int bytes_read = read( file_fd, buffer, nbytes ); /* C */ if ( bytes_read == -1 ) /* C */ err( 1, "error reading %d bytes from %s", nbytes, name ); Přečtené bajty přepíšeme na standardní výstup a posuneme se na nový řádek. if ( write( STDOUT_FILENO, buffer, bytes_read ) == -1 || /* C */ dprintf( STDOUT_FILENO, "\n" ) == -1 ) err( 1, "error writing to stdout" ); close_or_warn( file_fd, name ); /* C */ } Nezapomeneme uzavřít popisovače složek, které jsme otevřeli na začátku podprogramu. close_or_warn( dir_fd, "00" ); /* C */ close_or_warn( top_fd, top_path ); return 0; } ¹ Jméno souboru je libovolný řetězec, který ovšem nesmí obsahovat znak ‹/› ani nulový znak. Délka jména může být operačním systémem nebo souborovým systémem omezená. ² Protože na složky není dovoleno vytvořit víc než jeden standardní odkaz, a zároveň není možné neprázdnou složku odstranit, mohlo by se zdát, že tato situace nemá jak nastat. Odkazy na složky lze ale atomicky «přesouvat» použitím systémového volání ‹renameat›. Více si o něm povíme v další kapitole. ³ Uvažme situaci, kdy všechny vstupní soubory v zadané složce existují. Sestavujeme-li cesty jak bylo naznačeno, může se stát, že jiný program složku přejmenuje. Náš program některé soubory úspěšně vypíše a u jiných ohlásí chybu, a to přesto, že se soubory ani s odkazy na ně v rodičovské složce se vůbec nic nestalo. Považujeme-li takovéto chování za chybné (a bylo by to naprosto logické), prakticky celý náš program tvoří kritickou sekci vůči přesunu (přejmenování) rodičovské složky, kterou ale nemáme jak ochránit. Řešení s předáváním složky pomocí popisovače tuto kritickou sekci (a tedy ani popsaný hazard souběhu) neobsahuje. ## p. Přípravy ### 1. [‹long›] Napište podprogram ‹longest_line›, který přečte veškerá data ze zadaného popisovače,¹ a nalezne v nich nejdelší řádek. Tento řádek pak uloží do dynamicky alokovaného pole vhodné velikosti a vrátí ukazatel na toto pole. Za řádky považujeme pouze sekvence znaků ukončené znakem konce řádku (případný nekompletní řádek na konci souboru ignorujeme). Program nesmí alokovat více dynamické paměti, než je trojnásobek délky nalezeného nejdelšího řádku (+ 128 bajtů paměti k dobru).² Smíte předpokládat, že vstup neobsahuje nulové bajty. V případě neúspěchu podprogram vrátí nulový ukazatel (včetně situace, kdy vstup neobsahuje žádný kompletní řádek). ¹ Můžete předpokládat, že na popisovači bude fungovat ‹lseek›. ² Tento limit se vztahuje i na knihovní podprogramy, které využijete. Pozor, ‹fdopen› používá na mnoha systémech dynamickou paměť. char *longest_line( int fd ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹read›] Napište podprogram ‹read_until›, kterého úkolem je načíst ze zadaného popisovače jeden záznam neznámé délky. Záznamy jsou od sebe odděleny zadaným bajtem ‹delimiter›. Protože neznáme délku záznamu, může se lehce stát, že načteme víc dat, než je potřeba. Tato data přitom nesmíme ztratit – jsou součástí dalších záznamů, které budeme v budoucnu načítat (a samozřejmě nemůžeme data načítat po jednom bajtu). Samotný oddělovač chápeme jako součást předchozího záznamu. Tato přebytečná data budeme uchovávat ve struktuře ‹shift_buffer› – každému volání ‹read_until› z daného popisovače bude předán ukazatel na tutéž strukturu ‹shift_buffer›. Při prvním volání ‹read_until› budou všechny položky vynulované. Správa zdrojů spojených se strukturou ‹shift_buffer› je zcela v režii podprogramu ‹read_until›. Narazí-li ‹read_until› na konec souboru, veškeré zdroje uvolní a strukturu uvede do stavu, kdy ji lze použít pro další načítání (např. z jiného popisovače). Výsledkem takového volání je pak poslední záznam (potenciálně nulové velikosti, nikoliv ale nulový ukazatel). Výsledkem podprogramu ‹read_until› je buď ‹NULL› nastane-li nějaká systémová chyba, nebo ukazatel na dynamicky alokovanou paměť, která obsahuje načtený záznam. Počet bajtů uložených v načteném záznamu zapíše do výstupního parametru ‹length›. struct shift_buffer /* doplňte nebo upravte dle potřeby */ /* C */ { char *memory; int allocd; int used; }; char *read_until( int fd, char delimiter, /* C */ struct shift_buffer *buf, int *length ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹cat›] Naprogramujte proceduru ‹cat›, která obdrží tyto 3 parametry: • ‹dir_fd› – popisovač adresáře, ve kterém bude hledat všechny níže zmíněné soubory, • ‹list_fd› – popisovač, ze kterého podprogram ‹cat› přečte jména souborů, • ‹out_fd› – «výstupní» popisovač. Vstup ‹list_fd› bude obsahovat na každém řádku jméno souboru (samotný seznam nemusí být nutně zapsán v souboru). Procedura ‹cat› zapíše obsahy všech těchto souborů (v zadaném pořadí) do popisovače ‹out›. Za řádek považujeme posloupnost znaků «zakončenou» ‹'\n'› (nikoliv tedy ‹"\r\n"› nebo ‹'\r'›). Nepodaří-li se nějaký soubor otevřít, přeskočte jej. Návratová hodnota: • 0 – podařilo se otevřít, přečíst a zapsat všechny soubory, • -1 – chyba čtení z popisovače ‹list_fd›, chyba zápisu do ‹out_fd›, nebo jiná fatální systémová chyba, • -2 – vše proběhlo v pořádku, ale některé soubory byly přeskočeny, protože se je nepodařilo otevřít. int cat( int dir_fd, int list_fd, int out_fd ); /* C */ ### 4. [‹kvsd›] Vaším úkolem je tentokrát naprogramovat jednoduchý server, který bude komunikovat prostřednictvím proudového socketu s jedním klientem. Struktura protokolu je velmi jednoduchá: klient bude odesílat „klíče“ ukončené nulovým bajtem. Ke každému klíči, který server přečte, odešle odpověď, která bude obsahovat čtyřbajtovou délku hodnoty, která klíči náleží, následovanou samotnou hodnotou. Není-li klíč přítomen, odešle hodnotu 0xffffffff. Nejvýznamnější bajt je vždy odesílán jako první. Krom nulového bajtu (který slouží jako oddělovač) nebudou klíče obsahovat ani znak lomítka ‹/›. Klíče a hodnoty jsou uloženy v souborovém systému, ve složce předané podprogramu ‹kvsd› popisovačem ‹root_fd›. Klíč je název souboru, hodnota je pak jeho obsah. Podprogram ‹kvsd› vrátí hodnotu -1 v případě fatální chyby, jinak hodnotu 0. Neexistence klíče není fatální chybou (viz výše), jiné chyby při otevírání souboru ale ano. int kvsd( int root_fd, int client_fd ); /* C */ ### 5. [‹kvchk›] Uvažme proudový protokol z předchozí přípravy. Vaším úkolem bude naprogramovat klient, který ověří, zda je hodnota klíče na serveru rovna nějaké očekávané. Protože hodnoty mohou být velké, je očekávaná hodnota klientu předána jako soubor (resp. popisovač, který odkazuje na obyčejný soubor). Pro připomenutí, požadavek klienta sestává z nulou ukončeného klíče (budeme odesílat pouze jeden), odpověď serveru pak z čtyřbajtové velikosti hodnoty a pak samotné hodnoty. Nejvýznamnější bajt velikosti je odeslán jako první. Návratová hodnota bude: • 0 je-li vše v pořádku (hodnota odpovídá očekávání), • -1 došlo-li k chybě komunikace nebo jiné fatální chybě, • -2 nebyl-li požadovaný klíč na serveru přítomen, • -2 jestli byla hodnota přijata, ale neodpovídá očekávání. int kvchk( int server_fd, const char *key, int data_fd ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 6. [‹kvget›] Uvažme proudový protokol z předchozích dvou příprav. Vaším úkolem bude naprogramovat klient, který stáhne všechny zadané klíče a jim odpovídající hodnoty uloží do souborů v zadané složce – název souboru bude vždy jméno klíče. Seznam souborů ke stažení je zadán jako zřetězený seznam. Pro připomenutí, požadavek klienta sestává z nulou ukončeného klíče, odpověď serveru pak z čtyřbajtové velikosti hodnoty (v bajtech) a pak samotné hodnoty. Nejvýznamnější bajt velikosti je vždy odeslán jako první. Není-li klíč přítomen, server odešle hodnotu 0xffffffff. Návratová hodnota bude: • 0 je-li vše v pořádku (všechny hodnoty byly staženy a úspěšně uloženy), • -1 došlo-li k chybě komunikace, chybě při ukládání dat, nebo jiné fatální chybě, • -2 nebyl-li některý požadovaný klíč na serveru přítomen (nepřítomnost klíče ale nebrání stažení všech ostatních). Existuje-li v zadané složce soubor se stejným jménem, jaký by měl podprogram ‹kvget› vytvořit, považujeme to za fatální chybu (dotčený soubor se přitom nesmí nijak změnit). struct key_list /* C */ { const char *key; struct key_list *next; }; int kvget( int server_fd, int dir_fd, struct key_list *keys ); /* C */ # Složky (adresáře) V této kapitole budeme pracovat s adresáři – speciálním typem souboru, který je tvořen odkazy na další soubory. Ze známých abstraktních datových struktur se adresář podobá na «slovník» (asociativní pole). Adresář (složka) se skládá z «položek» – dvojic klíč/hodnota: 1. klíč je unikátní «jméno» (řetězec, který neobsahuje nulový znak ani znak ‹/›), 2. hodnota je «odkaz» na soubor (i-uzel) libovolného typu (může být opět adresářem). Jména jsou unikátní pouze v rámci jednoho adresáře. Soubor (který není adresářem)¹ může být odkazován více než jednou položkou, a položky, které odkazují na stejný soubor, mohou být ve stejné nebo různých adresářích (složkách). Ukázky: 1. ‹newest› – nalezení nejnovějšího souboru v zadané složce, 2. ‹depth› – počítání maximální hloubky adresářové struktury, 3. ‹refs› – kolik odkazů na soubor je v zadaném stromě. Přípravy: 1. ‹fcount› – počítání obyčejných souborů v adresáři, 2. ‹access› – kontrola práv u souborů zadaných seznamem, 3. ‹list› – sestavení spojovaného seznamu názvů souborů, 4. ‹archive› – přesun starých souborů do archivní složky, 5. ‹find› – rekurzivní hledání podle jména, 6. ‹du› – využití místa složkou (bez tvrdých odkazů). ¹ Kdybychom umožnili, aby na jeden adresář odkazovalo více různých adresářových položek, celková struktura by tvořila obecný graf. Toto omezení existuje, protože práce s grafem je obecně mnohem složitější než se stromem. ## Systémová volání • ‹O_DIRECTORY› pro ‹openat› • ‹fstatat› • ‹renameat› • ‹dup› ## Knihovní podprogramy • ‹strcmp› • ‹fdopendir› • ‹readdir› • ‹rewinddir› • ‹closedir› ## d. Demonstrace (ukázky) ### 1. [‹newest›] V této ukázce budeme pracovat se složkou novým způsobem – naučíme se získat výčet zde přítomných odkazů a s těmito dále pracovat, zejména o nich zjistit další informace pomocí systémového volání ‹fstatat›. Podprogram, na kterém si to předvedeme, se bude jmenovat ‹find_newest› a vrátí jméno odkazu, který patří nejnovějšímu obyčejnému souboru (takovému, který byl naposled modifikován nejblíže současnosti). Nejprve si nachystáme pomocnou čistou funkci ‹is_newer›, která pro dvojici struktur ‹timespec› rozhodne, zda první z nich reprezentuje pozdější časový okamžik. Ve struktuře ‹stat› máme zaručeno, že ‹tv_nsec› je v rozsahu 0 až 10⁶, tzn. struktura je vždy normalizovaná a hodnoty můžeme bez problémů srovnat po složkách. bool is_newer( struct timespec a, struct timespec b ) /* C */ { return a.tv_sec > b.tv_sec || ( a.tv_sec == b.tv_sec && a.tv_nsec > b.tv_nsec ); } Podprogramu ‹find_newest› předáme složku, se kterou bude pracovat, prostřednictvím popisovače. Výsledkem bude ukazatel na dynamicky alokované jméno odkazu, nebo nulový ukazatel v případě chyby. char *find_newest( int dir_fd ) /* C */ { Pro práci se seznamem odkazů ve složce slouží především knihovní podprogram ‹readdir›. Tento však nepracuje přímo s popisovačem, ale se strukturou ‹DIR›, kterou musíme nejprve získat – k tomu nám poslouží knihovní funkce ‹fdopendir›, která ale převezme vlastnictví popisovače a tím ho efektivně zničí. Popisovač ‹dir_fd› ale nepatří podprogramu ‹find_newest› (je mu pouze propůjčen volajícím) a tedy musí být zachován. Proto si nejprve vytvoříme kopii voláním ‹dup›. DIR *dir = NULL; /* C */ Nachystáme si také několik proměnných, ve kterých budeme uchovávat průběžné výsledky. char *rv = NULL, *newest_name = NULL; /* C */ struct timespec newest_time = { 0 }; int dup_fd = dup( dir_fd ); /* C */ if ( dup_fd == -1 ) /* C */ goto out; Nyní máme k dispozici popisovač, který můžeme předat knihovnímu podprogramu ‹fdopendir›, aniž bychom byli nuceni uvolnit zdroj, který nám nepatří. Popisovač ‹dup_fd› od této chvíle nesmíme přímo používat – stal se de-facto součástí vzniklé struktury ‹DIR›. dir = fdopendir( dup_fd ); /* C */ Volání ‹fdopendir› může selhat, např. v situaci, kdy nám byl předán neplatný nebo jinak nepoužitelný popisovač (např. neodkazuje na složku). if ( !dir ) /* C */ goto out; Pozice čtení je podobně jako u běžného souboru vlastností samotného objektu „otevřeného adresáře“ který je popisovačem odkazován (není tedy ani vlastností popisovače, ani struktury ‹DIR›). Protože nemáme jistotu, že pozice čtení je při volání ‹find_newest› na začátku složky, použijeme knihovní podprogram ‹rewinddir›, který pozici čtení přesune na začátek složky a zároveň synchronizuje strukturu ‹DIR› s touto novou pozicí. Podprogram ‹rewinddir› trochu neobvykle selhat nemůže. rewinddir( dir ); /* C */ Nyní jsme připraveni na čtení odkazů. Použijeme k tomu podprogram ‹readdir›, který bude postupně vracet jednotlivé položky adresáře, na který odkazuje předaná struktura ‹DIR› – každé volání přečte jeden odkaz. Výsledek obdržíme jako ukazatel na strukturu typu ‹dirent›. struct dirent *ptr; /* C */ struct stat st; while ( ( ptr = readdir( dir ) ) ) /* C */ { Obdrželi jsme nový odkaz, který nyní zpracujeme. Má pouze dvě položky, na které se můžeme spolehnout – ‹d_ino› – číslo odkazovaného i-uzlu a ‹d_name› – jméno odkazu. Další informace získáme voláním ‹fstatat› na toto jméno. V této posloupnosti operací je zabudovaný hazard souběhu – v době mezi čtením seznamu odkazů a voláním ‹fstatat› mohl odkaz přestat existovat. S tím nemůžeme dělat nic a musíme s takovou možností počítat. Pro účely tohoto předmětu můžeme takové zmizení odkazu považovat za fatální chybu a trochu si tak zjednodušit život. Abychom dostali skutečně informace o souboru, který je odkazován adresářem, jako čtvrtý parametr předáme příznak ‹AT_SYMLINK_NOFOLLOW›, který zabezpečí, že volání ‹fstatat› nebude následovat měkké odkazy. if ( fstatat( dir_fd, ptr->d_name, &st, /* C */ AT_SYMLINK_NOFOLLOW ) == -1 ) goto out; Ve struktuře ‹st› jsou nyní vyplněny další informace o odkazovaném souboru (pozor, nikoliv o samotném odkazu!). Mimo jiné se zde nachází položka ‹st_mode›, která kóduje přístupová práva a typ souboru – pro jeho dekódování můžeme použít makra ‹S_IS*›.¹ Obyčejný soubor například poznáme za pomoci makra-predikátu ‹S_ISREG›. Dále struktura ‹stat› obsahuje položku ‹st_mtim›, podle které určíme stáří souboru. Samotné srovnání časových razítek delegujeme na výše uvedenou pomocnou funkci. if ( S_ISREG( st.st_mode ) && /* C */ is_newer( st.st_mtim, newest_time ) ) { free( newest_name ); newest_time = st.st_mtim; newest_name = strdup( ptr->d_name ); if ( !newest_name ) /* C */ goto out; } } Tím je procházení adresáře úspěšně ukončeno a ukazatel ‹newest_name› obsahuje požadovaný výsledek. Uvolníme lokální zdroje a výsledek vrátíme volajícímu. rv = newest_name; /* C */ newest_name = NULL; out: free( newest_name ); if ( dir ) /* C */ closedir( dir ); else if ( dup_fd != -1 ) close( dup_fd ); return rv; /* C */ } int main( void ) /* demo */ /* C */ { int cwd_fd = open( ".", O_DIRECTORY | O_RDONLY ); if ( cwd_fd == -1 ) /* C */ err( 2, "opening working directory" ); char *name = find_newest( cwd_fd ); /* C */ dprintf( STDOUT_FILENO, "the newest file is %s\n", name ); free( name ); return 0; } ¹ Kde naleznete dokumentaci těchto maker závisí od systému, na systémech Linux to je manuálová stránka ‹inode›. Máte-li k dispozici normu POSIX ve formě manuálových stránek (dostupné např. na stroji ‹aisa›), naleznete je také pod klíčem ‹sys_stat.h›. ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹depth›] V této ukázce přidáme k té předchozí «rekurzi» – budeme pracovat s celým adresářovým podstromem, nikoliv jen jednotlivým adresářem. V takových případech bude obvykle výhodné rozdělit podprogram na dvě části – vstupní (‹tree_depth› definované níže) a rekurzivní (‹tree_depth_rec› definováno zde). Protože potřebujeme být schopni signalizovat chybu, pro tento účel si vyhradíme návratovou hodnotu a pro vše ostatní budeme používat parametry. To nám navíc umožní jednoduše sdílet data mezi různými úrovněmi zanoření. Nachystat tato společná data bude úkolem vstupní procedury ‹tree_depth›, v rekurzi si je budeme předávat ukazatelem. V tomto případě bude společným stavem pouze dosažená maximální hloubka ‹max_depth›. Lokální stav je pak tvořen aktuální hloubkou zanoření, předávaný v parametru ‹depth›. Každá aktivace podprogramu ‹tree_depth_rec› bude pracovat s jednou složkou, kterou jí předáme v parametru ‹root_fd› jako popisovač. Vlastnictví tohoto popisovače přechází voláním na aktivaci podprogramu ‹tree_depth_rec›, které byl předán jako parametr a tato je tedy odpovědna i za jeho uvolnění (uzavření). int tree_depth_rec( int root_fd, int *max_depth, int depth ) /* C */ { Abychom nemuseli kontrolovat platnost popisovače v místech volání (jsou dvě – vstupní a rekurzivní), sjednotíme tuto kontrolu zde. if ( root_fd == -1 ) /* C */ return -1; Vstupujeme-li do hloubky, která je větší než dosud největší navštívená, tuto skutečnost poznačíme do sdílené stavové proměnné ‹max_depth›. if ( depth > *max_depth ) /* C */ *max_depth = depth; Dále si připravíme strukturu ‹DIR›, abychom mohli prohledat případné podsložky (které nalezneme použitím podprogramu ‹readdir› níže). int rv = -1; /* C */ DIR *dir = fdopendir( root_fd ); if ( !dir ) /* C */ goto out; Popisovač kořenová složky je vstupnímu podprogramu ‹tree_depth› předán v neznámém stavu, před čtením jednotlivých položek v adresáři tedy přesuneme pozici čtení na začátek. rewinddir( dir ); /* C */ struct dirent *ptr; /* C */ struct stat st; Následuje standardní iterace položkami adresáře. while ( ( ptr = readdir( dir ) ) ) /* C */ { Novým prvkem je, že musíme hlídat dvě speciální položky, ‹.› a ‹..›, které nejsou stromové – ta první odkazuje na adresář samotný, ta druhá na adresář rodičovský. Kdybychom tyto položky zpracovali, v obou případech bychom skončili v nekonečné smyčce – proto je musíme přeskočit. if ( strcmp( ptr->d_name, "." ) == 0 || /* C */ strcmp( ptr->d_name, ".." ) == 0 ) continue; Nyní získáme informace o i-uzlu. Budou nás zajímat pouze podsložky, proto všechny ostatní typy souborů přeskočíme. if ( fstatat( root_fd, ptr->d_name, &st, /* C */ AT_SYMLINK_NOFOLLOW ) == -1 ) goto out; if ( !S_ISDIR( st.st_mode ) ) /* C */ continue; Nyní víme, že ‹ptr› popisuje skutečnou podsložku, kterou je potřeba rekurzivně zpracovat. Pro tento účel získáme k této složce popisovač. Případné selhání řešíme na začátku tohoto podprogramu – tato kontrola tedy proběhne až uvnitř rekurzivní aktivace. Ukazatel na sdílený stav ‹max_depth› předáváme beze změny, ale lokální počítadlo hloubky zvýšíme o jedničku. int sub_fd = openat( root_fd, ptr->d_name, /* C */ O_DIRECTORY | O_RDONLY ); if ( tree_depth_rec( sub_fd, max_depth, depth + 1 ) == -1 ) /* C */ goto out; } rv = 0; /* C */ out: if ( dir ) closedir( dir ); return rv; /* C */ } Rekurzi máme tímto vyřešenu, zbývá naprogramovat vstupní podprogram. Ten je velmi jednoduchý – na začátku nastavíme jak ‹max_depth› tak lokální hloubku ‹depth› na nulu a spustíme rekurzi z předané složky ‹root_fd›. Také zde vyřešíme to, že ‹root_fd› je podprogramu ‹tree_depth› pouze propůjčen a tedy ho nelze předat do ‹tree_depth_rec›, které očekává popisovač, který si může (a musí) přivlastnit. K tomu využijeme systémové volání ‹dup›, které vyrobí kopii popisovače, která bude odkazovat na tentýž otevřený adresář (ale který lze nezávisle zavřít). int tree_depth( int root_fd ) /* C */ { int max_depth = 0; if ( tree_depth_rec( dup( root_fd ), &max_depth, 0 ) == -1 ) /* C */ return -1; return max_depth; /* C */ } Dále již podprogram ‹tree_depth› pouze otestujeme. static int mkdir_or_die( int dir_fd, const char *name ) /* C */ { int fd; if ( mkdirat( dir_fd, name, 0777 ) == -1 && errno != EEXIST ) /* C */ err( 1, "creating directory %s", name ); if ( ( fd = openat( dir_fd, name, O_DIRECTORY ) ) == -1 ) err( 1, "opening newly created directory %s", name ); return fd; /* C */ } int main( void ) /* demo */ /* C */ { int fds[ 5 ]; fds[ 0 ] = mkdir_or_die( AT_FDCWD, "zt.d2_root" ); /* C */ fds[ 1 ] = mkdir_or_die( fds[ 0 ], "a" ); fds[ 2 ] = mkdir_or_die( fds[ 0 ], "b" ); fds[ 3 ] = mkdir_or_die( fds[ 2 ], "c" ); fds[ 4 ] = mkdir_or_die( fds[ 3 ], "d" ); assert( tree_depth( fds[ 0 ] ) == 3 ); /* C */ assert( tree_depth( fds[ 1 ] ) == 0 ); assert( tree_depth( fds[ 2 ] ) == 2 ); assert( tree_depth( fds[ 3 ] ) == 1 ); assert( tree_depth( fds[ 4 ] ) == 0 ); for ( int i = 0; i < 5; ++i ) /* C */ close( fds[ i ] ); return 0; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹refs›] int count_refs_rec( int root_fd, dev_t dev, ino_t ino, int *count ) /* C */ { if ( root_fd == -1 ) return -1; int rv = -1; /* C */ DIR *dir = fdopendir( root_fd ); if ( !dir ) /* C */ goto out; rewinddir( dir ); /* C */ struct dirent *ptr; /* C */ struct stat st; while ( ( ptr = readdir( dir ) ) ) /* C */ { if ( strcmp( ptr->d_name, "." ) == 0 || strcmp( ptr->d_name, ".." ) == 0 ) continue; if ( fstatat( root_fd, ptr->d_name, &st, /* C */ AT_SYMLINK_NOFOLLOW ) == -1 ) goto out; if ( st.st_dev == dev && st.st_ino == ino ) /* C */ { dprintf( 2, "found copy at %s\n", ptr->d_name ); ++ *count; } if ( S_ISDIR( st.st_mode ) ) /* C */ { int sub_fd = openat( root_fd, ptr->d_name, O_DIRECTORY | O_RDONLY ); if ( count_refs_rec( sub_fd, dev, ino, count ) == -1 ) /* C */ goto out; } } rv = 0; /* C */ out: if ( dir ) closedir( dir ); return rv; /* C */ } int count_refs( int root_fd, int file_fd ) /* C */ { struct stat st; int count = 0; if ( fstat( file_fd, &st ) == -1 ) /* C */ return -1; if ( count_refs_rec( dup( root_fd ), st.st_dev, /* C */ st.st_ino, &count ) == -1 ) return -1; return count; /* C */ } static int mkdir_or_die( int dir_fd, const char *name ) /* C */ { int fd; if ( mkdirat( dir_fd, name, 0777 ) == -1 && errno != EEXIST ) /* C */ err( 1, "creating directory %s", name ); if ( ( fd = openat( dir_fd, name, O_DIRECTORY ) ) == -1 ) err( 1, "opening newly created directory %s", name ); return fd; /* C */ } static int create_file( int dir_fd, const char *name ) /* C */ { int fd = openat( dir_fd, name, O_CREAT | O_WRONLY, 0666 ); if ( fd == -1 ) /* C */ err( 1, "creating file %s", name ); return fd; /* C */ } static void link_or_die( int fd_1, const char *name_1, /* C */ int fd_2, const char *name_2 ) { if ( unlinkat( fd_2, name_2, 0 ) == -1 && errno != ENOENT ) err( 1, "unlinking %s", name_2 ); if ( linkat( fd_1, name_1, fd_2, name_2, 0 ) == -1 ) err( 1, "linking %s to %s", name_1, name_2 ); } int main( void ) /* demo */ /* C */ { int fds[ 5 ]; fds[ 0 ] = mkdir_or_die( AT_FDCWD, "zt.d3_root" ); /* C */ fds[ 1 ] = mkdir_or_die( fds[ 0 ], "a" ); fds[ 2 ] = mkdir_or_die( fds[ 0 ], "b" ); fds[ 3 ] = mkdir_or_die( fds[ 2 ], "c" ); fds[ 4 ] = mkdir_or_die( fds[ 3 ], "d" ); int fd_1 = create_file( fds[ 0 ], "foo_1" ); /* C */ int fd_2 = create_file( fds[ 1 ], "bar_1" ); link_or_die( fds[ 0 ], "foo_1", fds[ 0 ], "foo_2" ); /* C */ link_or_die( fds[ 0 ], "foo_1", fds[ 1 ], "foo_3" ); link_or_die( fds[ 1 ], "bar_1", fds[ 2 ], "bar_2" ); assert( count_refs( fds[ 0 ], fd_1 ) == 3 ); /* C */ assert( count_refs( fds[ 1 ], fd_1 ) == 1 ); assert( count_refs( fds[ 1 ], fd_2 ) == 1 ); assert( count_refs( fds[ 2 ], fd_2 ) == 1 ); assert( count_refs( fds[ 2 ], fd_1 ) == 0 ); for ( int i = 0; i < 5; ++i ) /* C */ close( fds[ i ] ); close( fd_1 ); /* C */ close( fd_2 ); return 0; /* C */ } ## p. Přípravy ### 1. [‹fcount›] Napište podprogram ‹fcount›, kterému je předán popisovač otevřené složky, a který spočítá, kolik je (přímo) v této odkazů na «obyčejné» soubory (je-li tentýž soubor odkázán více než jednou, počítá se každý odkaz). Nezáporná návratová hodnota určuje počet nalezených souborů, -1 pak indikuje systémovou chybu. int fcount( int dir_fd ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 2. [‹access›] Naprogramujte proceduru ‹check_access›, která na vstupu dostane: 1. popisovač otevřené složky ‹dir_fd›, 2. spojovaný seznam ‹names› jmen. Tato ze seznamu odstraní všechna jména souborů, které jsou chráněny proti zápisu třetí stranou a ponechá ty, do kterých může zapisovat kdokoliv jiný, než vlastník. Všechna jména jsou relativní vůči složce ‹dir_fd›. Práva souboru zjistíte z položky ‹st_mode› struktury ‹stat›. Přijdou Vám zřejmě vhod také hodnoty ‹S_IWGRP› (Stat Inode Write GRouP – bitová maska odpovídající právu skupiny na zápis) a ‹S_IWOTH› (Stat Inode Write OTHer – totéž, ale pro všechny ostatní). Návratová hodnota: • -2 v případě, kdy nebylo možné práva u některého jména ověřit (např. proto, že takový odkaz neexistuje), • -1 při neopravitelné systémové chybě (např. chyba při načítání položek adresáře), • 0 při úspěšném dokončení. Odkazy, u kterých se nepodařilo přístupová práva ověřit, ponechte v seznamu. struct name_list_node /* C */ { const char *name; struct name_list_node *next; }; struct name_list /* C */ { struct name_list_node *head; }; int check_access( int dir_fd, struct name_list *names ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹list›] Váš úkol v této přípravě je přímočarý – na vstupu dostanete popisovač otevřené složky, ze kterého vytvoříte zřetězeny seznam jmen. Jména budou uložena v dynamicky alokovaných polích vhodné velikosti. Výsledkem je nulový ukazatel, dojde-li během zpracování složky k chybě, nebo ukazatel na hlavu sestaveného seznamu. struct name_list /* C */ { char *name; struct name_list *next; }; struct name_list *list( int real_dir_fd ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 4. [‹archive›] Napište proceduru ‹archive_files›, která na vstupu obdrží: 1. ‹work_fd› – popisovač složky, ze které bude staré soubory přesouvat do archivu, 2. ‹archive_fd› – popisovač archivní složky (do této složky budou staré soubory přesunuty), 3. ‹threshold› – hodnota ‹struct timespec›, která určuje nejstarší soubor, který má být ve složce ‹work_fd› zachován. Dále: • pro určení stáří souboru použijeme časové razítko posledního zápisu (‹mtime›), • dojde-li ke kolizi jmen ve složce ‹archive_fd›, existující soubory budou nahrazeny (jinak ale existující soubory zůstávají nedotčené), • dojde-li během zpracování k chybě a povaha chyby to umožňuje, procedura ‹archive_files› se pokusí zbývající položky přesto zpracovat. Návratová hodnota: • 0 značí, že vše proběhlo bez chyb, • -1 že operace byla z důvodu chyby přerušena, • -2 že některé soubory nebylo možné zkontrolovat nebo přesunout. int archive_files( int work_fd, int archive_fd, /* C */ struct timespec threshold ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 5. [‹find›] Naprogramujte proceduru ‹find›, která obdrží: • ‹root_fd› je popisovač otevřeného adresáře, • ‹name› je jméno hledaného odkazu, • ‹flags› jsou příznaky, které předá volání ‹openat›. a která prohledá podstrom začínající v ‹root_fd›. Nalezne-li v zadaném podstromě právě jeden odkaz hledaného jména, tento otevře (s příznaky nastavenými na ‹flags›) a výsledný popisovač vrátí. Nenalezne-li žádný nebo více než jeden vyhovující odkaz, vrátí hodnotu -2. Dojde-li při prohledávání k chybě, vrátí -1. Nezapomeňte uvolnit veškeré zdroje, které jste alokovali (s případnou výjimkou popisovače, který je funkcí vrácen). int find( int root_fd, const char *name, int flags ); /* C */ ### 6. [‹du›] Naprogramujte proceduru ‹disk_usage›, která prohledá zadaný podstrom a sečte velikosti všech «obyčejných» souborů (v bajtech, nikoliv alokovaných blocích), které se zde nachází. Výsledkem nechť je tato celková velikost. Chyby: • existuje-li se ve stromě obyčejný soubor, na který odkazuje více než jeden odkaz (nerozhoduje, zda je tento odkaz uvnitř nebo vně prohledávaného stromu), prohledávání ukončete s výsledkem -2, • dojde-li při zpracování podstromu k systémové chybě, výsledek bude -1. ssize_t disk_usage( int root_fd ); /* C */ # Adresy a síť V této kapitole se budeme zabývat «adresami», které nám umožní navázat komunikaci mezi různými programy a případně i různými počítači. Klíčové bude systémové volání ‹connect› a pro síťovou komunikaci také knihovní podprogram ‹getaddrinfo›. Ukázky: 1. ‹client› – klient pro proudový socket rodiny ‹AF_UNIX›, 2. ‹webget› – stažení webové stránky ze zadané adresy. Přípravy: 1. ‹echoc› – gai + connect + read 3. ‹ntpc› – (UDP) klient pro NTP 4. ‹whois› – vrátit první řádek WHOIS odpovědi 5. ‹newsc› – TCP verze 02/newsc 5. ‹host› – najde první IPv6 adresu zadaného systému 6. ‹splice› – přeposílat data z jednoho socketu do jiného Řešené příklady: 1. ‹bcast› – přeposílání datagramů na zadaný seznam adres, 2. ‹proxy› – zrcadlení datagramů na adresy uložené v nich, 3. ‹router› – přeposílání datagramů podle zadané tabulky adres, 4. ‹xxx› 5. ‹xxx› 6. ‹xxx› ## Systémová volání Se sockety jsme již v tomto kurzu pracovali mnohokrát, nicméně vždy to bylo v podobě již nachystaného popisovače.¹ Nyní se seznámíme se systémovými voláními, které nám umožní socket vytvořit, pro spojované sockety navázat spojení, a pro ty nespojované komunikovat s nějakou konkrétní protistranou. Tento týden se zaměříme na systémová volání ‹connect› a ‹bind›, na ‹listen› a ‹accept› si budete muset počkat do kapitoly deváté. ### ‹socket› První zásadní rozdíl mezi socketem a obyčejným souborem spočívá v tom, jak socket vznikne – místo volání ‹openat› použijeme volání ‹socket›.² Má 3 parametry, které mají významný vliv na to, jak lze takto vytvořený socket dále používat: 1. ‹int domain› určuje tzv. komunikační «doménu» resp. «adresovací rodinu», kterou nám socket zpřístupňuje – nejběžnějšími jsou tzv. internetové sockety (které umožňují komunikaci protokoly IPv4 a IPv6), ale prozatím se budeme zabývat jednoduššími sockety z domény ‹AF_UNIX›, které umožňují komunikaci pouze v rámci lokálního systému, 2. ‹int type› rozlišuje pro nás dva klíčové případy použití: ◦ tzv. «spojované» sockety typu ‹SOCK_STREAM›, kdy jednotlivé «spojení» je reprezentováno opět socketem, který již dále pracuje jako obousměrná roura,³ ◦ «datagramové» sockety typu ‹SOCK_DGRAM› jsou jednodušší, ale zároveň se méně podobají na obyčejné soubory, a práce s nimi vyžaduje speciální funkce (nelze použít již známé ‹read› a ‹write›), 3. ‹int protocol› rozlišuje konkrétní protokol v rámci možností určených předchozími dvěma parametry – pro tento parametr budeme vždy používat hodnotu 0, která volbu vhodného protokolu přenechá operačnímu systému. ### ‹sendto› Jako ‹send›/‹write›, ale na zadanou adresu. Použitelné pouze s datagramovými sockety. TBD. ### ‹recvfrom› Jako ‹recv›/‹read›, ale u datagramových socketů zároveň získá a vyplní adresu odesílatele. TBD. ### ‹bind› Přiřadí otevřenému anonymnímu socketu adresu. Prozatím budeme používat pouze s datagramovými sockety. ### ‹connect› Pro spojovaný socket naváže spojení. Pro datagramový nastaví implicitní cílovou adresu. ¹ Neplatí pro 2023, nicméně vytvoření socketu a navázání spojení si na tomto místě i tak zopakujeme. ## Knihovní podprogramy Získat adresu, kterou předáme systémovému volání ‹connect›, nám pomůže knihovní podprogram ‹getaddrinfo›, který nám zprostředkuje přistup (zejména) k systému DNS. Pro výpis internetových adres se Vám může hodit také podprogram ‹inet_ntop›, který zadanou adresu převede do „lidsky čitelné“ podoby. Pro řešení příkladů jej ale potřebovat nebudeme. ### ‹getaddrinfo› Nalezne a vyplní adresu (‹sockaddr›) podle zadaného jména. TBD. ## d. Demonstrace (ukázky) ### 1. [‹client›] Tento jednoduchý program demonstruje použití (spojovaného) socketu ze strany klienta – v této kapitole se budeme držet socketů typu ‹AF_UNIX›, které jako adresy používají cesty v souborovém systému (koncový bod takového socketu, má-li přiřazenu adresu, je v souborovém příkazu viditelný jako speciální typ souboru – například i příkazem ‹ls›). Tento program obdrží dva parametry: adresu (cestu) k socketu ke kterému se má připojit, a zprávu, kterou má na tento socket odeslat. V součinnosti s další ukázkou si můžete předání zprávy otestovat. int main( int argc, const char **argv ) /* demo */ /* C */ { if ( argc != 3 ) errx( 0, "need 2 arguments: socket_path message" ); Sockety jsou symetrické v tom smyslu, že bez ohledu na to, jsme-li „server“ nebo „klient“, musíme socket vždy připravit voláním ‹socket›. Zde se rozhodne o typu socketu – jakou bude používat komunikační doménu (zde ‹AF_UNIX›) a v jakém bude pracovat režimu (zde spojovaném, ‹SOCK_STREAM›). Výsledkem volání ‹socket› je popisovač otevřeného souboru (do kterého ale pro tuto chvíli nelze ani zapisovat, ani z něj číst). Jak je obvyklé, volání vrátí v případě neúspěchu hodnotu -1 a nastaví ‹errno›. int sock_fd = socket( AF_UNIX, SOCK_STREAM, 0 ); /* C */ if ( sock_fd == -1 ) /* C */ err( 1, "creating a unix socket" ); Abychom mohli pomocí socketu komunikovat, musíme ho propojit s jiným socketem, typicky v jiném programu. Klientská strana tohoto «připojení» je jednodušší, proto jí začneme.¹ Abychom se mohli připojit k nějakému socketu, musíme znát jeho «adresu» – jak tato adresa přesně vypadá je dáno právě doménou socketu. Proto má každá doména vlastní datový typ, který takovou adresu reprezentuje. V případě socketů ‹AF_UNIX› je to typ ‹sockaddr_un›, který obsahuje jedinou adresovací položku a to ‹sun_path›, do které uložíme cestu (nulou ukončený řetězec). struct sockaddr_un sa = { .sun_family = AF_UNIX }; /* C */ if ( strlen( argv[ 1 ] ) >= sizeof sa.sun_path - 1 ) /* C */ errx( 1, "socket address too long, maximum is %zu", sizeof sa.sun_path ); snprintf( sa.sun_path, sizeof sa.sun_path, "%s", argv[ 1 ] ); /* C */ Tím je adresa vyřešena, nyní pomocí volání ‹connect› socket připojíme. Protože adresy mohou být různých typů, mohou být aj různých velikostí, proto musíme volání ‹connect› sdělit, jak velká je adresa, kterou mu předáváme. Všimněte si, že výsledkem volání ‹connect› «není» nový popisovač – volání změní stav existujícího popisovače ‹sock_fd›. if ( connect( sock_fd, ( struct sockaddr * ) &sa, /* C */ sizeof sa ) == -1 ) err( 1, "connecting to %s", sa.sun_path ); Spojení jsme úspěšně navázali, můžeme posílat data. Podobně jako soubory, pomocí socketů lze přenášet libovolné posloupnosti bajtů (nemusíme se nutně omezovat na textová data, která posíláme v tomto případě). const char *message = argv[ 2 ]; /* C */ size_t nbytes = strlen( message ); if ( write( sock_fd, message, nbytes ) == -1 ) /* C */ err( 1, "sending data on %s", sa.sun_path ); Socket samozřejmě nesmíme zapomenout zavřít. Krom uvolnění zdrojů má v případě spojovaného socketu uzavření popisovače ještě jednu velmi důležitou funkci – ukončí navázané spojení (druhá strana dostane při čtení po ukončení spojení výsledek „konec souboru“).² if ( close( sock_fd ) == -1 ) /* C */ warn( "closing %s", sa.sun_path ); return 0; /* C */ } ¹ Samozřejmě, explicitně připojit lze pouze spojované sockety (ty zde používané, nebo např. TCP). Datagramové sockety žádné pevné spojení nenavazují, nicméně volání ‹connect› je pro ně stále platné, jak uvidíme v pozdější ukázce. ² Dokud spojení neuzavřeme, druhá strana nemá jak zjistit, že už žádná data nehodláme posílat, a volání ‹read› bude na případné další zprávy libovolně dlouho čekat. ## p. Přípravy ### 1. [‹host›] ### 2. [‹echoc›] ### 3. [‹ntpc›] ### 4. [‹whois›] Vaším úkolem je získat adresu whois serveru podle tld.whois-servers.net, připojit se k tomuto serveru a zjistit informace o zadané doméně. ### 5. [‹newsc›] ### 6. [‹splice›] ## r. Řešené úlohy ### 1. [‹bcast›] Naprogramujte proceduru ‹bcast›, která obdrží: • popisovač datagramového socketu, • zřetězený seznam adres. Jejím úkolem bude každý přijatý datagram přeposlat na všechny adresy v seznamu, s výjimkou původního odesílatele. Jako maximální možnou délku datagramu považujte pro tento úkol 4 KiB. V případě že nastane nějaká chyba, vypište pouze varování a pokračujte, je-li to možné. struct address_list /* C */ { struct sockaddr_un address; struct address_list *next; }; void bcast( int sock_fd, struct address_list *addresses ); /* C */ ### 2. [‹proxy›] Naprogramujte proceduru ‹proxy›, která obdrží popisovač datagramového socketu a která pro každý přijatý datagram provede následovné: 1. z datagramu přečte prvních ‹sizeof sockaddr_un› bajtů, 2. zbytek datagramu přepošle na takto získanou adresu. Předpokládejte, že datagramy budou mít celkovou délku nejvýše 4KiB. V případě chyby vypište varování, nebo není-li možné v programu pokračovat, tento ukončete s chybou. void proxy( int sock_fd ); /* C */ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 3. [‹router›] Naprogramujte proceduru ‹router›, která bude přeposílat pakety podle tabulky, která je zadaná binárním vyhledávacím stromem. Pro každý příchozí datagram: 1. vyhledejte odesílatele ve stromě (adresa ‹source› musí být stejná jako adresa odesílatele datagramu; strom je seřazen podle ‹source›),¹ 2. je-li odpovídající záznam ve stromě přítomen, přepošlete datagram na adresu ‹destination› uvedenou v tomto záznamu. Předpokládejte, že maximální velikost datagramu bude 512 bajtů. Za normálních okolností se procedura ‹router› nikdy nevrátí. Nastane-li při běhu procedury ‹router› fatální chyba, program ukončete s vhodným chybovým hlášením (rozmyslete si ale, které chyby lze považovat za fatální). Při chybách, které umožňují pokračování v běhu, vypište pouze varování na standardní chybový výstup. ¹ Ve smyslu vyhledávacího stromu – adresy v levém podstromě jsou lexikograficky menší, a v pravém podstromě lexikograficky větší, než je adresa v kořenu. struct address_map /* C */ { struct sockaddr_un source, destination; struct address_map *left, *right; }; void router( int sock_fd, struct address_map *root ); /* C */ # Spustitelné soubory • ‹execve› • ‹getenv› • ‹setenv›, ‹putenv› • ‹O_CLOEXEC› • ‹dup2› Přípravy: 1. ‹shell› – spustit ‹$SHELL -c něco› 2. ‹execp› – jako ‹execvp› ale nastavit argv0 podle nalezené cesty 3. ‹binex› – spustit soubor pokud to není skript (podle ELF hlavičky) 4. ‹uexec› – spustit soubor pokud není set(gs)id? 5. ‹withfile› – přesměruje zadaný soubor na stdin zadaného příkazu 6. ‹env› – spustit příkaz s rozšířeným prostředím ## p. Přípravy ### 1. [‹shell›] ### 2. [‹execp›] ### 3. [‹binex›] ### 4. [‹uexec›] ### 5. [‹wfile›] ### 6. [‹env›] # S.2. Složky, cesty, adresy Příklady druhé sady se zabývají prací se souborovým systémem (nikoliv už pouze jednotlivými soubory), ale také s adresami a síťovými spojeními. Příklady jsou tyto: A. ‹dup› – rozdělení pevných odkazů na samostatné soubory B. ‹dir› – práce s adresářovým stromem C. ‹dedup› – spojování identických souborů pevnými odkazy D. ‹httpc› – klient pro stažení souboru z webu E. ‹httpd› – jednoduchý webový server F. ‹execline› – skripty v jednom procesu ## a. ‹dup› Vaším úkolem bude naprogramovat proceduru ‹dup_links›, která dostane jako vstup popisovač složky (adresáře) a v celém takto určeném «podstromě» provede „rozdělení“ tvrdých odkazů. Procedura zajistí, že • ve výsledném stromě ukazuje na každý obyčejný soubor právě jeden odkaz, a to tak, že • obsah souboru, na který ukazuje více odkazů, nakopíruje do nového souboru a • původní sdílený odkaz nahradí odkazem na tento nový soubor. Soubory, na které vedl jediný odkaz již na začátku, se nesmí touto procedurou nijak změnit. Selže-li některá operace, podstrom musí zůstat v konzistentním stavu, zejména nesmí žádný odkaz chybět.¹ Je dovoleno, aby při násobném selhání systémových volání zůstal ve stromě nějaký odkaz nebo soubor navíc. Návratová hodnota: • nezáporné číslo označuje úspěch a zároveň indikuje počet nových souborů (i-uzlů), které bylo potřeba vytvořit, • ‹-1› indikuje situaci, kdy došlo k chybě, ale strom je zcela konzistentní (tzn. každý odkaz je buď v nedotčeném původním stavu, nebo ukazuje na správně duplikovaný soubor), • ‹-2› indikuje situaci, kdy došlo k chybě jak při samotné práci procedury, tak při pokusu o uvedení stromu do konzistentního stavu, a tedy ve stromě přebývají nějaké odkazy nebo soubory. Systémové volání, které jednou selhalo, již se stejnými hodnotami parametrů neopakujte – naopak předpokládejte, že každé «selhání» je trvalé (bez ohledu na konkrétní hodnotu ‹errno›). int dup_links( int root_fd ); /* C */ ¹ Pro zajištění těchto vlastností se Vám může hodit kombinace systémového volání ‹fchdir› a knihovního podprogramu ‹mkstemp›. Žel, vhodnější ‹mkstempat› není v tuto chvíli standardizované. Použijete-li ovšem ‹fchdir›, nezapomeňte před ukončením podprogramu vrátit nastavení pracovní složky do původního stavu. ## b. ‹dir› Souborové systémy bývají organizovány jako hierarchie souborů a složek. Vaším úkolem je naprogramovat procedury pro vyjádření obsahu podstromu zadaného adresářem. Výstup se bude realizovat skrze strukturu ‹node›, která má následující atributy: • ‹name› – ukazatel na nulou zakončený řetězec se jménem souboru; • ‹type› – hodnota výčtového typu níže popisující typ souboru; • ‹error› – hodnota ‹0›, nebo informace o nastalé chybě; • ‹dir› – v případě, že se jedná o adresář, obsahuje ukazatel na zřetězený seznam souborů, anebo ‹NULL›, pokud je adresář prázdný; • ‹next› – ukazatel na další prvek ve stejném adresáři, anebo ‹NULL›, pokud je poslední. Tato struktura reprezentuje jednu položku v adresářovém podstromě. Položka je buď adresářem, nebo jiným souborem. Jelikož se jedná o reprezentaci zřetězeným seznamem, struktura obsahuje ukazatele na následníka a potomka (‹next› a ‹dir›). Atributy ‹name›, ‹dir› a ‹next› jsou ukazatele na dynamicky alokovanou paměť, kterou následně musí být možné uvolnit zavoláním ‹free›. Strukturu nemůžete nijak měnit. Výčtový typ ‹file_type› popisuje pět typů souborů, které budeme rozlišovat: adresář, obyčejný soubor, symbolický odkaz, socket a vše ostatní. enum file_type /* C */ { t_directory, t_regular, t_symlink, t_socket, t_other, }; struct node /* C */ { char *name; enum file_type type; int error; /* C */ struct node *dir; /* C */ struct node *next; }; Úkol spočívá v implementaci dvou procedur níže. První z nich je ‹tree_create›, která bere parametry: • ‹at› – popisovač adresáře, ve kterém hledat složku, nebo konstanta ‹AT_FDCWD›; • ‹root_name› – název počátečního adresáře, který se má prohledávat; • ‹out› – výstupní ukazatel, do něhož má být uložen ukazatel na alokovanou zřetězenou strukturu. Její návratovou hodnotou bude: • ‹0› – úspěch; • ‹-1› – selhání pří přístupu k některému souboru či složce; • ‹-2› – selhání při alokaci, což je kritická chyba, a výstupní ‹out› nechť je nastaven na ‹NULL›. Jestliže je návratová hodnota ‹-1›, u dotčených souborů bude atribut ‹error› nastaven na odpovídající hodnotu proměnné ‹errno›. Jinak má být hodnota tohoto atributu ‹0›. int tree_create( int at, const char *root_name, struct node **out ); /* C */ Je rovněž potřeba implementovat odpovídající uvolňovací proceduru. Tou je zde ‹tree_free›, která musí být schopna přijmout výstupní ukazatel z ‹tree_create› a ten uvolnit, včetně všech přidělených zdrojů. void tree_free( struct node *tree ); /* C */ ## c. ‹dedup› V této úloze budete programovat proceduru, která realizuje opačný proces k tomu z první úlohy. Procedura ‹dedup› nalezne všechny soubory v zadaném podstromě, které mají totožný obsah, a nahradí je tvrdými odkazy na jedinou kopii. Bude mít dva parametry: • ‹root_fd› je popisovač složky, která určuje podstrom, se kterým bude pracovat, • ‹attic_fd› je popisovač složky, která bude sloužit jako „půda“ a kde se vytvoří odkazy na všechny redundantní soubory (tzn. ty, které byly nahrazeny tvrdým odkazem na jiný soubor). Složka ‹attic_fd› nesmí být uvnitř podstromu, který začíná složkou ‹root_fd›. Předpokládejte, že zadaný podstrom obsahuje pouze složky a odkazy na obyčejné soubory (tzn. neobsahuje žádné měkké odkazy, ani jiné speciální typy souborů). Na každý soubor, který má totožný «obsah» (metadata nerozhodují) jako některý jiný nalezený soubor, vytvořte ve složce ‹attic_fd› odkaz, kterého jméno bude číslo i-uzlu. Nevytvářejte zde odkazy na soubory, na které bude po skončení operace existovat odkaz ve stromě ‹root_fd›. Selže-li nějaká operace, vstupní podstrom musí zůstat v konzistentním stavu (jak vnitřně, tak vůči složce ‹attic_fd›). Povolen je pouze jeden typ nekorektního stavu, a to pouze v případě selhání několika systémových volání: ve složce ‹attic_fd› může existovat přebytečný odkaz na soubor, který je zároveň odkázán z podstromu ‹root_fd›. Bez ohledu na libovolná selhání musí každý odkaz ve stromě ‹root_fd› ukazovat na stejný «obsah» jako před provedením procedury ‹dedup›. Můžete předpokládat, že nikam do stromu ‹root_fd› ani do složky ‹attic_fd› neprobíhá souběžný zápis. Návratová hodnota nechť je ‹0› v případě úspěchu, ‹-1› v případě, že nastala chyba, ale vše je v korektním (i když nedokončeném) stavu a konečně ‹-2› došlo-li k situaci, kdy nebylo možné úplnou konzistenci zajistit. Podobně jako v úkolu A také platí, že každé selhání považujeme za trvalé (nelze jej zvrátit opakovaným voláním se stejnými parametry). int dedup( int root_fd, int attic_fd ); /* C */ ## d. ‹httpc› Vaším úkolem bude naprogramovat klient protokolu HTTP 1.0. Omezíme se na základní funkcionalitu – sestavení a odeslání požadavku typu GET nebo HEAD a přijetí a zpracování odpovědi. Řádky požadavku oddělujte sekvencí ‹\r\n› a můžete předpokládat, že server bude dodržovat totéž. Jak požadavek tak odpověď má tři části: 1. řádek požadavku (resp. stavový řádek pro odpověď), ◦ řádek požadavku má formu ‹METODA cesta HTTP/1.0›, ◦ stavový řádek má formu ‹HTTP/1.0 číselný_kód popis›, 2. hlavičky – každé pole začíná jménem, následované dvojtečkou a textovou hodnotou, která pokračuje až do konce řádku,¹ 3. tělo, oddělené od hlaviček prázdným řádkem. Obsah těla může být libovolný, nebudeme jej nijak interpretovat. Pozor, může se jednat o binární data (tzn. tělo nemusí být nutně textové). Všechny zde popsané podprogramy s návratovou hodnotou typu ‹int› vrací v případě úspěchu nulu a v případě systémové chyby -1, není-li uvedeno jinak. Jednotlivá pole hlavičky protokolu HTTP budeme reprezentovat typem ‹http_header› a celou hlavičku pak typem ‹http_header_list›. Jedná se o jednoduše zřetězený seznam dvojic klíč-hodnota. Hodnota ‹value› nebude ukončena znakem nového řádku (tzn. bude obsahovat pouze samotnou hodnotu příslušného pole). struct http_header /* C */ { char *name, *value; }; struct http_header_list /* C */ { struct http_header header; struct http_header_list *next; }; Zjednodušený požadavek protokolu HTTP budeme reprezentovat strukturou ‹http_request› – přitom budeme podporovat pouze dvě metody, totiž ‹GET› a ‹HEAD›. Tělo požadavku bude v obou případech prázdné. Prázdný seznam hlaviček je reprezentovaný nulovým ukazatelem ‹headers›. enum http_method { HTTP_GET = 1, /* C */ HTTP_HEAD }; struct http_request /* C */ { enum http_method method; char *path; struct http_header_list *headers; }; Pro zjednodušení tvorby požadavku implementujte následující dvě funkce. Veškerá paměť spojená s požadavkem je vlastnictvím požadavku – požadavek musí být platný i v situaci, kdy uživatel do funkce předané ‹path› atp. později přepíše nebo uvolní. Protože na pořadí hlaviček nezáleží, zvolte takové pořadí, aby byla implementace efektivní. int http_request_set_path( struct http_request *request, /* C */ const char *path ); int http_request_add_header( struct http_request *request, const char *field, const char *value ); Následující funkce nechť požadavek zapíše do otevřeného popisovače souboru. Tato funkce se Vám může hodit také v implementaci procedury ‹http_request› níže. int http_request_write( struct http_request *request, int fd ); /* C */ Konečně procedura ‹http_request_free› uvolní veškerou paměť spojenou s požadavkem. Opětovné volání ‹http_request_free› na stejný objekt nechť nemá žádný efekt. void http_request_free( struct http_request *request ); /* C */ Pro reprezentaci odpovědi serveru použijeme strukturu ‹http_response›, která bude obsahovat kód odpovědi, hlavičky a tělo. Podobně jako u předchozích typů, hodnota typu ‹http_response› bude vlastnit veškerou potřebnou paměť. V seznamu ‹headers› budou hlavičky seřazeny v pořadí, ve kterém je server odeslal (dejte si pozor na efektivitu!). struct http_response /* C */ { int code; struct http_header_list *headers; size_t body_length; char *body; }; Procedura ‹http_response_read› přečte odpověď protokolu HTTP ze zadaného popisovače a uloží ji do předané struktury ‹http_response›. Výsledkem bude 0 proběhlo-li vše v pořádku, -1 při systémové chybě a -2 je-li odpověď špatně sestavená. int http_response_read( struct http_response *response, int fd_in ); /* C */ Pro uvolnění veškeré paměti spojené s požadavkem slouží následující procedura. Předaná hodnota ‹http_response› bude uvedena do takového stavu, aby opětovné volání ‹http_response_free› na stejné hodnotě neprovedlo žádnou akci. void http_response_free( struct http_response *response ); /* C */ Procedura ‹http_request› provede požadavek podle parametru ‹request›. Začíná-li adresa tečkou nebo lomítkem, je interpretována jako adresa unixového socketu. V opačném případě je to hostitelské jméno počítače, ke kterému se má připojit, případně následované dvojtečkou a číslem portu. Není-li port uveden, použije standardní port 80. Procedura vyplní odpověď serveru do předané hodnoty typu ‹response›. Návratová hodnota bude 0 proběhlo-li vše v pořádku, -1 v případě systémové chyby a -2 v případě chybné odpovědi ze strany serveru. Není-li výsledek 0, předaná hodnota ‹response› zůstane nedotčena. int http_request( const char *address, /* C */ struct http_request *request, struct http_response *response ); Konečně procedura ‹http_get› navíc zařídí sestavení požadavku a uložení těla odpovědi do souboru. Na rozdíl od procedury ‹http_request› musí ‹http_get› korektně pracovat i s velkými soubory (takovými, které se nevejdou do paměti celé najednou). V sestavené hlavičce vyplní pole ‹Host› tak, aby odpovídalo zadanému hostitelskému jménu vzdáleného počítače. Soubor pak uloží do složky ‹dir_fd› pod jménem, které odpovídá poslední položce cesty ‹path›. int http_get( const char *host, const char *path, int dir_fd ); /* C */ ¹ Víceřádkové hlavičky pro zjednodušení nebudeme uvažovat. ## e. ‹httpd› TBD. Předmětem této úlohy bude server protokolu HTTP. ## f. ‹execline› Z předchozího studia znáte shell. Základní operací shellu je vytvoření nového procesu (voláním ‹fork›), typicky následované spuštěním nového programu v tomto procesu (voláním ‹exec›), přičemž rodičovský proces pouze čeká na ukončení potomka. Toto uspořádání má samozřejmě řadu výhod, není ale pro jednoduché skripty příliš efektivní – neustálé vytváření nových procesů vyžaduje od systému značné množství práce. Přitom v situaci, kdy nepotřebujeme žádnou souběžnost, bychom si teoreticky měli vystačit s jediným procesem. To má svá vlastní úskalí, ale pro jednoduché posloupnosti operací si bez větších problémů vystačíme s opakovaným voláním ‹exec› v jediném procesu (tzn. bez použití volání ‹fork›). Vaším úkolem bude naprogramovat malou sadu utilit (‹cd›, ‹cat› a ‹test›), které umožní sestavovat jednoduché skripty v tomto stylu. Každá utilita bude realizována jednou procedurou, níže připojený ‹main› se pak postará, aby se spustila ta správná procedura podle toho, jak byl přeložený program spuštěn. Pro účely testování tedy stačí pro každou proceduru vytvořit měkký odkaz daného jména: $ ln -s f_execline cd $ ln -s f_execline cat $ ln -s f_execline test Utility pak můžete testovat např takto: env PATH=$(pwd):$PATH test -L cd echo yes ; echo no Následují jednotlivé programy. Elipsa (‹…›) v popisu utility značí „zbytek příkazu“, tedy parametry, které samotná utilita nezpracovala a tvoří tak příkaz, který spustí voláním ‹execvp›. Program ‹cd {cesta} …› změní pracovní složku podle prvního parametru a zbytek příkazového řádku spustí ve stávajícím procesu. Uvažme např. příkaz ‹cd / ls -hl› – ten uvedené šabloně odpovídá takto: • ‹cd ≡ cd›, • ‹{cesta} ≡ /›, • ‹… ≡ ls -hl›. Po nastavení pracovní složky na ‹dir› tedy spustíme ‹ls -hl›. void cd( int argc, char **argv ); /* C */ Program ‹cat {popisovač} {soubor} …› přesměruje zadaný popisovač na zadaný soubor (pro jednoduchost vždy otevíráme soubor pro čtení a připojující zápis), poté spustí zbytek příkazového řádku. Popisovač může být zadán číselně, nebo jako slovo ‹stdin›, ‹stdout› a ‹stderr›. Např. cat stdout out.txt grep pomeranč in.txt cat stdout out.txt cat stdin in.txt grep pomeranč jsou ekvivalenty klasických shellových příkazů grep pomeranč in.txt >> out.txt grep pomeranč < in.txt >> out.txt void cat( int argc, char **argv ); /* C */ Konečně program ‹test {přepínač} {cesta} … ; …› je zvláštní v tom, že může pokračovat dvěma různými příkazy, podle výsledku testu. Zde ‹přepínač› je jedno z ‹-f›, ‹-d›, ‹-s› nebo ‹-L› ve stejném významu jako u POSIXové utility ‹test› (obyčejný soubor, složka, neprázdný soubor a měkký odkaz).¹ Je-li výsledek pozitivní, provede se zbytek příkazu ‹…› od začátku až k prvnímu středníku. Je-li výsledek negativní, provedený příkaz začíná po prvním středníku, např: test -f soubor rm soubor ; test -d soubor rmdir soubor odpovídá klasickému shellu: if test -f soubor; then rm soubor elif test -d soubor; then rmdir soubor fi void test( int argc, char **argv ); /* C */ ¹ Věnujte pozornost tomu, jak se POSIXová utilita chová, je-li soubor symbolickým odkazem, při jiných testech než ‹-L›. Pro všechny utility platí, že je-li zbývající příkaz prázdný, neprovede se nic a proces je ukončen. # Procesy • ‹fork› • ‹waitpid› • ‹pipe›, ‹socketpair› • kombinace ‹fork› + ‹exec› ## d. Demonstrace (ukázky) ### 2. [‹server›] Tento program představuje velmi jednoduchý server, který spolupracuje s klientem z předchozí ukázky. Server se vyznačuje dvěma vlastnostmi: 1. server je ta strana, která má reprezentovatelnou adresu – známe-li tuto adresu, můžeme se k serveru připojit,¹ 2. server komunikuje s větším počtem protistran, typicky souběžně (i když my se v této kapitole omezíme na sekvenční odbavování klientů). Server, který zde naprogramujeme, bude velmi jednoduchý – zprávy, které obdrží, jednoduše přepíše na svůj standardní výstup. Klientům nebude odesílat žádné odpovědi (ale připojený socket je plně duplexní – lze z něj nejen číst, ale do něj i zapisovat). int main( int argc, const char **argv ) /* C */ { if ( argc != 2 ) errx( 0, "expected argument: socket_path" ); První část serveru je shodná se serverem – vytvoříme socket ve správné doméně a správného typu. int sock_fd = socket( AF_UNIX, SOCK_STREAM, 0 ); /* C */ if ( sock_fd == -1 ) err( 1, "creating a unix socket" ); Opět budeme potřebovat adresu – tentokrát se na ni nebudeme připojovat, ale «poslouchat» a tím umožníme klientům, aby se k této adrese připojili. Postup pro vytvoření adresy je opět shodný s klientem struct sockaddr_un sa = { .sun_family = AF_UNIX }; /* C */ if ( strlen( argv[ 1 ] ) >= sizeof sa.sun_path - 1 ) /* C */ errx( 1, "socket address too long, maximum is %zu", sizeof sa.sun_path ); snprintf( sa.sun_path, sizeof sa.sun_path, "%s", argv[ 1 ] ); /* C */ Tím ale podobnost končí. Předtím, než nachystanému socketu přiřadíme adresu, musíme tuto ještě uvolnit – volání ‹bind› selže, je-li předaná cesta již existuje (bez ohledu na to, jakého je typu soubor, který odkazuje).² To provedeme pomocí volání ‹unlink›, které možná znáte z testů. Nepodaří-li se soubor odstranit, nemá smysl pokračovat – volání ‹bind› by selhalo. if ( unlink( sa.sun_path ) == -1 && errno != ENOENT ) /* C */ err( 1, "unlinking %s", sa.sun_path ); Konečně můžeme socket «svázat» s adresou, voláním ‹bind› (význam parametrů je stejný, jako u volání ‹connect›). if ( bind( sock_fd, ( struct sockaddr * ) &sa, sizeof sa ) ) /* C */ err( 1, "binding a unix socket to %s", sa.sun_path ); Samotné svázání s adresou nicméně nevytvoří server – k tomu musíme socket uvést do režimu, kdy bude možné se k němu na této adrese připojit. To provedeme voláním ‹listen›, které má dva parametry: popisovač socketu a tzv. backlog, totiž maximální počet klientů, které operační systém zařadí do fronty v situaci, kdy náš program není schopen ihned reagovat na požadavek ke spojení. if ( listen( sock_fd, 5 ) ) /* C */ err( 1, "listen on %s", sa.sun_path ); Komunikaci s klientem konečně navážeme voláním ‹accept›, které se podobá na ‹read› – program bude v tomto volání čekat (blokovat) dokud se nepřipojí nějaký klient. Po navázání spojení na úrovni operačního systému volání ‹accept› vrátí «nový» popisovač, který reprezentuje toto navázané spojení. Původní popisovač (uložený v ‹sock_fd›) nadále slouží k navazování spojení, a můžeme na něm kdykoliv opět zavolat ‹accept›, čím navážeme nové spojení s novým klientem. Nový popisovač (výsledek ‹accept›) pak používáme ke komunikaci s jedním konkrétním připojeným klientem. Druhý a třetí parametr volání ‹accept› umožňují programu získat adresu protistrany (klienta). Pro sockety v doméně ‹AF_UNIX› je tato adresa typicky prázdná a tedy nezajímavá, proto předáme volání ‹accept› dva nulové ukazatele.³ int client_fd, bytes; /* C */ char buffer[ 64 ]; while ( ( client_fd = accept( sock_fd, NULL, NULL ) ) >= 0 ) /* C */ { Popisovač, který jsme od ‹accept› obdrželi už lze používat podobně jako „obyčejný“ soubor. Je zde ovšem jeden důležitý rozdíl – čtení může být „krátké“ i v situaci, kdy nejsme u konce komunikace. Jak velké budou úseky dat, které voláním ‹read› získáme, rozhoduje mimo jiné to, jak klient data odesílá. Proto musíme být připraveni, že ‹read› přečte třeba polovinu řádku, nebo podobně nekompletní data. Protože tento server data pouze přeposílá na jiný popisovač, vystačíme si zde s jednoduchým cyklem, který zpracuje vždy tolik dat, kolik je k dispozici.⁴ while ( ( bytes = read( client_fd, buffer, 64 ) ) > 0 ) /* C */ dprintf( STDOUT_FILENO, "%.*s", bytes, buffer ); if ( bytes == -1 ) /* C */ warn( "reading from client" ); dprintf( STDOUT_FILENO, "\n" ); /* C */ Jakmile druhá strana spojení ukončí, popisovač uzavřeme a jsme připraveni přijmout další spojení od dalšího klienta.⁵ if ( close( client_fd ) == -1 ) /* C */ warn( "closing connection" ); } err( 1, "accept" ); /* C */ } ¹ Jak uvidíme v následující ukázce, při datagramové komunikaci tato asymetrie zmizí – nespojovaná komunikace vyžaduje, aby měly reprezentovatelnou adresu obě strany. ² Je otázkou, má-li server adresu uvolnit, nebo selhat. Protože server, který v této situaci selže, je přinejlepším nepohodlný (a přinejhorším frustrující), budeme se držet přístupu, kdy server případný existující socket nejprve odstraní. Může být smysluplné před odstraněním souboru ověřit, že se jedná o socket, nicméně prozatím k tomu nemáme prostředky. ³ V tomto předmětu nebudeme mít prostor programovat internetový server, ale to je situace, kdy bychom adresu protistrany mohli potřebovat, a kdy bude obsahovat smysluplné informace. ⁴ Zkuste si předchozí ukázku upravit tak, aby odeslala nejprve část dat, pak vyčkala, např. voláním ‹sleep›, a pak odeslala zbytek, a sledujte, jaký to bude mít dopad na chování serveru. ⁵ Další věc, kterou je dobré si zkusit (zejména máte-li už upraveného klienta, který volá ‹sleep›) je pokusit se připojit několika klienty najednou a opět sledovat chování. Přidejte si ladící výpisy, nebo zkuste použít program ‹strace›. ## p. Přípravy ### 1. [‹fork›] ### 2. [‹multi›] ### 3. [‹circle›] ### 4. [‹echod›] ### 5. [‹pipe›] ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 6. [‹buffered›] † Při obousměrné pomocí omezených komunikačních front může lehce dojít k uváznutí – obě strany čekají, až se uvolní místo ve frontě, a protože ani jeden z nich data z fronty nevybírá, potřebné místo se neuvolní nikdy. Roury i sockety se v systémech POSIX chovají jako omezené fronty – existuje nějaké maximální množství dat, které je operační systém ochoten uložit do vyrovnávací paměti, než odesílající program (ten, který do roury zapisuje voláním ‹write›) zablokuje. V tomto příkladu bude Vaším úkolem naprogramovat mechanismus, který tento problém řeší na straně programu. Uvažme situaci, kdy chceme externímu programu předat data ke zpracování, a zpracovaná data opět vyzvednout. Představte si například, že chcete ve svém programu použít nástroj ‹grep› – vstupní data mu pošlete rourou, a vyfiltrovaný výstup si vyzvednete z roury opačné. Nejjednodušší řešení, které Vás napadne, je pravděpodobně otevřít obě roury, program spustit, všechna vstupní data zapsat do té první a pak výsledek přečíst z té druhé. Toto řešení bude fungovat je-li dat na výstupu málo. Jakmile jich bude hodně, Váš program uvázne (rozmyslete si proč). Abychom podobné situaci předešli, vytvoříme si následující sadu podprogramů (de facto drobnou knihovnu): • ‹comm_init› – podprogramu předáme dvojici otevřených popisovačů a obdržíme ukazatel ‹handle›, který budeme dále používat pro komunikaci, • ‹comm_read›, ‹comm_write› mají stejné parametry jako systémová volání ‹read› a ‹write›, ale místo popisovače souboru obdrží ukazatel ‹handle› vytvořený podprogramem ‹comm_init›, • ‹comm_fini› uvolní veškeré zdroje spojené s předaným ukazatelem ‹handle› (včetně asociovaných popisovačů). Podprogram ‹comm_write› nesmí za žádných okolností blokovat – není-li možné data předat systému, musí je interně uložit skrze ukazatel ‹handle› a řízení vrátit volajícímu, jako by zápis uspěl. Podprogram ‹comm_read› blokovat smí, ale pouze v situaci, kdy jsou již všechna data z předešlých volání ‹comm_write› úspěšně odeslána. Podprogram ‹comm_init› vrátí v případě selhání nulový ukazatel, všechny ostatní vrátí v případě úspěchu 0 a v případě neúspěchu -1, a nastaví ‹errno›. void *comm_init( int read_fd, int write_fd ); /* C */ int comm_read( void *handle, char *buffer, int nbytes ); int comm_write( void *handle, const char *data, int nbytes ); int comm_fini( void *handle ); ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### 6. [‹meter›] # Vlákna • ‹pthread_create› • aktivní čekání ## p. Přípravy ### 1. [‹xxx›] ### 2. [‹xxx›] ### 3. [‹xxx›] ### 4. [‹xxx›] ### 5. [‹xxx›] ### 6. [‹xxx›] # Synchronizace • ‹pthread_mutex› • ‹pthread_rwlock› • ‹pthread_cond› ## p. Přípravy ### 1. [‹xxx›] ### 2. [‹xxx›] ### 3. [‹xxx›] ### 4. [‹xxx›] ### 5. [‹xxx›] ### 6. [‹xxx›] # Opakování ## p. Přípravy ### 1. [‹xxx›] ### 2. [‹xxx›] ### 3. [‹xxx›] ### 4. [‹xxx›] ### 5. [‹xxx›] ### 6. [‹xxx›] # S.3. Souběžnost ## a. ‹broadcast› Naprogramujte proudově orientovaný server, který bude přijímat data od všech klientů, a každou přijatou zprávu přepošle všem ostatním. Každá zpráva sestává z jednoho libovolně dlouhého řádku ukončeného znakem ‹\n›. Klienti se mohou průběžně připojovat a odpojovat. Server musí reagovat i v situaci, kdy někteří klienti delší dobu data nepřijímají. Čtou-li ovšem data všichni klienti, server musí být schopen libovolně dlouhého provozu v konečně velké paměti. ## b. ‹tftpd› Naprogramujte souběžný (víceprocesový nebo vícevláknový) tftpd server s podporou jak ukládání tak stahování souborů. Soubory budou uloženy ve vyhrazené složce (bez podpory podsložek). ## c. ‹supervise› ## d. ‹httpd› ## e. ‹newsd› ## f. ‹dns› # K. Vzorová řešení ## 1. Týden 1 ### r.1. [‹wcount›] int count_words( int dir_fd, const char *file, int *count ) /* C */ { const int nbytes = 1024; char buffer[ nbytes ]; int fd, bytes_read; int result = -1; bool in_word = false; *count = 0; /* C */ if ( ( fd = openat( dir_fd, file, O_RDONLY ) ) == -1 ) /* C */ goto out; do { /* C */ if ( ( bytes_read = read( fd, buffer, nbytes ) ) == -1 ) /* C */ goto out; for ( int i = 0; i < bytes_read; ++i ) /* C */ if ( in_word && isspace( buffer[ i ] ) ) { ++ *count; in_word = false; } else if ( !in_word && !isspace( buffer[ i ] ) ) in_word = true; } while ( bytes_read > 0 ); /* C */ if ( in_word ) /* C */ ++ *count; result = 0; /* C */ out: /* C */ if ( fd != -1 && close( fd ) == -1 ) warn( "closing %s", file ); return result; } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.2. [‹cgrep›] int copy_span( int fd_in, int fd_out, int from, int to ) /* C */ { const int nbytes = 1024; int bytes_total = 0, to_write, bytes_read; int orig_pos; char buffer[ nbytes ]; if ( ( orig_pos = lseek( fd_in, 0, SEEK_CUR ) ) == -1 ) /* C */ return -1; if ( lseek( fd_in, from, SEEK_SET ) == -1 ) /* C */ return -1; do { /* C */ if ( ( bytes_read = read( fd_in, buffer, nbytes ) ) == -1 ) return -1; bytes_total += bytes_read; /* C */ to_write = bytes_read; int extra = bytes_total + from - to; if ( extra > 0 ) /* C */ to_write -= extra; if ( write( fd_out, buffer, to_write ) == -1 ) /* C */ return -1; } while ( bytes_read > 0 && bytes_total < to - from ); if ( lseek( fd_in, orig_pos, SEEK_SET ) == -1 ) /* C */ return -1; return 0; /* C */ } int cgrep( int fd_in, char c, int fd_out ) /* C */ { const int nbytes = 3; char buffer[ nbytes ]; int bytes_read = 0, offset = 0, line_start = 0; bool matched = false; do { /* C */ if ( ( bytes_read = read( fd_in, buffer, nbytes ) ) == -1 ) return -1; for ( int i = 0; i < bytes_read; ++i, ++offset ) /* C */ { if ( buffer[ i ] == c ) matched = true; if ( buffer[ i ] == '\n' ) /* C */ { if ( matched && copy_span( fd_in, fd_out, line_start, offset + 1 ) == -1 ) return -1; line_start = offset + 1; /* C */ matched = false; } } } while ( bytes_read > 0 ); return 0; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.4. [‹bcount›] int count_distinct( int dir_fd, const char *file ) /* C */ { const int nbytes = 3; unsigned char buffer[ nbytes ]; int counts[ 256 ] = { 0 }; int fd = -1; int bytes_read; int result = -1, saved_errno; if ( ( fd = openat( dir_fd, file, O_RDONLY ) ) == -1 ) /* C */ goto out; do { /* C */ if ( ( bytes_read = read( fd, buffer, nbytes ) ) == -1 ) goto out; for ( int i = 0; i < bytes_read; ++i ) /* C */ counts[ buffer[ i ] ] ++; } while ( bytes_read > 0 ); result = 0; /* C */ for ( int i = 0; i < 256; ++i ) /* C */ if ( counts[ i ] ) ++ result; out: /* C */ saved_errno = errno; if ( fd != -1 && close( fd ) == -1 ) /* C */ warn( "closing fd %d for %s", fd, file ); errno = saved_errno; /* C */ return result; } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.6. [‹otp›] static void close_or_warn( int fd, const char *name ) /* C */ { if ( fd != -1 && close( fd ) == -1 ) warn( "closing %s", name ); } void xor( char *a, const char *key, int size ) /* C */ { for ( int i = 0; i < size; ++i ) a[ i ] = a[ i ] ^ key[ i ]; } int otp_fd( int fd_file, int fd_key, int out ) /* C */ { char buf[ 4 ]; char key[ sizeof buf ]; int f_bytes; while ( ( f_bytes = read( fd_file, buf, sizeof buf ) ) > 0 ) /* C */ { int k_bytes = read( fd_key, key, f_bytes ); if ( k_bytes == -1 ) return -2; if ( k_bytes != f_bytes ) return -1; xor( buf, key, f_bytes ); /* C */ if ( write( out, buf, f_bytes ) == -1 ) return -2; } if ( f_bytes == -1 ) return -2; return 0; /* C */ } int otp( int dir, const char* file, const char* key_file, int out ) /* C */ { int ok = -2; int e; int fd_file = -1; int fd_key = -1; if ( ( fd_file= openat( dir, file, O_RDONLY ) ) == -1 ) /* C */ goto error; if ( ( fd_key = openat( dir, key_file, O_RDONLY ) ) == -1 ) /* C */ goto error; ok = otp_fd( fd_file, fd_key, out ); /* C */ error: /* C */ e = errno; close_or_warn( fd_file, file ); close_or_warn( fd_key, key_file ); errno = e; return ok; } ## 2. Týden 2 ### r.1. [‹agree›] send, connect, AF_UNSPEC */ /* C */ int agree( int sock_fd, const char our_set[ 64 ], char intersection[ 64 ] ) { int rv = -1; int saved_errno = errno; int bytes; uint8_t confirmation[ 2 ]; const int msg_len = 64; /* C */ uint8_t buf[ msg_len + 1 ]; struct sockaddr_un sun; /* C */ struct sockaddr *sa = ( struct sockaddr * ) &sun; socklen_t sa_size = sizeof sun; if ( ( bytes = recvfrom( sock_fd, buf, msg_len + 1, /* C */ 0, sa, &sa_size ) ) == -1 ) { saved_errno = errno; warn( "server recvfrom" ); goto cleanup; } if ( bytes != msg_len ) /* C */ return -2; for ( int i = 0; i < msg_len; ++i ) /* C */ buf[ i ] &= our_set[ i ]; if ( ( bytes = sendto( sock_fd, buf, msg_len, /* C */ 0, sa, sa_size ) ) == -1 ) { saved_errno = errno; warn( "server send" ); goto cleanup; } assert( bytes == msg_len ); /* C */ if ( ( bytes = recv( sock_fd, confirmation, 2, 0 ) ) == -1 ) /* C */ { saved_errno = errno; warn( "server recv" ); goto cleanup; } if ( bytes != 1 ) /* C */ return -2; if ( confirmation[ 0 ] == 1 ) /* C */ { memcpy( intersection, buf, msg_len ); rv = 0; } else if ( confirmation[ 0 ] == 0 ) rv = -3; else rv = -2; cleanup: /* C */ errno = saved_errno; return rv; } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.3. [‹filter›] void filter( int fd_in, int fd_out, int offset, uint32_t thresh ) /* C */ { uint8_t buf[ 4096 ]; int bytes_in; while ( 1 ) /* C */ { if ( ( bytes_in = recv( fd_in, buf, 4096, 0 ) ) == -1 ) err( 1, "recv error" ); uint32_t value = * ( uint32_t * ) ( buf + offset ); /* C */ if ( ntohl( value ) > thresh ) /* C */ if ( send( fd_out, buf, bytes_in, 0 ) == -1 ) warn( "send" ); } } ## 3. Týden 3 ### r.1. [‹clone›] Pošle ‹data› velikosti ‹size› na ‹*fd› a nastaví ho na -1, jakmile protistrana ukončí spojení. int transmit( int *fd, const char *data, int size ) /* C */ { if ( *fd == -1 ) return 0; int bytes_written = write( *fd, data, size ); /* C */ Abychom dostali EPIPE, musí být ignorován signál SIGPIPE, který by jinak při pokusu o zápis do proudového soketu, jehož druhý konec už je uzavřen pro čtení, program ukončil. Toto není úlohou podprogramu ‹clone_stream› a nastavit by to měl někdo dřív. V našem případě se o ignorování SIGPIPE starají testy voláním služby jádra ‹signal›. if ( bytes_written == -1 && errno == EPIPE ) /* C */ { *fd = -1; return 0; } return bytes_written; /* C */ } int clone_stream( int in_fd, int out1_fd, int out2_fd ) /* C */ { char buf[ 512 ]; int bytes_read; while ( ( bytes_read = read( in_fd, buf, sizeof buf ) ) > 0 ) /* C */ { if ( transmit( &out1_fd, buf, bytes_read ) == -1 || transmit( &out2_fd, buf, bytes_read ) == -1 ) return -1; Obě protistrany ukončily spojení. if ( out1_fd == -1 && out2_fd == -1 ) /* C */ return 0; } return bytes_read; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.2. [‹bridge›] int other( int ix ) { return ( ix + 1 ) % 2; } /* C */ int bridge( int fd_1, int fd_2 ) /* C */ { struct pollfd fds[ 2 ]; for ( int i = 0; i < 2; ++i ) fds[ i ].events = POLLIN; fds[ 0 ].fd = fd_1; /* C */ fds[ 1 ].fd = fd_2; char buf[ 1024 ]; /* C */ while ( 1 ) /* C */ { int ready = poll( fds, 2, -1 ); if ( ready == -1 ) return -1; for ( int i = 0; i < 2; ++i ) /* C */ { if ( fds[ i ].revents & POLLIN ) { int bytes_read = read( fds[ i ].fd, buf, sizeof buf ); if ( bytes_read == -1 ) return -1; if ( bytes_read == 0 ) return 0; if ( write( fds[ other( i ) ].fd, buf, bytes_read ) == -1 ) /* C */ return -1; } else if ( fds[ i ].revents & POLLERR ) return -1; } } return 0; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.3. [‹slip›] int decode_slip( int in_fd, int out_fd ) /* C */ { char buf[ 513 ]; int buffered = 0; /* C */ int bytes_read; while ( ( bytes_read = read( in_fd, buf + buffered, /* C */ sizeof buf - buffered ) ) > 0 ) { buffered += bytes_read; char *end; /* C */ while (( end = memchr( buf, 0xc0, buffered ) )) { int pktlen = end - buf; if ( send( out_fd, buf, pktlen, 0 ) == -1 ) return -1; buffered -= pktlen + 1; /* C */ memmove( buf, buf + pktlen + 1, buffered ); /* C */ } } if ( bytes_read == -1 ) /* C */ return -1; if ( buffered > 0 && send( out_fd, buf, buffered, 0 ) == -1 ) /* C */ return -1; return 0; /* C */ } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.4. [‹seq›] ssize_t seq_one( int in_fd, int out_fd ) /* C */ { char buf[ UINT16_MAX + 2 ]; ssize_t bytes = recv( in_fd, buf + 2, sizeof buf - 2, 0 ); if ( bytes == -1 ) /* C */ return -2; uint8_t sz[ 2 ]; /* C */ sz[ 0 ] = ( bytes & 0xff00 ) >> 8; sz[ 1 ] = bytes & 0x00ff; memcpy( buf, sz, 2 ); /* C */ return write( out_fd, buf, bytes + 2 ); /* C */ } int packet_seq( int in_fd, int out_fd ) /* C */ { while ( 1 ) if ( seq_one( in_fd, out_fd ) < 0 ) return errno == EPIPE ? 0 : -1; } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.5. [‹log›] // Zkopíruje posledních ‹tail_size› bajtů souboru na jeho začátek. /* C */ // Velikost souboru a obsah od ‹tail_size›-tého bajtu dále zůstanou beze změny. int fmovetail( int fd, int tail_size ) { int buf[ 512 ]; int done = 0; while ( tail_size > 0 ) /* C */ { int bytes_read; if ( lseek( fd, -tail_size, SEEK_END ) == -1 || ( bytes_read = read( fd, buf, sizeof buf ) ) == -1 || lseek( fd, done, SEEK_SET ) == -1 || write( fd, buf, bytes_read ) == -1 ) return -1; done += bytes_read; /* C */ tail_size -= bytes_read; } return 0; /* C */ } int log_stream( int in_fd, int out_fd, int log_fd, int limit ) /* C */ { int half_limit = ( limit + 1 ) / 2; assert( half_limit > 0 ); char buf[ 1024 ]; /* C */ int bytes_dropped = 0; // Soubor už může obsahovat nějaká data. /* C */ int bytes_logged = lseek( log_fd, 0, SEEK_END ); if ( bytes_logged == -1 ) return -1; int bytes_read; /* C */ while ( ( bytes_read = read( in_fd, buf, sizeof buf ) ) > 0 ) { // Přeposlat přijatá data if ( write( out_fd, buf, bytes_read ) == -1 ) return -1; // Spočítat novou velikost logu a o kolik bajtů přijdeme. /* C */ int log_length = bytes_logged + bytes_read; int bytes_to_discard = 0; while ( log_length > limit ) /* C */ { bytes_to_discard += half_limit; log_length -= half_limit; } bytes_dropped += bytes_to_discard; /* C */ // Zahodit bajty z logovacího souboru /* C */ if ( bytes_to_discard > 0 ) { // Kolik původních bajtů má v logovacím souboru zůstat? int bytes_to_keep = bytes_logged - bytes_to_discard; if ( bytes_to_keep < 0 ) bytes_to_keep = 0; // Přesunout ušetřené bajty na začátek /* C */ if ( bytes_to_keep > 0 && fmovetail( log_fd, bytes_to_keep ) == -1 ) return -1; // Zkrátit soubor /* C */ if ( ftruncate( log_fd, bytes_to_keep ) == -1 ) return -1; bytes_to_discard -= ( bytes_logged - bytes_to_keep ); /* C */ // Posunout ukazovátko na konec logu (po fmovetail může být kdekoli) /* C */ int bytes_kept = lseek( log_fd, 0, SEEK_END ); if ( bytes_kept == -1 ) return -1; assert( bytes_kept == bytes_to_keep ); /* C */ } assert( bytes_to_discard < bytes_read ); /* C */ assert( bytes_to_discard >= 0 ); // Zapsat novou zprávu (nebo její konec, pokud máme bytes_to_discard) /* C */ if ( write( log_fd, buf + bytes_to_discard, bytes_read - bytes_to_discard ) == -1 ) return -1; // Sanity check: v logu je tolik bajtů, kolik jsme spočítali, že má být /* C */ bytes_logged = lseek( log_fd, 0, SEEK_END ); if ( bytes_logged == -1 ) return -1; assert( bytes_logged == log_length ); } if ( bytes_read == -1 ) /* C */ return -1; return bytes_dropped; /* C */ } ## 7. Týden 7 ### r.1. [‹bcast›] struct address_list /* C */ { struct sockaddr_un address; struct address_list *next; }; void bcast_once( int sock_fd, struct address_list *addresses ) /* C */ { char buf[ 4096 ]; struct sockaddr_un src_addr = { 0 }; /* C */ const socklen_t addr_un_size = sizeof( struct sockaddr_un ); socklen_t addrlen = addr_un_size; int bytes = recvfrom( sock_fd, buf, sizeof buf, 0, /* C */ ( struct sockaddr* )&src_addr, &addrlen ); if ( bytes == -1 ) warn( "recvfrom" ); for ( struct address_list *ptr = addresses; ptr; ptr = ptr->next ) /* C */ if ( strcmp( src_addr.sun_path, ptr->address.sun_path ) != 0 ) { if ( sendto( sock_fd, buf, bytes, 0, ( struct sockaddr * )&ptr->address, addr_un_size ) == -1 ) warn( "sendto %s", ptr->address.sun_path ); } } void bcast( int sock_fd, struct address_list *addresses ) /* C */ { while ( 1 ) bcast_once( sock_fd, addresses ); } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.2. [‹proxy›] void proxy( int sock_fd ) /* C */ { const int nbytes = 4096; char buffer[ nbytes ]; struct sockaddr_un addr; while ( 1 ) /* C */ { int nread = recv( sock_fd, buffer, nbytes, 0 ); if ( nread == -1 ) /* C */ err( 1, "recvfrom" ); if ( nread < ( int ) sizeof addr ) /* C */ warnx( "incorrect datagram of size %d", nread ); memcpy( &addr, buffer, sizeof addr ); /* C */ ssize_t nwrote = sendto( sock_fd, buffer + sizeof addr, /* C */ nread - sizeof( addr ), 0, ( struct sockaddr * ) &addr, sizeof addr ); if ( nwrote == -1 ) /* C */ warn( "forwarding datagram" ); } } ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ### r.3. [‹router›] struct address_map /* C */ { struct sockaddr_un source, destination; struct address_map *left, *right; }; const struct sockaddr_un * /* C */ find_destination( const struct sockaddr_un* source, const struct address_map *root ) { if ( root == NULL ) return NULL; int c = strcmp( source->sun_path, root->source.sun_path ); /* C */ if ( c == 0 ) return &root->destination; return find_destination( source, /* C */ c < 0 ? root->left : root->right ); } void router( int sock_fd, struct address_map *root ) /* C */ { const size_t buffer_size = 512; char buffer[ buffer_size ]; memset( buffer, 0, buffer_size ); struct sockaddr_un from_addr; /* C */ socklen_t from_addr_len; while ( 1 ) /* C */ { from_addr_len = sizeof( struct sockaddr_un ); ssize_t nread = recvfrom( sock_fd, buffer, buffer_size, 0, /* C */ (struct sockaddr *) &from_addr, &from_addr_len ); if ( nread == -1 ) /* C */ err( 1, "recvfrom" ); const struct sockaddr_un *to_addr = /* C */ find_destination( &from_addr, root ); if ( to_addr == NULL ) /* C */ { warnx( "no route to dest" ); continue; } ssize_t nwrote = sendto( sock_fd, buffer, nread, 0, /* C */ (struct sockaddr*) to_addr, sizeof( struct sockaddr_un ) ); if ( nwrote == -1 ) /* C */ warn( "sendto" ); } } # T. Technické informace Tato kapitola obsahuje informace o technické realizaci předmětu, a to zejména: • jak se pracuje s kostrami úloh, • jak sdílet obrazovku (terminál) ve cvičení, • jak se odevzdávají úkoly, • kde najdete výsledky testů a jak je přečtete, • kde najdete hodnocení kvality kódu (učitelské recenze), • jak získáte kód pro vzájemné recenze. ## Informační systém Informační systém tvoří primární „rozhraní“ pro stahování studijních materiálů, odevzdávání řešení, získání výsledků vyhodnocení a čtení recenzí. Zároveň slouží jako hlavní komunikační kanál mezi studenty a učiteli, prostřednictvím diskusního fóra. ### Diskusní fórum Máte-li dotazy k úlohám, organizaci, atp., využijte k jejich položení prosím vždy přednostně diskusní fórum.¹ Ke každé kapitole a ke každému příkladu ze sady vytvoříme samostatné vlákno, kam patří dotazy specifické pro tuto kapitolu nebo tento příklad. Pro řešení obecných organizačních záležitostí a technických problémů jsou podobně v diskusním fóru nachystaná vlákna. Než položíte libovolný dotaz, přečtěte si relevantní část dosavadní diskuse – je možné, že na stejný problém už někdo narazil. Máte-li ve fóru dotaz, na který se Vám nedostalo do druhého pracovního dne reakce, připomeňte se prosím tím, že na tento svůj příspěvek odpovíte. Máte-li dotaz k výsledku testu, nikdy tento výsledek nevkládejte do příspěvku (podobně nikdy nevkládejte části řešení příkladu). Učitelé mají přístup k obsahu Vašich poznámkových bloků, i k Vámi odevzdaným souborům. Je-li to pro pochopení kontextu ostatními čtenáři potřeba, odpovídající učitel chybějící informace doplní dle uvážení. ¹ Nebojte se do fóra napsat – když si s něčím nevíte rady a/nebo nemůžete najít v materiálech, rádi Vám pomůžeme nebo Vás nasměrujeme na místo, kde odpověď naleznete. ### Stažení koster Kostry naleznete ve «studijních materiálech» v ISu: ‹Student› → ‹PB152› → ‹Studijní materály› → ‹Učební materiály›. Každá kapitola má vlastní složku, pojmenovanou ‹00› (tento úvod a materiály k nultému cvičení), ‹01› (první běžná kapitola), ‹02›, …, ‹12›. Veškeré soubory stáhnete jednoduše tak, že na složku kliknete pravým tlačítkem a vyberete možnost ‹Stáhnout jako ZIP›. Stažený soubor rozbalte a můžete řešit. ### Odevzdání řešení Vypracované příklady můžete odevzdat do «odevzdávárny» v ISu: ‹Student› → ‹PB152› → ‹Odevzdávárny›. Pro přípravy používejte odpovídající složky s názvy ‹01›, …, ‹12›. Pro příklady ze sad pak ‹s1_a_csv›, atp. (složky začínající ‹s1› pro první, ‹s2› pro druhou a ‹s3› pro třetí sadu). Soubor vložíte výběrem možnosti ‹Soubor – nahrát› (první ikonka na liště nad seznamem souborů). Tímto způsobem můžete najednou nahrát souborů několik (například všechny přípravy z dané kapitoly). Vždy se ujistěte, že vkládáte správnou verzi souboru (a že nemáte v textovém editoru neuložené změny). «Pozor!» Všechny vložené soubory se musí jmenovat stejně jako v kostrách, jinak nebudou rozeznány (IS při vkládání automaticky předřadí Vaše UČO – to je v pořádku, název souboru po vložení do ISu «neměňte») . O každém odevzdaném souboru (i nerozeznaném) se Vám v poznámkovém bloku ‹log› objeví záznam. Tento záznam i výsledky testu syntaxe by se měl objevit do několika minut od odevzdání (nemáte-li ani po 15 minutách výsledky, napište prosím do diskusního fóra). Archiv všech souborů, které jste úspěšně odevzdali, naleznete ve složce ‹Private› ve studijních materiálech (‹Student› → ‹PB152› → ‹Studijní materiály› → ‹Private›). ### Výsledky automatických testů Automatickou zpětnou vazbu k odevzdaným úlohám budete dostávat prostřednictvím tzv. «poznámkových bloků» v ISu. Ke každé odevzdávárně existuje odpovídající poznámkový blok, ve kterém naleznete aktuální výsledky testů. Pro přípravy bude blok vypadat přibližně takto: testing verity of submission from 2022-09-17 22:43 CEST subtest p1_foo passed [0.5] subtest p2_bar failed subtest p3_baz failed subtest p4_quux passed [0.5] subtest p5_wibble passed [0.5] subtest p6_xyzzy failed {bližší popis chyby} verity test failed testing syntax of submission from 2022-09-17 22:43 CEST subtest p1_foo passed subtest p2_bar failed {bližší popis chyby} subtest p3_baz failed {bližší popis chyby} subtest p4_quux passed subtest p5_wibble passed subtest p6_xyzzy passed syntax test failed testing sanity of submission from 2022-09-17 22:43 CEST subtest p1_foo passed [ 1] subtest p2_bar failed subtest p3_baz failed subtest p4_quux passed [ 1] subtest p5_wibble passed [ 1] subtest p6_xyzzy passed [ 1] sanity test failed best submission: 2022-09-17 22:43 CEST worth *5.5 point(s) Jednak si všimněte, že každý odstavec má «vlastní časové razítko», které určuje, ke kterému odevzdání daný výstup patří. Tato časová razítka nemusí být stejná. V hranatých závorkách jsou uvedeny dílčí body, za hvězdičkou na posledním řádku pak celkový bodový zisk za tuto kapitolu. Také si všimněte, že ‹best submission› se vztahuje na jedno konkrétní odevzdání jako celek: v situaci, kdy odstavec „verity“ a odstavec „sanity“ nemají stejné časové razítko, «nemusí» být celkový bodový zisk součtem všech dílčích bodů. O konečném zisku rozhoduje vždy poslední odevzdání před příslušným termínem (opět jako jeden celek).¹ Výstup pro příklady ze sad je podobný, uvažme například: testing verity of submission from 2022-10-11 21:14 CEST subtest foo-small passed subtest foo-large passed verity test passed [ 10] testing syntax of submission from 2022-10-14 23:54 CEST subtest build passed syntax test passed testing sanity of submission from 2022-10-14 23:54 CEST subtest foo passed sanity test passed best submission: 2022-10-11 21:14 CEST worth *10 point(s) Opět si všimněte, že časová razítka se mohou lišit (a v případě příkladů ze sady bude k této situaci docházet poměrně často, vždy tedy nejprve ověřte, ke kterému odevzdání se který odstavec vztahuje a pak až jej dále interpretujte). ¹ Můžete si tak odevzdáním nefunkčních řešení na poslední chvíli snížit výsledný bodový zisk. Uvažte situaci, kdy máte v pátek 4 body za sanity příkladů p1, p2, p3, p6 a 1 bod za verity p1, p2. V sobotu odevzdáte řešení, kde p1 neprochází sanity testem, ale p4 ano a navíc projdou verity testy příklady p4 a p6. Váš výsledný zisk bude 5.5 bodu. Tento mechanismus Vám nikdy nesníží výsledný bodový zisk pod již jednou dosaženou hranici „best submission“. ### Recenze Vám adresované recenze, podobně jako archiv odevzdaných souborů, naleznete ve složce ‹Private› ve studijních materiálech (‹Student› → ‹PB152› → ‹Studijní materiály› → ‹Private›). Shrnutí bodového zisku za tyto recenze pak naleznete v poznámkovém bloku ‹reviews›. ### Další poznámkové bloky Blok ‹corr› obsahuje záznamy o manuálních bodových korekcích (např. v situaci, kdy byl Váš bodový zisk ovlivněn chybou v testech). Podobně se zde objeví záznamy o penalizaci za opisování. Blok ‹log› obsahuje záznam o všech odevzdaných souborech, včetně těch, které nebyly rozeznány. Nedostanete-li po odevzdání příkladu výsledek testů, ověřte si v tomto poznámkovém bloku, že soubor byl správně rozeznán. Blok ‹misc› obsahuje záznamy o Vaší aktivitě ve cvičení (netýká se bodů za vzájemné recenze ani vnitrosemestrální testy). Nemáte-li před koncem cvičení, ve kterém jste řešili příklad u tabule, záznam v tomto bloku, připomeňte se svému cvičícímu. Konečně blok ‹sum› obsahuje souhrn bodů, které jste dosud získali, a které ještě získat můžete. Dostanete-li se do situace, kdy Vám ani zisk všech zbývajících bodů nebude stačit pro splnění podmínek předmětu, tento blok Vás o tom bude informovat. Tento blok má navíc přístupnou statistiku bodů – můžete tak srovnat svůj dosavadní bodový zisk se svými spolužáky. Je-li blok ‹sum› v rozporu s pravidly uvedenými v tomto dokumentu, přednost mají pravidla zde uvedená. Podobně mají v případě nesrovnalosti přednost dílčí poznámkové bloky. Dojde-li k takovéto neshodě, informujte nás o tom prosím v diskusním fóru. Případná známka uvedená v poznámkovém bloku ‹sum› je podobně pouze informativní – rozhoduje vždy známka zapsaná v hodnocení předmětu. ## Studentský server ‹aisa› Použití serveru ‹aisa› pro odevzdávání příkladů je zcela volitelné a vše potřebné můžete vždy udělat i prostřednictvím ISu. Nevíte-li si s něčím z níže uvedeného rady, použijte IS. Na server ‹aisa› se přihlásíte programem ‹ssh›, který je k dispozici v prakticky každém moderním operačním systému (v OS Windows skrze WSL¹ – Windows Subsystem for Linux). Konkrétní příkaz (za ‹xlogin› doplňte ten svůj): $ ssh xlogin@aisa.fi.muni.cz Program se zeptá na heslo: použijte to fakultní (to stejné, které používáte k přihlášení na ostatní fakultní počítače, nebo např. ve ‹fadmin›-u nebo fakultním ‹gitlab›-u). ¹ Jako alternativu, nechcete-li z nějakého důvodu WSL instalovat, lze použít program ‹putty›. ### Pracovní stanice Veškeré instrukce, které zde uvádíme pro použití na stroji ‹aisa› platí beze změn také na libovolné školní UNIX-ové pracovní stanici (tzn. z fakultních počítačů není potřeba se hlásit na stroj ‹aisa›, navíc mají sdílený domovský adresář, takže svoje soubory z tohoto serveru přímo vidíte, jako by byly uloženy na pracovní stanici). ### Stažení koster Aktuální zdrojový balík stáhnete příkazem: $ pb152 update Stažené soubory pak naleznete ve složce ‹~/pb152›. Je bezpečné tento příkaz použít i v případě, že ve své kopii již máte rozpracovaná řešení – systém je při aktualizaci nepřepisuje. Došlo-li ke změně kostry u příkladu, který máte lokálně modifikovaný, aktualizovanou kostru naleznete v souboru s dodatečnou příponou ‹.pristine›, např. ‹01/e2_concat.cpp.pristine›. V takovém případě si můžete obě verze srovnat příkazem ‹diff›: $ diff -u e2_concat.cpp e2_concat.cpp.pristine Případné relevantní změny si pak již lehce přenesete do svého řešení. Krom samotného zdrojového balíku Vám příkaz ‹pb152 update› stáhne i veškeré recenze (jak od učitelů, tak od spolužáků). To, že máte k dispozici nové recenze, uvidíte ve výpisu. Recenze najdete ve složce ‹~/pb152/reviews›. ### Odevzdání řešení Odevzdat vypracované (nebo i rozpracované) řešení můžete ze složky s relevantními soubory takto: $ cd ~/pb152/01 $ pb152 submit Přidáte-li přepínač ‹--wait›, příkaz vyčká na vyhodnocení testů fáze „syntax“ a jakmile je výsledek k dispozici, vypíše obsah příslušného poznámkového bloku. Chcete-li si ověřit co a kdy jste odevzdali, můžete použít příkaz $ pb152 status nebo se podívat do informačního systému (blíže popsáno v sekci T.1). «Pozor!» Odevzdáváte-li stejnou sadu příprav jak v ISu tak prostřednictvím příkazu ‹pb152›, ujistěte se, že odevzdáváte vždy všechny příklady. ### Sdílení terminálu Řešíte-li příklad typu ‹r› ve cvičení, bude se Vám pravděpodobně hodit režim sdílení terminálu s cvičícím (který tak bude moct promítat Váš zdrojový kód na plátno, případně do něj jednoduše zasáhnout). Protože se sdílí pouze terminál, budete se muset spokojit s negrafickým textovým editorem (doporučujeme použít ‹micro›, případně ‹vim› umíte-li ho ovládat). Spojení navážete příkazem: $ pb152 beamer Protože příkaz vytvoří nové sezení, nezapomeňte se přesunout do správné složky příkazem ‹cd ~/pb152/NN›. ### Recenze Příkaz ‹pb152 update› krom zdrojového balíku stahuje také: 1. zdrojové kódy, které máte možnost recenzovat, a to do složky ‹~/pb152/to_review›, 2. recenze, které jste na svůj kód obdrželi (jak od spolužáků, tak od vyučujících), a to do stávajících složek zdrojového balíku (tzn. recenze na příklady z první kapitoly se Vám objeví ve složce ‹~/pb152/01› – že se jedná o recenzi poznáte podle jména souboru, který bude začínat uživatelským jménem autora recenze, např. ‹xrockai.00123.p1_nhamming.cpp›). Chcete-li vypracované recenze odeslat: 1. přesuňte se do složky ‹~/pb152/to_review› a 2. zadejte příkaz ‹pb152 submit›, případně doplněný o seznam souborů, které hodláte odeslat (jinak se odešlou všechny, které obsahují jakýkoliv přidaný komentář). ## Kostry úloh Pracujete-li na studentském serveru ‹aisa›, můžete pro překlad jednotlivých příkladů použít přiložený soubor ‹makefile›, a to zadáním příkazu $ make příklad kde ‹příklad› je název souboru bez přípony (např. tedy ‹make e1_factorial›). Tento příkaz postupně: 1. přeloží Vaše řešení překladačem ‹gcc›, 2. spustí přiložené testy, 3. spustí kontrolu nástrojem ‹valgrind›. Selže-li některý krok, další už se provádět nebude. Povede-li se překlad v prvním kroku, v pracovním adresáři naleznete spustitelný soubor s názvem ‹příklad›, se kterým můžete dále pracovat (např. ho ladit/krokovat nástrojem ‹gdb›). Existující přeložené soubory můžete smazat příkazem ‹make clean› (vynutíte tak jejich opětovný překlad a spuštění všech kontrol). ### Textový editor Na stroji ‹aisa› je k dispozici jednoduchý editor ‹micro›, který má podobné ovládání jako klasické textové editory, které pracují v grafickém režimu, a který má slušnou podporu pro práci se zdrojovým kódem. Doporučujeme zejména méně pokročilým. Další možností jsou samozřejmě pokročilé editory ‹vim› a ‹emacs›. Mimo lokálně dostupné editory si můžete ve svém oblíbeném editoru, který máte nainstalovaný u sebe, nastavit režim vzdálené editace (použitím protokolu ‹ssh›). Minimálně ve VS Code je takový režim k dispozici a je uspokojivě funkční. ### Vlastní prostředí Každý příklad je zcela obsažen v jednom standardním zdrojovém souboru, proto je jejich překlad velmi jednoduchý. Pravděpodobně každé IDE zvládne s příklady bez problémů pracovat (spouštět, ladit, atp.), musí ale běžet na systému typu POSIX (splňují všechny OS krom Windows – zde ale můžete využít WSL a případně jeho «integraci do VS Code¹»). Krom IDE můžete také použít dodaný soubor ‹makefile›, pouze si v nadřazené složce (tzn. «vedle» složek ‹01›, ‹02›, atd.) vytvořte soubor ‹local.mk›, ve kterém nastavíte, jak se na Vašem systému spouští potřebné příkazy. Implicitní nastavení je toto, a funguje-li Vám, není potřeba soubor ‹local.mk› vůbec vytvářet: CC = cc VALGRIND = valgrind Můžete samozřejmě příklady překládat a kontrolovat i ručně. ¹ ‹https://code.visualstudio.com/docs/remote/wsl› # U. Doporučení k zápisu kódu Tato sekce rozvádí obecné principy zápisu kódu s důrazem na čitelnost a korektnost. Samozřejmě žádná sada pravidel nemůže zaručit, že napíšete dobrý (korektní a čitelný) program, o nic více, než může zaručit, že napíšete dobrou povídku nebo namalujete dobrý obraz. Přesto ve všech těchto případech pravidla existují a jejich dodržování má obvykle na výsledek pozitivní dopad. Každé pravidlo má samozřejmě nějaké výjimky. Tyto jsou ale výjimkami proto, že nastávají «výjimečně». Některá pravidla připouští výjimky častěji než jiná: ### 1. Dekompozice Vůbec nejdůležitější úlohou programátora je rozdělit problém tak, aby byl schopen každou část správně vyřešit a dílčí výsledky pak poskládat do korektního celku. A. Kód musí být rozdělen do ucelených jednotek (kde jednotkou rozumíme funkci, typ, modul, atd.) přiměřené velikosti, které lze studovat a používat nezávisle na sobě. B. Jednotky musí být od sebe odděleny jasným «rozhraním», které by mělo být jednodušší a uchopitelnější, než kdybychom použití jednotky nahradili její definicí. C. Každá jednotka by měla mít «jeden» dobře definovaný účel, který je zachycený především v jejím pojmenování a případně rozvedený v komentáři. D. Máte-li problém jednotku dobře pojmenovat, může to být známka toho, že dělá příliš mnoho věcí. E. Jednotka by měla realizovat vhodnou «abstrakci», tzn. měla by být «obecná» – zkuste si představit, že dostanete k řešení nějaký jiný (ale dostatečně příbuzný) problém: bude Vám tato konkrétní jednotka k něčemu dobrá, aniž byste ji museli (výrazně) upravovat? F. Má-li jednotka parametr, který fakticky identifikuje místo ve kterém ji používáte (bez ohledu na to, je-li to z jeho názvu patrné), je to často známka špatně zvolené abstrakce. Máte-li parametr, který by bylo lze pojmenovat ‹called_from_bar›, je to jasná známka tohoto problému. G. Daný podproblém by měl být vyřešen v programu pouze jednou – nedaří-li se Vám sjednotit různé varianty stejného nebo velmi podobného kódu (aniž byste se uchýlili k taktice z bodu d), může to být známka nesprávně zvolené dekompozice. Zkuste se zamyslet, není-li možné problém rozložit na podproblémy jinak. ### 2. Jména Dobře zvolená jména velmi ulehčují čtení kódu, ale jsou i dobrým vodítkem při dekompozici a výstavbě abstrakcí. A. Všechny entity ve zdrojovém kódu nesou «anglická» jména. Angličtina je univerzální jazyk programátorů. B. Jméno musí být «výstižné» a «popisné»: v místě použití je obvykle jméno náš hlavní (a často jediný) «zdroj informací» o jmenované entitě. Nutnost hledat deklaraci nebo definici (protože ze jména není jasné, co volaná funkce dělá, nebo jaký má použitá proměnná význam) čtenáře nesmírně zdržuje.¹ C. Jména «lokálního» významu mohou být méně informativní: je mnohem větší šance, že význam jmenované entity si pamatujeme, protože byla definována před chvílí (např. lokální proměnná v krátké funkci). D. Obecněji, informační obsah jména by měl být přímo úměrný jeho rozsahu platnosti a nepřímo úměrný frekvenci použití: globální jméno musí být informativní, protože jeho definice je „daleko“ (takže si ji už nepamatujeme) a zároveň se nepoužívá příliš často (takže si nepamatujeme ani to, co jsme se dozvěděli, když jsme ho potkali naposled). E. Jméno parametru má dvojí funkci: krom toho, že ho používáme v těle funkce (kde se z pohledu pojmenování chová podobně jako lokální proměnná), slouží jako dokumentace funkce jako celku. Pro parametry volíme popisnější jména, než by zaručovalo jejich použití ve funkci samotné – mají totiž dodatečný globální význam. F. Některé entity mají ustálené názvy – je rozumné se jich držet, protože čtenář automaticky rozumí jejich významu, i přes obvyklou stručnost. Zároveň je potřeba se vyvarovat použití takovýchto ustálených jmen pro nesouvisející entity. Typickým příkladem jsou iterační proměnné ‹i› a ‹j›. G. Jména s velkým rozsahem platnosti by měla být také «zapamatovatelná». Je vždy lepší si přímo vzpomenout na jméno funkce, kterou právě potřebuji, než ho vyhledávat (podobně jako je lepší znát slovo, než ho jít hledat ve slovníku). H. Použitý slovní druh by měl odpovídat druhu entity, kterou pojmenovává. Proměnné a typy pojmenováváme přednostně podstatnými jmény, funkce přednostně slovesy. I. Rodiny příbuzných nebo souvisejících entit pojmenováváme podle společného schématu (‹table_name›, ‹table_size›, ‹table_items› – nikoliv např. ‹items_in_table›; ‹list_parser›, ‹string_parser›, ‹set_parser›; ‹find_min›, ‹find_max›, ‹erase_max› – nikoliv např. ‹erase_maximum› nebo ‹erase_greatest› nebo ‹max_remove›). J. Jména by měla brát do úvahy kontext, ve kterém jsou platná. Neopakujte typ proměnné v jejím názvu (‹cars›, nikoliv ‹list_of_cars› ani ‹set_of_cars›) nemá-li tento typ speciální význam. Podobně jméno nadřazeného typu nepatří do jmen jeho metod (třída ‹list› by měla mít metodu ‹length›, nikoliv ‹list_length›). K. Dávejte si pozor na překlepy a pravopisné chyby. Zbytečně znesnadňují pochopení a (zejména v kombinaci s našeptávačem) lehce vedou na skutečné chyby způsobené záměnou podobných ale jinak napsaných jmen. Navíc kód s překlepy v názvech působí značně neprofesionálně. ¹ Nejde zde pouze o samotný fakt, že je potřeba něco vyhledat. Mohlo by se zdát, že tento problém řeší IDE, které nás umí „poslat“ na příslušnou definici samo. Hlavní zdržení ve skutečnosti spočívá v tom, že musíme přerušit čtení předchozího celku. Na rozdíl od počítače je pro člověka „zanořování“ a zejména pak „vynořování“ na pomyslném zásobníku docela drahou operací. ### 3. Stav a data Udržet si přehled o tom, co se v programu děje, jaké jsou vztahy mezi různými stavovými proměnnými, co může a co nemůže nastat, je jedna z nejtěžších částí programování. TBD: Vstupní podmínky, invarianty, … ### 4. Řízení toku Přehledný, logický a co nejvíce lineární sled kroků nám ulehčuje pochopení algoritmu. Časté, komplikované větvení je naopak těžké sledovat a odvádí pozornost od pochopení důležitých myšlenek. TBD. ### 5. Volba algoritmů a datových struktur TBD. ### 6. Komentáře Nejde-li myšlenku předat jinak, vysvětlíme ji doprovodným komentářem. Čím těžší myšlenka, tím větší je potřeba komentovat. A. Podobně jako jména entit, komentáře které jsou součástí kódu píšeme anglicky.² B. Případný komentář jednotky kódu by měl vysvětlit především „co“ a „proč“ (tzn. jaký plní tato jednotka účel a za jakých okolností ji lze použít). C. Komentář by také neměl zbytečně duplikovat informace, které jsou k nalezení v hlavičce nebo jiné „nekomentářové“ části kódu – jestli máte například potřebu komentovat parametr funkce, zvažte, jestli by nešlo tento parametr lépe pojmenovat nebo otypovat. D. Komentář by «neměl» zbytečně duplikovat samotný spustitelný kód (tzn. neměl by se zdlouhavě zabývat tím „jak“ jednotka vnitřně pracuje). Zejména jsou nevhodné komentáře typu „zvýšíme proměnnou i o jedna“ – komentář lze použít k vysvětlení «proč» je tato operace potřebná – co daná operace dělá si může kažďý přečíst v samotném kódu. ² Tato sbírka samotná představuje ústupek z tohoto pravidla: smyslem našich komentářů je naučit Vás poměrně těžké a často nové koncepty, a její cirkulace je omezená. Zkušenost z dřívějších let ukazuje, že pro studenty je anglický výklad značnou bariérou pochopení. Přesto se snažte vlastní kód komentovat anglicky – výjimku lze udělat pouze pro rozsáhlejší komentáře, které byste jinak nedokázali srozumitelně formulovat. V praxi je angličtina zcela běžně bezpodmínečně vyžadovaná. ### 7. Formální úprava TBD.