Unijne systemy plików: Implementacje, część I


This article brought to you by LWN subscribers

Subscribers to LWN.net made this article – and everything that surrounds it – possible. Jeśli doceniasz naszą zawartość, proszę wykup subskrypcję i spraw, by następny zestaw artykułów był możliwy.

25 marca 2009

Ten artykuł został napisany przez Valerie Aurora (dawniej Henson)

W zeszłotygodniowym artykule przejrzałem przypadki użycia, podstawowe koncepcje i typowe problemy projektowe systemów plików z unifikacją. W tym tygodniu opiszę kilka implementacji unifikacyjnych systemów plików w szczegółach technicznych. Systemy plików, które omówię w tym artykule to: katalogi unii Planu 9, montaże unii BSD, montaże unii Linuksa. Następny artykuł będzie dotyczył unionfs, aufs, i być może jednego lub dwóch innych systemów plików, i zakończy serię.

Dla każdego systemu plików, opiszę jego podstawową architekturę, cechy i implementację. Dyskusja na temat implementacji będzie koncentrować się w szczególności na whiteout i czytania katalogów. Zakończę spojrzeniem na aspekty inżynierii oprogramowania każdej z implementacji; np. rozmiar i złożoność kodu, inwazyjność i obciążenie dla programistów systemów plików.

Przed przeczytaniem tego artykułu, warto zapoznać się z opublikowanym właśnie raportem AndreasaGruenbachera z warsztatów na temat montowania unii, które odbyły się w listopadzie zeszłego roku. Jest to dobre podsumowanie cech systemów plików unifikacyjnych, które są najbardziej naglące dla deweloperów dystrybucji. Ze wstępu: „Wszystkie przypadki użycia, którymi jesteśmy zainteresowani, w zasadzie sprowadzają się do tego samego: posiadanie obrazu lub systemu plików, który jest używany tylko do odczytu (albo dlatego, że nie ma możliwości zapisu, albo dlatego, że zapis do obrazu nie jest pożądany), i udawanie, że ten obraz lub system plików jest zapisywalny, przechowując zmiany gdzie indziej.”

Katalogi unii Planu 9

System operacyjny Planu 9 (tutaj można znaleźć kod źródłowy) implementuje unifikację na swój własny, specjalny sposób. W katalogach unii Planu 9, tylko przestrzeń nazw katalogów najwyższego poziomu jest łączona, nie żadne podkatalogi. Nieskrępowane standardami UNIX-a, katalogi unii Planu 9 nie implementują whiteoutów i nawet nie odsiewają zduplikowanych wpisów – jeśli ta sama nazwa pliku pojawia się w dwóch systemach plików, jest po prostu zwracana dwukrotnie w listach katalogów.

Katalog unii Planu 9 jest tworzony w następujący sposób:

 bind -a /home/val/bin/ /bin

Spowodowałoby to zamontowanie katalogu/home/val/bin

„po” (opcja-a)/bin; inne opcje to umieszczenie nowego katalogu przed istniejącym lub całkowite zastąpienie istniejącego katalogu. (Wydaje mi się to dziwną kolejnością, ponieważ lubię, gdy polecenia w moim osobistymbin/mają pierwszeństwo przed poleceniami ogólnosystemowymi, ale to przykład z dokumentacji Planu 9). Brian Kernighanexplains oneof the uses of union directories: „Ten mechanizm uniondirectories zastępuje ścieżkę wyszukiwania w konwencjonalnych powłokach UNIX. Jeśli chodzi o użytkownika, wszystkie programy wykonywalne znajdują się w /bin”. Katalogi unii mogą teoretycznie zastąpić wiele zastosowań fundamentalnych UNIX-owych bloków konstrukcyjnych dowiązań symbolicznych i ścieżek wyszukiwania.

Bez whiteoutów i eliminacji duplikatów, readdir() na katalogach unii jest trywialna do zaimplementowania. Offsety wpisu do katalogu z bazowego systemu plików odpowiadają bezpośrednio offsetowi w bajtach wpisu do katalogu od początku katalogu. Katalog zjednoczony jest traktowany tak, jakby zawartość bazowych katalogów była konkatenowana razem.

