Laboratoare:Old:Operatii IO asincrone

= Operații I/O asincrone =

Operațiile I/O asincrone se referă la posibilitatea de realizare de operații de intrare/ieșire fără blocarea threadului/procesului în așteptarea unui eveniment I/O. Există, în general, două metode de realizare a acestui lucru:
 * readiness notification (sau multiplexare I/O) pentru un grup de canale I/O;
 * API specializat pentru operații asincrone.

= Multiplexare I/O în Linux =

Există situații în care un program trebuie să trateze operațiile I/O de pe mai multe canale ori de câte ori acestea apar. Un astfel de exemplu este un program de tip server care folosește mecanisme precum pipe-uri sau socketi pentru comunicarea cu alte procese. Un program trebuie să citească practic simultan informații atât de la intrarea standard cât și de la un socket (sau mai mulți).

În aceste situații nu pot fi folosite operații obișnuite de citire sau scriere. Folosirea acestor operații are drept consecința blocarea thread-ului curent până la încheierea operației. O posibilă soluție este folosirea de operații non-blocante (spre exemplu folosirea flag-ul O_NONBLOCK) și interogarea succesivă a descriptorilor de fișier. Totuși, interogarea succesivă (polling) este o formă de așteptare ocupată (busy waiting) și este ineficientă.

Soluția este folosirea unor mecanimse care permit unui thread să aștepte producerea unui eveniment I/O pe un set de descriptori. Thread-ul se va bloca până când unul din descriptorii din set poate fi folosit pentru citire/scriere. Un server care folosește un mecanism de acest tip are, de obicei, o structură de forma: Detaliile variază de la o implementare la alta, dar secvența de pseudocod de mai sus reprezintă structura de bază pentru serverele care folosesc multiplexarea I/O.

select
O primă soluție este utilizarea funcțiilor select sau pselect. Folosirea acestor funcții conduce la blocarea thread-ului curent până la producerea unui eveniment I/O pe un set de descriptori de fișier, a unei erori pe set sau până la expirarea unui timer.

Funcțiile folosesc un set de descriptori de fișier pentru a preciza fișierele/socketii pe care thread-ul curent va aștepta producerea evenimentelor I/O. Tipul de date folosit pentru definirea acestui set este fd_set, care este, de obicei, o mască de biți. Pentru lucrul cu setul de descriptori, programatorul dispune de o serie de macrouri:

După cum sugerează și denumirile, FD_ZERO inițializează setul primit ca argument la un set vid, FD_CLR elimină din set descriptorul fd, FD_SET adaugă în set descriptorul fd</tt> iar FD_ISSET</tt> verifică dacă descriptorul fd</tt> se găsește în set.

Numărul maxim de descriptori de fișier dintr-un set este dat de FD_SETSIZE</tt>.

Cele două funcții pot aștepta nedefinit sau un anumit timp producerea unui eveniment I/O. Precizarea intervalului de timp de așteptare (timeout) se realizează prin intermediul structurilor struct timeval</tt>, respectiv struct timespec</tt>.

Funcțiile select</tt> și pselect</tt> sunt definite conform POSIX.1-2001 în sys/select.h</tt>

Un apel al uneia din cele două funcții testează dacă unul dintre cele trei seturi de descritori de fișier (citire, scriere, eroare) are un eveniment I/O în așteptare sau va bloca thread-ul curent până la producerea unui eveniment. Se poate folosi NULL</tt> în locul unui pointer la set dacă acel set nu prezintă interes aplicației.

nfds</tt> reprezintă valoarea maxima a oricărui file descriptor din cele 3 seturi, plus 1. Se vor testa descriptorii de la 0</tt> la nfds-1</tt>.

Argumentul timeout</tt> specifică perioada maximă de așteptare. Acesta poate fi NULL (caz în care se așteaptă indefinit), poate fi 0</tt> (ceea ce face ca select să returneze imediat), sau poate conține o valoare diferită de 0</tt>.

Ultimul argument pentru <tt>pselect</tt>, <tt>sigmask</tt>, este un pointer la o mască de semnale. Dacă nu este <tt>NULL</tt>, funcția <tt>pselect</tt> înlocuiește masca curentă de semnale cu cea specificată în apel, apelează <tt>select</tt>, și apoi reface masca de semnale.

Comportamentul celor două funcții este identic, cu trei diferențe:


 * 1) <tt>select</tt> folosește pentru timeout <tt>struct timeval</tt> (cu secunde și microsecunde), în timp ce <tt>pselect</tt> folosește <tt>struct timespec</tt> (cu secunde și nanosecunde)
 * 2) <tt>select</tt> poate modifica parametrul timeout pentru a indica cât timp mai rămăsese de așteptat; <tt>pselect</tt> nu își modifică parametrul
 * 3) <tt>select</tt> nu are parametru pentru <tt>sigmask</tt>, și se comportă ca un apel <tt>pselect</tt> cu ultimul argument (<tt>sigmask</tt>) configurat cu valoarea <tt>NULL</tt>

Cele două funcții returnează numărul total de descriptori de fișier (din toate seturile) pregătite pentru operația corespunzătoare. După apel, cele 3 seturi de descriptori date ca argumente vor fi suprascrise cu descriptorii gata de citire, gata de scriere sau cu o condiție îndeplinită în așteptare. Astfel, pentru a vedea dacă există o operație de intrare pe un anumit descriptor, se folosește <tt>FD_ISSET(descriptor, fdset)</tt> după apelul <tt>select</tt>.

Dacă select se întorce ca urmare a expirării intervalului, funcția va returna 0.

Semnalele forțează funcția <tt>select</tt> să se întoarcă imediat. Astfel, dacă avem un program în care folosim semnale, nu ne putem baza pe funcția <tt>select</tt> dacă dorim să astepte un anumit interval de timp. Putem verifica dacă <tt>errno</tt> este <tt>EINTR</tt> și repetăm apelul <tt>select</tt> pentru o altă perioadă de timp.

Pentru un tutorial cu discuții și exemple se recomandă parcurgerea paginii de manual <tt>select_tut(2)</tt>. Un exemplu succint de utilizare a apelului <tt>select</tt> este prezentat în continuare:

Avantaje select

 * simplitate;
 * portabilitate: funcția select este disponibilă chiar și pe Windows;

Dezavantaje select

 * lungimea setul de descriptori este definită cu ajutorul lui FD_SETSIZE, și implicit are valoarea 64;
 * seturile de descriptori trebuie reconstruite la fiecare apel select;
 * la apariția unui eveniment pe unul din descriptori, toți descriptorii puși în set înainte de select trebuie testați pentru a vedea pe care din ei a apărut evenimentul;
 * la fiecare apel, același set de descriptori este transmis în kernel.

poll
Funcția <tt>poll</tt> consolidează argumentele funcției <tt>select</tt> și permite notificarea pentru o gamă mai largă de evenimente. Funcția se definește ca mai jos:

Structura <tt>struct pollfd</tt> este definită în <tt>sys/poll.h</tt>:

Functia <tt>poll</tt> permite astfel asteptarea evenimentelor descrise de vectorul <tt>ufds</tt> de dimensiune <tt>nfds</tt>.

În cadrul structurii <tt>struct pollfd</tt>, câmpul <tt>events</tt> este o mască de biți în care se specifică evenimentele urmărite de <tt>poll</tt> pentru descriptorul <tt>fd</tt>. <tt>revents</tt> este, de asemenea, o mască de biți completată de kernel cu evenimentele apărute în momentul în care apelul se întoarce (<tt>POLLIN</tt>, <tt>POLLOUT</tt>) sau valori predefinite (<tt>POLLERR</tt>, <tt>POLLHUP</tt>, <tt>POLLNVAL</tt>) pentru situații speciale.

În caz de succes funcția returnează un număr diferit de zero reprezentând numărul de structuri pentru care <tt>revents</tt> nu e zero (cu alte cuvinte toți descriptorii cu evenimente sau erori). Se returnează 0 dacă a expirat timpul (timeout milisecunde) și nu a fost selectat nici un descriptor. În caz de eroare se returnează -1 și se setează errno. De asemenea, funcția <tt>poll</tt> poate fi întreruptă de semnale, caz în care va întoarce -1 și <tt>errno</tt> va fi setat la <tt>EINTR</tt>.

Un exemplu de utilizare a <tt>poll</tt> este prezentat în continuare:

Avantaje poll

 * transmiterea setului de descriptori este mai simplă decât în cazul funcției <tt>select</tt>;
 * setul de descriptori nu trebuie reconstruit la fiecare apel <tt>poll</tt>;

Dezavantaje poll

 * ineficiență - la apariția unui eveniment, trebuie parcurs tot setul de descriptori pentru a găsi descriptorul pe care a apărut evenimentul;
 * la fiecare apel, același set de descriptori (care poate fi mare) este copiat în kernel și înapoi.

epoll
Funcțiile <tt>select</tt> și <tt>poll</tt> nu sunt scalabile la un număr mare de conexiuni pentru că la fiecare apel al lor trebuie transmisă toată lista de descriptori. În astfel de situații, la fiecare pas, trebuie contruită lista de descriptori și apelat <tt>poll</tt> sau <tt>select</tt> care copiază tot setul în kernel. La apariția unui eveniment va fi marcat corespunzător descriptorul. Utilizatorul trebuie să parcurgă tot setul de descriptori pentru a-și da seama pe care dintre ei a apărut evenimentul. În acest fel se ajunge să se petreacă tot mai mult timp scanând după evenimente în setul de descriptori și tot mai puțin timp făcând I/O.

Din acest motiv, diverse sisteme au implementat interfețe scalabile dar non-portabile:
 * /dev/poll pe Solaris;
 * kqueue pe FreeBSD;
 * epoll pe Linux.

Aceste interfețe rezolvă problemele asociate cu <tt>select</tt> și <tt>poll</tt> și rezolvă problemele de scalabilitate.

Pentru a folosi <tt>epoll</tt>, trebuie inclus <tt>sys/epoll.h</tt>. Funcțiile asociate sunt <tt>epoll_create</tt>, <tt>epoll_ctl</tt> și <tt>epoll_wait</tt>. Interfața <tt>epoll</tt> oferă funcții pentru crearea unui obiect epoll, adăugarea sau eliminarea de descriptori de fișiere/sockeți la obiectul epoll și așteptarea unui eveniment pe unul dintre descriptori.

Crearea unui obiect epoll
Pentru crearea unui obiect epoll se folosește funcția <tt>epoll_create</tt>:

Apelul <tt>epoll_create</tt> facilitează crearea unui descriptor de fișier ce va fi ulterior folosit pentru așteptarea de evenimente. Descriptorul întors va trebui închis folosind <tt>close</tt>.

Argumentul <tt>size</tt> este doar un indiciu pentru nucleu despre numărul de descriptori pentru care se vor aștepta evenimente.

Adăugarea/eliminarea descriptorilor la/de la obiectul epoll
Operațiile de adăugare/eliminare de descriptori se realizează cu ajutorul funcției <tt>epoll_ctl</tt>:

Apelul <tt>epoll_ctl</tt> permite specificarea evenimentelor care vor fi așteptate. Câmpul <tt>event</tt> descrie evenimentul asociat descriptorului <tt>fd</tt> care poate fi adăugat, șters sau modificat în funcție de valoarea argumentului <tt>op</tt>:


 * <tt>EPOLL_CTL_ADD</tt>: pentru adaugare;
 * <tt>EPOLL_CTL_MOD</tt>: pentru modificare;
 * <tt>EPOLL_CTL_DEL</tt>: pentru ștergere.

Primul argument al apelului <tt>epoll_ctl</tt> (<tt>epoll_ctl</tt>) este descriptorul întors de <tt>epoll_create</tt>.

Structura <tt>struct epoll_event</tt> specifică evenimentele așteptate

Tipuri de evenimente care pot fi urmărite sau primite pe un descriptor:
 * <tt>EPOLLIN</tt>: descriptorul asociat are date de citit;
 * <tt>EPOLLOUT</tt>: descriptorul asociat are loc în buffer-ul asociat pentru a scrie date;
 * <tt>EPOLLERR</tt>: a apărut o eroare pe descriptorul asociat.

Union-ul <tt>epoll_data</tt> poate fi folosit pentru a asocia o cheie cu descriptorul urmărit.

