Hlavní navigace

Porovnání systémů Linux a FreeBSD (15)

11. 3. 2004
Doba čtení: 8 minut

Sdílet

V závěrečném dílu seriálu se podíváme na implementace asynchronního IO, a to nejen u našich dvou systémů, ale i u několika dalších.

Asynchronní IO

Při implementaci jednothreadového serveru vznikne další problém — pokud proces čte nějaký soubor a zablokuje se při čekání na dokončení diskové operace, nemůže přitom vyřizovat žádné další požadavky. Bylo by vhodné nějakým způsobem využít čas, kdy disk čte nebo zapisuje data. K tomu je potřeba tzv. asynchron­ní IO. Při použití asynchronního IO proces provede syscall pro čtení nebo zápis dat, tento syscall se okamžitě vrátí zpět do procesu a samotné čtení nebo zápis je prováděno paralelně při běhu procesu v userspace. Proces se pak nějakým způsobem dozví, že operace čtení nebo zápisu skončila.

Existuje standard POSIX async IO. Proces pošle asynchronní požadavek pomocí syscallů int aio_read(struct aiocb *iocb) nebo int aio_write(struct aiocb *iocb). struct aiocb obsahuje obvyklé parametry, jako je handle, adresa v paměti a délka. aiocb obsahuje také místo, kam jádro vrátí návratový kód operace. Funkce aio_read a aio_write pošlou požadavek na čtení nebo zápis jádru a okamžitě se vrátí. Pomocí int aio_return(struct aiocb *iocb) je možno zjistit návratový kód (pokud operace ještě nedoběhla, funkce vrátí chybu EINPROGRESS). Pomocí int aio_suspend(struct aiocb *iocbs[], int n_iocbs, struct timespec *timeout) je možno čekat, dokud některý z asynchronních požadavků určených v poli neskončí nebo než vyprší timeout. Probíhající asynchronní operaci je možno zastavit pomocí syscallu  aio_cancel.

POSIX async IO má značné nedostatky (je vidět, že je navrhovala standardizační komise a ne programátoři), není možno současně čekat na možnost čtení nebo zápisu do handlu (jako při select nebo poll) a na dokončení asynchronní operace. Dalším nedostatkem je syscall aio_suspend, který prochází všechny asynchronní požadavky. Složitost je úměrná počtu požadavků, takže tu vyvstává stejný problém jako u  select a poll.

FreeBSD 4 i 5 má POSIX async IO (je nutno povolit VFS_AIO v konfiguračním souboru při kompilaci jádra; komentář u této volby varuje, že to není moc bezpečné). Většina IO subsystémů v jádře je psaná synchronně a očekává, že budou běžet v kontextu procesu. Pro korektní implementaci async IO by bylo třeba všechny tyto subsystémy kompletně přepsat. Aby je vývojáři jádra nemuseli přepisovat, udělali následující řešení — jádro spustí několik kernel threadů, z nichž každý může provádět jeden asynchronní požadavek. Po skončení vykonávání požadavku thread čeká na další.

FreeBSD přichází s rozšířením rozhraní — je zde nová funkce int aio_waitcomplete (struct aiocb **iocbp, struct timeval *timeout), která počká na první dokončený požadavek a pointer na něj uloží na danou adresu. Tím se zabraňuje nutnosti specifikovat a procházet všechny požadavky u  aio_suspend. Na FreeBSD je také možno na dokončení asynchronního IO čekat pomocí kqueue.

Linux 2.4 nemá asynchronní IO. Knihovna GNU libc umí emulovat POSIX async IO pomocí syscallu clone— vyrobí nový thread a ten pak nechá provést požadavek. Výroba threadu je náročná operace, proto to není moc rychlé. V jádře Linuxu 2.6 existují nové syscally pro práci s async IO:

  • long io_setup (unsigned nr_events, aio_context_t *ctxp)  — vytvoří kontext, který bude moct vykonávat maximálně nr_events požadavků současně.
  • long io_destroy (aio_context_t ctx)  — zruší kontext.
  • long io_submit (aio_context_t ctx_id, long nr, struct iocb **iocbpp)  — pošle několik požadavků do kontextu.
  • long io_cancel (aio_context_t ctx_id, struct iocb *iocb, struct io_event *result)  — zruší požadavek.
  • long io_getevents (aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout)  — počká, než bude dokončeno aspoň min_nr požadavků a vrátí maximálně nr požadavků v poli.

