Журналната файлова система ext3 за Hurd

Огнян Кулев


Съдържание

1. Увод
2. Журнални файлови системи
2.1. Принципи и предимства на журналните файлови системи
2.2. Журнални файлови системи на Linux
2.2.1. ext3
2.2.2. reiserfs3
2.2.3. reiserfs4
2.2.4. XFS
2.2.5. JFS
2.3. Реализация на файлови системи под Linux
3. Журнална файлова система ext3
3.1. Файлова система ext2
3.1.1. Файлови системи за UNIX
3.1.2. Особености на ext2
3.1.3. Конкретна структура
3.1.4. Заделяне на блокове и възли
3.2. Разлики между ext2 и ext3
3.3. Структура на журнала на ext3
3.3.1. Основи
3.3.2. Обща структура
3.3.3. Освободени блокове
3.3.4. Физическа структура
4. Архитектура на Hurd
4.1. Микроядрото Mach
4.1.1. История
4.1.2. Понятия
4.1.3. Remote Procedure Calls
4.2. Файловите системи в Hurd
4.2.1. Модел
4.2.2. Реализиране
5. Реализацията на ext3 за Hurd
5.1. libe3pager
5.2. libscache
5.2.1. Функции на библиотеката
5.2.2. Вътрешности
5.3. libjstore
5.3.1. Функции на библиотеката
5.3.2. Вътрешности
5.4. libe3diskfs
5.4.1. Добавени към библиотеката функции
5.5. ext3fs
5.5.1. Предварително стъпки
5.5.2. Промени
6. Заключение

Списък на таблиците

3.1. Общо начало на всички метаблокове на журнала
3.2. Главен блок
3.3. Таг на блок с данни

Списък на примерите

4.1. Извикване на метод на обект
4.2. Куха реализация на io_read
4.3. Примерно описание на част от интерфейс за работа с файлове

Глава 1. Увод

В областта на съвременните операционните системи UNIX®, в лицето на своите многобройни варианти, е може би най-старата, и въпреки това е все още използвана и популярна. Голям шум се вдига около варианта й GNU/Linux, който от няколко години придобива все по-голямо ипо-голямо разпространение. Възможността всеки да подобрява цялата система и тези подобрения да се използват от всички се оказа доста сполучлива, и все повече хора и фирми оценяват това.

И все пак, ядрото на Linux си остава със същия монолитен дизайн, както и UNIX® в началото на 70-те години на миналия век. Наистина, почти всички ядра на операционни системи сега са такива. Но при милиони редове това вече става неприятен проблем, чието първо решение е поддръжката на модули, които могат да се зареждат и премахват по време на работа. Така големината на ядрото може да се поддържа в приемливи граници, като нужната функционалност, най-вече драйвери, се зарежда при нужда.

Ядрото Hurd на проекта GNU е опит за една по-различна организация на тази най-вътрешна част на всяка операционна система – ядрото. Това е вложено още в името на проекта GNU – “GNU's Not Unix”. Затова вместо ядро се използва микроядро, чиято единствена задача е да осъществява общуването между процесите[1]. Цялата останала функционалност, като например TCP/IP стека, файловите системи, оторизация и т.н., е разпределена между различни процеси, наричани сървъри. Нещата са докарани до там, че всеки потребител може да заменя тези сървъри за себе си, без това да влияе на цялостната стабилност на системата.

Обект на дипломната е файловата система на Hurd. Досега тя беше ext2, но с едно голямо ограничение – не можеха да се монтират дялове, по-големи от 1.5G. Дипломната работа се състои не само в поправяне на този дефект, но и в добавяне на журналните способности на ext3. Всичко това е доста ценно както за самия Hurd, така и за целия проект GNU, който пряко или косвено се използва от всеки свободен софтуер.



[1] Обикновено това се обозначава с термина IPC (Inter-Process Communication).

Глава 2. Журнални файлови системи

2.1. Принципи и предимства на журналните файлови системи

Всички операции върху файлове като правило “докосват” много места на файловата система. Тези промени на различни места по необходимост трябва да се извършат последователно – съвременните твърди дискове нямат способност да приемат заявка от типа: запиши тези няколко блока разпръснато едновременно. Проблемът се явява, ако по някаква причина, най-често спиране на тока, се прекъсне променянето на тези различни места. Тогава част от промените остават записани, а друга част остават незаписани. Появява се неконсистентност на файловата система. В компютрите обаче работата с такава неконсистентна файлова система почти сигурно води до още по-неприятни последици. Затова при стартиране на компютъра се проверява дали файловата система е затворена нормално. Ако не е, значи трябва да направим пълна проверка на всички метаданни на файловата система, за да бъдат поправени доколкото може всички неконсистентности. Всяка файлова система се придружава и със съответната програма, която прави тази проверка. В UNIX® тези програми се наричат под общото име fsck.

От друга страна обемът на твърдите дискове расте стремително. Фантастични обеми в миналото са долна граница на съвременните компютри. Това се превръща проблем, когато трябва да се провери консистентността на някоя файлова система. Например метаданните на файловата система ext2 са приблизително 1%. Това са 1G от 120G файлова система. Значи fsck трябва да премине през 1G само за да открие какво е развалено от незавършената операция.

Елегантно решение на този проблем е използване на транзакции, които са развити доста в базите данни. Транзакцията е поредица от операции, които или трябва всички да се изпълнят, или никоя да не се изпълни. В случая под “операция” разбираме промяна в блок от файловата система. За целта трябва да имаме предварителен пълен списък на промените, без те да са записани на съответните места във файловата система. Тези промени се записват само в журнал. След като са записани там, файловата система се променя според промените.

Смисъл на всичко това проличава, когато имаме непредвидено прекъсване на всико. Ако прекъсването е по средата на записването на промените в журнала, ние имаме напълно консистентна файлова система, защото самата файлова система не все още е докосната от тези промени. Ако прекъсването е по средата на прилагането на промените върху файловата система, имаме неконсистента файлова система. Обаче ние имаме пълния списък на промените в журнала. Простото повтаряне на тези промение от началото до края ни гарантира, че ще получим консистентна файлова система.

В общи линии това е действието на всеки журнал. Проверката на консистентността на цялата файлова система се заменя с проверка дали има транзакции в журнала, чиито промени да се приложат във файловата система.

2.2. Журнални файлови системи на Linux

Структура, разлики, силни и слаби страни. Всички поддържат ACL и квота под Linux.

2.2.1. ext3

Ext2 е измислена, за да бъде стандартната файлова система за Linux. Тя много прилича на файловата система UFS на операционната система BSD, която се води за класическа в UNIX®.

Ext3 е ext2 с добавен журнал, който се реализира от библиотеката JBD (Journal Block Device) вътре в Linux. Предимството на този подход е, че се стъпва на една изпитана технология (ext2), като просто се добавят журнални способности. Недостатъкът проличава в много сравнения с другите журнални файлови системи, където често ext3 заема последното място или последните места. Причината е именно в неоптимизираността на файловата система като цяло поради силната връзка с класическата файлова система за UNIX®.

2.2.2. reiserfs3

Това е една от най-дискутираните файлови системи за Linux. Проектът reiserfs има далечни цели, които ще позволят използването на файловата система да бъде толкова ефективно, колкото и база данни. Използването на много малки файлове (например от по няколко байта), разхвърляни в дълбока йерархия от директории, е доста бързо.

Технически reiserfs има няколко важни особености. Директорията съдържа не само имената на файловете, но и цялата метаинформация за тях. При това файловете са подредени в дърво, така че намирането на файл е много бързо. Присъствието на метаинформацията в дървото означава, че веднъж като се намери, можем направо да започнем да работим с файла, без да се обръщаме допълнително към друго място на диска, откъдето да взимаме метаинформация.

Важно подобрение е специалното отношение към опашките на файловете. Всяка файлова система разделя пространството си на равни по големина блокове. Когато се запазва файл, последният блок почти винаги съдържа празно пространство, просто защото големината на файла не се дели точно на блока. Данните на файла в последния блок се наричат опашка на файла. Във файловата система reiserfs опашката на блока се записва в метаинформацията, т.е. в директорията. Това позволява още по-голямо ускоряване на достъпа, особено за малки файлове. Излиза, че с достъпа до файл, по-малък от блок, вече е прочел и самия файл!

Както виждаме, reiserfs ще е особено силен, когато директориите съдържат голямо количество файлове, също и когато файловете са малки. Но даже и да не е така, reiserfs е много бърза.

2.2.3. reiserfs4

Reiserfs4 е включен на 29 август 2004 г. във версия 2.6.8.1-mm2 на Linux. Кодът е пренаписан и структурите са силно променени, което значи, че имаме напълно нова файлова система. Основната цел на reiserfs4 е да затвърди позициите й на най-бързата файлова система. Тестовете показват, че тази цел е постигната. Затова бъдещите версии на reiserfs ще се концентрират върху споменатите по-горе далечни цели – създаване на добри условия файловата система да се използва като база данни.

Вместо B+ дървета се използват танцуващи дървета, който се използва при системите за бази данни. Още по-сериозна промяна е възможността да се пишат плъгини. Това няма да бъде разглеждано тук.

