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ță 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 mecanisme 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 dintre 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:
set = setul de descriptori urmăriți while (true) { așteaptă producerea unui eveniment pe unul dintre 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 secvența de pseudocod de mai sus reprezintă structura de bază pentru serverele care folosesc multiplexarea I/O.
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
#include <sys/select.h> int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout );
Deoarece și apelul poll este specificat în standardul POSIX (deci oferă portabilitate mare), dar oferă performanță mai bună, nu vom insista asupra apelului select.
Avantaje:
Dezavantaje:
select
;select
trebuie testați pentru a vedea pe care dintre ei a apărut evenimentul;
Funcția poll consolidează argumentele funcției select
și permite notificarea pentru o gamă mai largă de evenimente. Funcția se definește ca mai jos:
#include <sys/poll.h> int poll( struct pollfd *ufds, unsigned int nfds, int timeout );
Timeout-ul este specificat în milisecunde. În caz de valoare negativă, semnificația este de așteptare pentru o perioadă nedefinită (“infinit”).
Structura pollfd
este definită în sys/poll.h
:
#include <sys/poll.h> struct pollfd { int fd; /* file descriptor */ short events; /* evenimente solicitate */ short revents; /* evenimente apărute */ };
Funcția poll
permite astfel așteptarea evenimentelor descrise de vectorul ufds
de dimensiune nfds
.
În cadrul structurii pollfd
avem:
events
este o mască de biți în care se specifică evenimentele urmărite de poll
pentru descriptorul fd
(POLLIN
- există date ce pot fi citite, POLLOUT
- se pot scrie date). revents
este, de asemenea, o mască de biți completată de kernel cu evenimentele apărute în momentul în care apelul se întoarce (POLLIN
, POLLOUT
) sau cu valori predefinite (POLLERR
, POLLHUP
, POLLNVAL
) 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 revents
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 poll
poate fi întreruptă de semnale, caz în care va întoarce -1 și errno va fi setat la EINTR
.
Un exemplu de utilizare pentru 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 */ } else { /* TODO ... handle message on connection sockets */ } } [...]
Avantaje poll
select
;poll
.Dezavantaje poll
Funcțiile select
și poll
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 construită lista de descriptori și apelat poll
sau select
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 select
și poll
, dar și problemele de scalabilitate.
Pentru a folosi epoll, trebuie inclus sys/epoll.h
. Interfața epoll
oferă funcții pentru crearea unui obiect epoll
(epoll_create
), adăugarea sau eliminarea de descriptori de fișiere/sockeți la obiectul epoll (epoll_ctl
) și așteptarea unui eveniment pe unul dintre descriptori (epoll_wait
).
Pentru crearea unui obiect epoll
se folosește funcția epoll_create:
int epoll_create(int size);
Apelul epoll_create
facilitează crearea unui descriptor de fișier ce va fi ulterior folosit pentru așteptarea de evenimente. Descriptorul întors va trebui la final închis folosind apelul close
.
Argumentul size
este ignorat în versiunile recente ale nucleului, acesta ajustând dinamic dimensiunea setului de descriptori asociat obiectului epoll
.
Operațiile de adăugare/eliminare de descriptori se realizează cu ajutorul funcției epoll_ctl:
int epoll_ctl( int epollfd, int op, int fd, struct epoll_event *event );
Apelul epoll_ctl
permite specificarea evenimentelor care vor fi așteptate.
Primul argument al apelului epoll_ctl
(epollfd) este descriptorul întors de epoll_create
.
Câmpul event
descrie evenimentul asociat descriptorului fd
care poate fi adăugat, șters sau modificat în funcție de valoarea argumentului op:
EPOLL_CTL_ADD
: pentru adăugare;EPOLL_CTL_MOD
: pentru modificare;EPOLL_CTL_DEL
: pentru ștergere.
Structura epoll_event
specifică evenimentele așteptate:
typedef union epoll_data { void *ptr; /* Pointer to user-defined data */ int fd; /* File descriptor */ __uint32_t u32; /* 32-bit integer */ __uint64_t u64; /* 64-bit integer */ } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events (bit mask) */ epoll_data_t data; /* User data*/ };
Exemple de evenimente:
EPOLLIN
- fișierul este disponibil pentru citire, EPOLLOUT
- fișierul este disponibil pentru scriere.
Thread-ul curent așteaptă producerea unui eveniment I/O la unul dintre descriptorii asociați obiectului epoll
prin intermediul funcției epoll_wait:
int epoll_wait( int epollfd, struct epoll_event* events, int maxevents, int timeout );
Funcția epoll_wait
este echivalentul funcțiilor select
și poll
. Este folosită pentru așteptarea unui eveniment la unul din descriptorii asociați obiectului epoll
.
La revenirea apelului, utilizatorul nu va trebui să parcurgă toți descriptorii configurați, ci numai cei care au evenimente produse. Argumentul events
va marca o zonă de memorie unde vor fi plasate maxim maxevents
evenimente de nucleu. Presupunând că valoarea câmpului timeout
este -1 (așteptare nedefinită), apelul se va întoarce imediat dacă există evenimente asociate, sau se va bloca până la apariția unui eveniment.
La fel ca și în cazul select/pselect
și poll/ppoll
, există apelul epoll_pwait
care permite precizarea unei măști de semnale.
Interfața epoll
are două comportamente posibile: edge-triggered
sau level-triggered
. Se poate folosi unul sau altul, în funcție de prezența flag-ului EPOLLET
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) epoll_wait
va raporta EPOLLIN
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 epoll_wait. În modul level-triggered se va raporta imediat EPOLLIN. Î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.
Mai jos este prezentat un exemplu de utilizare a epoll
echivalent cu exemplele pentru select și poll (server care multiplexează mai multe conexiuni pe sockeți și intrarea standard):
#define EPOLL_INIT_BACKSTORE 2 [...] int listenfd, sockfd; /* listener socket; connection socket */ struct epoll_event ev; /* create epoll descriptor */ epfd = epoll_create(EPOLL_INIT_BACKSTORE); /* read user data from standard input */ ev.data.fd = STDIN_FILENO; /* key is file descriptor */ ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); /* TODO ... create server socket (listener) */ /* add listener socket */ ev.data.fd = listenfd; /* key is file descriptor */ ev.events = 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.data.fd == listenfd && ((ret_ev.events & EPOLLIN) != 0)) { /* TODO ... handle new connection */ } else if ((rev_ev.data.fd == STDIN_FILENO && ((ret_ev.events & EPOLLIN) != 0)) { /* TODO ... read user data from standard input */ } else { /* TODO ... handle message on connection sockets */ } } [...]
O problemă a funcțiilor de multiplexare de mai sus (select
, poll
, epoll
) 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 WaitForMultipleObjects
datorită faptului că majoritatea mecanismelor din Windows sunt folosite cu ajutorul tipului de date HANDLE
.
Pentru a asigura în Linux posibilitatea așteptării de evenimente multiple s-a definit interfața eventfd
. Cu ajutorul acestei 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 și este suportată de către glibc începând cu versiunea 2.8.
Interfața eventfd
permite unificarea mecanismelor de notificare ale kernel-ului într-un descriptor de fișier care va fi folosit de utilizator.
Cele trei apeluri de bază pentru extinderea funcționalității multiplexării I/O sunt: eventfd, signalfd și timerfd_create.
#include <sys/eventfd.h> int eventfd(unsigned int initval, int flags);
#include <sys/signalfd.h> int signalfd(int fd, const sigset_t *mask, int flags);
#include <sys/timerfd.h> int timerfd_create(int clockid, int flags);
Toate cele trei apeluri întorc un descriptor de fișier pe care se vor putea primi notificări (evenimente, semnale, timere). Operațiile posibile pe descriptorul de fișier întors sunt:
write
: pentru transmiterea unui mesaj de notificare pe descriptor;read
: pentru primirea unui mesaj care înseamnă primirea notificării;select
, poll
, epoll
: pentru multiplexarea I/O;close
: pentru închiderea descriptorului și eliberarea resurselor asociate.
În următorul exemplu, apelul eventfd
este folosit pentru notificarea procesului părinte de către 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); default: printf("Parent about to read\n"); /* wait for notification */ s = read(efd, &u, sizeof(uint64_t)); exit(EXIT_SUCCESS); [...]
Apelul signalfd 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 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 (_NSIG / 8) #define SIZEOF_SIGSET (SIZEOF_SIG > sizeof(sigset_t) ? \ sizeof(sigset_t): SIZEOF_SIG) [...] sigset_t mask; int sfd; struct signalfd_siginfo fdsi; sigemptyset(&mask); sigaddset(&mask, SIGINT); /* CTRL-C */ sigaddset(&mask, SIGQUIT); /* CTRL-\ */ /* * Block signals so that they aren't handled * according to their default dispositions */ sigprocmask(SIG_BLOCK, &mask, NULL); /* create signalfd descriptor */ sfd = signalfd(-1, &mask, 0); 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 { printf("Read unexpected signal\n"); } } [...]
În mod clasic, operațiile de lucru cu datele aflate pe suporturi externe înseamnă utilizarea apelurilor sincrone de tipul read
, write
și fsync
. 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.
Standardul POSIX.1b
definește un nou set de operații I/O care pot reduce semnificativ timpul pe care o aplicație îl petrece așteptând pentru I/O. Noile funcții permit unui program să inițieze una sau mai multe operații de I/O și să-și continue lucrul normal în timp ce operațiile de I/O sunt executate în paralel.
Această funcționalitate este disponibilă dacă se instalează biblioteca libaio
:
so$ apt-cache search libaio libaio-dev - Linux kernel AIO access library - development files libaio1 - Linux kernel AIO access library - shared library libaio1-dbg - Linux kernel AIO access library - debugging symbols so$ sudo apt-get install libaio1 libaio-dev
Totodată, programul care folosește acest API trebuie să includă fișierul header libaio.h
și să link-eze biblioteca libaio
. Toate funcțiile și structurile de care vom vorbi în continuare se pot găsi în acest fișier header. Dacă ați instalat pachetul, fișierul se găsește în /usr/include/libaio.h
.
Structura iocb
folosită pentru încapsularea unei operații asincrone. Structura este definită în header-ul libaio.h
.
struct iocb { PADDEDptr(void *data, __pad1); /* Return in the io completion event */ PADDED(unsigned key, __pad2); /* For use in identifying io requests */ short aio_lio_opcode; short aio_reqprio; int aio_fildes; /* Perform async IO on this file descriptor */ union { struct io_iocb_common c; /* common read/write operation */ struct io_iocb_vector v; /* vectored read/write operations */ struct io_iocb_poll poll; struct io_iocb_sockaddr saddr; /* socket read/write operations */ } u; };
În principiu, nu se lucrează direct cu elementele din structura iocb
. Pentru asta există funcții de inițializare:
void io_prep_pread( struct iocb *iocb, int fd, void *buf, size_t count, long long offset );
void io_prep_pwrite( struct iocb *iocb, int fd, void *buf, size_t count, long long offset );
void io_prep_preadv( struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt, long long offset );
void io_prep_pwritev( struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt, long long offset );
Pentru folosirea acesteia o aplicație va include libaio.h
. Un exemplu de inițializare a acestei structuri este:
#include <libaio.h> /* ... */ struct iocb iocb; memset(&iocb, 0, sizeof(iocb)); io_prep_pwrite(&iocb, fd, buffer, BUFER_SIZE, 0);
Orice operație sau set de operații Linux AIO sunt identificate printr-o valoare de tipul io_context_t
ce reprezintă un context de operații asincrone.
Inițializarea, respectiv distrugerea contextului se realizează cu ajutorul funcțiilor io_setup și io_destroy:
#include <libaio.h> int io_setup(unsigned nr_events, aio_context_t *ctxp);
#include <libaio.h> int io_destroy(aio_context_t ctx);
Un exemplu de inițializare și distrugere a contextului:
#include <libaio.h> io_context_t ctx; int num_ops = 10; /* crează un context de I/O asincron capabil să primească măcar num_ops evenimente */ if (io_setup(num_ops, &ctx) < 0) { /* handle error */ } /* do work */ /* ... */ /* distruge contextul și anulează toate operațiile I/O asincrone necompletate */ if (io_destroy(ctx) < 0) { /* handle error */ }
Pentru realizarea unei operații asincrone se folosește funcția io_submit. Această funcție declanșează pornirea operațiilor asincrone definite în vectorul de pointeri de structuri iocb
primit ca argument. Această funcție nu blochează procesul curent.
#include <libaio.h> int io_submit( aio_context_t ctx_id, long nr, struct iocb **iocbpp );
Un exemplu de utilizare este:
#include <libaio.h> #define NUM_AIO_OPS 10 struct iocb iocb[NUM_AIO_OPS]; /* array of asynchronous operations */ struct iocb *piocb[NUM_AIO_OPS]; /* array of pointers to asynchronous operations */ io_context_t ctx = 0; /* init context, iocb */ /* fill piocb */ for (i = 0; i < NUM_AIO_OPS; i++) piocb[i] = &iocb[i]; /* * Submit NUM_AIO_OPS async operations in context 'ctx' * This does not wait for the operations to finish */ if (io_submit(ctx, NUM_AIO_OPS, piocb) < 0) { /* handle error */ } /* Do some other stuff in paralel with the execution of async I/O operations */
Pentru așteptarea încheierii unei operații AIO și obținerea de informații despre rezultatul acesteia se folosește funcția io_getevents. Funcția folosește structura io_event
pentru a obține informații despre încheierea unei operații asincrone.
#include <linux/time.h> #include <libaio.h> int io_getevents( aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout );
Un exemplu de utilizare este:
#include <libaio.h> #define NUM_AIO_OPS 10 io_context_t ctx = 0; struct io_event events[NUM_AIO_OPS]; /* aio result array */ /* ... */ /* * Wait _exactly_ NUM_AIO_OPS async operations to finish * min_nr - min nummber of async aio to finish for the function to return * max_nr - max nummber of async aio operations that can be returned */ rc = io_getevents(ctx, NUM_AIO_OPS, /* min_nr */ NUM_AIO_OPS, /* max_nr */ events, /* vector to store completed events */ NULL); /* no timeout */ if (rc < 0) { /* handle error */ }
Este utilă folosirea apelurilor de multiplexare I/O (select
, poll
, epoll
) ș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 eventfd.
Pentru aceasta se configurează flag-ul IOCB_FLAG_RESFD
iar câmpul resfd
al structurii iocb
va conține un descriptor eventfd
ce va fi notificat în momentul încheierii operației asincrone. Acest lucru se poate configura din start apelând funcția:
void io_set_eventfd(struct iocb *iocb, int eventfd)
Apelul io_getevents
este în continuare util pentru a obține informații despre încheierea operațiilor. eventfd
oferă doar mecanismul de așteptare a acestora.
#include <libaio.h> int efd; // creare event cu valoare inițială 0, fără flaguri speciale efd = eventfd(0, 0); /* ... */ struct iocb *iocb; /* ... */ /* use eventfd */ io_set_eventfd(&iocb[i], efd); /* ... */ u_int64_t efd_val; if (read(efd, &efd_val, sizeof(efd_val)) < 0) { /* handle error */ } printf("%llu operations have completed\n", efd_val);
Citirea din descriptorul eventfd reprezintă numărul de operații I/O încheiate. Această valoare va fi, de obicei, folosită ca al doilea și al treilea argument al io_getevents
.
Folosind integrarea operațiilor asincrone cu eventfd și mecanismele de multiplexare I/O (select
, poll
, epoll
) se poate aștepta unificat încheierea unei operații asincrone sau sosirea de date pe sockeți (Hint: util pentru Tema 5
).
Este un apel de sistem ce permite transferul de date între 2 descriptori de fișier, dintre care cel puțin unul este pipe. Avantajul este ca nu se folosește un buffer (byte array) în userspace.
#define _GNU_SOURCE // trebuie definit pentru că splice este o extensie nespecificată de standardele POSIX/SYSV/BSD/etc. #include <fcntl.h> long splice( int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags );
fd_in
reprezintă un pipe, atunci pointer-ul la offset off_in
trebuie să fie NULLoff_in
este NULL, atunci datele sunt citite de la fd_in
de la offset-ul curent, acesta modificându-se corespunzătoroff_in
trebuie sa fie un pointer la un întreg care reprezintă offset-ul de start de la care se va face citirea, iar offset-ul propriu descriptorului fd_in
rămâne neschimbat fd_out
și off_out
, la scriereExemplu de folosire:
int pipe, file1, file2; loff_t offset = 0; size_t count = 4096; // ... deschideri fișiere, creare pipe splice(file1, &offset, pipe, NULL, count, 0); splice(pipe, NULL, file2, &offset, count, 0);
Vectored I/O (sau scatter/gather I/O) reprezintă o metodă prin intermediul căreia un singur apel permite scrierea de date din mai multe buffere către un flux de ieșire sau citirea de date de la un flux de intrare în mai multe buffere. Bufferele sunt precizate ca un vector de buffere, de unde și denumirea de vectored I/O.
Apelurile din clasa vectored I/O sunt utile în momentul în care datele sunt disparate/dezasamblate în memorie și se dorește “concatenarea” acestora într-un singur flux de scriere sau “desfacerea” acestora dintr-un flux de citire. Un exemplu îl reprezintă pachetele de rețea în care headerele, datele și trailerele se găsesc, de obicei, în locații de memorie diferite pentru a facilita prelucrarea acestora. Folosirea Vectored I/O permite asamblarea/dezasamblarea pachetului în/din mai multe zone de memorie printr-o singură operație. Nu este nevoie de crearea unui buffer nou cu pachetele concatenate, drept care Vectored I/O poate fi considerat o formă de zero-copy.
Apeluri:
Funcțiile readv și writev sunt folosite în sistemele Unix ca operații de tipul vectored I/O. Structura de bază folosită de aceste funcții este struct iovec
:
#include <sys/uio.h> struct iovec { void *iov_base; /* Starting address */ size_t iov_len; /* bytes to transfer */ };
#include <sys/uio.h> ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
#include <sys/uio.h> ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
Un apel readv sau writev va permite recepționarea/transmiterea unui număr de buffere reprezentate de structura iovec
. Funcțiile întorc numărul total de octeți citiți sau scriși.
Apelul writev scrie datele (reprezentate de elementele din iov
în fișier), în ordinea în care acestea apar în vector:
#include <sys/uio.h> /* ... */ char *str0 = "Ana "; char *str1 = "are multe "; char *str2 = "mere, pere etc."; struct iovec iov[3]; ssize_t nwritten; iov[0].iov_base = str0; iov[0].iov_len = strlen(str0); iov[1].iov_base = str1; iov[1].iov_len = strlen(str1); iov[2].iov_base = str2; iov[2].iov_len = strlen(str2); nwritten = writev(fd, iov, 3); if (nwritten < 0) { /* handle error */ }
În rezolvarea laboratorului folosiți arhiva de sarcini lab11-tasks.zip
poll
1-pollpipe
.poll.c
pentru a vedea un exemplu de folosire al funcției poll
.fork
o aplicație de test pentru poll
. Aplicația folosește un server (părintele) și CLIENT_COUNT
clienți (copiii) ce comunică prin pipe-uri anonime.main
);MSG_SIZE
caractere de forma <pid>:<caracter random>
('a' + random() % 30);epoll
2-epollpipe
.epoll.c
.poll
folosiți epoll
.eventfd
eventfd
.epoll.c
linia cu #define USE_EVENTFD
MAGIC_EXIT
, iar în ultimii 32 de biți indexul clientului4-kaio
.kaio.c
.files
./tmp
. so$ sudo apt-get install libaio1 libaio-dev
eventfd
pentru așteptarea operațiilor asincrone.kaio.c
linia cu #define USE_EVENTFD
iocb
, folosiți funcția io_set_eventfd
pentru a activa folosirea eventfd
wait_aio
pentru a aștepta terminarea operațiilor asincrone folosind eventfd
/tmp
. signalfd