Operatii IO Avansate
Operatii IO Avansate
Contents
1 Linux - multiplexarea I/O 1.1 select 1.2 poll 1.2.1 Avantaje poll 1.2.2 Dezavantaje poll 1.3 epoll 1.3.1 Crearea unui obiect epoll 1.3.2 Adugarea/eliminarea descriptorilor la/de la obiectul epoll 1.3.3 Ateptarea unui eveniment I/O 1.3.4 Edge-triggered sau level-triggered 1.3.5 Exemplu folosire epoll 2 Linux - generalizarea multiplexarii 2.1 eventfd 2.2 signalfd 3 Windows - I/O asincron (overlapped) 3.1 Apeluri pentru transfer asincron (Overlapped I/O) 3.2 Interogarea/ateptarea operaiilor asincrone 3.3 Notificarea ncheierii operaiilor asincrone 3.4 Operatii asincrone pe socketi 4 Exerciii 4.1 Prezentare 4.2 Exerciii de laborator 4.2.1 Linux 4.2.2 Windows 5 Soluii 6 Resurse utile
while (true) { ateapt producerea unui eveniment pe unul din descriptori pentru fiecare descriptor pe care s-a produs un eveniment I/O { trateaz evenimentul I/O } }
Detaliile variaz de la o implementare la alta, dar secvena de pseudocod de mai sus reprezint structura de baz pentru serverele care folosesc multiplexarea I/O.
select
O prim soluie este utilizarea funciilor select sau pselect. Folosirea acestor funcii conduce la blocarea thread-ului curent pn la producerea unui eveniment I/O pe un set de descriptori de fiier, a unei erori pe set sau pn la expirarea unui timer. Funciile folosesc un set de descriptori de fiier pentru a preciza fiierele/socketii pe care thread-ul curent va atepta producerea evenimentelor I/O. Tipul de date folosit pentru definirea acestui set este fd_set, care este, de obicei, o masc de bii. Funciile select i pselect sunt definite conform POSIX.1-2001 n sys/select.h
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeou
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: funcia 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 apariia unui eveniment pe unul din descriptori, toi descriptorii pui n set nainte de select trebuie testai pentru a vedea pe care din ei a aprut evenimentul; la fiecare apel, acelai set de descriptori este transmis n kernel.
poll
Funcia poll consolideaz argumentele funciei select i permite notificarea pentru o gam mai larg de evenimente. Funcia se definete ca mai jos:
#include <sys/poll.h> int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
select
Timeout-ul este specificat in milisecunde. In caz de valoare negativa, semnificatia este de asteptare pentru o perioada nedefinita ("infinit"). Structura struct pollfd este definit n sys/poll.h:
#include <sys/poll.h> struct pollfd { int fd; /* file descriptor */ short events; /* evenimente solicitate */ short revents; /* evenimente aprute */ };
Functia poll permite astfel asteptarea evenimentelor descrise de vectorul ufds de dimensiune nfds. n cadrul structurii struct pollfd, cmpul events este o masc de bii n care se specific evenimentele urmrite de poll pentru descriptorul fd. revents este, de asemenea, o masc de bii completat de kernel cu evenimentele aprute n momentul n care apelul se ntoarce (POLLIN, POLLOUT) sau valori predefinite (POLLERR, POLLHUP, POLLNVAL) pentru situaii speciale. n caz de succes funcia returneaz un numr diferit de zero reprezentnd numrul de structuri pentru care revents nu e zero (cu alte cuvinte toi 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, funcia poll poate fi ntrerupt de semnale, caz n care va ntoarce -1 i errno va fi setat la EINTR. Un exemplu de utilizare a poll este prezentat n continuare:
#define MAX_PFDS 32
[...] struct pollfd pfds[MAX_PFDS]; int nfds; int listenfd, sockfd; /* listener socket; connection socket */ nfds = 0; /* read user data from standard input */ pfds[nfds].fd = STDIN_FILENO; pfds[nfds].events = POLLIN; nfds++; /* TODO ... create server socket (listener) */
/* add listener socket */ pfds[nfds].fd = listenfd pfds[nfds].events = POLLIN; nfds++; while (1) { /* server loop */ /* wait for readiness notification */ poll(pfds, nfds, -1); if ((pfds[1].revents & POLLIN) != 0) { /* TODO ... handle new connection */ } else if ((pfds[0].revents & POLLIN) != 0) { /* TODO ... read user data from standard input */
poll
Avantaje poll
transmiterea setului de descriptori este mai simpl dect n cazul funciei select; setul de descriptori nu trebuie reconstruit la fiecare apel poll;
Dezavantaje poll
ineficien - la apariia unui eveniment, trebuie parcurs tot setul de descriptori pentru a gsi descriptorul pe care a aprut evenimentul; la fiecare apel, acelai set de descriptori (care poate fi mare) este copiat n kernel i napoi.
epoll
Funciile select i poll nu sunt scalabile la un numr mare de conexiuni pentru c la fiecare apel al lor trebuie transmis toat lista de descriptori. n astfel de situaii, la fiecare pas, trebuie contruit lista de descriptori i apelat poll sau select care copiaz tot setul n kernel. La apariia unui eveniment va fi marcat corespunztor descriptorul. Utilizatorul trebuie s parcurg tot setul de descriptori pentru a-i da seama pe care dintre ei a aprut evenimentul. n acest fel se ajunge s se petreac tot mai mult timp scannd dup evenimente n setul de descriptori i tot mai puin timp fcnd I/O. Din acest motiv, diverse sisteme au implementat interfee scalabile dar non-portabile: /dev/poll pe Solaris; kqueue pe FreeBSD; epoll pe Linux. Aceste interfee rezolv problemele asociate cu select i poll i rezolv problemele de scalabilitate. Pentru a folosi epoll, trebuie inclus sys/epoll.h. Funciile asociate sunt epoll_create, epoll_ctl i epoll_wait. Interfaa epoll ofer funcii pentru crearea unui obiect epoll, adugarea sau eliminarea de descriptori de fiiere/sockei la obiectul epoll i ateptarea unui eveniment pe unul dintre descriptori.
Avantaje poll
Apelul epoll_create faciliteaz crearea unui descriptor de fiier ce va fi ulterior folosit pentru ateptarea de evenimente. Descriptorul ntors va trebui nchis folosind close. Argumentul size este ingnorat in versiunile recente ale nucleului, acesta ajustand dinamic dimensiunea setului de descriptori asociat obiectului epoll.
Apelul epoll_ctl permite specificarea evenimentelor care vor fi ateptate. Cmpul event descrie evenimentul asociat descriptorului fd care poate fi adugat, ters sau modificat n funcie de valoarea argumentului op: EPOLL_CTL_ADD: pentru adaugare; EPOLL_CTL_MOD: pentru modificare; EPOLL_CTL_DEL: pentru tergere. Primul argument al apelului epoll_ctl (epollfd) este descriptorul ntors de epoll_create. Structura struct epoll_event specific evenimentele ateptate
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; epoll_data_t data; };
Tipuri de evenimente care pot fi urmrite sau primite pe un descriptor: EPOLLIN: descriptorul asociat are date de citit; EPOLLOUT: descriptorul asociat are loc n buffer-ul asociat pentru a scrie date; EPOLLERR: a aprut o eroare pe descriptorul asociat. Union-ul epoll_data poate fi folosit pentru a asocia o cheie cu descriptorul urmrit.
Funcia epoll_wait este echivalentul funciilor select i poll. Este folosit pentru ateptarea unui eveniment la unul din descriptorii asociai descriptorului epollfd. La revenirea apelului programatorul nu va trebui s parcurg toi descriptorii configurai ci numai cei care au evenimente produse. Argumentul events va marca o zon de memorie unde vor fi plasate maxim maxevents evenimente de nucleu. Presupunnd c valoarea cmpului timeout este -1 (ateptare nedefinit), apelul se va ntoarce imediat dac exist evenimente asociate, sau se va ploca pn la apariia unui eveniment. La fel ca i n cazul select/pselect si poll/ppoll, exist apelul epoll_pwait care permite precizarea unei mti de semnale.
bufferul de scriere este gol) care ar trebui ignorat. La urmtorul apel epoll_wait nu se mai genereaz EPOLLOUT 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 urmtorul epoll_wait se genereaz EPOLLOUT, pentru c s-a schimbat starea socketului. Mai pe scurt, asta are ca efect faptul c nu mai trebuie activat/deactivat EPOLLOUT ca n cazul level-triggered.
/* create epoll descriptor */ epfd = epoll_create(EPOLL_INIT_BACKSTORE); /* read user data from standard input */ [Link] = STDIN_FILENO; /* key is file descriptor */ [Link] = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); /* TODO ... create server socket (listener) */
/* add listener socket */ [Link] = listenfd; /* key is file descriptor */ [Link] = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); while (1) { /* server loop */ struct epoll_event ret_ev; /* wait for readiness notification */ epoll_wait(epfd, &ret_ev, 1, -1); if ((rev_ev.[Link] == listenfd && ((ret_ev.events & EPOLLIN) != 0)) { /* TODO ... handle new connection */ } else if ((rev_ev.[Link] == STDIN_FILENO && ((ret_ev.events & EPOLLIN) != 0)) { /* TODO ... read user data from standard input */ } else { /* TODO ... handle message on connection sockets */ } } [...]
O problem a funciilor de multiplexare de mai sus (select, poll, epoll) este aceea c sunt limitate la descriptori de fiier. Altfel spus, se pot atepta doar evenimente asociate cu un fiier/socket: gata de citire, gata de scriere. De multe ori ns se dorete s existe un punct comun de ateptare a unui semnal, a unui semafor, a unui proces, a unei operaii de intrare/ieire, a unui timer. n Windows, acest lucru se poate realiza cu ajutorul funciei WaitForMultipleObjects i pe faptul c majoritatea mecanismelor din Windows sunt folosit cu ajutorul tipului de date HANDLE.
eventfd
Pentru a asigura n Linux posibilitate ateptrii de evenimente multiple s-a definit interfaa eventfd. Cu ajutorul aceste interfee i combinat cu interfeele de multiplexare I/O existente, kernel-ul poate notifica o aplicaie utilizator de orice tip de eveniment. Interfaa eventfd este prezent n nucleul Linux ncepnd 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 interfaa syscall:
#include <sys/syscall.h> static int eventfd(unsigned int initval, int flags) { return syscall(__NR_eventfd, initval, 0); }
Cele trei apeluri de baz pentru extinderea funcionalitii multiplexrii I/O sunt eventfd, signalfd i timerfd_create. Toate cele trei apeluri ntorc un descriptor de fiier pe care se vor putea primi notificri (semnale, timere etc.). Operaiile posibile pe descriptorul de fiier ntors sunt: write: pentru transmiterea unui mesaj de notificare pe descriptor; read: pentru primirea unui mesaj care nseamn primirea notificrii; select, poll, epoll: pentru multiplexarea I/O; close: pentru nchiderea descriptorului i eliberarea resurselor asociate. n urmtorul exemplu, apelul eventfd este folosit pentru notificarea procesului printe de ctre procesul fiu. Codul este cel prezent n pagina de manual (man eventfd).
[...] int efd; uint64_t u; /* create eventfd file descriptor */ efd = eventfd(0, 0); switch (fork()) { case 0: /* notify parent process */ s = write(efd, &u, sizeof(uint64_t)); printf("Child completed write loop\n"); exit(EXIT_SUCCESS);
eventfd
default: printf("Parent about to read\n"); /* wait for notification */ s = read(efd, &u, sizeof(uint64_t)); exit(EXIT_SUCCESS); [...]
signalfd
Apelul signalfd este folosit n mod similar pentru recepionarea de semnale prin intermediul unui descriptor de fiier. Pentru a putea recepiona un semnal cu ajutorul interfeei signalfd, 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 (man signalfd).
/* at this point Linux-specific headers are required to use struct signalfd_siginfo */ #include <linux/types.h> #include <linux/signalfd.h> #define SIZEOF_SIG #define SIZEOF_SIGSET (_NSIG / 8) (SIZEOF_SIG > sizeof(sigset_t) ? \ sizeof(sigset_t): SIZEOF_SIG)
/* on par with the current glibc wrapper */ static int signalfd(int fd, const sigset_t *mask, int flags) { return syscall(__NR_signalfd, fd, mask, SIZEOF_SIGSET); } [...] sigset_t mask; int sfd; struct signalfd_siginfo fdsi; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGQUIT);
/* CTRL-C */ /* CTRL-\ */
/* * Block signals so that they arent handled * according to their default dispositions */ sigprocmask(SIG_BLOCK, &mask, NULL); /* create signalfd descriptor */ sfd = signalfd(-1, &mask); for (;;) { /* wait for signals to be delivered by user */ s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo)); if (fdsi.ssi_signo == SIGINT) { printf("Got SIGINT\n"); } else if (fdsi.ssi_signo == SIGQUIT) { printf("Got SIGQUIT\n"); exit(EXIT_SUCCESS); } else {
signalfd
Interfaa eventfd permite unificarea mecanismelor de notificare ale kernel-ului ntr-un descriptor de fiier care va fi folosit de utilizator. n acest moment (kernel 2.6.29, glibc 2.9) a fost unificat subsistemul de operaii I/O al nucleului Linux cu interfaa 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.
Ultimul argument este un pointer la structura OVERLAPPED care descrie operaia asincron de realizat. Pentru ca operaiile asincrone s poat decurge pe fiiere, fiierul trebuie deschis cu flag-ul FILE_FLAG_OVERLAPPED:
HANDLE hFile;
10
/* open file for reading using overlapped I/O */ hFile = CreateFile( files[i], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL <PIPE> FILE_FLAG_OVERLAPPED, NULL);
Dup cum se poate observa, ultimul argument al apelului GetOverlappedResult difereniaz ntre interogarea operaiei asincrone (FALSE) sau ateptarea ncheierii acesteia (TRUE).
11
/* TODO ... connect socket */ [Link] = buffer; [Link] = BUFSIZ; flags = 0; /* start asynchronous I/O */ WSARecv(s, &DataBuf, 1, &recvBytes, &flags, &ov, NULL);
Exerciii
Prezentare
Pentru a urmri mai uor noiunile expuse la nceputul laboratorului folosii aceast prezentare (pdf) (odp).
Exerciii de laborator
Folositi arhiva de sarcini a laboratorului.
Linux
1. pollpipe (2p) Creai folosind fork(2) o aplicaie de test pentru poll(2). Aplicaia folosete un server (printele) i CLIENT_COUNT clieni (copiii) ce comunic prin pipe-uri anonime. server-ul: construiete un vector de pipe-uri (n funcia main); creeaz clienii; blocheaz n ateptarea datelor pe acestea i tiprete datele primite; termin execuia dup ce a primit date de la fiecare client; clienii: ateapt un numr aleator de secunde (mai mic dect 10); scriu n pipe-ul corespunztor un ir de MSG_SIZE caractere de forma "<pid>:<caracter random>" ('a' + random() % 30); scrierile i citirile n pipe-uri de pn la PIPE_BUF octei (4096 pe Linux) sunt atomice. Hints: Consultai seciunea poll i Pipe-uri n Linux. 2. epollpipe (2p) Modificai codul anterior pentru a folosi epoll. 3. eventpipe (2p) Modificai codul anterior pentru a aduga funcionaliatea de denregistrare a clienilor. Se va folosi un descriptor eventfd prin intermediul cruia serverul este notificat de ncheierea execuiei unui client. server-ul: creeaz eventfd-ul i l adaug la descriptor-ul epoll; la primirea unui eveniment prin acesta, dac primii 32 bii sunt MAGIC_EXIT, atunci scoate captul pipe-ului corespunztor din epoll i l inchide;
Exerciii
12
exist 2 opiuni pentru a determina pipe-ul care trebuie scos i nchis, pe baza restului de 32 de bii din event: i folosii pentru PID-ul client-ului i suplimentar retineti in server o asociere client-pipe va folositi de mostenirea descriptorilor la fork() face exit dupa ce a primit MAGIC_EXIT de la toti cei CLIENT_COUNT clienti clientii: fac trimiterea datelor intr-o bucla cu IT_COUNT iteratii la fiecare iteratie, verifica daca o valoare random() este multiplu de IT_COUNT, si daca da trimite un eveniment MAGIC_EXIT si termina executia. 4. signalpipe (2p) Inlocuiti in codul anterior notificarea (de terminare a clientilor) bazata pe eventfd cu una bazata pe semnale. server-ul: creeaz un descriptor via signalfd pentru SIGCHLD si-l adauga la epoll 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 fiiere folosind operaii sincrone i asincrone. Implementai funciile do_io_sync, respectiv do_io_async. Pentru operaiile sincrone putei folosi funcia xwrite definit n fiier. Va trebui s alocai spaiu pentru structurile OVERLAPPED pentru toate cele 4 fiiere. Pentru iniializarea structurilor OVERLAPPED se recomand implementarea funciei init_overlapped. Folosii GetOverlappedResult pentru realizarea operaiilor asincrone pe cele 4 fiiere. Funciile trebuie s scrie coninutul bufferului g_buffer n cele 4 fiiere cu numele dat de vectorul files. Folosii macro-ul IO_OP_TYPE pentru a determina comportamentul programului.
General libevent - bibliotec de operaii asincrone C10K - Problema celor 10.000 de clieni
Resurse utile
14