Rambler's
Top100

Файловая система EXT2. Часть 1

Автор: 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);

Структура таблицы разделов (см. выше):
typedef struct pt_struct {
u8 bootable;
u8 start_part[3];
u8 type_part;
u8 end_part[3];
u32 sect_before;
u32 sect_total;
} pt_t;

Размер записи таблицы разделов (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-контроллера выполняется при помощи следующих макросов:

#define ATA_STATUS(x) (channels[x] + 7)
#define ATA_CURRENT(x) (channels[x] + 6)
#define ATA_HCYL(x) (channels[x] + 5)
#define ATA_LCYL(x) (channels[x] + 4)
#define ATA_SECTOR(x) (channels[x] + 3)
#define ATA_NSECTOR(x) (channels[x] + 2)
#define ATA_ERROR(x) (channels[x] + 1)
#define ATA_DATA(x) (channels[x])

где x - номер канала.

Для работы с портами ввода/вывода определим несколько макросов.

Макросы OUT_P_B и OUT_P_W выполняют запись байта/слова в порт:
#define OUT_P_B(val,port) asm("outb %%al, %%dx"::"a"(val),"d"(port))
#define OUT_P_W(val,port) asm("outw %%ax, %%dx"::"a"(val),"d"(port))

Макросы IN_P_B и IN_P_W выполняют чтение байта/слова из порта:
#define IN_P_B(val,port) asm("inb %%dx, %%al":"=a"(val):"d"(port))
#define IN_P_W(val,port) asm("inw %%dx, %%ax":"=a"(val):"d"(port))

void (*handler)(void);
Назначение этого указателя будет рассмотрено далее.

Следующие функции были подробно рассмотрены в [1]:

- проверка занятости устройства:
int hd_busy(u8 dev)
{
int t = 0;
unsigned char status;

do {
t++;
IN_P_B(status,ATA_STATUS(dev));
if(t & TIMEOUT) break;
} while (status & BSY);
return t;
}

- проверка готовности устройства к восприятию команд:
int hd_ready(u8 dev)
{
int t = 0;
unsigned char status;

do {
t++;
IN_P_B(status,ATA_STATUS(dev));
if(t & TIMEOUT) break;
} while (!(status & DRDY));

return t;
}

- проверка готовности устройства к обмену данными:
int hd_data_request(u8 dev)
{
unsigned char status;

IN_P_B(status, ATA_STATUS(dev));
if(status & DRQ) return 1;
return 0;
}

- фиксация ошибки выполнения команды:
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++) {

Ожидаем освобождение устройства. Если таймаут исчерпан - на данном канале устройство отсутствует:

if(hd_busy(dev) & TIMEOUT) {
DEV_STAT(dev) = 0;
continue;
}

Если устройство на канале присутствует, то пытаемся получить от него информацию идентификации. Информация о наличии/отсутствии устройства на канале будет сохранена в поле status структуры struct dev_status_struct (см. раздел "Структуры и переменные"):

DEV_STAT(dev) = ATA;
if(get_ata_identity(dev) < 0) DEV_STAT(dev) = 0;
}
}


Функция получениe информации об устройстве имеет следующий вид:
int get_ata_identity(u8 dev)
{
int i = 0;

Ждем готовность устройства
if(hd_busy(dev) & TIMEOUT) return -1;

Устройство свободно, поэтому устанавливаем биты выбора устройства (ведущее/ведомое), режима работы (LBA):

if((dev == 0) || (dev == 2))
OUT_P_B(0xE0,ATA_CURRENT(dev));
if((dev == 1) || (dev == 3))
OUT_P_B(0xF0,ATA_CURRENT(dev));

и ожидаем готовность устройства к приёму команд:

if(hd_ready(dev) == TIMEOUT) return -1;

Дождавшись, отправляем ему (устройству) команду идентификации 0xEC:

OUT_P_B(0xEC,ATA_STATUS(dev));

Устройства ATAPI команду 0xEC (идентификация) отвергают. Если будет установлен бит ERR - на канале ATAPI:

if(check_error(dev)) return -1;

Ожидаем готовность устройства поделиться данными:

for(; i < TIMEOUT; i++) if(hd_data_request(dev)) break;
if(i == TIMEOUT) return -1;

Считываем информацию о жестком диске и сохраняем ее в поле hd структуры struct dev_status_struct (см. раздел "Структуры и переменные"):

asm(
" cld \n\t"
"1: inw %%dx, %%ax \n\t"
" stosw \n\t"
" decw %%cx \n\t"
" jnz 1b \n\t"
::"D"(&DEV_ID(dev)), "d"(ATA_DATA(dev)),"c"(0x100));

return 0;
}


