Работа с жестким диском на программном уровне

Автор: uncle Bob
Дата: 11.11.2003
Раздел: Низкоуровневое программирование в Linux

В статье рассматривается, как осуществить доступ к IDE-диску на программном уровне при помощи файла устройства и через порты ATA контроллера.

1. Файлы устройств

Файл - основа любой операционной системы, поскольку именно с ним производится наибольшее число действий. В UNIX- и POSIX-системах существуют файлы следующих типов:

Блок-ориентированный файл устройства служит для представления физического устройства, которое передает данные блоками. Примером блок-ориентированного устройства является жесткий диск. Байт-ориентированный файл устройства служит для представления физического устройства, которое передает данные побайтово (например, модем).

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

Файл устройства создается командой mknod, одним из аргументов которой является старший номер устройства (major device number). По сути старший номер - это индекс в таблице ядра, которая содержит адреса всех драйверов, известных системе. В ОС Linux создаются две таблицы - таблица блочных устройств (block device switch) и таблица символьных устройств (character device switch). Обе таблицы являются массивом структур и проиндексированы при помощи значения старшего номера устройства. Таблица блочных устройств определена в файле fs/block_dev.c следующим образом:

static struct { const char *name; struct block_device_operations *bdops; } blkdevs[MAX_BLKDEV];

Этот массив заполняется во время регистрации блочного устройства в системе. Для регистрации устройства соответствующий драйвер вызывает функцию register_blkdev (см. файл fs/block_dev.c):

int register_blkdev(unsigned int major, const char * name, struct block_device_operations *bdops) { .... blkdevs[major].name = name; blkdevs[major].bdops = bdops; return 0; }

Аргумент major - старший номер устройства, name - имя файла устройства, структура struct block_device_operations содержит функции, выполняемые драйвером устройства. Однако функции read и write в этой структуре отсутствуют. Дело в том, что пользовательский процесс не выполняет напрямую операции чтения/записи в блочное устройства. Для этой цели драйвер предоставляет системе механизм request, и все операции ввода/вывода выполняются через буферный кеш системы, но это тема для отдельной статьи.

При снятии регистрации соответствующий элемент массива blkdevs обнуляется:

int unregister_blkdev(unsigned int major, const char * name) { .... blkdevs[major].name = NULL; blkdevs[major].bdops = NULL; return 0; }

Таблица символьных устройств определена в файле fs/devices.c и также является массивом структур, который заполняется при регистрации устройства в системе:

struct device_struct { const char * name; struct file_operations * fops; }; static struct device_struct chrdevs[MAX_CHRDEV];

Структура struct file_operations определена в файле linux/fs.h и содержит функции, выполняемые драйвером символьного устройства.

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

Кроме операций чтения/записи, драйвер также предоставляет возможность управления устройством. Операция управления осуществляется при помощи функции ioctl. Эта функция вызывается пользовательским процессом и имеет следующий прототип:

int ioctl(int fd, int cmd, ...);

Аргументы функции:
int fd - файловый дескриптор устройства;
int cmd - команда, посылаемая устройству.

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

2. Доступ к жесткому диску через файл устройства

Предположим, что в системе присутствует один накопитель на жестком магнитном диске, который подключен как Primary Master. Согласно обозначениям блочных устройств, принятым в ОС Linux, ему соответствует файл устройства /dev/hda. Разработаем программный модуль, выполняющий чтение первого сектора (MBR) и получающий информацию об устройстве, такую как модель жесткого диска, его серийный номер, геометрию (число цилиндров/головок/секторов) и число логических блоков.

Нам понадобятся следующие заголовочные файлы:

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/types.h> #include <errno.h> #include <linux/hdreg.h>

В файле <linux/hdreg.h> определена структура struct hd_driveid, предназначенная для хранения информации идентификации устройства, и перечень команд управления устройством.