Možné operace jsou IOCB_CMD_PREAD a IOCB_CMD_PWRITE. Teoreticky je možno provádět ještě fsync a fdatasync pomocí IOCB_CMD_FSYNC a IOCB_CMD_FDSYNC, nicméně žádný filesystém je nepodporuje. Asi se předpokládá, že jednou bude možno klasické aio_ funkce emulovat pomocí těchto syscallů, nicméně současná glibc to nedělá.

Bohužel asynchronní IO na Linuxu 2.6 je silně neúplné — na méně obvyklých filesystémech (např. FAT) vrací syscall io_submit EINVAL, na běžných filesystémech funguje asynchronně pouze, pokud byl handle otevřen s  O_DIRECT nebo pokud se jedná o blokové zařízení. Bez O_DIRECT syscall sice taky projde, nicméně dělá synchronní blokující IO. Rovněž na socketech se zablokuje jako obyčejné read a write. I s  O_DIRECT se navíc syscall blokuje po krátkou chvíli, kdy hledá k danému offsetu v souboru jeho mapovaní na blok na disku.

Nevytváří se žádný kernel thread na obsluhu asynchronních požadavků. Až bude naprogramováno async IO i pro ne-direct soubory a pro sockety (asi v 2.7), bude to implementace celkem efektivní. V současném stavu se to hodí na databáze (neboť ty pracují s  O_DIRECT a cachování si dělají samy), ale na servery ne, tam je cache potřeba. Na servery se bohužel nedá moc použít ani POSIX async IO, neboť tam chybí asynchronní open — na serveru pracujícím se spoustou malých souborů je čas otevření souboru srovnatelný nebo doknce větší než čas čtení souboru.

Řešení problému čekání na události na jiných systémech

Zde se krátce zmíním o tom, jak byl problém čekání na události řešen na jiných operačních systémech.

Většina komerčních Unixů (Solaris, IRIX, AIX, Digital Unix) mají asynchronní IO podle normy POSIX. Zpravidla je implementováno pomocí několika kernel threadů.

Na Solarisu může proces otevřít /dev/poll a tím získá handle, který je funkčně podobný kqueue. Je možno na něm registrovat události nebo vybírat z těch, které nastaly. Množství událostí není tak velké jako u kqueue — u /dev/poll je možno registrovat jen možnost čtení nebo zápisu na nějaký handle.

Systém VMS používá AST neboli Asynchronous system trap. Kód procesu může kdykoli poslat na svůj vlastní ring nebo na ring s nižší úrovní privilegovanosti asynchronous system trap. AST je pointer na funkci a parametr, který této funkci bude předán. Až se systém dostane na úroveň privilegovanosti, na kterou bylo AST posláno, a pokud není vykonávání AST zamaskováno, je funkce s daným parametrem zavolána (popis vypadá poněkud složitě — zjednodušeně to asi znamená tohle: jádro může poslat AST samo sobě a tato AST se ihned vykoná. Jádro může poslat AST uživatelskému procesu a tato AST se vykoná, až proces opustí jádro a vrátí se do userspace (je to obdoba unixového signálu). Proces může poslat AST sám sobě a tato AST se ihned vykoná. Ve skutečnosti je to trochu složitější, neboť VMS má celkem čtyři ringy — dva pro jádro a dva pro procesy). Většina syscallů VMS má dvě verze — blokující verzi, poznáme ji tím, že název syscallu končí písmenem „W”, a neblokující verzi, se stejným názvem, ale bez „W”. Blokující verze syscallu počká, než se syscall dokončí, a pak se vrátí. Neblokující verze jako parametr dostane AST, vrátí se ihned a zavolá danou AST, až byl syscall dokončen. Ukážeme si to na syscallu sys$qio, který se používá ke vstupu z handle nebo k výstupu na něj. Prototyp syscallu je int sys$qio(unsigned event_flag, unsigned short handle, unsigned function, struct _iosb *iosb, void (*astaddr)(__int64 param), __int64 astparam, další parametry specifické pro dané zařízení a funkci...). První parametr je event flag, který se má nastavit (popisem event flagů se zde nebudu zabývat — viz manuál k VMS), druhý parametr je handle, třetí parametr je kód funkce, která se má provést, čtvrtý parametr je pointer na místo v paměti, kam se uloží návratová hodnota a počet přenesených bytů, pátý parametr je funkce, která bude zavolána, až přenos skončí, a šestý parametr je hodnota, která bude předána této funkci. Stejně tak v systému existuje funkce sys$qiow, která má shodné parametry s  sys$qio, ale počká a vrátí se, až bude operace dokončena. Pomocí těchto asynchronních funkcí je možno jednoduše naprogramovat jednothreadový server, který bude paralelně zpracovávat větší množství požadavků. AST řeší jak problém současného čekání na větší množství událostí, tak asynchronní IO.