2.2.4. XFS

В сайта на XFS тя е определена като високопроизводителна журнална файлова система. Целите при конструирането на тази файлова система са различни от тези на reiserfs. Тя е създадена с мисъл за мащабируемост на капацитета и производителността, поддържайки много големи файлови системи. Това включва боравене с много големи файлове, голям общ брой файлове, големи директории и високопроизводителна входно-изходна система.

За тези цели се стъпва силно върху употребата на B+ дървета. Те се използват за следното:

  • Директориите са B+ дървета на файлове.

  • Отрязъците (extents) с блокове на всеки файл се държат в B+ дърво.

  • Индексите на динамично заделените възли (inodes) на файловете са B+ дърво.

XFS е напълно 64-битова: за брой на блоковете и файловете например. Обаче пространството е разделено на allocation groups, където за някои неща се използват локални 32-битови числа. Тези allocation groups се използват и в промяната на големината на файловата система.

За висока производителност е обърнато внимание на следните неща:

  • Стремеж блоковете на файловете да бъдат последователни. Фрагментираността забавя много. Използва се предварително заделяне на последователност от блокове, още преди те да бъдат поискани от програмите, блоковете на файла се представят групирани в отрязъци (extents), големината на блока на файловата система може да достигне до 64K

  • Групиране на четенето и записването на последователни блокове, което понякога ускорява много операциите. При четенето това подразбира и четене на блокове, които се очакват да се поискат.

  • Преки входно-изходни операции (Direct I/O), които позволяват блоковете да се записват направо от адресното пространство на програмите, без да се минава през ядрото.

  • Оптимизации за случая, когато много файлове четат и пишат в един файл. Това включва по-пестеливо използване на заключване и отключване на структурите от данни на файла в ядрото.

2.2.5. JFS

JFS, също както и XFS, е направена от голяма фирма (IBM) с цел голяма производителност и използване на транзакции за много по-бързо рестартира на системата. Системата е напълно 64-битова. Големината на блока е до 4K. Част от метаинформацията за цялата файлова система се съхранява във файлове. Използват се отрязъци (extents) за блоковете на файловете, които B+ дървета. Свободните блокове се отбелязват във вектор от битове, като има някои оптимизации. Възлите за файлове (inodes) могат динамично да се заделят. Директориите се съхраняват в един блок, ако файловете са до 8, и в B+ дърво ако няма място.

Файловата система е построена с мисъл за поддръжка на DCE DFS (Distributed Computing Environment Distributed File System). Най-голямата единица е агрегатът, който е дял в обикновения смисъл. Той може да съдържа няколко fileset, които могат да се монтират. Засега обаче се поддържа само един fileset. Пространството на един fileset се разделя на aggregation groups, съдържащи данни и метаданни. Стремежът е данните винаги да са в същата aggregaтion group като метаданните. Поддържа се промяна на големината на файловата система, както и дефрагментиране.

В някои тестове се отбелязва, че JFS заема сравнително малко памет в ядрото.

2.3. Реализация на файлови системи под Linux

Преди всичко трябва да сме наясно как програмите работят с файлове. Програмите се обръщат към ядрото чрез системни извиквания. Те са ограничен брой, обикновено 100–200, и трябва да обхващат всичко, което една програма може да поиска от ядрото. За програмите това са обикновени извиквания на функции.

Определена част от тези системни извиквания се отнасят до файлове. Именно те трябва да могат да бъдат обработвани от всяка от файловите системи. Само че в самите системни извиквания няма информация за коя файлова система става въпрос. Затова по средата стои виртуалната файлова система (VFS). Тя поема заявките за файлове и ги пренасочва към съответната файлова система. В UNIX® “файл” може да означава много неща – мрежова връзка, генерирано от ядрото съдържание (/proc) и др. VFS поема общите моменти в заявките към тези файлове.

Основните обекти, с които борави VFS, са: файлова система, възел (inode) и отворен файл. Всички системни извиквания, касаещи VFS, в крайна сметка се отнасят до точно едно от тези трите. Всеки такъв обект се поддържа от VFS, но в данните за него има и вектор на функции, които изпълняват конкретни действия, като например четене на байтове от отворен файл. Затова всички отворени файлове са като една голяма група от едни и същи обекти, но векторите им на действията сочат към различни файлови системи.

Отгоре на файловата система е VFS, която чрез векторите на действията вика файловата система. Отдолу е кешът на буферите, чрез който файловата система чете и променя самата файлова система. Кешът на буферите има две основни функции: взимане на буфер, съдържащ определен блок от файловата система, и връщане (записване) на този буфер. Използването на кеш на буфер дава огромно ускорение и удобство в сравнение с това файловата система пряко да работи с дисковете. Прякото взаимодействие с дисковете е оставена на кеша.

Глава 3. Журнална файлова система ext3

Всички файлови системи за UNIX® могат да се смятат като варианти на един първоначален модел на файловата система. Засега е достатъчно да отбележим, че пространството в заделения за файловата система дял се използва за два основни вида данни: данни и метаданни. По необходимост всяка операция с файл променя няколко блока от метаданните и няколко блока от данните. Естествено, това става в някакъв ред и ако внезапно спре захранването на компютъра, например, операцията ще бъде наполовина изпълнена и файловата система ще остане в неконсистентно състояние. Затова такива случаи се отбелязват и при пръв удобен случай файловата система се проверява за всевъзможни грешки, резултат на такива наполовина свършени операции.

Официалната” файлова система за ядрото Linux е ext2. Тя не се различава много от Berkley Fast File System, която отличава от оригиналната UNIX® файлова система най-вече по разпределението на метаданните по диска, позволяващо по-бърз достъп. Т.е. общо взето ext2 се вписва в класическия модел. Изискванията на потребителите обаче, и малки, и големи, са тези бавни проверки за грешки на файловите системи да се избегнат максимално, а ако може – и въобще да изчезнат. Това се постига с използването на някакъв вид транзакции, както в базите данни. Общото наименование на такива файлови системи, които използват транзакции, е журнална файлова система. Името идва от използването на журнал, в който се записват всички промени още преди те да се извършат действително[2]. След това се извършват и самите промени. Ако по средата на промените процесът се прекъсне, промените се извършват по-късно, по време на възстановяването, според информацията в журнала. И така, ext3 не е нищо друго, освен ext2, но с добавен журнал. Това е направено съвсем съзнателно с цел удобство. (XXX: Какво удобство?)

3.1. Файлова система ext2

3.1.1. Файлови системи за UNIX

Всички файлови системи за UNIX-системите по необходимост имат еднакви свойства. Дискът условно може да бъде разделен на четири части: главен блок (super block), възли (i-nodes), таблица на свободните блокове и място за данни. Всеки възел се достъпва чрез номера му, който е число от тип ino_t. Там се съдържа всички метаданни за файл, като например време на последна промяна и големина, освен самото име на файла. Пълният списък на метаданните във възел може да се види в определението на struct stat от sys/stat.h (или bits/stat.h в GNU, който файл никога не се включва направо). Директориите се представят по същия начин като файловете, но във възела са отбелязани като директории. Съдържанието на тези директории-файлове е списък от двойки име на файл и номер на възел. По този начин се позволява различни имена на файлове в една файлова система да сочат към един и същ файл. Това са твърди връзки към файл (hard links).

В областта за данни няма само и единствено съдържание на файловете. Списъкът на блоковете на съдържанието също се държи в блокове от областта за данни. Класическото изпълнение е във възела да има номерата на първите 10 блока от данни. Ако файлът е голям до 10 блока, използваното пространство от областта за данни ще е само за съдържанието на файла. Това насърчава използването на кратки файлове. При повече от 10 блока се добавя един косвен блок, чието съдържание е само от списък от блокове с данни. Ако това не стигне, добавя се двоен косвен блок, който съдържа номерата на единични косвени блокове. Ако и това не стигне, добавя се троен косвен блок, който съдържа номерата на двойни косвени блокове.

3.1.2. Особености на ext2

Оттук нататък конкретното представяне на файловите системи върху диска се различава. В ext2 вторият 1024-байтов блок от дяла е на главния блок. Останалата част от дяла е разделена на блокови групи. Всяка блокова група си поддържа таблица на свободните блокове и масив от възли, чиите файлове са разположени най-вече върху същата блокова група. Смисълът зад всичко това е работата с файлове да предизвиква възможно най-малко преместване на главата на твърдия диск. Ако искаме да създадем нов файл, след избиране на възела ще се работи най-вече с таблицата на свободните блокове на същата блокова група, като и блоковете на файла ще се изберат от там.

3.1.3. Конкретна структура

Главният блок се намира във втория 1024-байтов блок на дяла. Там се съдържат основните данни за файловата система като големина на блока, брой на блоковете и възлите, брой на свободните блокове и възли. Понеже трябва все пак през определено време да се проверява файловата система, поддържат се брояч на монтиранията, време на последната проверка, както и настройка при какъв брой на монтиранията или след колко време да се извърши нова проверка.

От първия блок веднага след главния блок започват описателите на всички блокови групи. Всеки описател съдържа информация колко е голям блока, броя на всичките и свободните блокове и възли в тази блокова група, както и в кои блокове са таблицата на свободните блокове и таблицата на възлите.