int main() { struct hd_driveid ide; int hda, sect; char sector[512];

Для доступа к устройству достаточно стандартным способом открыть соответствующий файл устройства:

hda=open("/dev/hda",O_RDONLY); if(!hda) { perror("hda"); } sect=open("mbr",O_CREAT|O_RDWR,0600); if(!sect) { perror("sect"); }

Для получения информации идентификации устройству посылаем команду HDIO_GET_IDENTITY. Команда идентификации позволяет считать из контроллера блок из 256 слов, характеризующих устройство. Результат будет сохранен в структуре struct hd_driveid ide, адрес которой задается в качестве третьего аргумента функции

ioctl: if(ioctl(hda,HDIO_GET_IDENTITY,&ide)) perror ("HDIO_GET_IDENTITY");

Как уже было упомянуто, перечень команд управления устройством определен в файле <linux/hdreg.h>. Например, команды включения 32-битного режима обмена данными, режима DMA и мультисекторного чтения выглядят следующим образом:

static u_long dma=1, io32set=1, mult=16; if(ioctl(hda,HDIO_SET_32BIT,io32set)) perror("HDIO_SET_32BIT"); if(ioctl(hda,HDIO_SET_DMA,dma)) perror("HDIO_SET_DMA"); if(ioctl(hda,HDIO_SET_MULTCOUNT,mult)) perror("HDIO_SET_MULTCOUNT");

Отобразим информацию идентификации устройства:

printf("Серийный номер - %s\n",ide.serial_no); printf("Модель - %s\n",ide.model); printf("Число логических блоков - %d\n",ide.lba_capacity); printf("Число цилиндров - %d\n",ide.cyls); printf("Число головок - %d\n",ide.heads); printf("Число секторов - %d\n",ide.sectors);

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

read(hda,sector,sizeof(sector)); write(sect,sector,sizeof(sector)); close(hda); close(sect); return (0); }

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

3. Интерфейс АТА

Ниже приведены краткие сведения об интерфейсе АТА-2. Для получения детальной информации обратитесь к спецификации.

3.1. Регистры АТА контроллера

Каждое устройство АТА (жесткий диск с интерфейсом АТА) имеет стандартный набор регистров, адресуемых сигналами от хост-адаптера (средства сопряжения интерфейса АТА с системной шиной). Набор регистров состоит из двух блоков - блока командных регистров и блока управляющих регистров.

Блок командных регистров служит для посылки команд устройству и передачи информации о его состоянии. Состав блока командных регистров:

1. Регистр состояния/команд - в режиме чтения отражает текущее состояние устройства в процессе выполнения команды. Чтение регистра состояния разрешает дальнейшее изменение его бит и сбрасывает запрос аппаратного прерывания. В режиме записи принимает коды команд для выполнения.

Назначение бит регистра состояния:

Бит 7 - BSY (Busy) указывает на занятость устройства. При единичном значении устройство игнорирует попытки записи в блок командных регистров. При нулевом значении этого бита регистры командного блока доступны. Бит устанавливается под действием аппаратного или программного сброса, а также при получении команды.

Бит 6 - DRDY (Device Ready) указывает на готовность устройства к восприятию любых кодов команд.

Бит 5 - DF (Device Fault) - индикатор отказа устройства.

Бит 4 - DSC (Device Seek Complite) - индикатор завершения поиска трека.

Бит 3 - DRQ (Data Request) - индикатор готовности к обмену словом или байтом данных.

Бит 2 - CORR (Correct Data) - индикатор исправленной ошибки данных.

Бит 1 - IDX (Index) - индекс, трактуется специфично для каждого производителя.

Бит 0 - ERR (Error) - индикатор ошибки выполнения предыдущей операции. Дополнительная информация содержится в регистре ошибок.

2. Регистр номера цилиндра (старшего и младшего байта) и номера сектора имеют двоякое назначение в зависимости от выбранной системы адресации (CHS или LBA). Они инициализируются хост-адаптером, а в случае возникновения ошибки при операции устройство поместит в них адрес, по которому встретилась ошибка.

3. Регистр номера устройства и головки кроме хранения части адресной информации служит для выбора ведущего или ведомого устройства (Device-0 и Device-1 согласно спецификации ATA) и метода адресации.

Биты 7 и 5 - зарезервированы.

Бит 6 - единичным значением указывает на применение режима адресации LBA. При нулевом значении бита используется режим CHS.

Бит 4 - DEV (Device) - выбор устройства. При DEV=0 выбрано устройство-0 (Master), при DEV=1 - устройство-1 (Slave).

Биты 3-0 имеют двоякое назначение в зависимости от выбранной системы адресации. В режиме CHS они содержат номер головки, в режиме LBA - старшие биты логического адреса.

4. Регистр данных может использоваться как 8-битный и 16-битный в зависимости от типа данных, передаваемых в текущей команде.