Așteptarea unui eveniment I/O
Thread-ul curent așteptă producerea unui eveniment I/O la unul din descriptorii asociați obiectului epoll prin intermediul funcției <tt>epoll_wait</tt>:

Funcția <tt>epoll_wait</tt> este echivalentul funcțiilor <tt>select</tt> și <tt>poll</tt>. Este folosită pentru așteptarea unui eveniment la unul din descriptorii asociați descriptorului <tt>epollfd</tt>.

La revenirea apelului programatorul nu va trebui să parcurgă toți descriptorii configurați ci numai cei care au evenimente produse. Argumentul <tt>events</tt> va marca o zonă de memorie unde vor fi plasate maxim <tt>maxevents</tt> evenimente de nucleu. Presupunând că valoarea câmpului <tt>timeout</tt> este <tt>-1</tt> (așteptare nedefinită), apelul se va întoarce imediat dacă există evenimente asociate, sau se va ploca până la apariția unui eveniment.

La fel ca și în cazul <tt>select</tt>/<tt>pselect</tt> si <tt>poll</tt>/<tt>ppoll</tt>, există apelul <tt>epoll_pwait</tt> care permite precizarea unei măști de semnale.

Edge-triggered sau level-triggered
Interfața <tt>epoll</tt> are două comportamente posibile: edge-triggered sau level-triggered. Se poate folosi unul sau altul, în funcție de prezența flag-ului <tt>EPOLLET</tt> la adăugarea unui descriptor în lista epoll.

Presupunem existența unui socket funcționând în mod non-blocant pe care sosesc 100 de octeți. În ambele moduri (edge sau level triggered) <tt>epoll_wait</tt> va raporta <tt>EPOLLIN</tt> pentru acel socket.

Vom presupune că se citesc 50 de octeți din cei 100 primiți. Diferența între cele două moduri de funcționare apare la un nou apel <tt>epoll_wait</tt>. În modul level-triggered se va raporta imediat <tt>EPOLLIN</tt>. În modul edge-triggered nu se va mai raporta nimic, nici măcar la sosirea unor noi date pe socket. Se poate observa cum modul edge-triggered sesizează schimbarea stării descriptorului în relație cu evenimentul, iar level-triggered prezența stării. Modul edge-triggered este implementat mai eficient în kernel, chiar dacă pare mai greu de folosit.

În continuare, sunt prezentate câteva reguli care trebuie urmărite cu o metodă sau alta. Pentru ambele metode este recomandată folosirea sockeților în modul non-blocant.


 * Level-triggered
 * La apariția unui eveniment <tt>EPOLLIN</tt> se poate citi oricât, la următorul apel epoll_wait se va raporta din nou <tt>EPOLLIN</tt> dacă mai sunt date de citit.
 * <tt>EPOLLOUT</tt> nu trebuie configurat inițial pentru un socket pentru că astfel <tt>epoll_wait</tt> va raporta imediat că este loc de scris în buffer (inițial bufferul de scriere asociat cu socketul este gol). Acesta este o formă deghizată de busy waiting. Folosirea corectă implică scrierea normală pe socket și numai dacă la un moment dat funcțiile de scriere raportează că nu mai este loc de scriere in buffer (<tt>EAGAIN</tt>), se va activa <tt>EPOLLOUT</tt> pe descriptorul respectiv și salva ce mai este de scris. Când în sfârșit se face loc, se va raporta EPOLLOUT și atunci se poate încerca să se scrie datele păstrate. Dacă se reușește scrierea lor integrală, trebuie eliminat flagul <tt>EPOLLOUT</tt> pentru a nu intra într-un nou ciclu de busy-waiting. În concluzie, <tt>EPOLLOUT</tt> trebuie activat doar când nu se reușește scrierea integrală a datelor și scos imediat după ce acestea au fost scrise.


 * Edge-triggered
 * La apariția unui eveniment <tt>EPOLLIN</tt> pe un descriptor, trebuie citit tot ce se poate citi înainte de reapelarea <tt>poll_wait</tt>, altfel nu va mai fi raportat <tt>EPOLLIN</tt> niciodată.
 * Pentru scrierea folosind edge-triggered se poate activa de la început <tt>EPOLLOUT</tt>. Aceasta va cauza apariția unui eveniment <tt>EPOLLOUT</tt> imediat dupa apelarea <tt>epoll_wait</tt> (pentru că bufferul de scriere este gol) care ar trebui ignorat. La următorul apel <tt>epoll_wait</tt> nu se mai generează <tt>EPOLLOUT</tt> pentru că nu s-a schimbat starea de la ultimul apel. Dacă la un moment dat se încearcă scrierea unor date pe socket și acestea nu pot fi scrise integral, la următorul <tt>epoll_wait</tt> se generează <tt>EPOLLOUT</tt>, pentru că s-a schimbat starea socketului. Mai pe scurt, asta are ca efect faptul că nu mai trebuie activat/deactivat <tt>EPOLLOUT</tt> ca în cazul level-triggered.

Exemplu folosire epoll
Mai jos este prezentat un exemplu de utilizare a <tt>epoll</tt> echivalent cu exemplele pentru <tt>select</tt> și <tt>poll</tt> (server care multiplexează mai multe conexiuni pe sockeți și intrarea standard):

Generalizarea multiplexării I/O
O problemă a funcțiilor de multiplexare de mai sus (<tt>select</tt>, <tt>poll</tt>, <tt>epoll</tt>) este aceea că sunt limitate la descriptori de fișier. Altfel spus, se pot aștepta doar evenimente asociate cu un fișier/socket: gata de citire, gata de scriere. De multe ori însă se dorește să existe un punct comun de așteptare a unui semnal, a unui semafor, a unui proces, a unei operații de intrare/ieșire, a unui timer. În Windows, acest lucru se poate realiza cu ajutorul funcției <tt>WaitForMultipleObjects</tt> și pe faptul că majoritatea mecanismelor din Windows sunt folosit cu ajutorul tipului de date <tt>HANDLE</tt>.

Pentru a asigura în Linux posibilitate așteptării de evenimente multiple s-a definit interfața eventfd. Cu ajutorul aceste interfețe și combinat cu interfețele de multiplexare I/O existente, kernel-ul poate notifica o aplicație utilizator de orice tip de eveniment.

