Перейти к содержимому

Что такое суперблок в файловой системе

  • автор:

Что такое суперблок в файловой системе

Первый элемент этой структуры (суперблок) — одинаков для всех групп, а все остальные — индивидуальны для каждой группы. Суперблок хранится в первом блоке каждой группы блоков (за исключением группы 1, в которой в первом блоке расположена загрузочная запись). Суперблок является начальной точкой файловой системы. Он имеет размер 1024 байта и всегда располагается по смещению 1024 байта от начала файловой системы. Наличие нескольких копий суперблока объясняется чрезвычайной важностью этого элемента файловой системы. Дубликаты суперблока используются при восстановлении файловой системы после сбоев.

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

Суперблок имеет следующую структуру.

Название поля Тип Комментарий
1.2em s_inodes_count ULONG Число индексных дескрипторов в файловой системе
s_blocks_count ULONG Число блоков в файловой системе
s_r_blocks_count ULONG Число блоков, зарезервированных для суперпользователя
s_free_blocks_count ULONG Счетчик числа свободных блоков
s_free_inodes_count ULONG Счетчик числа свободных индексных дескрипторов
s_first_data_block ULONG Первый блок, который содержит данные. В зависимости от размера блока, это поле может быть равно 0 или 1
s_log_block_size ULONG Индикатор размера логического блока: 0 = 1 Кб; 1 = 2 Кб; 2 = 4 Кб
s_log_frag_size LONG Индикатор размера фрагментов (кажется, понятие фрагмента в настоящее время не используется)
s_blocks_per_group ULONG Число блоков в каждой группе блоков
s_frags_per_group ULONG Число фрагментов в каждой группе блоков
s_inodes_per_group ULONG Число индексных дескрипторов (inodes) в каждой группе блоков
s_mtime ULONG Время, когда в последний раз была смонтирована файловая система
s_wtime ULONG Время, когда в последний раз производилась запись в файловую систему s_mnt_count USHORT Счетчик числа монтирований файловой системы. Если этот счетчик достигает значения, указанного в следующем поле ( s_max_mnt_count ), файловая система должна быть проверена (это делается при перезапуске), а счетчик обнуляется
s_max_mnt_count SHORT Число, определяющее, сколько раз может быть смонтирована файловая система
s_magic USHORT «Магическое число» ( 0xEF53 ), указывающее, что файловая система принадлежит к типу ex2fs
s_state USHORT Флаги, указывающее текущее состояние файловой системы (является ли она чистой ( clean ) и т.п.)
s_errors USHORT Флаги, задающие процедуры обработки сообщений об ошибках (что делать, если найдены ошибки)
s_pad USHORT Заполнение s_lastcheck ULONG Время последней проверки файловой системы
s_checkinterval ULONG Максимальный период времени между проверками файловой системы

s_creator_os ULONG Указание на тип ОС, в которой создана файловая система
s_rev_level ULONG Версия (revision level) файловой системы
s_reserved ULONG[235] Заполнение до 1024 байт


Next: Group Descriptors Up: Структура дискового раздела ext2 Previous: Структура дискового раздела ext2 Contents Index Alex Otwagin 2002-12-16

Суперблок

Суперблок содержит информацию, необходимую для монтирования и управления работой файловой системы в целом (например, для размещения новых файлов). В каждой файловой системе существует только один суперблок, который располагается в начале раздела. Суперблок считывается в память при монтировании файловой системы и находится там до ее отключения (размонтирования).

Суперблок содержит следующую информацию:

? Тип файловой системы (s_type)

? Размер файловой системы в логических блоках, включая сам суперблок, ilist и блоки хранения данных (s_fsize)

? Размер массива индексных дескрипторов (s_isize)

? Число свободных блоков, доступных для размещения (s_tfree)

? Число свободных inode, доступных для размещения (s_tinode)

? Флаги (флаг модификации s_fmod, флаг режима монтирования s_fronly)

? Размер логического блока (512, 1024, 2048)

? Список номеров свободных inode

? Список адресов свободных блоков

Поскольку число свободных inode и блоков хранения данных может быть значительным, хранение двух последних списков целиком в суперблоке непрактично. Например, для индексных дескрипторов хранится только часть списка. Когда число свободных inode в этом списке приближается к 0, ядро просматривает ilist и вновь формирует список свободных inode. Для этого ядро анализирует поле di_mode индексного дескриптора, которое равно 0 у свободных inode.

К сожалению, такой подход неприменим в отношении свободных блоков хранения данных, поскольку по содержимому блока нельзя определить, свободен он или нет. Поэтому необходимо хранить список адресов свободных блоков целиком. Список адресов свободных блоков может занимать несколько блоков хранения данных, но суперблок содержит только один блок этого списка. Первый элемент этого блока указывает на блок, хранящий продолжение списка и т.д., как это показано на рис. 4.1.

Выделение свободных блоков для размещения файла производится с конца списка суперблока. Когда в списке остается единственный элемент, ядро интерпретирует его как указатель на блок, содержащий продолжение списка. В этом случае содержимое этого блока считывается в суперблок и блок становится свободным. Такой подход позволяет использовать дисковое пространство под списки, пропорциональное свободному месту в файловой системе. Другими словами, когда свободного места практически не остается, список адресов свободных блоков целиком помещается в суперблоке.

Данный текст является ознакомительным фрагментом.

Продолжение на ЛитРес

Читайте также

4.5 СУПЕРБЛОК

4.5 СУПЕРБЛОК До сих пор в этой главе описывалась структура файла, при этом предполагалось, что индекс предварительно связывался с файлом и что уже были определены дисковые блоки, содержащие информацию. В следующих разделах описывается, каким образом ядро назначает

Учим файловую систему читать

image

Продолжаем цикл статей о создании файловой системы в ядре Linux, основанный на материалах курса ОС в Академическом университете .

В прошлый раз мы настроили окружение, которое понадобится нам, чтобы знакомится с ядром. Затем мы взглянули на загружаемые модули ядра и написали простой «Hello, World!». Ну и наконец, мы написали простую и бесполезную файловую систему. Пришло время продолжить.

Главная цель этой статьи научить файловую систему читать с диска. Пока она будет читать только служебную информацию (суперблок и индексные узлы), так что пользоваться ей все еще довольно трудно.

Почему так мало? Дело в том, что в этом посте нам потребуется определить структуру нашей файловой системы — то как она будет хранится на диске. Кроме того мы столкнемся с парой интересных моментов, таких как SLAB и RCU. Все это потребует некоторых объяснений — много слов и мало кода, так что пост и так будет довольно объемным.

Скучное начало

