Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Руководство по созданию простой UNIX-подобной ОС

8. Файловая система VFS и initrd

Оригинал: "8. The VFS and the initrd"
Автор: James Molloy
Дата публикации: 2008
Перевод: Н.Ромоданов
Дата перевода: январь 2012 г.

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

8.1. Виртуальная файловая система

Граф нодов

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

  • Open – Вызывается, когда нод открывается как дескриптор файла.
  • Close – Вызывается, когда нод закрывается.
  • Read – Я, надеюсь, понятно, что эта функция делает!
  • Write – Смотри выше :-)
  • Readdir – Если текущий нод является директорием, то нам нужен способ перечисления его содержимого. Функция Readdir должна возвращать n-ый дочерний нод директория, либо, в противном случае, NULL. Она возвращает структуру 'struct dirent', которая совместима с функцией readdir в UNIX.
  • Finddir – Нам также нужен способ нахождения дочернего нода по имени, указанного в виде строки. Он будет использоваться для следования по абсолютным путям.

Так что тогда наша структура нода выглядит примерно так:

typedef struct fs_node
{
  char name[128];     // Имя файла.
  u32int flags;       // Включает тип нода (Директорий, файл и т.п.).
  read_type_t read;   // Эти типы данных являются просто указателями на функции. Мы их определим позже!
  write_type_t write;
  open_type_t open;
  close_type_t close;
  readdir_type_t readdir; // Возвращает n-ый дочерний нод директория.
  finddir_type_t finddir; // Пытается найди в директории дочерний нод по его имени.
} fs_node_t; 

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

typedef struct fs_node
{
   char name[128];     // Имя файла.
   u32int mask;        // Маска прав доступа.
   u32int uid;         // Пользователь, владеющий файлом.
   u32int gid;         // Группа, владеющая файлом.
   u32int flags;       // Включает тип нода.
   u32int length;      // Размер файла в байтах.
   read_type_t read;
   write_type_t write;
   open_type_t open;
   close_type_t close;
   readdir_type_t readdir;
   finddir_type_t finddir;
} fs_node_t; 

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

Наконец, нам также нужно учитывать символические ссылки (ярлыки в смысле Windows). Это всего лишь заполнители и указатели на другие файлы, поэтому необходима переменная, в которой можно хранить указатели.

typedef struct fs_node
{
   char name[128];     // Имя файла.
   u32int mask;        // Маска прав доступа.
   u32int uid;         // Пользователь, владеющий файлом.
   u32int gid;         // Группа, владеющая файлом.
   u32int flags;       // Включает тип нода. Смотрите определение #defines, приведенное выше.
   u32int inode;       // Зависит от устройства, позволяет файловой системе идентифицировать файлы.
   u32int length;      // Размер файла в байтах.
   u32int impl;        // Номер,б зависящий от реализации.
   read_type_t read;
   write_type_t write;
   open_type_t open;
   close_type_t close;
   readdir_type_t readdir;
   finddir_type_t finddir;
   struct fs_node *ptr; // Используется для точек монтирования и символических ссылок.
} fs_node_t; 

8.1.1. Точки монтирования

devfs монтируется в директории /dev

Точки монтирования, являются способом, с помощью которого в UNIX осуществляется доступ к различным файловым системам. Файловая система монтируется в директории - любой последующий доступ в этот директорий будет, на самом деле, доступом в корневой директорий новой файловой системы. Поэтому говорят, что директорий, по существу, является точкой монтирования, из которой берется указатель на корневой нод новой файловой системы. Мы, в действительности, можем для этой цели использовать поле указателя ptr в структуре fs_node_t (поскольку оно в настоящее время используется только для символических ссылок, а они никогда не могут быть точками монтирования).

8.1.2. Реализация

8.1.2.1. Файл fs.h

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

typedef u32int (*read_type_t)(struct fs_node*,u32int,u32int,u8int*);
typedef u32int (*write_type_t)(struct fs_node*,u32int,u32int,u8int*);
typedef void (*open_type_t)(struct fs_node*);
typedef void (*close_type_t)(struct fs_node*);
typedef struct dirent * (*readdir_type_t)(struct fs_node*,u32int);
typedef struct fs_node * (*finddir_type_t)(struct fs_node*,char *name); 

struct dirent // One of these is returned by the readdir call, according to POSIX.
{
  char name[128]; // Имя файла..
  u32int ino;     // Номер inode. Требеся для POSIX.
}; 

Нам также нужно определить, что будут означать значения в поле fs_node_t::flags:

#define FS_CHARDEVICE  0x03
#define FS_BLOCKDEVICE 0x04
#define FS_PIPE        0x05
#define FS_SYMLINK     0x06
#define FS_MOUNTPOINT  0x08 // Файл является активной точкой монтирования?

Обратите внимание, что для FS_MOUNTPOINT присвоено значение 8, а не 7. Это объясняется тем, что оно может быть объединено с помощью побитового OR (ИЛИ) со значением FS_DIRECTORY. Для других флагов даны последовательные значения, поскольку они являются взаимоисключающими.

Наконец, мы должны определить корневой нод файловой системы и наши функции чтения, записи и т.д.

extern fs_node_t *fs_root; // The root of the filesystem.

// Стандартные функции чтения, записи, открытия, закрытия. Обратите внимание,
//  что у них всех используется суффикс _fs с тем, чтобы отличать от функций чтения, 
// записи, открытия и закрытия дескрипторов файлов, а не нодов файлов. 
u32int read_fs(fs_node_t *node, u32int offset, u32int size, u8int *buffer);
u32int write_fs(fs_node_t *node, u32int offset, u32int size, u8int *buffer);
void open_fs(fs_node_t *node, u8int read, u8int write);
void close_fs(fs_node_t *node);
struct dirent *readdir_fs(fs_node_t *node, u32int index);
fs_node_t *finddir_fs(fs_node_t *node, char *name); 
8.1.2.2.Файл fs.c
// fs.c – Определяет интерфейс и структуры, связанные с виртуальной файловой системой.
// Написано для  руководств по разработке ядра - автор James Molloy

#include "fs.h"

fs_node_t *fs_root = 0; // The root of the filesystem.

u32int read_fs(fs_node_t *node, u32int offset, u32int size, u8int *buffer)
{
  // Получил ли нод обратный вызов функции чтения?
  if (node->read != 0)
    return node->read(node, offset, size, buffer);
  else
    return 0;
}

Приведенный выше код должен понятен. Если в ноде нет набора обратных вызовов, то просто возвращается значение ошибки. Вы должны повторить приведенный выше код для функций open(), close() и write(). То же самое относится и к функциям readdir() и finddir(), хотя в них должна быть дополнительная проверка: является ли нод фактически директорием!

if ((node->flags&0x7) == FS_DIRECTORY && node->readdir != 0 ) 

Верьте или нет, это весь код, который нужен для того, чтобы создать простую виртуальную файловую систему! С помощью этого кода, используемого в качестве базового, мы можем сделать наш диск initial ramdisk и, возможно, позже более сложные файловые системы, такие как FAT или ext2.

8.2. Диск initial ramdisk

Диск initial ramdisk является просто файловой системой, которая загружается в память при загрузке ядра. Он используется для хранения драйверов и конфигурационных файлов, которые необходимы раньше, чем ядро может получить доступ к корневой файловой системе (на самом деле, на нем, как правило, находятся драйверы для доступа к этой корневой файловой системе!).

На диске initrd, как его называют, как правило, используется файловая система с проприетарным форматом. Причина этого в том, что для нее не нужны такие сложные возможности обработки файловой системы, как удаление файлов и перераспределение пространства. Ядро должно попытаться как можно быстрее получить корневую файловую систему и ее запустить — зачем вам потребуется удалять файлы с initrd??

Вам также может потребоваться отформатировать файловую систему! Я сделал это за вас на тот случай, если вы не чувствуете себя достаточно уверенно ;)

8.3. Мое собственное решение

В моем формате не поддерживаются поддиректории. В нем хранится число файлов в системе в виде первых 4 байтов файла initrd. Далее следует определенное количество (64) заголовочных структур, назначение имен, смещения и размеры имеющихся файлов. Далее идут файлы с фактическими данными. Я написал небольшую программу на С, которая делает все это за меня: она для каждого файла, который добавляется, получает два аргумента: путь к файлу из текущего директория и имя, которое будет дано файлу в созданном файловой системы.

8.3.1. Генератор файловой системы

#include <stdio.h>

struct initrd_header
{
   unsigned char magic; // Здесь указывается магическое число, которое используется для проверки непротиворечивости.
   char name[64];
   unsigned int offset; // Смещение в initrd, указывающее откуда начинается файл.
   unsigned int length; // Длина файла.
};

