FUSE (Filesystem in Userspace)

Wojciech Baranowski

Wstęp

W klasycznym podejściu dodanie obsługi nowego typu systemu plików wymaga stworzenia i załadowania specjalnie do tego przeznaczonego modułu jądra. Ponadto, poza sytuacjami wyjątkowymi, do montowania systemów plików uprawniony jest tylko użytkownik root. Jednak interfejs VFS jądra coraz częściej z powodzeniem wykorzystuje się do zastosowań innych, niż zarządzanie danymi przechowywanymi na urządzeniach blokowych. Wobec tych zmian pojawia się potrzeba z jednej strony umożliwienia zwykłym użytkownikom montowania systemów plików i ładowania do nich sterowników na własny użytek i odpowiedzialność, z drugiej - ułatwienia pisania sterowników dla takich systemów nieopartych na urządzeniach blokowych. Framework Filesystem in Userspace (FUSE) wychodzi naprzeciw tym potrzebom.

Zasada działania

FUSE składa się z dwóch głównych komponentów:

  1. Modułu (fuse.ko)
  2. Biblioteki dostępnej w przestrzeni użytkownika (libfuse.so)

Systemy plików oparte na FUSE obsługiwane są przez proces działający w przestrzeni użytkownika (tzw. filesystem daemon). Każdy system plików jest obsługiwany przez osobny daemon. Właścicielem procesu-daemona jest użytkownik, który zamontował obsługiwany przez niego system plików (a więc nie musi to być root).

Warto zwrócić uwagę, że w jednej chwili może być uruchomionych wiele daemonów obsługujących systemy plików tego samego typu, nawet z tym samym właścicielem.

Operacje na systemie plików obsługiwane przez daemona są dostępne przez interfejs VFS jadra dzięki modułowi fuse.ko. Komunikacja pomiędzy modułem fuse.ko a kodem obsługującym system plików jest realizowana przez bibliotekę libfuse.

Przepływ żądania przy przykładowej operacji na systemie plików opartym na FUSE dobrze ilustruje poniższy obrazek (w tym przykładzie plik example/hello jest daemonem obsługującym system plików zamontowany w katalogu /tmp/fuse):

Źródło: http://fuse.sourceforge.net

Perspektywa użytkownika

Montowanie i odmontowanie

Najłatwiejszym sposobem montowania systemu pliku jest uruchomienie explicite obsługującego go daemona wraz z wymaganymi parametrami. Na przykładzie sshfs:

# sshfs students.mimuw.edu.pl: lokalny_katalog [ -o opcje ]
            
Możemy też użyć polecenia mount:
# mount -t fuse sshfs#students.mimuw.edu.pl: lokalny_katalog [ -o opcje ]
            

W tym wypadku sshfs (ograniczony przez znak # prefiks łańcucha przekazanego jako nazwa urządzenia) musi być plikiem wykonywalnym dostępnym w standardowy sposób przez zmienną $PATH.

Ta druga wersja zazwyczaj wymaga oczywiście uprawnień roota, ale na jej podstawie możemy przygotować odpowiedni wpis w /etc/fstab, aby system plików był ładowany automatycznie:

sshfs#students.mimuw.edu.pl: lokalny_katalog fuse default 0 0
            

Do odmontowania możemy użyć standardowego polecenia umount lub dostarczanego z pakietem FUSE programu fusermount z opcją -u.

Bezpieczeństwo

Umożliwienie nieuprzywilejowanym użytkownikom tworzenia i montowania własnych systemów plików może pociągać za sobą wiele zagrożeń: dla całego systemu lub innych użytkowników. Najbardziej oczywistym zapobiega się przez:

Dodatkowo uniemożliwia się dostęp do zamontowanego przez nieuprzywilejowanego użytkownika systemu plików wszystkim pozostałym użytkownikom, włączając w to roota. Ma to zapobiec dwóm rodzajom zagrożeń:

Powyższe ograniczenie możemy znieść, montując system plików z opcją allow_root lub allow_other (aby umożliwić dostęp odpowiednio rootowi lub wszystkim użytkownikom). Jeśli jednak system jest montowany przez użytkownika nieuprzywilejowanego, to aby mógł użyć tych opcji, w pliku konfiguracyjnym FUSE (/etc/fuse.conf), musi być obecna opcja user_allow_other.

Perspektywa programisty

FUSE definiuje pokaźny zestaw możliwych operacji. Poniżej przedstawiono definicję struktury reprezentującej cały zestaw.


struct fuse_operations {
	int (*getattr) (const char *, struct stat *);
	int (*readlink) (const char *, char *, size_t);
	int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
	int (*mknod) (const char *, mode_t, dev_t);
	int (*mkdir) (const char *, mode_t);
	int (*unlink) (const char *);
	int (*rmdir) (const char *);
	int (*symlink) (const char *, const char *);
	int (*rename) (const char *, const char *);
	int (*link) (const char *, const char *);
	int (*chmod) (const char *, mode_t);
	int (*chown) (const char *, uid_t, gid_t);
	int (*truncate) (const char *, off_t);
	int (*utime) (const char *, struct utimbuf *);
	int (*open) (const char *, struct fuse_file_info *);
	int (*read) (const char *, char *, size_t, off_t,
		     struct fuse_file_info *);
	int (*write) (const char *, const char *, size_t, off_t,
		      struct fuse_file_info *);
	int (*statfs) (const char *, struct statvfs *);
	int (*flush) (const char *, struct fuse_file_info *);
	int (*release) (const char *, struct fuse_file_info *);
	int (*fsync) (const char *, int, struct fuse_file_info *);
	int (*setxattr) (const char *, const char *, const char *, size_t, int);
	int (*getxattr) (const char *, const char *, char *, size_t);
	int (*listxattr) (const char *, char *, size_t);
	int (*removexattr) (const char *, const char *);
	int (*opendir) (const char *, struct fuse_file_info *);
	int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t,
			struct fuse_file_info *);
	int (*releasedir) (const char *, struct fuse_file_info *);
	int (*fsyncdir) (const char *, int, struct fuse_file_info *);
	void *(*init) (struct fuse_conn_info *conn);
	void (*destroy) (void *);
	int (*access) (const char *, int);
	int (*create) (const char *, mode_t, struct fuse_file_info *);
	int (*ftruncate) (const char *, off_t, struct fuse_file_info *);
	int (*fgetattr) (const char *, struct stat *, struct fuse_file_info *);
	int (*lock) (const char *, struct fuse_file_info *, int cmd,
		     struct flock *);
	int (*utimens) (const char *, const struct timespec tv[2]);
	int (*bmap) (const char *, size_t blocksize, uint64_t *idx);
	unsigned int flag_nullpath_ok : 1;
	unsigned int flag_reserved : 31;
	int (*ioctl) (const char *, int cmd, void *arg,
		      struct fuse_file_info *, unsigned int flags, void *data);
	int (*poll) (const char *, struct fuse_file_info *,
		     struct fuse_pollhandle *ph, unsigned *reventsp);
};
            
