ВИРТУАЛЬНЫЕ ФАЙЛОВЫЕ СИСТЕМЫ

Thank you for reading this post, don't forget to subscribe!

фай­ло­вая систе­ма - это иерар­хи­че­ское хра­ни­ли­ще дан­ных, при­дер­жи­ва­ю­щи­е­ся опре­де­лён­ной структуры.

ОСНОВЫ ФАЙЛОВЫХ СИСТЕМ

Ядро Linux тре­бу­ет, что­бы во всём, что счи­та­ет­ся фай­ло­вой систе­мой были реа­ли­зо­ва­ны мето­ды open()read(), и write() для посто­ян­ных объ­ек­тов, у кото­рых есть име­на. С точ­ки зре­ния объ­ек­тив­но ори­ен­ти­ро­ван­но­го про­грам­ми­ро­ва­ния, ядро счи­та­ет фай­ло­вую систе­му абстракт­ным интер­фей­сом, в кото­ром опре­де­ле­ны эти вир­ту­аль­ные функ­ции без реа­ли­за­ции. Таким обра­зом реа­ли­за­ция фай­ло­вой систе­мы на уровне ядра назы­ва­ет­ся VFS (Virtual Filesystem).

Тер­мин VFS лежит в осно­ве всем извест­но­го утвер­жде­ния о том, что в Unix-подоб­ных систе­мах всё явля­ет­ся фай­лом. Поду­май­те о том, насколь­ко стран­но, что при­ве­ден­ная выше после­до­ва­тель­ность дей­ствий с фай­лом /dev/console рабо­та­ет. На сним­ке пока­зан интер­ак­тив­ный сеанс Bash в вир­ту­аль­ном тер­ми­на­ле TTY. При отправ­ке стро­ки устрой­ству вир­ту­аль­ной кон­со­ли, она появ­ля­ет­ся на вир­ту­аль­ном экране. VFS име­ет и дру­гие, даже более стран­ные свой­ства. Напри­мер, в таких фай­лах мож­но выпол­нять поиск.

В таких попу­ляр­ных фай­ло­вых систе­мах как Ext4, NFS и даже в под­си­сте­ме /proc реа­ли­зо­ва­ны три основ­ные функ­ции в струк­ту­ре дан­ных на язы­ке Си, кото­рая назы­ва­ет­ся file_operations. Кро­ме того, неко­то­рые фай­ло­вые систе­мы рас­ши­ря­ют и пере­опре­де­ля­ют функ­ции VFS подоб­ным объ­ек­тив­но ори­ен­ти­ро­ван­ным спо­со­бом. Как утвер­жда­ет Роберт Лав, абстрак­ция VFS поз­во­ля­ет поль­зо­ва­те­лям Linux копи­ро­вать фай­лы из дру­гих опе­ра­ци­он­ных систем или абстракт­ных объ­ек­тов, таких как кана­лы не бес­по­ко­ясь об их внут­рен­нем фор­ма­те дан­ных. В про­стран­стве поль­зо­ва­те­ля с помо­щью систем­но­го вызо­ва read() про­цес­сы могут копи­ро­вать содер­жи­мое фай­ла в струк­ту­ры ядра из одной фай­ло­вой систе­мы, а затем исполь­зо­вать систем­ный вызов write() в дру­гой фай­ло­вой систе­ме, что­бы запи­сать полу­чен­ные дан­ные в файл.

