Автор: uncle Bob Дата:
22.12.2003 Раздел: Низкоуровневое программирование в Linux
ФАЙЛОВАЯ
СИСТЕМА EXT2
В статье рассматривается процедура чтения файла c
раздела жесткого диска с файловой системой ext2. С этой целью разработаем
программный модуль, эмулирующий работу драйвера жесткого диска и драйвера
файловой системы ext2 (далее модуль). Доступ к жесткому диску выполняется через
пространство портов ввода-вывода ATA-контроллера (порядок доступа к диску через
порты рассмотрен в [1]).
ЧАСТЬ 1
1. Структурная схема и алгоритм
функционирования модуля
По сути, модуль является приложением
пользователя, функционирующим под управлением операционной системы Linux.
В состав модуля входят
следующие структурные элементы: - эмулятор драйвера блочного устройства
(жесткого диска) (далее драйвер жесткого диска); - эмулятор драйвера
файловой системы 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().
Определим необходимые
переменные: 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, необходимо
познакомиться с самой файловой системой, с её логической структурой. Об этом
читайте во второй части статьи. Исходники к статье