Dokładną dokumentację poszczególnych pól zobaczyć można w automatycznie wygenerowanej dokumentacji. Stworzenie własnego systemu plików sprowadza się do zaimplementowania dowolnego niepustego ich podzestawu. Choć biblioteka nie nakłada żadnych wymogów na ów podzestaw, według dokumentacji większość z nich jest niezbędna dla w pełni funkcjonalnego systemu plików.

Z drugiej strony, (ponownie, według dokumentacji) w większości można się obejść bez operacji: open, flush, release, fsync, opendir, releasedir, fsyncdir, access, create, ftruncate, fgetattr, lock, init, destroy. Te znajdą zastosowanie tylko w szczególnych przypadkach.

Implementacja w C

Przedstawiona powyżej struktura fuse_operations zdefiniowana w pliku fuse.h zawiera wskaźniki do funkcji odpowiadających wszystkim możliwym w FUSE operacjom. W większości ich deklaracja oraz semantyka jest podobna lub identyczna do standardowych funkcji operujących na plikach. Wyjątkiem jest przekazywanie informacji o błędach: Zamiast ustawiać odpowiednią wartość zmiennej errno, funkcje powinny zwrócić wartość ujemną, równą co do wartości bezwględnej kodowi błędu.

Implementacja systemu plików sprowadza się do zaimplementowania wybranych funkcji, następnie utworzenia zmiennej typu struct fuse_operations, w której ustawione zostaną wskaźniki odpowiadające zaimplementowanym funkcjom.

Następnie pozostaje już tylko wywołać funkcję fuse_main:

fuse_main(argc, argv, &my_ops);
            
gdzie pierwsze dwa argumenty są analogiczne do wywołania funkcji main, a ostatni wskazuje na naszą wartośc struktury fuse_operations. Program trzeba jeszcze tylko zlinkować z biblioteką libfuse.

Warto przyjrzeć się demonstracyjnej implementacji minimalistycznego systemu plików HelloWorld, dostępnej na stronie projektu: hello.c.

Implementacja w ciekawszych językach

Na stronach wiki projektu znaleźć można zaniedbaną listę bibliotek pozwalających na zaimplementowanie własnego systemu plików opartego na FUSE w języku innym niż C. Wiele z odnośników już nie działa, ale pewnie wciąż można znaleźć projekty, które pod nimi były kiedyś dostępne.

Na tę stronę warto zwrócić uwagę ze względu na ogromny zestaw dostępnych języków: Od dość oczywistych (C++), przez języki funkcyjne (m.in. Ocaml, Haskell, Erlang), języki wysokiego i bardzo wysokiego poziomu (m.in. Java, Python, PHP, Ruby), na skryptach powłoki kończąc. Większość pakietów zawiera też po kilka przykładowych systemów plików.

Ciekawe systemy plików

Poniżej przedstawiam skromną listę systemów plików napisanych w oparciu o FUSE, które moim zdaniem są interesujące:

Po pełną listę systemów plików należy się udać na stronę wiki projektu oraz wpis w Wikipedii. Znajdą się tam systemy o wielu różnych zastosowaniach, niewymienionych na powyższej liście.

Źródła


Janina Mincer-Daszkiewicz