6 - Dynamická alokace a textové soubory

Cíle cvičení

  1. Dynamická alokace: malloc a realloc.
  2. Načítání vstupu ze souboru.
  3. Zpracování argumentů programu.
  4. Implementace funkčního a správného programu s ošetřením možných chybových stavů.
Cvičení na dynamickou alokaci pokračuje 7 - Dynamická alokace a struktury. Věnujte dostatek času pochopení základů a případně pokračujete na dalším cvičení.

Materiály

Úkoly

  • Implementujte program, který načte vstupní textový soubor, který následně vypíše v opačném pořadí řádků (varianta příkazu tac).
  • Implementujte načtení řádků s dynamickou alokací. Předpokládejte, že vstup má řádky zakončeny znakem '\n'.
  • Program vhodně dekomponujte a uvažujte tři chybové stavy.
    • 100 - Chyba načtení.
    • 101 - Chyba alokace dynamické paměti.
    • 102 - Chyba výstupu. Uvažujte, že tisk na standardní výstup může selhat.
  • Jako datovou strukturu uvažujte dynamicky alokované pole ukazatelů na textové řetězce reprezentující postupně načtené řádky. (Není nutné používat složený typ struct, byť to může být výhodné a ukážeme si v lab07.).
  • Při implementaci můžete využít ladění programem valgrind.
  • Před ukončením programu uvolněte alokovanou paměť.
Funkčnost programu si můžete ověřit vytvořením kontrolního výstupu programem tac a porovnáním s výstupem implementovaného programu. Např. ./main in.txt > my_out.txt; tac in.txt > out.txt; diff my_out.txt out.txt

Program dekomponujeme na následující funkce.

  • Načtení řádku, např. .
  • Načtení řádků, např. .
  • Tisk řádku a řádků, pokud nechceme například použít printf(“%s”), např. .
  • Uvolnění paměti např. .

Implementační postup můžeme zvolit například následující.

  1. Implementace načtení řádku znak-po-znaku s dynamickou alokací, pro zjednodušení budeme uvažovat FILE *fd = stdin;.
  2. Implementace načtení řádků.
  3. Vytištění výstupu.
  4. Přidání zpracování argumentu programu se jménem vstupního souboru.
  5. Rozlišení chyby vstupu a chyby alokace.

Načtení řádku s dynamickou alokací

  • Vytvoříme si funkce pro načtení textové řetězce ze souboru typu FILE* a jeho vytištění na stdout, funkce print.
  • Pro začátek použijme vstupní stdin, např. jako FILE *fd = stdin;.
  • Výpis řádku předpokládá, že na vstupu načteme konec řádku '\n', který je součástí textového řetězce.
  • Výpis je tisk jednotlivých znaků dokud nenarazíme na znak konce řetězce '\0'.
  • Naše funkce print předpokládá, že arugment je platný ukazatel na paměť zakončenou znakem '\0'. To musíme zajistit programově, korektní incializací, načtením vstupu a předáváním proměnných.

Příklad funkce print()

  • Pro zjednodušení zatím neuvažujeme rozlišení chyby vstupu a chyby alokace paměti.
  • Nicméně program ve funkci read korektně ošetřuje možné chybové stavy načtení a alokace paměti.
  • V případě chyby vrací funkce read hodnotu neplatného ukazatele NULL.

Příklad volání načtení vstupu a tisk

  • Ve funkci read() použijeme dynamickou alokaci (volání malloc) a realokaci (volání realloc) s kontrolou úspěšného přidělení paměti.
  • Můžeme použít různé strategie postupného zvětšování, přesto je postačující zvěšovat počáteční velikost dvakrát.
  • Možná úspora je po načtení řádku realokovat paměť řetězce na velikost (+1 pro '\0') voláním realloc.