В началото на всяка блокова група винаги стоят главен блок и описатели на блоковите групи. Обаче промяната им винаги е само в първата блокова група, освен ако при монтирането на файловата система не е зададено друго. В останалите информацията не е актуална и съвпада само в непроменливите части. Например във всички копия на описателя на дадена блокова група има верен общ брой на блоковете, но само в един описател ще има актуален брой на свободните блокове. Цялото това излишество на важна метаинформация е за да може в случай на разваляне на първта блокова група, все пак използвайки останалите блокови групи да се възстанови донякъде файловата система. Неактуалността на променливата метаинформация не е толкова важна, защото тя може да се възстанови. Например броя на свободните блокове може да се възстанови просто като се преброят тези блокове в таблицата на свободните блокове.

В блоковата група се нейните таблица на свободните блокове, таблица на свободните възли и таблица на самите възлите. Таблиците на свободните блокове и възли са от по един блок, в който всеки блок или възел от блоковата група е представен от един бит, показващ дали е свободен или не. Разположението на тези таблици изцяло се определя от описателя на блоковата група.

Почти всичко от информацията във възела може да се получи от програмите чрез системното извикване stat. Във възела обаче информацията е представена в един по-“разпръснат” вид. Например големината на файла се съдържа в две 32-битови полета, i_size и i_size_high, които съвсем не са едно до друго физически на диска. Това е резултат от стремежа за съвместимост с по-старите версии на ext2.

От полетата, които не се връщат от stat, най-важен е векторът с номерата на блоковете с данни. Първите 12 номера са на първите 12 блока с данни. 13-ият номер е на косвен блок, който изцяло е вектор с номера на блокове с данни. 14-ият номер е на косвен блок, който изцяло е вектор с номера на косвени блокове, приличащи на 13-ия. 15-ият номер е на косвен блок, който изцяло е вектор с номера на блокове, приличащи на 14. По този начин могат да бъдат представени наистина много големи файлове, но колкото е по-малък файла, толкова по-малко ще заема и метаинформацията за файла. Номер 0 на блок означава празен блок, който трябва да се запълни с нули, ако се изиска. Това са така наречените дупки във файла. Те не са нещо специфично за ext2, а се поддържат от почти всички файлови системи за UNIX®.

3.1.4. Заделяне на блокове и възли

Алгоритмите за заделяне са важна част от ext2. Принципът е блоковете и възлите да са близо един до друг, ако се очаква да се използват заедно. По отношение на възлите това значи възелът да е в същата блокова група, където е и съдържащата го директория. По отношение на блоковете функцията за заделяне на нов блок приема като параметър блок-цел, около която да се задели новия блок. Първо се пробва дали има свободен блок малко по-напред, после малко по-назад, след това изобщо някакъв блок в същата блокова група, и чак когато всичко е неуспешно, се търси из другите блокови групи. Едно от усъвършенстванията е предварителното заделяне на блокове. Когато се заделя блок, с това се заделят по специален начин и следващите 7 блока. При следващото искане на блок направо се връща вече заделения следващ блок.

3.2. Разлики между ext2 и ext3

ext3 е създадена така, че реализация на ext2 да може да чете и пише върху файлова система ext3. По отношение на дисковия формат, файловата система ext3 има просто добавен един скрит възел за журнала си. Освен този скрит възел, единствената друга разлика е, че има флаг за наличието на този възел на журнала. Когато ext2 чете и пише върху такава файлова система, журналът просто се игнорира.

Има и други добавки, които не са включени в реализацията за Hurd. Това са например хеширането на директориите и списъците за контрол на достъпа (Access Control Lists, ACL). И двете са реализирани по същия начин – ext2 може да монтира и променя такава файлова система, като просто няма да вижда разширенията.

3.3. Структура на журнала на ext3

3.3.1. Основи

Принципът на работа на журнала е да се запишат предварително действията, които ще се извършат. След това се извършват самите действия. Тези две записвания могат да се нарекат предварителен запис и същински запис. Ако предварителният запис се прекъсне, още нищо не е записано в действителност и всичко е консистентно. Ако същинския запис се прекъсне, при ново стартиране на системата предварителният запис е пълен и той се преиграва (replay). Частичен предварителен запис не се преиграва. Затова последният блок от предварителния запис е завършващия блок. Ако той липсва, частичният предварителен запис не се преиграва.

3.3.2. Обща структура

Журналът е отделна единица от файловата система. Въпреки че за удобство често се реализира като скрит файл на файловата система, журналът може да бъде и върху дял на друг диск, например. Във формата на журнала е включена възможността той да се използва от няколко файлови системи едновременно.

Самият журнал е цикличен и се състои от последователност от транзакции, които съдържат най-вече предварителни записи. Всяка транзакция има номер и номерата се раздават в нарастващ ред. Главният блок на журнала съдържа две важни полета – началния блок и номера на най-старата транзакция, която не е завършила същинския си запис. При преиграването се започва от тази транзакция и се продължава или до достигане на недовършена транзакция, или до такава с по-малък номер.

3.3.3. Освободени блокове

Обикновено журналът се използва променен. Вместо да се записват и данни, и метаданни, само данните се слагат в журнала. Това ускорява журналната файлова система, като тя пак си остава консистентна. Но консистентни остават метаданните, не и данните. В повечето случаи това е приемливо, защото една от основните цели на журнала е да се избегне проверяването на файловата система след всяко неочаквано прекъсване на системата. Това проверяване е изцяло за метаданните, но не и за данните.

Обаче с тази промяна има един случай, при който може да има разваляне на данни. Той се може да се прояви само ако в един момент блок, който е бил с метаданни, се използва за данни. Такива метаданни са косвените блокове (indirect blocks), които съдържат номерата на блокове с данни или на други косвени блокове. Когато някой файл се изтрие или се смали, такива косвени блокове могат да бъдат освободени и по-късно да бъдат използвани за данни. Ако системата обаче се прекъсне, в журнала може да има транзакции, които да презапишат стария косвен блок върху новите данни. Нека проследим постъпково какво се случва:

  1. В транзакция x в косвен блок y се записва нещо.

  2. По време на транзакция x+1 косвен блок y е освободен.

  3. В транзакция x+2 блок y е използван за данни. Според правилата на журнала, данните се записват преди да е завършил предварителния запис.

  4. Транзакция x+3 е прекъсната. Същинският запис на всички транзакции от x нататък не е завършен.

  5. При преиграването на транзакция x върху блок y се записва съдържанието на стария косвен блок, по този начин изтривайки новите данни в блока.

Това положение може да се постигне и с по-малко транзакции, но тук са представени повече за по-голяма яснота.

Това е еднственият проблем, свързан с отделянето на данните от журнала. Решението е просто – поддържа се списък на освободените блокове (revoked blocks) и те не се преиграват. Точният алгоритъм се състои от следните правила:

  • Транзакциите, освен предварителен запис на блокове с метаданни, съдържат и списък с освободени блокове. В този списък са блоковете, които са освободени в съответната транзакция.

  • При преиграване преди всичко друго се преминава през всички транзакции и се прави краен списък на освободените блокове. За да влезе блок в този краен списък, трябва да са изпълнени следните условия:

    1. Блокът да е в списъка на освободените блокове на някоя транзакция.

    2. Блокът да отсъства от предварителните записи на всички транзакции, които са след транзакцията, в която е обявен за свободен.

След като имаме крайния списък на освободените блокове, по време на преиграването на предварителните записи никога не са записва върху тези блокове. Това е и причината за второто условие – един освободен блок може да се използва по-късно пак за метаданни. Тогава въпреки че в по-ранна транзакция е обявен за освободен, този блок трябва да се включи в преиграването.

3.3.4. Физическа структура

Таблица 3.1. Общо начало на всички метаблокове на журнала

ТипПолеОписание
uint32_th_magicJFS_MAGIC_NUMBER (0xc03b3998U)
uint32_th_blocktypeТип на блока: superblock, descriptor, revoke, commit
uint32_th_sequenceПореден номер на транзакцията на блока

Всички останали описания на блокове започват с общото начало.

Таблица 3.2. Главен блок

ТипПолеОписание
uint32_ts_blocksizeГолемина на блок
uint32_ts_maxlenОбщ брой на блоковете
uint32_ts_firstПървият блок, който може да се използва за транзакции
uint32_ts_sequenceНомер на първата транзакция за преиграване. Всички номера под този ще се игнорират.
uint32_ts_startНомер на блок от журнала, откъдето да започне преиграването.
int32_ts_errnoНомер на грешка при абортиране на журнала
uint32_ts_feature_compatОбратно съвместими възможности
uint32_ts_feature_incompatОбратно несъвместими възможности
uint32_ts_feature_ro_compatОбратно съвместими възможности, но само за четене
uint8_ts_uuid[16]128-битов UUID на журнала
uint32_ts_nr_usersБрой на файловите системи, които си споделят този журнал
uint32_ts_dynsuperНомер на блок на динамичното копие на главния блок
uint32_ts_max_transactionГраница на журналните блокове за транзакция
uint32_ts_max_trans_dataГраница на блоковете с данни за транзакция
uint8_ts_users[16*48]UUID на файловите системи, които си споделят журнала