Interfața eventfd este prezentă în nucleul Linux începând cu versiunea 2.6.22. În momentul de față (glibc 2.7), biblioteca standard C nu oferă suport pentru aceste mecanisme. Suportul va fi oferit în versiunea 2.8 a glibc. Pentru folosirea mecanismelor oferite de kernel se poate folosi interfața <tt>syscall</tt>:

Cele trei apeluri de bază pentru extinderea funcționalității multiplexării I/O sunt <tt>eventfd</tt>, <tt>signalfd</tt> și <tt>timerfd_create</tt>.

Toate cele trei apeluri întorc un descriptor de fișier pe care se vor putea primi notificări (semnale, timere etc.). Operațiile posibile pe descriptorul de fișier întors sunt:


 * <tt>write</tt>: pentru transmiterea unui mesaj de notificare pe descriptor;
 * <tt>read</tt>: pentru primirea unui mesaj care înseamnă primirea notificării;
 * <tt>select</tt>, <tt>poll</tt>, <tt>epoll</tt>: pentru multiplexarea I/O;
 * <tt>close</tt>: pentru închiderea descriptorului și eliberarea resurselor asociate.

În următorul exemplu, apelul <tt>eventfd</tt> este folosit pentru notificarea procesului părinte de către procesul fiu. Codul este cel prezent în pagina de manual (<tt>man eventfd</tt>).

Apelul <tt>signalfd</tt> este folosit în mod similar pentru recepționarea de semnale prin intermediul unui descriptor de fișier. Pentru a putea recepționa un semnal cu ajutorul interfeței <tt>signalfd</tt>, va trebui blocat în masca de semnale a procesului. La fel ca și exemplul de mai sus, codul de mai jos este cel prezent în pagina de manual (<tt>man signalfd</tt>).

Interfața eventfd permite unificarea mecanismelor de notificare ale kernel-ului într-un descriptor de fișier care va fi folosit de utilizator. În acest moment (kernel 2.6.25) a fost unificat subsistemul de operații I/O al nucleului Linux cu interfața eventfd, lipsind însă, deocamdată, suportul bibliotecii standard C.

= Operații asincrone în Linux =

În mod clasic, operațiile de lucru cu datele aflate pe suporturi externe înseamnă utilizarea apelurilor sincrone de tipul <tt>read</tt>, <tt>write</tt> și <tt>fsync</tt>. Aceste apeluri garantează faptul că la terminarea apelului datele sunt scrise/citite (de) pe suportul extern (sau în cache-ul asociat). Un astfel de apel poate întârzia continuarea fluxului de instrucțiuni curent până la terminarea operației cerute.

Pentru fire de execuție care nu au nevoie frecvent de operații de intrare-ieșire, această abordare funcționează. În schimb, pentru aplicații specializate pe lucrul cu memoria externă, folosirea apelurilor sincrone (blocante) încetinește semnificativ execuția programului. Timpul necesar unui acces la memorie (cu atât mai mult memoria externă) depășește cu mult timpul de execuție a unei instrucțiuni strict aritmetice.

Pentru asemenea aplicații specializate pe lucrul cu memoria externă există apeluri asincrone de tipul <tt>aio_read</tt> și <tt>aio_write</tt>. Aceste operații lansează în execuție o operație de lucru cu memoria externă și se întorc imediat; sistemul continuă execuția operației independent (și în paralel cu) firul de execuție care a lansat operația. Momentul terminării execuției este anunțat înapoi aplicației prin trimiterea unui semnal sau prin executarea unei rutine de către un thread.

Funcțiile asociate cu operații asincrone sunt definite în <tt>aio.h</tt> și își au codul în biblioteca <tt>librt</tt>, cu care aplicațiile trebuie legate (<tt>-lrt</tt> la linking).

AIO control block
Toate operațiile asincrone de intrare-ieșire sunt controlate printr-o structură de date numită AIO control block în forma <tt>struct aiocb</tt>, definită în <tt>aio.h</tt>:

Un exemplu de inițializare a unei structuri aiocb este prezentat în continuare:

Pentru detalii despre structură consultați pagina de documentație asociată (<tt>info libc "Low-level I/O" "Asynchronous I/O"</tt>).

Un parametru de tip <tt>struct aiocb</tt> va fi un element prezent la apelul oricărei operații asincrone.

Scrierea și citirea asincronă
Apelurile echivalente cu read și write pentru operații asincrone sunt aio_read și aio_write, descrise în continuare.

Aceste apeluri inițiază o operație asincronă și returnează imediat după punerea cererii în coada de operații, sau dacă întâlnește o eroare. Comportamentul operației este descris de câmpurile structurii <tt>aiocbp</tt> transmise ca parametru.

Astfel, pentru citire, primii <tt>aio->aio_nbytes</tt> octeți ai fișierului <tt>aio->aio_fildes</tt> sunt scriși în bufferul indicat de <tt>aio->aio_buf</tt>. Citirea începe de la poziția absolută în fișier dată de offsetul <tt>aio->aio_offset</tt>. Dacă prioritizarea operațiilor asincrone este posibilă, valoarea <tt>aio->aio_reqprio</tt> ajustează prioritatea operației înainte de punerea ei în coadă. Procesul apelant este notificat la terminarea cererii de citire în conformitate cu valoarea <tt>aio->aio_sigevent</tt>.

La scriere, primii <tt>aio->aio_nbytes</tt> octeți ai fișierului <tt>aio->aio_fildes</tt> sunt suprascriși cu datele memorate în bufferul indicat de <tt>aio->aio_buf</tt>. Scrierea începe de la poziția absolută în fișier dată de offsetul <tt>aio->aio_offset</tt>. Dacă prioritizarea operațiilor asincrone este posibilă, valoarea <tt>aio->aio_reqprio</tt> ajustează prioritatea operației înainte de punerea ei în coadă. Procesul apelant este notificat la terminarea cererii de citire în conformitate cu valoarea <tt>aio->aio_sigevent</tt>.

Pentru interogarea statusului unei cereri se folosesc apelurile <tt>aio_error</tt> sau <tt>aio_return</tt>, descrise într-o secțiune ulterioară.