Опре­де­ле­ния функ­ций, отно­ся­щи­е­ся к VFS нахо­дят­ся в фай­лах fs/*.c в исход­ном коде ядра. Под­ка­та­ло­ги fs/ же содер­жат раз­лич­ные фай­ло­вые систе­мы. Ядро так­же содер­жит объ­ек­ты, похо­жие на фай­ло­вые систе­мы, это cgroups, /dev и tmpfs, кото­рые нуж­ны на ран­нем эта­пе загруз­ки систе­мы и поэто­му опре­де­ле­ны в под­ка­та­ло­ге исход­ни­ков init/. Обра­ти­те вни­ма­ние, что они не вызы­ва­ют функ­ции боль­шой трой­ки из file_operations, зато они могут непо­сред­ствен­но читать и запи­сы­вать в память.

На схе­ме ниже нагляд­но пока­за­но, как из про­стран­ства поль­зо­ва­те­ля мож­но полу­чить доступ к боль­шин­ству фай­ло­вых систем. На рисун­ке нет кана­лов, тай­ме­ра POSIX и dmesg, но они тоже исполь­зу­ют мето­ды из струк­ту­ры file_operations и рабо­та­ют через VFS:

Суще­ство­ва­ние VFS спо­соб­ству­ет повтор­но­му исполь­зо­ва­нию кода, посколь­ку основ­ные мето­ды для рабо­ты с фай­ло­вы­ми систе­ма­ми не пере­опре­де­ля­ют­ся в каж­дой фай­ло­вой систе­ме. Это широ­ко исполь­зу­е­мая прак­ти­ка, одна­ко если в таком коде есть ошиб­ки, то от них стра­да­ют все реа­ли­за­ции, исполь­зу­ю­щие общие методы.

/TMP

Самый про­стой спо­соб выве­сти все вир­ту­аль­ные фай­ло­вые систе­мы, это выпол­нить такую команду:

mount | grep -v sd | grep -v :/

Она выве­дет все смон­ти­ро­ван­ные фай­ло­вые систе­мы, кото­рые не свя­зан­ны с физи­че­ским или сете­вым дис­ком. Оной из пер­вых точек мон­ти­ро­ва­ния вир­ту­аль­ных фай­ло­вых систем будет /tmp. Так поче­му не реко­мен­ду­ет­ся хра­нить содер­жи­мое /tmp на дис­ке? Пото­му что фай­лы из /tmp вре­мен­ные, а посто­ян­ные хра­ни­ли­ща намно­го мед­лен­нее памя­ти, где нахо­дит­ся tmpfs. Кро­ме того, физи­че­ские устрой­ства более под­вер­же­ны изно­су от частой запи­си, в отли­чие от опе­ра­тив­ной памя­ти. И нако­нец, фай­лы в /tmp могут содер­жать кон­фи­ден­ци­аль­ную инфор­ма­цию, поэто­му их луч­ше уда­лять при каж­дой перезагрузке.

К сожа­ле­нию, уста­нов­щи­ки неко­то­рых дис­три­бу­ти­вов всё ещё раз­ме­ща­ют /tmp на физи­че­ском дис­ке. Но если такое слу­чи­лось с вашей систе­мой, не рас­стра­и­вай­тесь. Вы може­те исполь­зо­вать инструк­цию с ArchWiki что­бы испра­вить про­бле­му. Но не забы­вай­те, что выде­лен­ная под tmpfs память боль­ше ни для чего не может быть использована.

/PROC И /SYS

Поми­мо /tmp, вир­ту­аль­ные фай­ло­вые систе­мы, с кото­ры­ми зна­ко­мо боль­шин­ство поль­зо­ва­те­лей Linux - это /proc и /sys (/dev пола­га­ет­ся на общую память и не под­дер­жи­ва­ет file_operations). Но зачем аж две? Давай­те разбираться.

Фай­ло­вая систе­ма procfs предо­став­ля­ет момен­таль­ный сни­мок состо­я­ния ядра и про­цес­сов, кото­рые оно кон­тро­ли­ру­ет в про­стран­стве поль­зо­ва­те­ля. Кро­ме того, в /proc ядро дела­ет доступ­ной инфор­ма­цию о пре­ры­ва­ни­ях, вир­ту­аль­ной памя­ти и пла­ни­ров­щи­ке. А ещё в /proc/sys раз­ме­ще­ны пара­мет­ры, кото­рые мож­но настро­ить из про­стран­ства поль­зо­ва­те­ля с помо­щью коман­ды sysctl. Состо­я­ние и ста­ти­сти­ка по каж­до­му про­цес­су нахо­дят­ся в дирек­то­ри­ях /proc/<PID>.

Пове­де­ние фай­лов в /proc пока­зы­ва­ет, насколь­ко вир­ту­аль­ные фай­ло­вые систе­мы могут отли­чать­ся от, тех, у кото­рых есть фай­лы на дис­ке. С одной сто­ро­ны, файл /etc/meminfo содер­жит инфор­ма­цию, выво­ди­мую коман­дой free. Но с дру­гой сто­ро­ны, он пустой! Как такое может быть. Ситу­а­ция напо­ми­на­ет зна­ме­ни­тую ста­тью, напи­сан­ную физи­ком Кор­нель­ско­го уни­вер­си­те­та Дэви­дом Мер­ми­ном в 1985 году под назва­ни­ем Есть ли Луна когда никто не смот­рит? На самом деле ядро соби­ра­ет ста­ти­сти­ку о памя­ти, когда про­цесс запра­ши­ва­ет её из /proc. И на самом деле в фай­лах из /proc ниче­го нет, когда никто не смот­рит. Как ска­зал Мер­мин, "Фун­да­мен­таль­ная кван­то­вая док­три­на состо­ит в том, что изме­ре­ние, как пра­ви­ло, не выяв­ля­ет ранее суще­ство­вав­шее зна­че­ние изме­ря­е­мо­го свойства.

Кажу­ща­я­ся пусто­та /proc име­ет смысл, посколь­ку доступ­ная инфор­ма­ция дина­ми­че­ская и акту­аль­ная на момент полу­че­ния. Ситу­а­ция с фай­ло­вой систе­мой /sys. Давай­те срав­ним сколь­ко фай­лов, раз­ме­ром хотя бы один байт, нахо­дят­ся в /proc и /sys:

В procfs такой файл толь­ко один - это кон­фи­гу­ра­ция ядра, кото­рую надо гене­ри­ро­вать толь­ко один раз во вре­мя загруз­ки. С дру­гой сто­ро­ны в ката­ло­ге /sys содер­жит­ся мно­го фай­лов боль­шо­го раз­ме­ра, но боль­шин­ство из них состав­ля­ют одну стра­ни­цу памя­ти. Обыч­но фай­лы из sysfs содер­жат одно чис­ло или стро­ку, в отли­чие от таб­лиц инфор­ма­ции, напри­мер, из /proc/meminfo.

Цель sysfs - предо­ста­вить доступ для чте­ния и запи­си свойств так назы­ва­е­мых объ­ек­тов kobjects в поль­зо­ва­тель­ском про­стран­стве. Един­ствен­ная цель kobject - под­счёт ссы­лок, когда послед­няя ссыл­ка на kobject будет уда­ле­на, систе­ма осво­бо­дит ресур­сы, свя­зан­ные с ним. К тому же, /sys/ состав­ля­ет основ­ную часть ста­биль­но­го ABI ядра для поль­зо­ва­тель­ско­го про­стран­ства, кото­рую никто и ни при каких обсто­я­тель­ствах не может сло­мать. Но это не зна­чит, что фай­лы в sysfs ста­тич­ны. Это бы про­ти­во­ре­чи­ло под­счё­ту ссылок.

Ста­биль­ность ABI огра­ни­чи­ва­ет то, что может появит­ся в /sys, а не то, что на самом деле есть в любой кон­крет­ный момент. Пере­чис­ле­ние раз­ре­ше­ний на фай­лы в sysfs дает пред­став­ле­ние о том, что настра­и­ва­е­мые пара­мет­ры устройств, моду­лей и фай­ло­вых систем мож­но про­чи­тать и изме­нить. По логи­ке procfs тоже долж­на быть частью ста­биль­но­го ABI ядра, одна­ко в доку­мен­та­ции об этом ниче­го не сказано.

СЛЕЖЕНИЕ ЗА VFS

Самый про­стой спо­соб узнать как ядро управ­ля­ет фай­ла­ми в sysfs - это посмот­реть на это всё в дей­ствии. А самый про­стой спо­соб это сде­лать на ARM64 или x86_64 - это исполь­зо­ва­ние eBPF. eBPF (extended Berkeley Packet Filter) - состо­ит из вир­ту­аль­ной маши­ны, рабо­та­ю­щей на уровне ядра, к кото­рой при­ви­ле­ги­ро­ван­ные поль­зо­ва­те­ли могут обра­щать­ся из команд­ной стро­ки. Исход­ный код ядра пока­зы­ва­ет чита­те­лю как ядро может что-то сде­лать. Инстру­мен­ты eBPF пока­зы­ва­ют как на самом деле всё происходит.

К сча­стью, начать рабо­ту с eBPF доволь­но про­сто с помо­щью инстру­мен­тов bcc, для кото­рых доступ­ны паке­ты в мно­же­стве дис­три­бу­ти­вов. Инстру­мен­ты bcc - это скрип­ты на Python с неболь­ши­ми фраг­мен­та­ми кода на Си, а это зна­чит, что любой кто зна­ком с этим язы­ком может их моди­фи­ци­ро­вать. На дан­ный момент суще­ству­ет око­ло 80 скрип­тов на Python в bcc, поэто­му каж­дый най­дёт то, что ему надо.

Что­бы полу­чить общее пред­став­ле­ние о рабо­те VFS в  рабо­та­ю­щей систе­ме исполь­зуй­те про­стые скрип­ты vfscount и vfsstat, кото­рые пока­жут, что каж­дую секун­ду выпол­ня­ют­ся десят­ки вызо­вов vfs_open() и подоб­ных функций:

В каче­стве менее обще­го при­ме­ра, давай­те посмот­рим что про­ис­хо­дит, когда к рабо­та­ю­щей систе­ме под­клю­ча­ет­ся USB накопитель:

В пер­вом при­ме­ре на этом сним­ке скрипт trade.py выво­дит сооб­ще­ние вся­кий раз, когда вызы­ва­ет­ся функ­ция sysfs_create_files(). Вы може­те видеть, что эта функ­ция была вызва­на про­цес­сом kworker после под­клю­че­ния USB, но какой файл был создан? Сле­ду­ю­щий при­мер иллю­стри­ру­ет пол­ную силу eBPF. Скрипт trade.py выво­дит трас­си­ров­ку вызо­вов ядра (опция -K), а так­же имя фай­ла, создан­но­го функ­ци­ей sysfs_create_files(). Фраг­мент в оди­нар­ных кавыч­ках - это стро­ка кода на Си, кото­рую Python скрипт ком­пи­ли­ру­ет и выпол­ня­ет внут­ри вир­ту­аль­ной маши­ны в ядре. Пол­ную сиг­на­ту­ру функ­ции sysfs_create_files () надо вос­про­из­ве­сти во вто­ром при­ме­ре что­бы мож­но было ссы­лать­ся на один из её пара­мет­ров в функ­ции выво­да. Ошиб­ки в этом коде вызо­вут ошиб­ки компиляции.

Когда USB-нако­пи­тель встав­лен, появ­ля­ет­ся трас­си­ров­ка вызо­вов ядра, пока­зы­ва­ю­щая что один из пото­ков kworker с PID 7711 создал файл с име­нем events в sysfs. При попыт­ке отсле­жи­вать вызов sysfs_remove_files() вы уви­ди­те, что извле­че­ние флеш­ки при­во­дит к уда­ле­нию фай­ла events в соот­вет­ствии с иде­ей отсле­жи­ва­ния ссы­лок. Отсле­жи­ва­ние sysfs_create_link() во вре­мя под­клю­че­ния USB нако­пи­те­ля пока­зы­ва­ет что созда­ет­ся не менее 48 ссылок.

Зачем же нужен файл events? Исполь­зуя инстру­мент cscope мож­но най­ти функ­цию __device_add_disk(), кото­рая вызы­ва­ет функ­цию disk_add_events(). А та в свою оче­редь может запи­сать в файл "media_change" или "eject request". Здесь ядро сооб­ща­ет в поль­зо­ва­тель­ское про­стран­ство о появ­ле­нии или исчез­но­ве­нии дис­ка. Это намно­го инфор­ма­тив­нее, чем про­сто ана­лиз исходников.

ФАЙЛОВЫЕ СИСТЕМЫ ТОЛЬКО ДЛЯ ЧТЕНИЯ

Никто не выклю­ча­ет сер­вер или ком­пью­тер, про­сто отклю­чив пита­ние. Поче­му? Пото­му что при­мон­ти­ро­ван­ные фай­ло­вые систе­мы на физи­че­ских устрой­ствах могут выпол­нять запись и струк­ту­ры дан­ных фай­ло­вой систе­мы могут быть не пол­но­стью пере­не­се­ны на диск. Когда такое про­ис­хо­дит, вам при­дет­ся при сле­ду­ю­щей загруз­ке подо­ждать вос­ста­нов­ле­ния фай­ло­вой систе­мы с помо­щью fsck, а в худ­шем слу­чае вы даже може­те поте­рять данные.

Тем не менее, вы навер­ное слы­ша­ли, что мно­гие встра­и­ва­е­мые устрой­ства, такие как марш­ру­ти­за­то­ры, тер­мо­ста­ты и даже авто­мо­би­ли рабо­та­ют под Linux. У мно­гих из этих устройств нет поль­зо­ва­тель­ско­го интер­фей­са и их невоз­мож­но кор­рект­но выклю­чить. Пред­ставь­те авто­мо­биль, основ­ной ком­пью­тер кото­ро­го пол­но­стью отклю­ча­ет­ся, когда раз­ря­жа­ет­ся бата­рея. Как тогда быст­ро вклю­чить ком­пью­тер, когда дви­га­тель сно­ва зара­бо­та­ет и бата­рея вос­ста­но­вит заряд? Ответ прост. IoT устрой­ства исполь­зу­ют кор­не­вую систе­му в режи­ме толь­ко для чте­ния или корот­ко ro-rootfs.

Такой под­ход даёт несколь­ко пре­иму­ществ, кото­рые менее оче­вид­ны чем непо­вре­жда­е­мость. Одна из них - виру­сы не могут выпол­нять запись в /user или /lib если ни один про­цесс Linux не может выпол­нять туда запись. Во вто­рых это обес­пе­чи­ва­ет удоб­ную под­держ­ку уда­лён­ных устройств, посколь­ку они име­ют такую же фай­ло­вую систе­му как и тесто­вые образ­цы. Самое важ­ное обсто­я­тель­ство тако­го под­хо­да - раз­ра­бот­чи­кам при­хо­дит­ся решать на эта­пе про­ек­ти­ро­ва­ния какие ком­по­нен­ты систе­мы долж­ны быть неиз­ме­ня­е­мы­ми. Рабо­та с ro-rootfs ино­гда может быть очень слож­ной, но пре­иму­ще­ства ком­пен­си­ру­ют недостатки.

Созда­ния таких фай­ло­вых систем тре­бу­ет допол­ни­тель­ных уси­лий от раз­ра­бот­чи­ков и здесь сно­ва на помощь при­хо­дит VFS. Linux тре­бу­ет, что­бы фай­лы в /var были доступ­ны для запи­си, кро­ме того, мно­же­ство при­ло­же­ний будут пытать­ся созда­вать скры­тые фай­лы в домаш­ней пап­ке. Реше­ни­ем для фай­лов кон­фи­гу­ра­ции в домаш­нем ката­ло­ге моет быть их пред­ва­ри­тель­ное созда­ние. В ката­лог /var мож­но мон­ти­ро­вать отдель­ный раз­дел, доступ­ный для запи­си, тогда как корень будет все ещё досту­пен толь­ко для чте­ния. Дру­гая же аль­тер­на­ти­ва - исполь­зо­ва­ние bind и overlay монтирования.

BIND И OVERLAY МОНТИРОВАНИЕ

Луч­ше все­го про мон­ти­ро­ва­ние фай­ло­вых систем и про bind мон­ти­ро­ва­ние в част­но­сти мож­но узнать выпол­нив такую команду:

man mount

Bind и overlay мон­ти­ро­ва­ние поз­во­ля­ет раз­ра­бот­чи­кам встра­и­ва­е­мых систем и систем­ным адми­ни­стра­то­рам созда­вать фай­ло­вую систе­му, доступ­ную по опре­де­лён­но­му пути, а потом делать её доступ­ной при­ло­же­ни­ям по совсем дру­го­му пути. Для встра­и­ва­е­мых систем под­ра­зу­ме­ва­ет­ся что мож­но хра­нить фай­лы из /var на устрой­стве доступ­ном толь­ко для чте­ния, но нало­жить на /var tmpfs, так что­бы при­ло­же­ния мог­ли туда писать если им это надо. При пере­за­груз­ке все изме­не­ния в /var будут уте­ря­ны. Overlay мон­ти­ро­ва­ние поз­во­ля­ет объ­еди­нить tmpfs и обыч­ную фай­ло­вую систе­му так, что­бы в ней мож­но было изме­нять фай­лы, а bind мон­ти­ро­ва­ние поз­во­ля­ет сде­лать доступ­ны­ми для запи­си пустые пап­ки с tmpfs в ro-rootfs. Хотя overlayfs - это отдель­ная фай­ло­вая систе­ма, bind мон­ти­ро­ва­ние выпол­ня­ет­ся с помо­щью VFS.

Исхо­дя из все­го это­го, не уди­ви­тель­но, что кон­тей­не­ры Linux актив­но исполь­зу­ют overlay и bind мон­ти­ро­ва­ние. Давай­те посмот­рим что про­ис­хо­дит, когда мы запус­ка­ем кон­тей­нер с помо­щью systemd-nspawn. Будем исполь­зо­вать инстру­мент mountsnoop из набо­ра bcc:

Оста­лось посмот­реть что произошло

Здесь systemd-nspawn раз­ме­ща­ет нуж­ные фай­лы из procfs и sysfs хоста в кон­тей­нер. Здесь поми­мо фла­га MS_BIND, озна­ча­ю­ще­го bind-мон­ти­ро­ва­ние есть фла­ги, кото­рые настра­и­ва­ют связь при изме­не­ни­ях фай­лов про­цес­са­ми кон­тей­не­ра или хоста. Напри­мер, фай­ло­вая систе­ма может авто­ма­ти­че­ски отоб­ра­жать изме­не­ния в кон­тей­не­ре или скры­вать их.