Драйверы устройств в Linux Часть 14: Исследуем жесткий диск и разбираемся с его разделамиИсточник: rus-linux
Эта статья, которая является частью серии статей о драйверах устройств в Linux, познакомит вас с внутренним устройством жесткого диска. "Разве это не тема для изучения инженерами-механиками: конструкция жесткого диска"? - с сомнением спросила Светлана. "Да, это так. Но понимание этой темы позволит нам учитывать особенности диска при его программировании" - резонно ответил Пагс, ожидая начала семинара, посвященного системам хранения данных. Семинар начался с того, что в руках у ведущего оказались несколько жестких дисков, а затем он перешел к системе и стал показывать результат работы команды fdisk -l (рис. 1). Рис.1: Список разделов, выдваемый командой fdisk В первой строке в удобочитаемом формате выдается размер жесткого диска в байтах. Во второй строке сообщается о количестве логических головок, логических секторов на дорожке и фактическом числе цилиндров на диске - о том, что, как известно, называется геометрией диска. 255 головок указывают на количество пластин или физических поверхностей, поскольку для каждого физической поверхности диска требуется отдельная головка чтения-записи. Давайте их перенумеруем, скажем, D1, D2, ..., D255. Теперь на каждом диске будет одно и то же количество концентрических круговых дорожек, которые будут подсчитываться начиная с внешней дорожки - к внутренней. В приведенном выше случае на диске имеется 60801 таких дорожек. Давайте их перенумеруем, скажем, T1, T2, …, T60801. Дорожки с одним и тем же номером, находящиеся на всех дисках, образовывают цилиндр с таким же номером. Например, дорожки T2 на дисках D1, D2, …, D255 будут вместе образовывать цилиндр С2. Теперь, на каждой дорожке есть одно и тоже количество логических секторов - 63 в нашем случае, скажем, S1, S2, …, S63. А размер каждого сектора, как правило, составляет 512 байтов. Учитывая эти данные, можно с помощью следующей формулы подсчитать общий полезный размер жесткого диска: Usable hard disk size in bytes = (Number of heads or disks) * (Number of tracks per disk) * (Number of sectors per track) * (Number of bytes per sector, i.e. sector size) Для диска, который мы рассматриваем, это будет 255 * 60801 * 63 * 512 байтов = 500105249280 байтов Обратите внимание, что эта цифра может быть немного меньше, чем фактический размер жесткого диска (500107862016 байтов, в нашем случае). Причина этого в том, что в формуле не учитываются байты в последнем частичном или неполно сформированном цилиндре. Это связано с различием между современными технологиями организации реальной физической геометрии диска и традиционным представлением его геометрии с использованием головок, цилиндров и секторов. Отметим, что в данных, выдаваемых командой fdisk, указываются логические, а не физические дорожки и сектора дорожек. Кто-то может спросить, почему, если у современных дисков нет такого понятия, как физическая геометрия, для чего в них все еще поддерживается представление в логической форме? Основная причина состоит в возможности продолжать использовать одну и ту же концепцию разбиения диска на разделы и поддерживать один и тот же формат таблицы разделов, в частности для наиболее распространенных таблиц разделов типа DOS, которые сильно зависят от этой упрощенной геометрии. Обратите внимание на подсчитанный в третьей строке размер цилиндра (255 головок * 63 секторов / на дорожке * 512 байтов / в секторе = 8225280 байтов), а затем то, что разбиение на разделы выполняется в единицах полных цилиндров. Таблицы разделов типа DOSЭто подводит нас к рассмотрению следующего важного вопроса: таблиц разделов типа DOS. Но, во-первых, что такое раздел, и почему мы должны разбивать диск на разделы? На жестком диске можно определить один или нескольких логических дисков, каждый из которых называется разделом. Это удобно для раздельной организации хранения различных типов данных, например, данных для другой операционной системы, пользовательских данных, временных данных и т.д. Итак, деление жесткого диска на разделы является, главным образом, логическим и для его поддержки требуются метаданные, которые представлены в виде таблицы разделов. В таблице разделов типа DOS имеется четыре записи о разделах, длина каждой записи - 16-байтов. Каждую из этих записей можно на языке C представить в виде следующей структуры: typedef struct { unsigned char boot_type; // 0x00 - Inactive; 0x80 - Active (Bootable) unsigned char start_head; unsigned char start_sec:6; unsigned char start_cyl_hi:2; unsigned char start_cyl; unsigned char part_type; unsigned char end_head; unsigned char end_sec:6; unsigned char end_cyl_hi:2; unsigned char end_cyl; unsigned long abs_start_sec; unsigned long sec_in_part; } PartEntry; Эта таблица разделов, которая завершается двухбайтовой сигнатурой 0xAA55, находится в конце первого сектора диска, который обычно называется главной загрузочной записью (Master Boot Record - MBR). Так что смещение для этой таблице разделов внутри MBR будет следующим 512 - (4 * 16 + 2) = 446. А 4-байтовая сигнатура диска будет размещена со смещением 440. Остальные верхние 440 байтов записи MBR, как правило, используются для размещения первой части загрузочного кода, который при загрузке системы с диска загружается с помощью BIOS. В листинге part_info.c показаны все эти разнообразные определения, а также приводится код, используемый при анализе и выдаче в форматированном виде содержимого таблицы разделов. Из анализа структуры записи таблицы разделов становится понятно, что для полей начального и конечного цилиндров отводится всего по 10 битов, что позволяет использовать только 1023 цилиндра. Но для современных громадных жестких дисков этого совсем недостаточно. Поэтому, в случае переполнения, для соответствующей тройки <головка, цилиндр, сектор> в записи таблицы разделов указываются максимальные значения, а фактические значения вычисляется с использованием последних двух полей: абсолютного номера начального сектора (abs_start_sec) и количество секторов этого раздела (sec_in_part). Этот код также есть в файле part_info.c: #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define SECTOR_SIZE 512 #define MBR_SIZE SECTOR_SIZE #define MBR_DISK_SIGNATURE_OFFSET 440 #define MBR_DISK_SIGNATURE_SIZE 4 #define PARTITION_TABLE_OFFSET 446 #define PARTITION_ENTRY_SIZE 16 // sizeof(PartEntry) #define PARTITION_TABLE_SIZE 64 // sizeof(PartTable) #define MBR_SIGNATURE_OFFSET 510 #define MBR_SIGNATURE_SIZE 2 #define MBR_SIGNATURE 0xAA55 #define BR_SIZE SECTOR_SIZE #define BR_SIGNATURE_OFFSET 510 #define BR_SIGNATURE_SIZE 2 #define BR_SIGNATURE 0xAA55 typedef struct { unsigned char boot_type; // 0x00 - Inactive; 0x80 - Active (Bootable) unsigned char start_head; unsigned char start_sec:6; unsigned char start_cyl_hi:2; unsigned char start_cyl; unsigned char part_type; unsigned char end_head; unsigned char end_sec:6; unsigned char end_cyl_hi:2; unsigned char end_cyl; unsigned long abs_start_sec; unsigned long sec_in_part; } PartEntry; typedef struct { unsigned char boot_code[MBR_DISK_SIGNATURE_OFFSET]; unsigned long disk_signature; unsigned short pad; unsigned char pt[PARTITION_TABLE_SIZE]; unsigned short signature; } MBR; void print_computed(unsigned long sector) { unsigned long heads, cyls, tracks, sectors; sectors = sector % 63 + 1 /* As indexed from 1 */; tracks = sector / 63; cyls = tracks / 255 + 1 /* As indexed from 1 */; heads = tracks % 255; printf("(%3d/%5d/%1d)", heads, cyls, sectors); } int main(int argc, char *argv[]) { char *dev_file = "/dev/sda"; int fd, i, rd_val; MBR m; PartEntry *p = (PartEntry *)(m.pt); if (argc == 2) { dev_file = argv[1]; } if ((fd = open(dev_file, O_RDONLY)) == -1) { fprintf(stderr, "Failed opening %s: ", dev_file); perror(""); return 1; } if ((rd_val = read(fd, &m, sizeof(m))) != sizeof(m)) { fprintf(stderr, "Failed reading %s: ", dev_file); perror(""); close(fd); return 2; } close(fd); printf("\nDOS type Partition Table of %s:\n", dev_file); printf(" B Start (H/C/S) End (H/C/S) Type StartSec TotSec\n"); for (i = 0; i < 4; i++) { printf("%d:%d (%3d/%4d/%2d) (%3d/%4d/%2d) %02X %10d %9d\n", i + 1, !!(p[i].boot_type & 0x80), p[i].start_head, 1 + ((p[i].start_cyl_hi << 8) / p[i].start_cyl), p[i].start_sec, p[i].end_head, 1 + ((p[i].end_cyl_hi << 8) / p[i].end_cyl), p[i].end_sec, p[i].part_type, p[i].abs_start_sec, p[i].sec_in_part); } printf("\nRe-computed Partition Table of %s:\n", dev_file); printf(" B Start (H/C/S) End (H/C/S) Type StartSec TotSec\n"); for (i = 0; i < 4; i++) { printf("%d:%d ", i + 1, !!(p[i].boot_type & 0x80)); print_computed(p[i].abs_start_sec); printf(" "); print_computed(p[i].abs_start_sec + p[i].sec_in_part - 1); printf(" %02X %10d %9d\n", p[i].part_type, p[i].abs_start_sec, p[i].sec_in_part); } printf("\n"); return 0; } Как уже ранее объяснялось для приложений, этот код можно скомпилировать с помощью команды gcc part_info.c -o part_info, а затем, чтобы увидеть информацию о ваших первичных разделах диска /dev/sda, запустить с помощью команды ./part_info /dev/sda. На рис.2 показан результат работы этой команды в системе у преподавателя, ведущего семинар. Сравните его с результатом работы команды fdisk , приведенным на рис.1. Рис.2: Результат работы команды ./part_info Типы разделов и загрузочные записиПоскольку в таблице разделов жестко заданы только четыре записи, то максимальное количество разделов, которое у вас может быть, равно четырем. Это так называемые первичные разделы, для каждого из которых в соответствующей записи таблицы разделов указывается тип. Эти типы, как правило, придуманы различными поставщиками ОС, и поэтому, то, как они отображаются и используются в различных операционных системах, например, в DOS, Minix, Linux, Solaris, BSD, FreeBSD, QNX, W95, Novell Netware и т. д., будет зависеть от конкретной ОС. Однако, это больше формальность, чем реальная необходимость. Кроме того, один из четырех первичных разделов может быть помечен как так называемый расширенный раздел, который используется по-особенному. Как следует из названия, он используется для дальнейшего разбиения жесткого диска на разделы, т. е. в нем есть еще разделы. Это так называемые логические разделы, которые создаются внутри расширенного раздела. Метаданные для них хранятся в формате связного списка, что позволяет иметь неограниченное число логических разделов (по крайней мере, теоретически). В этом случае первый сектор расширенного раздела, обычно называемый загрузочной записью (Boot Record - BR), используется точно также как и MBR, для хранения (головы связного списка) таблицы логических разделов. Последующие узлы связного списка, в которых хранятся первые сектора последующих логических разделов, называются логическими загрузочными записями (Logical Boot Record - LBR). Каждый узел связного списка является полной таблицей, содержащей 4 записи, хотя используются только первые две записи: первая - для связанных в список данных, а именно информации, непосредственно касающейся логического раздела, а вторая - в качестве указателя на следующий элемент связного списка и указывает на остальные логические разделы. Чтобы сравнить и разобраться с конкретными особенностями разбиения жесткого диска в вашей системе на первичные разделы, выполните указанные ниже действия (в роли пользователя root, т.е, аккуратно): ./part_info /dev/sda ## Displays the partition table on /dev/sda fdisk -l /dev/sda ## To display and compare the partition table entries with the above В случае, если у вас несколько жестких дисков (/dev/sdb, …), причем файлы дисковых устройств имеют другие имена (/dev/hda, …), либо у вас расширенный раздел, то в этом случае используйте команду ./part_info <имя_файла_устройства>. Попытка выполнить эту команду для расширенного раздела даст вам информацию о начальной таблице логических разделов. Теперь мы можем аккуратно и с особой осторожностью поэкспериментировать с жестким диском системы (только чтение). Почему с осторожностью? Поскольку в противном случае, мы можем сделать нашу систему незагружаемой. Но обучение никогда не обходится без полного исследования. Поэтому на нашем следующем занятии мы создадим фиктивный диск в оперативной памяти и на нем будем проводить деструктивные исследования. |