Načtení řádků do pole ukazatelů na textové řetězce

  • Program zobecníme pro postupné načítání všech řádků s využitím implementované funkce read.
  • Využijeme k tomu funkce read_lines a print_lines.
  • Můžeme explicitně uvažovat počet načtených řádků, nebo využijeme podobného mechanismu jako null terminating string.

Příklad volání read_lines(), print_lines() a free_lines()

Zpracování argumentu programu se jménem vstupního programu

  • Program zobecníme pro načítání vstupu ze souboru, jehož jméno je prvním argumentem programu. Pokud není argument zadán, budeme uvažovat stdin nebo ohlásíme chybu.
  • Použijeme funkci fclose() pro explicitní uzavření souboru, jakmile načteme řádky ze souboru. Podobně jako volání free() v krátkých programech, učíme se programovat správně, přestože v malém, jednorázovém programu to nemusí být pragmaticky nutné, neboť soubory jsou uzavřeny po skončení programu.

Příklad zpracování argumentu programu

Přidání ošetření chybových stavů a reportování chyby návratovou hodnotou

  • Upravíme program pro ošetření chybových stavů. (Řešení je několik, můžete realizovat různě.)
  • Jednou z možností je přidat do funkcí načítání a výpisu ukazatel na proměnnou error. (podobně funguje errno ve std. knihovně C).
  • Přidáme výčtový typ, kde zavedeme též vlastní ERROR_OK jako EXIT_SUCCESS, abychom unifikovali identifikátory.

Příklad chybových kódu programu

  • Hlavičky funkcí načítání upravíme přidáním ukazatele na error. Při chybě vrací NULL, kterým nerozlišíme důvod chyby.

Defince prototypů funkcí

  • Výstup realizujeme vlastní funkcí print() a přidáme návratovou hodnotu.

Příklad hlavičky funkcí print a read

  • Opakování cyklu výpisu řádku podmíníme úspěšným výpisem, např.

Příklad funkce print_lines

  • Podobně výpis jednoho řádku

Jiný příklad tisku jednoho řádku

  • Nebo kratší verzí bez break s využitím ternárního operátor.

Kratší verze ternárním operátorem ? :

  • Podobně upravíme funkce načítání řádku read a read_lines.

Testování vstupu a chybových stavů

  • Otestování chyby dynamické alokace z důvodu nedostatečné paměti můžeme realizovat nastavením limitu process, např. limit, ulimit nebo limits, dle OS a použitého interpretu příkazů.
  • Lze omezit heap paměť, ale také přímo velikost virtuální paměti přidělené processu.
  • Pro otestování budeme potřebovat nějaký rozumě veliký vstupný soubor (příklad vygenerování 20MB souboru ). Nicméně i tak je heap paměť využívána standardní knihovnou a i 2MB mohou být málo pro incializaci knihovny libc.

limits  -v 2621440 ./main <in-long.txt; echo $?
ld-elf.so.1: /lib/libc.so.7: mmap of entire address space failed: Cannot allocate memory
1

  • V případě nastavení 5 MB již program pro vstup řádově desítky MB selže a korektně vrátí návratovou hodnotu 101.

limits -v 5242880 ./main <in-long.txt; echo $?
101

V Linuxu spíše použijete příkaz ulimit, který nastavuje aktuální sezení příkazového interpretu, což samo o sobě vyžaduje paměti více. Mimoto je nastavení paměti v kB a simulace malé paměti tak může být náročnější. Nicméně princip ošetření zůstává stále validní, ikdyž zrovna teď máme paměti dost, zásadní je princip, že uvažujeme takovou možnost a jsme na ni připraveni. Alternativně můžeme přijmout fakt, že chyba se může stát, ale když se stane, tak s tím nic neuděláme, což je typicky příklad vypisování zpráv na stderr, případně stdout, kde běžně nekontrolujeme, že se vypsalo vše co mělo. V takovém případě indikaci špatného chování máme, v případě přístupu do nepřidělené paměti může program stále běžet, ale nebude mít definované chování a uživatel o tom nebude informován.
  • Program dále můžeme rozšířit o vypsání chybové hlášky na stderr.