Pe lângă funcții clasice de lucru cu apelurile IO asincrone, standardul pune la dispoziție și o funcție care poate iniția mai multe operații asincrone, comportându-se ca un set de <tt>aio_read</tt> și <tt>aio_write</tt> într-un singur apel. Apelul <tt>lio_listio</tt> primește o listă de structuri de tip <tt>control block</tt> și le lansează în execuție:

Apelul preia lista <tt>list</tt> de cereri de tip <tt>struct aiocb</tt>, în număr de <tt>nent</tt>. Cererile IO din lista <tt>list</tt> pot fi efectuate asupra oricărui fișier, inclusiv același de mai multe ori. Tipul operației este memorat separat pentru fiecare cerere în câmpul <tt>aio_lio_opcode</tt> din control block. Câmpul mode determină comportamentul apelului după punerea în coadă a tuturor operațiilor.

Interogarea stării AIO
Cum apelurile de inițiere a operațiilor asincrone returnează imediat, ele nu pot oferi informații ulterioare despre starea cererii. Apelurile <tt>aio_error</tt> și <tt>aio_return</tt> permit interogarea unei cereri pentru a afla dacă aceasta a fost executată, și în caz că a fost, cu ce rezultat.

Un exemplu de folosire a acestor apeluri este prezentat în continuare (copiat cu nerușinare din pagina de manual a HP-UX):

Așteptarea operațiilor AIO
În unele cazuri așteptarea notificărilor care anunță execuția cererilor nu este o soluție bună pentru o aplicație, de exemplu pentru că aceasta nu vrea să fie întreruptă în orice moment. Pentru asemenea cazuri aplicațiile pot folosi apelul <tt>aio_suspend</tt> care supendă thread-ul curent până la încheierea operațiilor asincrone primite ca argument:

Linux AIO
Implementarea curentă (glibc 2.7) a operațiilor POSIX asincrone în Linux este realizată în user-space cu ajutorul thread-urilor POSIX. Totuși, nucleul Linux 2.6 oferă interfață (apeluri de sistem) pentru operații asincrone implementate la nivelul nucleului. Există, în acest sens, biblioteci specializate care oferă interfața POSIX AIO peste API-ul nativ al nucleului Linux 2.6 (spre exemplu PAIOL - POSIX Asynchronous I/O for Linux

Apelurile de sistem care asigură interfața cu implementarea AIO în Linux sunt prezentate mai jos:

Se folosește macro-ul <tt>syscall</tt> care permite folosirea apelurilor de sistem native. Exemplul de mai sus este cel folosit de Davide Libenzi pentru a prezenta modul de folosire a interfeței AIO sub Linux (http://www.xmailserver.org/eventfd-aio-test.c).

După cum se observă, există o structură internă (<tt>struct iocb</tt>) care descrie operația asincronă de realizat.

Integrarea Linux AIO cu eventfd
În mod evident este utilă folosirea apelurilor de multiplexare I/O (<tt>select</tt>, <tt>poll</tt>, <tt>epoll</tt>) și pentru așteptarea încheierii operațiilor asincrone. Pentru aceasta, interfața AIO a Linux 2.6 permite integrarea API-ului de operații asincrone cu mecanismul <tt>eventfd</tt>.

Pentru aceasta, un câmp al structurii <tt>struct iocb</tt> va conține un descriptor <tt>eventfd</tt> ce va fi notificat în momentul încheierii operației asincrone:

Folosind integrarea operații asincrone cu <tt>eventfd</tt> și mecanismele de multiplexare I/O (<tt>select</tt>, <tt>poll</tt>, <tt>epoll</tt>) se poate aștepta unificat încheierea unei operații asincrone sau sosirea de date pe sockeți. (Hint: util pentru Tema 5)

= Operații asincrone în Windows =

În Windows se folosesc funcțiile <tt>ReadFile</tt>, <tt>WriteFile</tt> sau <tt>ReadFileEx</tt>, <tt>WriteFileEx</tt> pentru pornirea operațiilor asincrone. Pentru notificarea terminării operațiilor în Windows se pot folosi evenimente, APC-uri, Thread Pooling sau Completion Port-uri.

Apeluri pentru transfer asincron (Overlapped I/O)
În Windows, operațiile asincrone se numesc overlapped I/O (operații suprapuse). Pentru folosirea overlapped I/O în cazul fișierelor se folosește ultimul argument transmis funcției <tt>ReadFile</tt>, respectiv <tt>WriteFile</tt>, ca în exemplul de mai jos:

Ultimul argument este un pointer la structura <tt>OVERLAPPED</tt> care descrie operația asincronă de realizat. Pentru ca operațiile asincrone să poată decurge pe fișiere, fișierul trebuie deschis cu flag-ul <tt>FILE_FLAG_OVERLAPPED</tt>:

Interogarea/așteptarea operațiilor asincrone
Pentru determinarea stării operației asincrone se poate folosi funcția <tt>GetOverlappedResult</tt>:

După cum se poate observa, ultimul argument al apelului <tt>GetOverlappedResult</tt> diferențiază între interogarea operației asincrone (<tt>FALSE</tt>) sau așteptarea încheierii acesteia (<tt>TRUE</tt>).

Notificarea încheierii operațiilor asincrone
Pentru notificarea încheierii unei operații asincrone se pot folosi:


 * evenimente - prin completarea corespunzătoare a câmpului <tt>hEvent</tt> al structurii <tt>OVERLAPPED</tt>;
 * APC - prin folosirea apelurilor <tt>ReadFileEx</tt>/<tt>WriteFileEx</tt> care permit specificarea unei rutine apelată la încheierea operației;
 * I/O completion ports - mecanismul scalabil de așteptare a operațiilor asincrone.

Operatii asincrone pe socketi
La fel ca la fisiere, folosirea operatii asincrone (overlapped I/O) pe socketi presupune folosirea unei structuri specializate (<tt>WSAOVERLAPPED</tt>). Pentru transmiterea/receptia de informatii in format asincron se folosesc functiile <tt>WSARecv</tt>, respectiv <tt>WSASend</tt>.

Un exemplu de folosire a functiei <tt>WSARecv</tt> pentru primirea asincrona a informatiilor este prezentat in continuare. Codul este inspirat din exemplul din pagina MSDN asociata:

= I/O Completion Ports =

Mecanismul de completion ports este cel mai scalabil dintre toate cele prezentate până acum. Un server care folosește completion ports poate face față la foarte multe (zeci de mii) conexiuni simultan, fără probleme prea mari. Celelalte metode își ating limitările cu mult înainte.

Un completion port este un obiect în kernel cu care se asociază alți descriptori (fișiere, sockeți) și prin intermediul căruia se transmit notificările de completare a unor operații asincrone lansate anterior. Un completion port are asociat un pool de worker threads. Aceste threaduri așteaptă să primească notificări de completare a operațiilor asincrone. În momentul în care un thread primește o notificare va deveni activ și va lucra o perioada până se va întoarce din nou așteptând următoarea notificare.

Crearea unui completion port
Pentru crearea unui completion port se folosește funcția <tt>CreateIoCompletionPort</tt> ca în exemplul de mai jos:

Adăugarea unui descriptor la completion port
Pentru adăugarea unui descriptor deschis cu opțiunea de overlapped I/O la completion port se folosește tot funcția <tt>CreateIoCompletionPort</tt>. În această situație primul argument va fi handle-ul fișierului/socketului care se dorește adăugat, iar al doilea handle-ul completion port-ului obținut la crearea acestuia:

Dupa cum se observa, in cazul crearii unui completion port al doilea argument este <tt>NULL</tt>. La adaugarea unui handle de fisier la completion port al doilea argument este handle-ul de completion port. Al treilea argument este o cheie care va fi folosita pentru identificarea handle-ului in momentul receptionarii unei notificari.

Așteptarea încheierii unei operații asincrone
Thread-urile worker sunt folosite pentru așteptarea încheierii operațiilor asincrone și prelucrările ulterioare. Thread-urile vor primi notificări de la handle-ul completion port-ului folosind funcția <tt>GetQueuedCompletionStatus</tt>:

Pe baza cheii obtinute se poate determina handle-ul care a generat notificarea.

Exemplu de folosire completion ports
In exemplul de mai jos este prezentata folosirea mecanismului de completion ports in cazul operatiilor asincrone pe socketi. Exemplul este similar cu cel prezentat in sectiunile dedicate functiilor de multiplexare I/O pe Linux. Exista un thread worker care va astepta primirea notificarilor la completion port, iar thread-ul principal va fi responsabil cu primirea de cereri de conexiune (apeluri <tt>accept</tt>).

= Exerciții =

Quiz
Pentru autoevaluare raspundeți la întrebările din acest quiz.

Exerciții pre-laborator

 * Folosiți [[Media:lab11-pre.zip|arhiva de pre-sarcini]] a laboratorului.

Linux

 * 1) Folosiți [[Media:lab11-pre.zip|arhiva de pre-sarcini]] a laboratorului (directorul <tt>aio_lin/</tt>) pentru a implementa o operație asincronă simplă pe un fișier.
 * Hints:
 * 1) * Folosiți indicațiile din comentarii.
 * 2) * Folosiți <tt>aio_suspend</tt> pentru a aștepta încheierea operației asincrone.
 * 3) Compilați și rulați exemplul din pagina de manual a eventfd (<tt>man 2 eventfd</tt>).
 * Hint:
 * 1) * Va trebui să folosiți header-ul <tt>sys/syscall.h</tt> și apelul <tt>syscall</tt> așa cum se prezintă în exemplele de cod din laborator.
 * 2) Compilați și rulați exemplul din pagina de manual a signalfd (<tt>man 2 signalfd</tt>).
 * Hints:
 * 1) * Va trebui să folosiți header-ul <tt>sys/syscall.h</tt> și apelul <tt>syscall</tt> așa cum se prezintă în exemplele de cod din laborator.
 * 2) * Va trebui să includeți header-ele <tt>linux/types.h</tt> și <tt>linux/signalfd.h</tt> pentru structura <tt>struct signalfd_siginfo</tt>.
 * 3) Folosiți Internetul sau pagina de manual (<tt>man 7 socket</tt>) pentru a afla cum se poate configura un socket BSD pentru a lucra în mod non-blocant.
 * 4) Compilați și rulați exemplul din pagina de manual a select (<tt>man 2 select</tt>).