Plan 9 implementuje alternatywę dla readdir() wartą odnotowania, dirread(). dirread() zwraca struktury typu Dir, opisane na stronie podręcznika stat()man. Ważną częścią Dir jest członek Qid. Człon Qid to:

…struktura zawierająca pola ścieżka i vers: ścieżka jest gwarantowana jako unikalna wśród wszystkich nazw ścieżek znajdujących się obecnie na serwerze plików, a vers zmienia się za każdym razem, gdy plik jest modyfikowany. Ścieżka jest długim long (64 bity, vlong), a vers jest unsigned long (32 bity, ulong).

Dlaczego więc jest to interesujące? Jednym z powodów, dla których readdir() jest tak bolesne w implementacji, jest to, że zwraca d_off członka struct dirent, pojedynczy off_t (32 bity, chyba że aplikacja jest skompilowana z obsługą dużych plików), aby oznaczyć wpis w katalogu, w którym aplikacja powinna kontynuować czytanie przy następnym wywołaniu readdir(). Działa to dobrze tak długo, jak d_off jest prostym przesunięciem bajtowym w płaskim pliku o rozmiarze mniejszym niż 232 bajty, a istniejące wpisy katalogowe nie są nigdy przenoszone – nie jest tak w przypadku wielu nowoczesnych systemów plików (XFS, btrfs, ext3 z indeksami htree). 96-bitowe Qid jest znacznie bardziej użytecznym znacznikiem miejsca niż 32- lub 64-bitowe off_t. Aby uzyskać dobre podsumowanie problemów związanych z implementacją readdir(), przeczytaj TheodoreY. Ts’o’s doskonały post na ten temat na liście mailingowej btrfs.

Z punktu widzenia inżynierii oprogramowania, katalogi unii Planu 9 są niebiańskie. Bez whiteoutów, eliminacji duplikatów wpisów, skomplikowanych offsetów katalogów czy łączenia przestrzeni nazw poza katalogiem najwyższego poziomu, implementacja jest prosta i łatwa w utrzymaniu.Jednakże, każda praktyczna implementacja unifikacyjnych systemów plików dlaLinuksa (lub jakiegokolwiek innego UNIX-a) musiałaby rozwiązać te problemy. Dla naszych celów, katalogi unii Planu 9 służą przede wszystkim jako inspiracja.

BSD union mounts

BSD implementuje dwie formy unioningu: opcję"-o union"poleceniamount, która tworzy katalog unii podobny do katalogu Planu 9, oraz poleceniemount_unionfs, które implementuje bardziej funkcjonalny system plików z wybielaniem i łączeniem całej przestrzeni nazw. Skupimy się na tym ostatnim.

Do tego artykułu, używamy dwóch źródeł dla specyficznych szczegółów implementacji: oryginalnej implementacji montowania unii BSD, jak opisano w 1995 USENIX paperUnionmounts in 4.4BSD-Lite , i FreeBSD7.1 mount_unionfs strona man i kod źródłowy. InneBSD mogą się różnić.

Katalog może być montowany unią zarówno „poniżej” jak i „powyżej” istniejącego katalogu lub montowania unii, tak długo jak górna gałąź zapisywalnej unii jest zapisywalna. Obsługiwane są dwa tryby wyczyszczenia: albo wyczyszczenie jest tworzone zawsze, gdy katalog jest usuwany, albo jest tworzone tylko wtedy, gdy inna pozycja katalogu o tej nazwie istnieje w gałęzi poniżej gałęzi zapisywalnej. Obsługiwane są trzy tryby ustawiania własności i trybu kopiowanych plików. Najprostszym z nich jesttransparent, w którym nowy plik zachowuje tego samego właściciela i tryb, co oryginał. Tryb masquerade sprawia, że kopiowane pliki są własnością konkretnego użytkownika i obsługuje zestaw opcji montowania do określania trybu nowego pliku. Tryb traditional ustawia właściciela na użytkownika, który wykonał polecenie montowania unii i ustawia tryb zgodnie z umask w czasie montowania unii.