Наша файловая система должна быть простой, поэтому и хранится на диске она будет просто:

  • data blocks — блоки полезных данных, т. е. содержимое файлов и папок;
  • table of inodes — таблица индексных узлов; количество индексных узлов определяется при форматировании и хранятся они в непрерывной последовательности блоков;
  • free inodes — битовая карта свободных/занятых индексных узлов, которая занимает ровно 1 блок;
  • free blocks — битовая карта свободных/занятых блоков, так же занимает 1 блок;
  • super block — очевидно, суперблок нашей файловой системы; он хранит размер блока, количество блоков в таблице индексных узлов, номер индексного узла корневой папки;

Собственно похожая разметка диска используется и в других файловых системах. Например, группа блоков в ext2/3 имеет похожую структуру, только ext2/3 работает с несколькими такими группами, а мы ограничимся одной. Такой формат считается устаревшим, и новые файловые системы отходят от него. Например, btrfs использует более интересную схему , которая дает ряд преимуществ над семейством ext. Но да вернемся к нашим баранам.

Мы определили, что суперблок и битовые карты блоков/индексных узлов занимают первые три блока файловой системы, а сколько занимает таблица индексных узлов? Вообще говоря, фиксировать эту величину не правильно, она сильно зависит от того, как файловая система будет использоваться. Например, если вы собираетесь хранить преимущественно большие файлы, то имеет смысл сделать эту таблицу поменьше, т. к. маловероятно, что вы исчерпаете индексные узлы раньше, чем пространство на диске. С другой стороны, если вы собираетесь хранить много мелких файлов, есть вероятность исчерпать индексные узлы раньше, чем свободное место на диске, если таблица будет слишком маленькой.

В утилите mke2fs, которая используется для форматирования диска под файловые системы ext2/3, есть ключик -i , который указывает на какой объем диска нужно создавать индексный узел, т. е. если указать -i 16384, то на каждые 16 килобайт дискового пространства будет создано по индексному узлу. Я воспользуюсь самым простым вариантом — буду создавать индексный узел на каждые 16 Кб дискового пространства, без возможности изменения этого значения (пока по крайней мере).

Последний общий момент, который стоит затронуть — размер блока. Файловая система может работать с блоками разного размера, я буду поддерживать блоки в 512, 1024, 2048 и 4096 байт — ничего необычного. Это связано с тем, что с блоками, влезающими в страницу работать проще (мы вернемся к этому чуть позже), но делать так совсем не обязательно, более того, большие размеры блоков могут способствовать большей производительности.

Вообще подбор правильного размера блока для классических файловых систем — довольно занятная тема. Например, в известной книге по ОС приводятся информация о том, что при размере блока в 4 Кб 60 — 70% файлов будут помещаться в один блок. Чем больше файлов помещается в один блок, тем меньше фрагментация, выше скорость чтения, но и больше места используется впустую. В нашем случае, ко всему прочему, размер блока является главным ограничителем файловой системы — при размере блока в 4 Кб битовая карта свободных блоков может покрыть всего 128 Мб дискового пространства.

Назад к практике

Настало время написать немного кода. Начнем со структур, которые мы будем использовать, начиная с самой простой:

struct aufs_disk_super_block < __be32 dsb_magic; __be32 dsb_block_size; __be32 dsb_root_inode; __be32 dsb_inode_blocks; >; 

Структура супер блока хранится в самом начале 0 блока диска. Начинается она с магического числа, по нему мы можем убедиться, что на диске хранится именно aufs (я упоминал об этом в предыдущий раз).

Учитывая, что мы будем поддерживать разные размеры блоков (что, впрочем, не потребует от нас никаких усилий), мы должны знать какой размер используется в данной конкретной файловой системе, и суперблок хранит для нас эту информацию в поле dsb_block_size.

Кроме того, в поле dsb_inode_blocks хранится количество блоков в таблице индексных узлов — я уже упоминал, что размер этой таблицы может быть различным.

Поле dsb_root_inode хранит номер индексного узла корневого каталога. Конечно, хранить его не обязательно, можно просто использовать фиксированный номер для корня, но тогда структура будет совсем пустой, а с ним она кажется солиднее.

Обратите внимание, что для полей используются типы фиксированного размера. Это потому что мы будем записывать эту структуру на диск «как есть». Однако зафиксировать размер недостаточно, нужно зафиксировать еще порядок байт. Я буду использовать big endian , он же сетевой порядок байт, о чем говорит название типа (__be32).

В принципе, не особо важно какой именно порядок использовать, главное чтобы он был зафиксирован. Хотя есть мнение, что платформ использующих little endian больше, и поэтому использовать его предпочтительнее, но да вернемся к делу.

Тип __be32, по факту, является синонимом uint32_t, но его название подчеркивает, что переменная хранит данные в big endian (этакий способ документации). В ядре есть аналогичный тип и для little endian.

Теперь посмотрим, пожалуй, на самую главную структуру файловой системы — индексный узел:

struct aufs_disk_inode < __be32 di_first; __be32 di_blocks; __be32 di_size; __be32 di_gid; __be32 di_uid; __be32 di_mode; __be64 di_ctime; >; 

Индексный узел, в первую очередь, определяет где на диске хранится файл/каталог. Способы хранения файлов могут быть самыми разнообразными, я буду использовать довольно простой — один экстент на один файл. Экстент — непрерывная последовательность блоков диска, т. е. каждый файл/каталог будет храниться в непрерывной последовательности блоков. Поля di_first и di_blocks хранят первый блок и количество блоков в экстенте соответственно.

Тут вы скажете, а как же в такой файловой системе дописывать данные в конец файла и добавлять записи в каталог? Действительно, полноценная реализации операций, приводящих к изменению размера файла/каталога, при таком способе хранения — это головная боль (и это не говоря об эффективности такой реализации), поэтому мы не будем делать полноценную реализации записи, но об этом в другой статье.

Однако у такой организации есть и положительные моменты — файлы не фрагментированы, а это хорошо влияет на скорость последовательного чтения. Поэтому такая структура может замечательно использоваться в файловых системах рассчитанных только на чтение, например, в iso 9660 (хотя и она уже поддерживает фрагментацию файлов).

Понятно, что экстенты — не хитрая структура и мало чего дает, но вместе с классическими древовидными структурами для хранения файлов на диске, они оказываются довольно хорошим вариантом и для файловых систем с фрагментацией.

Кроме указания на место на диске, индексный узел так же хранит реальный размер файла в поле di_size. И правда, не обязан же размер файла точно попадать в размер блока.

Поля di_gid и di_uid — идентификаторы группы и пользователя. Сохранять такую информацию в файловой системе не всегда имеет смысл, я вставил их для примера.

Поле di_mode — хранит права доступа для группы владельца файла, владельца файла и всех остальных пользователей. Раз уж я сохранил группу и владельца то и права доступа стоит сохранить. Еще di_mode хранит тип объекта, который описывает индексный узел, например, является ли объект каталогом или файлом.

Наконец поле di_ctime хранит дату создания файла. Обычно файловые системы хранят вместе с датой создания еще и даты последней модификации и доступа к файлу, но мы забьем на них.

Форматируем диск

Итак, когда мы определили формат хранения файловой системы на диске, пора написать утилиту, которая приведет диск в правильный формат. Диск в Linux — это просто файл (тут уместно вспомнить известное дизайнерское решение Unix ). Так что форматирование диска — просто запись в файл нужных данных. А нужные данные в нашем случае — суперблок, битовые карты, и корневой каталог (пока пустой).

Чтобы не превращать статью о ядре Linux в статью о C++ (особенно в свете отношения Линуса к последнему ) я предлагаю вам самостоятельно разобраться с исходниками на github, но кратко пройдусь по основным классам:

  • Configuration — класс который хранит конфигурацию будущей файловой системы (размер блока, размер таблицы индексных узлов, количество блоков, имя файла устройства).
  • Block — представляет один блок диска. Данные пишутся и читаются с диска блоками.
  • BlocksCache — предоставляет доступ к блокам.
  • Inode — обертка над индексным узлом. Она скрывает преобразования порядка байт, запись и чтение данных индексного узла из блока.
  • SuperBlock — обертка на суперблоком. Как и в случае с Inode скрывает запись и чтение из блока, заполняет битовые карты, выделяет индексные узлы и блоки, т. е. по факту выполняет форматирование.

Утилита позволяет изменять размер блока через ключи -s или —block_size, и количество блоков, которые будут использованы под файловую систему через ключи -b или —blocks.

В целом утилита совсем не хитрая, но многие могут справедливо подумать, что код слишком навороченный для такой простой задачи. Дело в том, что в первую очередь мы научим файловую систему читать с диска, а потом уже возьмемся за запись. Но, чтобы проверить работу нашего драйвера, нужно записать что-то на диск. Поэтому мы в последствии добавим в утилиту возможность импортировать файлы/каталоги при форматировании диска, что очень нам поможет.

Назад к файловой системе

Теперь вернемся к нашему загружаемому модулю. Начнем мы с чтения суперблока с диска. Но перед этим заведем еще одну структуру для суперблока:

struct aufs_super_block < uint32_t asb_magic; uint32_t asb_inode_blocks; uint32_t asb_block_size; uint32_t asb_root_inode; uint32_t asb_inodes_in_block; >; 

Эта структура будет представлять суперблок в памяти. Идея простая — читаем с диска aufs_disk_super_block и преобразуем его в aufs_super_block выполняя преобразования порядка байт и попутно вычисляя всякие полезные данные (в данном случае asb_inodes_in_block). Вообще эта структура — отличное место для всяких глобальных переменных файловой системы.

Вспоминая прошлый пост, мы имеем уже три структуры для представления суперблока:

  • super_block — структура, которую предоставляет ядро;
  • aufs_disk_super_block — структура, которая хранится на диске;
  • aufs_super_block — еще одна структура, которая будет хранится в памяти;

Две структуры — это понятно, но зачем нам третья? Дело в том, что Linux не знает ничего о нашей файловой системе, поэтому вполне вероятно, что super_block (как и inode, как и любая другая структура ядра Linux) не содержит всех нужных нам полей. Поэтому мы вынуждены заводить свои дополнительные структуры и связывать их со структурами ядра. Как же их связать?

В ядре есть два распространенных способа организации такой связи (назовем их композицией и наследованием). Для супер блока мы воспользуемся композицией. Этот способ требует поддержки со стороны ядра — внутри структуры super_block есть интересное поле:

struct super_block < . void *s_fs_info; . >; 

В это поле мы можем сохранить указатель на любые данные, собственно, туда мы сохраним указатель на aufs_super_block. И везде, где у нас есть доступ к структуре super_block мы можем получить доступ и к структуре aufs_super_block. Но да это все лирика, наша задача прочитать суперблок с диска. Для этого мы напишем пару функций:

static struct aufs_super_block *aufs_super_block_read(struct super_block *sb) < struct aufs_super_block *asb = (struct aufs_super_block *)kzalloc(sizeof(struct aufs_super_block), GFP_NOFS); struct aufs_disk_super_block *dsb = NULL; struct buffer_head *bh = NULL; if (!asb) < pr_err("aufs cannot allocate super block\n"); return NULL; >bh = sb_bread(sb, 0); if (!bh) < pr_err("cannot read 0 block\n"); goto free_memory; >dsb = (struct aufs_disk_super_block *)bh->b_data; aufs_super_block_fill(asb, dsb); brelse(bh); if (asb->asb_magic != AUFS_MAGIC) < pr_err("wrong magic number %u\n", (unsigned)asb->asb_magic); goto free_memory; > return asb; free_memory: kfree(asb); return NULL; > 

Первое, что делает эта функция — это выделяет память под структуру суперблока. Для выделения памяти в ядре есть довольно много способов, kzalloc (и kmalloc вместе с ним) — самый простой. Работает как и обычный malloc, только требует передачи дополнительного набора флагов. Отличие kzalloc от kmalloc в том, что первый заполняет выделенную память нулями (что просто сводится к передаче дополнительно флага внутрь kmalloc).

Я упомянул о флагах, зачем они? Дело в том, что разные части ядра должны удовлетворять различным гарантиям. Например, в контексте обработки сетевого пакета нельзя блокироваться, а чтобы задействовать DMA требуется выделять память в специальном регионе памяти. Так как выделение памяти используется везде, требуется механизм «настройки». В нашем случае используется флаг GFP_NOFS, который говорит, что аллокатор памяти не будет обращаться к средствам файловой системы, что логично при реализации файловой системы, хотя в данном конкретном случае и не обязательно.

Естественно в ядре не забываем проверить, что память была выделена без проблем.

Следующий принципиальный момент — вызов функции sb_bread . Вот оно чтение с диска! Функция принимает указатель на суперблок и номер блока, который нужно прочитать — совсем просто. Возвращает функция указатель на структуру buffer_head , а сами данные блока доступны через поле b_data этой структуры.

Естественно и в этом случае не забываем проверить, что чтение прошло удачно.

Далее мы просто преобразуем указатель на char к указателю на структуру aufs_disk_super_block. Функция aufs_super_block_fill заполняет структуру aufs_super_block используя aufs_disk_super_block не делая ничего необычного:

static inline void aufs_super_block_fill(struct aufs_super_block *asb, struct aufs_disk_super_block const *dsb) < asb->asb_magic = be32_to_cpu(dsb->dsb_magic); asb->asb_inode_blocks = be32_to_cpu(dsb->dsb_inode_blocks); asb->asb_block_size = be32_to_cpu(dsb->dsb_block_size); asb->asb_root_inode = be32_to_cpu(dsb->dsb_root_inode); asb->asb_inodes_in_block = asb->asb_block_size / sizeof(struct aufs_disk_inode); > 

Как не трудно догадаться функция be32_to_cpu преобразует число из big endian в порядок байт используемый платформой.

После того, как мы закончили работу с блоком его нужно освободить, для этого существует функция brelse . Она на самом деле просто уменьшает счетчик ссылок на этот блок. Блок не будет освобожден сразу, как только счетчик ссылок дойдет до 0 — для блоков в ядре работает сборщик мусора, который без серьезной необходимости не будет освобождать блок. Причина в том, что чтение блоков с диска — довольно дорогая операция, поэтому разумно поддерживать кеш прочитанных блоков, и при повторном чтении того же блока возвращать уже прочитанный (если, конечно, он еще присутствует в кеше).

Последнее что мы делаем — проверяем магическое число, нужно же убедиться, что на диске хранится действительно aufs.

Для обративших внимание на goto, в ядре goto используется довольно часто. В основном, для организации обработки ошибок — в языке C нет исключений, а идея разделения основного пути выполнения и обработки ошибок довольно привлекательна, тут то нам и приходит на выручку goto. В данном случае использование goto почти ничего не дает — я вставил его сюда намерено, как пример того, зачем он используется. Вообще среди разработчиков ядра ненавистников goto не так уж и много, так что есть места в коде злоупотребляющие злосчастным оператором — стоит быть к этому готовым.

Внимательный читатель, наверно, обратил внимание на одну нестыковку. Как я уже говорил, файловые системы могут работать с разными размерами блока, и хранится эта информация скорее всего в суперблоке. Так какого же размера блок прочитает функция sb_bread при чтении суперблока? В нашем случае все просто, по-умолчанию, размер блока устанавливается в размер блока блочного устройства (как много блоков. ). И мы надеемся, что его размера достаточно для структуры суперблока — в нашем случае так и есть.

Мы написали функцию для чтения суперблока, вызывать ее мы будем из aufs_fill_super (см. предыдущий пост), теперь она выглядит так:

static int aufs_fill_sb(struct super_block *sb, void *data, int silent) < struct inode *root = NULL; struct aufs_super_block *asb = aufs_super_block_read(sb); if (!asb) return -EINVAL; sb->s_magic = asb->asb_magic; sb->s_fs_info = asb; sb->s_op = &aufs_super_ops; if (sb_set_blocksize(sb, asb->asb_block_size) == 0) < pr_err("device does not support block size %u\n", (unsigned)asb->asb_block_size); return -EINVAL; > root = aufs_inode_get(sb, asb->asb_root_inode); if (IS_ERR(root)) return PTR_ERR(root); sb->s_root = d_make_root(root); if (!sb->s_root) < pr_err("aufs cannot create root\n"); return -ENOMEM; >return 0; > 

Как я уже упоминал, мы сохраняем указатель на aufs_super_block в поле s_fs_info. Кроме того мы устанавливаем правильный размер блока вызовом sb_set_blocksize . Как говорит комментарий внутри функции размер блока должен быть от 512 байт до размера страницы — этим и обусловлен наш выбор размеров блока. Если файловая система должна работать с большим размером блока — потребуются дополнительные усилия (впрочем не такие большие).

Итак мы выделили aufs_super_block в динамической памяти, а значит мы и должны его освободить. Для этого нам нужно внести некоторые изменения в другую функцию из прошлого поста:

static void aufs_put_super(struct super_block *sb) < struct aufs_super_block *asb = (struct aufs_super_block *)sb->s_fs_info; if (asb) kfree(asb); sb->s_fs_info = NULL; pr_debug("aufs super block destroyed\n"); > 

Не трудно догадаться что парной к функции kmalloc является функция kfree , точнее даже функции, так как есть несколько реализаций kfree в ядре (еще тут и тут ), но не будем углубляться в детали.

Еще одно важное изменение внутри функции aufs_fill_sb — вызов aufs_inode_get. В прошлой статье мы создавали фиктивный inode, теперь мы научимся читать их с диска.

Но перед этим обращу ваше внимание на интересный момент — пару IS_ERR и PTR_ERR . Это простые преобразования указателей к числу и обратно, основанные на том, что ядро владеет полной информацией о расположении своей памяти и, соответственно, о том какие биты указателя можно использовать не по прямому назначению. Это самый простой пример использования знания о структуре указателя, есть и более интересные, причем не только в ядре .

Работу с индексными узлами мы начнем с расширения структуры super_operations , с которой мы познакомились в прошлый раз. Теперь мы заполним ее таким образом:

static struct super_operations const aufs_super_ops = < .alloc_inode = aufs_inode_alloc, .destroy_inode = aufs_inode_free, .put_super = aufs_put_super, >; 

Мы добавили в нее еще пару указателей на функции aufs_inode_alloc и aufs_inode_free. Это специфичные функции для аллокации и освобождения inode, тут то мы и столкнемся с SLAB (с этим зверем мы, на самом, деле уже столкнулись в виде kmalloc) и RCU (совсем чуть-чуть).

Итак выделение памяти для индексного узла начнем с определения еще одной структуры — представления индексного узла в памяти (как это было с суперблоком):

struct aufs_inode < struct inode ai_inode; uint32_t ai_block; >; 

В этот раз мы будем использовать «наследование» вместо композиции. Наследование в C выглядит совсем не хитро (что не удивительно, учитывая, что в C нет поддержки наследования). Для этого мы просто делаем первым полем структуры aufs_inode базовую структуру (базовый класс) — структуру inode . Таким образом указатель на aufs_inode можно использовать в качестве указателя на inode, как впрочем и наоборот (если конечно мы точно знаем, что данный указатель ссылается именно на aufs_inode).

По сравнению с композицией, «наследование» само по себе не требует поддержки со стороны ядра, кроме того оно выгоднее с точки зрения числа выделений памяти — на каждый индексный узел требуется одно выделение, вместо двух (как это было с суперблоком). Так же в отличие от суперблока, почти все нужные поля уже присутствуют внутри inode. Однако это скорее исключение, чем правило, ведь наша файловая система хранит данные на диске очень просто.

Для выделения памяти под индексные узлы мы будем использовать SLAB аллокатор. SLAB аллокатор — кэширующий аллокатор, который позволяет выделять блоки памяти одного размера. Не трудно догадаться, что за счет этого ограничения можно упростить управление памятью и ускорить выделение памяти. SLAB аллокатор запрашивает у ОС большие куски памяти и выделяет из них небольшие участки по запросу, соответственно, запросы к менеджеру памяти ОС происходят реже, а запросы пользователей удовлетворяются быстрее.

Но изначально выигрыш в скорости выделения памяти при использовании SLAB-ов был не только (и не столько) за счет более простого управления памяти, а за счет сокращения расходов на инициализацию этой памяти. Действительно, SLAB аллокатор зачастую используется не просто для выделения объектов одного размера, а для выделения объектов одного типа, что позволяет пропустить инициализацию некоторых полей при повторном выделении одного участка памяти. Например, мьютексы, спинлоки и другие подобные объекты при освобождении объекта скорее всего имеют «правильное» значение, и при повторном выделении не нуждаются в повторной инициализации. За деталями и результатами измерений прошу обратиться к оригинальной статье .

На данный момент в Linux имеется три различных вида SLAB аллокаторов — SLAB, SLUB и SLOB. Не будем вдаваться в различия между ними, интерфейс они предоставляют один и тот же. Итак, для создания SLAB аллокатора мы будем использовать следующую функцию:

int aufs_inode_cache_create(void)

При создании SLAB-а функции kmem_cache_create передаются имя, размер объекта, функция инициализации (функция, которая будет вызвана только один раз, при первом выделении объекта), и еще пара параметров, в суть которых я вдаваться не буду. Но чтобы не оставлять интересующихся совсем без информации я скажу, что создание SLAB-а для индексных узлов во всех файловых системах выглядит одинаково — различия не существенны.

Вызывать функцию aufs_inode_cache_create мы будем при загрузке модуля, перед регистрацией файловой системы в ядре. Есть так же парная функция, которую мы вызовем при выгрузке модуля:

void aufs_inode_cache_destroy(void)

Функция kmem_cache_destroy уничтожает SLAB аллокатор. К моменту уничтожения все объекты из этого кеша должны быть освобождены, иначе мы получим неприятное сообщение об ошибке в системном логе, вместе с целым букетом трудно отлавливаемых неприятностей.

Теперь обещанное касание RCU. В двух словах, RCU — распространенный в ядре механизм синхронизации (а также безопасного освобождения памяти для lock-free алгоритмов). RCU сам по себе заслуживает отдельной статьи и на хабре есть такая . Более того, создатель этой техники, а по совместительству, мейнтейнер RCU в ядре Linux написал целую книгу , в которой коснулся и своего детища тоже.

Но нам из всего зоопарка RCU функций необходимо разобраться только с rcu_barrier (как и с kfree, есть и другая реализация этой функции тут ). Если по-простому, то эта функция дождется, пока все отложенные работы над защищенными RCU данными завершатся, после чего вернет управление тому, кто ее вызвал, соответственно функция блокирующая. Зачем нам это нужно мы увидим чуть ниже.

Вернемся к выделению памяти, рассмотрим уже упомянутую выше функцию:

struct inode *aufs_inode_alloc(struct super_block *sb) < struct aufs_inode *const i = (struct aufs_inode *) kmem_cache_alloc(aufs_inode_cache, GFP_KERNEL); if (!i) return NULL; return &i->ai_inode; > 

Она использует созданный ранее SLAB аллокатор (через одну из реализаций kmem_cache_alloc ) и возвращает указатель на inode — ничего необычного, а вот функция освобождения немного более интересная:

void aufs_inode_free(struct inode *inode) < call_rcu(&inode->i_rcu, aufs_free_callback); > 

Тут мы опять сталкиваемся с RCU. Тут стоит сказать пару слов о lock-free алгоритмах, проблема таких алгоритмов в том, что без блокировок нет гарантий, что объект не используется параллельно каким-либо другим потоком исполнения, а значит освобождать память занятую этим объектом нельзя — другой поток может хранить указатель на него. Поэтому в lock-free алгоритмах приходится задумываться о стратегиях безопасного освобождения памяти, а RCU предоставляет средства для решения этой проблемы. Все реализации функции call_rcu откладывают выполнение некоторой функции (в нашем случае функции освобождения aufs_free_callback) до тех пор, пока это не станет безопасным. А уже упомянутая выше rcu_barrier ждет завершения всех отложенных функций.

Вы устали? Ничего страшного, мы уже приближаемся к финалу. Теперь мы будем читать индексный узел с диска. Для этого я написал уже упомянутую функцию aufs_inode_get:

struct inode *aufs_inode_get(struct super_block *sb, uint32_t no) < struct aufs_super_block const *const asb = AUFS_SB(sb); struct buffer_head *bh = NULL; struct aufs_disk_inode *di = NULL; struct aufs_inode *ai = NULL; struct inode *inode = NULL; uint32_t block = 0, offset = 0; inode = iget_locked(sb, no); if (!inode) return ERR_PTR(-ENOMEM); if (!(inode->i_state & I_NEW)) return inode; ai = AUFS_INODE(inode); block = aufs_inode_block(asb, no); offset = aufs_inode_offset(asb, no); pr_debug("aufs reads inode %u from %u block with offset %u\n", (unsigned)no, (unsigned)block, (unsigned)offset); bh = sb_bread(sb, block); if (!bh) < pr_err("cannot read block %u\n", (unsigned)block); goto read_error; >di = (struct aufs_disk_inode *)(bh->b_data + offset); aufs_inode_fill(ai, di); brelse(bh); unlock_new_inode(inode); return inode; read_error: pr_err("aufs cannot read inode %u\n", (unsigned)no); iget_failed(inode); return ERR_PTR(-EIO); > 

Объяснение я начну с простых моментов — функции AUFS_SB и AUFS_INODE позволяют получить указатель на структуры aufs_super_block и aufs_inode, через указатели на super_block и inode соответственно. Я не буду приводить их код (он довольно простой), потому что я уже описал выше как эти структуры связаны.

Функции aufs_inode_block и aufs_inode_offset позволяют получить номер блока и смещение внутри блока по номеру индексного узла — никакой магии, простая арифметика, так что останавливаться на них я тоже не буду.

А вот теперь интересный момент — пара функций iget_locked и unlock_new_inode . Как и в случае с блоками ядро поддерживает кеш inode-ов, это нужно не только для того, чтобы лишний раз не читать индексный узел с диска. Дело в том, что один и тот же файл/каталог может быть открыт сразу несколькими процессами, в этом случае все они должны оперировать одним экземпляром inode, чтобы их можно было синхронизировать друг с другом. Аналогичное рассуждение справедливо для блоков пожалуй даже в большей степени, так что к блока это тоже применимо.

Итак функция idet_locked в первую очередь ищет inode в кеше и выделяет память под новый, если inode не найден. Если индексный узел был выделен заново, а не найден в кеше, в поле i_state будет установлен флаг I_NEW, а также будет захвачен спинлок этого узла (поле i_lock). Поэтому наша функция сначала проверяет поле i_state, и если флаг I_NEW сброшен просто возвращаем кешированный inode. В противном случае мы должны заполнить inode, для этого мы читаем нужный блок с диска (с помощью уже известной вам sb_bread).

Функция aufs_inode_fill как раз и занимается заполнением:

static void aufs_inode_fill(struct aufs_inode *ai, struct aufs_disk_inode const *di) < ai->ai_block = be32_to_cpu(di->di_first); ai->ai_inode.i_mode = be32_to_cpu(di->di_mode); ai->ai_inode.i_size = be32_to_cpu(di->di_size); ai->ai_inode.i_blocks = be32_to_cpu(di->di_blocks); ai->ai_inode.i_ctime.tv_sec = be64_to_cpu(di->di_ctime); ai->ai_inode.i_mtime.tv_sec = ai->ai_inode.i_atime.tv_sec = ai->ai_inode.i_ctime.tv_sec; ai->ai_inode.i_mtime.tv_nsec = ai->ai_inode.i_atime.tv_nsec = ai->ai_inode.i_ctime.tv_nsec = 0; i_uid_write(&ai->ai_inode, (uid_t)be32_to_cpu(di->di_uid)); i_gid_write(&ai->ai_inode, (gid_t)be32_to_cpu(di->di_gid)); > 

Опять никакой магии, за исключением пары функций i_uid_write и i_gid_write . Но и они не делают ничего особенного — просто присваивают значения соответствующим полям.

Кроме того обращу внимание на представление времени в виде структуры timespec , эта структура состоит всего из пары чисел — количество секунд и наносекунд. Т. е. потенциально время можно хранить довольно точно.

Наконец в самом конце функции мы должны освободить спинлок и вернуть указатель, для этого и используется функция unlock_new_inode.

Вместо заключения

Пост получился действительно большим и даже так не покрывает всех моментов. Я постарался объяснить все ключевые части реализации.

Все исходники доступны по ссылке. В репозитории теперь две папки — kern и user. Как не трудно догадаться, одна хранит код нашего модуля, вторая — код утилиты для форматирования. Кода стало больше, а значит вероятность появления в нем ошибок стала больше — замечания, исправления, любая конструктивная критика и pull request-ы приветствуются.

Чтобы получить образ диска для монтирования можно сделать так:

dd bs=1M count=100 if=/dev/zero of=image ./mkfs.aufs ./image 

Теперь можно использовать файл image так, как это показано в предыдущем посте.

Из примеров кода в этой статье убрано некоторое количество дебажного вывода, но если вы буде использовать код из репозитория, вы сможете убедиться, что модуль работает используя команду dmesg.

Архитектура файловой системы ext2

Все пространство раздела диска разбивается на блоки фиксированного размера, кратные размеру сектора – 1024, 2048 и 4096 байт. Размер блока указывается при создании файловой системы на разделе жесткого диска. Меньший размер блока позволяет экономить место на жестком диске, но также ограничивает максимальный размер файловой системы. Все блоки имеют порядковые номера. С целью уменьшения фрагментации и количества перемещений головок жесткого диска при чтении больших массивов данных блоки объединяются в группы.

Базовым понятием файловой системы является информационный узел, information node, или inode. Это специальная структура, которая содержит информацию об атрибутах и физическом расположении файла. Атрибутами файла являются его тип (обычный файл, каталог и т. д.), права доступа к нему, идентификатор владельца, размер, время создания. Информация о физическом расположении представляет собой последовательность абсолютных номеров блоков, содержащих данные файла.

Суперблок – основной элемент файловой системы ext2. Он содержит следующую информацию о файловой системе (список неполный):

  • общее число блоков и inode в файловой системе;
  • число свободных блоков и inode в файловой системе;
  • размер блока файловой системы;
  • количество блоков и inode в группе;
  • размер inode;
  • идентификатор файловой системы;
  • номер первого блока данных.

Другими словами, это номер блока, содержащего суперблок. Этот номер всегда равен 0, если размер блока файловой системы больше 1024 байт, и 1, если размер блока равен 1024 байт.

От целостности суперблока напрямую зависит работоспособность файловой системы. Операционная система создает несколько резервных копий суперблока для возможности его восстановления в случае повреждения. Главная копия находится по смещению 1024 байт от начала раздела, на котором создана файловая система (первые 1024 байта зарезервированы для загрузчика операционной системы).

Ранние версии файловой системы ext2 создавали копии суперблока в начале каждой группы блоков. Это приводило к большим потерям дискового пространства, поэтому позже количество резервных копий суперблока было уменьшено, и для их размещения были выделены группы блоков 0, 1, 3, 5 и 7.

Формат группы блоков

Обобщенная структурная схема файловой системы ext2 представлена на рис. 1.

Рисунок 1. Обобщенная структурная схема файловой системы ext2

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

Битовая карта занятости блоков обычно расположена в первом блоке группы. Если в группе присутствует резервная копия суперблока, битовая карта располагается во втором блоке группы. Размер битовой карты – один блок. Каждый бит этой карты обозначает состояние блока. Если бит установлен (1), то блок занят, если сброшен (0) – блок свободен. Первому блоку группы соответствует нулевой бит карты, второму блоку – первый бит и т. д.

Inode, находящиеся в пределах одной группы, собраны в таблицу. В битовой карте занятости inode группы каждый бит характеризует состояние элемента в таблице inode группы.

Каждая группа блоков описывается при помощи дескриптора группы блоков. Дескриптор группы – это структура, которая содержит информацию об адресах битовой карты занятости блоков, битовой карты занятости inode и таблицы inode соответствующей группы. Все дескрипторы групп собраны в таблицу дескрипторов групп, которая хранится в группе блоков 0. Так же, как и для суперблока, операционная система создает резервные копии таблицы дескрипторов групп.

Алгоритм чтения файла

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

Как было сказано выше, информация о физическом расположении файла содержится в inode. Эта информация представляет собой последовательность 32-битных номеров блоков, содержащих данные файла (рис. 1). Первые 12 номеров – это прямые ссылки на информационные блоки (direct blocks number). 13-й номер является косвенной ссылкой (indirect blocks number). В нем находится адрес блока, в котором хранятся адреса информационных блоков. 14-й номер – двойная косвенная ссылка (double blocks number), 15-й номер – тройная косвенная ссылка (triple blocks number).

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

Файлы в UNIX- и POSIX-системах хранятся в древовидной иерархической файловой системе. Корень файловой системы – это корневой каталог, обозначенный символом «/». Каждый промежуточный узел в дереве файловой системы – это каталог. Конечные вершины дерева файловой системы являются либо пустыми каталогами, либо файлами. Абсолютное путевое имя файла состоит из имен всех каталогов, ведущих к указанному файлу, начиная с корневого каталога. Так, путевое имя /home/test.file означает, что файл test.file расположен в каталоге home, который, в свою очередь, находится в корневом каталоге «/».

Каталог, так же как и файл, описывается при помощи inode. Содержимое каталога представляет собой массив записей, каждая из которых содержит информацию о файле, который находится «внутри» текущего каталога.

Запись каталога имеет следующий формат:

  • порядковый номер inode файла;
  • длина записи в байтах;
  • имя файла;
  • длина имени файла.

Поиск номера inode файла всегда начинается с корневого каталога. Например, чтобы получить порядковый номер inode файла, находящегося в корневом каталоге, операционная система должна получить содержимое корневого каталога, найти в нем запись с именем этого файла и извлечь из этой записи порядковый номер inode файла.

Несколько первых номеров inode зарезервированы файловой системой, их перечень содержится в заголовочном файле :

* Special inode numbers

#define EXT2_BAD_INO 1 /* Bad blocks inode */

#define EXT2_ROOT_IN 2 /* Root inode */

#define EXT2_ACL_IDX_IN 3 /* ACL inode */

#define EXT2_ACL_DATA_INO 4 /* ACL inode */

#define EXT2_BOOT_LOADER_INO 5 /* Boot loader inode */

#define EXT2_UNDEL_DIR_INO 6 /* Undelete directory inode */

Для записи корневого каталога зарезервирован inode под номером 2 (root inode). Этот inode находится в группе блоков 0 и занимает вторую позицию в таблице inode этой группы. Номер первого незарезервированного inode хранится в суперблоке.

Определив порядковый номер inode файла, ядро вычисляет номер группы, в которой этот inode расположен, и его позицию в таблице inode группы. Считав из этой позиции inode, операционная система получает полную информацию о файле, включая адреса блоков, в которых хранится содержимое файла.

Номер группы блоков, в которой расположен inode, вычисляется по формуле:

group = (inode_num — 1) / inodes_per_group

  • group – искомый номер группы блоков;
  • inode_num – порядковый номер inode, определяющего файл;
  • inodes_per_group – число inode в группе (эта информация находится в суперблоке).

Позиция inode в таблице inode группы определяется по формуле:

index = (inode_num — 1) % inodes_per_groupe

где index – позиция inode в таблице.

Рассмотрим пример получения содержимого файла test.file, находящегося в корневом каталоге. Для чтения файла /test.file необходимо:

  • в массиве записей корневого каталога найти запись об этом файле;
  • извлечь порядковый номер inode файла, вычислить номер группы, в которой этот inode расположен;
  • из дескриптора данной группы извлечь адрес таблицы inode группы;
  • вычислить позицию inode в этой таблице;
  • считать inode файла;
  • из inode извлечь адреса информационных блоков и осуществить чтение информации, находящейся в этих блоках.

На рис. 2 подробно показаны этапы чтения файла /test.file.

Рисунок 2. Порядок выполнения процедуры чтения файла в файловой системе ext2 (на примере файла /test.file)

Этапы 1-6 – чтение корневого каталога:

Определив номер inode, можно получить доступ к информационным блокам файла (этапы 7-11):

Программная реализация алгоритма чтения файла

Исходные данные: имеется раздел жесткого диска, на котором создана файловая система ext2. Этому разделу соответствует файл устройства /dev/hda3. В корневом каталоге раздела создан подкаталог home, а в нем находится файл test.file следующего содержания:

В чащах юга жил бы цитрус?

Да, но фальшивый экземпляр!

Не подумайте плохого, это не бред, а тестовое упражнение из курса подготовки телеграфистов в войсках связи бывшего СССР!

Внимание! Следует учесть один важный момент. Созданный файл не будет сразу записан на диск, а сначала попадет в дисковый буфер. Попытка сразу же получить содержимое файла по вышеприведенному алгоритму ни к чему не приведет, так как информация об этом файле физически на диске отсутствует. Необходимо «заставить» систему записать дисковый буфер на диск. Самый простой способ сделать это – выполнить операцию перезагрузки. Поэтому после того, как файл создан, перезагрузите систему.

Наша задача – используя файл устройства /dev/hda3, осуществить чтение файла /home/test.file методом прямого доступа к его информационным блокам.

Рассмотрим программную реализацию модуля, выполняющего эту операцию.

В заголовочном файле определены структурные типы, описывающие основные компоненты файловой системы ext2 – суперблок, дескриптор группы блоков, информационный узел, запись каталога.

Рассмотрим кратко поля, которые входят в каждую из этих структур:

  1. Структура суперблока struct ext2_super_block:
    • __u32 s_inodes_count – общее число inode в файловой системе;
    • __u32 s_blocks_count – общее число блоков в файловой системе;
    • __u32 s_free_blocks_count – количество свободных блоков;
    • __u32 s_free_inodes_count – количество свободных inode;
    • __u32 s_first_data_block – номер первого блока данных (номер блока, в котором находится суперблок);
    • __u32 s_log_block_size – это значение используется для вычисления размера блока. Размер блока определяется по формуле: block size = 1024
    • __u32 s_blocks_per_group – количество блоков в группе;
    • __u32 s_inodes_per_group – количество inode в группе;
    • __u16 s_magic – идентификатор файловой системы ext2 (сигнатура 0xEF53);
    • __u16 s_inode_size – размер информационного узла (inode);
    • __u32 s_first_ino – номер первого незарезервированного inode.
  2. Структура дескриптора группы блоков struct ext2_group_desc:
    • __u32 bg_block_bitmap – битовая карта занятости блоков группы;
    • __u32 bg_inode_bitmap – битовая карта занятости inode группы;
    • __u32 bg_inode_table – адрес таблицы inode группы.
  3. Структура информационного узла struct ext2_inode:
    • __u16 i_mode – тип файла и права доступа к нему. Тип файла определяют биты 12-15 этого поля:
      • 0xA000 – символическая ссылка;
      • 0x8000 – обычный файл;
      • 0x6000 – файл блочного устройства;
      • 0x4000 – каталог;
      • 0x2000 – файл символьного устройства;
      • 0x1000 – канал FIFO.
    • __u32 i_size – размер в байтах;
    • __u32 i_atime – время последнего доступа к файлу;
    • __u32 i_ctime – время создания файла;
    • __u32 i_mtime – время последней модификации;
    • __u32 i_blocks – количество блоков, занимаемых файлом;
    • __u32 i_block[EXT2_N_BLOCKS] – адреса информационных блоков (включая все косвенные ссылки).

