Автор: uncle Bob
Дата: 22.12.2003
Раздел: Низкоуровневое программирование в Linux
ФАЙЛОВАЯ СИСТЕМА EXT2
В статье рассматривается процедура чтения файла c раздела жесткого диска с файловой системой ext2. С этой целью разработаем программный модуль, эмулирующий работу драйвера жесткого диска и драйвера файловой системы ext2 (далее модуль). Доступ к жесткому диску выполняется через пространство портов ввода-вывода ATA-контроллера (порядок доступа к диску через порты рассмотрен в [1]).
ЧАСТЬ 1
1. Структурная схема и алгоритм функционирования модуля
По сути, модуль является приложением пользователя, функционирующим под управлением операционной системы Linux. Структурная схема модуля показана на рис. 1.
В состав модуля входят следующие структурные элементы:
- эмулятор драйвера блочного устройства (жесткого диска) (далее драйвер жесткого диска);
- эмулятор драйвера файловой системы ext2 (далее драйвер файловой системы);
- подсистема ввода/вывода (I/O);
- таблица блочных устройств (ТБУ).
Адресное пространство процесса условно разделено на адресное пространство ядра и адресное пространство пользователя.
В UNIX-системах доступ к устройству на уровне пользователя выполняется через файл устройства, атрибутами которого являются старший и младший номера устройства. Старший номер указывает, к какому классу (типу) относится устройство, младший номер используется для непосредственной адресации устройства определенного типа. В нашем примере мы будем следовать этой традиции. Все АТА-устройства (жесткие диски с интерфейсом АТА) имеют единый старший номер, и обслуживаются одним драйвером. Младший номер определяет, к какому именно устройству драйвер должен обратиться для считывания/записи данных, т.к. к системе может быть подключено четыре АТА-устройства. Младший номер устройства - это 32-х разрядное число следующего формата:
0x00000XYY,
где X - номер канала (устройства)
YY - номер раздела на устройстве. Если этот номер равен нулю, драйвер будет обращаться к физическому устройству (RAW-режим), расположенному на канале X.
Как видно из схемы, все обращения к драйверу жесткого диска со стороны драйвера файловой системы выполняются через подсистему I/O.
В структуре драйвера блочного устройства определены следующие функции:
- функция инициализации и регистрации устройства в системе;
- функция, принимающая запросы подсистемы ввода/вывода (подсистема I/O) на чтение/запись данных (функция-диспетчер);
- функции чтения/записи данных
Перед обращением к драйверу выполняется его инициализацию. Команда инициализации поступает из подсистемы I/O. Во время инициализации драйвер выполняет следующие действия:
- опрашивает все каналы (их четыре) на предмет наличия АТА-устройств. Если устройство присутствует, драйвер считывает информацию о таблице разделов этого устройства и о самом устройстве (информацию идентификации устройства);
- выполняет процедуру регистрации в системе соответствующего блочного устройства путем заполнения таблицы блочных устройств (ТБУ). Каждая запись ТБУ содержит информацию об одном драйвере. Индексом в таблице является старший номер устройства.
После регистрации в системе драйвер готов к работе.
Для считывания (записи) данных с блочного устройства драйвер файловой системы обращается к подсистеме I/O. Одним из параметров, передаваемых подсистеме I/O, является старший номер устройства, для которого необходимо выполнить операцию считывания данных (записи данных). Используя старший номер в качестве индекса, подсистема I/O находит в ТБУ адрес функции-диспетчера соответствующего драйвера, и выполняет вызов данной функции, передав тем самым драйверу команду для выполнения, например, команду чтения. Функция-диспетчер драйвера принимает команду от подсистемы I/O, формирует запрос к устройству путем заполнения глобальной структуры ata_request, и вызывает функцию чтения с устройства. Считанные данные помещаются в буфер, адрес которого передается драйвером файловой системы через подсистему I/O. В случае, если поступила команда на запись, по этому адресу будут находяться данные, которые необходимо записать на устройство.
В нашем примере драйвер диска имеет ограничение - операции чтения/записи выполняются только для primary-разделов. О том, что такое primary-разделов и какие ещё бывают, нам расскажет следующий пункт.
2. Таблица разделов жесткого диска
На жестком диске по физическому адресу 0-0-1 располагается главная загрузочная запись (master boot record, MBR). В структуре MBR находятся следующие элементы:
- внесистемный загрузчик (non-system bootstrap - NSB);
- таблица описания разделов диска (partition table, PT). Располагается в MBR по смещению 0x1BE и занимает 64 байта;
- сигнатура MBR. Последние два байта MBR должны содержать число 0xAA55.
Таблица разделов описывает размещение и характеристики имеющихся на винчестере разделов. Разделы диска могут быть двух типов - primary (первичный, основной) и extended (расширенный). Максимальное число primary-разделов равно четырем. Наличие на диске хотя бы одного primary-раздела является обязательным. Extended-раздел может быть разделен на большое количество подразделов - логических дисков.
Упрощенно структура MBR представлена в таблице 1. Таблица разделов располагается в конце MBR, для описания раздела в таблице отводится 16 байт.
Таблица 1. Структура MBR.
Смещение (offset) Размер (Size) Содержимое (contents)
------------------------------------------------------------------------
0 446 Программа анализа таблицы разделов
и загрузки System Bootstrap
с активного раздела
-------------------------------------------------------------------------
0x1BE 16 Partition 1 entry (первый раздел)
-------------------------------------------------------------------------
0x1CE 16 Partition 2 entry
-------------------------------------------------------------------------
0x1DE 16 Partition 3 entry
-------------------------------------------------------------------------
0x1EE 16 Partition 4 entry
-------------------------------------------------------------------------
0x1FE 2 Сигнатура 0xAA55
Первым байтом в элементе раздела идет флаг активности раздела (0 - неактивен, 0x80 - активен). Он служит для определения, является ли раздел системным загрузочным и есть ли необходимость производить загрузку операционной системы с него при старте компьютера. Активным может быть только один раздел. За флагом активности раздела следуют координаты начала раздела - три байта, означающие номер головки, номер сектора и номер цилиндра. Затем следует кодовый идентификатор System ID, указывающий на принадлежность данного раздела к той или иной операционной системе. Идентификатор занимает один байт. За системным идентификатором расположены координаты конца раздела - три байта, содержащие номера головки, сектора и цилиндра, соответственно. Следующие четыре байта - это число секторов перед разделом, и последние четыре байта - размер раздела в секторах.
Таким образом, раздел можно описать при помощи следующей структуры:
struct pt_struct {
u8 bootable; // флаг активности раздела
u8 start_part[3]; // координаты начала раздела
u8 type_part; // системный идентификатор
u8 end_part[3]; // координаты конца раздела
u32 sect_before; // число секторов перед разделом
u32 sect_total; // размер раздела в секторах (число секторов в разделе)
};
Итак, все необходимые теоретические сведения у нас есть, можно приступить непосредственно к рассмотрению программной реализации модуля.
3. Структуры и переменные
Начнем с описания переменных и информационных структур, которые будут использованы при разработке.
Введем обозначение типов данных:
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;
Системные ресурсы, выделенные каналам:
#define CH0 0x1f0 // Primary Master, канал 0
#define CH1 0x1E8 // Primary Slave, канал 1
#define CH2 0x170 // Secondary Master, канал 2
#define CH3 0x168 // Secondary Slave, канал 3
Биты основного регистра состояния ATA-устройства (назначение каждого бита рассмотрено в [1]):
#define BSY 0x80 // флаг занятости устройства
#define DRDY 0x40 // готовность устройства к восприятию команд
#define DF 0x20 // индикатор отказа устройства
#define DRQ 0x08 // индикатор готовности устройства к обмену данными
#define ERR 0x01 // индикатор ошибки выполнения операции
Получение номера устройства и номера раздела из младшего номера файла устройства выполняют макросы:
#define GET_DEV(X) ((X & 0x00000F00) >> 8);
#define GET_PART(X) (X & 0x000000FF);
Размер записи таблицы разделов (0x10):
#define PT_SIZE 0x10
Следующий массив структур заполняется драйвером диска в процессе инициализации ATA-устройств, подключенных к системе:
struct dev_status_struct {
u8 status;
struct hd_driveid hd;
pt_t pt[4];
} dev_status[4];
Назначение полей структуры:
- status - информация о состоянии устройства (0/1 - отсутствие/наличие)
- struct hd_driveid hd - информация идентификации устройства. Данная структура содержится в заголовочном файле <linux/hdreg.h>
- pt - информация о таблице разделов на устройстве
Для работы с полями данной структуры определим несколько макросов:
#define DEV_STAT(X) dev_status[X].status
#define DEV_ID(X) dev_status[X].hd
#define DEV_PT(X,Y) dev_status[X].pt[Y]
Здесь X - номер устройства, Y - номер раздела
Поскольку жесткий диск - устройство блочное, то обмен данными осуществляется только блоками. Информация о том, сколько на разделе устройства блоков и размер одного блока будет находиться здесь:
typedef struct device_info_struct {
int blocks_num;
int block_size;
} device_info_t;
Размер блока на устройстве и размер одного сектора (в байтах):
#define BLK_SIZE 2048
#define BYTE_PER_SECT 512
Драйверу устройства можно послать три команды:
#define WRITE 0 // записать данные на устройство
#define READ 1 // прочитать данные с устройства
#define STAT 2 // получить характеристику раздела устройства
По команде STAT драйвер вернет о информацию о размере одного блока и число блоков на разделе устройстве. Данной информацией заполняется структура struct device_info_struct
Идентификатор ATA-устройства:
#define ATA 1
4. Драйвер ATA-устройства (жесткого диска)
Ресурсы, выделенные каналам, разместим в массиве:
u16 channels[4] = { CH0, CH1, CH2, CH3 };
Адресация к регистрам ATA-контроллера выполняется при помощи следующих макросов:
- фиксация ошибки выполнения команды:
int check_error(u8 dev)
{
unsigned char a;
IN_P_B(a, ATA_STATUS(dev));
if (a & ERR) return 1;
return 0;
}
В соответствии с алгоритмом, первая команда, посылаемая драйверу - это команда инициализации. Команда выполняется путем вызова функции инициализации, которая находится в теле драйвера:
/* Инициализация драйвера АТА */
int hd_init()
{
int i = 0, major = 0;
get_ata_info(); // опросить каналы на предмет наличия ATA-устройств
show_ata_info(); // отобразить информацию о найденых устройствах
get_pt_info(); // получить таблицу разделов с каждого устройства
major = reg_blkdev(MAJOR_ATA,"ATA",&hd_request); // зарегистрировать драйвер устройства
return major;
}
При выполнении инициализации драйвер опрашивает все каналы на предмет наличия ATA-устройств, отображает информацию о найденых устройствах, получает от каждого найденого устройства таблицу разделов и регистрируется в системе.
Опрос каналов выполняет функция get_ata_info(). Вот как она выглядит:
void get_ata_info()
{
Для поиска устройств организуем цикл из четырех итераций:
int dev = 0;
for(; dev < 4 ; dev++) {
Ожидаем освобождение устройства. Если таймаут исчерпан - на данном канале устройство отсутствует:
Если устройство на канале присутствует, то пытаемся получить от него информацию идентификации. Информация о наличии/отсутствии устройства на канале будет сохранена в поле status структуры struct dev_status_struct (см. раздел "Структуры и переменные"):
Вывод информации об устройствах, подключенных к системе, выполняет функция show_ata_info():
void show_ata_info()
{
int i = 0;
for(; i < 4; i++) {
printf("ATA%d - ",i);
if(!DEV_STAT(i)) printf("none\n");
if(DEV_STAT(i) == ATA) {
printf("exists\n");
printf("\tType - ATA Disk drive\n");
printf("\tModel - %s\n",DEV_ID(i).model);
printf("\tLBA capacipty - %d\n",DEV_ID(i).lba_capacity);
}
}
}
Получаем от каждого устройства таблицу разделов:
void get_pt_info()
{
u8 dev;
u32 minor = 0;
int i = 0;
unsigned char buff[0x200];
Опрашиваем все ATA устройства и получаем от каждого таблицу разделов:
for(; i < 4; i++) {
dev = GET_DEV(minor);
if(DEV_STAT(CURRENT) != ATA) continue;
if(hd_request(minor,READ,0,1,buff) < 0) break;
memcpy(dev_status[dev].pt,(struct pt_struct *)(buff+0x1BE),PT_SIZE*4);
minor += 0x100;
}
return;
}
Считывание таблицы разделов с устройства выполняет функция-диспетчер hd_request, одним из параметров которой является младший номер устройства. Опрос устройств начинается с нулевого канала, при этом поле номера раздела равно нулю, что означает работу с устройством в RAW-режиме.
После того, как информация о таблице разделов собрана с каждого устройства, драйвер регистрируется в системе, вызвав функцию reg_blkdev. В параметрах функции указывается старший номер устройства, соответствующий позиции в таблице блочных устройств, имя драйвера и адрес функции-диспетчера. Функция регистрации входит в состав подсистемы ввода-вывода, которую мы рассмотрим ниже.
4.1. Функция-диспетчер.
По определению, данная функция принимает запросы подсистемы I/O на чтения/запись данных.
Для выполнения команды функция-диспетчер формирует запрос к устройству, который представляет собой структуру следующего вида:
struct ata_request {
u8 dev; /* номер канала (устройства) : 0,1,2,3 */
u16 *buff; /* указатель на буфер с данными для чтения/записи (r/w) на устройство */
u32 nlba; /* номер логического сектора для r/w */
u32 nsect; /* число секторов для r/w */
u8 err; /* индикатор ошибки выполнения команды*/
u8 lock; /* флаг блокировки буфера данных на время выполнения поступившей команды */
u8 complite; /* флаг завершения операции (команды) */
} dev_r;
#define CURRENT dev_r.dev
Функция выглядит следующим образом:
int hd_request(u32 minor, u8 cmd, u32 start_sect, u32 s_count, u8 *buff)
{
Параметры функции-диспетчера:
- u32 minor - младший номер устройства;
- u8 cmd - команда, подлежащая выполнению. Их у нас целых три - READ, WRITE, STAT (см. раздел "Структуры и переменные");
- u32 start_sect - адрес стартового сектора для чтения/записи данных. Адрес задается в формате LBA и по сути является порядковым номером сектора на устройстве;
- u32 s_count - число секторов для чтения/записи;
- u8 *buff - указатель на буфер, куда необходимо поместить прочитанные с устройства данные, если поступила команда READ. Если поступила команда WRITE, по этому адресу будут находится данные, которые надо записать на устройство.
Извлекаем из младшего номера номер устройства и номер раздела на устройстве:
u16 part = GET_PART(minor);
u8 command;
CURRENT = GET_DEV(minor);
Проверяем, присутствует ли в системе устройство, с которого мы пытаемся прочесть данные (или записать):
if(DEV_STAT(CURRENT) != ATA) return -1;
Работать можно только с основными разделами, или со всем устройством в RAW-режиме, поэтому проверяем номер раздела. Он не должен быть больше четырех:
if(part > 4) return -1;
Проверяем, не заблокирован ли буфер для данных в структуре запроса struct ata_request. Если нет - блокируем его на время выполнения запроса, выставив флаг lock:
while(dev_r.lock) continue;
dev_r.lock = 1;
Заполняем поля структуры запроса значениями:
dev_r.nlba = start_sect; /* стартовый сектор */
dev_r.nsect = 1; /* число cекторов для чтения/записи */
dev_r.buff = (unsigned short *)buff;
Определяем, какая команда поступила:
switch(cmd) {
case STAT:
return stat_hd(part);
break;
case READ:
command = 0x20;
handler = &intr_read;
break;
case WRITE:
command = 0x30;
handler = &intr_write;
break;
Если приходит команда STAT, драйвер просто вернет подсистеме I/O информацию о характеристиках раздела устройства, такую как размер блока и число блоков на разделе устройстве, вызвав функцию stat_hd:
Уменьшаем счетчик секторов. Если он равен нулю - завершаем выполнение команды и выходим из цикла. Если нет - считываем следующий сектор и смещаем указатель в буфере на 512 байт (размер сектора):
Рассмотрение драйвера жесткого диска на этом завершим, и переходим к рассмотрению подсистемы ввода-вывода.
5. Подсистема I/O
В соответствии с алгоритмом, подсистема I/O выполняет инициализацию драйвера блочного устройства, и в дальнейшем принимает запросы драйвера файловой системы на чтение/запись данных на устройство. Во время инициализации соответствующий драйвер заполняет таблицу блочных устройств, которая представляет собой массив структур:
static struct blkdev_struct blkdev[MAX_BLKDEV],
где MAX_BLKDEV - число элементов в таблице блочных устройств, и, соответственно, количество блочных устройств, которое можно подключить к системе:
#define MAX_BLKDEV 256
Элемент таблицы блочных устройств представляет собой структуру следующего вида:
struct blkdev_struct {
const char name[20];
int (*dev_request)(u32, u8, u32, u32, unsigned char *);
};
Назначение полей структуры struct blkdev_struct:
- const char name[20] - имя драйвера блочного устройства
- int (*dev_request)(u32, u8, u32, u32, unsigned char *) - адрес функции-диспетчера драйвера блочного устройства.
Таблица блочных устройств проиндексирована при помощи старшего номера устройства. Для ATA-устройств старший номер равен 5:
#define MAJOR_ATA 5
Процедура инициализации выполняется путем вызова функции blkdev_init():
Во время инициализации вызывается функция hd_init(), находящаяся в теле драйвера. Эту функцию мы уже практически полностью рассмотрели, за исключением функции reg_blkdev - функции регистрации драйвера устройства в системе:
Параметры вызова функции мы уже рассмотрели. Эта функция заполняет соответствующий элемент таблицы блочных устройств, и, тем самым, у нас появляется возможность обратиться к функции-диспетчеру драйвера ATA-устройства.
Эту возможность реализует функция blkdev_io():
Параметры функции blkdev_io():
- u32 major - старший номер устройства, и, соответственно, индекс в таблице блочных устройств;
- u32 minor - младший номер, определяет номер устройства и номер раздела на устройстве;
- u8 cmd - команда, посылаемая устройству;
- u32 start_sect - адрес стартового сектора для чтения(записи);
- u32 count - число секторов для чтения(записи);
- u8 *buff - указатель на буфер для данных;
Перед выполнением операций чтения/записи данных на раздел устройства сперва необходимо получить характеристики раздела, такие как размер блока на разделе и количество этих блоков. Для этого устройству посылается команда STAT при помощи функции stat_blkdev():
Получив характеристики раздела устройства, можно приступать к чтению/записи данных. Функция read_blkdev(), которую мы сейчас рассмотрим, выполняет чтение данных с раздела жесткого диска. Одновременно эта функция является точкой входа для драйвера файловой системы.
Параметрами функции являются старший и младший номер устройства, смещение к данным на разделе в байтах (т.к. драйвер ФС "видит" раздел как последовательность байт), число байт для считывания и указатель на буфер, куда будут помещены считанные данные.
Так как драйвер жесткого диска считывает информацию блоками, то необходимо преобразовать величину смещения в номер блока на устройстве, и при этом нет никаких гарантий, что смещение к данным попадет точно на границу блока. Поэтому алгоритм считывания данных следующий с раздела жесткого диска следующий:
- определяется номер блока, в который "попадает" величина смещения, количество блоков для чтения, и эти блоки считываются в дисковый кеш;
- определяется величина смещения к данным в кеше, и эти данные копируются в область памяти, на которую указывает последний параметр вызова функции read_blkdev().
Весь этот процесс показан на рис. 2.
Определим необходимые переменные:
u32 start_lba, // стартовый сектор для чтения
s_count, // число секторов для чтения
start_block, // стартовый блок для чтения (0,1,2, ...)
end_block, // конечный блок для чтения. Может быть равен стартовому
tail, // смещение к данным в буферном кеше
num_block; // число блоков для считывания
device_info_t dev_i;
u8 *cache_buff; // указатель на начало буферного кешв
printf("Стартовый сектор для чтения на устройстве - %d\n",start_lba);
printf("Число секторов для чтения - %d\n\n",s_count);
И вот теперь вызываем функцию-диспетчер соответствующего блочного устройства (жесткого диска), передав ей команду для выполнения и необходимые параметры:
Перед тем, как приступить к рассмотрению драйвера файловой системы ext2, необходимо познакомиться с самой файловой системой, с её логической структурой. Об этом читайте во второй части статьи.
Исходники к статье