Każdy katalog jest otwierany, katalog o tej samej nazwie jest tworzony na górnej warstwie zapisywalnej, jeśli jeszcze nie istnieje. Dzięki agresywnemu tworzeniu katalogów cieni podczas wyszukiwania, unionfilesystem unika konieczności sprawdzania i ewentualnego tworzenia łańcucha katalogów od korzenia wierzchowca do punktu kopiowania.Ponieważ miejsce na dysku zajmowane przez katalog jest pomijalne, tworzenie katalogów podczas ich pierwszego przejścia wydawało się lepszą alternatywą.

W rezultacie, "find /union" spowoduje skopiowanie każdego katalogu (ale nie wpisów wskazujących na nie-katalogi) do warstwy zapisywalnej. Dla większości obrazów systemów plików będzie to wymagało nieistotnej ilości miejsca (mniej niż, na przykład, miejsce zarezerwowane dla użytkownika root lub zajęte przez nieużywane inody w systemie plików typu FFS).

Plik jest kopiowany do górnej warstwy, gdy jest otwierany z uprawnieniem do zapisu lub gdy zmieniane są jego atrybuty. (Ponieważ katalogi są kopiowane, gdy są otwierane, katalog zawierający plik na pewno istnieje już na warstwie zapisywalnej). Jeżeli plik, który ma zostać skopiowany posiada wiele twardych dowiązań, pozostałe dowiązania są ignorowane, a nowy plik ma liczbę dowiązań równą jeden. Może to spowodować uszkodzenie aplikacji, które używają twardych dowiązań i oczekują, że modyfikacje dokonane za pomocą jednego dowiązania pojawią się przy odwołaniu do innego twardego dowiązania. Takie aplikacje są stosunkowo rzadkie, ale nikt nie przeprowadził systematycznych badań, aby sprawdzić, które aplikacje zawiodą w tej sytuacji.

Whiteout jest implementowany za pomocą specjalnego typu wpisu do katalogu, DH_WHT. Wpisy katalogu whiteout nie odnoszą się do żadnego prawdziwego węzła, ale dla łatwej kompatybilności z istniejącymi narzędziami systemu plików, takimi jak fsck, każdy wpis katalogu whiteout zawiera fałszywy numer węzła, WINO zarezerwowany numer węzła whiteout. Podstawowy system plików musi być zmodyfikowany, aby wspierać typ wpisu whiteout directory. Nowe katalogi, które zastępują wpisy typu whiteout, są oznaczone jako nieprzezroczyste przez nowy atrybut inode „opaque”, aby odszukiwanie nie odbywało się przez nie (ponownie wymagając minimalnego wsparcia ze strony bazowego systemu plików).

Duplikaty wpisów katalogów i whiteout są obsługiwane w implementacji w przestrzeni użytkownikareaddir(). W opendir()czasie biblioteka C odczytuje katalog naraz, usuwa duplikaty, stosuje whiteouty i buforuje wyniki.

BSD union mounts nie próbują radzić sobie ze zmianami w gałęziach poniżej zapisywalnej gałęzi górnej (choć są one dozwolone). Sposób, w jaki rename() jest obsługiwany, nie jest opisany.

Przykład ze strony mount_unionfs man:

 The commands mount -t cd9660 -o ro /dev/cd0 /usr/src mount -t unionfs -o noatime /var/obj /usr/src mount the CD-ROM drive /dev/cd0 on /usr/src and then attaches /var/obj on top. For most purposes the effect of this is to make the source tree appear writable even though it is stored on a CD-ROM. The -o noatime option is useful to avoid unnecessary copying from the lower to the upper layer.

Inny przykład (zauważając, że uważam, iż kontrola źródła jest najlepiej zaimplementowana poza systemem plików):

 The command mount -t unionfs -o noatime -o below /sys $HOME/sys attaches the system source tree below the sys directory in the user's home directory. This allows individual users to make private changes to the source, and build new kernels, without those changes becoming visible to other users.