int main(char argc, char **argv)
{
   int nheaders = (argc-1)/2;
   struct initrd_header headers[64];
   printf("size of header: %d\n", sizeof(struct initrd_header));
   unsigned int off = sizeof(struct initrd_header) * 64 + sizeof(int);
   int i;
   for(i = 0; i < nheaders; i++)
   {
       printf("writing file %s->%s at 0x%x\n", argv[i*2+1], argv[i*2+2], off);
       strcpy(headers[i].name, argv[i*2+2]);
       headers[i].offset = off;
       FILE *stream = fopen(argv[i*2+1], "r");
       if(stream == 0)
       {
         printf("Error: file not found: %s\n", argv[i*2+1]);
         return 1;
       }
       fseek(stream, 0, SEEK_END);
       headers[i].length = ftell(stream);
       off += headers[i].length;
       fclose(stream);
       headers[i].magic = 0xBF;
   }
   
   FILE *wstream = fopen("./initrd.img", "w");
   unsigned char *data = (unsigned char *)malloc(off);
   fwrite(&nheaders, sizeof(int), 1, wstream);
   fwrite(headers, sizeof(struct initrd_header), 64, wstream);
   
   for(i = 0; i < nheaders; i++)
   {
     FILE *stream = fopen(argv[i*2+1], "r");
     unsigned char *buf = (unsigned char *)malloc(headers[i].length);
     fread(buf, 1, headers[i].length, stream);
     fwrite(buf, 1, headers[i].length, wstream);
     fclose(stream);
     free(buf);
   }
   
   fclose(wstream);
   free(data);
   
   return 0;
} 

Я не собираюсь объяснять содержимое этого файла: Это вспомогательный файл и он не столь важен. Кроме того, в любом случае вам следует создать свой собственный файл! ;)

8.3.2. Интеграция с вашей собственной ОС

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

8.3.2.1. Файл initrd.h

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

// initrd.h – Определяется интерфейс и структуры, используемые для диска initial ramdisk.
// Написано для  руководств по разработке ядра - автор James Molloy

#ifndef INITRD_H
#define INITRD_H

#include "common.h"
#include "fs.h"

typedef struct
{
   u32int nfiles; // Число файлов в ramdisk.
} initrd_header_t;

typedef struct
{
   u8int magic;     // Магическое число для проверки ошибок.
   s8int name[64];  // Имя файла.
   u32int offset;   // Смещение в initrd, указывающее откуда начинается файл.
   u32int length;   // Длина файла.
} initrd_file_header_t;

// Инициализация initial ramdisk. Ему передается адрес модуля module,
// а возвращается нод созданной файловой системы.
fs_node_t *initialise_initrd(u32int location);

#endif
8.3.2.2. Файл initrd.c

Первое, что нам нужно, это некоторые статические объявления:

// initrd.c -- Определяется интерфейс и структуры, используемые для диска initial ramdisk.
// Написано для  руководств по разработке ядра - автор James Molloy

#include "initrd.h"

initrd_header_t *initrd_header;     // Заголовок.
initrd_file_header_t *file_headers; // Список заголовков файлов.
fs_node_t *initrd_root;             // Нод нашего корневого директория.
fs_node_t *initrd_dev;              // Мы также добавляем нод директория для /dev, так что мы 
                                                //сможем смонтировать в нем файловую систему devfs.
fs_node_t *root_nodes;              // Список нодов файлов.
int nroot_nodes;                    // Количество нодов файлов.

struct dirent dirent; 

Следующее, что нам нужно, это функция для чтения файла в нашем initrd.

static u32int initrd_read(fs_node_t *node, u32int offset, u32int size, u8int *buffer)
{
   initrd_file_header_t header = file_headers[node->inode];
   if (offset > header.length)
       return 0;
   if (offset+size > header.length)
       size = header.length-offset;
   memcpy(buffer, (u8int*) (header.offset+offset), size);
   return size;
} 

В этой функции продемонстрирована одна очень раздражающая вещь, касающаяся написания кода низкого уровня: 80% его составляют проверки ошибок. К сожалению, вы не можете избавиться от этого - если вы уберете его, вам может потребоваться без преувеличения несколько дней с тем, чтобы понять, почему код не работает.

Было бы также весьма полезно иметь несколько рабочих функций readdir и finddir:

static struct dirent *initrd_readdir(fs_node_t *node, u32int index)
{
   if (node == initrd_root && index == 0)
   {
     strcpy(dirent.name, "dev");
     dirent.name[3] = 0; // Удостоверьтесь, что строка завершается символом NULL.
     dirent.ino = 0;
     return &dirent;
   }

   if (index-1 >= nroot_nodes)
       return 0;
   strcpy(dirent.name, root_nodes[index-1].name);
   dirent.name[strlen(root_nodes[index-1].name)] = 0; // Удостоверьтесь, что строка завершается символом NULL.
   dirent.ino = root_nodes[index-1].inode;
   return &dirent;
}

