Внутреннее устройство ОС RT-11: копаемся в исходом коде. Часть первая

от автора

RT-11 — это операционная система из 1970-х годов для популярного в то время мини-компьютера PDP-11 фирмы DEC. В СССР известна под именами Электроника 60, ДВК, БК 0011М. Для тех, кто любит изучать чужой код в поисках инженерной эстетики — дальнейшее изложение.

Границы исследования

Ядро RT-11 поставлялось в виде исходного кода, которое предполагается с помощью утилиты sysgen настроить и перекомпилировать под конкретное железо и возможности — знакомый сценарий для тех, кто пересобирал ядро Linux. Совпадение не случайно: Unix тоже зародилась в компании DEC.

Благодаря этому исходный код ядра доступен для изучения, но, к сожалению, он написан на ассемблере и все комментарии вырезаны. Из-за чего можно было бы бросить дело изучения как бесперспективное. Однако выяснилось, что комментариями к коду являются следующие книги:

  1. RT-11 Software Support Manual

  2. RT–11 System Internals Manual

  3. RT-11 Programmer’s Reference Manual

В них описаны структуры данных, базовые алгоритмы функционирования, а также аргументы и назначение системных вызовов. А еще объем кода невелик — около 2 тыс. строк на файл.

Состав ядра:

  1. RMON — резидентный монитор, постоянно находится в оперативной памяти.

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

  3. Драйверы устройств.

  4. KMON — интерпретатор командной строки для интерфейса с пользователем. При запуске программ тоже выгружается из памяти.

Резидентный монитор существует в нескольких вариантах: однозадачном, двухзадачном и с расширенной памятью. Мы рассмотрим однозадачный вариант в части асинхронного ввода-вывода и двухзадачный вариант в части реализации многозадачности. Из драйверов возьмем только драйвер дисковода. KMON оставим за скобками.

Примеры (псевдо)кода будут в виде, переведенном на язык Си, потому что он создан для PDP-11. В начале 70-х памяти было мало, ее старательно экономили, и поэтому длину идентификаторов ужимали до шести символов. Не удивляйтесь малопонятным шестибуквенным сокращениям в коде. Интересно отметить, что в стандартной библиотеке Си короткие идентификаторы дожили до наших дней.

Итак, в первой части расскажем про асинхронный ввод-вывод. Во второй части — про файловую систему. В третьей — про многозадачность.

Тема 1. Асинхронный ввод-вывод

PDP-11 — компьютер однопроцессорный и однопоточный. Однако периферия типа жесткого диска может выполнять операции ввода-вывода без участия центрального процессора и сигнализирует о завершении через механизм прерываний. Поэтому в RT-11 функция QMANGR лишь добавляет запрос операции ввода-вывода в очередь драйвера устройства. Аппаратное прерывание при завершении операции вызывает обработчик в драйвере, который вызывает функцию QCOMP, чтобы проинформировать программу о готовности данных.

Запуск операции может произойти в двух местах:

  • QMANGR — если очередь была пуста (устройство не занято).

  • QCOMP — если устройство освободилось.

Такой подход удобен, когда RT-11 работает в многозадачном режиме: центральный процессор не простаивает, когда одна программа ожидает результат ввода-вывода, а переключается на выполнение задачи с меньшим приоритетом (привет node.js).

Структуры данных

В памяти монитора выделено место для элементов очереди операций ввода-вывода типа QueueElement. Первоначально память только на один элемент, но при помощи системного вызова QSET можно увеличить размер очереди. В переменной AVAIL хранится указатель на первый свободный элемент.

// Структура элемента очереди операций ввода-выводаtypedef struct QueueElement {    // Q.LINK - указатель на следующий элемент    struct QueueElement *link;    // Q.CSW - указатель на запись канала    Channel *csw;    // Q.BLKN - номер блока на устройстве    uint16_t block_number;    // Q.FUNC - операция (0 = чтение/запись и т.д.)    uint8_t func;    // Q.UNIT - номер устройства (например, 1 в названии DX1)    uint8_t unit;    // Q.BUFF - адрес буфера (для чтения или записи)    uint16_t *buffer;    // Q.WCNT - количество слов (которые нужно прочитать или записать)    int16_t word_count;    // Q.COMP - функция, которую нужно вызвать после завершения операции    void (*completion)(void);} QueueElement;// очередь из одного элементаQueueElement QSTART;QueueElement* AVAIL = &QSTART;int16_t QCNT = 1;int16_t QSIZE = 1;

