Operační systém UNIX a jazyk C

Jan Brodský
Luděk Skočovský

SNTL Nakladatelství technické literatury Praha 1989, ISBN 80-03-00049-1

5. Programovací jazyk C


5.9 Knihovny

V etapě sestavování se k programu dodají funkce z knihoven. Jaké funkce to jsou? Zatím jsme např. nepopsali způsob jak v jazyku C psát vstupní a výstupní operace (v příkladech jsme zatím používali pouze funkci printf formátovaného výstupu na terminál).

Vstupní a výstupní operace nejsou totiž součástí jazyka C a programují se jako volání funkcí (dostupných v knihovně, která přísluší použitému kompilátoru jazyka C a operačnímu systému). Protože kompilátory jazyka C existují pro počítače odlišných architektur, je přenositelnost programů napsaných v jazyku C závislá na způsobu programování vstupů a výstupů. Historicky se proto vyvinula standardní knihovna vstupů a výstupů, jako množina funkcí použitelných v jazyku C a zabezpečujících propojení programu s okolím počítače. Protože jazyk C vznikal současně s Unixem, navazují funkce této standardní knihovny na souborovou strukturu Unixu. V ostatních operačních systémech se mohou implementace knihovny lišit podle toho, jak se jejich systém souborů liší od systému souborů Unixu.

Unix udržuje knihovny funkcí pro sestavující program v adresáři /lib (obr. 5.4).

[OBR. 5.4]

Knihovna /lib/libc.a obsahuje funkce, zprostředkující volání jádra; jim je věnována následující kapitola. Dále obsahuje standardní funkce vstupu a výstupu, o kterých pojednáváme v následujících odstavcích tohoto článku. To se týká i knihovny /lib/curses, která obsahuje funkce jednotné obsluhy různých typů terminálů. Knihovna /lib/libm.a obsahuje základní matematické funkce, např. sin a exp, knihovna /lib/libF77 obsahuje funkce podpory fortranského kompilátoru, ale těmi se nebudeme zabývat. Rozhodujícím zdrojem informací je totiž vždy příručka knihoven toho systému, který používáte.

5.9.1 Knihovny a vkládané soubory

Každý program, který použije některou ze standardních funkcí vstupu nebo výstupu, musí obsahovat příkaz makroprocesoru

#include < stdio.h >
čímž se zajistí, že bude mít k dispozici deklarace maker a externích funkcí knihovny standardního vstupu a výstupu. Soubor < stdio.h > je totiž obsahuje. Není to přitom jediný takový soubor. V adresáři /usr/include je jich řada. U popisu každé funkce je v příručce programátora uvedeno, který hlavičkový soubor musí programátor zahrnout příkazem #include, aby funkci mohl použít.

Seznam téměř všech funkcí standardní knihovny je uveden v příloze D.


Pozn.: Sestavující program Unixu automaticky prohledává /lib/libc.a; pro volání jádra (kap. 6) tedy není třeba do zdrojového souboru vkládat žádný soubor s jejich deklarací. Kompilátor totiž identifikátory funkcí, které jsou v programu volány, ale nejsou v něm definovány, prohlásí za extern. V etapě sestavení jsou pak k programu připojeny funkce, které obsahují externí definice použitých identifikátorů.

5.9.2 Standardní knihovna vstupů a výstupů

Jak již bylo řečeno, standardní knihovna vstupů a výstupů je silně ovlivněna Unixem. Soubory v Unixu lze ovládat přímo z úrovně volání jádra (funkcemi jako jsou open, close, read, write, lseek, ...). Programy se na otevřené soubory odvolávají deskriptorem souboru, tj. celým číslem, které jádro přiděluje v okamžiku otevírání souboru. Ukazovátko na další čtenou, resp. zapisovanou slabiku udržuje jádro, a program může toto ukazovátko přesunout funkcí lseek. Operace read, resp. write přenáší data mezi adresovým prostorem programu a jádra. Jádro přitom využívá systémové vyrovnávací paměti, to však nemusí platit u jiných operačních systémů.

Standardní knihovna vstupů a výstupů jazyka C je další vrstvou nad jádrem. Soubor je reprezentován ukazatelem na strukturu typu FILE, deklarovanou v souboru < stdio.h >. Tato struktura se naplní v průběhu otevírání souboru funkcí fopen a je dalšími funkcemi využívána. Pro jednoduchost budeme soubor takto reprezentovaný nazývat tokem. Ukazovátko na další čtenou, resp. zapisovanou slabiku toku uchovávají standardní funkce v příslušné struktuře typu FILE a lze ho přesunout funkcí fseek. Datové operace (fread, fwrite, ...) se odehrávají mezi objekty programu a vyrovnávacími pamětmi ve standardních funkcích, tedy v adresovém prostoru programu.

