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.
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>.
Návratová hodnota: 0 – úspěch; 1 – systémová chyba.
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.
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í.
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.
V této úloze si naprogramujeme jednoduchou techniku
z kryptografie označovanou jako „one-time pad.“
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.
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.
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ěď.
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ží:
V této úloze je Vaším úkolem naprogramovat klient pro velmi
jednoduchý protokol echo, konkrétně jeho proudově orientovanou
verzi. Server tohoto protokolu od klienta přijímá data a obratem
je odesílá nezměněná zpět.
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:
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ů.
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.
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
.
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
.
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
).
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
.
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).
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ů.
Nulová návratová hodnota značí, že protistrana spojení
uzavřela, a žádná další data už doručena nebudou.
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.
Vyčkáme na ukončení procesu–zapisovatele, ověříme, že skončil
bez chyb a následně celý program ukončíme.
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í).
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ě.
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.
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í.
int read_buffer( int fd, char *buffer, int nbytes );
3.p.3 [ready
]
Vaším úkolem je implementovat podprogram readyfd
, který obdrží
tyto dva parametry:
bits
je ukazatel na bitové pole,
count
je velikost takto předaného pole v bitech.
Každý nenulový bit v předaném bitovém poli označuje popisovač, na
který se volající dotazuje. Funkce vrátí hodnotu nejnižšího
z dotazovaných popisovačů, z něhož je možné
číst. Není-li možné
číst z žádného z nich, program zablokuje do doby, než to možné
bude.
Bitového pole je předáno v ⌈count/8⌉ bajtech, přitom jednotlivé
bity jsou indexovány následovně:
- bajt s nejnižší adresou odpovídá popisovačům 0–7,
- následující bajt popisovače 8–15, atd.,
- uvnitř každého bajtu odpovídá nejméně významný bit popisovači
s nejnižším číslem.
Návratová hodnota je číslo popisovače připraveného ke čtení, nebo
-1 nastane-li chyba. Krom systémových chyb ošetřete i případ, kdy
je vstupní množina popisovačů prázdná – v takové situaci nastavte
errno
na
EDEADLK
.
int readyfd( unsigned char* bits, int count );
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ží:
count
– počet popisovačů, se kterými bude pracovat,
fds
– ukazatel na pole popisovačů,
buffers
– ukazatel na pole ukazatelů, kde každý ukazatel
určuje paměť, do které se načtou data z odpovídajícího
popisovače,
sizes
– ukazatel na pole čísel, které určí, kolik nejvýše
bajtů se má z odpovídajícího popisovače načíst,
pivot
– index popisovače (význam viz níže).
Uživatel bude podprogram volat opakovaně, vždy když bude
připraven zpracovat další data. Při každém volání podprogram
collect
:
- načte data z jednoho popisovače,
- posune odpovídající ukazatel v poli
buffers
za konec právě
načtených dat,
- sníží odpovídající hodnotu v poli
sizes
o počet načtených
bajtů,
- vrátí index dotčeného popisovače.
Není-li žádný popisovač ke čtení připraven, funkce blokuje, než
se situace změní. Je-li připraven více než jeden popisovač,
použije se ten s nejnižším indexem větším nebo rovným hodnotě
pivot
. Jsou-li všechny připravené popisovače na indexech
menších než
pivot
, použije se nejnižší z nich.
Dojde-li k systémové chybě, podprogram vrátí hodnotu -1 a zařídí,
aby se budoucí volání se stejnými parametry pokusilo neprovedené
akce zopakovat.
int collect( int count, int *fds, char **buffers,
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:
- pošle další blok, je-li nějaký k dispozici,
- jinak zavře spojení.
V reakci na odpověď
0x02
server ukončí spojení (již bez další
komunikace).
Procedura bget
obdrží popisovač spojovaného socketu a popisovač
souboru, do kterého bude data od serveru zapisovat. Po zapsání
každého bloku použije na popisovač systémové volání fdatasync
–
úspěch tohoto volání pak potvrdí serveru odesláním bajtu 0x01
.
Návratová hodnota bget
bude:
- 0 v případě úspěchu (všechna data, která server odeslal, byla
uložena),
- -2 v případě, že byla komunikace ukončena kvůli selhání zápisu
dat do souboru a
- -1 v případě, že nastala chyba komunikace.
Do výstupního parametru
block_count
pak uloží počet bloků,
které byly úspěšně uloženy do souboru, bez ohledu na celkový
výsledek.
int bget( int sock_fd, int file_fd, int *block_count );
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:
- 0 v případě úspěchu – server potvrdil všechny zprávy a soubor
s daty k odeslání je prázdný,
- -3 je-li předán nesprávný datový soubor,
- -2 v případě, že byla komunikace ukončena kvůli selhání na
straně serveru (server indikoval, že data nebyl schopen
uložit),
- -1 v případě, že nastala chyba komunikace nebo systémová chyba
na straně klienta.
Do výstupního parametru
block_count
pak uloží počet bloků,
které byly úspěšně odeslány a potvrzeny, bez ohledu na celkový
výsledek. Popisovač spojení vždy před návratem uzavře, popisovač
souboru nikoliv.
int bput( int sock_fd, int file_fd, int *block_count );
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. 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 );
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.
Z in_fd
bude číst velmi jednoduchý zapouzdřovací protokol:
každý datagram je zakódován do jednoho rámce, přitom jednotlivé
rámce jsou v proudu dat odděleny bajtem 0xc0
. Oddělovač se
v takto vymezených rámcích (a tedy ani odpovídajících
datagramech) nesmí objevit. Můžete předpokládat, že velikost
jednoho rámce nepřesáhne 512 bajtů.
Procedura decode_slip
konečně z každého rámce vytvoří jeden
datagram, který odešle na out_fd
.
int decode_slip( int in_fd, int out_fd );
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:
in_fd
– popisovač, ze kterého bude data číst,
out_fd
– popisovač, do kterého bude data přeposílat,
log_fd
– popisovač souboru, ve kterém bude udržovat záznamy,
limit
– limit na počet bajtů uložených v souboru log_fd
.
Dojde-li během přenosu kapacita logovacího souboru, tento bude
zkrácen na polovinu limitu smazáním nejstarších dat. Návratová
hodnota
-1
indikuje chybu, jinak procedura vrátí počet bajtů,
které byly přeposlány, ale ze souboru
log_fd
byly pro
nedostatek místa smazány.
Poznámka: Vhodnou volbou velikosti čtení z popisovače in_fd
se
může řešení značně zjednodušit (zejména ji můžete přizpůsobit
aktuálně volnému místu v logovacím souboru).
int log_stream( int in_fd, int out_fd, int log_fd, int limit );
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:
- zpráva, která začíná nulovým bajtem, obsahuje pouze jeden další
bajt, který označuje nenulový kód kategorie – jedná se
o „subscribe“ část protokolu, tzn. odesílatel této zprávy si
přeje dostávat zprávy z dané kategorie,
- ve zprávě, která začíná nenulovým bajtem, tento označuje kód
kategorie; tato zpráva dále obsahuje bajt, který značí jak
dlouhý je zbytek zprávy – tento typ zprávy tvoří „publish“
část.
Každá příchozí „publish“ zpráva bude přeposlána právě těm
připojeným protistranám, které si předtím vyžádaly (zprávou
„subscribe“) odběr příslušné kategorie zpráv.
Zprávy ani jednoho typu není potřeba odesílateli potvrzovat.
Je-li odesílatel odběratelem dané kategorie, zpráva je mu
odeslána stejně, jako všem ostatním. Procedura pubsub
skončí,
jakmile se odpojí poslední z protistran.
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:
sum
– sumarizace tabulky s pevně velkými záznamy
list
– kontrola struktury záznamů
flood
– in-situ flood fill v bitmapovém souboru
write
– zápis bloku dat do neblokujícího popisovače
hose
– souběžný zápis do několika popisovačů
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. Pro interakci s tímto mechanismem
nabízí POSIX tři systémová volání:
mmap
vytvoří mapování – vstupem je popisovač obyčejného
souboru, vlastnosti mapování (přístupová práva, sdílený vs
soukromý přístup) a jeho rozsah,
msync
vyžádá zápis změn, které program provedl v mapované
paměti, zpátky do souboru na disku, a konečně
munmap
již nepotřebné mapování uvolní a zároveň provede
případné nedokončené zápisy do souboru.
Mapování může být dvou základních typů:
- sdílené (
MAP_SHARED
) – změny provedené skrz toto mapování se
projeví v souboru, do kterého takto mapované adresy odkazují,
- soukromé (
MAP_PRIVATE
) – změny provedené v mapované paměti
zůstávají viditelné pouze v procesu, který mapování vlastní.
Pozor! Není zaručeno, že v
soukromém mapování se nebudou
projevovat vnější změny souboru – přesné chování závisí na
konkrétním operačním systému. Zároveň není zaručeno, že změny
provedené ve
sdíleném mapování budou viditelné pro ostatní
vstupně-výstupní operace ihned, a to ani ze stejného procesu –
operace
read
může vrátit verzi dat, kde se zápis skrze mapování
ještě neprojevil. Přenos upraveného obsahu paměti do souboru je
nutné vyžádat voláním
msync
nebo
munmap
.
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:
- každý zdroj, který byl vyžádán (alokován), musí být také vrácen
(uvolněn),
- odpovědnost za vrácení zdroje musí být jasně určena,
- 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:
- popisovač vstupního souboru,
- počet čtyřbajtových slov v jednom záznamu
record_size
,
- pořadové číslo čtyřbajtového slova v záznamu, které budeme
zpracovávat
value_index
,
- výstupní parametr
result
.
Hodnoty, které budeme zpracovávat, budou čtyřbajtové, uložené od
nejvýznamnějšího bajtu.
int mode( int fd, int record_size, int value_index,
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ý ukazatel, dále jí
předáme:
- velikost mapování (v tomto případě velikost souboru),
- přístupový mód (v tomto případě „pouze pro čtení“),
- režim sdílení (
MAP_PRIVATE
protože do souboru nehodláme
zapisovat),
- popisovač souboru, kterého obsah chceme do paměti mapovat,
- místo, od kterého chceme obsah souboru mapovat (v bajtech,
v tomto případě mapujeme celý soubor od prvního bajtu).
uint32_t *base = mmap( NULL, size, PROT_READ, MAP_PRIVATE, fd, 0 );
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;
}
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ží:
fd
– popisovač souboru, se kterým bude pracovat (můžete
předpokládat, že objekt, se kterým je svázaný, lze mapovat do
paměti),
cols
– počet sloupců zde uložené tabulky,
sizes
– pole čísel (pro každý sloupec jedno), které určuje
kolik bitů daný sloupec obsahuje (1–64),
results
– pole cols
64bitových čísel, kam uloží součty
jednotlivých sloupců.
Hodnoty jsou uloženy od nejvýznamnějšího bajtu po nejméně
významný, vždy v nejmenším možném počtu bajtů. Není-li počet bitů
sloupce dělitelný 8, zbývající nejvyšší bity nejvyššího bajtu jsou
nevyužity. Tyto bity ignorujte – ve vstupním souboru mohou mít
libovolné hodnoty. Hodnoty ve sloupcích jsou bezznaménkové.
Nedošlo-li při sčítání k chybě, podprogram vrátí hodnotu 0.
V případě systémové chyby vrátí -1 a pokud soubor obsahuje
neúplný řádek, vrátí -2, aniž by změnil pole results
.
Podprogram musí pracovat efektivně i v situaci, kdy soubor
obsahuje velmi velký počet krátkých řádků.
int sum( int fd, int cols, int *sizes, uint64_t *results );
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:
- uložen v posledních 4 bajtech,
- formou celého čísla se znaménkem (v dvojkovém doplňkovém kódu,
nejvýznamnější bajt první),
- které určuje o kolik bajtů dále od začátku soubor začíná
odkazovaný záznam oproti tomu aktuálnímu.
Podprogram
check_list
ověří, že soubor má správnou strukturu:
- soubor neobsahuje nic než záznamy,
- každý záznam vyjma posledního odkazuje na začátek jiného záznamu,
- poslední záznam odkazuje sám na sebe (tj. odkaz je nulový),
- na první záznam žádný jiný neodkazuje,
- na každý další záznam existuje právě jeden odkaz.
Vstupem pro
check_list
je popisovač souboru (o kterém můžete
předpokládat, že odkazuje objekt, který lze mapovat do paměti) a
velikost jednoho záznamu. Návratová hodnota 0 znamená, že soubor
je korektní, hodnota 1, že nikoliv a -1 že při zpracování souboru
nastala systémová chyba.
Podprogram musí pracovat efektivně i v situaci, kdy je v souboru
velké množství malých záznamů. Bez ohledu na obsah souboru musí
výpočet skončit (podprogram se nesmí zacyklit).
int check_list( int fd, int rec_size );
4.p.3 [flood
]
Implementujte podprogram flood_fill
s těmito parametry:
fd
je popisovač souboru ve formátu BMP, se kterým bude
pracovat (a který lze mapovat do paměti),
offset
index prvního pixelu v souboru,
w
, h
je šířka a výška obrázku v pixelech,
x
, y
jsou souřadnice pixelu, od kterého začne vyplňovat
jednobarevnou plochu,
color
je barva, na kterou plochu přebarví.
Obrázek používá formát, kde jeden pixel je kódovaný jedním
bajtem a různé hodnoty představují různé barvy. Pixely jsou
uloženy po řádcích a každý řádek je zarovnán na celé 4 bajty
doleva (tzn. nepoužité bajty jsou na konci),
Podprogram vrátí nulu proběhne-li vše úspěšně, -2 není-li vstup
v očekávaném formátu (nesouhlasí první dva bajty nebo je soubor
příliš krátký), nebo -1 nastane-li při zpracování systémová
chyba.
int flood_fill( int fd, int offset, int w, int h,
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
.
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 );
4.p.5 [hose
]
Uvažme situaci analogickou k přípravě p4_collect
, ale se
zápisem – naprogramujte podprogram hose
, který obdrží:
count
– počet popisovačů, se kterými se bude pracovat,
fds
– ukazatel na pole popisovačů,
buffers
– ukazatel na pole ukazatelů, kde každý určuje data,
která se mají zapsat do příslušného popisovače,
sizes
– ukazatel na pole čísel, která určují, kolik dat se
má do kterého popisovače zapsat.
Není-li možné provést žádný zápis, podprogram
hose
bude
blokovat, než se situace změní. Jinak provede všechny zápisy,
které provést lze, aniž by volání blokovalo, a odpovídajícím
způsobem upraví ukazatele v
buffers
a velikosti v
sizes
.
Návratová hodnota:
- 0 – veškeré požadované zápisy byly provedeny,
- kladné číslo – počet popisovačů, které nejsou připravené
k zápisu, ale jsou pro ně přichystaná data,
- -1 – došlo k systémové chybě – opětovné volání se stejnými
parametry se pokusí akci zopakovat.
int hose( int count, int* fds, char** buffers, int* sizes );
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:
tee_init
obdrží pole popisovačů, do kterých si přejeme data
kopírovat, a vrátí ukazatel handle
(nebo nulový ukazatel
v případě selhání),
tee_write
obdrží ukazatel handle
, ukazatel na data a
velikost dat, které si přejeme rozeslat,
tee_fini
zapíše všechna zbývající data a uvolní veškeré
zdroje přidružené k předanému ukazateli handle
.
Podprogram
tee_write
nebude za žádných okolností blokovat.
Zároveň zabezpečí, že veškerá data která odeslat lze budou
odeslána ihned, a že data, která už byla odeslána do všech kanálů
nebudou zabírat paměť. Podprogram
tee_write
lze volat s nulovou
velikostí, v takovém případě pouze provede případné odložené
zápisy. Návratová hodnota
tee_write
je:
- 0 – veškerá data byla odeslána všem příjemcům,
- kladné číslo – počet popisovačů, které jsou „pozadu“,
- -1 – nastala chyba, která znemožnila zpracování předaných dat
(data lze bezpečně do
tee_write
předat znovu, aniž by
hrozilo dvojité odeslání).
Nastane-li chyba při zápisu na některý popisovač,
tee_write
tento popisovač přeskočí a data se pokusí při dalším volání opět
odeslat.
Konečně tee_fini
dokončí veškeré zápisy. Dojde-li při některém
z nich k chybě, vrátí -1
, ale až potom, co dokončí všechny
zápisy, které provést lze, a uvolní zdroje spojené s ukazatelem
handle
(včetně uzavření všech přidružených popisovačů).
Očekává se, že tee_fini zavře všechny popisovače předané tee_ini
void *tee_init( int fd_count, int *fds );
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:
csv
merge
lmdb
httpc
httpd
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é:
- obsahuje-li v sobě některá hodnota právě používaný oddělovač,
musí být uzavřena v uvozovkách (jinak jsou uvozovky kolem
hodnot volitelné),
- obsahuje-li hodnota uzavřená v uvozovkách znaky
"
nebo \
,
tyto musí být uvozeny znakem \
,
- znak nového řádku v hodnotě nepřipouštíme,
- oddělovač, uvozovka, zpětné lomítko a znak nového řádku jsou
každý kódovány jedním bajtem.
Vaším úkolem bude naprogramovat proceduru, která na vstupu
(zadaném pomocí popisovače) obdrží soubor v tomto formátu a na
výstup (opět popisovač) zapíše novou verzi, která bude:
- používat zadaný oddělovač (může být jiný nebo stejný jako byl
na vstupu),
- právě ty hodnoty, které obsahují nový oddělovač, uzavře do
uvozovek.
int reformat_csv( int fd_in, char old_delim,
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:
- vstupní soubory mají stejnou velikost záznamu,
- soubory mohou být mnohem větší, než je dostupná paměť,
- vstupy mohou být roury nebo jiné objekty, které neumožňují
opakované čtení (podobně výstup),
- výstup nebude obsahovat dva záznamy se stejným klíčem, a to
ani v situaci, kdy je obsahoval některý soubor na vstupu
(použije se první záznam z prvního souboru, který ho
obsahuje).
Návratová hodnota 0 značí úspěch, -1 systémovou chybu, -2
nekompletní záznam v některém vstupním souboru.
int merge( int in1_fd, int in2_fd, int record_size,
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ě):
- [00] 64bitové pořadové číslo stránky v souboru,
- [08] nevyužité 16bitové číslo,
- [0a] 16bitové pole příznaků,
- [0c] 16bitová celková velikost metadatové části,
- [0e] 16bitové číslo prvního bajtu datové části.
První dvě stránky datového souboru popisují databázi jako celek
a po výše uvedené hlavičce v každé z nich následuje:
- [10] 32bitové identifikační číslo (vždy 0xbeefc0de),
- [14] 32 + 64 + 64 bitů, které přeskočíme,
- [28] 6 × 64 bitů metadat o prázdném místě,
- [58] informace o hlavním B+stromě:
- [58] 32 nevyužitých bitů,
- [5c] 16 bitů příznaků,
- [5e] 16bitová hloubka stromu,
- [60] 3 × 64 bitů statistických informací,
- [78] 64bitový počet klíčů,
- [80] číslo kořenové stránky,
- [88] číslo poslední stránky,
- [90] pořadové číslo transakce, která tuto stránku zapsala.
Samotné B+stromy jsou uloženy po stránkách – jedna stránka
obsahuje jeden uzel. V metadatové části jsou uloženy 16bitové
odkazy na začátky jednotlivých klíčů – odkaz je index prvního
bajtu daného klíče, počítáno od začátku stránky. Jedná-li se
o vnitřní uzel nebo o list se pozná z příznaků v metadatové
hlavičce stránky – vnitřní uzly mají nastavený nejnižší bit
(
0x01
), listy mají nastavený druhý nejnižší bit (
0x02
).
Formát klíče se mírně liší podle toho, jestli se jedná o vnitřní
uzel nebo o list. Vnitřní uzly používají tuto strukturu:
- 48bitové číslo stránky (uzlu), na kterou tento klíč odkazuje,
- 16bitová velikost klíče,
- samotný klíč.
Protože ke každému klíči je připojen jeden odkaz, musí vnitřní
uzly obsahovat jeden prázdný klíč navíc – je to ten nejvíce vlevo
a má nastavenu délku klíče na nulu.
Listy pak používají tuto strukturu dat:
- 32bitová velikost hodnoty náležící tomuto klíči,
- 16 bitů příznaků (uvažujeme nulové),
- 16bitová velikost klíče,
- samotný klíč,
- hodnota náležící klíči.
Poznámky na závěr:
- pro práci s čísly pevné bitové šířky se budou hodit typy
uint16_t
, uint64_t
, int16_t
, atd., definované hlavičkou
stdint.h
,
- při testování se může hodit modul
lmdb
pro Python:
- získáte jej příkazem
pip install lmdb
,
env = lmdb.open('test')
vytvoříte prázdnou databázi,
txn = env.begin(write=True)
otevřete transakci,
txn.put(b'key', b'value')
vložíte dvojici klíč/hodnota,
txn.commit()
transakci ukončíte,
- datový soubor se bude jmenovat
test/data.mdb
.
S.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. Řá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:
- řá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
,
- hlavičky – každé pole začíná jménem, následované dvojtečkou a
textovou hodnotou, která pokračuje až do konce řádku,
- 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 );
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í:
ok, logging as id\n
čím potvrdí, že požadavek přijal a je
připraven zaznamenávat přijaté zprávy,
- nebo
error <popis chyby>\n
narazí-li na nějaký problém
(např. se nepodařilo otevřít logovací soubor).
Připojí-li se víc klientů se stejným
id
, nejedná se o chybu,
logovací soubor budou sdílet. Když soubor se záznamy již
existuje, existující obsah nechť je zachován – nové záznamy bude
server přidávat na konec.
Proběhne-li počáteční výměna v pořádku, server bez zbytečné
prodlevy uloží každou další zprávu, kterou od klienta obdrží, do
obou souborů.
Hlavní soubor bude proceduře logd
předán jako popisovač
otevřeného souboru main_log_fd
, vedlejší soubory bude podle
potřeby vytvářet ve složce předané popisovačem otevřené složky
log_dir_fd
. Soubory se budou jmenovat id.log
kde id
je
identifikátor odeslaný klientem.
Narazí-li logd
na fatální chybu, ukončí program s chybovým
kódem. Není-li server schopen zprávy některého klienta ukládat,
ukončí s tímto klientem spojení.
void logd( const char *addr, int main_log_fd, int log_dir_fd );
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.
Ukázky:
strings
– základy práce s řetězci a textem,
dirs
– nalezení / otevření obyčejného souboru,
acrostic
– řetězec prvních písmen každého řádku.
Přípravy:
long
– nalezení nejdelšího řádku,
read
– čtení záznamů oddělených specifickým bajtem,
cat
– načtení souboru se jmény souborů pro spojení,
kvsd
– slovníkový server s úložištěm v souborovém systému,
kvchk
– ověření přítomnosti klíče a asociované hodnoty,
kvget
– dávkové stažení (linked list klíčů na vstupu).
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:
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,
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,
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,
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:
- 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),
- 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),
- vybranou množinu znaků, nazývanou Portable Character Set,
musí být systém schopen kódovat, a to navíc tak, že každému
znaku odpovídá jeden bajt (jedná se o většinu znaků
obsažených v ASCII),
- 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é bajty, 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.
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
}
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éna se soubory
(v obecném smyslu, tzn. nejen obyčejnými).
Složku lze otevřít stejně jako jiné typy souborů – takto získaný
popisovač tuto složku jednoznačně identifikuje, a to i v situaci,
kdy se změní odkazy na tuto složku, a tedy její jméno v nadřazené
složce nemusí platit, nebo se může přesunout v adresářové
struktuře na úplně jiné místo.
#include <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:
- 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
- takto sestavené cesty mohou v různých iteracích ukazovat
na soubory v různých složkách – souborový systém je
sdílený a každá operace je potenciálním hazardem
souběhu.
for ( int i = 0; filenames[ i ]; ++i )
{
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;
}
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, a nalezne v nich nejdelší řádek. Tento
řádek pak uloží do dynamicky alokovaného pole vhodné velikosti a
vrátí ukazatel na toto pole. Za řádky považujeme pouze sekvence
znaků ukončené znakem konce řádku (případný nekompletní řádek na
konci souboru ignorujeme).
Program nesmí alokovat více dynamické paměti, než je trojnásobek
délky nalezeného nejdelšího řádku (+ 128 bajtů paměti k dobru).
Smíte předpokládat, že vstup neobsahuje nulové bajty. V případě
neúspěchu podprogram vrátí nulový ukazatel (včetně situace, kdy
vstup neobsahuje žádný kompletní řádek).
char *longest_line( int fd );
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:
dir_fd
– popisovač adresáře, ve kterém bude hledat všechny
níže zmíněné soubory,
list_fd
– popisovač, ze kterého podprogram cat
přečte
jména souborů,
out_fd
– výstupní popisovač.
Vstup
list_fd
bude obsahovat na každém řádku jméno souboru
(samotný seznam nemusí být nutně zapsán v souboru). Procedura
cat
zapíše obsahy všech těchto souborů (v zadaném pořadí) do
popisovače
out
. Za řádek považujeme posloupnost znaků
zakončenou '\n'
(nikoliv tedy
"\r\n"
nebo
'\r'
).
Nepodaří-li se nějaký soubor otevřít, přeskočte jej. Návratová
hodnota:
- 0 – podařilo se otevřít, přečíst a zapsat všechny soubory,
- -1 – chyba čtení z popisovače
list_fd
, chyba zápisu do
out_fd
, nebo jiná fatální systémová chyba,
- -2 – vše proběhlo v pořádku, ale některé soubory byly
přeskočeny, protože se je nepodařilo otevřít.
int cat( int dir_fd, int list_fd, int out_fd );
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:
- 0 je-li vše v pořádku (hodnota odpovídá očekávání),
- -1 došlo-li k chybě komunikace nebo jiné fatální chybě,
- -2 nebyl-li požadovaný klíč na serveru přítomen,
- -2 jestli byla hodnota přijata, ale neodpovídá očekávání.
int kvchk( int server_fd, const char *key, int data_fd );
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:
- 0 je-li vše v pořádku (všechny hodnoty byly staženy a úspěšně
uloženy),
- -1 došlo-li k chybě komunikace, chybě při ukládání dat, nebo
jiné fatální chybě,
- -2 nebyl-li některý požadovaný klíč na serveru přítomen
(nepřítomnost klíče ale nebrání stažení všech ostatních).
Existuje-li v zadané složce soubor se stejným jménem, jaký by měl
podprogram
kvget
vytvořit, považujeme to za fatální chybu
(dotčený soubor se přitom nesmí nijak změnit).
struct key_list
{
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:
- klíč je unikátní jméno (řetězec, který neobsahuje nulový znak
ani znak
/
),
- hodnota je odkaz na soubor (i-uzel) libovolného typu (může být
opět adresářem).
Jména jsou unikátní pouze v rámci jednoho adresáře. Soubor (který
není adresářem) může být odkazován více než jednou položkou, a
položky, které odkazují na stejný soubor, mohou být ve stejné nebo
různých adresářích (složkách).
Ukázky:
newest
– nalezení nejnovějšího souboru v zadané složce,
depth
– počítání maximální hloubky adresářové struktury,
refs
– kolik odkazů na soubor je v zadaném stromě.
Přípravy:
fcount
– počítání obyčejných souborů v adresáři,
access
– kontrola práv u souborů zadaných seznamem,
list
– sestavení spojovaného seznamu názvů souborů,
archive
– přesun starých souborů do archivní složky,
find
– rekurzivní hledání podle jména,
du
– využití místa složkou (bez tvrdých odkazů).
6.1 Systémová volání
O_DIRECTORY
pro openat
fstatat
renameat
dup
6.2 Knihovní podprogramy
strcmp
fdopendir
readdir
rewinddir
closedir
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*
. 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;
}
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:
- popisovač otevřené složky
dir_fd
,
- spojovaný seznam
names
jmen.
Tato ze seznamu odstraní všechna jména souborů, které jsou
chráněny proti zápisu třetí stranou a ponechá ty, do kterých může
zapisovat kdokoliv jiný, než vlastník. Všechna jména jsou
relativní vůči složce
dir_fd
.
Práva souboru zjistíte z položky st_mode
struktury stat
.
Přijdou Vám zřejmě vhod také hodnoty S_IWGRP
(Stat Inode Write
GRouP – bitová maska odpovídající právu skupiny na zápis) a
S_IWOTH
(Stat Inode Write OTHer – totéž, ale pro všechny
ostatní).
Návratová hodnota:
- -2 v případě, kdy nebylo možné práva u některého jména ověřit
(např. proto, že takový odkaz neexistuje),
- -1 při neopravitelné systémové chybě (např. chyba při načítání
položek adresáře),
- 0 při úspěšném dokončení.
Odkazy, u kterých se nepodařilo přístupová práva ověřit, ponechte
v seznamu.
struct name_list_node
{
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ží:
work_fd
– popisovač složky, ze které bude staré soubory
přesouvat do archivu,
archive_fd
– popisovač archivní složky (do této složky
budou staré soubory přesunuty),
threshold
– hodnota struct timespec
, která určuje
nejstarší soubor, který má být ve složce work_fd
zachován.
Dále:
- pro určení stáří souboru použijeme časové razítko posledního
zápisu (
mtime
),
- dojde-li ke kolizi jmen ve složce
archive_fd
, existující
soubory budou nahrazeny (jinak ale existující soubory
zůstávají nedotčené),
- dojde-li během zpracování k chybě a povaha chyby to umožňuje,
procedura
archive_files
se pokusí zbývající položky přesto
zpracovat.
Návratová hodnota:
- 0 značí, že vše proběhlo bez chyb,
- -1 že operace byla z důvodu chyby přerušena,
- -2 že některé soubory nebylo možné zkontrolovat nebo
přesunout.
int archive_files( int work_fd, int archive_fd,
struct timespec threshold );
6.p.5 [find
]
Naprogramujte proceduru find
, která obdrží:
root_fd
je popisovač otevřeného adresáře,
name
je jméno hledaného odkazu,
flags
jsou příznaky, které předá volání openat
.
a která prohledá podstrom začínající v
root_fd
. Nalezne-li
v zadaném podstromě právě jeden odkaz hledaného jména, tento
otevře (s příznaky nastavenými na
flags
) a výsledný popisovač
vrátí. Nenalezne-li žádný nebo více než jeden vyhovující odkaz,
vrátí hodnotu -2. Dojde-li při prohledávání k chybě, vrátí -1.
Nezapomeňte uvolnit veškeré zdroje, které jste alokovali
(s případnou výjimkou popisovače, který je funkcí vrácen).
int find( int root_fd, const char *name, int flags );
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:
- existuje-li se ve stromě obyčejný soubor, na který odkazuje
více než jeden odkaz (nerozhoduje, zda je tento odkaz uvnitř
nebo vně prohledávaného stromu), prohledávání ukončete
s výsledkem -2,
- dojde-li při zpracování podstromu k systémové chybě, výsledek
bude -1.
ssize_t disk_usage( int root_fd );
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:
client
– klient pro proudový socket rodiny AF_UNIX
,
webget
– stažení webové stránky ze zadané adresy.
Přípravy:
echoc
– gai + connect + read
ntpc
– (UDP) klient pro NTP
whois
– vrátit první řádek WHOIS odpovědi
newsc
– TCP verze 02/newsc
host
– najde první IPv6 adresu zadaného systému
splice
– přeposílat data z jednoho socketu do jiného
Řešené příklady:
bcast
– přeposílání datagramů na zadaný seznam adres,
proxy
– zrcadlení datagramů na adresy uložené v nich,
router
– přeposílání datagramů podle zadané tabulky adres,
xxx
xxx
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. 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é.
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:
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,
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
),
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. 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“).
if ( close( sock_fd ) == -1 )
warn( "closing %s", sa.sun_path );
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ží:
- popisovač datagramového socketu,
- zřetězený seznam adres.
Jejím úkolem bude každý přijatý datagram přeposlat na všechny
adresy v seznamu, s výjimkou původního odesílatele.
Jako maximální možnou délku datagramu považujte pro tento
úkol 4 KiB.
V případě že nastane nějaká chyba, vypište pouze varování
a pokračujte, je-li to možné.
struct address_list
{
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é:
- z datagramu přečte prvních
sizeof sockaddr_un
bajtů,
- 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:
- vyhledejte odesílatele ve stromě (adresa
source
musí být
stejná jako adresa odesílatele datagramu; strom je seřazen
podle source
),
- 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 );
8 Spustitelné soubory
execve
getenv
setenv
, putenv
O_CLOEXEC
dup2
Přípravy:
shell
– spustit $SHELL -c něco
execp
– jako execvp
ale nastavit argv0 podle nalezené cesty
binex
– spustit soubor pokud to není skript (podle ELF hlavičky)
uexec
– spustit soubor pokud není set(gs)id?
withfile
– přesměruje zadaný soubor na stdin zadaného příkazu
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:
dup
– rozdělení pevných odkazů na samostatné soubory
dir
– práce s adresářovým stromem
dedup
– spojování identických souborů pevnými odkazy
httpc
– klient pro stažení souboru z webu
httpd
– jednoduchý webový server
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
- ve výsledném stromě ukazuje na každý obyčejný soubor právě
jeden odkaz, a to tak, že
- obsah souboru, na který ukazuje více odkazů, nakopíruje do
nového souboru a
- původní sdílený odkaz nahradí odkazem na tento nový soubor.
Soubory, na které vedl jediný odkaz již na začátku, se nesmí
touto procedurou nijak změnit. Selže-li některá operace, podstrom
musí zůstat v konzistentním stavu, zejména nesmí žádný odkaz
chybět. Je dovoleno, aby při násobném selhání systémových volání
zůstal ve stromě nějaký odkaz nebo soubor navíc.
Návratová hodnota:
- nezáporné číslo označuje úspěch a zároveň indikuje počet
nových souborů (i-uzlů), které bylo potřeba vytvořit,
-1
indikuje situaci, kdy došlo k chybě, ale strom je zcela
konzistentní (tzn. každý odkaz je buď v nedotčeném původním
stavu, nebo ukazuje na správně duplikovaný soubor),
-2
indikuje situaci, kdy došlo k chybě jak při samotné práci
procedury, tak při pokusu o uvedení stromu do konzistentního
stavu, a tedy ve stromě přebývají nějaké odkazy nebo soubory.
Systémové volání, které jednou selhalo, již se stejnými hodnotami
parametrů neopakujte – naopak předpokládejte, že každé
selhání
je trvalé (bez ohledu na konkrétní hodnotu
errno
).
int dup_links( int root_fd );
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:
name
– ukazatel na nulou zakončený řetězec se jménem
souboru;
type
– hodnota výčtového typu níže popisující typ souboru;
error
– hodnota 0
, nebo informace o nastalé chybě;
dir
– v případě, že se jedná o adresář, obsahuje ukazatel na
zřetězený seznam souborů, anebo NULL
, pokud je adresář
prázdný;
next
– ukazatel na další prvek ve stejném adresáři, anebo
NULL
, pokud je poslední.
Tato struktura reprezentuje jednu položku v adresářovém
podstromě. Položka je buď adresářem, nebo jiným souborem. Jelikož
se jedná o reprezentaci zřetězeným seznamem, struktura obsahuje
ukazatele na následníka a potomka (
next
a
dir
).
Atributy name
, dir
a next
jsou ukazatele na dynamicky
alokovanou paměť, kterou následně musí být možné uvolnit
zavoláním free
.
Strukturu nemůžete nijak měnit.
Výčtový typ file_type
popisuje pět typů souborů, které budeme
rozlišovat: adresář, obyčejný soubor, symbolický odkaz, socket
a vše ostatní.
enum file_type
{
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:
at
– popisovač adresáře, ve kterém hledat složku, nebo
konstanta AT_FDCWD
;
root_name
– název počátečního adresáře, který se má
prohledávat;
out
– výstupní ukazatel, do něhož má být uložen ukazatel
na alokovanou zřetězenou strukturu.
Její návratovou hodnotou bude:
0
– úspěch;
-1
– selhání pří přístupu k některému souboru či složce;
-2
– selhání při alokaci, což je kritická chyba, a výstupní
out
nechť je nastaven na NULL
.
Jestliže je návratová hodnota
-1
, u dotčených souborů bude
atribut
error
nastaven na odpovídající hodnotu proměnné
errno
. Jinak má být hodnota tohoto atributu
0
.
int tree_create( int at, const char *root_name, struct node **out );
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:
root_fd
je popisovač složky, která určuje podstrom, se
kterým bude pracovat,
attic_fd
je popisovač složky, která bude sloužit jako „půda“
a kde se vytvoří odkazy na všechny redundantní soubory (tzn.
ty, které byly nahrazeny tvrdým odkazem na jiný soubor).
Složka
attic_fd
nesmí být uvnitř podstromu, který začíná
složkou
root_fd
. Předpokládejte, že zadaný podstrom obsahuje
pouze složky a odkazy na obyčejné soubory (tzn. neobsahuje žádné
měkké odkazy, ani jiné speciální typy souborů).
Na každý soubor, který má totožný obsah (metadata nerozhodují)
jako některý jiný nalezený soubor, vytvořte ve složce attic_fd
odkaz, kterého jméno bude číslo i-uzlu. Nevytvářejte zde odkazy
na soubory, na které bude po skončení operace existovat odkaz ve
stromě root_fd
.
Selže-li nějaká operace, vstupní podstrom musí zůstat
v konzistentním stavu (jak vnitřně, tak vůči složce attic_fd
).
Povolen je pouze jeden typ nekorektního stavu, a to pouze
v případě selhání několika systémových volání: ve složce
attic_fd
může existovat přebytečný odkaz na soubor, který je
zároveň odkázán z podstromu root_fd
.
Bez ohledu na libovolná selhání musí každý odkaz ve stromě
root_fd
ukazovat na stejný obsah jako před provedením
procedury dedup
. Můžete předpokládat, že nikam do stromu
root_fd
ani do složky attic_fd
neprobíhá souběžný zápis.
Návratová hodnota nechť je 0
v případě úspěchu, -1
v případě,
že nastala chyba, ale vše je v korektním (i když nedokončeném)
stavu a konečně -2
došlo-li k situaci, kdy nebylo možné úplnou
konzistenci zajistit. Podobně jako v úkolu A také platí, že každé
selhání považujeme za trvalé (nelze jej zvrátit opakovaným
voláním se stejnými parametry).
int dedup( int root_fd, int attic_fd );
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:
- řá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
,
- hlavičky – každé pole začíná jménem, následované dvojtečkou a
textovou hodnotou, která pokračuje až do konce řádku,
- 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 );
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:
cd ≡ cd
,
{cesta} ≡ /
,
… ≡ ls -hl
.
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). 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.
9 Procesy
fork
waitpid
pipe
, socketpair
- kombinace
fork
+ exec
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:
- server je ta strana, která má reprezentovatelnou adresu –
známe-li tuto adresu, můžeme se k serveru připojit,
- 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). 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.
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.
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.
if ( close( client_fd ) == -1 )
warn( "closing connection" );
}
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):
comm_init
– podprogramu předáme dvojici otevřených
popisovačů a obdržíme ukazatel handle
, který budeme dále
používat pro komunikaci,
comm_read
, comm_write
mají stejné parametry jako systémová
volání read
a write
, ale místo popisovače souboru obdrží
ukazatel handle
vytvořený podprogramem comm_init
,
comm_fini
uvolní veškeré zdroje spojené s předaným
ukazatelem handle
(včetně asociovaných popisovačů).
Podprogram
comm_write
nesmí za žádných okolností blokovat –
není-li možné data předat systému, musí je interně uložit skrze
ukazatel
handle
a řízení vrátit volajícímu, jako by zápis
uspěl. Podprogram
comm_read
blokovat smí, ale pouze v situaci,
kdy jsou již všechna data z předešlých volání
comm_write
úspěšně odeslána.
Podprogram comm_init
vrátí v případě selhání nulový ukazatel,
všechny ostatní vrátí v případě úspěchu 0 a v případě neúspěchu
-1, a nastaví errno
.
void *comm_init( int read_fd, int write_fd );
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
pthread_create
- aktivní čekání
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
pthread_mutex
pthread_rwlock
pthread_cond
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:
- jak se pracuje s kostrami úloh,
- jak sdílet obrazovku (terminál) ve cvičení,
- jak se odevzdávají úkoly,
- kde najdete výsledky testů a jak je přečtete,
- kde najdete hodnocení kvality kódu (učitelské recenze),
- jak získáte kód pro vzájemné recenze.
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. 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í.
T.1.2 Stažení koster
Kostry naleznete ve studijních materiálech v ISu: Student
→
PB152
→ Studijní materály
→ Učební materiály
. Každá kapitola
má vlastní složku, pojmenovanou 00
(tento úvod a materiály
k nultému cvičení), 01
(první běžná kapitola), 02
, …, 12
.
Veškeré soubory stáhnete jednoduše tak, že na složku kliknete pravým
tlačítkem a vyberete možnost Stáhnout jako ZIP
. Stažený soubor
rozbalte a můžete řešit.
T.1.3 Odevzdání řešení
Vypracované příklady můžete odevzdat do odevzdávárny v ISu:
Student
→ PB152
→ Odevzdávárny
. Pro přípravy používejte
odpovídající složky s názvy 01
, …, 12
. Pro příklady ze sad pak
s1_a_csv
, atp. (složky začínající s1
pro první, s2
pro druhou
a s3
pro třetí sadu).
Soubor vložíte výběrem možnosti Soubor – nahrát
(první ikonka na
liště nad seznamem souborů). Tímto způsobem můžete najednou nahrát
souborů několik (například všechny přípravy z dané kapitoly). Vždy
se ujistěte, že vkládáte správnou verzi souboru (a že nemáte
v textovém editoru neuložené změny). Pozor! Všechny vložené
soubory se musí jmenovat stejně jako v kostrách, jinak nebudou
rozeznány (IS při vkládání automaticky předřadí Vaše UČO – to je
v pořádku, název souboru po vložení do ISu neměňte) .
O každém odevzdaném souboru (i nerozeznaném) se Vám v poznámkovém
bloku log
objeví záznam. Tento záznam i výsledky testu syntaxe by
se měl objevit do několika minut od odevzdání (nemáte-li ani po 15
minutách výsledky, napište prosím do diskusního fóra).
Archiv všech souborů, které jste úspěšně odevzdali, naleznete ve
složce Private
ve studijních materiálech (Student
→ PB152
→
Studijní materiály
→ Private
).
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).
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).
T.1.5 Recenze
Vám adresované recenze, podobně jako archiv odevzdaných souborů,
naleznete ve složce Private
ve studijních materiálech (Student
→
PB152
→ Studijní materiály
→ Private
). Shrnutí bodového zisku
za tyto recenze pak naleznete v poznámkovém bloku reviews
.
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
WSL – Windows Subsystem for Linux). Konkrétní příkaz (za xlogin
doplňte ten svůj):
$ ssh xlogin@aisa.fi.muni.cz
Program se zeptá na heslo: použijte to fakultní (to stejné, které
používáte k přihlášení na ostatní fakultní počítače, nebo např. ve
fadmin
-u nebo fakultním gitlab
-u).
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é:
- zdrojové kódy, které máte možnost recenzovat, a to do složky
~/pb152/to_review
,
- 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:
- přesuňte se do složky
~/pb152/to_review
a
- 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ě:
- přeloží Vaše řešení překladačem
gcc
,
- spustí přiložené testy,
- 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.
- 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ě.
- 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í.
- 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.
- 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í.
- 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?
- 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.
- 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í.
- Všechny entity ve zdrojovém kódu nesou anglická jména.
Angličtina je univerzální jazyk programátorů.
- 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.
- 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).
- 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).
- 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.
- 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
.
- 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).
- 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.
- 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
).
- 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
).
- 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ě.
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.
- Podobně jako jména entit, komentáře které jsou součástí kódu
píšeme anglicky.
- 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).
- 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.
- 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.
U.0.7 Formální úprava
TBD.