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

от автора

В операционной системе RT-11 существуют варианты резидентного монитора с поддержкой многозадачности — например, RMONFB. Многозадачность здесь реализована полностью на программном уровне, без аппаратной поддержки. Если вам интересно посмотреть исходный код, отвечающий за многозадачность в RT-11, вместе с пояснениями из Руководства системного программиста — добро пожаловать под кат.

Ликбез по ассемблеру PDP-11

Без знания ассемблера PDP-11 не обойтись, поэтому приведем здесь описание части инструкций. Полное описание можно найти в здесь.

Скрытый текст

В процессоре PDP-11 имеется восемь 16-битных регистров: R0, …, R7. Указатель стека SP — это R6, счетчик команд PC — это R7.

Присваивание (обратите внимание на обратный порядок аргументов):

MOV src, dst; dst = src

Регистровая адресация:

MOV R1, R2; R2 = R1

Непосредственная адресация:

MOV #12, R2; R2 = 012

Косвенная адресация:

MOV (R3), R4; R4 = R3 MOV @R3, R4; R4 = R3

Косвенная адресация с инкрементом:

MOV (R3)+, R4; R4 = R3++ MOV -(R3), R4; R4 = (–-R3)

Косвенная адресация со смещением:

MOV JNUM(R3), R4; R4 = R3->JNUM или R4 = *(R3 + JNUM)

Добавление и извлечение из стека:

MOV R4, -(SP); PUSH(R4) MOV (SP)+, R4; R4 = POP()

Безусловный переход:

JMP label; goto label BR label; goto label

Сравнение и условный переход:

CMP R4, R5 BNE label; if (R4 != R5) goto labelCMP R4, R5 BLO label; if (R4 < R5) goto labelCMP R4, R5 BHIlabel; if (R4 > R5) goto label BGTlabel; if (R4 > R5) goto label BEQlabel; if (R4 = R5) goto labelTST R3 BMIlabel; if (R3 < 0) goto label BPL label; if (R3 > 0) goto label

Побитовые операции:

BIC R1, R2; R2 = R2 & ~R1 BIS R1, R2; R2 = R2 | R1

Вызов подпрограммы и возврат:

JSR Ri, lsubr; PUSH(Ri); Ri = lafter; goto lsubrlafter:; ...lsubr:; ...RTS Ri; temp = Ri; Ri = POP(); goto temp

Общая идея алгоритма

Резидентный монитор предоставляет программам системные вызовы для работы с файлами, терминальным вводом-выводом и т.д. У каждого задания (задачи, процесса) есть собственный стек. В момент системного вызова в этот стек записывается адрес возврата в программу. В мониторе хранится служебная информация по всем работающим заданиям, в том числе указатели на стек каждого задания. В конце системного вызова планировщик проверят, нет ли более приоритетного задания, ожидающего выполнения. Если такое задание есть, происходит переключение на него. Для этого из служебной информации задания извлекается указатель стека и загружается в регистр SP процессора, после чего выполняется инструкция возврата RTI.

Аналогичное переключение происходит и в конце обработчиков прерываний.

RT-11 относится к операционным системам реального времени, поэтому цель планировщика — выполнять наиболее приоритетное задание, а не распределять время процессора равномерно между всеми.

Задания и приоритеты

Номер задания в RT-11 — всегда четное число. Фоновому заданию (background job) присвоен номер 0, а оперативному заданию (foreground job) — 2 (в двухзадачной конфигурации системы). В многозадачной конфигурации фоновому заданию также присвоен номер 0, а номер оперативного задания всегда 16_8. Номера остальных заданий (системных) находятся между ними.

Приоритет определяется номером задания: минимальный у фонового, максимальный у оперативного. Динамически изменить приоритет нельзя. В резидентном мониторе номер активного задания хранится в переменной JOBNUM.

Смешанная область и контекст

Вся информация, необходимая резидентному монитору для управления выполнением задания, хранится в так называемой смешанной области (impure area). У фонового задания эта область расположена внутри самого монитора, у остальных — в адресном пространстве задания.