Windows

 * 1) Folosiți [[Media:lab11-pre.zip|arhiva de pre-sarcini]] a laboratorului (directorul <tt>aio_win/</tt>) pentru a implementa o operație asincronă simplă (overlapped I/O) pe un fișier.
 * Hints:
 * 1) * Folosiți indicațiile din comentarii.
 * 2) Folosiți Internetul pentru a afla cum poate fi folosit un listener socket în mod asincron (să poată fi folosit mecanismul de I/O completion ports pentru notificare în cazul acceptării unei conexiuni).

Exerciții de laborator

 * Laboratorul are 20 de puncte.
 * Punctajul maxim se obține cu 10 puncte. Bonusul de puncte poate compensa alte laboratoare.
 * Folosiți [[Media:lab11-tasks.zip|arhiva de sarcini]] a laboratorului.
 * Puteți folosi fișierele tags din directoarele <tt>lin/</tt>, respectiv <tt>win/</tt> pentru o parcurgere rapidă a surselor folosind vim (fișierul <tt>tags</tt>) sau Emacs (fișierul <tt>TAGS</tt>).
 * Pentru folosirea <tt>epoll</tt> aveți nevoie de o versiune >= <tt>2.6</tt> a nucleului Linux. Pentru <tt>signalfd</tt>, <tt>eventfd</tt> aveți nevoie de o versiune >= <tt>2.6.22</tt>.