Для работы с файлом необходим дескриптор файла, который называется Channel:

typedef struct {    // C.CSW - Channel Status Word    uint16_t csw;    // C.SBLK - номер первого блока файла (0 если не файл)    uint16_t start_block;    // C.LENG - длина файла в блоках (если открыт через .LOOKUP)    // или размер свободного места (если открыт через .ENTER)    uint16_t length;    // C.USED - наибольший записанный блок    uint16_t used_block;uint16_t unused_field;    // C.DEVQ - количество ожидающих запросов ввода/вывода    uint8_t device_queue;    // C.UNIT - номер устройства    uint8_t unit;    // C.SIZ - размер блока в байтах    uint16_t block_size;} Channel;

Непосредственно ввод-вывод осуществляет драйвер устройства:

typedef struct DeviceQueue {    // последний элемент в очереди (LQE)    QueueElement *last;    // первый элемент в очереди (CQE)    QueueElement *first;    // обработчик, который берет в работу очередной элемент очечеди (CQE)void (*start)(struct DeviceQueue*);    // ... остальные поля драйвера    // interrupt_vector, interrupt_handler, stat} DeviceQueue;

QMANGR: постановка в очередь

Вызывается внутри монитора при выполнении системного вызова. Забирает свободный элемент, заполняет его параметрами операции и добавляет в очередь драйвера.

/** * @param block - номер блока на диске * @param device - указатель на драйвер устройства * @param channel - указатель на CSW (Channel Status Word) - запись канала * @param buffer - адрес буфера * @param word_count - длина читаемого/записываемого в словах * @param unit - номер устройства * @param completion - функция завершения * @param is_async - асинхронный ввод-вывод */void QMANGR(uint16_t block, DeviceQueue *device, Channel *channel,     uint16_t *buffer, uint16_t word_count, uint8_t unit,void (*completion)(void), bool is_async){    QueueElement *current_elem;        // Ожидаем появления свободного элемента в очереди    do {        INTON();        INTOFF();    } while (QCNT <= 0);        // Забираем один элемент из списка свободных    QCNT--;    current_elem = AVAIL;    AVAIL = current_elem->link;        INTON();        // Заполняем элемент очереди    current_elem->link = NULL;    current_elem->csw = channel;        // Если уже слишком много запросов, то подождем их завершения    while (channel->device_queue == 255) {        // пустой цикл ожидания// устройство на аппаратном уровне вызывает прерывание и срабатывает его обработчик// который уменьшит device_queue// поэтому цикл не бесконечный    }    // Увеличиваем счётчик запросов в канале    channel->device_queue++;        // Продолжаем заполнение    QueueElement *fill_ptr = current_elem;    fill_ptr->block_number = block;    fill_ptr->func = 0; // рассмотриваем только функции чтения/записи    fill_ptr->unit = unit;    fill_ptr->buffer = buffer;    fill_ptr->word_count = word_count;    fill_ptr->completion = completion;        INTOFF();        // Помещаем элемент в очередь драйвераQueueElement *first_elem = device->first;if (first_elem != NULL) {// Очередь не пуста - добавляем в конецdevice->last->link = current_elem;device->last = current_elem;} else {// Очередь пуста - элемент становится и первым, и последнимdevice->first = current_elem;device->last = current_elem;// Запускаем обработку очередного элементаdevice->start(device);}        INTON();        // Если операция синхронная, то ожидаем завершения    if (!is_async) {        // Ожидаем, пока счётчик запросов не станет 0        while (channel->device_queue != 0) {            // пустой цикл ожидания        }    }}

QCOMP: завершение операции

Вызывается из драйвера в обработчике прерывания. Она:

  1. Проверяет наличие ошибки оборудования.

  2. Обновляет счётчики канала.

  3. Удаляет элемент из очереди драйвера.

  4. Возвращает элемент в список свободных.

  5. Вызывает функцию завершения, если необходимо.

  6. Запускает следующий запрос в очереди (если есть).

/** * @param device - указатель на драйвер устройства */void QCOMP(DeviceQueue *device){    QueueElement *qe = device->first;    int channel_num;    void (*comp)(void);        channel = qe->csw;        // Проверяем бит ошибки оборудования в статусе канала    if (channel->csw & HARD_ERROR_BIT) {        // HALT - останов процессора        exit(EXIT_FAILURE);    }           // Уменьшаем счётчик запросов в канале    channel->device_queue--;        // Сохраняем состояние процессора    GETPSW();    INTOFF();        // Удаляем элемент из очереди обработчика    // Проверяем, есть ли следующий элемент    if (qe->link != NULL) {        // Очередь не пуста - обновляем CQE        device->first = qe->link;    } else {        // Очередь пуста - очищаем LQE и CQE        device->first = NULL;device->last = NULL;    }        // Возвращаем элемент в список свободных    qe->link = AVAIL;    AVAIL = qe;    QCNT++;        // Получаем код завершения (COMP)    comp = qe->completion;        // Восстанавливаем состояние процессора    PUTPSW();  // включает INTON    // Вызываем функцию завершенияif (comp != NULL) {comp();}        // Если есть следующий элемент в очереди    // то запускаем обработку очередного элемента    if (device->first != NULL) {        device->start(device);    }}

В качестве примеров использования этого фреймворка далее идет рассказ про драйвер дисковода и системный вызов .READ.

Тема 2. Драйвер дисковода DX (реализация)

Давайте посмотрим на примере кода драйвера дисковода DX, вариант реализации операции чтения/записи. Документацию взаимодействия с контроллером можно посмотреть здесь. Исходный код драйвера с авторскими комментариями доступен здесь.

Кратко про алгоритм чтения сектора. Размер сектора равен 128 байтам. На следующие два адреса отображаются порты ввода-вывода устройства DX:

// порт для передачи команд и чтения статуса#define RX_CS 0177170// порт для приема и передачи данных#define RX_DB 0177172
  1. В регистр команд RX_CS нужно записать команду CS_READ.

  2. В регистр данных RX_DB нужно записать номер сектора.

  3. В RX_DB нужно записать номер дорожки.

  4. Подождать, пока на аппаратном уровне контроллер прочитает сектор с диска в свой буфер.

  5. В RX_CS нужно записать команду CS_EMPTY_BUF.

  6. Из RX_DB нужно последовательно 128/2=64 раз прочитать значение (контроллер последовательно слово за словом передает содержимое своего буфера).

Функция DXSTRT запускает операцию. По сути она копирует аргументы из элемента очереди в локальные переменные.

// интерфейс драйвераDeviceQueue dx_driver = { .start = DXSTRT };// адрес буфераuint16_t* BUFFER_ADDR;// логический номер сектораuint16_t RX_LSN;// код функцииint16_t RX_FUNC2;// количество байт для чтения/записиuint16_t BYTE_COUNT;// счетчик прерыванийbool is_first_int;void DXSTRT(DeviceQueue* device){    QueueElement *cqe = device->first;        int16_t oper = CS_GO;    // кол-во слов, положительное означает чтение    int16_t word_count = cqe->word_count;    if (word_count < 0) {        // записьoper = oper | CS_WRITE;        word_count = -word_count;    } else {// чтениеoper = oper | CS_READ;}// Сохранение параметров операцииBYTE_COUNT = word_count * 2;    BUFFER_ADDR = cqe->buffer;    RX_FUNC2 = oper;// Сохраняем логический номер сектора// 1 блок = 4 сектораRX_LSN = cqe->block_number * 4;    is_first_int = true;    uint16_t* cs_ptr = (uint16_t*)RX_CS;//нужно выставить бит CS_INT, чтобы устройство сгенерировало прерывание, //если оно освободилось и готово принимать новые команды    *cs_ptr |= CS_INT;}

Далее дисковод генерирует прерывание о готовности и вызывается обработчик DXINT. Он прочитывает или записывает сектор на диск и делает это несколько раз в зависимости от переданного аргумента cqe->word_count.

Обратите внимание, что часть команд, записываемых в порт RX_CS, имеют 0 в бите прерывания CS_INT. Поэтому контроллер не генерирует прерывание после завершения команды. И код узнает о готовности чтением из регистра состояния RX_CS бита готовности.

// Ожидание готовности контроллера// проверкой бита готовности#define WAIT_READY    do { } while (!(*cs_ptr & 0x00FF));#define WAIT_READY2   do { } while (!(*cs_ptr & CS_TRANSFER));#define CHECK_ERROR   if (!(*cs_ptr & CS_TRANSFER)) { RXERR2(device); return; }void DXINT(){DeviceQueue* device = &dx_driver;    uint16_t* cs_ptr = (uint16_t*)RX_CS;    uint16_t* db_ptr = (uint16_t*)RX_DB;    // Проверяем статус контроллера    if (*cs_ptr & CS_ERR) {        RXERR2(device);        return;    }    uint16_t size = 128; // размер сектора в байтах    if (CS_WRITE & RX_FUNC2) { // передача данных на диск        if (!is_first_int) {// подготовка следующего сектора            if (!NEXT_SEC(device, size)) {    return;}        }        // Код операции "заполнить буфер"        SILOFE(CS_GO | CS_FILL_BUF, size, true);    } else if (!is_first_int) { // получение данных с диска        // Код операции "опустошить буфер"        SILOFE(CS_GO | CS_EMPTY_BUF, size, false);        if (!NEXT_SEC(device, size)) {    return;}    }    // Преобразование логического номера сектора в физические координаты    // из RXLSN нужно получить: физическую дорожку (0..75), физический сектор (1..26)    uint16_t track = RX_LSN / 26;    uint16_t sector = RX_LSN % 26 + 1;    // Выполнение команды на устройсте    *cs_ptr = RX_FUNC2;    WAIT_READY;    CHECK_ERROR;    // Записываем сектор в регистр данных    *db_ptr = sector;WAIT_READY;    CHECK_ERROR;    // Записываем дорожку в регистр данных    *db_ptr = track;    is_first_int = false;    *cs_ptr |= CSINT;}bool NEXT_SEC(DeviceQueue* device, uint16_t size){    //Увеличиваем адрес буфера на размер сектора    BUFFER_ADDR = BUFFER_ADDR + (size / 2);    ++RX_LSN;    BYTE_COUNT -= size;        if (BYTE_COUNT > 0) {        return true;              }// на этом операция завершена    BYTE_COUNT = 0;//Очищаем CS контроллера    uint16_t* cs_ptr = (uint16_t*)RX_CS;    *cs_ptr = 0; // Вызываем функцию завершения    QCOMP(device);    return false;}void SILOFE(uint16_t cmd, int max_buffer, bool is_fill){    uint16_t* cs_ptr = (uint16_t*)RX_CS;    uint16_t* db_ptr = (uint16_t*)RX_DB;    //Записываем команду в контроллер    *cs_ptr = cmd;    uint16_t byte_count = BYTE_COUNT;    if (byte_count == 0) {        goto lzf;    }        // Сравниваем с размером сектора    if (byte_count > max_buffer) {        byte_count = max_buffer;    }    uint16_t* R2 = BUFFER_ADDR;    // передача/получение буфера    do {         WAIT_READY2;        if (is_fill) {            *db_ptr = *R2;            R2++;        } else {            *R2 = *db_ptr;            R2++;        }        byte_count -= 2;    } while (byte_count > 0);    lzf:    uint16_t t;    // заполнение нулями остатка    while (true) {         WAIT_READY;        if (*cs_ptr & CS_TRANSFER) {            if (is_fill) {                *db_ptr = 0;            } else {                t = *db_ptr;            }        } else {            // значит CS_DONE            break;        }    }}

Тема 3. Системные вызовы

Программы обращаются к монитору посредством системных вызовов. Их список и описание можно посмотреть здесь. Системный вызов выполняется при помощи инструкции EMT — по сути это вызов подпрограммы, адрес которой записан по адресу 30_8. В момент запуска RT-11 записывает там ссылку на функцию EMTPRO, которая выступает в роли диспетчера, переадресуя вызов в зависимости от его аргументов. Передача аргументов может быть различной, рассмотрим в качестве примера EMT 375. В регистр R0 нужно записать адрес области памяти с аргументами и выполнить инструкцию EMT 375. Первый байт области — номер канала, второй — номер функции. Состав и расположение остальных аргументов специфично для каждой функции. В случае ошибки во время выполнения системного вызова будет установлен флаг переноса и код ошибки можно посмотреть в ERRBYT — ячейке памяти с адресом 52_8.

; (числовые константы в ассемблере MACRO-11 - восмеричные); параметрыARGS:   .BYTE   CHANNEL; номер канала ввода-вывода.BYTE#10; системный вызов .READ        .WORD   BLOCK; порядковый номер блока в файле, который нужно прочитать в буфер        .WORD   USRBUF; адрес буфера        .WORD   1000; длина буфера в словах,                         ; т.е. количество слов, которые нужно прочитать        .WORD   0; синхронный ввод-вывод; вызов        MOV     ARGS,R0        EMT     375        BCS     IO_ERROR

На псевдокоде реализация EMTPRO выглядит так:

// количество каналов по умолчанию#define CHNUM   16// набор из 16 каналовChannel _CSW[CHNUM];// количество каналовuint16_t I_CNUM = CHNUM;// указатель на набор каналовChannel* I_CSW = &_CSW[0];bool EMTPRO(EmtParams *params){    if (params->channel_num >= I_CNUM) {        MONERR(7/*CHAN_E*/, 0, false);        return false;    }    switch (params->function)    {        case FUNC_LOOKUP:            return LOOKUP(&I_CSW[params->channel_num], params->file_name, params->channel_num);            break;        case FUNC_READ:            return READ(&I_CSW[params->channel_num], params->block, params->buffer, params->size,                 params->completion, params->is_async);            break;        case FUNC_WRITE:            return WRITE(&I_CSW[params->channel_num], params->block, params->buffer, params->size);            break;        case FUNC_CSTAT:            return CSTAT(&I_CSW[params->channel_num], params->buffer);            break;// ...// и многие-многое другие системные вызовы                default:            exit(EXIT_FAILURE);    }    return false;}

Системный вызов .READ

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

Назначение: чтение блока из канала и запись его в буфер. Если размер буфера больше одного блока, то прочитываются несколько блоков.

Алгоритм работы такой:

  1. Проверяется корректность номера блока.

  2. Из канала извлекается ссылка на драйвер.

  3. При помощи функции QMANGR добавляется операция чтения в очередь драйвера.

bool READ(Channel *channel, uint16_t block, uint16_t* buffer, uint16_t size,void (*completion)(void), bool is_async) {    // Вычисление actual_size: сколько слов (<= size) можно прочитать, чтобы не выйти за конец файлаuint16_t actual_size;    uint16_t result = TSWCNT(block, size, channel, &actual_size);    if (result == 7) {// Запрашиваемый блок за пределами файла        return false;    }    // Получение индекса устройства (биты 1-5) из статуса канала    uint16_t csw = channel->csw;     uint16_t dev_index = (csw & INDEX_MASK) >> 1;          // Получение драйвера из таблицы $ENTRY    DeviceQueue* lqe = ENTRY[dev_index];        // Вычисление абсолютного номера блока на диске =// относительный блок в файле + начальный блок файла на диске    uint16_t physical_block = block + channel->start_block;        // Добавление операции чтения в очередь драйвера    QMANGR(physical_block, lqe, channel, buffer, actual_size, channel->unit, completion, is_async);        return true;}uint16_t TSWCNT(uint16_t block, uint16_t size, Channel *channel, uint16_t *buffer_size) {    *buffer_size = size;        if (channel->start_block == 0) {        // Файл не открыт        return 0;    }        // сравниваем запрашиваемый блок с длиной файла в блоках    uint16_t file_last_block = channel->length;    if (block >= file_last_block) {        // Запрашиваемый блок за пределами файла        // Ошибка "Attempted to write past end-of-file."        EMTERR(0);        return 7;    }        // Преобразование длины запроса в блоки    uint16_t req_blocks = *buffer_size;// добавляем 256 для округления вверх    // req_blocks := (req_blocks + 256) / 256    req_blocks += 0x00FF;    req_blocks &= 0xFF00;    req_blocks >>= 8;        // Вычисление количества блоков до конца файла    // блок конца буфера: начальный блок + длина запроса в блоках    uint16_t last_block_to_read = block + req_blocks;        uint16_t remaining_blocks = file_last_block - last_block_to_read;    if (remaining_blocks >= 0) {        // Достаточно блоков до конца файла        return 2;    }        // Недостаточно блоков - осталось меньше, чем запрошено    // Фактическое количество блоков до конца файла    uint16_t remaining = last_block_to_read + remaining_blocks - block;    // преобразуем длину в блоках в длину в словах    remaining = (remaining << 8) | (remaining >> 8); // SWAB*buffer_size = remaining;        return 1;}

Системный вызов .WRITE очень похож на .READ, только actual_size передается в QMANGR отрицательным — это признак операции записи.

***

На этом первая часть повествования про устройство RT-11 завершена. В следующей части расскажем про файловую систему и работу с файлами.

ссылка на оригинал статьи https://habr.com/ru/articles/1026302/