Podsystem Wejścia/Wyjścia w systemie Linux 2.4.7
Urządzenia znakowe i blokowe. Funkcja block_read(), block_write().
8. Funkcje struktury def_blk_fops - funkcja block_read()
< Poprzednia strona Spis treści Następna strona >

8. Funkcje struktury def_blk_fops - funkcja block_read()


8.1 Wstęp

Od razu na początku pragnę zauważyć, że celem tego dokumentu nie jest opisanie sposobu zarządzania buforami przy obsłudze urządzeń blokowych. W celu uzyskania informacji należy się posłużyć materiałami dotyczącymi systemu plików.

8.2 Ogólne spojrzenie na buforowanie w aspekcie urządzeń blokowych

W systemie Linux istnieje mechanizm buforowania. Spełnia on podstawową zasadę, którą musi spełniać system buforowania, mianowicie w danym momencie jest co najwyżej jeden bufor odpowiadający blokowi urządzenia blokowego. Funkcja może pobrać bufor i zapisać do niego dane, jak również może chcieć odczytać dane z bufora. Każdy bufor posiada trzy stany. Mówimy, że bufor jest ważny jeśli zawiera dane, które odpowiadają pewnemu blokowi w urządzeniu. Mówimy, że bufor jest zablokowany, jeśli trwa jakaś operacja na buforze i nie może być obecnie użyty. Mówimy, że bufor jest pobrudzony, jeśli jego dane trzeba zapisać na dysk, ponieważ został zmodyfikowany i dane różnią się od tych na dysku (nie oznacza to oczywiście, że nastąpi to od razu). System Linux udostępnia dwie ważne funkcje do operowania na buforach. Pierwsza - getblk(urządzenie, numer bloku, rozmiar) odpowiada za zwrócenie odpowiedniego bufora. Oczywiście jeśli dany blok na danym urządzeniu jest już buforowany, to jest on zwracany. Druga funkcja - brelse(bufor) zaznacza, że skończyliśmy korzystać z bufora. Po wywołaniu tej funkcji, w zależności od stanu, bufor może zostać zapisany do urządzenia lub ponownie użyty dla tego samego lub innego bloku.

8.3 Przepływ sterowania w przypadku odczytu/zapisu do pliku specjalnego

Podobnie jak przy pozostałych funkcjach, funkcje odczytu i zapisu są opakowaniami dla odpowiednich funkcji systemowych. I tak dla read() jest wywoływane sys_read(), która po sprawdzeniu blokad wywołuje funkcję block_read(). Analogicznie działa funkcja write(), wywołująca funkcję sys_write(), a następnie block_write().



8.4 Funkcja block_read()

Idea działania algorytmu jest bardzo prosta. Odczyt danych jest podzielony na dwie fazy, które wykonują się na zmianę. W pierwszej fazie funkcja próbuje pobrać tak dużo nie ważnych buforów jak to tylko możliwe. Następnie w razie potrzeby funkcja zleca odczytanie zbioru buforów. W drugiej fazie dane z buforów są kopiowane do procesu wywołującego. Realizacja również nie jest skomplikowana.
Funkcja przyjmuje cztery parametry:
struct file * filp, const char * buf, size_t count, loff_t *ppos.
Krótki komentarz:

Używane są między innymi następujące zmienne pomocnicze:

Na początku działania algorytmu, na podstawie struktury file (zmienna filp) ustawiana jest wartość zmiennej dev na numer urządzenia, z którego odczytuje dane. W tym czasie wykorzystywana jest praktycznie niepotrzebna zmienna inode.

dev = inode->i_rdev;

Nastepnie ustalana jest wielkość bloku dla urządzenia. W tym celu odczytywane są odpowiednie pola z tablicy blksize_size, o ile są one określone. W tym bowiem przypadku przyjmuje się, że wielkość bloku jest równa BLOCK_SIZE, który jest zdefiniowany w pliku include/linux/fs.h i może mieć wartość np. 2^10.

blocksize = BLOCK_SIZE;
if (blksize_size[MAJOR(dev)] && blksize_size[MAJOR(dev)][MINOR(dev)])
  blocksize = blksize_size[MAJOR(dev)][MINOR(dev)];


