Wstęp
Asynchroniczne wejście/wyjście (ang. asynchronous I/O) to technika zarządzania
żądaniami wejścia/wyjścia w oderwaniu od wątków wykonywania.
Standardowe operacje wejścia/wyjścia, np. read() i write() powodują
zablokowanie wątku wykonującego daną operację do czasu jej zakończenia.
Powoduje to, że wątek może naraz zainicjować tylko jedno żądanie
wejścia/wyjścia. W przypadku dostępu do zwykłych plików możliwe jest
zniwelowanie tej wady poprzez zapamiętywanie przez system danych w pamięci
podręcznej (zapisywanie z opóźnieniem dla operacji zapisu i czytanie z
wyprzedzeniem dla operacji odczytu).
Są jednak zastosowania, w których proces
chce mieć większą kontrolę nad wykorzystaniem pamięci podręcznej (np.
bazy danych) lub dostęp do urządzenia nie jest buforowany (tzw. urządzenia
surowe - ang. raw devices).
Użycie asynchronicznego wejścia/wyjścia umożliwia zarządzanie przez jeden
wątek wieloma żądaniami wejścia wyjścia naraz i precyzyjną kontrolę nad
rozpoczęciem i zakończeniem żądania wejścia/wyjścia.
Istnieje kilka standardów operacji asynchronicznego wejścia/wyjścia. W
świecie Uniksa obowiązuje standard POSIX.1b i właśnie ten standard będzie
dalej opisany.
Standard asynchronicznego wejścia/wyjścia POSIX.1b
Żądania wejścia/wyjścia
W standardowych operacjach wejścia/wyjścia wszystkie informacje opisujące
żądanie wejścia/wyjścia zawarte są w parametrach wywoływanej funkcji
systemowej. Ponieważ operacje asynchronicznego wejścia/wyjścia wykonują się w
oderwaniu od wątku wykonywania, wszystkie parametry opisujące żądanie są
zebrane w jednej strukturze aiocb. Wszystkie funkcje asynchronicznego
wejścia/wyjścia operują właśnie na takich strukturach.
Najważniejsze pola struktury aiocb:
- int aio_fildes - deskryptor pliku, którego dotyczy
żądanie.
- off_t aio_offset - pozycja w pliku, od której ma się
rozpocząć operacja.
- size_t aio_nbytes - liczba bajtów do
odczytania/zapisania.
- void *aio_buf - wskaźnik do bufora zawierającego dane do
zapisania lub gdzie zostaną umieszczone odczytane dane.
- int aio_lio_opcode - operacja, która ma zostać wykonana
(pole używane tylko w przypadku rozpoczęcia kilku operacji naraz
za pomocą funkcji lio_listio()).
- struct sigevent aio_sigevent - struktura określająca
sposób powiadomienia o zakończeniu żądania (patrz
Powiadamianie o zakończeniu operacji).
Rozpoczęcie operacji transmisji
Do rozpoczęcia operacji transmisji można użyć jednej z trzech funkcji:
- aio_read() - rozpoczęcie jednej operacji odczytu.
- aio_write() - rozpoczęcie jednej operacji zapisu.
- lio_listio() - rozpoczęcie kilku operacji odczytu/zapisu
naraz.
int aio_read(struct aiocb *aiocbp)
Rozpoczyna operację odczytu opisywaną strukturą aiocbp. Pola
aio_offset i aio_nbytes określają położenie i długość odczytywanego obszaru.
Wczytane dane zostaną zapisane w buforze wskazywanym przez aio_buf.
Funkcja zwraca 0, jeśli żądanie zostało zarejestrowane. W przeciwnym wypadku
funkcja zwraca -1 i ustawia odpowiednio zmienną errno.
int aio_write(struct aiocb *aiocbp)
Rozpoczyna operację zapisu opisywaną strukturą aiocbp. Pola
aio_offset i aio_nbytes określają położenie i długość
zapisywanego obszaru.
Dane do zapisania znajdują się w buforze określonym przez aio_buf.
Funkcja zwraca 0, jeśli żądanie zostało zarejestrowane. W przeciwnym wypadku
funkcja zwraca -1 i ustawia odpowiednio zmienną errno.
Rozpoczyna operacje opisywane przez tablicę list wskaźników do struktur
aiocb. Liczbę wskaźników określa parametr nent. Rodzaj operacji do
wykonania wskazuje pole aio_lio_opcode w każdej ze struktur aiocb
wskazywanych przez elementy tablicy. Możliwe wartości tego pola to:
- LIO_READ - zostanie wykonana operacja odczytu.
- LIO_WRITE - zostanie wykonana operacja zapisu.
- LIO_NOP - struktura zostanie zignorowana (brak operacji).
Parametr mode określa czy operacja ma się wykonać w sposób
blokujący, czy asynchronicznie. Może przybrać jedną z dwóch wartości:
- LIO_WAIT - powrót z funkcji nastąpi po zakończeniu
wszystkich wskazanych operacji (tryb blokujący).
- LIO_NOWAIT - powrót z funkcji nastąpi po zarejestrowaniu wszystkich
operacji (tryb asynchroniczny).
Parametr sig określa sposób powiadamiania, który ma zostać użyty do
powiadomienia o zakończeniu żądań (patrz
Powiadamianie o zakończeniu operacji).
Funkcja zwraca 0, jeśli wszystkie żądania zostały zarejestrowane (lub
pomyślnie zakończone, jeśli tryb wywołania był blokujący). W przeciwnym wypadku
funkcja zwraca -1 i ustawia odpowiednio zmienną errno.
Sposób powiadomienia o zakończeniu operacji wejścia wyjścia określa
struktura sigevent. Jest ona częścią struktury aiocb opisującej
żądanie lub zostaje przekazana w wywołaniu funkcji lio_listio().
Sposób powiadamiania określa pole sigev_notify. Są następujące
sposoby powiadamiania:
- SIGEV_NONE - brak powiadamiania, stan operacji musi być
ręcznie sprawdzany, np. za pomocą funkcji aio_error() lub wątek musi
zaczekać na zakończenie operacji za pomocą funkcji
aio_suspend().
- SIGEV_SIGNAL - po zakończeniu operacji zostanie wysłany
sygnał, numer sygnału określa pole sigev_signo.
- SIGEV_THREAD - po zakończeniu operacji wywołana zostanie
(w oddzielnym wątku) funkcja, której adres znajduje się w polu
sigev_notify_function.
Sprawdzanie stanu operacji
Stan operacji można sprawdzić wywołując odpowiednią funkcję na jej
strukturze aiocb.
int aio_error(const struct aiocb *aiocbp)
Sprawdza stan operacji. Może zwrócić:
- EINPROGRESS - operacja jeszcze się nie zakończyła.
- 0 - operacja zakończyła się pomyślnie.
- inna wartość - operacja zakończyła się błędem, zwrócona
wartość jest analogiczna do wartości errno zwróconej w wywołaniu
funkcji read() lub write().
ssize_t aio_return(const struct aiocb *aiocbp)
Pozwala sprawdzić liczbę odczytanych/zapisanych bajtów. Liczba ta ma
analogiczne znaczenie jak liczba zwracana przez funkcje read() lub
write().
Tej funkcji można użyć dopiero kiedy operacja się zakończy. Uwaga! Można ją wywołać
tylko raz dla każdego zakończonego żądania transmisji.
Oczekiwanie na zakończenie operacji
int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout)
Funkcja ta zawiesza wątek, który ją wykona do czasu aż któreś z żądań
wskazywanych przez wskaźnik w tablicy list nie zostanie zakończone.
Pole nent określa liczbę wskaźników w tablicy. Pole timeout
określa czas po jakim ma nastąpić powrót z funkcji, nawet jeśli żadne z
żądań nie zostanie zakończone. Jeśli w polu timeout znajduje się NULL,
oczekiwanie nie będzie przerwane do czasu zakończenia żądania (o ile nie
zostanie dostarczony sygnał przerywający funkcję systemową).
Podanie wskaźnika NULL w polu tablicy list powoduje zignorowanie
tego pola tablicy.
Funkcja zwraca 0, jeśli któreś z żądań zostało zakończone. W przeciwnym
wypadku funkcja zwraca -1 i ustawia odpowiednio zmienną errno.
Dalsze informacje
Przedstawione funkcje służą do operowania asynchronicznym wejściem/wyjściem
w podstawowym zakresie. Do innych użytecznych funkcji należą:
- aio_fsync() - pozwala zaczekać na zakończenie wszystkich
operacji związanych z określonym deskryptorem pliku.
- aio_cancel() - pozwala anulować żądanie.
- aio_init() - pozwala określić parametry implementacji
AIO.
W strukturze aiocb znajduje się również nieomówione pole aio_reqprio,
które służy do nadawania priorytetu żądaniom wejścia/wyjścia.
Dalsze szczegółowe informacje o standardzie AIO, dokładny opis parametrów i
wartości zwracanych przez poszczególne funkcje można znaleźć w
opisie standardu asynchronicznego wejścia/wyjścia POSIX.1b w bibliotece GLIBC.
Implementacja AIO w Linuksie
W celu pełnego korzystania z zalet AIO, jądro systemu musi wspierać
asynchroniczne żądania wejścia/wyjścia. Jeśli takiego wsparcia nie ma,
biblioteka GLIBC emuluje asynchroniczne wejście/wyjście za pomocą puli
wątków wykonujących operacje wejścia/wyjścia. Parametry tej emulacji można
ustawić za pomocą funkcji aio_init().
Standardowe jądro Linuksa w wersji 2.6.x zawiera obsługę asynchronicznego
wejścia/wyjścia.
Dla wersji 2.4.x istnieją łaty dodające takie wsparcie.
Uwaga:
niektóre operacje (np. dopisywanie na końcu pliku) i
niektóre systemy plików, zwłaszcza we wcześniejszych
wersjach serii 2.6 mogą powodować problemy, np. blokować wykonanie
programu. W razie wątpliwości, najlepiej zbadać zachowanie programu w
interesującym nas zastosowaniu.
Szczegóły techniczne
W celu korzystania z AIO należy dołączyć plik nagłówkowy aio.h i
łączyć program wykonywalny z biblioteką librt.
Inne implementacje asynchronicznego wejścia/wyjścia
AIO nie jest jedynym ani najlepszym standardem asynchronicznego wejścia/wyjścia.
Asynchroniczne wejście/wyjście jest np. częścią interfejsu Win32 używanego w
systemach Windows NT/2000/XP.
Program przykładowy
Załączony program przykładowy aio-copy używa interfejsu AIO do
wykonywania jednocześnie kilku kopii jednego pliku. Wszystkie operacje
wejścia/wyjścia obsługiwane są przez jeden wątek główny.
Zasada działania
Stała NUM_ASYNC_IO określa ile może jednocześnie się wykonywać operacji
odczytu. Z każdym żądaniem odczytu związane jest num_copies (liczba
kopii) żądań zapisu - po jednym dla każdej kopii. Każde żądanie odczytu i
jego żądania zapisu stanowią jedną przegródkę (ang. slot),
niezależną od innych.
Na początku działania programu inicjowany jest odczyt NUM_ASYNC_IO
pierwszych fragmentów pliku. Następnie wątek główny za pomocą funkcji
aio_suspend() czeka na zakończenie którejś operacji wejścia/wyjścia.
Po zakończeniu operacji odczytu rozpoczynane są wszystkie operacje zapisu odczytanych danych
do plików kopii.
Kiedy zakończy się operacja zapisu, sprawdzane jest, czy wszystkie operacje
zapisu dla danej przegródki się zakończyły. Jeśli tak, przegródka jest znowu
wolna, więc inicjowany jest odczyt kolejnego fragmentu pliku do tej
przegródki i cykl rozpoczyna się od nowa.
Przedstawianie stanu żądań
Zdefiniowanie makra SHOW_IO_STATUS powoduje przedstawianie stanu żądań w momencie,
gdy nastąpi zakończenie któregoś żądania. Stan każdego aktywnego żądania
jest określony literą:
- W - zakończone żądanie zapisu.
- w - żądanie zapisu w trakcie realizacji.
- R - zakończone żądanie odczytu
- r - żądanie odczytu w trakcie realizacji.
Zadania
Oto propozycje zadań do samodzielnego wykonania, uporządkowane według
rosnącej trudności:
- Po zakończeniu operacji wejścia/wyjścia jest sprawdzane, czy
operacja zakończyła się pomyślnie, ale nie jest sprawdzone, czy
liczba przeczytanych/zapisanych bajtów jest zgodna z tym, co zostało zawarte w
żądaniu (liczbę przeczytanych/zapisanych bajtów zwraca funkcja
aio_return()). Należy dodać obsługę tego przypadku.
- Rozpoczęcie operacji zapisu do wszystkich kopii w przegródce (i
początkowe odczytanie fragmentów pliku do przegródek) jest
zrealizowane za pomocą kilku wywołań
aio_write() (aio_read()). Należy to zrealizować za
pomocą jednego wywołania funkcji lio_listio().
- Po zakończeniu wszystkich operacji zapisu nie można rozpocząć
natychmiast kolejnych operacji zapisu w tej przegródce, ponieważ
dane muszą zostać najpierw wczytane do bufora. Efektywniejszym
rozwiązaniem jest wczytanie z wyprzedzeniem danych do innego
bufora tak, by w momencie zakończenia zapisów można było się
przełączyć na inny bufor i rozpocząć od razu operacje zapisu.
Należy zaimplementować pulę buforów do czytania z wyprzedzeniem.
Pula tych buforów ma być wspólna dla wszystkich przegródek, to
znaczy że każda przegródka może wziąć gotowy bufor z puli. Jeśli
nie ma wczytanego bufora w puli, należy wczytywać dane do bufora
tak, jak to jest zrealizowane obecnie. Uwaga: nie wolno
kopiować danych między pulą buforów do wczytywania z wyprzedzeniem a
buforami używanymi w przegródkach - ma następować wymiana aktualnie
używanego przez przegródkę bufora na bufor z wczytanymi danymi.
Można założyć, że rozmiar puli buforów do czytania z wyprzedzeniem
jest dany stałą NUM_READAHEAD_BUFFERS.
Rozwiązanie zadania znajduje się w pliku aio-copy-readahead.c.
Autor: Krzysztof Lichota lichota@mimuw.edu.pl