Linux
Intrați în directorul <tt>lin/</tt> din [[Media:lab11-tasks.zip|arhiva de sarcini]] a laboratorului.


 * 1) (2 puncte) Operații POSIX asincrone (<tt>aio_read</tt>, <tt>aio_write</tt> etc.)
 * 2) * Intrați în directorul <tt>aio/</tt>.
 * 3) * Analizați conținutul fișierului <tt>aio.c</tt>.
 * 4) * Scopul exercițiului este scrierea unor date aleatoare în fișiere folosind operații sincrone și asincrone.
 * 5) * Implementați funcțiile <tt>do_io_sync</tt>, respectiv <tt>do_io_async</tt>.
 * Hints:
 * 1) ** Pentru operațiile sincrone puteți folosi funcția <tt>xwrite</tt> definită în fișier.
 * 2) ** Funcțiile trebuie să scrie conținutul bufferului <tt>g_buffer</tt> în cele 4 fișiere cu numele dat de vectorul <tt>files</tt>.
 * 3) * Folosiți <tt>lio_listio</tt> pentru realizarea operațiilor asincrone pe cele 4 fișiere.
 * Hints:
 * 1) ** Pentru inițializarea structurilor <tt>aiocb</tt> se recomandă implementarea funcției <tt>init_aiocb</tt>.
 * 2) ** Documentație despre folosirea funcției <tt>lio_listio</tt> găsiți în pagina de documentatație asociată (sau <tt>info libc "Low-Level I/O" "Asynchronous I/O"</tt>).
 * 3) ** Va trebui să alocați un vector de pointeri la structuri <tt>struct aiocb</tt> de transmis ca argument funcției <tt>lio_listio</tt>.
 * 4) ** Nu uitați să apelați <tt>aio_return</tt> pentru a elibera resursele asociate structurilor <tt>struct aiocb</tt>.
 * 5) * Folosiți macro-ul <tt>IO_OP_TYPE</tt> pentru a determina comportamentul programului.
 * 6) * Compilați și rulați programul.
 * 7) (5 puncte) Funcții de multiplexare I/O
 * 8) * Intrați în directorul <tt>include/</tt>.
 * 9) ** Analizați conținutul fișierelor <tt>mpx_types.h</tt>, respectiv <tt>mpx_funs.h</tt>.
 * Hints:
 * 1) *** Funcțiile descrise în <tt>mpx_funs.h</tt> se doresc a fi generice. Uneori implementarea va fi ineficientă.
 * 2) *** Separația <tt>mpx_funs.h</tt> de <tt>mpx_types.h</tt> este puțin forțată, dar menține o anumită simplitate.
 * 3) * Intrați în directorul <tt>mpx/</tt>.
 * 4) * Analizați conținutul fișierelor <tt>mpx_select.c</tt>, <tt>mpx_poll.c</tt> respectiv <tt>mpx_epoll.c</tt>.
 * 5) * Implementați funcțiile definite în <tt>mpx_select.c</tt>.
 * Hint:
 * 1) ** Urmăriți indicațiile din comentariile asociate funcțiilor.
 * 2) * Implementați funcțiile definite în <tt>mpx_poll.c</tt>.
 * Hints:
 * 1) ** Se presupune că se așteaptă doar notificări pentru citire (<tt>POLLIN</tt>) sau scriere (<tt>POLLOUT</tt>).
 * 2) ** Urmăriți indicațiile din comentariile asociate funcțiilor.
 * 3) * Implementați funcțiile definite în <tt>mpx_epoll.c</tt>.
 * Hints:
 * 1) ** Interfața oferită peste <tt>epoll</tt> presupune așteptarea unui singur tip de eveniment pe un descriptor (<tt>EPOLLIN</tt> sau <tt>EPOLLOUT</tt>). Funcția <tt>epoll_del_</tt> va elimina complet descriptorul fișierului.
 * 2) ** Urmăriți indicațiile din comentariile asociate funcțiilor.
 * 3) (1 punct) eventfd
 * 4) * Intrați în directorul <tt>eventfd</tt>.
 * 5) * Analizați conținutul fișierului <tt>eventfd.c</tt>.
 * 6) * Scopul exercițiului este să se aștepte simultan notificarea procesului părinte de la un proces fiu creat și primirea informațiilor de la intrarea standard.
 * 7) * Folosiți macroul <tt>MPX_TYPE</tt> pentru a face selecția între diversele API-uri de multiplexare I/O.
 * 8) * Folosiți funcțiile generice <tt>mpx_*</tt> definite în <tt>mpx/mpx_funs.h</tt>.
 * 9) * Urmăriți indicațiile din comentariile din cod.
 * 10) * Compilați și rulați programul pentru toate cele 3 tipuri de API de multiplexare I/O.
 * Hint
 * 1) ** Dacă nu există pagina de manual (<tt>man 2 eventfd</tt>), puteți folosi pagina de manual online a eventfd.
 * 2) (1 punct) signalfd
 * 3) * Intrați în directorul <tt>signalfd</tt>.
 * 4) * Analizați conținutul fișierului <tt>signalfd.c</tt>.
 * 5) * Scopul exercițiului este să se aștepte simultan notificarea procesului părinte de la un proces fiu creat (semnalul <tt>SIGCHLD</tt>) și primirea informațiilor de la intrarea standard.
 * 6) * Folosiți macroul <tt>MPX_TYPE</tt> pentru a face selecția între diversele API-uri de multiplexare I/O.
 * 7) * Folosiți funcțiile generice <tt>mpx_*</tt> definite în <tt>mpx/mpx_funs.h</tt>.
 * 8) * Urmăriți indicațiile din comentariile din cod.
 * 9) * Compilați și rulați programul pentru toate cele 3 tipuri de API de multiplexare I/O.
 * Hint
 * 1) ** Dacă nu există pagina de manual (<tt>man 2 signalfd</tt>), puteți folosi pagina de manual online a signal.
 * 2) (2 puncte) Multiplexare I/O peste socketi
 * 3) * Intrați în directorul <tt>sock</tt>.
 * 4) * Parcurgeți fișierele <tt>sock_util.h</tt>, <tt>sock_util.c</tt>, <tt>client.c</tt>.
 * 5) * Analizați conținutul fișierului <tt>mpx_server.c</tt>.
 * 6) * Completați fișierul <tt>mpx_server.c</tt>.
 * 7) * Scopul exercițiului este crearea unui server care să multiplexeze mai multe conexiuni de la clienți. Serverul _doar_ va citi informații de la clienți pe care le va afișa la ieșirea standard.
 * Hints:
 * 1) ** Serverul va multiplexa socketul listener și socketii de conectare. Dacă socketul listener este gata de citire atunci se acceptă o nouă conexiune; dacă un socket de conectare e gata de citire se citește informația de la acesta.
 * 2) ** Urmăriți indicațiile din comentariile din cod.
 * 3) * Folosiți fișierul <tt>Makefile</tt> pentru a obține executabilele <tt>client</tt> și <tt>mpx_server</tt>.
 * 4) * Pentru testare porniți serverul și mai multe instanțe de client.