static fs_node_t *initrd_finddir(fs_node_t *node, char *name)
{
   if (node == initrd_root &&
       !strcmp(name, "dev") )
       return initrd_dev;

   int i;
   for (i = 0; i < nroot_nodes; i++)
       if (!strcmp(name, root_nodes[i].name))
           return &root_nodes[i];
   return 0;
} 

Не самое последнее, что нам потребуется, это инициализация файловой системы:

fs_node_t *initialise_initrd(u32int location)
{
   // Инициализирует указатели main и заголовке файлов и заполняет корневой директорий.
   initrd_header = (initrd_header_t *)location;
   file_headers = (initrd_file_header_t *) (location+sizeof(initrd_header_t)); 

Будем считать, что в ядре известно, где начинается initrd и может передать указание на это место в функцию инициализации.

// Инициализация корневого директория.
   initrd_root = (fs_node_t*)kmalloc(sizeof(fs_node_t));
   strcpy(initrd_root->name, "initrd");
   initrd_root->mask = initrd_root->uid = initrd_root->gid = initrd_root->inode = initrd_root->length = 0;
   initrd_root->flags = FS_DIRECTORY;
   initrd_root->read = 0;
   initrd_root->write = 0;
   initrd_root->open = 0;
   initrd_root->close = 0;
   initrd_root->readdir = &initrd_readdir;
   initrd_root->finddir = &initrd_finddir;
   initrd_root->ptr = 0;
   initrd_root->impl = 0; 

Здесь мы создаем нод корневого директория. Мы получаем из кучи ядра некоторый объем памяти и задаем ноду имя. В действительности нам не нужно задавать имя для этого нода, поскольку к корневому директорию обращение осуществляется просто как '/'.

Большая часть кода инициализирует указатели значениями NULL (0), но вы заметите, что в ноде указывается, что это директорий (flags = FS_DIRECTORY) и что есть обе функции readdir и finddir.

То же самое сделано для нода /dev:

// Инициализируем директрий /dev (требуется!)
   initrd_dev = (fs_node_t*)kmalloc(sizeof(fs_node_t));
   strcpy(initrd_dev->name, "dev");
   initrd_dev->mask = initrd_dev->uid = initrd_dev->gid = initrd_dev->inode = initrd_dev->length = 0;
   initrd_dev->flags = FS_DIRECTORY;
   initrd_dev->read = 0;
   initrd_dev->write = 0;
   initrd_dev->open = 0;
   initrd_dev->close = 0;
   initrd_dev->readdir = &initrd_readdir;
   initrd_dev->finddir = &initrd_finddir;
   initrd_dev->ptr = 0;
   initrd_dev->impl = 0; 

Теперь, когда все сделано, мы можем начать на самом деле добавлять файлы в ramdisk. Сначала выделим для них место:

   root_nodes = (fs_node_t*)kmalloc(sizeof(fs_node_t) * initrd_header->nfiles);
   nroot_nodes = initrd_header->nfiles; 

Затем мы создадим их:

   // Для каждого файла...
   int i;
   for (i = 0; i < initrd_header->nfiles; i++)
   {
       // Отредактируем заголовок файла — в настоящее время в нем указывается смещение файла
       // относительно ramdisk. Мы хотим, чтобы оно указывалось относительно начала 
       // памяти.
       file_headers[i].offset += location;
       // Создаем нод нового файла.
       strcpy(root_nodes[i].name, &file_headers[i].name);
       root_nodes[i].mask = root_nodes[i].uid = root_nodes[i].gid = 0;
       root_nodes[i].length = file_headers[i].length;
       root_nodes[i].inode = i;
       root_nodes[i].flags = FS_FILE;
       root_nodes[i].read = &initrd_read;
       root_nodes[i].write = 0;
       root_nodes[i].readdir = 0;
       root_nodes[i].finddir = 0;
       root_nodes[i].open = 0;
       root_nodes[i].close = 0;
       root_nodes[i].impl = 0;
   }

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

   return initrd_root;
} 

8.4. Загружаем initrd как модуль multiboot

Теперь нам нужно сделать так, чтобы наш initrd загружался в память в первую очередь. К счастью, спецификация multiboot позволяет указывать "модули", которые должны быть загружены. Мы можем указать GRUB загрузить наш initrd как модуль. Вы можете сделать это с помощью монтирования файла floppy.img как устройства loopback; для этого найдите файл /boot/grub/menu.lst и добавьте строку 'module (fd0)/initrd' сразу ниже строки 'kernel'.

