В операционной системе 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 label CMP R4, R5 BLO label ; if (R4 < R5) goto label CMP R4, R5 BHI label ; if (R4 > R5) goto label BGT label ; if (R4 > R5) goto label BEQ label ; if (R4 = R5) goto label TST R3 BMI label ; 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 lsubr lafter: ; ... 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; // массив ссылок на ImpureData ImpureData* $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 PC 3$: ; место после выхода из системного режима
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 = SP TASKSP: 0 MOV (PC)+,SP ; SP = RMSTAK RMONSP: 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)+,R4 INTACT: 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] - length QMANGR: 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 содержит указатель на поле BLKN 5$: 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,R1 CMPLT2: 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 = R4 7$: 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# со ссылками на документацию.
longtolik
Спасибо! Отличная архитектура, в институтах, насколько я знаю, до сих пор студентов учат по ней.
А мы писали программы в машинных кодах, потом появился ассемблер. Я купил книгу М.Сингер "Мини-ЭВМ PDP-11 Программирование на языке ассемблера и организация машины". И после этого процесс не пошёл, а полетел. Еще купили массу документации на английском языке. А потом по RSX-11 на русском.
Кстати, есть сайт pdp11.org, там много интересного. До сих пор помню многие машинные коды.