5. Регистр ошибок хранит состояние выполнения последней операции или диагностический код.

6. Регистр свойств (Features Register) - используется в зависимости от команды.

7. Регистр счетчика секторов содержит число секторов, участвующих в обмене. Нулевое значение соответствует 256 секторам.

Блок управляющих регистров используется для управления устройством и получения байта его состояния.

В состав блока входят альтернативный регистр состояния и регистр управления устройством.

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

В регистре управления устройством биты 7-3 зарезервированы, бит 0 всегда нулевой, используются только два бита:

Бит 2 - SRST (Software Reset) - программный сброс, действует все время, пока бит не будет сброшен. Оба устройства шины воспринимают программный сброс одновременно.

Бит 1 - IEN# (Interrupt Enable) - инверсный бит разрешения прерывания.

Адреса регистров контроллера устройства-0 определены в файле <linux/hdreg.h>: #define HD_DATA 0x1f0 /* регистр данных */ #define HD_ERROR 0x1f1 /* регистр ошибок */ #define HD_NSECTOR 0x1f2 /* регистр счетчика секторов */ #define HD_SECTOR 0x1f3 /* регистр стартового сектора */ #define HD_LCYL 0x1f4 /* регистр младшего байта номера цилиндра */ #define HD_HCY 0x1f5 /* регистр старшего байта номера цилиндра */ #define HD_CURRENT 0x1f6 /* 101dhhhh , d=устройство, hhhh=головка */ #define HD_STATUS 0x1f7 /* регистр состояния/команд */

3.2. Протокол взаимодействия

Обычный протокол взаимодействия хоста с устройством выглядит следующим образом:

1. Хост читает регистр состояния устройства, дожидаясь нулевого значения бита BSY.

2. Дождавшись освобождения устройства, хост записывает в регистр номера устройства и головки байт, у которого бит DEV указывает на адресуемое устройство.

3. Хост читает основной регистр состояния адресованного устройства, дожидаясь признака его готовности (DRDY = 1).

4. Хост заносит требуемые параметры в блок командных регистров.

5. Хост записывает код команды в регистр команд.

6. Устройство устанавливает бит BSY и переходит к исполнению команды.

Для команд, не требующих передачи данных (ND):

7. Завершив исполнение команды, устройство сбрасывает бит BSY и устанавливает запрос прерывания. К этому моменту в регистрах состояния и ошибок уже имеется информация о результате выполнения.

Для команд, требующих чтения данных в режиме PIO:

7. Подготовившись к передаче первого блока данных по шине АТА, устройство устанавливает бит DRQ. Если была ошибка, она фиксируется в регистрах состояния и ошибок. Далее устройство сбрасывает бит BSY и устанавливает запрос прерывания.

8. Зафиксировав обнуление бита BSY (или по прерыванию), хост считывает регистр состояния, что приводит к сбросу прерывания от устройства.

9. Если хост обнаружил единичное значение бита DRQ, он производит чтение первого блока данных в режиме PIO (адресуясь к регистру данных). Если обнаружена ошибка, считанные данные могут быть недостоверными.

После передачи блока данных возможно одно из следующих действий:

- если на шаге 8 ошибка не обнаружена, а требуется передача следующего блока, устройство устанавливает бит BSY, и данная последовательность повторяется с шага 7.

- если есть ошибка или передан последний блок данных, устройство сбрасывает бит DRQ и выполнение команды завершается.

Операцию записи на жесткий диск рассматривать не будем.

4. Доступ к жесткому диску через порты АТА контроллера

Задача прежняя - получить информацию идентификации устройства и считать MBR.

Рассмотрим программный код.

Заголовочные файлы: #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <linux/hdreg.h>

Для работы с портами ввода/вывода определим несколько макросов: #define OUT_P_B(val,port) \ asm( \ "outb %%al, %%dx" \ ::"a"(val),"d"(port) \ ) #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) \ )

Макрос OUT_P_B осуществляет запись байта в порт, макросы IN_P_B и IN_P_W - чтения байта/слова из порта.

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

Функция проверки статуса устройства (занято/свободно): void hd_busy() { unsigned char status; do { IN_P_B(status,HD_STATUS); } while (status & 0x80); return; }

Проверка статуса устройства осуществляется проверкой значения бита 7 (BSY) регистра состояния. Если бит сброшен, устройство свободно и регистры контроллера доступны.