Następnie obliczany jest logarytm dwójkowy z wielkości bloku. Sensowność tego rozwiązania jest dyskutowana przy omawianiu funkcji block_write() (Patrz podrozdział 9.1). Następnie zmienna offset jest ustawiana na pozycję początkową odczytu z urządzenia, natomiast size jest ustawiane na rozmiar urządzenia. W tym celu wykorzystywana jest tablica blk_size zawierająca wielkości urządzeń blokowych oraz ustawiona zmianna dev.

offset = *ppos;
if (blk_size[MAJOR(dev)])
  size = (loff_t) blk_size[MAJOR(dev)][MINOR(dev)] << BLOCK_SIZE_BITS;
else
  size = (loff_t) INT_MAX << BLOCK_SIZE_BITS;


Mając już rozmiar urządzenia funkcja może sprawdzić, czy podane parametry dotyczące początkowej pozycji odczytu i rozmiaru danych do odczytania są poprawne. W tym celu najpierw jest sprawdzane czy czytanie nie zaczyna się poza urządzeniem. Dalej sprawdzane jest sztuczne ograniczenie na ilość danych, które ogranicza wielkość jednorazowo odczytywanych danych do wartości INT_MAX. Również w oczywisty sposób nie jest możliwe odczytanie większej ilości danych, niż pozostała od pozycji początkowej do końca. Biorąc pod uwagę wszystkie te warunki ustawiana jest zmienna left oznaczająca ilość danych, które powinny być odczytane. Warto zwrócić uwagę, że może być to wartość różna niż podany przez wywołującego parametr count. Oczywiście jak ilość danych do odczytania wynosi w tym miejscu 0, to można od razu opuścić funkcję.

if (offset > size)
  left = 0;
else if (size - offset > INT_MAX)
  left = INT_MAX;
else
  left = size - offset;
if (left > count)
  left = count;
if (left <= 0)
  return 0;


Biorąc pod uwagę fakt, że urządzenie blokowe zawsze operuje na całych blokach, to aby rozpocząć operacje odczytu potrzebne są jeszcze dane o początkowej pozycji i o ilości danych do przeczytania wyrażone w blokach. Tak więc kolejnym krokiem jest wyliczenie bloku oraz przesunięcia wewnątrz niego pozycji, od której rozpocznie się odczyt, a także w bardzo sprytny sposób wyznaczenia ilości bloków o odczytu. Początkowy numer bloku i przesunięcie względem niego jest przechowywane w zmiennych block i offset natomiast ilość bloków do odczytania w blocks. Ponadto do zmiannej size obliczana jest wielkość urządzenia wyrażona w blokach.

block = offset >> blocksize_bits;
offset &= blocksize-1;
size >>= blocksize_bits;
rblocks = blocks = (left + offset + blocksize - 1) >> blocksize_bits;


W tym momencie zauważmy, że liczba bloków do odczytania może być mała, co więcej można się spodziewać, że kolejne żądanie odczytu będzie dotyczyło kolejnych bloków urządzenia, z którego czytamy. Zatem funkcja jeśli tylko ma taką możliwość, to znaczy jeśli urządzenie dostarcza usługę, próbuje czytać bloki z wyprzedzeniem. Stwierdzenie czy urządzenie zaleca czytanie z wyprzedzeniem polega na sprawdzeniu czy pole w tablicy read_ahead jest ustawione.

if (filp->f_reada) {
  if (blocks < read_ahead[MAJOR(dev)] / (blocksize >> 9))
    blocks = read_ahead[MAJOR(dev)] / (blocksize >> 9);
  if (rblocks > blocks)
    blocks = rblocks;
}


Teraz trzeba znowu sprawdzić czy nowa wartość ilości bloków do przeczytania nie odwołuje się do bloków poza urządzeniem.

if (block + blocks > size) {
  blocks = size - block;
  if (blocks == 0)
    return 0;
}