Upinanie unii w Linuksie

Podobnie jak montowanie unii w BSD, montowanie unii w Linuksie implementuje łączenie systemów plików w warstwie VFS, z pewnym drobnym wsparciem ze strony bazowych systemów plików dla whiteoutów i nieprzejrzystych znaczników katalogów. Istnieje kilka wersji tych poprawek, napisanych i zmodyfikowanych przez Jana Bluncka, Bharatę B. Rao i Miklosa Szeredi, między innymi.

Jedna z wersji tego kodu łączy tylko katalogi najwyższego poziomu, podobnie jak katalogi unii Planu 9 i opcja montowania BSD -o union. Ta wersja montowania unii, którą określam jako uniondirectories, jest opisana szczegółowo w artykule Goldwyna Rodriguesa z LWN oraz w ostatnim poście Miklosa Szeredi, zawierającym uaktualniony zestaw poprawek. W dalszej części tego artykułu, skupimy się na wersjach montowania unii, które łączą pełną przestrzeń nazw.

Montowanie unii w systemie Linux jest obecnie aktywnie rozwijane. Ten artykuł opisuje wersję wydaną przez Jana Bluncka dla Linux2.6.25-mm1, util-linux 2.13 i e2fsprogs 1.40.2. Zestawy poprawek, jako seria asquilt, można pobrać z ftp Jana:

Poprawki jądra: ftp://ftp.suse.com/pub/people/jblunck/patches/

Utilities: ftp://ftp.suse.com/pub/people/jblunck/union-mount/

Utworzyłem stronę z linkami do wersji git powyższych łat i dokumentacją w stylu HOWTO pod adresem http://valerieaurora.org/union.

Unia jest tworzona przez zamontowanie systemu plików z zestawem flag MS_UNION. (Flagi MS_BEFORE, MS_AFTER i MS_REPLACE są zdefiniowane w bazie kodowej mount, ale obecnie nie są używane). Jeśli podano flagę MS_UNION, to montowany system plików musi być albo tylko do odczytu, albo obsługiwać whiteout. W tej wersji montowania unii, flaga montowania unii jest określona przez opcję „-o union” do mount. Na przykład, aby utworzyć unię dwóch systemów plików loopbackdevice, /img/ro i /img/rw, wykonałbyś:

 # mount -o loop,ro,union /img/ro /mnt/union/ # mount -o loop,union /img/rw /mnt/union/

Każde montowanie unii tworzystruct union_mount:

 struct union_mount {atomic_t u_count;/* reference count */struct mutex u_mutex;struct list_head u_unions;/* list head for d_unions */struct hlist_node u_hash;/* list head for searching */struct hlist_node u_rhash;/* list head for reverse searching */struct path u_this;/* this is me */struct path u_next;/* this is what I overlay */ };

Jak opisano wDocumentation/filesystems/union-mounts.txt, „Wszystkie struktury union_mount są buforowane w dwóch tablicach haszujących, jednej do wyszukiwania następnej niższej warstwy stosu unii i jednej do odwrotnego wyszukiwania następnej wyższej warstwy stosu unii.”

Whiteout i nieprzezroczyste katalogi są zaimplementowane w taki sam sposób jak w BSD. Podstawowy system plików musi jawnie wspierać whiteout przez zdefiniowanie operacji .whiteout inode dla katalogów (obecnie whiteout jest zaimplementowany tylko dla ext2, ext3 i tmpfs).Implementacje ext2 i ext3 używają whiteout directory entrytype, DT_WHT, który był zdefiniowany w include/linux/fs.h przez lata, ale do tej pory nie był używany poza systemem plików Coda. Zarezerwowany numer inode whiteout, EXT3_WHT_INO, jest zdefiniowany, ale jeszcze nie używany; wpisy whiteout obecnie alokują normalny inode. Zdefiniowano nową flagę inode, S_OPAQUE, do oznaczania katalogów jako nieprzezroczystych.Podobnie jak w BSD, katalogi są oznaczane jako nieprzezroczyste tylko wtedy, gdy zastępują wpis whiteout.