Функция проверки готовности устройства к восприятию команд: void hd_ready() { unsigned char status; do { IN_P_B(status,HD_STATUS); } while (!(status & 0x40)); return; }

Устройство готово, если бит 6 (DRDY) регистра состояния установлен.

Функция проверки готовности устройства к обмену данными: int hd_data_request() { unsigned char status; IN_P_B(status,HD_STATUS); if(status & 0x8) return 1; return 0; }

Если бит 3 (DRQ) регистра состояния установлен, данные находятся в регистре данных и готовы для считывания.

Следующая функция проверяет, не произошла ли ошибка при работе устройства: void check_error() { unsigned char a; IN_P_B(a,HD_STATUS); if (a & 0x1) { perror("HD_STATUS"); exit(-1); } return; }

Установленный бит 0 (ERR) регистра состояния означает, то при выполнении последней операции произошла ошибка. Дополнительная информация содержится в регистре ошибок.

А теперь рассмотрим функцию получения информации идентификации устройства. void get_hd_identity(struct hd_driveid *hd) { unsigned short a = 0; int i = 0; unsigned short buff1[0x100]; memset(buff1,0,0x100);

В соответствии с протоколом взаимодействия проверяем статус устройства. Оно должно быть свободно: hd_busy();

Как только устройство освободилось, в регистр номера устройства и головки заносим значение 0xA0 (10100000 в двоичном виде). Бит 4 (DEV) равен 0, следовательно, нами выбрано ведущее устройство. Режим адресации в данном случае роли не играет, бит 6 оставим нулевым: OUT_P_B(0xA0,HD_CURRENT);

Ожидаем готовность устройства к восприятию команд: hd_ready();

Итак, устройство готово. В регистр команд (HD_STATUS) записываем код команды идентификации устройства - 0xEC. Данная команда выполняется в режиме PIO. Полный перечень команд смотрите в спецификации: OUT_P_B(0xEC,HD_STATUS);

В ответ на эту команду устройство установит бит DRQ и вернет блок данных, содержащих информацию идентификации. Для считывания информации организуем цикл: do { hd_busy(); check_error(); IN_P_W(a,HD_DATA); if((i>=10 && i<=19) || (i>=27 && i<=46)) asm( "xchgb %%ah, %%al" :"=a"(a) :"0"(a)); buff1[i++] = a; } while(hd_data_request());

Дождавшись освобождения устройства, при помощи функции check_error() читаем регистр состояния. При этом мы сбрасываем прерывание от устройства и проверяем, не произошла ли ошибка. Затем считываем из регистра данных значение. Считывание производим до тех пор, пока установлен бит DRQ. Как только будет передан последний блок данных, устройство этот бит сбросит. Считанную информацию сохраним в буфере buff1.

Копируем полученную информацию из буфера buff1 в структуру struct hdreg hd: memcpy(hd,(struct hdreg *)buff1,0x100);

Очищаем буфер и выходим: memset(buff1,0,0x100); return; }

Следующая функция осуществляет чтение сектора в режиме адресации CHS. void read_hd_sector_chs(unsigned short N, unsigned short s_sect, unsigned short s_cyl, unsigned short head, unsigned short *buff) { int i = 0; unsigned short a; if((!N) || (!s_sect)) return;

Аргументы функции: N - число секторов для чтения s_sect - стартовый сектор s_cyl - стартовый цилиндр head - номер головки buff - буфер, куда все помещается

Ожидаем освобождения устройства: hd_busy();

В регистр номера устройства и головки заносим соответствующие данные. Бит 6 сброшен, что указывает на режим адресации CHS: OUT_P_B(0xA0|head,HD_CURRENT);

Ждем готовность устройства к приему команд: hd_ready();

В блок командных регистров заносим требуемые параметры: OUT_P_B(N,HD_NSECTOR); OUT_P_B(s_sect,HD_SECTOR); OUT_P_B(s_cyl,HD_LCYL); OUT_P_B((s_cyl >> 8),HD_HCYL);

В регистр команд записываем код команды чтения секторов с повторами - 0x20. Данная команда выполняется в режиме PIO: OUT_P_B(0x20,HD_STATUS);

Считываем блок данных в буфер buff: do { hd_busy(); check_error(); IN_P_W(a,HD_DATA); buff[i++] = a; } while(hd_data_request());

Считываем последние 4 байта и выходим из функции: IN_P_W(a,HD_DATA); buff[i++] = a; IN_P_W(a,HD_DATA); buff[i] = a; return; }