Вывод информации об устройствах, подключенных к системе, выполняет функция 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;

default:
printf("Unknown command\n");
dev_r.lock = 0;
return -1;
break;
}

Если приходит команда STAT, драйвер просто вернет подсистеме I/O информацию о характеристиках раздела устройства, такую как размер блока и число блоков на разделе устройстве, вызвав функцию stat_hd:

int stat_hd(u16 part)
{
device_info_t dev_i;

dev_i.block_size = BLK_SIZE;
dev_i.blocks_num = DEV_ID(CURRENT).lba_capacity/(BLK_SIZE/BYTE_PER_SECT);

if(part != 0)
dev_i.blocks_num = DEV_PT(CURRENT,(part-1)).sect_total/(BLK_SIZE/BYTE_PER_SECT);

memcpy(dev_r.buff, (u16 *)&dev_i, sizeof(device_info_t));

dev_r.lock = 0;
return 0;
}


Если поступила команда чтения/записи - выполняем её:

do_command(s_count, command);

По окончании выполнения команды сбрасываем флаг "Операция завершена" и разблокируем буфер данных:
dev_r.complite = 0;
dev_r.lock = 0;
return 0;
}

Выполнение поступившей команды осуществляется путем вызова функции do_command:
int do_command(u32 count, u8 com)
{
for(;;) {

Посылаем устройству команду com и вызываем соответствующую функцию для чтения/записи, на которую настрооен указатель handler():

send_command(com);
handler();

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

while(!(dev_r.complite)) continue;
if(dev_r.err) return -1;

Уменьшаем счетчик секторов. Если он равен нулю - завершаем выполнение команды и выходим из цикла. Если нет - считываем следующий сектор и смещаем указатель в буфере на 512 байт (размер сектора):

count--;
if(!count) break;
dev_r.nlba++;
dev_r.buff += 0x100;
}

return 0;
}


Команду чтения/записи данных устройству посылает функция send_command:

void send_command(u8 cmd)
{
hd_busy(CURRENT);

Выбираем устройства (ведущее/ведомое).
Ведущее устройство:

if((CURRENT == 0) || (CURRENT == 2))
OUT_P_B(0xE0|((dev_r.nlba & 0x0F000000) >> 24),ATA_CURRENT(CURRENT));

Ведомое устройство:

if((CURRENT == 1) || (CURRENT == 3))
OUT_P_B(0xF0|((dev_r.nlba & 0x0F000000) >> 24),ATA_CURRENT(CURRENT));

hd_ready(CURRENT);

OUT_P_B(dev_r.nsect,ATA_NSECTOR(CURRENT));
OUT_P_B((dev_r.nlba & 0x000000FF),ATA_SECTOR(CURRENT));
OUT_P_B(((dev_r.nlba & 0x0000FF00) >> 8),ATA_LCYL(CURRENT));
OUT_P_B(((dev_r.nlba & 0x00FF0000) >> 16),ATA_HCYL(CURRENT));
OUT_P_B(cmd,ATA_STATUS(CURRENT));

return;
}


Чтение данных с устройства выполняет функция intr_read:
void intr_read()
{
int i = 0;
dev_r.complite = 0;

hd_busy(CURRENT);
if(check_error(CURRENT)) {
dev_r.err = 1;
return;
}

while(!(hd_data_request(CURRENT))) continue;

for(;i < 0x100; i++)
IN_P_W(dev_r.buff[i],ATA_DATA(CURRENT));

dev_r.complite = 1;
return;
}


Запись данных на устройство выполняет функция intr_write:
void intr_write()
{
int i = 0;
dev_r.complite = 0;

hd_busy(CURRENT);
if(check_error(CURRENT)) {
dev_r.err = 1;
return;
}

while(!(hd_data_request(CURRENT))) continue;

for(;i < 0x100; i++)
OUT_P_W(dev_r.buff[i],ATA_DATA(CURRENT));

dev_r.complite = 1;
return;
}


Рассмотрение драйвера жесткого диска на этом завершим, и переходим к рассмотрению подсистемы ввода-вывода.


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():

int blkdev_init()
{
if(hd_init() != MAJOR_ATA) {
printf("init error\n");
return -1;
}
return 0;
}

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

int reg_blkdev(u32 major,const char *name,
int (*dev_req)(u32, u8, u32, u32, unsigned char *))
{
blkdev[major].name = name;
blkdev[major].dev_request = dev_req;
return major;
}

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

