RT-11 — это операционная система из 1970-х годов для популярного в то время мини-компьютера PDP-11 фирмы DEC. В СССР известна под именами Электроника 60, ДВК, БК 0011М. Для тех, кто любит изучать чужой код в поисках инженерной эстетики — дальнейшее изложение.
Границы исследования
Ядро RT-11 поставлялось в виде исходного кода, которое предполагается с помощью утилиты sysgen настроить и перекомпилировать под конкретное железо и возможности — знакомый сценарий для тех, кто пересобирал ядро Linux. Совпадение не случайно: Unix тоже зародилась в компании DEC.
Благодаря этому исходный код ядра доступен для изучения, но, к сожалению, он написан на ассемблере и все комментарии вырезаны. Из-за чего можно было бы бросить дело изучения как бесперспективное. Однако выяснилось, что комментариями к коду являются следующие книги:
В них описаны структуры данных, базовые алгоритмы функционирования, а также аргументы и назначение системных вызовов. А еще объем кода невелик — около 2 тыс. строк на файл.
Состав ядра:
-
RMON — резидентный монитор, постоянно находится в оперативной памяти.
-
USR — предоставляет функции для работы с файловой системой. Может быть временно выгружен на диск, чтобы освободить место программе пользователя.
-
Драйверы устройств.
-
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: завершение операции
Вызывается из драйвера в обработчике прерывания. Она:
-
Проверяет наличие ошибки оборудования.
-
Обновляет счётчики канала.
-
Удаляет элемент из очереди драйвера.
-
Возвращает элемент в список свободных.
-
Вызывает функцию завершения, если необходимо.
-
Запускает следующий запрос в очереди (если есть).
/** * @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
-
В регистр команд RX_CS нужно записать команду CS_READ.
-
В регистр данных RX_DB нужно записать номер сектора.
-
В RX_DB нужно записать номер дорожки.
-
Подождать, пока на аппаратном уровне контроллер прочитает сектор с диска в свой буфер.
-
В RX_CS нужно записать команду CS_EMPTY_BUF.
-
Из 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 — по сути это вызов подпрограммы, адрес которой записан по адресу . В момент запуска RT-11 записывает там ссылку на функцию EMTPRO, которая выступает в роли диспетчера, переадресуя вызов в зависимости от его аргументов. Передача аргументов может быть различной, рассмотрим в качестве примера EMT 375. В регистр R0 нужно записать адрес области памяти с аргументами и выполнить инструкцию EMT 375. Первый байт области — номер канала, второй — номер функции. Состав и расположение остальных аргументов специфично для каждой функции. В случае ошибки во время выполнения системного вызова будет установлен флаг переноса и код ошибки можно посмотреть в ERRBYT — ячейке памяти с адресом
.
; (числовые константы в ассемблере 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
Параметры: номер канала, порядковый номер блока в файле, адрес буфера, размер буфера в словах и функция завершения в случае асинхронного ввода-вывода.
Назначение: чтение блока из канала и запись его в буфер. Если размер буфера больше одного блока, то прочитываются несколько блоков.
Алгоритм работы такой:
-
Проверяется корректность номера блока.
-
Из канала извлекается ссылка на драйвер.
-
При помощи функции 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/