Rozšíření rozhraní mezi procesy a jádrem
Systémy Linux i FreeBSD podporují standardní unixové rozhraní specifikované normou POSIX. Toto rozhraní je velmi staré a v poslední době se začínají projevovat některé jeho nedostatky, proto bylo třeba jej rozšířit.
Čekání na více událostí
Při psaní síťového serveru vyvstává otázka, jaký zvolit model paralelního zpracování požadavků. Na klasickém Unixu server fungoval tak, že proces při přijetí každého požadavku udělal fork
a nechal dětský proces zpracovávat požadavek, zatímco rodičovský proces čekal na další požadavek. Tento model je značně neefektivní, neboť fork
je náročná operace a zpracovávání požadavků značně zpomaluje. Pokud server zpracovává paralelně veliké množství požadavků (např. ftp server může udržovat až několik tisíc spojení), zabírají informace spojené s procesy značné množství paměti. Další možností je použití threadů místo procesů. Toto řešení se skutečně používá, nicméně stále je zde problémem časté přepínání threadů, velká zátěž scheduleru a množství paměti, které thready konzumují (každý thread potřebuje zásobník v jádře a v userspace a položku v tabulce procesů).
Pokud nechceme, aby každý požadavek zpracovával zvláštní proces nebo thread, musíme mít způsob, jak jeden thread bude moci čekat na více událostí současně. V Unixu se k tomu používá funkce int select(int n, fd_set *rd, fd_set *wr, fd_set *except, struct timeval *timeout)
. n
je o jedna vyšší, než je číslo nejvyššího nastaveného handlu, rd
, wr
a except
jsou ukazatele na bitová pole specifikující, které handly nás zajímají. rd
jsou handly pro čtení, wr
handly pro zápis, except
jsou handly, na které přišla urgentní out-of-band data. Funkce select
zablokuje proces, dokud z některého handlu z množiny rd
nelze číst, dokud na některý handle z množiny wr
nejde zapisovat, dokud na některý handle z množiny except
nedošla out-of-band data nebo dokud nevyprší timeout
.
Problémem funkce select
je, že bitová pole mají omezenou délku — maximálně 1024 handlů. Funkci nelze použít k čekání na handly s vyššími čísly. V novějších systémech je tento problém odstraněn a jádro je již schopno pracovat s bitovými poli libovolné délky (délka se určí z parametru n
), nicméně makra v hlavičkových souborech na tuto změnu nejsou vždy připravena. Na některých systémech je možno před vložením hlavičkových souborů definovat FD_SETSIZE
na větší hodnotu a hlavičkové soubory pak samy definují větší strukturu fd_set
. Funguje to na FreeBSD, Solarisu, IRIXu, EMX, ale ne na GLibc.
V Unixu byla zavedena novější funkce int poll(struct pollfd *fds, unsigned long nfds, int timeout)
. Handly, na které má čekat, nejsou uloženy v bitovém poli, ale v poli struktur struct pollfd
. V každé pollfd
se nachází číslo handlu, bitová maska událostí, na něž se má čekat, a bitová maska, do níž jádro vrátí události, které nastaly.
Obě funkce select
i poll
jsou pro čekání na větší množství událostí zcela nepoužitelné — důvodem je jejich časová složitost, která je úměrná počtu událostí, na něž se čeká. Představíme-li si například ftp server s 5000 paralelními spojeními, je po vyřízení každého požadavku třeba zavolat funkci select
nebo poll
, která bude procházet 5000 handlů a kontrolovat, na kterém nějaká data jsou a na kterém ne. Procházení takového množství dat je pomalé, a proto je třeba zavést jiné rozhraní, ve kterém uživatelský program událost, na niž čeká, jednou zaregistruje a je upozorněn, až událost nastane, aniž by musel po vyřízení každého požadavku znovu registrovat všechny události, na které čeká.
Realtimové signály na Linuxu
Realtimové signály jsou standardizovány v normě POSIX. Původně nebyly určeny pro čekání na události. Jeden proces může jinému procesu poslat realtimový signál pomocí syscallu rt_sigqueueinfo
. K signálu je možno přidat nějakou informaci ve struktuře siginfo_t
. Cílový proces tuto informaci dostane jako parametr handleru signálu. Realtimový signál má tu vlastnost, že cílovému procesu dojde přesně tolikrát, kolikrát byl odeslán, pokaždé s jinou siginfo_t
, která byla předána syscallu rt_sigqueueinfo
(na rozdíl od obyčejných signálů, které dojdou jen jednou, pokud byly odeslány vícekrát dříve, než byl v cílovém procesu zavolán handler).
Realtimové signály je možno použít i pro čekání na události. Je známá věc, že pokud pomocí syscallu fcntl(h, F_SETFL, FASYNC)
nastavíme na handlu příznak FASYNC
, proces dostane signál SIGIO
, pokud z handlu je možno číst nebo do něj zapisovat. Pokud navíc pomocí fcntl(h, F_SETSIG, signal)
nastavíme číslo signálu jako realtimový signál, proces dostane realtimový signál, až bude možno z handlu číst nebo do něj zapisovat. Ve struktuře sigingo_t
, kterou handler signálu dostane, se objeví číslo handlu, který signál způsobil.
Realtimové signály vypadají krásně, nicméně reálné použití už tak pěkné není. Obyčejné signály se posílají pouhým nastavením bitu v masce signálů, a proto k poslání signálu není potřeba žádná paměť. Realtimové signály potřebují paměť na frontu signálů (neboť signál musí dojít přesně tolikrát, kolikrát byl odeslán) a struktury siginfo_t
. Aby paměť jádra nebyla zaplněna těmito strukturami, pokud si nějaký proces signály nevyzvedává, existuje v systému limit na maximální počet nevyřízených signálů držených v paměti. Je zde však mnohem horší problém — realtimový signál je často třeba poslat ze softwarového přerušení sítě (např. pokud došla nějaká data na socket). V softwarovém přerušení neexistuje kontext procesu, a proto se není možno zablokovat. Pokud se není možno zablokovat, není možno vždy alokovat paměť (neboť není možno čekat na swapper, než nějaké stránky odswapuje). Je možno alokovat paměť pouze s příznakem GFP_ATOMIC
, a taková alokace může kdykoli selhat. Pokud alokace selže, procesu není možno poslat realtimový signál, místo toho se mu pošle SIGIO
, k jehož poslání žádná paměť třeba není. Pokud jeden proces pošle druhému procesu realtimový signál pomocí
rt_sigqueueinfo
, může to také kdykoli selhat — buď proto, že se přeplní systémový limit, nebo proto, že selže GFP_ATOMIC
alokace (zde jsme sice v kontextu procesu, a proto by bylo možno alokovat pomocí GFP_KERNEL
a čekat, než bude volná paměť, ale nedělá se to, protože poslání signálu je obsluhováno jednou rutinou, která může být volána jak z kontextu procesu, tak z kontextu přerušení). Pokud alokace při rt_sigqueueinfo
selže, není posláno nic, návratová hodnota značí chybu a errno
EAGAIN
.
Závěr: Pro čekání na události je použití realtimových signálů dost krkolomné — signál může být kdykoli ztracen — jediné rozumné použití je odchytit signál SIGIO
a v jeho handleru pomocí select
nebo poll
zkontrolovat všechny handly, aby bylo možno vyřídit i události, od nichž byl realtimový signál ztracen.
kqueue na FreeBSD
Vývojáři FreeBSD neimplementovali realtimové signály, ale navrhli a implementovali vlastní rozhraní nazvané kqueue. kqueue je specifické pro FreeBSD a není standardizováno (na rozdíl od realtimových signálů). Je implementováno na FreeBSD 4 i 5.
Syscall int kqueue(void)
vyrobí frontu a vrátí handle této fronty. Syscall int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout)
umožňuje do fronty přidávat nebo z ní ubírat události, na které je třeba čekat, nebo vybírat seznam událostí, které nastaly. kq
je handle fronty vrácený syscallem kqueue
. changelist
je seznam událostí, na které se má začít nebo přestat čekat (zda se má začít, nebo přestat čekat, je rozhodnuto příznakem ve struktuře struct kevent
). nchanges
je velikost tohoto seznamu, pokud je nulová, žádné události se přidávat ani ubírat nebudou. eventlist
je místo v paměti o velikosti nevents
, kam jádro uloží seznam událostí, které byly (při současném volání nebo někdy dříve) registrovány a které nastaly. Pokud je nevents
nulové, žádné události sem uloženy nebudou. Příznakem v struct kevent
je možno určit, zda bude událost automaticky odebrána, pokud nastala a byla vrácena v eventlist
, nebo se na ni bude čekat dál a bude moci být vrácena, až znovu nastane.
Události, na které je možno čekat, jsou mnoha druhů — kromě standardní možnosti číst nebo zapisovat do handlu je tu i možnost čekat na dokončení asynchronního IO (viz příští článek), změnu libovolného souboru nebo adresáře, ukončení, forknutí nebo execnutí jiného procesu nebo přijetí signálu.
Každá kqueue je implementována jako fronta událostí knote
, které nastaly. Pokud proces registruje událost, vyrobí se struktura knote
, která obsahuje pointer na kqueue, k níž náleží, a tato struktura se přiřadí do seznamu na příslušném handlu, procesu, souboru či jiné struktuře, na jejíž změnu stavu se čeká. Až událost nastane, jsou struktury knote
ze seznamu zařazeny do odpovídajících kqueue, odkud si je uživatelské procesy mohou vybrat. Nedochází k problému, jaký má Linux s realtimovými signály — když událost nastane, tak je pouze struktura vybrána ze seznamu a uložena do fronty, k tomu není potřeba žádná paměť, takže nemůže nastat problém s jejím nedostatkem. Struktura knote
je alokována při volání kevent
v kontextu procesu, a tam je možno alokovat paměť vždy.
Závěr: kqueue je velmi dobrá implementace, která bohužel není standardizována. Nemá problémy s přeplněním fronty a ztrácením událostí. Další její velikou výhodou je možnost čekat nejen na čtení nebo zápis do handlu, ale i na množství dalších událostí. kqueue je možno snadno rozšířit o nové události.
epoll na Linuxu 2.6
Linux 2.6 přichází s novým rozhraním epoll
. Aplikační program vytvoří pomocí syscallu epoll_create
handle, který bude dále používat k čekání na události (je to ekvivalent syscallu kqueue
na FreeBSD). Události je možno registrovat pomocí int epoll_ctl(int efpd, int op, int fd, unsigned events)
. První parametr je handle, který vrátil syscall epoll_create
, druhý parametr je operace EP_CTL_ADD
, EP_CTL_DEL
nebo EP_CTL_MOD
pro přidání, odebrání nebo modifikování události. Třetí parametr je handle, na kterém má být událost sledována, a čtvrtý parametr je typ události (používají se stejné hodnoty jako u syscallu poll
). Události je možno vybírat pomocí syscallu int epoll_wait(int epfd, struct pollfd *events, int maxevents, int timeout)
. První parametr je handle z epoll_create
, druhý parametr je pole, do kterého budou události uloženy, třetí parametr je velikost pole a čtvrtý parametr je čas, po který bude funkce čekat, pokud žádné události nenastaly.
Závěr: Rozhraní epoll
je podobné kqueue
z FreeBSD, nenabízí však takové možnosti — pomocí epoll
je možno čekat pouze na čtení nebo zápis do handlu a nikoli na dokončení asynchronního IO, ukončení procesu, signál nebo na změnu adresáře (pro čekání na změnu adresáře je možno použít syscall fcntl
s parametrem F_NOTIFY
). Navzdory tomu je epoll
výrazně lepší než realtimové signály na předchozích verzích Linuxu.