Využívaná velikost vyrovnávacích pamětí ve standardních funkcích se dá nastavit funkcí setbuf, předtím je však vhodné vyrovnávací paměti vyprázdnit funkcemi fflush, resp. fclose (pak je vyprázdnění vyrovnávacích pamětí vedlejším efektem). Je-li velikost vyrovnávacích pamětí nastavena na nulu, probíhají přesuny dat přímo mezi adresovými prostory procesu a jádra, stejně jako na úrovni volání jádra.

Funkce standardní knihovny využívají pro svou činnost ovládání souborů na úrovni jádra operačního systému (např. fopen vede na volání jádra open, fwrite vede, je-li vyrovnávací paměť plná, na write apod.). Principiálně je možné v jednom programu ovládat týž soubor z obou úrovní, avšak nedoporučuje se to, protože dochází k nepředvídatelným výsledkům.

Standardní knihovna vstupů a výstupů definuje tři speciální toky. Jsou to stdin, stdout a stderr a odpovídají standardním souborům procesu (s deskritptory 0, 1 a 2 - čl. 3.3) včetně možnosti přesměrování. Jádro každému procesu zajistí otevření těchto souborů, takže program procesu nemusí jejich otevření vyžadovat. Definice speciálních toků bude jasnější z ukázky souboru < stdio.h >:

/* stdio.h ukazkovy */
#define BUFSIZ 512
#define NFILE 20

extern struct  iobuf {
	int count; /* pocet znaku ve vyrovnavaci pameti */
	char *ptr; /* ukazatel na dalsi znak ve vyrovnavaci pameti */
	char fd; /* deskriptor souboru */
	char *base; /* ukazovatko souboru */
} iob [NFILE];

#define FILE struct iobuf
#define NULL 0
#define EOF (-1)
#define stdin (&iob[0])
#define stdout (&iob[1])
#define stderr (&iob[2])

#define getc(p) (--(p)->count >= 0 ? *(p)->ptr++ &0377 : filbuf(p))
#define getchar() getc(stdin)
	...
extern FILE *fopen();
extern char *fgets();

Budete-li někdy číst skutečný soubor < stdio.h >, nenajdete tam deklarace těch funkcí, které vracejí int a nemusí tedy být v programech deklarovány (např. printf).

5.9.3 Činnost jednotlivých funkcí

Tok můžeme otevřít třemi způsoby. Funkce fopen přiřadí jménu souboru první volnou strukturu typu FILE a vrátí ukazatel na tuto strukturu. Funkce freopen přiřadí již obsazené struktuře typu FILE nový význam tím, že soubor s ní spojený uzavře a do ní vloží údaje o nově otevíraném souboru. Konečně fdopen přiřadí první volnou strukturu FILE souboru již otevřenému voláním jádra open, tj. se známým deskriptorem souboru. Uzavření toku zařídí funce fclose.

Soubor může být otevřen pro různé režimy práce:

RežimVýznamČinnost, když soubor
existujeneexistuje
"r"jen čteníchyba
"r+"čtení a zápischyba
"w"jen zápiszrušení starého obsahuvytvoření nového souboru
"w+"zápis a čtenízrušení starého obsahuvytvoření nového souboru
"a"jen zápis za konec vytvoření nového souboru
"a+"zápis za konec a čtení vytvoření nového souboru.

Čtení, resp. zápis lze provádět po znacích (funkce fgetc, getc, getchar pro čtení, funkce fputc, putc, putchar pro zápis); zajímavou je funkce ungetc, která umožní vrátit již přečtený znak zpět do vyrovnávací paměti.

Další možností je formátované čtení (funkce scanf, fscanf, sscanf), resp. formátový zápis (funkce printf, fprinf, sprintf). Mnemotechnika názvů napovídá, že scanf a printf pracují se standardními soubory stdin a stdout, fscanf a fprintf s libovolnými soubory a konečně sscanf a sprintf provádějí formátové konverze z řetězce a do něj (konverze typu paměť - paměť).

Logickým důsledkem posledního typu konverze je možnost provádět čtení (funkce gets, fgets) a zápis (funkce puts, fputs) řetězců.

Jsou rovněž k dispozici operace binárního čtení a zápisu (funkce fread, fwrite), které přenášejí část paměti, zadanou počtem objektů a adresou prvního z nich (jednorozměrné pole objektů).