int blkdev_io(u32 major, u32 minor, u8 cmd, u32 start_sect, u32 count, u8 *buff)
{
if(blkdev[major].dev_request(minor, cmd, start_sect, count, buff) < 0) return -1;
return 0;
}

Параметры функции blkdev_io():
- u32 major - старший номер устройства, и, соответственно, индекс в таблице блочных устройств;
- u32 minor - младший номер, определяет номер устройства и номер раздела на устройстве;
- u8 cmd - команда, посылаемая устройству;
- u32 start_sect - адрес стартового сектора для чтения(записи);
- u32 count - число секторов для чтения(записи);
- u8 *buff - указатель на буфер для данных;


Перед выполнением операций чтения/записи данных на раздел устройства сперва необходимо получить характеристики раздела, такие как размер блока на разделе и количество этих блоков. Для этого устройству посылается команда STAT при помощи функции stat_blkdev():

int stat_blkdev(u32 major, u32 minor, u8 *buff)
{
if(blkdev_io(major,minor,STAT,0,0,buff) < 0) return -1;
return 0;
}

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

int read_blkdev(u32 major, u32 minor, u64 start, u64 count, u8 *buff)
{

Параметрами функции являются старший и младший номер устройства, смещение к данным на разделе в байтах (т.к. драйвер ФС "видит" раздел как последовательность байт), число байт для считывания и указатель на буфер, куда будут помещены считанные данные.
Так как драйвер жесткого диска считывает информацию блоками, то необходимо преобразовать величину смещения в номер блока на устройстве, и при этом нет никаких гарантий, что смещение к данным попадет точно на границу блока. Поэтому алгоритм считывания данных следующий с раздела жесткого диска следующий:
- определяется номер блока, в который "попадает" величина смещения, количество блоков для чтения, и эти блоки считываются в дисковый кеш;
- определяется величина смещения к данным в кеше, и эти данные копируются в область памяти, на которую указывает последний параметр вызова функции 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; // указатель на начало буферного кешв

Получаем характеристики раздела:

if(stat_blkdev(major,minor,(u8 *)&dev_i) < 0) return -1;

Вычисляем номера стартового и конечного блока, смещение к данным и число блоков для считывания:

start_block = start/dev_i.block_size;
end_block = (start+count)/dev_i.block_size;
tail = start%dev_i.block_size;
num_block = (end_block - start_block) + 1;

Выведем отладочную информацию:
printf("Размер блока - %d байт\n",dev_i.block_size);
printf("Число блоков на устройстве (разделе) - %d\n",dev_i.blocks_num);
printf("Стартовый блок на разделе - %d\n",start_block);
printf("Смещение к данным в буферном кеше, байт - %d\n",tail);
printf("Число блоков для чтения - %d\n\n",num_block);

Выделяем память для буферного кеша:

cache_buff = (u8 *)malloc(num_block * dev_i.block_size);
memset(cache_buff,0,num_block * dev_i.block_size);

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

start_lba = block_to_lba(start_block,minor);
if(start_lba < 0) return -1;
s_count = num_block * (BLK_SIZE/BYTE_PER_SECT);

printf("Стартовый сектор для чтения на устройстве - %d\n",start_lba);
printf("Число секторов для чтения - %d\n\n",s_count);


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

if(blkdev_io(major,minor,READ,start_lba,s_count,cache_buff) < 0) return -1;

В область памяти, на которую указывает buff (параметр вызова функции read_blkdev()), скопируем данные из буферном кеше.

memcpy(buff, cache_buff+tail, count);

Очищаем кеш и возвращаемся из функции:

free(cache_buff);
return 0;
}


Пересчет номера блока на устройстве в стартовый номер логического сектора выполняет функция block_to_lba():

u32 block_to_lba(u32 start_block, int minor)
{
u32 lba;
u8 dev = GET_DEV(minor);
u16 part = GET_PART(minor);

if((start_block < 0) || (part > 4)) return -1;

lba = start_block * (BLK_SIZE/BYTE_PER_SECT);
if(part != 0) lba += DEV_PT(dev,(part-1)).sect_before;

return lba;
}


Перед тем, как приступить к рассмотрению драйвера файловой системы ext2, необходимо познакомиться с самой файловой системой, с её логической структурой. Об этом читайте во второй части статьи.
Исходники к статье


[Главная] [Другие статьи] [Обсудить в форуме]
©2003-2004 Lowlevel.RU