Либо вы можете скачать отсюда новый и улучшенный образ.

GRUB получает сведения о местоположении этого файла из структуры, содержащую информацию о multiboot, которую мы объявили в первом руководстве, но нигде не определяли. Мы должны определить ее сейчас: Это определение берется непосредственно из спецификаций Multiboot.

multiboot.h

#include "common.h"

#define MULTIBOOT_FLAG_MEM     0x001
#define MULTIBOOT_FLAG_DEVICE  0x002
#define MULTIBOOT_FLAG_CMDLINE 0x004
#define MULTIBOOT_FLAG_MODS    0x008
#define MULTIBOOT_FLAG_AOUT    0x010
#define MULTIBOOT_FLAG_ELF     0x020
#define MULTIBOOT_FLAG_MMAP    0x040
#define MULTIBOOT_FLAG_CONFIG  0x080
#define MULTIBOOT_FLAG_LOADER  0x100
#define MULTIBOOT_FLAG_APM     0x200
#define MULTIBOOT_FLAG_VBE     0x400

struct multiboot
{
   u32int flags;
   u32int mem_lower;
   u32int mem_upper;
   u32int boot_device;
   u32int cmdline;
   u32int mods_count;
   u32int mods_addr;
   u32int num;
   u32int size;
   u32int addr;
   u32int shndx;
   u32int mmap_length;
   u32int mmap_addr;
   u32int drives_length;
   u32int drives_addr;
   u32int config_table;
   u32int boot_loader_name;
   u32int apm_table;
   u32int vbe_control_info;
   u32int vbe_mode_info;
   u32int vbe_mode;
   u32int vbe_interface_seg;
   u32int vbe_interface_off;
   u32int vbe_interface_len;
}  __attribute__((packed));

typedef struct multiboot_header multiboot_header_t;

Интересны поля mods_addr и mods_count. В поле mods_count указывается количество загруженных модулей. Мы должны проверить, что оно > 0. Поле mods_addr представляет собой массив адресов: Каждая "запись" содержит начальный адрес модуля и его конец, размер каждой из которых равен 4 байта.

Поскольку мы предполагаем загрузить один модуль, мы просто рассматриваем поле mods_addr как указатель и ищем все, на что он указывает. Это будет место нашего файла initrd. Значение следующих 4 байтов адреса будет адресом конца файла. Мы можем использовать эту информацию с тем, чтобы при изменении адреса, используемого для управления памятью, случайно не перезаписать наш ramdisk!

main.c

int main(struct multiboot *mboot_ptr)
{
   // Инициализируем все ISR и сегментацию
   init_descriptor_tables();
   // Инициализируем экран (очищаем его)
   monitor_clear();

   // Находим местно нашего файла initial ramdisk.
   ASSERT(mboot_ptr->mods_count > 0);
   u32int initrd_location = *((u32int*)mboot_ptr->mods_addr);
   u32int initrd_end = *(u32int*)(mboot_ptr->mods_addr+4);
   // Пожалуйста, не затрите наш модуля при доступе к адресам размещения!
   placement_address = initrd_end;

   // Запуск страничной организации памяти.
   initialise_paging();

   // Инициализируем initial ramdisk и указываем его как корневую файловую систему.
   fs_root = initialise_initrd(initrd_location);
} 

Отлично! Это файловая система VFS и диск initrd, которые мы создали в кратчайшие сроки. Давайте проверим их.

8.5. Проверяем

Во-первых давайте добавим тестовый код, который найдет все файлы в директории '/' и выдаст его содержимое:

main.c

// Список содержимого директория /
int i = 0;
struct dirent *node = 0;
while ( (node = readdir_fs(fs_root, i)) != 0)
{
  monitor_write("Found file ");
  monitor_write(node->name);
  fs_node_t *fsnode = finddir_fs(fs_root, node->name);

  if ((fsnode->flags&0x7) == FS_DIRECTORY)
    monitor_write("\n\t(directory)\n");
  else
  {
    monitor_write("\n\t contents: \"");
    char buf[256];
    u32int sz = read_fs(fsnode, 0, 256, buf);
    int j;
    for (j = 0; j < sz; j++)
      monitor_put(buf[j]);

    monitor_write("\"\n");
  }
  i++;
} 

Создадим пару тестовых файлов и выполним сборку!

./make_initrd test.txt test.txt test2.txt test2.txt
cd src
make clean
make
cd ..
./update_image.sh
./run_bochs.sh 

Код для этого руководства можно найти здесь.


Назад К началу Вперед