Datové operace obvykle probíhají sekvenčně, tj. od současné pozice ukazovátka. Je však možné ukazovátko nastavit funkcí fseek; funkce ftell naopak vrátí současnou pozici ukazovátka.

Funkce fflush vyprázdní vyrovnávací paměti příslušného souboru, funkce foef vrátí nenulovou hodnotu, je-li již ukazovátko nastaveno na konec toku, jinak nulu.

Funkce ferror vrátí číslo chyby, ke které došlo při práci s tokem, chyba přitom zůstane nastavena dokud se nevymaže voláním funkce clearerr nebo dokud není tok uzavřen.

Funkce rewind přesune ukazovátko na začátek toku (speciální použití fseek).

Jako příklad si uveďme program vypisující obsah toku na stdout se současnou změnou malých písmen na velká:

/* program velka.c */
#include 

void main(argc, argv)
int argc; char *argv[];

{ FILE *text;
	if(argc!=2 || (text=fopen(*++argv, "r")==NULL)){
		fprintf(stderr, "chybny pocet argumentu\
nebo soubor nelze otevrit\n");
		exit(1);
	}
	else {
		register c;
		while((c=getc(text))!=EOF)
			putchar(islower(c)?toupper(c):c);
		fclose(text);
		exit(0);
	}
}

Funkce islower a toupper jsou ve skutečnosti makra definovaná v souboru < ctype.h >:

#define _U 01
#define _L 02
   ...
extern char _ctype[];
#define islower(c) ((_ctype +1)[c] & _L)
#define toupper(c) ((c) - 'a' + 'A')
   ...

Makra využívají znakového pole c_type, v němž jsou pro každý znak ASCII uloženy bitové příznaky vypovídající o jeho vlastnostech (např. _L o tom, že jde o malé písmeno). Je-li příznak nastaven, makro islower vrátí nenulovou hodnotu (true), jinak nulu (false).

5.9.4 Formáty

Funkcím formátovaného čtení, resp. zápisu (scanf, ..., resp. printf, ...) se jako první argument zadává formát, podle něhož se má provést formátovaná konverze ze znakového vyjádření na binární (čtení), resp. naopak (zápis). Formát se zadává jako řetězec (ať již jako konstanta nebo dynamicky naplňované pole znaků). Význačnou roli ve formátovém řetězci má znak %, uvozující každou konverzi specifikaci, jichž může být ve formátu více.

Znaky, které nejsou součástí konverzní specifikace, jsou pro zápis považovány za součást výstupu. Toho jsme již mnohokrát využili, např.

printf("Dobry den!\n");

Pro čtení jsou oddělovací mezery (tabulátory) vynechány, jiné znaky musí přesně odpovídat čteným znakům.