Самият журнал представлява последователност от транзакция. Смесени са revoke и descriptor блоковете, а последният блок е commit блокът. Без commit блок цялата транзакция е невалидна.

Самият descriptor блок има само обща част и последователност от тагове на блокове с данни.

Таблица 3.3. Таг на блок с данни

ТипПолеОписание
uint32_tt_blocknrНомер на блок na файлова система
uint32_tt_flagsФлагове

Последният таг има флаг JFS_FLAG_LAST_TAG. Всеки таг трябва да има UUID на файловата система, на който е блока. Това UUID се записва в 16 байта веднага след тага. Ако то е същото като предишното, може просто да се използва флага JFS_FLAG_SAME_UUID и да не се добавя след тага.

Веднага след descriptor блока има по един блок за всеки таг. Там се записва новото съдържание на блока от файловата система. Това съдържание не трябва да започват с магическата стойност JFS_MAGIC_NUMBER, защото иначе ще бъдат разпознати като журнални блокове. В такъв случай там се записва 0 и това се отбелязва чрез флага JFS_FLAG_ESCAPE. При преиграването нулата ще се замени обратно с истинската стойност.

Revoke блоковете имат само едно поле r_count от тип uint32_t. След него следват толкова на брой номера на блокове, които са обявени като вече несъдържащи метаданни на файловата система.



[2] Това е единият основен вариант за журнал, наричан write-ahead (XXX: Така ли се наричаше?), защото записва новото съдържание преди да стане действителна промяна. Другият вариант е да се записва старото съдържание преди да стане действителна промяна.

Глава 4. Архитектура на Hurd

От потребителска гледна точка задачата на операционната система е да осигурява среда за изпълнение на потребителските програми – приложенията. Освен библиотеки и програми, ОС се грижи и за разпределение на ресурсите на компютъра. В архитектурата на класическия UNIX®, както и на всички негови производни и подобия, централно място заема ядрото. То стои между програмите и целия останал свят – други програми, файлова система, мрежа и т.н. Ядрата са много големи програми, които често имат способността да се добавя и изкарва функционалност от тях. Контрастирайки на тях, в Hurd ядрото е малко и затова се нарича микроядро.

4.1. Микроядрото Mach

4.1.1. История

Микроядрото Mach е първото популярно микроядро, което и до днес има силно влияние, макар и не в оригиналния си вид. Всичко започва през 1975 в Университета на Рочестър. Там е било демонстрирано как операционните системи могат да се построят на модулен принцип, като общуването между процесите се осъществява чрез предаване на съобщения. Тази абстракция на общуването позволява програмите да са на различни машини в мрежа, но самите програми да не знаят това. Това и не ги засяга, понеже това предаване на съобщения е абстракция, които скрива тези детайли.

Един от инженерите в проекта е бил Ричард Рашид. През 1979 той се премества в Университета Карнеги Мелон, където продължава изследванията си върху операционните системи за предаване на съобщения, и през 1981 излиза резултатът – системата Accent.

Системата е била популярна в CMU, но през 1984 е изместена от UNIX®. Тогава Рашид решава да работи върху третото поколение на проекта, което той нарича Mach. Една от целите на Mach е да може да се използва огромното количество софтуер, написано за UNIX®. Други подобрения спрямо предишните проекти са въвеждането на нишки, подобрено общуване между процесите, многопроцесорна поддръжка и съвременна система за виртуална памет. По това време DARPA (същите, които са финансирали реализацията на TCP/IP) търсят многопроцесорна операционна система и харесват Mach. Така проектът си осигурява финансиране и е добавена поддръжка на 4.2BSD направо в ядрото, правейки го доста голямо, но за сметка на това съвместимо с UNIX®.

По това време започват проблемите на BSD с лицензите на AT&T, която е имала силно влияние тогава. Големи компании като IBM и DEC създават OSF (Open Software Foundation), опитвайки се да се справят с проблема. Те взимат Mach 2.5 и издават операционната система OSF/1. В крайна сметка този клон не става популярен, но има своето влияние в развитието на операционните системи. Ядрото Darwin на MacOS X е произлязло от OSF/1 и FreeBSD. Може би това е най-популярната операционна система в момента, използваща в крайна сметка Mach.

Решението на CMU относно лицензните проблеми е да отдели BSD функционалността в отделен процес на системата, изчиствайки по този начин Mach от всички лицензионни проблеми. Тази версия е 3.0 и излиза през 1989. Интересно е, че системата за виртуална памет на Mach по-късно е прехвърлена върху BSD. По-късно този клонинг се развива до UVM, който обаче не е върнат обратно в Mach.

Развитието на Mach продължава в Университета на Юта, където през 1984 излиза Mach 4, която е и последната версия на оригиналния Mach. Там е пренаписана поддръжката на компютърната архитектура IA-32 (x86) на Intel, както и са включени драйверите на версия 2.0 на изгряващата звезда Linux. От Mach 4 е създаден GNU Mach 1, който се използва от Hurd. През това време в Университета на Юта, използвайки опита си от Mach, създават библиотеката за операционни системи oskit, където се използват драйверите на Linux 2.2. Започвайки като частен проект, през 1999 Роланд МакГрад променя GNU Mach да използва изцяло oskit. По-късно е обявено, че това ще е GNU Mach 2, но към настоящия момент този клон е нестабилен и не се развива от никого.

4.1.2. Понятия

Mach използва някои понятия, които са по-бедни на съдържание от обикновено. Основната единица е задача, която обикновено се нарича процес, което е по-технически термин на програма или приложение. Задачата в Mach не е нищо повече от контейнер на останалите понятия: адресно пространство, нишки, портови права.

4.1.2.1. Нишки

Нишките винаги се отнасят за точно една задача, като задача може да има много нишки. Изпълнението на машинните инструкции винаги се изпълнява в някоя нишка. Когато процесорът превключи на друга нишка, състоянието на регистрите се запазва като контекст на нишката? и регистрите се зареждат от контекста на другата нишка. Нишките изцяло се изпълняват в контекста на притежаващата ги задача и използват общото адресно пространство и портовете. Частта от микроядрото, която превключва между нишките, се нарича планировчик (scheduler) и тя присъства във всички ядра. Всяка нишка има собствен стек, но това са неща, с които самият Mach не се занимава, а оставя на библиотеки за нишки, които заделят и освобождават памет за стек, както и други работи. Използваната в Hurd библиотека за нишки е стандартната за Mach – cthreads.

4.1.2.2. Портове

По-усложнени са портовете. Бидейки първото микроядро, в Mach предаването на съобщения е доста натоварен процес, за разлика от по-съвременни ядра от ново поколение, като L4 например. Порт е опашка от съобщения вътре в адресното пространство на Mach. Понеже е опашка, съобщенията се обработват в дисциплината FIFO (First-In First-Out). Единственият начин задача да се обърне към порт е да притежава портово право (port right), което, както и File Descriptor в UNIX®, е едно число, което може да се използва само и единствено в задачата, на която е дадено портовото право. Портовите права, както и нишките, принадлежат на точно една задача.

Портовите права са няколко вида:

  • За добавяне (изпращане) на съобщение (send right)

  • За изваждане (получаване) на съобщение (receive right)

  • За еднократно добавяне (изпращане) на съобщение (send-once right) Веднага след първото добавяне на съобщение, портовото право автоматично става невалидно.

Само една задача може да притежава правото за изваждане на съобщения, но различни нишки вътре в тази задача могат да чакат получаване на съобщение. При появяване на съобщение, по случаен начин една от чакащите нишки получава съобщението.

Има и още един специален вид портови права, наричани портови групи. Те са група от портови права за изваждане на съобщения и не могат да включват портови права от друг вид. От портова група може само да се изваждат съобщения, като портът, от който ще се извади съобщение, не може да се определи предварително. Портовите групи служат за задачи-сървъри, които трябва да обработват заявките на много клиенти, които идват чрез много портове. Вместо да има поне по една нишка за клиент, може всички портови права за изваждане да се включат в портова група и тя да чака съобщения (заявки) от клиентите. За по-голяма отзивчивост на сървъра може няколко нишки да чакат съобщение от една портова група.

Всички обекти, разглеждани тук – задачи, нишки, адресни пространства, – се управляват чрез портове, или по-точно – портови права за изпращане. Например създаването на нишка в задача е изпращане на съобщение към портовото право за изпращане на задачата. По този начин една задача може буквално да има пълен контрол върху друга задача, ако притежава портови права към всички Mach обекти на задачата. Получаването на тези портови права обаче е изцяло във властта на задачата, която ще бъде управлявана.

Съобщенията могат да съдържат и портови права. При създаването на портовите права в задачата, която изважда съобщението, естествено числата на портовите права са различни и специфични само за новата задача. Освен копиране на портово право, може да се извършва и преместване на портово право, при което то допълнително се изважда от задачата, която изпраща съобщението. Друга способност на такова предаване на портови права е, че те могат да се трансформират. Възможностите са следните:

  • Портово право за изваждане се превръща в портово право за добавяне. Така обикновено сървърите предават портови права на клиенти.

  • Портово право за изваждане се превръща в портово право за еднократно добавяне. Понеже предаването на съобщения винаги е еднопосочно, чрез това превръщане се дава възможност на другата страна да даде еднократен отговор.

  • Портово право за добавяне се превръща в портово право за еднократно добавяне.