W tym momencie funkcja posiada wszystkie informacje, aby rozpocząć czytanie. W dalszym części funkcja spróbuje odczytać tyle bloków na ile jest ustawiona zmienna blocks. Pierwszym blokiem będzie blok o numerze ze zmiennej block. W pierwszym bloku przesunięcie czytanych danych wynosi offset. Do skopiowania jest w sumie left bajtów.

Jak już wspominałem algorytm odczytu składa się z dwóch występujących na zmianę faz. W tych fazach wykorzystywane są tablice bhreq i buflist. Z pierwszą związany jest licznik bhrequest, który zawiera ilość zajętych pól w tablicy. Z drugą zawiazane są wskaźniki bhb i bhe, które wykorzystują tą tablicę w celu obsługi kolejki. Początkowo oczywiście obie tablice są puste. Od tej chwili mówiąc kolejka będziemy mieli na myśli zmienne bhb i bhe, gdzie bhb jest początkiem kolejki zaś bhe jej końcem. Zawsze wstawiamy na początek kolejki zaś usuwamy z końca.

Faza pierwsza.
Celem tej fazy jest pobranie do kolejki jak największej ilości buforów dla kolejnych bloków. Bufory są pobierane oczywiście za pomocą funkcji getblk(). Przy każdym buforze sprawdzane jest czy bufor jest ważny. Jeśli nie, to w oczywisty sposób trzeba do niego sprowadzić dane z urządzenia. Dlatego właśnie wszystkie bufory, do których trzeba sprowadzić dane są zapamiętywane w tablicy bhreq. W tym momencie korzystamy z tego, że wielkość tablicy bhreq jest niemniejsza niż tablicy buflist (w tym przypadku równa). Zauważmy, że wszelkie błędy przy pobieraniu buforów są w tym momencie ignorowane.

*bhb = getblk(dev, block++, blocksize);
if (*bhb && !buffer_uptodate(*bhb)) {
  uptodate = 0;
  bhreq[bhrequest++] = *bhb;
}


Koniec fazy następuje jeśli wypełni się kolejka lub jeśli odczytany bufor jest pierwszym pobranym buforem w tej fazie i jest ważny lub też jeśli po prostu zostały przeczytane wszystkie bloki.

while (blocks) {
  blocks;
  (...)
  if (uptodate)
    break;
  if (bhb == bhe)
    break;
}


Po zakończeniu pierwszej fazy wysyłane jest do urządzenia żądanie odczytu dla wszystkich bloków, które zostały przetworzone w poprzedniej fazie.

if (bhrequest) {
  ll_rw_block(READ, bhrequest, bhreq);
}

Faza druga.
Celem drugiej fazy jest odczytanie danych z buforów pobranych w fazie pierwszej i skopiowanie ich do pamięci wywołującego. W tej fazie przetwarzane i usuwane są kolejne elementy kolejki. Dla każdego elementu na początku sprawdzane jest czy w ogóle jest on buforem, ponieważ w fazie pierwszej ignorowane są blędy pobrania bufora. Jeśli jest to bufor, to najpierw trzeba poczekać na zakończenie operacji na nim. Jeśli po zakończeniu operacji bufor jest ważny, to wszystko jest w porządku w przeciwnym przypadku prawdopodobnie wystąpił jakiś bląd przy odczycie danych. Następnie aktualny bufor jest zaznaczany jako nieużywany, zaś ilość danych do odczytu jest ustawiana na 0. Zatem w tym momencie kończone są fazy odczytu.

if (*bhe) {
  wait_on_buffer(*bhe);
  if (!buffer_uptodate(*bhe)) { /* read error? */
    brelse(*bhe);
    if (++bhe == &buflist[NBUF])
      bhe = buflist;
    left = 0;
    break;
  }
}


Jeśli wszystko poszło dobrze, funkcja przystępuje do kopiowania danych do pamięcie procesu wywołującego, to znaczy do buforu wskazywanego przez parametr buf. Aby jednak to zrobić konieczne jest wyliczenie ile danych należy skopiować. Następnie aktualizowana jest pozycja danych do odczytu oraz zaktualizowane są liczniki danych pozostałych do skopiowania (left) i ilości skopiowanych danych (read). Zauważmy, że wielkość chars, ilości danych do skopiowania, jest prawie zawsze równa wielkości bloku. Jedyne przypadki, gdy tak nie jest występują gdy blok jest pierwszym lub ostatnim odczytywanym blokiem. Można wiec się zastanawić czy rzeczywiście takie rozwiązanie jest optymalne, ponieważ w przypadku odczytywania dużej ilości bloków w każdej pętli wykonywane są niepotrzebne porównania i obliczenia.