Systém Windows NT má funkce WaitForSingleObject a WaitForMultipleObjects, které jsou podobné jako unixové select nebo poll. Pomocí nich je možno čekat, než se objekt dostane do signálovaného stavu. Objektem může být nejen soubor, ale i proces (v takovém případě funkce počká, než proces doběhne) nebo mutex, semafor či čekací fronta.

Async IO se na Windows NT dělá pomocí funkcí ReadFile a WriteFile s nenulovým parametrem overlapped. V něm je specifikována struktura, do které budou umístěny informace o skončení operace. Na dokončení operace je možno čekat pomocí WaitForSingleObject nebo WaitForMultipleObjects s parametrem handlu daného souboru. Má to vadu — pokud více procesů dělá IO na jednom souboru, není možno rozpoznat, které IO bylo dokončeno a způsobilo probuzení čekání.

Byly zavedeny funkce ReadFileEx a WriteFileEx umožňující specifikovat funkci, které bude zavolána, až operace skončí (APC – Asynchronous procedure call). Je to trochu podobné jako AST na VMS. Rozdíl je v tom, že AST může být zavoláno kdykoli (pokud nebylo volání AST zamaskováno), ale APC může být zavoláno jen na přesně definovaných místech, kde proces čeká pomocí funkce, jež má na konci názvu „Ex” (např.  WaitForSingleObjectEx, WaitForMultipleObjectsEx, SleepEx a jiné … na druhou stranu ne každá funkce končící na „Ex” je bodem, ve kterém budou zavolána APC).

CS24_early

Windows NT mají i completion porty. Vzdáleně připomínají kqueue nebo epoll, ale jen vzdáleně, neboť v kqueue a epoll se objevují informace o možnosti dělat IO a v completion portech se objevují informace o dokončeném IO. Pomocí funkce CreateIOCompletionPort je možno vytvořit port nebo přidat handle do portu existujícího. Na handlu přidaném do completion portu je možno dělat asynchronní IO pomocí ReadFile a WriteFile (ale ne ReadFileEx a WriteFileEx  — completion porty a APC současně nejdou použít). Když nějaké asynchronní IO na handlu skončí, je na completion port poslán packet, který je možno vyzvednout pomocí GetQueuedCompletionStatus.

Bohužel veškerá krása tohoto rozhraní se rozplyne, když si uvědomíme, že nefunguje na sockety. Sockety mají svůj vlastní prostor handlů, není na nich možno provádět operace pomocí ReadFile( Ex) ani WriteFile( Ex). Na sockety není možno čekat pomocí WaitForSingleObject nebo WaitForMultipleObjects. Na čekání na události na socketech například existuje speciální funkce WSAWaitForMultipleEvents (nebo i  select), která zase nemůže být použita pro čekání na normální události. Sockety mají i vlastní způsob volání runtin při dokončení (podobný APC). Aby bylo snažší programovat síťové okenní aplikace, je možno si nechat zasílat události o socketu do fronty zpráv nějakého okna.

Byl pro vás článek přínosný?