Като следствие на правилата за портовите права, следните ограничения са в сила:

  • При предаване в съобщение на портово право за изваждане, това трябва да бъде предаване чрез преместване, понеже не може две задачи да имат право за изваждане от един и същ порт.

  • При предаване в съобщение на портово право за еднократно добавяне, това трябва да бъде предаване чрез преместване, понеже иначе би се позволило към този порт да се добавят 2 съобщения, използвайки отначало само едно право за еднократно предаване на съобщение.

4.1.2.3. Адресно пространство

Всяка задача има точно едно адресно пространство, което е разделено на равни по големина страници, като големината изцяло зависи от компютърната архитектура. Някой страници се намират във физическата памет, а някои въобще не се намират там. Така задачата може да си мисли, че има много памет на разположение, но колко от тази памет реално е готова за ползване е друг въпрос.

Страниците от адресното пространство винаги са отражение на нещо зад тях. Затова тези страници, които в момента са във физическата памет, се наричат кеширани страници и винаги са свързани с някой обект за памет (memory object). Тези обекти за памет доставят и връщат обратно страници. Отвъд тези операции, Mach не знае откъдето се появяват и изчезват страниците. Подразбиращият се обект за памет просто записва страниците в swap мястото.

Обектите за памет не взимат никакви решения – те просто изпълняват заявките на Mach. Самите решения се взимат от микроядрото, което разполага с цялата физическа памет, т.е. с целия кеш за страници, и по този начин има пълен поглед кои страници се използват и кои – не. Тази информация помага в решенията кои страници да се изхвърлят (“evict”) и кои да останат. При достъп до страница, която не е кеширана, т.е. не е във физическата памет, микроядрото спира съответната нишка, изпраща заявка към обекта за памет да даде страницата, изчаква и след като я получи, “монтира” я във физическата памет и адресното пространство на нишката (т.е. адресното пространство на задачата на нишката) и продължава изпълнението на тази нишка. Дори може обектът за памет да върне грешка при доставяне на страницата и тази грешка да се предаде на задачата. Това става чрез изключенията на Mach, които са подобни на сигналите, използвани в UNIX®.

Реализирането на обекти за памет обаче не е лесна работа. Трябва да се изпълни строгия протокол на Mach и да се вземат впредвид всякакви странични ефекти. Затова в Hurd е създадена специална библиотека за управление на обекти за памет – libpager. Използвайки тази библиотека, потребителят може да дефинира страницатор, който използвайки потребителски функции за четене и записване на страници, да може да се използва като обект за памет. Всички детайли около взаимодействието с Mach са оставени на библиотеката. По този начин асоциирането на части от адресното пространство с някакъв постоянен склад за данни става лесна задача за всяка програма.

Това е използвано в оригиналния ext2fs, за да може да се асоциира целия дял на файловата система с непрекъсната област от адресното пространство. И понеже това пространство е ограничено от 2G[3], транслираните дялове могат да бъдат до максимум около 1.8G[4].

Системата за виртуална памет на Mach е важна част от цялостната среда, доставяна от микроядрото. Това е използвано и във формата на съобщенията. Както видяхме по-рано, освен масив от байтове, едно съобщение може да съдържа и предавани портови права. Освен тях допълнително съобщението може да съдържа и страници от адресното пространство на задачата, която е добавила съобщението към опашката на порта. Който изважда съобщението, получава и виртуални копия на тези страници, т.е. двете пространства съдържат страница, която отразява една и съща страница от даден обект за памет. Ако някоя от двете задачи промени страницата, микроядрото прихваща това и първо прави копие на страницата. По този начин много ефективно могат да се предават големи количества данни, използвайки съобщения.

4.1.3. Remote Procedure Calls

Както и случая с обектите за памет, прякото програмиране чрез предаване на съобщения е трудна задача, изискваща много внимание и програмиране. Затова често се използват генератора на код MiG (Mach Interface Generator). Приликите с COM и CORBA не са случайни – имат общи корени. Мисленето при този подход се променя към обектно-ориентирано. Вместо да се предават съобщения, имаме манипулатори на обекти (object references). Чрез тези манипулатори програмите извикват методи на обектите. Списъкът на методите на един обект се нарича интерфейс. Ето пример за извикване на метод:

Пример 4.1. Извикване на метод на обект

error_t err;
mach_port_t port;
void *buf;
size_t buf_len;
off_t offset;
size_t size;
err = io_read (port, &buf, &buf_len, offset, size);

Първият аргумент на метода е манипулатора на обекта, който всъщност е порт. По традиция тези методи се наричат отдалечени извиквания на процедури (Remote Procedure Calls), защото отвънка изглежда просто като извикване на функция, но зад това извикване стоят много действия, докато заявката стигне до същинската функция в някоя друга задача или някоя друга машина и резултатът се върне.

Пример 4.2. Куха реализация на io_read

kern_return_t
trivfs_S_io_read (struct trivfs_protid *cred,
                  mach_port_t reply,
                  mach_msg_type_name_t replytype,
                  char **data,
                  mach_msg_type_number_t *datalen,
                  off_t off,
                  mach_msg_type_number_t amt)
{
  return EOPNOTSUPP;
}

Остава да разгледаме въпроса как така от клиентското извикване на io_read се стига до trivfs_S_io_read, включително и връщането на резултата обратно. Всичко това се изпълнява от генериран от mig код. Този код генерира две библиотеки: една за сървъра и една за клиенти. Всички подробности по приемането и изпращането на съобщения се съдържа в тези библиотеки. За целта обаче mig трябва да има подробна информация за отдалечените процедури. Тя се доставя от файл, написан на специален език за определяне на интерфейси (Interface Definition Language). Този език е специфичен за mig, както са специфични и езиците на COM и CORBA, които също се наричат IDL. Ето примерно описание на интерфейс, взето от hurd/io.defs:

Пример 4.3. Примерно описание на част от интерфейс за работа с файлове

subsystem io 21000;

#include <hurd/hurd_types.defs>
routine io_read (
        io_object: io_t;
        RPT
        out data: data_t, dealloc;
        offset: loff_t;
        amount: vm_size_t);

4.1.3.1. Интерфейси на GNU Mach

Както вече беше казано, всичко в Mach се управлява чрез портове. С цел удобство обаче, интерфейсите към тези Mach обекти са стандартния начин да се извършат действия. Затова всички действия се извършват чрез RPC на mig.

Стандартни интерфейси на Mach

mach/bootstrap.defs

Използва се само от първата задача, пусната от Mach. Тази задача извиква единствения метод на интерфейса, за да вземе привилигерованите портове, с които може да се управлява всичко останало.

mach/default_pager.defs

Управление на подразбиращия се обект за памет, който обикновено използва swap дял.

mach/default_pager_helper.defs

Добавяне на място към подразбиращия се обект за памет.

mach/exc.defs

Предаване на Mach изключение към задача

mach/mach4.defs

Профайлинг на задачи.

mach/mach.defs

Управление на задачи, нишки, адресни пространства, портове и обекти за памет. Това е главният файл с интерфейси.

mach/mach_host.defs

Управление на процесора и политиката му по отношение на виртуалната памет (“заковаване” на страници) и нишките (например приоритети).

mach/mach_norma.defs

NORMA позволява съобщенията да се предават между задачи на различни машини, свързани в мрежа. Не се използва в GNU Mach.

mach/mach_port.defs

Допълнителни действия с портове.

mach/memory_object_default.defs

Специфични за подразбиращия се обект за памет методи.

mach/memory_object.defs

Управление на обекти за памет.

mach/norma_task.defs

Създаване на задача в среда на неизползваната от GNU Mach NORMA.

mach/notify.defs

Извиквани от ядрото потребителски процедури за оведомяване относно събития.

4.2. Файловите системи в Hurd

4.2.1. Модел

Може да се каже, че UNIX® наложи дървовидното категоризиране на файлове, както е разпространено навсякъде сега. По този начин всички файлове са разпръснати в директории[5]. От своя страна директориите се водят като различен вид файлове, които са вътре в други директории. Една директория е по-специална: кореновата директория – началото на всички други директории.

Тази проста представа за файловете и директориите обаче не е достатъчна. В компютрите понякога има няколко диска, особено в сървърите, и всеки диск има по няколко дяла, които съдържат файлови системи. Затова някои директории на файловата система, така, както се виждат от потребителите, всъщност препращат към файлова система на друг дял. Тези директории се наричат точки на монтиране и е нормално в монтирана файлова система да има друга точка на монтиране. По този начин потребителят все още продължава да има единен поглед върху всички файлови системи на всички дялове, или поне тези, които са монтирани.

Концепцията за монтиране на файлови системи има интересни развития. Например не е задължително да се монтира дял от диск, а например споделена директория от друг компютър, чрез протокола NFS. Или пък направо FTP директория на някой сървър. А може и въобще да не става въпрос за физически файлове, а за генерирано съдържание, например /proc/123/cmdline в Linux съдържа командния ред, довел до стартирането на процес 123. Или стандартният начин за включване на маршрутизирането в Linux чрез командата echo 1 > /proc/sys/net/ipv4/ip_forward.

