Добавить в корзинуПозвонить
Найти в Дзене
Триалогия

Пишем операционную систему Триалогия - AHCI SATA через DMA

IDE сам посылает каждое слово через процессор. AHCI, это современный противовес, и разница принципиальна. Вместо того чтобы говорить через порты и обрабатывать каждый байт, процессор здесь записывает команды как структуры данных в памяти, а диск забирает их оттуда и пишет свой ответ назад в RAM через DMA. Процессор отдаёт запрос и затем свободен. Три вещи отличают AHCI от IDE. Сверху AHCI всё же взаимозаменяем с IDE, оба имеют один интерфейс ops из слоя Storage. uint8_t sata_read(uint32_t port_base, void* cmd_list, uint32_t sector, uint8_t n, void* memory) { return ahci_send_ata_command(port_base, cmd_list, AHCI_READ, 0x25, n, sector, memory, 512 * n); // 0x25 = READ DMA EXT } Вот и весь код обёртки для чтения данных. Работа происходит под ней, в структурах. AHCI связывает три структуры в RAM. Регистры порта (через MMIO) указывают через CLB на command list, список до 32 заголовков. Здесь используется лишь entry 0, и он указывает на command table. Она содержит само сердце command FI
Оглавление
Триалогия - Пишем операционную систему
Триалогия - Пишем операционную систему

AHCI - SATA через DMA

IDE сам посылает каждое слово через процессор. AHCI, это современный противовес, и разница принципиальна. Вместо того чтобы говорить через порты и обрабатывать каждый байт, процессор здесь записывает команды как структуры данных в памяти, а диск забирает их оттуда и пишет свой ответ назад в RAM через DMA. Процессор отдаёт запрос и затем свободен.

Что AHCI делает иначе

Три вещи отличают AHCI от IDE.

  • Во-первых, он говорит не через порты I/O, а через Memory-Mapped I/O (mmio_read32/mmio_write32 по базовому адресу).
  • Во-вторых, команды лежат не в регистрах, а в структурах данных в RAM.
  • В-третьих, и это суть, передача данных идёт через DMA. Диск сам обращается к памяти.

Сверху AHCI всё же взаимозаменяем с IDE, оба имеют один интерфейс ops из слоя Storage.

uint8_t sata_read(uint32_t port_base, void* cmd_list, uint32_t sector, uint8_t n, void* memory) {

return ahci_send_ata_command(port_base, cmd_list, AHCI_READ,

0x25, n, sector, memory, 512 * n); // 0x25 = READ DMA EXT

}

Вот и весь код обёртки для чтения данных. Работа происходит под ней, в структурах.

Структуры AHCI

Триалогия - Структуры AHCI в памяти
Триалогия - Структуры AHCI в памяти

AHCI связывает три структуры в RAM. Регистры порта (через MMIO) указывают через CLB на command list, список до 32 заголовков. Здесь используется лишь entry 0, и он указывает на command table. Она содержит само сердце command FIS, который несёт команду ATA, опционально команду ATAPI, и PRDT, которая говорит, куда должны идти данные через DMA. Вот как всё это строится:

// command list entry 0: указывает на command table

struct ahci_command_list_entry_t* e = command_list_memory;

e->command_fis_length_in_dwords = 5;

e->write = command_direction; // направление

e->number_of_command_table_entries = 1; // ровно 1 PRD

e->command_table_low_memory = dma_phys(command_list_memory + 0x1000); // ФИЗИЧЕСКИЙ!

// command table: H2D register FIS (сама команда ATA)

struct ahci_command_and_prd_table_t* t = command_list_memory + 0x1000;

t->fis_type = 0x27; // Host-to-Device register FIS

t->flags = 0x80; // бит «command»

t->command = command; // 0x25 READ / 0x35 WRITE

t->lba_0 = lba & 0xFF; t->lba_1 = (lba >> 8) & 0xFF; // LBA по нескольким полям

t->sector_count_low = sector_count & 0xFF;

FIS (Frame Information Structure) это SATA-двойник последовательности портов из статьи про IDE - та же команда, тот же LBA, то же число секторов, только в структуре, а не в отдельных записях в порты.

DMA вместо PIO

Триалогия - DMA против PIO
Триалогия - DMA против PIO

Теперь суть. PRDT (Physical Region Descriptor Table) говорит диску, куда положить данные через DMA. И вот решающий момент, это должен быть физический адрес, ведь контроллер DMA не знает виртуальных адресов, он видит настоящий RAM.

phyaddr data_phys = dma_phys(dma_target); // перевести виртуальный -> физический

t->data_base_low_memory = data_phys;

t->data_byte_count = byte_count - 1;

Пока IDE перелопачивает каждое из 256 слов (2 bytes) сектора через процессор, AHCI лишь даёт диску этот адрес назначения и "стартовый выстрел", и тогда данные текут прямо с диска в RAM мимо процессора. Это быстрее и разгружает процессор, но имеет свою цену. Надо иметь дело с физическими адресами и следить, чтобы целевая память была непрерывной. Об этом сейчас подробнее.

Команды AHCI

Команда AHCI
Команда AHCI

Отправить команду затем почти буднично очистить регистр ошибок, выставить бит command-issue и ждать, пока он снова не упадёт в ноль.

// очистить регистр ошибок

mmio_write32((void*)(port_base + AHCI_PORT_IS), 0xFFFFFFFF);

// «вперёд»: command issue, слот 0

mmio_write32((void*)(port_base + AHCI_PORT_CI), 0x01);

while (/* тайм-аут через per-CPU ticks */) {

if ((mmio_read32((void*)(port_base + AHCI_PORT_CI)) & 0x1) == 0)

return true; // бит CI упал в ноль -> команда готова

if (mmio_read32((void*)(port_base + AHCI_PORT_IS)) & 0x40000000)

return false; // бит ошибки -> прервать (и перезапустить command list)

}

Здесь тоже опрос, а не ожидание прерывания, тот же стиль, что у IDE. Если случается ошибка, command list надо остановить и перезапустить (стартовый бит ST ненадолго выключается и снова включается), иначе порт больше не принимает новых команд.

Ловушка DMA - bounce buffer

И вот самое поучительное место всего драйвера. Этот код использует ровно одну запись PRD, а это значит целевая область должна лежать физически непрерывно. Звучит безобидно, но это не так.

// Буфер может быть непрерывен виртуально и всё же пересекать границу страницы.

// Тогда две физические страницы могут НЕ быть соседними, контроллер DMA

// читает/пишет мусор.

bool cross_page = (buf_start & ~PAGE_MASK) != (buf_end & ~PAGE_MASK);

if (cross_page) {

bounce_buffer = aligned_kmalloc(PAGE_SIZE, PAGE_SIZE); // 1 физ. страница = точно непрерывна

if (command_direction == AHCI_WRITE)

memcpy(bounce_buffer, memory, byte_count);

dma_target = bounce_buffer; // DMA идёт через bounce buffer

}

// ... после READ: memcpy(memory, bounce_buffer, byte_count) ...

Ловушка, что здесь таится, буфер, который красиво лежит непрерывно в виртуальном адресном пространстве, может быть разбросан по двум совсем разным страницам в физической памяти, как только он пересекает границу страницы. Процессор этого никогда не замечает, ведь пейджинг прячет разрыв. Но контроллер DMA видит физическую память, и если вторая страница лежит в другом месте, он пишет вторую половину данных не туда. В VirtualBox это сразу всплывало как порча данных, QEMU был терпимее и проблема не вылезала даже в дебагере. Решение - это page-aligned bounce buffer (одна физическая страница непрерывна по определению), через который затем идёт передача.

Честное место

Этот драйвер AHCI намеренно прост. Один PRD, один слот команды, опрос вместо прерываний. Проработанный AHCI драйвер умеет куда больше, много PRD для разбросанной памяти, 32 команды в очереди сразу, завершение по прерыванию. Мой драйвер не использует ничего из этого и именно поэтому ему нужен bounce buffer как костыль для случая пересечения страницы. Это работает, это понятно, и для системы в том виде как сейчас этого хватает, но это упрощённый взгляд на по сути мощный интерфейс.

Что дальше

На этом весь путь от приложения вниз до физического диска пройден: VDS, VFS, абстрактный слой, раздел (Partition), Storage и с IDE и AHCI, два способа, как секторы действительно читаются. Чего пока не хватало, это вопроса, что же собственно лежит на этих секторах. Это решает последняя часть, сами конкретные файловые системы, TriaFS, FAT, EXT и только читаемые ISO9660 и CDDA, и как каждая из них упорядочивает байты в файлы и каталоги.

Было бы интересно увидеть ваши комментарии и улучшить статьи.

Предыдущая статья Содержание Следующая статья

*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением