В операционной системе 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, а номер оперативного задания всегда . Номера остальных заданий (системных) находятся между ними.
Приоритет определяется номером задания: минимальный у фонового, максимальный у оперативного. Динамически изменить приоритет нельзя. В резидентном мониторе номер активного задания хранится в переменной 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) — область памяти с адресами
,
-
содержимое регистров процессора.
Указатель на стек (на момент системного вызова) активного задания хранится в переменной 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 + . Изменение 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/