Продължавайки в същия дух, стигаме до понятието транслатор, както се употребява в Hurd. Транслаторът е директория или файл, заявките към който се изпълняват от обикновена програма, обикновено наричана сървър на транслатора. От своя страна в транслираната файлова система може да има още транслатори, които да препращат към сървъри.

Така получаваме една “разпределена” файлова система, в смисъл, че обработката на файловите операции, като четене и запис например, се извършва между разпръснати процеси. Транслатори може да поставя всеки потребител, което значи, че един потребител може влезе в транслирана от друга потребител част от файловата система. Несъмнено това поставя нови предизвикателства пред безопасността на цялата операционна система, които още не са добре проучени.

4.2.2. Реализиране

Както беше посочено в началото, микроядрото осигурява само и единствено общуването между отделните потребителски процеси[6]. Затова голяма част от Hurd-а се заема от разнообразни библиотеки, които улесняват системното програмиране. Важните за нашето разглеждане са следните:

Библиотеки за правене на файлови системи

libstore

В Hurd под “store” се обозначава това, което в UNIX-света се нарича “block device”. Само че store-ът не е задължително да съответства на дял от диска, а може да е, например, резултат от слепването на други два store-а.

libdiskfs

Реализира общите части на файловите системи, които са разположени върху дял, а не например споделени директории от мрежата.

libpager

Страницатор, който може да управлява части от адресното пространство на процеси. Микроядрото има интерфейс за това, но libpager прави това по много по-удобен за програмите начин.

Всички общувания между процесите в Hurd се осъществяват чрез интерфейси. В тях има изброени функциите и техните входни и изходни параметри. Описват се във файлове с разширение defs. Следните интерфейси се използват пряко във файловите системи:

Интерфейси, използвани във файловите системи

fsys.defs

Управление на файловата система като цяло, например чрез fsys_startup и fsys_syncfs.

fs.defs

Операции върху файловете като цяло, като например file_exec и file_chmod.

io.defs

Основни входно-изходни операции с отворен файл, например io_read и io_select.

Всички тези интерфейси се реализират от libdiskfs и са точно отражение на основните единици във виртуалната файлова система (VFS) в Linux. Библиотеката се опитва да свърши максимално работа, използвайки callback функции на програмата. От своя страна програмата се очаква да е получила store, който да достъпва с libstore.

Примерни callback функции[7], използвани от libdiskfs за промяна на списъка на файловете в директория.

diskfs_lookup_hard

Търси име на файл в директория. Задава се явно какво ще се прави с този файл, т.е. коя от следващите функции се очаква да се извика. В резултат се получава манипулатор[8], който се използва като параметър на функциите по-долу.

diskfs_drop_dirstat

Затваря върнатия от diskfs_lookup_hard манипулатор. Използва се, когато първоначалното намерение за употребата на манипулатора не може да се осъществи поради странични причини.

diskfs_null_dirstat

Връща такъв dirstat, който да се игнорира от diskfs_drop_dirstat.

diskfs_direnter_hard

Добавяне файл.

diskfs_dirrewrite_hard

Преименуване на файл.

diskfs_dirremove_hard

Изтриване файл.

Така стигаме до ограничението от 1G за файлова система. Понеже кодът е писан около 1995–1996, целият store на файловата система се изобразява върху адресното пространство на сървърския процес за файловата система. Заявките към това адресно пространство се управляват чрез libpager, който от своя страна работи със store-а. И понеже цялото адресно пространство е 32-битово, имаме 4G, половината от които обаче се използват за системни цели. Затова за изобразяването на целия store се използват гигабайт и нещо.

В ядрото Linux достъпа до блоковите устройства почти винаги се извършва през block layer. Основните операции в него са взимане на блок и даване на блок, като явно се указва дали блокът е променен при даването. Същият подход се използва и в решаването на ограничението от 1G за store на файлова система.



[3] Другите 2G от 32-битовото адресно пространство се използват за системни цели.

[4] Все пак има и други неща, които да се слагат в това пространство от 2G.

[5] Наричани още каталози и папки.

[6] Разбира се, някои от тези процес са чисто администраторски, например сървърът proc, който се грижи за съответствието процес–PID (микроядрото няма понятие за Process IDentifier), както и за UNIX–сигналите.

[7] Част от конкретните примери завършват на _hard, защото обикновено не се извикват направо, а чрез съответните функции без наставка.

[8] Структурата dirstat, която трябва да се дефинира от потребителя на libdiskfs.

Глава 5. Реализацията на ext3 за Hurd

По-долните библиотеки са сложени първи.

5.1. libe3pager

Към тази библиотека е добавена възможността да съобщава, когато страница от паметта се извади (evict). За целта се използват така наречените ценни страници (precious pages). Когато микроядрото Mach иска някоя страница да бъде записана, то вика потребителския RPC memory_object_data_return. Аргументът kcopy показва дали ядрото все още държи копие (“kernel copy”) на страницата. (Това копие е всъщност страницата в адресното пространство на задачата.) Когато страницата е отбелязана като ценна, тази функция се вика винаги, когато страницата повече няма копие. По този начин ние знаем кога страницата е извадена.

Стандартната библиотека libpager няма възможности за управление на ценни страници, така че те са добавени.

5.2. libscache

Това е нова библиотека, която реализира кеша на буферите. Работи върху склад (store) и дава при поискване буфери, съдържащи блокове от склада. Важно е всяко поискване на буфер да бъде съпроводено и с освобождаването му, понеже се държи сметка колко потребители има даден буфер във всеки един момент от време.

В реализацията на библиотеката броят на буферите е фиксиран брой. Когато няма свободен буфер, търси се такъв, чиято страница е извадена и който няма потребители. Съдържанието на такъв буфер се заменя с това на новия блок.

Освен тези най-прости действия – даваме и приемане обратно на буфери – към библиотеката са добавени и две способности от по-високо ниво. Едната е буферите-сенки (shadow buffers), които са копия на истинските буфери (real buffers). Промените в буферите-сенки не се отразяват върху блоковете на диска, от където са копирани. Също така един и същ буфер може да има много буфери-сенки. Поискването на даден блок чрез scache_get винаги връща истински буфер. Затова търсенето на буфер-сянка трябва да става винаги в някоя група от буфери (buffer set).

Групата от буфери е втората необичайна способност на libscache. Това позволява да се правят действия върху точно определен списък от буфери. Тези действия са връщане (записване) и игнориране на промените на страниците на буферите, както и търсене на блок в рамките на група. Не се позволява да има два буфера за един и същ блок вътре в група, но няма проблем някои буфери да са сенки. Принадлежността на буфер към група не увеличава брояча на потребителите на буфера и затова потенциално във всеки един момент някой буфер може да “изчезне” от всички групи, в които се намира, ако съдържанието на буфера се замени с друг блок.

5.2.1. Функции на библиотеката

5.2.1.1. Жизнен цикъл

struct scache *scache_open(struct store * store,
 size_t  block_size,
 size_t  cache_size_hint,
 struct port_bucket * bucket);

В допълнение на интересуващия ни склад store, трябва да се даде и големината на блоковете block_size. Може да се даде препоръка колко да е голям кешът на блоковете cache_size_hint, т.е. общия обем за данни, заеман от буферите. Засега тази стойност не се използва. Аргументът bucket указва къде да се сложи портовото право за изваждане на обекта за памет на използвания страницатор. Може да бъде NULL, при което библиотеката си създава собствен bucket.

void scache_close(struct scache *scache);

5.2.1.2. Работа с буфери

struct scache_buffer *scache_get(struct scache * scache,
 scache_block_t  block);

Тази функция връща буфер, който съдържа истинския блок. Броячът на потребителите на буфера се увеличава. Единствената легална употреба на получения буфер е използване на полето-указател data, което сочи съдържанието на блока.

void scache_put(struct scache *  scache,
 struct scache_buffer *  buffer);

С извикването на тази функция се освобождава буфера, като се намалява брояча на потребителите. След това извикване всяка употреба на буфера е недопустима, тъй като буферът може вече да съдържа друг блок.

void scache_ref(struct scache *  scache,
 struct scache_buffer *  buffer);

Понякога се налага буферът да бъде предаден на функция, която извиква го освобождава чрез scache_put. Ако е нужно същият буфер да се използва и след функцията, може да се увеличи брояча на потребителите на буфера и така да продължим да използваме буфера след извикване на scache_put.

5.2.1.3. Буфери-сенки

Има само една функция, която пряко работи със сенки:

struct scache_buffer *scache_copy(struct scache * scache,
 struct scache_buffer * buffer,
 struct scache * target_scache,
 scache_block_t  target_block,
 int  want_shadow);