Windows
Intrați în directorul <tt>win/</tt> din [[Media:lab11-tasks.zip|arhiva de sarcini]] a laboratorului.


 * 1) (2 puncte) Operații I/O asincrone (overlapped I/O)
 * 2) * Intrați în directorul <tt>aio/</tt>.
 * 3) * Analizați conținutul fișierului <tt>aio.c</tt>.
 * 4) * Scopul exercițiului este scrierea unor date aleatoare în fișiere folosind operații sincrone și asincrone.
 * 5) * Implementați funcțiile <tt>do_io_sync</tt>, respectiv <tt>do_io_async</tt>.
 * Hints:
 * 1) ** Pentru operațiile sincrone puteți folosi funcția <tt>xwrite</tt> definită în fișier.
 * 2) ** Va trebui să alocați spațiu pentru structurile <tt>OVERLAPPED</tt> pentru toate cele 4 fișiere.
 * 3) ** Pentru inițializarea structurilor <tt>OVERLAPPED</tt> se recomandă implementarea funcției <tt>init_overlapped</tt>.
 * 4) ** Folosiți <tt>GetOverlappedResult</tt> pentru realizarea operațiilor asincrone pe cele 4 fișiere.
 * 5) ** Funcțiile trebuie să scrie conținutul bufferului <tt>g_buffer</tt> în cele 4 fișiere cu numele dat de vectorul <tt>files</tt>.
 * 6) * Folosiți macro-ul <tt>IO_OP_TYPE</tt> pentru a determina comportamentul programului.
 * 7) * Compilați și rulați programul.
 * 8) (1 puncte) I/O completion ports
 * 9) * Anlizați conținutul fișierului <tt>include/iocp.h</tt>.
 * 10) * Intrați în directorul <tt>iocp/</tt>.
 * 11) * Analizați conținutul fișierului <tt>iocp.c</tt>.
 * 12) * Completați cele 4 funcții definite în <tt>iocp.c</tt>.
 * 13) (2 puncte) Operații I/O asincrone cu I/O completion ports
 * 14) * Intrați în directorul <tt>aio_cp/</tt>.
 * 15) * Analizați conținutul fișierului <tt>aio.c</tt>.
 * 16) * Scopul exercițiului este folosirea I/O completion ports pentru așteptarea încheierii operațiilor I/O asincrone (overlapped I/O).
 * 17) * Implementați funcțiile <tt>init_io_async</tt>, <tt>do_io_async</tt> și <tt>wait_io_async</tt>.
 * Hints:
 * 1) ** Pentru inițializarea structurilor <tt>OVERLAPPED</tt> se recomandă implementarea funcției <tt>init_overlapped</tt>.
 * 2) ** Urmăriți indicațiile din exemplele de cod.
 * 3) * Compilați și rulați programul.
 * 4) (4 puncte) Operații asincrone pe sockeți și I/O completion ports
 * 5) * Intrați în directorul <tt>sock/</tt>.
 * 6) * Parcurgeți fișierele <tt>sock_util.h</tt>, <tt>sock_util.c</tt>, <tt>client.c</tt>.
 * 7) * Analizați conținutul fișierului <tt>iocp_server.c</tt>.
 * 8) * Completați fișierul <tt>iocp_server.c</tt>.
 * 9) * Scopul exercițiului este crearea unui server care să multiplexeze mai multe conexiuni de la clienți folosind operații asincrone pe socketi și I/O completion ports. Serverul _doar_ va citi informații de la clienți pe care le va afișa la ieșirea standard.
 * 10) * Serverul dispune de două thread-uri: un thread acceptă conexiuni pe care le adaugă la I/O completion port și inițiază operație asincronă de citire; un thread worker așteaptă notificarea de la I/O completion port, o tratează și apoi inițiază o nouă operație asincronă.
 * Hints:
 * 1) ** Folosiți structura <tt>struct overlappedContext</tt> pentru a reține informațiile despre un apel asincron.
 * 2) ** Se recomandă folosirea structurii pe post de cheie pentru a putea demultiplexa notificarea sosită la I/O completion port.
 * 3) * Urmăriți indicațiile din comentariile din cod.
 * 4) * Folosiți fișierul <tt>Makefile</tt> pentru a obține executabilele <tt>client.exe</tt> și <tt>iocp_server.exe</tt>.
 * 5) * Pentru testare porniți serverul și mai multe instanțe de client.

= Soluții =


 * [[Media:lab11-pre-sol.zip|Soluții exerciții pre-laborator 11]]
 * [[Media:lab11-tasks-sol.zip|Soluții exerciții laborator 11]]

= Resurse utile =


 * Linux AIO
 * linux-aio mailing list
 * Biblioteca PAIOL - POSIX Asynchronouos I/O for Linux
 * libaio - O biblioteca (veche) pentru operații AIO cu suport în nucleul Linux
 * eventfd
 * Exemplu de cod integrare eventfd cu Linux AIO
 * select/poll/epoll
 * Linux System Programming - Chapter 2 - File I/O (Multiplexed I/O)
 * Linux System Programming - Chapter 4 - Advanced File I/O (The Event Poll Interface)
 * Beginning Linux Programming, 4th Edition - Chapter 15: Sockets (select)
 * Un articol interesant despre epoll, la un nivel mai inalt
 * Windows Overlapped I/O
 * Syncrhonization and Overlapped Input and Output
 * Synchronous and Asynchronous I/O
 * Programming with Asynchronous Sockets
 * Asynchronous Input/Output and Completion Ports - Windows System Programming, 3rd Edition - Chapter 14
 * Windows I/O Completion Ports
 * MSDN - I/O Completion Ports
 * Inside I/O Completion Ports
 * O introducere în Completion Ports
 * Tutorial IOCP și sockeți
 * General
 * Asynchronous I/O - Wikipedia
 * libevent - bibliotecă de operații asincrone
 * C10K - Problema celor 10.000 de clienți