Функция чтения сектора в режиме адресации LBA. void read_hd_sector_lba(unsigned short N, unsigned int lba, unsigned short *buff) { int i = 0; unsigned short a; if(!N) return;

Аргументы функции: N - число секторов для чтения lba - номер блока buff - буфер, куда все помещается

Ожидаем освобождения устройства: hd_busy();

Спецификацией АТА-2 в режиме LBA предусмотрен 28-битный адрес сектора размером 512 байт, при этом максимальный объем ограничивается значением 0,5 терабайт.

В регистре номера устройства и головки бит 6 устанавливаем в 1, а биты 3-0 будут содержать старшие биты логического адреса (27-24): OUT_P_B(0xE0|((lba & 0x0F000000) >> 24),HD_CURRENT);

Ожидаем готовность устройства к приему команд: hd_ready();

В блок командных регистров заносим требуемые параметры: OUT_P_B(N,HD_NSECTOR);

В регистр номера сектора заносим биты 7-0 логического адреса: OUT_P_B((lba & 0x000000FF),HD_SECTOR);

В регистр младшего байта номера цилиндра - биты 15-8 логического адреса: OUT_P_B(((lba & 0x0000FF00) >> 8),HD_LCYL);

В регистр старшего байта номера цилиндра - биты 23-16 логического адреса: OUT_P_B(((lba & 0x00FF0000) >> 16),HD_HCYL);

В регистр команд - команду чтения секторов с повторами: OUT_P_B(0x20,HD_STATUS);

Получаем результат: do { hd_busy(); check_error(); IN_P_W(a,HD_DATA); buff[i++] = a; } while(hd_data_request());

Считываем последние 4 байта и выходим: IN_P_W(a,HD_DATA); buff[i++] = a; IN_P_W(a,HD_DATA); buff[i] = a; return; }

Рассмотрим главную функцию: int main () {

Определим необходимые структуры и переменные: struct hd_driveid hd; int out; unsigned short N = 1; unsigned int sect, cyl, head, lba; /* * N - число секторов для чтения * sect - номер сектора * cyl - номер цилиндра * head - номер головки * lba - номер логического блока * */ unsigned short buff[0x100*N]; memset(buff,0,0x100*N); memset(&hd,0,sizeof(struct hd_driveid));

Чтобы не схлопотать Segmentation fault, запросим у системы разрешения доступа к портам в диапазоне 0x1f0 - 0x1f7: ioperm(0x1f0,8,1);

Вызовем функцию получения информации идентификации. Результат будет помещен в структуру struct hd_driveid hd: get_hd_identity(&hd);

Отобразим результаты: printf("Серийный номер - %s\n",hd.serial_no); printf("Модель - %s\n",hd.model); printf("Число цилиндров - %d\n",hd.cur_cyls); printf("Число головок - %d\n",hd.cur_heads); printf("Число секторов - %d\n",hd.cur_sectors); printf("Число логических блоков - %d\n",hd.lba_capacity);

А теперь прочитаем первый сектор устройства (MBR) в режиме CHS: sect = 1; cyl = 0; head = 0; read_hd_sector_chs(N,sect,cyl,head,buff);

Запишем в файл результат: out=open("sect_chs", O_CREAT|O_RDWR, 0600); write(out,(unsigned char *)buff,0x200*N); close(out);

Тоже самое - в режиме LBA: lba = 0; read_hd_sector_lba(N,lba,buff); out=open("sect_lba", O_CREAT|O_RDWR, 0600); write(out,(unsigned char *)buff,0x200*N); close(out); ioperm(0x1f0,8,0); return (0); }

Весь вышеприведенный код сохраним в файле disk.c. Исполняемый модуль получим, введя команду: gcc -o disk disk.c

Работоспособность кода была проверена для ОС Linux, версия ядра 2.4.20.

Литература.

1. Теренс Чан. Системное программирование на С++ для UNIX: Пер. с англ. - К.: Издательская группа BHV, 1999. - 592 с.
2. Гук М. Интерфейсы ПК: справочник - СПб: Питер Ком, 1999 - 416 с.

Впервые статья опубликована в журнале "Системный администратор"
http://www.samag.ru