Функцията прави копие на буфера buffer. Копието може да бъде в друг кеш target_scache, или в същия, което може да бъде отбелязано чрез предаване на NULL. Копирането може да запише съдържанието на един блок в друг блок с номер target_block, или в същия номер на блок, което може да бъде отбелязано чрез предаване на SCACHE_NO_BLOCK. И накрая, целевият буфер може да бъде нов буфер-сянка, което се задава чрез ненулева стойност на want_shadow. Ако се иска сянка, тя винаги е нова сянка, а ако се иска истински буфер, той винаги е единственият буфер за този блок в целевия кеш. На получения буфер се увеличава брояча на потребителите, но както вече беше казано, за да се освободи буфер-сянка, не само броячът на потребителите трябва да е нула, но и буфера-сянка да не е в нито една група.

5.2.1.4. Групи от буфери

struct scache_bufset *scache_bufset_new(struct scache * scache);

Създаване на нова група от буфери.

void scache_bufset_delete(struct scache_bufset * bufset);

Унищожава групата от буфери bufset. Преди това всички буфери от групата се изключват от групата

error_t scache_bufset_add(struct scache_bufset * bufset,
 struct scache_buffer * buffer);

Добавяне на буфера buffer към групата от буфер bufset. Буферът трябва да е от същият кеш, какъвто е и на групата. Също така в групата не трябва вече да има друг буфер за същия блок, т.е. един и същ буфер може да се добавя много пъти в групата, но ако се добави друг буфер за същия блок, например буфер-сянка, ще бъде върната грешка EEXIST.

error_t scache_bufset_remove(struct scache_bufset * bufset,
 struct scache_buffer * buffer);

Буферът buffer се изключва от групата буфери bufset.

struct scache_buffer *scache_bufset_find(struct scache_bufset * bufset,
 struct scache_buffer * buffer);

Проверява дали буферът buffer е в групата bufset. Ако е там, връща се buffer, иначе се връща NULL. Буферът buffer трябва да е отчетен като потребител, но това не се консумира.

struct scache_buffer *scache_bufset_get(struct scache_bufset * bufset,
 scache_block_t  block);

Търси дали в групата bufsetима буфер за блок block, дори и да е буфер-сянка. Ако има, увеличава му се брояча за потребителите и буферът се връща като резултат. Ако няма, връща се NULL.

error_t scache_bufset_put(struct scache_bufset * bufset,
 struct scache_buffer * buffer);

Връща взетият от групата bufset чрез scache_bufset_get буфер buffer.

error_t scache_bufset_iterate(struct scache_bufset * bufset,
 error_t  (*func) (struct scache_buffer *, void *);
 void * arg);

Функцията func се извиква за всеки буфер на групата bufset. Аргументът arg се предава като втори аргумент на func.

error_t scache_bufset_return(struct scache_bufset * bufset,
 int  wait);

Всички буфери с променено съдържание от групата bufset се записват в склада. Ако wait не е нула, при връщането от функцията всички буфер са синхронизирани. Буферите-сенки не се влияят от тази функция.

error_t scache_bufset_flush(struct scache_bufset * bufset,
 int  wait);

Игнорира промените във всички буфери от групата bufset. Следващият достъп до който и да е буфер ще прочете оригиналното непроменено съдържание на блока. Ако wait не е нула, при връщането от функцията всички промени в буферите са игнорирани, иначе ефектът може да дойде малко по-късно. Буферите-сенки не се влияят от тази функция.

5.2.2. Вътрешности

5.2.2.1. struct scache

Структурата scache представя един кеш на буфери. Броят на буферите се определят при създаването на кеша и не могат да се променят по-късно. Винаги се използва препоръчвания размер, който се дава на scache_open. Този размер се разделя на исканата големина на блок и така се получава броя на буферите.

Полето buffer сочи към масив от всички налични буфери. Съответните им данни са в соченото от image пространство. Така имайки номера на буфера, лесно може да се изчисли къде са неговите данни, както и имайки указател към данни, да се изчисли на кой буфер съответстват. Обаче буферите-сенки не пазят данните си в image.

Главната причина за поставянето на всички данни в непрекъсната област от адресното пространство е за да може да се използва libe3pager. Така libscache напълно определя какво съдържание ще има всяка страница от тази област. За целта са определени потребителски функции scache_pager_* за всички нужни потребителски функции pager_*, които се използват от libe3pager. Това позволява потребителят да има собствени страницатори. Потребителят трябва да определи функциите pager_*, които винаги приемат като един от параметрите си pager_info, който е указател към тип struct pager_info. Ако първата 32-битова дума, сочена от указателя, е SCACHE_PAGER_INFO_MAGIC, то потребителят трябва да остави libscache да обработи извикването чрез съответната функция scache_pager_*.

Изображението от номер на блок към истински буфер се пази в полето hash. Буферите-сенки не се отразяват по никакъв начин на това изображение, а могат да бъдат достигнати само чрез групите от буфери.

При искане на блок, който го няма в кеша, scache_get трябва да пренастрои някой буфер така, че да съдържа искания блок. Тази операция е същността на библиотеката. Първо, трябва да се намери подходящ буфер. Преди всичко той не трябва да се използва в момента, т.е. броячът на потребителите му трябва да е нула. След това трябва да е такъв, че да е не е използван наскоро. Това се постига чрез търсене на буфер, чието съдържание е изтласкано от адресното пространство. Нашият страницатор чрез scache_pager_notify_pageout се грижи винаги да поддържа информация кой буфер е изтласкан и кой – не. Търсенето на такъв буфер започва от буфер hint и продължава последователно. Ако се стигне последния буфер, преминава се към първия. След като се намери буфер, hint става следващия буфер. По този начин винаги се търси първо измежду най-старите буфери, защото за да се стигне до току-що избрания буфер, hint трябва да обиколи всички останали буфери.

Ако все пак не се намери буфер, отговарящ на условията, прави се опит да се изтласкат всички буфери, който имат нулев брояч на потребителите. После се пробва пак.

5.2.2.2. struct buffer

Полето block е номерът на блока, съдържащ се в буфера, а data сочи към данните на буфера. Тези две полета е позволено да се използват от потребителите, но само за четене. При истински буфер data може да се изчисли, но когато буферът е сянка, това е допълнително заделена чрез mmap памет. Това изисква и допълнителни действия, когато буферът се свързва с друг блок от диска. За улеснение буферите-сенки се освобождават веднага, след който никой вече не ги използва. Условието за това е броячът на потребителите да е нула и те да не са в никоя група от буфери. В момента, когато това стане изпълнено, например чрез scache_put или scache_bufset_remove, буферът-сянка става истински буфер, който обаче не сочи към блок.

Полето bufsets е изображение и служи като множество на групите, в които буферът е включен. Ключът и стойността в това изображение съвпадат и те са групата.

Полета flags показва дали буферът е в паметта (SCB_INCORE), дали е сянка (SCB_SHADOW). Флагът SCB_UNTOUCHED се използва при свързване на буфера с друг блок от диска. Проблемът е, че трябва буферът за малко да се отключи. През това време може някоя друга нишка също да търси буфер и да използва този буфер, който сме избрали. За да се избегнат проблемите, буферът се отбелязва като недокосван, отключва се и след като пак се заключи, проверява се дали някой не го е докосвал. Ако е докосван, този буфер се оставя и търсенето на подходящ буфер за блока продължава.

Последното поле е ref_count и то съдържа броя на потребителите, които използват в момента този буфер.

5.2.2.3. struct bufset

Полето buffers е изображение и служи като множество на буферите в групата. Ключът е номерът на блока, а стойността е буферът на този блок. В една група не може да има два буфера за един и същ блок, но може да има буфери-сенки.

5.2.2.4. Заключване и отключване

Библиотеката очаква да работи в многонишкова среда. За целта трябва да се изгради дисциплина за достъп до структурите. Всяка структура има поле-ключалка lock. За достъп до всяко поле, което търпи промяна, трябва структурата да е заключена. Пример за поле, което не се променя, е scache на bufset. Редът за заключване винаги трябва да е scache, bufset и накрая buffer. Ако не се използва някоя от тези структури, заключването й може да се прескача, но, например, не може да се заключва buffer и после, докато той е заключен, да се заключва scache. Обратният ред е забранен заради опасността от мъртва хватка (deadlock).

5.3. libjstore

Тази библиотека е еквивалентът на JBD (Journalled Block Device), само че за складове (store), а не за блокови устройства. Изцяло се стъпва върху libscache, като буферите-сенки и групите от буфери са съществена част от реализацията. Предполага се, че потребителят на библиотеката изцяло ще използва libjstore за достъп до склада, използвайки изключително двете функции jstore_block_get и jstore_block_put. Буферите винаги се взимат и оставят в рамките на някоя атомарна промяна (update), чието начало и край се бележат с jstore_update_begin и jstore_update_end.

5.3.1. Функции на библиотеката

5.3.1.1. Жизнен цикъл

#define JSTORE_OPEN_RECOVER	(1 << 0)
struct jstore *jstore_open(struct scache * journal_scache,
 struct scache * data_scache,
 int  flags);

Отваряне на журнала, съхраняван в journal_scache. Данните, които се журнализират, са в data_scache. Аргументът flags настройва отварянето. Засега може да има само един флаг – JSTORE_OPEN_RECOVER, – който разрешава автоматичното възстановяване на консистентността на журнализираните данни, ако в журнала има информация за това.

error_t jstore_close(struct jstore * jstore);

Затваря журнала jstore.