Pliki są kopiowane w górę, gdy plik jest otwierany do zapisu. Jeśli jest to konieczne, każdy katalog w ścieżce do pliku jest kopiowany do górnej gałęzi (kopiowanie na żądanie katalogów). Obecnie, kopiowanie w górę jest obsługiwane tylko dla zwykłych plików i katalogów.

readdir() jest jednym z najsłabszych punktów obecnej implementacji. Jest on zaimplementowany w ten sam sposób jak BSD union mountreaddir(), ale w jądrze. Pole d_off jest ustawiane na offset w bieżącym katalogu bazowym, pomniejszony o rozmiary poprzednich katalogów. Wpisy z katalogów znajdujących się pod górną warstwą muszą być sprawdzane względem poprzednich wpisów pod kątem duplikatów lub whiteoutów. Jak obecnie zaimplementowano, każde readdir() (technicznie, getdents()) wywołanie systemowe odczytuje wszystkie poprzednie wpisy katalogów do pamięci podręcznej jądra, a następnie porównuje każdy zwracany wpis z tymi, które już znajdują się w pamięci podręcznej, przed skopiowaniem go do bufora użytkownika. W rezultacie readdir() jest złożone, powolne i potencjalnie alokuje dużą ilość pamięci jądra.

Jednym z rozwiązań jest podejście BSD i buforowanie, whiteout i duplikowanie przetwarzania w przestrzeni użytkownika. Bharata B. Raois projektuje wsparcie dla union mount readdir() w glibc. (Standard POSIX pozwala na implementację readdir() na poziomie libc, jeśli wywołanie systemowe jądra nie spełnia wszystkich wymagań). Pozwoliłoby to przenieść użycie pamięci do aplikacji i uczynić cache trwałym. Innym rozwiązaniem byłoby uczynienie pamięci podręcznej w jądrze trwałą w jakiś sposób.

Moją sugestią jest wzięcie techniki z BSD union mounts i rozszerzenie jej: proaktywne kopiowanie nie tylko wpisów do katalogów, ale wszystkich wpisów do katalogów z niższych systemów plików, przetwarzanie duplikatów i whiteoutów, uczynienie katalogu nieprzezroczystym i wypisanie go na dysk. W efekcie przetwarzasz wpisy katalogów na białe i duplikaty przy pierwszym otwarciu katalogu, a następnie zapisujesz wynikowy „cache” wpisów katalogowych na dysku. Wpisy w katalogu wskazujące na pliki w bazowych systemach plików muszą w jakiś sposób oznaczać, że są to wpisy „fall-through” (przeciwieństwo whiteout – wyraźnie wymaga szukania obiektu w niższym systemie plików). Efektem ubocznym tego podejścia jest to, że Whiteout nie są już potrzebne w ogóle.

Jeden problem, który musi być rozwiązany z tym podejściem jest to, jak reprezentować wpisy katalogów wskazujących na niższe systemy plików. Istnieje wiele rozwiązań: wpis może wskazywać na zarezerwowany numer węzła, system plików może przydzielić węzeł dla każdego wpisu, ale oznaczyć go nowym S_LOOKOVERTHERE atrybutem węzła, może utworzyć symlink do zarezerwowanego celu, itp. To podejście wykorzysta więcej miejsca w bazowym systemie plików, ale wszystkie inne podejścia wymagają alokacji tego samego miejsca w pamięci, a pamięć jest droższa niż dysk.

Mniej palącym problemem z obecną implementacją jest to, że liczby inod nie są stabilne podczas uruchamiania systemu (zobacz poprzedni artykuł o systemach plików, aby dowiedzieć się, dlaczego jest to problem).Jeśli katalogi „fall-through” są zaimplementowane przez alokację inode dla każdej pozycji katalogu w bazowych systemach plików, to stabilne liczby inod będą naturalnym efektem ubocznym. Inną opcją jest przechowywanie stałej mapy inode gdzieś – w pliku w katalogu najwyższego poziomu, lub w zewnętrznym systemie plików, być może.