Приведем некоторые данные, которые в ней хранятся (на псевдокоде):

typedef struct {    // I.JSTA - слово состояния задания    uint16_t JSTA;// I.QHDR - указатель на первый свободный элемент очереди операций ввода-вывода    QueueElement* QHDR;// I.CMPE - указатель на последний элемент очереди подпрограмм завершения операций ввода-выводам    CompQueueElement* CMPE;// I.CMPL - указатель начала очереди подпрограмм завершения операций ввода-вывода    CompQueueElement* CMPL;// I.CHWT - указатель на канал:// если задание ожидает, когда счетчик количества элементов очереди канала достигнет нуля,// то указатель на канал хранится здесь.    Channel* CHWT;// I.JNUM - номер задания, которому принадлежит эта смешанная область    uint16_t JNUM;// I.CNUM - количество каналов (размер массива I_CSW)    uint16_t I_CNUM;// I.CSW - указатель на массив каналов данного задания    Channel* I_CSW;// I.IOCT - общее число запросов ввода-вывода у задания    uint16_t IOCT;// I.SP - указатель на стек задания    uint16_t SP; // I.QUE - место для одного элемента очереди ввода-вывода (стандартная очередь из одного элемента)    QueueElement QUE; // ...} ImpureData;// массив ссылок на ImpureDataImpureData* $IMPUR[MXJNUM/2+1];

Например, очень важным является поле JSTA, в битах которого хранится информация о состоянии процесса:

; Задание ожидает освобождения USR.USRWT$  =     20; Задание приостановлено в результате выполнения команды SUSPEND.KSPND$  =    100; Есть элементы в очереди подпрограмм завершения операций ввода-вывода.CPEND$  =    200; Задание ожидает завершения всех операций ввода-вывода (перед завершением программы).EXIT$   =    400; Программа не запущена.NORUN$  =   1000; Задание приостановлено.SPND$   =   2000; Задание ожидает, когда счетчик количества элементов очереди канала достигнет нуля.CHNWT$  =   4000; Задание ожидает освобождения места в буфере вывода терминала.TTOWT$  =  20000; Задание ожидает ввода с терминала.TTIWT$  =  40000; Выполняется подпрограмма завершения операции ввода-вывода в этом задании.CMPLT$  = 100000; Условие блокировки выполнения задания (побитовое или)BLOCK$  = TTIWT$ ! TTOWT$ ! CHNWT$ ! SPND$ ! NORUN$ ! EXIT$ ! KSPND$ ! USRWT$

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

  • ссылка на ImpureData,

  • системная область связи (system communication area) — область памяти с адресами 34_8 - 53_8,

  • содержимое регистров процессора.

Указатель на стек (на момент системного вызова) активного задания хранится в переменной TASKSP, а ссылка на ImpureData хранится в CNTXT.

Процедура переключения контекста CNTXSW

Резидентный монитор вызывает подпрограмму CNTXSW, чтобы сменить контекст и переключиться с одного задания на другое. У нее один аргумент — R5, в нем передается ссылка на смешанную область задания, на которое нужно переключиться, т.е. $IMPUR[job_number/2]. Первым делом в стек активного задания сохраняется содержимое регистров (часть уже сохранили в момент системного вызова):

;; @param R5 - контекст другого заданияCNTXSW:        MOV     TASKSP,R4; R4 = TASKSP        CMP     CNTXT,R5        BNE     10$; if (CNTXT == R5) {        JMP     C.20$;     goto C.20$10$:; }        MOV     R3,-(R4); R4.PUSH(R3)        MOV     R2,-(R4); R4.PUSH(R2)        MOV     R1,-(R4); R4.PUSH(R1)        MOV     R0,-(R4); R4.PUSH(R0)

Далее в тот же стек сохраняется системная область связи:

        MOV     #34,R0; R0 = 034; do {20$:    MOV     (R0)+,-(R4);     TASKSP.PUSH(*R0++)        CMP     R0,#54        BLO     20$; } while (R0 < 054)

И в конце в CNTXT->SP сохраняется ссылка на вершину стека:

        MOV     CNTXT,R2        ADD     #I.SP,R2        MOV     R4,(R2)+; CNTXT->SP = TASKSP

После этого для другого задания в обратном порядке восстанавливаются:

  • CNTXT — ссылка на ImpureData,

  • TASKSP — ссылка на вершину стека другого задания,

  • системная область связи,

  • содержимое регистров.

        MOV     R5,CNTXT; CNTXT = R5        ADD     #I.SP,R5        MOV     @R5,R4; R4 = CNTXT->SP; do {85$:    MOV     (R4)+,-(R0);     *(--R0) = R4.POP()        CMP     R0,#34        BHI     85$; } while (R0 > 034)        MOV     (R4)+,R0; R0 = R4.POP()        MOV     (R4)+,R1; R1 = R4.POP()        MOV     (R4)+,R2; R2 = R4.POP()        MOV     (R4)+,R3; R3 = R4.POP()        MOV     R4,TASKSP; TASKSP = R4

Далее проверяется наличие элементов в очереди подпрограмм завершения операций ввода-вывода:

C.20$:  MOV     CNTXT,R5; R5 = CNTXT        MOV     I.JNUM(R5),JOBNUM ; JOBNUM = CNTXT->JNUM        TST     @R5; // CMPLT$ = Выполняется подпрограмма завершения в этом задании        BMI     150$; if (!(CNTXT->JSTA & CMPLT$)) {        TSTB    @R5;     // CPEND$ = Есть элементы в очереди подпрограмм завершения операций ввода-вывода        BPL     150$;     if (CNTXT->JSTA & CPEND$) {BIC     #<BLOCK$!CPEND$>&^C<NORUN$>,@R5; CNTXT->JSTA &= ~((BLOCK$ | CPEND$) & ~NORUN$)        BIS     #CMPLT$,@R5;         CNTXT->JSTA |= CMPLT$        ; далее происходит добавление в стек адреса менеджера очереди подпрограмм завершения; чтобы его выполнить после переключения на задание; ... (пропустим этот код);     }; }150$:   RTS     PC; return

Запрос планировщика

Планировщик запускается только при наличии соответствующего запроса. Номер задания, инициировавшего запуск планировщика, хранится в переменной INTACT = JOBNUM / 2 + 200_8. Изменение INTACT происходит путем вызова подпрограммы $RQTSW. В INTACT хранится номер задания с наивысшим приоритетом из запрашивавших планировщик, поэтому $RQTSW игнорирует запросы от заданий с более низким приоритетом.

;; @param R5 - номер задания$RQTSW: CMP     R5,JOBNUM        BLO     1$; if (R5 >= JOBNUM) {$RQSIG: SEC        RORB    R5;     R5 = R5 / 2 + 0200        JSR     PC,GETPSW;     GETPSW() //сохранение приоритета в стеке        SPL     7;     SPL(7) //запрет прерываний        CMPB    R5,INTACT        BLOS    2$;     if (R5 > INTACT) {;         //обновление приоритета для планировщика        MOVB    R5,INTACT;         INTACT = R5;     }2$:     JSR     PC,$MTPS;     $MTPS() //восстановление прежнего приотритета        ASLB    R5;     R5 = (R5 - 0200) * 2; }1$:     RTS     PC; return

Переключение в системный режим

Для того чтобы изолировать задания от монитора, система предоставляет два режима выполнения:

  • пользовательский режим (user state),

  • системный режим (system state).

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

При выполнении системного вызова происходит переключение в системный режим. Для этого используется подпрограмма $ENSYS. Обработчики прерываний используют аналогичную подпрограмму $INTEN.

Для вызова $ENSYS используется макрос ENSYS. У него один аргумент — адрес, куда передается управление после выхода из системного режима. Код, который следует за вызовом ENSYS и до инструкции RTS PC выполняется в системном режиме:

        ENSYS   3$; начало кода, который выполняется в системном режиме        MOVB    #377,USROWN        MOV     IMPLOC,R4        ; ...; конец кода, который выполняется в системном режиме        RTS     PC3$:; место после выхода из системного режима

ENSYS поддерживает вложенные вызовы. Для этого заведен счетчик INTLVL с начальным значением -1. В начале выполнения ENSYS он увеличивается на единицу, перед выходом из нее — уменьшается. При переходе -1 → 0 происходит переключение в системный режим. При обратном переходе 0 → -1 происходит переключение в пользовательский режим.

Если посмотреть на определение макроса ENSYS, то видим, что у подпрограммы $ENSYS два аргумента — относительный адрес выхода из системного режима и приоритет процессора, который нужно установить перед выполнением кода:

.MACRO  ENSYS   ADR        JSR    R5,$ENSYS          .WORD  ADR-.; относительный адрес ADR          .WORD  340; 0-й приоритет процессора.ENDM   ENSYS

Итак, в начале увеличивается счетчик INTLVL и происходит переключение на системный стек, ссылка на вершину которого хранится в RMSTAK:

;; Вызывается другими подпрограммами монитора, которым необходимо перейти в системный режим.;; Инструкции следующие за вызовом $ENSYS, будут выполняться в системном режиме.;; При появлении инструкции RTS PC происходит переход в пользовательский режим;; и передача управления по адресу, указанному при вызове $ENSYS.$ENSYS::        ; пропустим вычисление абсолютного адреса возврата        ; и добавление PSW в стек перед адресом возврата (т.е. имитация прерывания)        ; ...                SPL     7; SPL(7) // 7-й приоритет процессора (запрет прерываний);; Вызывается из обработчиков прерываний.;; Осуществляет переключение в системный режим.;; Ожидается что предыдущие PSW и PC сохранены в стеке задания.;; Вход в подпрограмму $INTEN происходит на 7-м приоритете процессора.$INTEN: MOV     R4,-(SP)        INC     (PC)+; INTLVL++INTLVL: .WORD   -1        BGT     1$; if (INTLVL == 0) { //еще не в системном режиме;     //переключение на системный стек        MOV     SP,(PC)+;     TASKSP = SPTASKSP: 0        MOV     (PC)+,SP;     SP = RMSTAKRMONSP: RMSTAK; }1$:     MOV     R4,-(SP)

Далее вызывается код, расположенный после ENSYS, как подпрограмма с помощью инструкции JSR:

RMONPS: MOV     #PS,R4        BIC     (R5)+,@R4; SPL(arg) //установка приоритета процессора из указанного в агрументе вызова $INTEN        MOV     (SP)+,R4        JSR     PC,@R5; action() //выполнение инструкций, следующих за вызовом $INTEN или $ENSYS, до инструкции RTS PC

После завершения подпрограммы возможны различные варианты. Самый простой случай — выход из вложенного ENSYS. Происходит только уменьшение счетчика INTLVL:

SPL     7; SPL(7) // 7-й приоритет процессора (запрет прерываний)        TST     INTLVL        BEQ     EXUSER; if (INTLVL != 0) {        DEC     INTLVL;     INTLVL--        BR      RTICMN;     return //INTLVL > 0 => все еще остаемся в системном режиме; }

Если же ситуация перехода 0 → -1, т.е. готовы переключиться в пользовательский режим, то возможен вариант, когда не было запроса на переключение на другое задание (INTACT пустой). В этом случае происходит уменьшение счетчика INTLVL и переключение на стек активного задания:

EXUSER: SPL     0; SPL(0) // 0-й приоритет процессора (разрешение всех прерываний); далее происходит вызов менеджера очереди fork; ... (пропустим этот код)        SPL     7; SPL(7) // запрет прерываний        MOV     (PC)+,R4INTACT: 0        BNE     EXSWAP; if (INTACT == 0) {;     //переключение на другое задание не требуется        DEC     INTLVL;     INTLVL--        MOV     TASKSP,SP;     SP = TASKSP //переключение на стек активного заданияRTICMN: MOV     (SP)+,R4;     // восстановление регистров        MOV     (SP)+,R5        RTI;     return; }

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

EXSWAP: BMI     ABORT        CLR     INTACT; INTACT = 0        SPL     0; SPL(0) //разрешение прерываний        INC     R4        ASLB    R4        MOV     R4,JOBNUM; JOBNUM = INTACT.JNUM + 2        ADDR    $IMPUR,R4,ADD; R4 = &$IMPUR[JOBNUM]; //просматриваем состояния заданий в порядке убывания их приоритета начиная с INTACT.JNUM; do {1$:     SUB     #2,JOBNUM;     JOBNUM -= 2        BMI     3$;     if (JOBNUM < 0) goto 3$        MOV     -(R4),R5;     R5 = &$IMPUR[JOBNUM]

Задание может не существовать (программа не запущена). Тогда оно исключается из рассмотрения:

        BEQ     1$;     if (R5 == NULL) continue

Если задание блокировано, то появляется возможность передать управление менее приоритетному заданию:

        BIT     #BLOCK$,@R5        BEQ     2$;     if (!(R5->JSTA & BLOCK$)) break        TST     @R5        BMI     1$;     if (R5->JSTA & CMPLT$) continue // Выполняется подпрограмма завершения в этом задании        TSTB    @R5;        BPL     1$;     if (!(R5->JSTA & CPEND$)) continue //Есть элементы в очереди подпрограмм завершения операций ввода-вывода        BIT     #KSPND$!NORUN$,@R5; // KSPND$ = Задание приостановлено в результате выполнения команды SUSPEND.; // NORUN$ = Программа не запущена.        BNE     1$; } while (R5->JSTA & (KSPND$ | NORUN$))

Если задание существует и готово к выполнению, монитор переключает контекст задания и передает ему управление:

2$:     JSR     PC,CNTXSW; CNTXSW()EXUSLK: BR      EXUSER; goto EXUSER

Очередь ввода-вывода: QMANGR и QCOMP

Операция постановки в очередь QMANGR имеет отличия по сравнению с однозадачным монитором.

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

QFULL:  SPL     0; SPL(0) //разрешение прерываний        ENSYS   QGTELT; //вызов планировщика с помощью ENSYS        MOV     JOBNUM,R5; R5 = JOBNUM        BEQ     9$; if (R5 != 0) {        TST     -(R5);     R5--; }9$:JMP     $RQSIG; $RQTSW(R5) //обновление INTACT; return; //вызов планировщика и после возврат на QGTELT
;; Забирает первый свободный элемент из очереди операций ввода-вывода,;; заполняет его,;; добавляет в очередь устройства;; и вызывает обработчик.;;;; @param R0 - номер блока на диске;; @param R2 - &(device->last);; @param R3 - указатель на канал;; @param R5 - указатель на запись с параметрами: block, buffer_addr, length, is_async/completion;;             (указывает на второе поле - buffer_addr);; @param SP[0] - lengthQMANGR: MOV     R4,-(SP); //сохранение регистров        MOV     R1,-(SP)        MOV     CNTXT,R1        TST     (R1)+QGTELT: SPL     7; SPL(7) //запрет прерываний        MOV     @R1,R4; R4 = CNTXT->QHDR //Указатель на первый свободный элемент очереди операций ввода-вывода        BEQ     QFULL; if (R4 == NULL) QFULL() //переключимся на выполнение других заданий        CMPB    #255.,10(R3)        BEQ     QFULL; if (R3->DEVQ == 255) QFULL()        MOV     @R4,@R1; CNTXT->QHDR = R4->LINK //удаляем элемент из очереди        SPL     0; SPL(0) //разрешение прерываний; //заполняем элемент        CLR     (R4)+; R4->LINK = NULL        MOV     R3,(R4)+; R4->CSW = R3        INCB    10(R3); R3->DEVQ++        ; ... (пропустим код заполнения элемента)

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

3$:     ADD     #Q.BLKN-Q.COMP,R4; R4 = &(R4->BLKN) //ссылки в очереди драйвера указывают не на начало элемента (поле LINK), а на поле BLKN        MOV     R3,-(SP); PUSH(channel)        ENSYS   7$;         INC     I.IOCT-I.QHDR(R1); CNTXT->IOCT++; //Общее число запросов ввода-вывода        MOV     R2,R1; device = R2        BIS     #100000,-(R1); device->vector |= 0100000 //флаг блокирования драйвера        TST     (R2)+        BNE     4$; if (device->last == NULL) {;     //добавляем элемент в пустую очередь устройства        CLR     (R1)+;     device->vector = 0        MOV     R4,(R1)+;     device->last = R4        MOV     R4,(R1)+;     device->first = R4        JMP     @R1;     device->start(); ;     goto 7$; } else {;     //поиск конца группы элементов в очереди, принадлежащих заданию JOBNUM;     do {4$:     MOV     @R2,R5;         R5 = device->first или R5 = R2->LINK;         // в обоих случаях R5 содержит указатель на поле BLKN5$:     MOV     R5,R2        CMP     -(R2),-(R2);         // R2 содержит указатель на поле LINK        MOV     @R2,R5;         R5 = R2->LINK;         // R5 содержит указатель на поле BLKN        BEQ     6$;         if (R5 == NULL) break        CMP     2(R5),R0        BHIS    5$;     } while (R5->JNUM > R0 /*JOBNUM*/);     //вставляем элемент6$:     MOV     R5,-4(R4);     R4->LINK = R2->LINK        MOV     R4,@R2;     R2->LINK = R4; }        ASL     @R1; device->vector &= ~0100000 // снимаем флаг блокирования драйвера

В-третьих, ожидание завершения добавленной операции ввода-вывода тоже сделано передачей управления планировщику. Здесь используется подпрограмма $SYSWT. У нее два аргумента: флаг причины ожидания для слова состояния задания и инструкции для определения, что ожидаемое событие наступило (т.е. операции ввода-вывода завершены):

7$:     MOV     (SP)+,R3; channel = POP()        TST     -(R5)        BNE     8$; if (!is_async) {CHWAIT: MOV     CNTXT,R1        MOV     R3,I.CHWT(R1);     CNTXT->CHWT = R3        JSR     R4,$SYSWT;     $SYSWT(CHNWT$) //ожидание, когда счетчик элементов очереди канала достигнет нуля          .WORD  CHNWT$;     //эти инстркции выполняет $SYSWT, чтобы определить что ожидание завершено        SPL     7;     SPL(7) // запрет прерываний        MOVB    10(R3),R2;     R2 = R3->DEVQ        NEGB    R2;        JSR     PC,@(SP)+;     return (R2 != 0) //возврат в $SYSWT; }8$:     RTS     PC; return

Перед выходом из QMANGR еще раз передается управление планировщику, т.к. код выполнялся внутри ENSYS.

Операция завершения QCOMP тоже имеет отличия по сравнению с однозадачным монитором.

Добавили изменение слова состояния задания при изменении счетчиков количества запросов ввода-вывода:

;; Возвращает элемент в список свободных.;; Или добавлят его в конец очереди подпрограмм завершения.;; ;; @param R4 - &(device->first)QCOMP:  ASR     -4(R4)        BMI     8$; if (device->vector & 0100000) return //драйвер заблокирован        JSR     R3,SAVE30; //сохранение регистров        MOV     R4,R1CMPLT2: MOV     @R1,R4; R4 = device->first        MOV     -(R4),R3; R3 = R4->CSW //канал        MOVB    Q.JNUM-Q.CSW(R4),R5        ASR     R5        ASR     R5        ASR     R5        BIC     #177761,R5; R5 = R4->JNUM        ADD     PC,R5        MOV     $IMPUR-.(R5),R5; R5 = $IMPUR[R5] //CNTXT; //Перед продолжением программы, которая запросила синхронный ввод-вывод,; //монитор ожидает, когда счетчик элементов очереди канала достигнет нуля        DECB    10(R3); R3->DEVQ--        BNE     2$; if (R3->DEVQ == 0) {        CMP     R3,I.CHWT(R5)        BNE     2$;     if (R3 == R5->CHWT) {        JSR     R4,UNBLOK;         UNBLOK(CHNWT$) //снимаем флаг CHNWT$ из CNTXT->JSTA          .WORD  CHNWT$;     }; }; //Перед завершением программы система ожидает, ; //когда общее число запросов ввода-вывода у задания дотигнет нуля2$:     DEC     I.IOCT(R5); R5->IOCT--        BNE     3$; if (R5->IOCT == 0) {        JSR     R4,UNBLOK;     UNBLOK(EXIT$) //снимаем флаг EXIT$ из CNTXT->JSTA          .WORD   EXIT$; }; //убираем элемент из очереди драйвера3$:     MOV     -(R4),(R1)+; device->first = R4->LINK

Убрали прямой вызов функции завершения. Убрали запуск следующего запроса в очереди. Теперь монитор преобразует элемент очереди ввода-вывода в элемент очереди подпрограмм завершения и добавляет в конец соответствующей очереди:

5$:     CMP     Q.COMP(R4),#1        BLOS    AQLINK; if (R4->COMP > 1) {        BIT     #ABORT$,@R5        BNE     AQLINK;     if (!(R5->JSTA & ABORT$)) {;         //заполнение элемента очереди подпрограмм завершения;         R4->QC_CMP = R4->COMP //адрес подпрограммы завершения        MOV     @R3,Q.BUFF(R4);         R4->QC_CSW = R3->CSW //канал        SUB     I.CSW(R5),R3;         R3 = R3 - R5->I_CSW        MOV     R3,Q.WCNT(R4);         R4->QC_OFT = R3 //наподобие индекса в массиве каналов        TST     (R5)+        MOV     R5,R2        MOV     I.JNUM-2(R5),R5        JSR     PC,$RQTSW;         $RQTSW(R5->JNUM) //обновление INTACT        MOV     R2,R5        BIS     #CPEND$,-(R2);         R5->JSTA |= CPEND$CQLINK: TST     (R5)+        CLR     @R4;         R4->QC_LNK = NULL //следующий элемент в очереди подпрограмм завершения        JSR     PC,GETPSW;         //сохранение приоритета процессора в стеке        SPL     7;         SPL(7) //запрет прерываний;         //добавляем элемент в конец очереди подпрограмм завершения        MOV     (R5)+,R0;         R0 = R5->CMPE        BNE     6$;         if (R0 == NULL) {        MOV     R5,R0;             R0 = &(R5->CMPL);         }6$:     MOV     R4,@R0;         R5->CMPE->LINK = R4 или R5->CMPL = R47$:     MOV     R4,-(R5);         R5->CMPE = R4        JSR     PC,$MTPS;         PS = POP() //восстановление прежнего приоритета процессора8$:     RTS     PC;         return;     }; }AQLINK: TST     (R5)+        JSR     PC,GETPSW; GETPSW() //сохранение приоритета процессора        SPL     7; SPL(7) //запрет прерываний; //возвращаем элемент в список свободных        MOV     (R5)+,@R4; R4->LINK = R5->QHDR        BR      7$; R5->QHDR = R4

Небольшое пояснение. Подпрограмма QCOMP вызывается из драйвера в системном режиме. А функция завершения находится в коде программы и должна выполняться в пользовательском режиме. Поэтому ссылка на нее добавляется в очередь подпрограмм завершения. В конце подпрограммы переключения контекста CNTXSW в стек задания вставляется адрес менеджера очереди подпрограмм завершения $CRTNE. Он вызывается в первую очередь после переключения на стек задания до продолжения выполнения программы. $CRTNE последовательно запускает все подпрограммы завершения из очереди.

Заключение

В многозадачном режиме работы монитора раскрывается польза асинхронного ввода-вывода. Переключение между задачами сделано на основе простой и элегантной концепции — переключении стека задач. Этот механизм легко понять и легко изучать. Остаётся только выразить респект старшему поколению системных программистов и их изобретательности.

PS. Части второй цикла про ОС RT-11 скорее всего не будет. Кому интересно устройство ее файловой системы, можно посмотреть здесь реализацию на языке C# со ссылками на документацию.

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