5.3.1.2. Атомарни промени

struct jstore_update *jstore_update_begin(struct jstore * jstore,
 scache_block_t  credits);

Започва атомарна промяна за журнала jstore. Обявява се колко блока credits най-много ще засегне промяната. Това е нужно за да се гарантира, че извършителят на промените няма да бъде изненадан по средата с грешка, че няма повече блокове в транзакцията.

error_t jstore_update_more_credits(struct jstore_update * update,
 scache_block_t  blocks,
 int  allow_restart);

Опит за увеличаване на кредита, даден за атомарната промяна update, с още blocks блока. Ако не може да бъдат дадени толкова блока и allow_restart е нула, връща се незабавно грешка. Ако е разрешено рестартирането на атомарната промяна и не могат да бъдат заделени толкова блокове, атомарната промяна приключва, изчаква се създаването на нова транзакция и от нея се взимат нужния брой блокове. В този случай извършителят на атомарната промяна трябва да е довел промените в един завършен вид, за да може в случай на рестартиране на промяната всичко винаги да остане консистентно.

error_t jstore_update_end(struct jstore_update * update);

Завършване на атомарната промяна update.

5.3.1.3. Взимане и връщане на буфери

#define JSTORE_GET_CREATE	(1 << 0)
#define JSTORE_GET_COMMITTED	(1 << 1)
#define JSTORE_GET_DATA		(1 << 1)
struct scache_buffer *jstore_block_get(struct jstore_update * update,
 scache_block_t  block,
 int  flags);

Връща буфер за блока block, който да се чете и/или променя като част от атомарната промяна update. По подразбиране се приема, че се искат метаданни, освен ако не е зададен флагът JSTORE_GET_DATA. Ако е зададен флагът JSTORE_GET_CREATE, предишното съдържание на блока няма значение и той трябва да се получи нулиран. Това е малка оптимизация. Ако е зададен флагът JSTORE_GET_COMMITTED, иска се съдържанието на метаданни както вече са записани в утвърдена (XXX: какво име да се сложи тук?) транзакция. Този флаг не трябва да се използва с друг флаг.

#define JSTORE_PUT_NOTMODIFIED		(1 << 0) /*???*/
#define JSTORE_PUT_FORGET		(1 << 1)
#define JSTORE_PUT_REVOKE		(1 << 2)
#define JSTORE_PUT_CLEAN		(1 << 3)
void jstore_block_put(struct jstore_update * update,
 struct scache_buffer * buffer,
 int  flags);

Връщане на буфера buffer, взет за атомарната промяна update. Ако не е променян, това трябва да се отбележи със задаването на флага JSTORE_PUT_CLEAN. Ако промените в буфера трябва да се игнорират, задава се флагът JSTORE_PUT_FORGET. Ако буферът е взет като метаданни и с тази атомарна промяна вече няма да се използва като метаданни, т.е. вече може да се използва за данни, трябва да се зададе флагът JSTORE_PUT_REVOKE.

5.3.2. Вътрешности

5.3.2.1. struct jstore

Полетата journal_scache и data_scache сочат кешовете на журнала и на файловата система, съответно.

Главният блок на журнала често се променя. Затова има полета за него. Буферът е superblock_buffer, като съдържанието му се сочи от superblock. За да може лесно да се записва, superblock_bufset е група, включваща само буфера на главния блок. Така записването на блока се състои в извикването на scache_bufset_return.

Транзакциите са от 3 типа: текуща, записваща се и изчакваща. Текущата е running_transaction, записващата е committing_transaction, а групата от изчакващите транзакции се слага в изображението checkpointed. В него ключът и стойността са самата транзакция.

Полето checkpointed_holes е вектор на освободените места в журнала, които са след tail_block. Полетата checkpointed_holes_count и checkpointed_holes_size са съответно текущата големина на вектора и заделената му големина.

Полето next_sequence е следващия номер, който да бъде използван за транзакция. Най-старият все още използван номер е tail_sequence.

Първият и последният блок от журнала са first_block и last_block-1, съответно. Следващият блок за заделяне е head_block, а tail_block е последният заделен блок. Така че заетите блокове в журнала са от tail_block до head_block-1, като при достигане на края на журнала (last_block), се продължава с началото (first_block). Незаетите блокове на free_nblocks на брой.

Кодът на фаталните грешки (като лош блок например) се пази в err.

При поискване на блок от страна на потребителя, важно е да знаем в коя транзакция се намира буферът, съдържащ този блок. За целта all_blocks е изображение от номер на блок към група от буфери, където се намира буфера на този блок. Имайки групата, сравнително лесно е да определим и самия буфер, както и транзакцията, в която е той. Обаче е възможно да не намерим блока в дадената група. Причината е, че той може автоматично да е изключен от групата, ако е в изчакваща транзакция. За ранно откриване на такива проблеми се използва групата all_blocks_bufset, която обхваща всички буфери, използвани от журнала.

5.3.2.2. struct transaction

Полето condition се използва за съобщаване на всички чакащи, че полето ref_count е променено.

В полето jstore е журналът, към който принадлежи транзакцията.

Номерът на транзакцията се съдържа в полето sequence.

Полето revoked е хеш-таблица, представяща множество от revoked блокове в тази транзакция. Ключът и стойността са номера на блокове.

Транзакциите обхващат няколко групи от буфери. Тези групи са:

data_bufset

Истински буфери с данни, които не подлежат на журнализиране.

metadata_bufset

Буфери-сенки с метаданни, които ще се записват предварително в журнала.

committed_bufset

Буфери-сенки с копия на метаданни преди промените в текущата транзакция.

log_bufset

Истински буфери за всичко, което отива в журнала. Например тук са копията на метаданните, които се записват в журнала.

shadow_metadata_bufset

Спомагателна група, участваща в прехода на метаданните от буфери-сенки в истински буфери.

Броят на позволените блокове, които могат да се слагат в журнала в тази транзакция, се държи в полето credits.

Броят на текущите потребителите на транзакцията се държи в ref_count.

Checkpoint транзакциите трябва да съдържат информация за това кой отрязък от журнала са ангажирали, за да се знае кои части от журнала са свободни за повторна употреба. Началото и края се пазят в полетата begin и end.

5.3.2.3. struct update

В тази структура няма почти нищо. Държат се указатели към журнала и транзакцията в полетата jstore и transaction съответно.

Важното поле е credits, което показва колко блока са дадени на разположение на тази атомарна промяна.

5.4. libe3diskfs

Основната промяна спрямо стандартния libdiskfs е добавянето на аргумент rpc_context към почти всички функции на библиотеката. Такъв обект се създава в началото на всеки RPC към сървъра на файловата система и се разрушава в каря на RPC-то. Създаването и разрушаването се извършва от потребителските функции diskfs_rpc_context_new и diskfs_rpc_context_delete.

5.4.1. Добавени към библиотеката функции

error_t diskfs_rpc_context_new(struct rpc_context ** context,
 struct protid * cred);

Отбелязва началото на атомарно действие, предизвикано от потребител. Тази потребителска функция трябва да създаде структура rpc_context и да върне указател към нея в *context. cred е стойността, която да се запише в структурата, като тя може да е NULL.

error_t diskfs_rpc_context_delete(struct rpc_context *  context);

Отбелязва края на атомарно действие, предизвикано от потребител.

5.5. ext3fs

ext3fs е променено копие на ext2fs на Hurd, но използващо libjstore за достъп до диска.

5.5.1. Предварително стъпки

Първата стъпка в правенето на дипломната работа беше премахване на гореспоменатото ограничение от 2G за големината на файл. За решаване на този проблем се използва като отправна точка реализирания в почти всички операционни системи кеш на буферите (buffer cache), но по различен начин.

Имаме кеш от буфери, в които слагаме съдържанието на блоковете от диска. Сървърът на файловата система иска все повече и повече блокове и в един момент целия кеш се запълва. Въпросът е съдържанието на кой буфер да изхвърлим, за да сложим новия блок.

Преди всички ние трябва да сме сигурни, че няма да изхвърлим буфер, който се използва в момента. За целта всеки буфер има брояч на потребителите (reference count). Един буфер никога не сменя съдържанието си, ако има потребител.

Целият код на ext2fs обаче въобще не води такова “счетоводство” за това кой използва каква част от диска. Затова всичко беше променено да използва тази нова дисциплина за работа с блок от диска.

От останалите буфери трябва да изберем някой, който вече да има ново съдържание. В помощ на това идва микроядрото, което има глобален поглед върху това кои страници от адресното пространство се употребяват повече от други. Най-малко употребяваните просто се изваждат, когато е нужна памет за други страници. Това изваждане се прихваща и така се знае във всеки един момент от време кои страници са извадени и кои са заредени в адресното пространство. За нов блок винаги се избира буфер, чиято страница е извадена и който няма потребители.

Сега цялата тази логика е изнесена в libscache.

5.5.2. Промени

Навсякъде за достъп до блоковете се използват функциите за взимане и освобождаване на буфери на libjstore. Осигуряват се необходимите функции, за да може libe3diskfs да започва и приключва атомарни промени. pokel.c въобще не се ползва.

Глава 6. Заключение

Много кръпки бяха пратени.