Konverzní specifikace se udávají postupně zleva doprava. Specifikují vlastnosti znakového obrazu převáděné hodnoty (např. znak + zajistí, že číslo bude vždy předcházeno znaménkem, znak # zajistí nulu jako první znak reprezentace oktálového čísla) a typ převáděné hodnoty. Typ převáděné hodnoty se určí konverzním písmenem (které může být doplněno dalším písmenem l pro long. Možnosti uvádí přehled:

KonverzeTyp objektuZnaková reprezentace
%ccharjeden znak
%schar[]řetězec znaků, končící \n
%d (ld)int (long)celé číslo se znaménkem dekadicky
%u (lu)unsigned (long)celé číslo bez znaménka dekadicky
%o (lo)unsigned (long)celé číslo bez znaménka oktalově
%x (lx)unsigned (long)celé číslo bez znaménka hexadecimálně
%fdoublečíslo tvaru +-xxx.xxxxxx
%edoublečíslo s exponentem tvaru +-x.xxxxx e+-xx
%gdoublekratší z formátů %f a %e
%%znak %

Požadovaná šířka znakové reprezentace se udává počtem znaků uvedeným těsně před konverzním písmenem. Tak např.

printf("%2d / %3s / %4d", 28, "JUL", 1987);
vypíše na obrazovce terminálu
28 /  JUL / 1987
a naopak, čte-li se z klávesnice terminálu řádek textu
012 3456 78
voláním funkce
scanf("%3o %4s %d", &i, s, &k);
(za předpokladu, že platí deklarace int i,k; char s[];) budou mít objekty hodnoty
i = 012,
s[] = "3456"  a
k = 78.

5.9.5 Knihovna curses

Tato knihovna slouží k ovládání terminálových obrazovek. Využívá přitom popisu vlastností terminálů v souboru /etc/termcap, kap. 3. Programy, které využívají funkce z knihovny curses, tedy nezávisí na typu použitého terminálu. Rozdíly v terminálech vyrovnají právě funkce z této knihovny.

Základním pojmem je okno (angl. window). Nejde však o grafická okna osobních počítačů, ale pouze o část obrazovky textového terminálu. Okno je chápáno jako interní reprezentace potenciálního obrazu příslušné části obrazovky. Zobrazeno však může být něco jiného.

Okno je určeno souřadnicemi levého horního rohu a svou velikostí. Základním oknem je curscr (current screen), které reprezentuje to, co je skutečně vidět na obrazovce. Změny se odehrávají na implicitním okně, stdscr. Chceme-li obnovit, změnit i obrazovku terminálu, provedeme funkci refresh(). Obdobně, chceme-li promítnout okno na obrazovku, použijeme funkci wrefresh.

Stručný popis jednotlivých funkcí knihovny curses je uveden v příloze D. Zde na příkladě programu list.c ukážeme použití některých z nich. Program list dovoluje prohlížet txtové soubory po stránkách a pohybovat se v souboru dopředu i dozadu. Obrazovku terminálu rozděluje na několik pomocných oken, uživateli v horním řádku nabízí možné jednoznakové příkazy (počáteční písmena nápovědy). Na příkaz c nebo C vypíše roletový jídelníček tak, že obsah okna pomoc přepíše přes stdscr:

/* list.c aplikace curses */

#include < curses.h >
WINDOW *jmenos, *cradku, *pomoc, *obsah;
char radek[80];
int n=1, c, d, p=1;
FILE *soubor;
long pozice[1000];

int
stranka()
{
register int i;
if feof(soubor)
	return(FALSE);
pozice[p] = ftell(soubor);
wclear(cradku);
wprintw(cradku, "%d", n);
overwrite(cradku, stdscr);
wclear(obsah);
for(i=0; i < LINES-3; i++){
	fgets(radek, 80, soubor);
	if feof(soubor){
		overwrite(obsah, stdscr);
		return(FALSE);
	}
	waddstr(obsah, radek);
} /* end of for */
overwrite(obsah, stdscr);
return(TRUE);
} /* end of stranka */

main(ac, av)
int ac;
char *av[];

{
soubor = (ac == 1) ? NULL : fopen(av[1], "r");
initscr();
clear();
noecho();
nonl()
crmode();
jmenos = newwin(1,30,0,10);
cradku = newwin(1,10,0,60);
obsah = newwin(LINES-2, COLS, 2, 0);
pomoc = newwin(4,10,2,70);
mvaddstr(0,0,"soubor:");
mvaddstr(0,50,"radek:");
mvaddstr(0,70,"Cinnost:");

if(soubor != NULL){
	waddstr(jmenos, av[1]);
	overwrite(jmenos, stdscr);
}
mvwaddstr(pomoc, 0, 0, "Soubor");
mvwaddstr(pomoc, 1, 0, "vPred");
mvwaddstr(pomoc, 2, 0, "vZad");
mvwaddstr(pomoc, 3, 0, "Konec");
c = (soubor == NULL) ? 's' : 'p';

for(;;){
	switch(c){
		case 'c':
		case 'C':
			overwrite(pomoc, stdscr);
			break;
		case 's':
		case 'S':
			if(soubor != NULL){
				fclose(soubor);
				wclear(cradku);
				wclear(jmenos);
				wclear(obsah);
				overwrite(jmenos, stdscr);
				overwrite(cradku, stdscr);
				overwrite(obsah, stdscr);
			}

			move(0,10);
			refresh();
			nl();
			echo();
			wgetstr(jmenos, radek);
			overwrite(jmenos, stdscr);
			noecho();
			nonl();
			soubor = fopen(radek, "r");
			n = 1;
			p = 1;

		case 'p':
		case 'P':
			if(stranka()){
				p++;
				n+=LINES-3;
			}
			break;

		case 'z':
		case 'Z':
			switch(p){
				case 1: 
					d=0;
					break;
				case 2: 
					d=1;
					break;
				default:
					d=feof(soubor) ? 1 : 2;
			}
			p -= d;
			n -= d*(LINES-3);
			fseek(soubor, pozice[p], 0);
			c = 'p';
			continue;
		case 'k':
		case 'K':
			mvcur(0, COLS-1, LINES-1, 0);
			endwin();
			exit(0);
} /* end of switch */

move(0,0);
refresh();
c = getch();

} /* end of for */

} /* end of main */