Laboratoare:Operatii IO avansate (1)

= Linux - multiplexarea I/O =

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.

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

Deoarece si apelul poll este specificat in standardul POSIX (deci portabilitate mare), dar ofera performante mai bune, nu vom insista asupra apelului select!

Avantaje:


 * simplitate;
 * portabilitate: funcția select este disponibilă chiar și in API-ul Win32;

Dezavantaje:


 * lungimea setul de descriptori este definită cu ajutorul lui FD_SETSIZE, și implicit are valoarea 64;
 * este necesar ca seturile de descriptori sa fie 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 poll consolidează argumentele funcției select</tt> și permite notificarea pentru o gamă mai largă de evenimente. Funcția se definește ca mai jos:

Timeout-ul este specificat in milisecunde. In caz de valoare negativa, semnificatia este de asteptare pentru o perioada nedefinita ("infinit").

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

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

În cadrul structurii struct pollfd</tt>, câmpul events</tt> este o mască de biți în care se specifică evenimentele urmărite de poll</tt> pentru descriptorul fd</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 (POLLIN</tt>, POLLOUT</tt>) sau valori predefinite (POLLERR</tt>, POLLHUP</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 ingnorat in versiunile recente ale nucleului, acesta ajustand dinamic dimensiunea setului de descriptori asociat obiectului epoll.

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>epollfd</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):

= Linux - generalizarea multiplexarii =

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>.

eventfd
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>).

signalfd
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.29, glibc 2.9) a fost unificat subsistemul de operații I/O al nucleului Linux cu interfața eventfd si exista si suport in biblioteca standard C pentru acest apel (incepand cu 2.8).

ATENTIE! Versiunea glibc instalata in laborator (2.7) nu are suport pentru eventfd, fiind necesara folosirea wrapper-ului de mai sus, ce foloseste syscall.

= Windows - I/O asincron (overlapped) =

Î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:

= Exerciții =

Prezentare
Pentru a urmări mai ușor noțiunile expuse la începutul laboratorului folosiți această prezentare (pdf) (odp).

Exerciții de laborator
Folositi arhiva de sarcini a laboratorului.

Linux

 * 1) pollpipe (2p) Creați folosind fork(2) o aplicație de test pentru poll(2). Aplicația folosește un server (părintele) și <tt>CLIENT_COUNT</tt> clienți (copiii) ce comunică prin pipe-uri anonime.
 * 2) * server-ul:
 * 3) ** construiește un vector de pipe-uri (în funcția <tt>main</tt>);
 * 4) ** creează clienții;
 * 5) ** blochează în așteptarea datelor pe acestea și tipărește datele primite;
 * 6) ** termină execuția după ce a primit date de la fiecare client;
 * 7) * clienții:
 * 8) ** așteaptă un număr aleator de secunde (mai mic decât 10);
 * 9) ** scriu în pipe-ul corespunzător un șir de <tt>MSG_SIZE</tt> caractere de forma <tt>" : "</tt> (<tt>'a' + random % 30</tt>);
 * 10) ** scrierile și citirile în pipe-uri de până la <tt>PIPE_BUF</tt> octeți (<tt>4096</tt> pe Linux) sunt atomice.
 * Hints:
 * 1) ** Consultați secțiunea poll și Pipe-uri în Linux.
 * 2) epollpipe (2p) Modificați codul anterior pentru a folosi epoll.
 * 3) eventpipe (2p) Modificați codul anterior pentru a adăuga funcționaliatea de deînregistrare a clienților.
 * 4) * Se va folosi un descriptor eventfd prin intermediul căruia serverul este notificat de încheierea execuției unui client.
 * 5) * server-ul:
 * 6) ** creează eventfd-ul și îl adaugă la descriptor-ul epoll;
 * 7) ** la primirea unui eveniment prin acesta, dacă primii 32 biți sunt <tt>MAGIC_EXIT</tt>, atunci scoate capătul pipe-ului corespunzător din <tt>epoll</tt> şi îl inchide;
 * 8) ** există 2 opțiuni pentru a determina pipe-ul care trebuie scos și închis, pe baza restului de 32 de biți din event:
 * 9) *** îi folosiți pentru PID-ul client-ului și suplimentar retineti in server o asociere client-pipe
 * 10) *** va folositi de mostenirea descriptorilor la fork
 * 11) ** face exit dupa ce a primit MAGIC_EXIT de la toti cei CLIENT_COUNT clienti
 * 12) * clientii:
 * 13) ** fac trimiterea datelor intr-o bucla cu IT_COUNT iteratii
 * 14) ** la fiecare iteratie, verifica daca o valoare random este multiplu de IT_COUNT, si daca da trimite un eveniment MAGIC_EXIT si termina executia.
 * 15) signalpipe (2p) Inlocuiti in codul anterior notificarea (de terminare a clientilor) bazata pe eventfd cu una bazata pe semnale.
 * 16) * server-ul:
 * 17) ** creează un descriptor via signalfd pentru SIGCHLD si-l adauga la epoll
 * 18) ** la primirea unui semnal, prin read(2) pe descriptorul creat, determina PID-ul copilului defunct, afiseaza un mesaj si scote pipe-ul din epoll.

Windows

 * 1) aio (2p) Scrierea unor date aleatoare în fișiere folosind operații sincrone și asincrone. Implementați funcțiile <tt>do_io_sync</tt>, respectiv <tt>do_io_async</tt>.
 * 2) * Pentru operațiile sincrone puteți folosi funcția <tt>xwrite</tt> definită în fișier.
 * 3) * Va trebui să alocați spațiu pentru structurile <tt>OVERLAPPED</tt> pentru toate cele 4 fișiere.
 * 4) * Pentru inițializarea structurilor <tt>OVERLAPPED</tt> se recomandă implementarea funcției <tt>init_overlapped</tt>.
 * 5) * Folosiți <tt>GetOverlappedResult</tt> pentru realizarea operațiilor asincrone pe cele 4 fișiere.
 * 6) * 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>.
 * 7) * Folosiți macro-ul <tt>IO_OP_TYPE</tt> pentru a determina comportamentul programului.

= Soluții =


 * Soluții exerciții laborator 10

= Resurse utile =


 * 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 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
 * libevent - bibliotecă de operații asincrone
 * C10K - Problema celor 10.000 de clienți