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í verzi1 (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), 0112 (jednotlivé kapitoly = týdny semestru), dále s1s3 (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í.
    1
    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.

    A.1 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ě.

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

    A.1.2 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.2 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.
    2
    Pravidla jsou velmi podobná těm v kurzu IB111, ale přesto si je pozorně přečtěte.

    A.1.3 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):
    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
                 

    A.2 Hodnocení

    Abyste předmět úspěšně ukončili, musíte v každém bloku3 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 přípravy a cvičení lze tedy získat teoretické maximum 60 bodů. Dále můžete získat:
    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:
    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:
    Celkově tedy potřebujete:
    3
    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.
    4
    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.

    A.3 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).

    A.3.1 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*).

    A.3.2 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.

    A.4 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:
    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í).

    A.5 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.

    A.5.1 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.

    A.5.2 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:
    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ý:
    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.

    A.5.3 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:

    A.5.4 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:
    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:
    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ě:
    Samotné body za funkcionalitu se při opravě kvality již nijak nemění.

    A.5.5 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).

    A.6 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).

    A.6.1 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:

    A.7 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:
    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íny5 jsou tyto:
    5
    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.

    A.7.1 Vnitrosemestrálky

    V posledním týdnu každého bloku, tedy
    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í.

    A.8 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ě):
    Opíšete-li tedy například 2 přípravy ve druhém týdnu a:
    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:
    budete penalizováni:
    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.

    B.1 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.

    B.2 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:
    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.
    6
    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.

    B.3 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).

    B.4 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ý).

    B.d Demonstrace (ukázky)

    B.d.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 
    
    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 <stdlib.h>     /* exit */ 
    
    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 */ 
    {
    
    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 ); 
    }
    

    B.d.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.h>        /* err, errx, warn, warnx */ 
    #include <stdio.h>      /* dprintf */
    #include <unistd.h>     /* 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 */ 
    {
    
    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 ) 
            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 ) 
            err( 1, "write to stdout failed" );
    
        return 0; 
    }
    

    B.d.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,7
    3. volající byl o tomto selhání co možná nejpodrobněji informován.
    #define _POSIX_C_SOURCE 200809L 
    #include <unistd.h>     /* read, write */
    #include <fcntl.h>      /* openat */
    #include <err.h>        /* warn */
    #include <errno.h>      /* 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, 
                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 ); 
    
    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 ) 
            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 ); 
    
    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 ) 
        {
    
    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; 
    
            if ( close( fd_in ) == -1 ) 
                warn( "failed to close file %s", from_name );
    
            errno = saved_errno; 
            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; 
        char buffer[ nbytes ];
        int bytes_read, bytes_written;
    
        do { 
            bytes_read = read( fd_in, buffer, nbytes );
    
            if ( bytes_read == -1 ) 
            {
    
    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; 
    
                if ( close( fd_in ) == -1 ) 
                    warn( "failed to close file %s", from_name );
                if ( close( fd_out ) == -1 )
                    warn( "failed to close file %s", to_name );
    
                errno = saved_errno; 
                return 2;
            }
    
            bytes_written = write( fd_out, buffer, bytes_read ); 
    
            if ( bytes_written == -1 ) 
            {
    
    A ještě jednou totéž.
                int saved_errno = errno; 
    
                if ( close( fd_in ) == -1 ) 
                    warn( "failed to close file %s", from_name );
                if ( close( fd_out ) == -1 )
                    warn( "failed to close file %s", to_name );
    
                errno = saved_errno; 
                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 ) 
            warn( "failed to close file %s", from_name );
        if ( close( fd_out ) == -1 )
            warn( "failed to close file %s", to_name );
    
        return 0; 
    }
    
    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 ) 
    {
        int saved_errno = errno;
    
        if ( fd != -1 && close( fd ) == -1 ) 
            warn( "failed to close file %s", name );
    
        errno = saved_errno; 
    }
    
    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é.8
    int copy_2( int from_dir, const char *from_name, 
                int to_dir, const char *to_name )
    {
        int fd_in = -1, fd_out = -1;
        int rv = 1;
    
        const int nbytes = 1024; 
        char buffer[ nbytes ];
        int bytes_read, bytes_written;
    
        if ( ( fd_in = openat( from_dir, from_name, O_RDONLY ) ) == -1 ) 
            goto error;
    
        if ( ( fd_out = openat( to_dir, to_name, O_WRONLY | O_CREAT, 
                                0666 ) ) == -1 )
            goto error;
    
        rv = 2; 
    
        do { 
    
            if ( ( bytes_read = read( fd_in, buffer, nbytes ) ) == -1 ) 
                goto error;
    
            if ( ( bytes_written = write( fd_out, buffer, 
                                          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 ); 
    
        rv = 0; 
    
    error: 
        close_or_warn( fd_in, from_name );
        close_or_warn( fd_out, to_name );
        return rv;
    }
    
    int main( void ) /* demo */ 
    {
        int wd = openat( AT_FDCWD, ".", O_DIRECTORY );
    
        if ( wd == -1 ) 
            err( 1, "could not open working directory" );
    
        const char *src = "a0_intro.txt"; 
        const char *dest = "zt.intro.txt";
    
        if ( copy_1( wd, src, wd, dest ) != 0 ) 
            warn( "copying %s to %s failed", src, dest );
    
        if ( copy_2( wd, src, wd, dest ) != 0 ) 
            warn( "copying %s to %s failed", src, dest );
    
        if ( unlinkat( wd, dest, 0 ) != 0 ) 
            warn( "removing %s failed", dest );
    
        return 0; 
    }
    
    7
    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.
    8
    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áší.

    1 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.9 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,
    3. buniq – výpis každého bajtu nejvýše jednou,
    4. 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ý.
    9
    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“.

    1.1 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 dprintf10 – vnitřně používá pro výstup na zadaný popisovač volání write. Bude se nám hodit také volání lseek.
    10
    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.

    1.1.1 write

    Protože výstup je jednodušší než vstup, začneme systémovým voláním write. Má tři parametry:
    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:
    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.

    1.1.2 read

    TBD. Vstup ze souborů je zprostředkován systémovým voláním read.

    1.1.3 lseek

    TBD.

    1.2 Knihovní podprogramy

    1.d Demonstrace (ukázky)

    1.d.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 
    
    #include <unistd.h>  /* read, write */ 
    #include <fcntl.h>   /* openat */
    #include <string.h>  /* memcmp */
    #include <err.h>     /* 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 */ 
    {
    
    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"; 
    
        int fd = openat( AT_FDCWD, filename, O_RDONLY ); 
    
    fd je tradiční název proměnné, která uchovává popisovač otevřeného souboru (z angl. file descriptor; 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 */ 
    
    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 ); 
    
    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; 
        const int expect = 4;
    
        char buffer[ nbytes ]; 
        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.11
    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 ) 
            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 ) 
            errx( 1, "file %s was shorter than expected, only %zd bytes",
                  filename, bytes_read );
    
        if ( memcmp( buffer, "foo\n", expect ) ) 
            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 ) 
            warn( "error closing %s", filename );
    
    Návratová hodnota 0 značí, že program bez chyby doběhl.
        return 0; 
    }
    
    11
    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.

    1.d.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:12
    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ů:
    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.13
    #include <unistd.h> /* write, STDIN_FILENO, … */ 
    #include <stdio.h>  /* dprintf */
    #include <err.h>    /* err */
    
    int main( void ) /* demo */ 
    {
    
    Veškerý výstup do popisovače otevřeného souboru je realizován systémovým voláním write.14 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; 
        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.15
    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 ) 
            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 ) 
            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 ) 
            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; 
    }
    
    12
    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).
    13
    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?
    14
    Nebo některou jeho variantou – pwrite, writev, pwritev, které jdou ale nad rámec tohoto předmětu.
    15
    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).

    1.p Přípravy

    1.p.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:
    int count_lines( int fd, int *count ); 
    

    1.p.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: 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 ); 
    

    1.p.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:
    Úč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 ); 
    

    1.p.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 neexistuje16 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í:
    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:
    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.
    int write_aligned( int fd, const int *values, int cols, int rows ); 
    
    16
    Existuje definice dle RFC 4180, nicméně tato se nezdá být širší komunitou považována za závaznou.

    1.p.5 [cut]

    Implementujte proceduru cut, která na popisovač out vypíše sloupec zadaný parametrem field ze souboru file (sloupce číslujeme od 1).17 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.
    int cut( int dir, const char* file, char delim, int field, int out ); 
    
    17
    Toto chování je podobné standardnímu příkazu cut -d delim -f field file.

    1.p.6 [cat]

    Naprogramujte proceduru cat, která obdrží tyto 3 parametry:
    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; 
    
    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 ); 
    

    1.r Řešené úlohy

    1.r.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 <ctype.h>.
    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 ); 
    

    1.r.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 ); 
    

    1.r.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:
    int path( const char* file, int len ); 
    
    void main( void ) 
    {
        return 0;
    }
    

    1.r.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 ); 
    

    1.r.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; 
    
    int linked( const char* file, int out ); 
    

    1.r.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 ); 
    

    2 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.18
    S obyčejnými soubory mají sockety dvě klíčové společné vlastnosti:
    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
    18
    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í).

    2.1 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.19
    19
    To se změní ve druhém bloku, kdy budeme pracovat s adresami a volání sendto a recvfrom.

    2.1.1 send

    TBD.

    2.1.2 recv

    TBD.

    2.d Demonstrace (ukázky)

    2.d.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,20 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 ) 
    {
        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 ); 
    }
    
    void bind_unix( int sock_fd, const char *addr ) 
    {
        struct sockaddr_un sa = { .sun_family = AF_UNIX };
    
        setup_unix( &sa, addr ); 
    
        if ( unlink( sa.sun_path ) == -1 && errno != ENOENT ) 
            err( 1, "unlinking %s", sa.sun_path );
    
        if ( bind( sock_fd, ( struct sockaddr * ) &sa, sizeof sa ) ) 
            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 ) 
    {
        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; 
        struct sockaddr *sa = ( struct sockaddr * ) &sun;
        socklen_t sa_size = sizeof sun;
    
        char buffer[ 512 ]; 
        int bytes_recvd;
    
        if ( ( bytes_recvd = recvfrom( sock_fd, buffer, 512, 0, 
                                       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", 
                bytes_recvd, buffer, sun.sun_path );
    
        if ( sendto( sock_fd, "pong", 4, 0, sa, sa_size ) == -1 ) 
            err( 1, "sending pong on %s", sun.sun_path );
    
    } 
    
    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, 
                  const char *addr_us, const char *addr_dest )
    {
        bind_unix( sock_fd, addr_us );
    
        struct sockaddr_un sun_dest = { .sun_family = AF_UNIX }; 
        struct sockaddr *sa_dest = ( struct sockaddr * ) &sun_dest;
        socklen_t sa_size = sizeof sun_dest;
    
        setup_unix( &sun_dest, addr_dest ); 
    
    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 ) 
            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ží:
    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 ]; 
        int bytes_recvd;
    
        if ( ( bytes_recvd = recv( sock_fd, buffer, 512, 0 ) ) == -1 ) 
            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", 
                 bytes_recvd, buffer );
    }
    
    int main( int argc, const char **argv ) 
    {
        if ( argc != 3 && argc != 4 )
            errx( 0, "expected arguments: <send|recv> recv_socket [send_socket]" );
    
    V obou případech budeme ke komunikaci potřebovat socket:
        int sock_fd = socket( AF_UNIX, SOCK_DGRAM, 0 ); 
    
        if ( sock_fd == -1 ) 
            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 ) 
        {
            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 ] ); 
        }
        else
            errx( 1, "expected 'send' or 'recv' as first argument" );
    
        return 0; 
    }
    
    20
    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í.

    2.p Přípravy

    2.p.1 [echoc]

    V této úloze je Vaším úkolem naprogramovat klient pro velmi jednoduchý protokol echo, konkrétně jeho proudově orientovanou verzi.21 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í server22 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:
    int check_echo( const char* sock_path, const char* msg, int size ); 
    
    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.
    21
    Protokol echo je přes svoji jednoduchost standardizovaný, konkrétně v RFC 862.
    22
    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á.

    2.p.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:
    Návratová hodnota bude:
    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:
    Na každý klientem odeslaný příkaz dostanete jednořádkovou24 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).
    int lmtp_send( const char *addr, const char *sender, 
                   const char *recip, const char *message );
    
    23
    Zahrnuje mimo jiné chybnou adresu serveru. Při výsledku -1 nechť je errno nastavené na odpovídající hodnotu.
    24
    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.

    2.p.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:
    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,
    
    1. server odpoví paketem ACK nebo ERROR (viz níže),
    2. 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),
    1. 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,
    2. 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:
    Všechny dvoubajtové číselné hodnoty jsou odesílány v pořadí vyšší bajt, nižší bajt (tzv. „big endian“).
    Procedura tftp_put vrátí:
    int tfpt_put( const char *addr, const char *name, 
                  const void *data, size_t size );
    

    2.p.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:
    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 ); 
    
    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.

    2.p.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 ); 
    
    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.

    2.p.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 ); 
    
    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.

    2.r Řešené úlohy

    2.r.1 [agree]

                                   socket, recv, send, connect */
    #include <sys/un.h>         /* struct sockaddr_un */
    #include <unistd.h>         /* close, unlink, fork, alarm */
    #include <sys/wait.h>       /* waitpid */
    #include <signal.h>         /* kill, SIGTERM */
    #include <errno.h>          /* errno, ENOENT, ESRCH */
    #include <string.h>         /* memcmp, memcpy, memset, strncpy */
    #include <err.h>            /* err, warn */
    
    Naprogramujte proceduru agree, která obdrží:
    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:
    int agree( int sock_fd, const char our_set[ 64 ], 
                            char intersection[ 64 ] );
    
    25
    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.

    2.r.3 [filter]

    Naprogramujte proceduru filter, která obdrží:
    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, 
                 int offset, uint32_t threshold );
    

    2.r.6 [tftpr]

    Naprogramujte proceduru tftp_get, která ze serveru protokolu TFTP26 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:
    int tftp_get( const unsigned char *id, int id_len, 
                  int sock_fd, int out_fd );
    
    26
    TODO popsat RRQ část protokolu.

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

    3.1 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.27 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.28 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é velikosti29 – 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).
    27
    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.
    28
    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.
    29
    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.

    3.2 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).30
    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.31
    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í poll32 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.
    30
    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.
    31
    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.
    32
    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.

    3.3 Systémová volání

    Jedinou novinkou je v této kapitole systémové volání poll.

    3.4 Knihovní podprogramy

    3.d Demonstrace (ukázky)

    3.d.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 ) 
    {
        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 ) 
        {
            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 ) 
                return total;
    
            total += bytes_read; 
        }
    }
    
    static void close_or_warn( int fd, const char *name ) 
    {
        if ( close( fd ) == -1 )
            warn( "closing %s", name );
    }
    
    int main( void ) /* demo */ 
    {
        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 ) 
            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(); 
        alarm( 5 ); /* die after 5 seconds if we get stuck */
    
        if ( pid == -1 ) 
            err( 1, "fork" );
    
    Potomek bude postupně zapisovat data do socketu.
        if ( pid == 0 ) /* child */ 
        {
            close_or_warn( fds[ 1 ], "receiver side of a socketpair" );
    
            if ( write( fds[ 0 ], "hel", 3 ) == -1 || 
                 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" ); 
            return 0;
        }
    
        close_or_warn( fds[ 0 ], "sender side of a socketpair" ); 
    
    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 ) 
            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 ); 
    
    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 ) 
            err( 1, "waiting for child %d", pid );
    
        assert( WIFEXITED( status ) ); 
        assert( WEXITSTATUS( status ) == 0 );
    
        close_or_warn( fds[ 1 ], "receiver side of a socketpair" ); 
    
        return 0; 
    }
    

    3.d.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 ); 
    
    void pong( int fd_1, int fd_2 ) 
    {
    
    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 ]; 
    
    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; 
        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; 
        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 ) 
        {
    
    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 ) 
                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 ) 
                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.
        } 
    }
    
    void recv_and_reply( int fd ) 
    {
    
    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 ]; 
        int bytes = recv( fd, message, 4096, 0 );
    
        if ( bytes == -1 ) 
            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 (host to network, long).
        uint32_t reply = htonl( bytes ); 
    
    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 ) 
            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í).
    } 
    
    static void close_or_warn( int fd, const char *name ) 
    {
        if ( close( fd ) == -1 )
            warn( "closing %s", name );
    }
    
    int main( void ) /* demo */ 
    {
        int fds_1[ 2 ], fds_2[ 2 ];
    
        if ( socketpair( AF_UNIX, SOCK_DGRAM, 0, fds_1 ) == -1 || 
             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(); 
        alarm( 5 ); /* die after 5 seconds if we get stuck */
    
        if ( pid == -1 ) 
            err( 1, "fork" );
    
        if ( pid == 0 ) /* child */ 
        {
            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" ); 
        close_or_warn( fds_2[ 0 ], "server side of a socketpair" );
    
        int fd_1 = fds_1[ 1 ], 
            fd_2 = fds_2[ 1 ];
    
        uint32_t reply_1, reply_2; 
    
    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 ) 
            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 ) 
            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 ); 
        assert( ntohl( reply_2 ) == 3 );
    
        int status; 
    
        if ( kill( pid, SIGTERM ) == -1 || 
             waitpid( pid, &status, 0 ) == -1 )
            err( 1, "terminating child process" );
    
        assert( WIFSIGNALED( status ) ); 
        assert( WTERMSIG( status ) == SIGTERM );
    
        close_or_warn( fd_1, "client side of a socketpair" ); 
        close_or_warn( fd_2, "client side of a socketpair" );
    
        return 0; 
    }
    

    3.p Přípravy

    3.p.1 [min]

    V této přípravě je Vaším úkolem naprogramovat datagramový server, který od každého z  klientů obdrží jedno číslo, nalezne mezi nimi minimum a toto vrátí. Čísla v rozsahu 0 až 2³33 - 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 ); 
    
    33
    Jak krátké čtení řešit u protokolů s proměnnou délkou zprávy si ukážeme v páté kapitole.

    3.p.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í.34
    int read_buffer( int fd, char *buffer, int nbytes ); 
    
    34
    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.p.3 [ready]

    Vaším úkolem je implementovat podprogram readyfd, který obdrží tyto dva parametry:
    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ě:
    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 ); 
    

    3.p.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ží:
    Uživatel bude podprogram volat opakovaně, vždy když bude připraven zpracovat další data. Při každém volání podprogram collect:
    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, 
                 int *sizes, int pivot );
    

    3.p.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:
    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:
    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 ); 
    

    3.p.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:
    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 ); 
    

    3.r Řešené úlohy

    3.r.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 ); 
    

    3.r.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.35 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 ); 
    
    35
    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.

    3.r.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.
    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.36 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 ); 
    
    36
    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í.

    3.r.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 ); 
    

    3.r.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:
    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 ); 
    

    3.r.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:
    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.

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

    4.1 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.37 Pro interakci s tímto mechanismem nabízí POSIX tři systémová volání:
    Mapování může být dvou základních typů:
    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.
    37
    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.

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

    4.3 Neblokující zápis

    4.d Demonstrace (ukázky)

    4.d.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 
    {
        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 
    {
        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 ) 
    {
        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 ) 
            if ( node->value == value )
                break;
    
        if ( !node ) 
        {
            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; 
    }
    
    Podprogram hash_free uvolní paměť alokovanou pro hašovací tabulku.
    void hash_free( struct hash_table *ht ) 
    {
        struct hash_node *n, *next;
    
        for ( int i = 0; i < 65536; ++i ) 
            for ( n = ht->buckets[ i ]; n != NULL; n = next )
            {
                next = n->next;
                free( n );
            }
    }
    
    Podprogram mode bude mít 4 parametry:
    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, 
              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 ); 
    
        if ( size == -1 || size % record_size != 0 ) 
            return -1;
    
    Dále vytvoříme mapování souboru do paměti. První parametr systémové služby mmap bude vždy nulový ukazatel38, dále jí předáme:
        uint32_t *base = mmap( NULL, size, PROT_READ, MAP_PRIVATE, fd, 0 ); 
        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 ) 
            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 }; 
        struct hash_node *node;
    
        for ( ssize_t offset = 0; offset < size / 4; offset += record_size ) 
        {
            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; 
        int max = 0;
    
        for ( int i = 0; i < 65536; ++i ) 
            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; 
    out:
    
    Nezapomeneme uvolnit zdroje – jak paměť alokovanou pro hašovací tabulku, tak mapování souboru do paměti.
        hash_free( &ht ); 
        if ( munmap( base, size ) == -1 )
            warn( "unmapping region %p", base );
        return retcode;
    }
    
    int main() /* demo */ 
    {
        const char *name = "zt.d1_data.bin";
        int fd = open( name, O_CREAT | O_TRUNC | O_RDWR, 0666 );
    
        if ( fd == -1 ) 
            err( 1, "creating file %s", name );
    
        uint32_t buffer[ 65536 ]; 
    
        for ( int i = 0; i < 65536 ; ++i ) 
            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 ) 
            if ( write( fd, buffer, sizeof buffer ) == -1 )
                err( 1, "writing data into %s", name );
    
        uint32_t result; 
    
        assert( mode( fd, 8, 0, &result ) == 0 ); 
        assert( result == 7 );
        assert( mode( fd, 8, 1, &result ) == 0 );
        assert( result == 1 );
    
        if ( close( fd ) == -1 ) 
            warn( "closing %s", name );
    
        return 0; 
    }
    
    38
    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.

    4.d.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 ) 
    {
        uint32_t last = ntohl( buffer[ count - 1 ] ) + 1;
        int i = 0;
    
        for ( ; i < count && last + i <= max; ++i ) 
            buffer[ i ] = htonl( last + i );
    
        return i * 4; 
    }
    
    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 
    {
        int fd;
        int nbytes;
        int offset;
        uint32_t buffer[ 64 ];
    };
    
    int iota_write( struct iota_state *state, 
                    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 ) 
    {
    
    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 ] = 
        {
            { .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 ]; 
    
        for ( int i = 0; i < 2; ++i ) 
        {
            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 ) 
        {
            for ( int i = 0; i < 2; ++i )
                pfds[ i ].fd = state[ i ].fd;
    
            if ( poll( pfds, 2, -1 ) == -1 ) 
                return -1;
    
            for ( int i = 0; i < 2; ++i ) 
                if ( pfds[ i ].revents & POLLOUT )
                    if ( iota_write( state + i, 64, max ) == -1 )
                        return -1;
        }
    
        return 0; 
    }
    
    int iota_write( struct iota_state *state, 
                    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; 
    
        int written = write( state->fd, data + state->offset, 
                             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 )  
            return -1;
    
        state->offset += written; 
        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 ) 
        {
            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 ) 
        {
            close( state->fd );
            state->fd = -1;
        }
    
        return 0; 
    }
    
    static void close_or_warn( int fd, const char *name ) 
    {
        if ( close( fd ) == -1 )
            warn( "closing %s", name );
    }
    
    int main( void ) /* demo */ 
    {
        int fds_1[ 2 ], fds_2[ 2 ];
    
        if ( socketpair( AF_UNIX, SOCK_STREAM, 0, fds_1 ) == -1 || 
             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(); 
        alarm( 120 ); /* die if we get stuck */
    
        if ( pid == -1 ) 
            err( 1, "fork" );
    
        if ( pid == 0 ) /* child */ 
        {
            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 || 
                 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 ) 
                err( 1, "iota_pipe unexpectedly failed" );
            else
                exit( 0 ); /* success */
        }
    
        close_or_warn( fds_1[ 0 ], "producer side of a socketpair" ); 
        close_or_warn( fds_2[ 0 ], "producer side of a socketpair" );
    
        int fd_1 = fds_1[ 1 ], 
            fd_2 = fds_2[ 1 ];
    
        uint32_t reply_1, reply_2; 
    
    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 ) 
        {
            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 ); 
            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 ); 
    
        for ( uint32_t i = ( 1 << 19 ) + 1; i <= 1 << 22; ++i ) 
        {
            assert( read( fd_1, &reply_1, 4 ) == 4 );
            assert( ntohl( reply_1 ) == i );
        }
    
        assert( read( fd_1, &reply_1, 4 ) == 0 ); 
    
        int status; 
    
        if ( waitpid( pid, &status, 0 ) == -1 ) 
            err( 1, "awaiting child process" );
    
        assert( WIFEXITED( status ) ); 
        assert( WEXITSTATUS( status ) == 0 );
    
        close_or_warn( fd_1, "consumer side of a socketpair" ); 
        close_or_warn( fd_2, "consumer side of a socketpair" );
    
        return 0; 
    }
    

    4.p Přípravy

    4.p.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ží:
    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 ); 
    

    4.p.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:
    Podprogram check_list ověří, že soubor má správnou strukturu:
    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 ); 
    

    4.p.3 [flood]

    Implementujte podprogram flood_fill s těmito parametry:
    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, 
                    int x, int y, int color );
    

    4.p.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.39
    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 ); 
    
    39
    Je zaručeno, že všechny vstupní popisovače budou mít nastavený příznak O_NONBLOCK.

    4.p.5 [hose]

    Uvažme situaci analogickou k přípravě p4_collect, ale se zápisem – naprogramujte podprogram hose, který obdrží:
    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:
    int hose( int count, int* fds, char** buffers, int* sizes ); 
    
    40
    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.

    4.p.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:
    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:
    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 ); 
    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:
    1. csv
    2. merge
    3. lmdb
    4. httpc
    5. httpd
    6. logd

    S.1.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é:
    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:
    int reformat_csv( int fd_in, char old_delim, 
                      char new_delim, int fd_out );
    

    S.1.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:
    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, 
               int key_begin, int key_end,
               int out_fd );
    

    S.1.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, 
                      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ě):
    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:
    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.43 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:
    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.44
    Listy pak používají tuto strukturu dat:
    Poznámky na závěr:
    41
    Používá se pouze pro typy stránek, které zde nebudeme uvažovat.
    42
    Protože nás zajímá pouze nejnovější transakce, budeme metadatovou stránku s nižším číslem transakce ignorovat.
    43
    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í.
    44
    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).

    S.1.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.45 Řá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,46
    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 
    {
        char *name, *value;
    };
    
    struct http_header_list 
    {
        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, 
                       HTTP_HEAD };
    
    struct http_request 
    {
        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, 
                               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 ); 
    
    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 ); 
    
    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 
    {
        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 ); 
    
    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 ); 
    
    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, 
                      struct http_request *request,
                      struct http_response *response );
    
    45
    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ů.
    46
    Víceřádkové hlavičky pro zjednodušení nebudeme uvažovat.

    S.1.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 
    {
        const char *path;
        const char *content_type;
        const char *data;
        size_t data_size;
    };
    
    struct file_list 
    {
        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 ); 
    
    static void unlink_if_exists( const char* file ) 
    {
        if ( unlink( file ) == -1 && errno != ENOENT )
            err( 2, "unlinking %s", file );
    }
    
    int main( int argc, const char **argv ) 
    {
        if ( argc == 1 )
            return 0; /* no automated tests */
    
        if ( argc > 2 ) 
            errx( 1, "expected arguments: socket_path" );
    
        struct file_list hello, bye; 
    
        hello.file.path = "/hello.txt"; 
        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"; 
        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 ] ); 
        httpd( argv[ 1 ], &hello );
    
        return 1; /* httpd should never return */ 
    }
    

    S.1.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í:
    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 ); 
    
    int main( int argc, const char **argv ) 
    {
        if ( argc == 1 )
            return 0; /* no automated tests */
    
        if ( argc < 4 ) 
            errx( 1, "arguments expected: "
                     "socket_path main_log log_dir" );
    
        int log_fd, dir_fd; 
        const char *sock = argv[ 1 ],
                   *log  = argv[ 2 ],
                   *dir  = argv[ 3 ];
    
        if ( unlink( sock ) == -1 && errno != ENOENT ) 
            err( 1, "removing %s", sock );
    
        if ( mkdir( dir, 0777 ) == -1 && errno != EEXIST ) 
            err( 1, "creating directory %s", dir );
    
        if ( ( log_fd = open( log, 
                              O_CREAT | O_TRUNC | O_RDWR,
                              0666 ) ) == -1 )
            err( 1, "creating %s", log );
    
        if ( ( dir_fd = open( dir, O_DIRECTORY ) ) == -1 ) 
            err( 1, "opening %s", dir );
    
        logd( sock, log_fd, dir_fd ); 
    
        return 1; /* logd should never return */ 
    }
    

    5 Ř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.47
    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).
    47
    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.

    5.1 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).

    5.1.1 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).

    5.2 Knihovní funkce

    5.d Demonstrace (ukázky)

    5.d.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 ASCII48),
    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 
    #include <string.h>     /* strlen, strcmp */
    #include <stdio.h>      /* dprintf, snprintf */
    #include <unistd.h>     /* STDOUT_FILENO */
    #include <assert.h>
    
    int main( void ) /* demo */ 
    {
    
    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"; 
    
    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 ); 
        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é bajty49, záporná je-li levá strana lexikograficky (po bajtech) menší a kladná jinak.
        assert( strcmp( string, "hello" ) == 0 ); 
    
    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.50
        const char thing_1[] = { 0x76, 0xc4, 0x9b, 0x63, 0 }; 
        const char thing_2[] = { 0x76, 0x65, 0xcc, 0x8c, 0x63, 0 };
    
        assert( strcmp( "věc",  thing_1 ) == 0 ); 
        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 ); 
    
    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 ], 
                 (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 ]; 
        assert( snprintf( buffer, 5, "123456789" ) == 9 );
        assert( strcmp( buffer, "1234" ) == 0 );
    
        return 0; 
    
        // cflags: -Wno-format-truncation 
    }
    
    48
    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.
    49
    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é.
    50
    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.

    5.d.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éna51 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.52
    #include <stdio.h>      /* dprintf */ 
    #include <unistd.h>     /* read, write */
    #include <fcntl.h>
    #include <err.h>
    
    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 ) 
    {
        int fd = openat( dir_fd, path, flags );
    
        if ( fd == -1 ) 
            err( 1, "error opening %s", path );
    
        return fd; 
    }
    
    void close_or_warn( int fd, const char *name ) 
    {
        if ( close( fd ) == -1 )
            warn( "error closing descriptor %d for file %s", fd, name );
    }
    
    int main( int argc, const char **argv ) /* demo */ 
    {
    
    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[] = 
        {
            "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 ] : ".."; 
        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.53
        for ( int i = 0; filenames[ i ]; ++i ) 
        {
            const char *name = filenames[ i ];
            int file_fd = open_or_die( dir_fd, name, O_RDONLY );
    
            const int nbytes = 10; 
            char buffer[ nbytes ];
    
            int bytes_read = read( file_fd, buffer, nbytes ); 
    
            if ( bytes_read == -1 ) 
                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 || 
                 dprintf( STDOUT_FILENO, "\n" ) == -1 )
                err( 1, "error writing to stdout" );
    
            close_or_warn( file_fd, name ); 
        }
    
    Nezapomeneme uzavřít popisovače složek, které jsme otevřeli na začátku podprogramu.
        close_or_warn( dir_fd, "00" ); 
        close_or_warn( top_fd, top_path );
        return 0;
    }
    
    51
    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á.
    52
    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.
    53
    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.

    5.p Přípravy

    5.p.1 [long]

    Napište podprogram longest_line, který přečte veškerá data ze zadaného popisovače,54 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).55 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).
    char *longest_line( int fd ); 
    
    54
    Můžete předpokládat, že na popisovači bude fungovat lseek.
    55
    Tento limit se vztahuje i na knihovní podprogramy, které využijete. Pozor, fdopen používá na mnoha systémech dynamickou paměť.

    5.p.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 */ 
    {
        char *memory;
        int allocd;
        int used;
    };
    
    char *read_until( int fd, char delimiter, 
                      struct shift_buffer *buf, int *length );
    

    5.p.3 [cat]

    Naprogramujte proceduru cat, která obdrží tyto 3 parametry:
    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:
    int cat( int dir_fd, int list_fd, int out_fd ); 
    

    5.p.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 ); 
    

    5.p.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:
    int kvchk( int server_fd, const char *key, int data_fd ); 
    

    5.p.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:
    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 
    {
        const char *key;
        struct key_list *next;
    };
    
    int kvget( int server_fd, int dir_fd, struct key_list *keys ); 
    

    6 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)56 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ů).
    56
    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.

    6.1 Systémová volání

    6.2 Knihovní podprogramy

    6.d Demonstrace (ukázky)

    6.d.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 ) 
    {
        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 ) 
    {
    
    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; 
    
    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; 
        struct timespec newest_time = { 0 };
    
        int dup_fd = dup( dir_fd ); 
    
        if ( dup_fd == -1 ) 
            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 ); 
    
    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 ) 
            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 ); 
    
    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; 
        struct stat st;
    
        while ( ( ptr = readdir( dir ) ) ) 
        {
    
    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, 
                          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*.57 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 ) && 
                 is_newer( st.st_mtim, newest_time ) )
            {
                free( newest_name );
                newest_time = st.st_mtim;
                newest_name = strdup( ptr->d_name );
    
                if ( !newest_name ) 
                    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; 
        newest_name = NULL;
    out:
        free( newest_name );
    
        if ( dir ) 
            closedir( dir );
        else if ( dup_fd != -1 )
            close( dup_fd );
    
        return rv; 
    }
    
    int main( void ) /* demo */ 
    {
        int cwd_fd = open( ".", O_DIRECTORY | O_RDONLY );
    
        if ( cwd_fd == -1 ) 
            err( 2, "opening working directory" );
    
        char *name = find_newest( cwd_fd ); 
        dprintf( STDOUT_FILENO, "the newest file is %s\n", name );
        free( name );
        return 0;
    }
    
    57
    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.

    6.d.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 ) 
    {
    
    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 ) 
            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 ) 
            *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; 
        DIR *dir = fdopendir( root_fd );
    
        if ( !dir ) 
            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 ); 
    
        struct dirent *ptr; 
        struct stat st;
    
    Následuje standardní iterace položkami adresáře.
        while ( ( ptr = readdir( dir ) ) ) 
        {
    
    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 || 
                 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, 
                          AT_SYMLINK_NOFOLLOW ) == -1 )
                goto out;
    
            if ( !S_ISDIR( st.st_mode ) ) 
                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, 
                                 O_DIRECTORY | O_RDONLY );
    
            if ( tree_depth_rec( sub_fd, max_depth, depth + 1 ) == -1 ) 
                goto out;
        }
    
        rv = 0; 
    out:
        if ( dir )
            closedir( dir );
    
        return rv; 
    }
    
    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 ) 
    {
        int max_depth = 0;
    
        if ( tree_depth_rec( dup( root_fd ), &max_depth, 0 ) == -1 ) 
            return -1;
    
        return max_depth; 
    }
    
    Dále již podprogram tree_depth pouze otestujeme.
    static int mkdir_or_die( int dir_fd, const char *name ) 
    {
        int fd;
    
        if ( mkdirat( dir_fd, name, 0777 ) == -1 && errno != EEXIST ) 
            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; 
    }
    
    int main( void ) /* demo */ 
    {
        int fds[ 5 ];
    
        fds[ 0 ] = mkdir_or_die( AT_FDCWD, "zt.d2_root" ); 
        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 ); 
        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 ) 
            close( fds[ i ] );
    
        return 0; 
    }
    

    6.d.3 [refs]

    int count_refs_rec( int root_fd, dev_t dev, ino_t ino, int *count ) 
    {
        if ( root_fd == -1 )
            return -1;
    
        int rv = -1; 
        DIR *dir = fdopendir( root_fd );
    
        if ( !dir ) 
            goto out;
    
        rewinddir( dir ); 
    
        struct dirent *ptr; 
        struct stat st;
    
        while ( ( ptr = readdir( dir ) ) ) 
        {
            if ( strcmp( ptr->d_name, "." ) == 0 ||
                 strcmp( ptr->d_name, ".." ) == 0 )
                continue;
    
            if ( fstatat( root_fd, ptr->d_name, &st, 
                          AT_SYMLINK_NOFOLLOW ) == -1 )
                goto out;
    
            if ( st.st_dev == dev && st.st_ino == ino ) 
            {
                dprintf( 2, "found copy at %s\n", ptr->d_name );
                ++ *count;
            }
    
            if ( S_ISDIR( st.st_mode ) ) 
            {
                int sub_fd = openat( root_fd, ptr->d_name,
                                     O_DIRECTORY | O_RDONLY );
    
                if ( count_refs_rec( sub_fd, dev, ino, count ) == -1 ) 
                    goto out;
            }
        }
    
        rv = 0; 
    out:
        if ( dir )
            closedir( dir );
    
        return rv; 
    }
    
    int count_refs( int root_fd, int file_fd ) 
    {
        struct stat st;
        int count = 0;
    
        if ( fstat( file_fd, &st ) == -1 ) 
            return -1;
    
        if ( count_refs_rec( dup( root_fd ), st.st_dev, 
                             st.st_ino, &count ) == -1 )
            return -1;
    
        return count; 
    }
    
    static int mkdir_or_die( int dir_fd, const char *name ) 
    {
        int fd;
    
        if ( mkdirat( dir_fd, name, 0777 ) == -1 && errno != EEXIST ) 
            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; 
    }
    
    static int create_file( int dir_fd, const char *name ) 
    {
        int fd = openat( dir_fd, name, O_CREAT | O_WRONLY, 0666 );
    
        if ( fd == -1 ) 
            err( 1, "creating file %s", name );
    
        return fd; 
    }
    
    static void link_or_die( int fd_1, const char *name_1, 
                             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 */ 
    {
        int fds[ 5 ];
    
        fds[ 0 ] = mkdir_or_die( AT_FDCWD, "zt.d3_root" ); 
        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" ); 
        int fd_2 = create_file( fds[ 1 ], "bar_1" );
    
        link_or_die( fds[ 0 ], "foo_1", fds[ 0 ], "foo_2" ); 
        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 ); 
        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 ) 
            close( fds[ i ] );
    
        close( fd_1 ); 
        close( fd_2 );
    
        return 0; 
    }
    

    6.p Přípravy

    6.p.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 ); 
    

    6.p.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:
    Odkazy, u kterých se nepodařilo přístupová práva ověřit, ponechte v seznamu.
    struct name_list_node 
    {
        const char *name;
        struct name_list_node *next;
    };
    
    struct name_list 
    {
        struct name_list_node *head;
    };
    
    int check_access( int dir_fd, struct name_list *names ); 
    

    6.p.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 
    {
        char *name;
        struct name_list *next;
    };
    
    struct name_list *list( int real_dir_fd ); 
    

    6.p.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:
    Návratová hodnota:
    int archive_files( int work_fd, int archive_fd, 
                       struct timespec threshold );
    

    6.p.5 [find]

    Naprogramujte proceduru find, která obdrží:
    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 ); 
    

    6.p.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:
    ssize_t disk_usage( int root_fd ); 
    

    7 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
    2. ntpc – (UDP) klient pro NTP
    3. whois – vrátit první řádek WHOIS odpovědi
    4. 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

    7.1 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.58 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é.
    58
    Neplatí pro 2023, nicméně vytvoření socketu a navázání spojení si na tomto místě i tak zopakujeme.

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

    7.1.2 sendto

    Jako send/write, ale na zadanou adresu. Použitelné pouze s datagramovými sockety. TBD.

    7.1.3 recvfrom

    Jako recv/read, ale u datagramových socketů zároveň získá a vyplní adresu odesílatele. TBD.

    7.1.4 bind

    Přiřadí otevřenému anonymnímu socketu adresu. Prozatím budeme používat pouze s datagramovými sockety.

    7.1.5 connect

    Pro spojovaný socket naváže spojení. Pro datagramový nastaví implicitní cílovou adresu.

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

    7.2.1 getaddrinfo

    Nalezne a vyplní adresu (sockaddr) podle zadaného jména. TBD.

    7.d Demonstrace (ukázky)

    7.d.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 */ 
    {
        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 ); 
    
        if ( sock_fd == -1 ) 
            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.59 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 }; 
    
        if ( strlen( argv[ 1 ] ) >= sizeof sa.sun_path - 1 ) 
            errx( 1, "socket address too long, maximum is %zu",
                     sizeof sa.sun_path );
    
        snprintf( sa.sun_path, sizeof sa.sun_path, "%s", argv[ 1 ] ); 
    
    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, 
                      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 ]; 
        size_t nbytes = strlen( message );
    
        if ( write( sock_fd, message, nbytes ) == -1 ) 
            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“).60
        if ( close( sock_fd ) == -1 ) 
            warn( "closing %s", sa.sun_path );
    
        return 0; 
    }
    
    59
    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.
    60
    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.

    7.p Přípravy

    7.p.1 [host]

    7.p.2 [echoc]

    7.p.3 [ntpc]

    7.p.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ě.

    7.p.5 [newsc]

    7.p.6 [splice]

    7.r Řešené úlohy

    7.r.1 [bcast]

    Naprogramujte proceduru bcast, která obdrží:
    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 
    {
        struct sockaddr_un address;
        struct address_list *next;
    };
    
    void bcast( int sock_fd, struct address_list *addresses ); 
    

    7.r.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 ); 
    

    7.r.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),61
    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.
    struct address_map 
    {
        struct sockaddr_un source, destination;
        struct address_map *left, *right;
    };
    
    void router( int sock_fd, struct address_map *root ); 
    
    61
    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.

    8 Spustitelné soubory

    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

    8.p Přípravy

    8.p.1 [shell]

    8.p.2 [execp]

    8.p.3 [binex]

    8.p.4 [uexec]

    8.p.5 [wfile]

    8.p.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:
    1. dup – rozdělení pevných odkazů na samostatné soubory
    2. dir – práce s adresářovým stromem
    3. dedup – spojování identických souborů pevnými odkazy
    4. httpc – klient pro stažení souboru z webu
    5. httpd – jednoduchý webový server
    6. execline – skripty v jednom procesu

    S.2.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
    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.62 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:
    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 ); 
    
    62
    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.

    S.2.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: 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 
    {
        t_directory,
        t_regular,
        t_symlink,
        t_socket,
        t_other,
    };
    
    struct node 
    {
        char *name;
        enum file_type type;
    
        int error; 
    
        struct node *dir; 
        struct node *next;
    };
    
    Úkol spočívá v implementaci dvou procedur níže. První z nich je tree_create, která bere parametry: Její návratovou hodnotou bude: 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 ); 
    
    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 ); 
    

    S.2.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:
    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 ); 
    

    S.2.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,63
    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 
    {
        char *name, *value;
    };
    
    struct http_header_list 
    {
        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, 
                       HTTP_HEAD };
    
    struct http_request 
    {
        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, 
                               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 ); 
    
    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 ); 
    
    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 
    {
        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 ); 
    
    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 ); 
    
    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, 
                      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 ); 
    
    63
    Víceřádkové hlavičky pro zjednodušení nebudeme uvažovat.

    S.2.e httpd

    TBD. Předmětem této úlohy bude server protokolu HTTP.

    S.2.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:
    Po nastavení pracovní složky na dir tedy spustíme ls -hl.
    void cd( int argc, char **argv ); 
    
    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 ); 
    
    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).64 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 ); 
    
    Pro všechny utility platí, že je-li zbývající příkaz prázdný, neprovede se nic a proces je ukončen.
    64
    Věnujte pozornost tomu, jak se POSIXová utilita chová, je-li soubor symbolickým odkazem, při jiných testech než -L.

    9 Procesy

    9.d Demonstrace (ukázky)

    9.d.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,65
    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 ) 
    {
        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 ); 
        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 }; 
    
        if ( strlen( argv[ 1 ] ) >= sizeof sa.sun_path - 1 ) 
            errx( 1, "socket address too long, maximum is %zu",
                     sizeof sa.sun_path );
    
        snprintf( sa.sun_path, sizeof sa.sun_path, "%s", argv[ 1 ] ); 
    
    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).66 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 ) 
            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 ) ) 
            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 ) ) 
            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.67
        int client_fd, bytes; 
        char buffer[ 64 ];
    
        while ( ( client_fd = accept( sock_fd, NULL, NULL ) ) >= 0 ) 
        {
    
    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.68
            while ( ( bytes = read( client_fd, buffer, 64 ) ) > 0 ) 
                dprintf( STDOUT_FILENO, "%.*s", bytes, buffer );
    
            if ( bytes == -1 ) 
                warn( "reading from client" );
    
            dprintf( STDOUT_FILENO, "\n" ); 
    
    Jakmile druhá strana spojení ukončí, popisovač uzavřeme a jsme připraveni přijmout další spojení od dalšího klienta.69
            if ( close( client_fd ) == -1 ) 
                warn( "closing connection" );
        }
    
        err( 1, "accept" ); 
    }
    
    65
    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.
    66
    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.
    67
    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.
    68
    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.
    69
    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.

    9.p Přípravy

    9.p.1 [fork]

    9.p.2 [multi]

    9.p.3 [circle]

    9.p.4 [echod]

    9.p.5 [pipe]

    9.p.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):
    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 ); 
    int comm_read( void *handle, char *buffer, int nbytes );
    int comm_write( void *handle, const char *data, int nbytes );
    int comm_fini( void *handle );
    

    9.p.6 [meter]

    10 Vlákna

    10.p Přípravy

    10.p.1 [xxx]

    10.p.2 [xxx]

    10.p.3 [xxx]

    10.p.4 [xxx]

    10.p.5 [xxx]

    10.p.6 [xxx]

    11 Synchronizace

    11.p Přípravy

    11.p.1 [xxx]

    11.p.2 [xxx]

    11.p.3 [xxx]

    11.p.4 [xxx]

    11.p.5 [xxx]

    11.p.6 [xxx]

    12 Opakování

    12.p Přípravy

    12.p.1 [xxx]

    12.p.2 [xxx]

    12.p.3 [xxx]

    12.p.4 [xxx]

    12.p.5 [xxx]

    12.p.6 [xxx]

    S.3 Souběžnost

    S.3.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.

    S.3.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).

    S.3.c supervise

    S.3.d httpd

    S.3.e newsd

    S.3.f dns

    K Vzorová řešení

    K.1 Týden 1

    K.1.r.1 [wcount]

    int count_words( int dir_fd, const char *file, int *count ) 
    {
        const int nbytes = 1024;
        char buffer[ nbytes ];
        int fd, bytes_read;
        int result = -1;
        bool in_word = false;
    
        *count = 0; 
    
        if ( ( fd = openat( dir_fd, file, O_RDONLY ) ) == -1 ) 
            goto out;
    
        do { 
    
            if ( ( bytes_read = read( fd, buffer, nbytes ) ) == -1 ) 
                goto out;
    
            for ( int i = 0; i < bytes_read; ++i ) 
                if ( in_word && isspace( buffer[ i ] ) )
                {
                    ++ *count;
                    in_word = false;
                }
                else if ( !in_word && !isspace( buffer[ i ] ) )
                    in_word = true;
    
        } while ( bytes_read > 0 ); 
    
        if ( in_word ) 
            ++ *count;
    
        result = 0; 
    
    out: 
        if ( fd != -1 && close( fd ) == -1 )
            warn( "closing %s", file );
        return result;
    }
    

    K.1.r.2 [cgrep]

    int copy_span( int fd_in, int fd_out, int from, int to ) 
    {
        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 ) 
            return -1;
    
        if ( lseek( fd_in, from, SEEK_SET ) == -1 ) 
            return -1;
    
        do { 
            if ( ( bytes_read = read( fd_in, buffer, nbytes ) ) == -1 )
                return -1;
    
            bytes_total += bytes_read; 
            to_write = bytes_read;
            int extra = bytes_total + from - to;
    
            if ( extra > 0 ) 
                to_write -= extra;
    
            if ( write( fd_out, buffer, to_write ) == -1 ) 
                return -1;
        } while ( bytes_read > 0 && bytes_total < to - from );
    
        if ( lseek( fd_in, orig_pos, SEEK_SET ) == -1 ) 
            return -1;
    
        return 0; 
    }
    
    int cgrep( int fd_in, char c, int fd_out ) 
    {
        const int nbytes = 3;
        char buffer[ nbytes ];
        int bytes_read = 0, offset = 0, line_start = 0;
        bool matched = false;
    
        do { 
            if ( ( bytes_read = read( fd_in, buffer, nbytes ) ) == -1 )
                return -1;
    
            for ( int i = 0; i < bytes_read; ++i, ++offset ) 
            {
                if ( buffer[ i ] == c )
                    matched = true;
    
                if ( buffer[ i ] == '\n' ) 
                {
                    if ( matched &&
                         copy_span( fd_in, fd_out, line_start,
                                    offset + 1 ) == -1 )
                        return -1;
    
                    line_start = offset + 1; 
                    matched = false;
                }
            }
        } while ( bytes_read > 0 );
    
        return 0; 
    }
    

    K.1.r.4 [bcount]

    int count_distinct( int dir_fd, const char *file ) 
    {
        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 ) 
            goto out;
    
        do { 
            if ( ( bytes_read = read( fd, buffer, nbytes ) ) == -1 )
                goto out;
    
            for ( int i = 0; i < bytes_read; ++i ) 
                counts[ buffer[ i ] ] ++;
        } while ( bytes_read > 0 );
    
        result = 0; 
    
        for ( int i = 0; i < 256; ++i ) 
            if ( counts[ i ] )
                ++ result;
    
    out: 
        saved_errno = errno;
    
        if ( fd != -1 && close( fd ) == -1 ) 
            warn( "closing fd %d for %s", fd, file );
    
        errno = saved_errno; 
        return result;
    }
    

    K.1.r.6 [otp]

    static void close_or_warn( int fd, const char *name ) 
    {
        if ( fd != -1 && close( fd ) == -1 )
            warn( "closing %s", name );
    }
    
    void xor( char *a, const char *key, int size ) 
    {
        for ( int i = 0; i < size; ++i )
            a[ i ] = a[ i ] ^ key[ i ];
    }
    
    int otp_fd( int fd_file, int fd_key, int out ) 
    {
        char buf[ 4 ];
        char key[ sizeof buf ];
        int f_bytes;
    
        while ( ( f_bytes = read( fd_file, buf, sizeof buf ) ) > 0 ) 
        {
            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 ); 
            if ( write( out, buf, f_bytes ) == -1 )
                return -2;
        }
        if ( f_bytes == -1 )
            return -2;
    
        return 0; 
    }
    
    int otp( int dir, const char* file, const char* key_file, int out ) 
    {
        int ok = -2;
        int e;
        int fd_file = -1;
        int fd_key = -1;
    
        if ( ( fd_file= openat( dir, file, O_RDONLY ) ) == -1 ) 
            goto error;
    
        if ( ( fd_key = openat( dir, key_file, O_RDONLY ) ) == -1 ) 
            goto error;
    
        ok = otp_fd( fd_file, fd_key, out ); 
    
    error: 
        e = errno;
        close_or_warn( fd_file, file );
        close_or_warn( fd_key, key_file );
        errno = e;
        return ok;
    }
    

    K.2 Týden 2

    K.2.r.1 [agree]

                                   send, connect, AF_UNSPEC */ 
    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; 
        uint8_t buf[ msg_len + 1 ];
    
        struct sockaddr_un sun; 
        struct sockaddr *sa = ( struct sockaddr * ) &sun;
        socklen_t sa_size = sizeof sun;
    
        if ( ( bytes = recvfrom( sock_fd, buf, msg_len + 1, 
                                 0, sa, &sa_size ) ) == -1 )
        {
            saved_errno = errno;
            warn( "server recvfrom" );
            goto cleanup;
        }
    
        if ( bytes != msg_len ) 
            return -2;
    
        for ( int i = 0; i < msg_len; ++i ) 
            buf[ i ] &= our_set[ i ];
    
        if ( ( bytes = sendto( sock_fd, buf, msg_len, 
                               0, sa, sa_size ) ) == -1 )
        {
            saved_errno = errno;
            warn( "server send" );
            goto cleanup;
        }
    
        assert( bytes == msg_len ); 
    
        if ( ( bytes = recv( sock_fd, confirmation, 2, 0 ) ) == -1 ) 
        {
            saved_errno = errno;
            warn( "server recv" );
            goto cleanup;
        }
    
        if ( bytes != 1 ) 
            return -2;
    
        if ( confirmation[ 0 ] == 1 ) 
        {
            memcpy( intersection, buf, msg_len );
            rv = 0;
        }
        else if ( confirmation[ 0 ] == 0 )
            rv = -3;
        else
            rv = -2;
    
    cleanup: 
        errno = saved_errno;
        return rv;
    }
    

    K.2.r.3 [filter]

    void filter( int fd_in, int fd_out, int offset, uint32_t thresh ) 
    {
        uint8_t buf[ 4096 ];
        int bytes_in;
    
        while ( 1 ) 
        {
            if ( ( bytes_in = recv( fd_in, buf, 4096, 0 ) ) == -1 )
                err( 1, "recv error" );
    
            uint32_t value = * ( uint32_t * ) ( buf + offset ); 
    
            if ( ntohl( value ) > thresh ) 
                if ( send( fd_out, buf, bytes_in, 0 ) == -1 )
                    warn( "send" );
        }
    }
    

    K.3 Týden 3

    K.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 ) 
    {
        if ( *fd == -1 )
            return 0;
    
        int bytes_written = write( *fd, data, size ); 
    
    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 ) 
        {
            *fd = -1;
            return 0;
        }
    
        return bytes_written; 
    }
    
    int clone_stream( int in_fd, int out1_fd, int out2_fd ) 
    {
        char buf[ 512 ];
        int bytes_read;
    
        while ( ( bytes_read = read( in_fd, buf, sizeof buf ) ) > 0 ) 
        {
            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 ) 
                return 0;
        }
    
        return bytes_read; 
    }
    

    K.3.r.2 [bridge]

    int other( int ix ) { return ( ix + 1 ) % 2; } 
    
    int bridge( int fd_1, int fd_2 ) 
    {
        struct pollfd fds[ 2 ];
        for ( int i = 0; i < 2; ++i )
            fds[ i ].events = POLLIN;
    
        fds[ 0 ].fd = fd_1; 
        fds[ 1 ].fd = fd_2;
    
        char buf[ 1024 ]; 
    
        while ( 1 ) 
        {
            int ready = poll( fds, 2, -1 );
            if ( ready == -1 )
                return -1;
    
            for ( int i = 0; i < 2; ++i ) 
            {
                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 ) 
                        return -1;
                }
                else if ( fds[ i ].revents & POLLERR )
                    return -1;
            }
        }
    
        return 0; 
    }
    

    K.3.r.3 [slip]

    int decode_slip( int in_fd, int out_fd ) 
    {
        char buf[ 513 ];
    
        int buffered = 0; 
        int bytes_read;
    
        while ( ( bytes_read = read( in_fd, buf + buffered, 
                                     sizeof buf - buffered ) ) > 0 )
        {
            buffered += bytes_read;
    
            char *end; 
            while (( end = memchr( buf, 0xc0, buffered ) ))
            {
                int pktlen = end - buf;
                if ( send( out_fd, buf, pktlen, 0 ) == -1 )
                    return -1;
    
                buffered -= pktlen + 1; 
    
                memmove( buf, buf + pktlen + 1, buffered ); 
            }
        }
    
        if ( bytes_read == -1 ) 
            return -1;
    
        if ( buffered > 0 && send( out_fd, buf, buffered, 0 ) == -1 ) 
            return -1;
    
        return 0; 
    }
    

    K.3.r.4 [seq]

    ssize_t seq_one( int in_fd, int out_fd ) 
    {
        char buf[ UINT16_MAX + 2 ];
        ssize_t bytes = recv( in_fd, buf + 2, sizeof buf - 2, 0 );
    
        if ( bytes == -1 ) 
            return -2;
    
        uint8_t sz[ 2 ]; 
        sz[ 0 ] = ( bytes & 0xff00 ) >> 8;
        sz[ 1 ] =   bytes & 0x00ff;
    
        memcpy( buf, sz, 2 ); 
    
        return write( out_fd, buf, bytes + 2 ); 
    }
    
    int packet_seq( int in_fd, int out_fd ) 
    {
        while ( 1 )
            if ( seq_one( in_fd, out_fd ) < 0 )
                return errno == EPIPE ? 0 : -1;
    }
    

    K.3.r.5 [log]

    // Zkopíruje posledních ‹tail_size› bajtů souboru na jeho začátek. 
    // 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 ) 
        {
            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; 
            tail_size -= bytes_read;
        }
    
        return 0; 
    }
    
    int log_stream( int in_fd, int out_fd, int log_fd, int limit ) 
    {
        int half_limit = ( limit + 1 ) / 2;
        assert( half_limit > 0 );
    
        char buf[ 1024 ]; 
        int bytes_dropped = 0;
    
        // Soubor už může obsahovat nějaká data. 
        int bytes_logged = lseek( log_fd, 0, SEEK_END );
        if ( bytes_logged == -1 )
            return -1;
    
        int bytes_read; 
        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. 
            int log_length = bytes_logged + bytes_read;
            int bytes_to_discard = 0;
    
            while ( log_length > limit ) 
            {
                bytes_to_discard += half_limit;
                log_length -= half_limit;
            }
    
            bytes_dropped += bytes_to_discard; 
    
            // Zahodit bajty z logovacího souboru 
            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 
                if ( bytes_to_keep > 0 && fmovetail( log_fd, bytes_to_keep ) == -1 )
                    return -1;
    
                // Zkrátit soubor 
                if ( ftruncate( log_fd, bytes_to_keep ) == -1 )
                    return -1;
    
                bytes_to_discard -= ( bytes_logged - bytes_to_keep ); 
    
                // Posunout ukazovátko na konec logu (po fmovetail může být kdekoli) 
                int bytes_kept = lseek( log_fd, 0, SEEK_END );
                if ( bytes_kept == -1 )
                    return -1;
    
                assert( bytes_kept == bytes_to_keep ); 
            }
    
            assert( bytes_to_discard < bytes_read ); 
            assert( bytes_to_discard >= 0 );
    
            // Zapsat novou zprávu (nebo její konec, pokud máme bytes_to_discard) 
            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 
            bytes_logged = lseek( log_fd, 0, SEEK_END );
            if ( bytes_logged == -1 )
                return -1;
            assert( bytes_logged == log_length );
        }
    
        if ( bytes_read == -1 ) 
            return -1;
    
        return bytes_dropped; 
    }
    

    K.7 Týden 7

    K.7.r.1 [bcast]

    struct address_list 
    {
        struct sockaddr_un address;
        struct address_list *next;
    };
    
    void bcast_once( int sock_fd, struct address_list *addresses ) 
    {
        char buf[ 4096 ];
    
        struct sockaddr_un src_addr = { 0 }; 
        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, 
                              ( struct sockaddr* )&src_addr, &addrlen );
        if ( bytes == -1 )
            warn( "recvfrom" );
    
        for ( struct address_list *ptr = addresses; ptr; ptr = ptr->next ) 
            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 ) 
    {
        while ( 1 )
            bcast_once( sock_fd, addresses );
    }
    

    K.7.r.2 [proxy]

    void proxy( int sock_fd ) 
    {
        const int nbytes = 4096;
        char buffer[ nbytes ];
        struct sockaddr_un addr;
    
        while ( 1 ) 
        {
            int nread = recv( sock_fd, buffer, nbytes, 0 );
    
            if ( nread == -1 ) 
                err( 1, "recvfrom" );
    
            if ( nread < ( int ) sizeof addr ) 
                warnx( "incorrect datagram of size %d", nread );
    
            memcpy( &addr, buffer, sizeof addr ); 
    
            ssize_t nwrote = sendto( sock_fd, buffer + sizeof addr, 
                                     nread - sizeof( addr ), 0,
                                     ( struct sockaddr * ) &addr,
                                     sizeof addr );
    
            if ( nwrote == -1 ) 
                warn( "forwarding datagram" );
        }
    }
    

    K.7.r.3 [router]

    struct address_map 
    {
        struct sockaddr_un source, destination;
        struct address_map *left, *right;
    };
    
    const struct sockaddr_un * 
    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 ); 
        if ( c == 0 )
            return &root->destination;
    
        return find_destination( source, 
                                 c < 0 ? root->left : root->right );
    }
    
    void router( int sock_fd, struct address_map *root ) 
    {
        const size_t buffer_size = 512;
        char buffer[ buffer_size ];
        memset( buffer, 0, buffer_size );
    
        struct sockaddr_un from_addr; 
        socklen_t from_addr_len;
    
        while ( 1 ) 
        {
            from_addr_len = sizeof( struct sockaddr_un );
    
            ssize_t nread = recvfrom( sock_fd, buffer, buffer_size, 0, 
                                      (struct sockaddr *) &from_addr,
                                      &from_addr_len );
    
            if ( nread == -1 ) 
                err( 1, "recvfrom" );
    
            const struct sockaddr_un *to_addr = 
                find_destination( &from_addr, root );
    
            if ( to_addr == NULL ) 
            {
                warnx( "no route to dest" );
                continue;
            }
    
            ssize_t nwrote = sendto( sock_fd, buffer, nread, 0, 
                                     (struct sockaddr*) to_addr,
                                     sizeof( struct sockaddr_un ) );
    
            if ( nwrote == -1 ) 
                warn( "sendto" );
        }
    }
    

    T Technické informace

    Tato kapitola obsahuje informace o technické realizaci předmětu, a to zejména:

    T.1 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.

    T.1.1 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.70 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í.
    70
    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.

    T.1.2 Stažení koster

    Kostry naleznete ve studijních materiálech v ISu: StudentPB152Studijní materályUč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.

    T.1.3 Odevzdání řešení

    Vypracované příklady můžete odevzdat do odevzdávárny v ISu: StudentPB152Odevzdá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 (StudentPB152Studijní materiályPrivate).

    T.1.4 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).71
    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).
    71
    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“.

    T.1.5 Recenze

    Vám adresované recenze, podobně jako archiv odevzdaných souborů, naleznete ve složce Private ve studijních materiálech (StudentPB152Studijní materiályPrivate). Shrnutí bodového zisku za tyto recenze pak naleznete v poznámkovém bloku reviews.

    T.1.6 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.

    T.2 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 WSL72 – 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).
    72
    Jako alternativu, nechcete-li z nějakého důvodu WSL instalovat, lze použít program putty.

    T.2.1 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).

    T.2.2 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.

    T.2.3 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.

    T.2.4 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.

    T.2.5 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ář).

    T.3 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).

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

    T.3.2 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ě.

    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á:

    U.0.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.
    1. 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ě.
    2. 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í.
    3. 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.
    4. 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í.
    5. 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?
    6. 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.
    7. 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.

    U.0.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í.
    1. Všechny entity ve zdrojovém kódu nesou anglická jména. Angličtina je univerzální jazyk programátorů.
    2. 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.73
    3. 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).
    4. 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).
    5. 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.
    6. 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.
    7. 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).
    8. 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.
    9. 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).
    10. 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).
    11. 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ě.
    73
    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í.

    U.0.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, …

    U.0.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.

    U.0.5 Volba algoritmů a datových struktur

    TBD.

    U.0.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.
    1. Podobně jako jména entit, komentáře které jsou součástí kódu píšeme anglicky.74
    2. 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).
    3. 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.
    4. 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.
    74
    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á.

    U.0.7 Formální úprava

    TBD.