int main(int argc, char const *argv[])
{
  ...
  print_error(ret);
  return ret;
}

Například

V případě jediného místa ukončení programu, return v hlavní funkci main, je to relativně snadné. Samotné rozšíření programu o testování chybových stavů je spíše pracné, než náročné. Zdrojový kód se stane relativně méně přehledný. Nicméně při krátkých funkcí je rozšíření relativně rychlé a pokud jste implementovali vlastní řešení vhodně, mělo by se pohybovat mezi 5 až 10 minutami.
  • V programu můžeme vypsat ladící informaci o počtu načtených řádků, např. ve funkci read_lines
    fprintf(stderr, "DEBUG: n: %lu size: %lu\n", *n, size);
  • Při spuštění však zjistíme, že ani 5 MB není dost.

limits -v 5242880 ./main <in-long.txt; echo $?
101

  • Proto nastavíme více, např. 12 MB.

limits -v 12582912 ./main <in-long.txt; echo $?
...
DEBUG: n: 5921 size: 8192
DEBUG: n: 5922 size: 8192
101

  • Z čehož vidíme, že se nám podařilo načíst pouze 5922 řádků, což v našem případě odpovídá 742533 bytům, např. si necháme vypsat pouze prvních 5922 řádků programem head a spočítáme řádky, slova a znaky programem wc.

head -n 5922 in-long.txt |wc
  5922   95927  742533

Omezení paměti v Linuxu

Protože v Linuxu můžeme ještě narazit na chování OS a volání malloc, které spíše vrátí přidělenou paměť (z virtuálního adreseního prostoru, který je v případě 64-bit dostatečně veliký) nicméně k fyzickému přidělení paměti dojde až při zápisu do paměti, které v případě nedostatku paměti skončí ukončením programu. Toto tzv. overcommit chování je možné nastavit globálně sysctl vm.overcommit_memory = 2 a následně vm.overcommit_ratio a vm.overcommit_kbytes, což však může mít katastrofální důsledky pro běžící OS.
  • Alternativou, tak může být přidat do funkce read_lines ladící výpis o pokusu rozšíření paměti, např. následovně.
  • Následně program
  • kdy se nám podaří načíst 65536 řádků, následně malloc (nebo realloc) selže ve funkci read(), podařilo se nám tak načíst pouze část vstupu.
  • Program tak havaruje z důvodu alokace unsigned int a[n]; a omezené velikosti zásobníku.
  • Pokud paměť zvětšíme na 32 MB, program havaruje později, např.
Přidání uvedeného ladícího výstupu je příkladem ladění, které není tak přímočaré realizovat krokováním, neboť nás zajímá poslední hodnota, kdy program selže a krokování je v takovém případě neefektivní.

Ověření chování při chybě vstupu nebo výstupu

  • Ověření správného chování při chybě vstupu nebo výstupu je náročnější, o to více, že používáme stdin a stdout společně s dostatečnými zdroji. Spíše můžeme realizovat např. omezený diskový prostor při ukládání na disk nebo vzdálené síťové uložiště, což vyžaduje hlubší znalost používání OS.
  • V našem případě se tak prozatím spokojíme s inspekcí kódu.

Další úkoly k procvičení

  • Program rozšiřte o zpracování dvou argumentů program indikující vstupní a výstupní soubor.
  • Práci se soubory můžete realizovat funkcemi fopen, fclose a náhradou getchar a putchar funkcemi pro práci s proudem getc a putc.
  • Místo explicitního udržování početu řádků můžeme použít dynamicky alokované pole se “zarážkou” reprezentovanou položkou s hodnotou NULL, podobně jako je u textového řetězce null character, viz https://en.wikipedia.org/wiki/Null-terminated_string.
courses/b0b36prp/labs/lab06.txt · Last modified: 2024/09/22 15:55 by szadkrud