Hard links są obsługiwane – lub, dokładniej, nie są obsługiwane – w taki sam sposób jak montaże unii BSD. Ponownie, nie jest jasne, jak wiele aplikacji zależy od modyfikowania pliku przez jedną ścieżkę z twardymi dowiązaniami i oglądania zmian przez inną ścieżkę z twardymi dowiązaniami (w przeciwieństwie do dowiązań symbolicznych). Jedyną metodą, jaką mogę wymyślić, aby poradzić sobie z tym poprawnie, jest utrzymywanie trwałej pamięci podręcznej gdzieś na dysku inodów, które napotkaliśmy z wieloma twardymi odnośnikami.

Oto przykład, jak to będzie działać: Powiedzmy, że uruchamiamy kopię dla węzła 42 i stwierdzamy, że ma on liczbę łączy wynoszącą trzy. Stworzylibyśmy wpis do bazy danych twardych łączy, który zawierałby identyfikator systemu plików, numer węzła, liczbę łączy i numer węzła nowej kopii w systemie plików najwyższego poziomu. Może to być zapisane w pliku w formacie CSV, lub jako symlink w zarezerwowanym katalogu w katalogu głównym (np. „/.hardlink_hack/<fs_id>/42„, który jest linkiem do „<new_inode_num> 3„), lub w prawdziwej bazie danych. Za każdym razem, gdy otwieramy węzeł w bazowym systemie plików, sprawdzamy go w naszej bazie twardych łączy; jeśli taki wpis istnieje, zmniejszamy liczbę łączy i tworzymy twarde łącze do właściwego węzła w nowym systemie plików. Gdy wszystkie ścieżki zostaną odnalezione, liczba łączy spada do jednego i wpis może zostać usunięty z bazy danych. Fajną rzeczą w tym podejściu jest to, że ilość narzutu jest ograniczona i zniknie całkowicie, gdy wszystkie ścieżki do odpowiednich inodów zostaną odnalezione. Jednakże, nadal wprowadza to znaczną ilość prawdopodobnie niepotrzebnej złożoności; implementacja BSD pokazuje, że wiele aplikacji będzie z radością działać z nie do końca poprawnym zachowaniem hardlinków.

Obecnie, rename() katalogów w gałęziach zwraca EXDEV, błąd przy próbie zmiany nazwy pliku w różnych systemach plików. Przestrzeń użytkownika zwykle radzi sobie z tym niepostrzeżenie (ponieważ już musi sobie radzić z tym przypadkiem dla katalogów z różnych systemów plików) i powraca do kopiowania zawartości katalogu jeden po drugim. Implementowanie rekursywnego rename() katalogów przez gałęzie w jądrze nie jest dobrym pomysłem z tych samych powodów, co zmiana nazwy przez nieregularne systemy plików; prawdopodobnie powrót EXDEV jest najlepszym rozwiązaniem.

Z punktu widzenia inżynierii oprogramowania, montowanie unii wydaje się być rozsądnym kompromisem między funkcjami a łatwością konserwacji. Większość zmian w VFS jest odizolowana w fs/union.c, pliku o długości około 1000 linii. Około 1/3 tego pliku to implementacja readdir() w jądrze, która prawie na pewno zostanie zastąpiona czymś innym przed ewentualnym scaleniem. Zmiany w bazowych systemach plików są dość minimalne i potrzebne tylko dla systemów plików montowanych jako zapisywalne gałęzie. Główną przeszkodą w połączeniu tego kodu jest implementacja readdir(). Poza tym, opiekunowie systemów plików byli zauważalnie bardziej pozytywnie nastawieni do montowania unii niż do jakiejkolwiek innej implementacji unii.

Dobre podsumowanie montowania unii można znaleźć w slajdach Bharaty B. Rao dla FOSS.IN .

Następny

W następnym artykule, przejrzymy unionfs i aufs, i porównamy różne implementacje unii systemów plików dla Linuksa. Staytuned!

Wpisy indeksu dla tego artykułu
Kernel Filesystems/Union
Kernel Union mounts
GuestArticles Aurora (Henson), Valerie

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.