if (left < blocksize - offset)
  chars = left;
else
  chars = blocksize - offset;
*ppos += chars;
left -= chars;
read += chars;


Teraz można przystąpić do kopiowania danych. W przypadku gdy bufor dla bloku jest określony, to kopiowane są dane z bufora do pamięci wywołującego, a nastepnie bufor jest zaznaczany jako nieużywany. W przypadku gdy nie jest określony bufor zamiast danych z urządzenia wpisywane są wartości 0.

if (*bhe) {
  copy_to_user(buf,offset+(*bhe)->b_data,chars);
  brelse(*bhe);
  buf += chars;
} else {
  while (chars-- > 0)
  put_user(0,buf++);
}


W tym momencie przesunięcie danych względem początku bloku jest równe zero.

offset = 0;

Jedyna rzecz jaka pozostała, to usunąć właśnie przetworzony blok z kolejki.

if (++bhe == &buflist[NBUF])
  bhe = buflist;


Powstaje pytanie kiedy zakończyć fazę ? Więc oczywiście należy to zrobić jeśli nie ma już nic do odczytania. Jest jasne, że koniec jest w przypadku przetworzeniu wszystkich buforów przygotowanych w fazie pierwszej. Również kończona jest faza jeśli kolejny bufor do przetworzenia jest zablokowany. Warto się zastanowić co się dalej będzie działo z tym buforem ? Otóż nie zostanie on usunięty z kolejki. Przy następnej fazie pierwszej funkcja nie wykona na nim żadnej operacji, ale być może zostaną pobrane i odczytane kolejne bufory. Zatem zamiast bezczynnego czekania na odblokowanie bufora. zostaną wykonane kolejne odczyty danych. Czekanie nastąpi dopiero w kolejnej fazie drugiej. Po opuszczeniu fazy drugiej spwawdzane jest czy należy kontynuować odczytywanie. Więc ewidentnie nie ma po co kontynuować odczytywania jeśli wszystko zostało odczytane, ale także w przypadku gdy odczytano wszystkie bloki i kolejka jest pusta.

do {
  (...)
  if (bhe == bhb && !blocks)
    break;
} while (left > 0);


Moim zdaniem ten fragment kodu jest wyjątkowo nieelegancko napisany. W tym stylu nie należy pisać w języku imperatywnym, szczególnie gdy istnieje takie oto eleganckie rozwiązanie.

do {
  (...)
} while ((bhe != bhb || blocks) && left > 0);


Po zakończeniu odczytywania danych funkcja musi po sobie posprzątać, to znaczy zaznaczyć jako nieużywane wszystkie bufory z kolejki. Trzeba sobie zdać sprawę, że takie pozostałości nie muszą być wynikiem błędu, ponieważ możemy wykorzystywać czytanie z wyprzedzeniem. A więc jasne jest, że nie wszystkie dane, które zostaną przeczytane z urządzenia są istotne, więc część buforów będzie w takim przypadku niewykorzystana.

while (bhe != bhb) {
  brelse(*bhe);
  if (++bhe == &buflist[NBUF])
    bhe = buflist;
};


To już jest właściwie koniec funkcji. Warto jeszcze powiedzieć, że jeśli w funkcji udało się przeczytać choć jeden bajt danych poprawnie, to niezależnie co by się działo, to takie wykonanie nie jest błędne i wywołujący nie dowie się co się stało.

if (!read)
  return -EIO;
return read;


Używane tablice danych - patrz opis funkcji block_write() (rozdział 9.1).

Używane struktury - patrz opis funkcji block_write() (rozdział 9.1).

Typy danych - patrz opis funkcji block_write() (rozdział 9.1).


Autor: Łukasz Kamiński