Значение EXT2_N_BLOCKS определено в файле :

* Constants relative to the data blocks

#define EXT2_NDIR_BLOCKS 12

#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS

#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)

#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)

#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)

#define EXT2_NAME_LEN 255

  • __u32 inode – номер inode файла;
  • __u16 rec_len – длина записи каталога;
  • __u8 name_len – длина имени файла;
  • char name[EXT2_NAME_LEN] – имя файла .

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

#define PART_NAME «/dev/hda3»

struct ext2_super_block sb;

/* буфер для хранения таблицы дескрипторов групп */

unsigned char buff_grp[4096];

unsigned char buff[4096]; /* информационный буфер */

int indev; /* дескриптор файла устройства */

int BLKSIZE; /* размер блока файловой системы */

Определим несколько функций, которые нам понадобятся для работы:

Функция чтения суперблока:

Смещаемся на 1024 байта от начала раздела и считываем суперблок в структуру struct ext2_super_block sb:

Проверяем идентификатор файловой системы:

printf(«Неизвестный тип файловой системы! «);

Значение EXT2_SUPER_MAGIC определено в заголовочном файле .

Отображаем информацию о файловой системе, которая находится в суперблоке:

printf(» Superblock info ———— «);

printf(«Inodes count — %u «,sb.s_inodes_count);

printf(«Blocks count — %u «,sb.s_blocks_count);

printf(«First inode — %d «,sb.s_first_ino);

printf(«Magic — 0x%X «,sb.s_magic);

printf(«Inode size — %d «,sb.s_inode_size);

printf(«Inodes per group — %u «,sb.s_inodes_per_group);

printf(«Blosks per group — %u «,sb.s_blocks_per_group);

printf(«First data block — %u «,sb.s_first_data_block);

Функция чтения таблицы дескрипторов групп:

Вычисляем размер блока файловой системы:

Таблица дескрипторов групп находится в блоке, который расположен сразу же за первым блоком данных (за суперблоком).

if(lseek(indev, (sb.s_first_data_block + 1) * BLKSIZE, 0) < 0)

Функция получения содержимого inode по его номеру:

void get_inode(int inode_num, struct ext2_inode *in)

Входные параметры функции – порядковый номер inode и структура struct ext2_inode.

struct ext2_group_desc gd;

__u64 group, index, pos;

Вычисляем номер группы блоков, в которой находится inode с порядковым номером inode_num:

group = (inode_num — 1) / sb.s_inodes_per_group;

Из таблицы дескрипторов групп извлекаем дескриптор группы group и копируем его в структуру struct ext2_group_desc gd:

Вычисляем позицию inode c порядковым номером inode_num в таблице inode группы group и считываем этот inode в структуру struct ext2_inode:

index = (inode_num — 1) % sb.s_inodes_per_group;

pos = ((__u64)gd.bg_inode_table) * BLKSIZE + (index * sb.s_inode_size);

pread64(indev, in, sb.s_inode_size, pos);

Функция чтения блока данных:

void read_iblock(struct ext2_inode *in, int blk_num)

Входные параметры функции – структура inode и номер блока (имеется в виду номер из последовательности адресных блоков, расположенных в inode).

Вычисляем смещение к информационному блоку на разделе и считываем этот блок в глобальный буфер buff:

pos = ((__u64)in->i_block[blk_num]) * BLKSIZE;

pread64(indev, buff, BLKSIZE, pos);

Функция получения содержимого корневого каталога:

struct ext2_inode in;

Порядковый номер inode корневого каталога известен, поэтому получаем содержимое inode корневого каталога и считываем в буфер buff его содержимое:

В буфере buff будет находиться содержимое корневого каталога.

Функция получения номера inode по имени файла:

int get_i_num(char *name)

Входные параметры функции – имя файла. Возвращаемое значение – порядковый номер inode файла.

int i = 0, rec_len = 0;

struct ext2_dir_entry_2 dent;

В буфере buff находится массив записей каталога. Для определения порядкового номера inode файла необходимо найти в этом массиве запись с именем этого файла:

if(!memcmp(dent.name, name, dent.name_len)) break;

А теперь распишем главную функцию:

Переменные и структуры:

struct ext2_inode in;

// абсолютное путевое имя файла

unsigned char *full_path = «/home/test.file»;

unsigned char buff1[EXT2_NAME_LEN];

static int i = 1;

int n, i_num, outf, type;

Первым символом в абсолютном путевом имени файла должен быть прямой слэш (/). Проверяем это:

Открываем файл устройства, считываем суперблок и таблицу дескрипторов групп:

Получаем содержимое корневого каталога:

Сейчас в буфере buff находятся все записи корневого каталога (если хотите, можете сохранить их в отдельном файле). Теперь, имея записи корневого каталога, мы можем добраться до содержимого файла test.file, используя вышеприведенный алгоритм чтения файла. С этой целью организуем цикл. В теле цикла проведем разбор абсолютного путевого имени файла, выделяя его элементы – подкаталоги (он у нас один, home) и имя искомого файла (test.file). Для каждого элемента определим порядковый номер inode, считаем этот inode и затем получим содержимое нулевого блока (из последовательности адресных блоков, находящихся в inode):

Для каждого элемента абсолютного путевого имени файла определяем порядковый номер inode, считываем этот inode в память и затем получаем содержимое нулевого блока:

Отобразим информацию о файле (имя, порядковый номер inode, размер файла и его тип):

printf(«Inode number — %u «, i_num);

printf(«File name — %s «, buff1);

printf(«File size — %u «,in.i_size);

Тип файла определяют старшие четыре бита поля i_mode структуры struct ext2_inode:

printf(«Type — %d «,type);

printf(«(файл блочного устройства) «);

printf(«(файл символьного устройства) «);

Проверяем тип файла. Если это обычный файл – прерываем цикл:

В буфере buff будет находиться информация, считанная из информационных блоков файла /home/test.file. Запишем эту информацию в файл:

write(outf, buff, sizeof(buff));

На этом рассмотрение логической структуры файловой системы ext2 завершим.

  1. http://www.nongnu.org/ext2-doc
  2. http://e2fsprogs.sourceforge.net/ext2intro.html

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Luxury Car Rental in Miami | Reserve a Premium Vehicle with RealCar