2007年11月3日 星期六

OrzMicrokernel tasks.asm 工作排程器的切換執行權函式註解

Standard
工作新增、載入後,一切都準備就緒了,main.asm 最後一個任務,就是讓作業系統的心臟:『工作排程器』開始跳動,使所有工作能夠生生不息的運轉。Orz Microkernel 利用了 Schedule 這個 Function 切換到工作清單中的下一個工作,並利用 IRQ_0 的計時器,每一定單位時間就切換一次,使所有工作如同同時執行一般。

原始程式碼和詳細註解:
;--------------------------------------------------------------------------
; Scheduler 工作排程器:
; 多工的實作原理是讓所有的工作行程,依序輪流切換佔用 CPU 的運算能力。
; 由於每個工作佔用 CPU 的時間極小,因此近乎同時所有工作同時執行一般。
;
; Schedule: 是用來切換到下一個工作,並讓其佔用 CPU 的函式。在 main.asm 中呼叫後,
; 就會設定 taskRunningTime 工作執行的時間長度,而在 irq_handlers.asm 中
; 的 IRQ_0 計時器,就會開始依照 taskRunningTime 定時轉換到下一個工作。

tx_NoMoreTasks db "No more tasks",0 ; 設定沒有工作的訊息字串

Schedule:
mov eax, [tasksCount] ; 設 EAX 為目前工作數量
and eax, eax ; 若工作數量為零則設定零值旗標 ZF = 1
jnz Schedule0 ; 若旗標 ZF = 0 則跳到 Schedule0

mov esi, tx_NoMoreTasks ; 在 ESI 寫入沒有工作的訊息字串
jmp GpFault ; 輸出訊息

Schedule0:
mov eax, [currentTaskN] ; 設 EAX = currentTaskN
inc eax ; EAX 加一
cmp eax, [tasksCount] ; 若EAX<tasksCount則設定進位旗標CF = 1
jc Schedule1 ; 若進位旗標 CF = 1 則跳到 Schedule1

xor eax, eax ; 清除 EAX

Schedule1:
mov [currentTaskN], eax ; 從 EAX 回存到 currentTaskN

; tasksList 所占記憶體大小為 MAX_TASKS(參考 tables.asm)
add eax, tasksList ; 從工作清單的記憶體起始位址
movzx eax, BYTE [eax] ; 只留下 EAX 最後 8bits(1 Byte)
imul eax, s_task.msize ; 計算此工作的狀態記錄記憶體偏移量
; = 目前執行的工作編號 x s_task 的大小
add eax, tasks ; EAX = 偏移量 + 工作的狀態記錄的記憶體開始位址
mov [currentTaskP], eax ; 設 currentTaskP 為此工作狀態記錄的記憶體位址
mov ebp, eax ; 設 EBP 指向工作狀態記錄的記憶體位址

mov eax, [ebp + s_task.state] ; 取得工作狀態放入 EAX
cmp eax, STATE_SENDW ; 若 EAX = STATE_SENDW 則零值旗標 ZF = 1
jz Schedule0 ; 若零值旗標 ZF = 1 則跳到 Schedule0
cmp eax, STATE_STOP ; 若 EAX = STATE_STOP 則零值旗標 ZF = 1
jz Schedule0 ; 若零值旗標 ZF = 1 則跳到 Schedule0

ScheduleCurr:
; 轉換到將要執行的工作
cli ; 禁用中斷以免干擾工作執行權轉換
mov eax, 0x20 ; 設定 EAX = 0x20 (此工作執行的時間長度)
mov [taskRunningTime], eax ; 設 taskRunningTime 為 EAX

; taskcode 和 task_data(參考 tables.asm)
mov eax, [ebp + s_task.taskPageAddr] ; 設 EAX 為此工作的記憶體分頁位址

mov [task_code + 2], ax ; 設定 taskcode 中第 3~4 byte 為分頁偏移位址
mov [task_data + 2], ax ; 設定 taskdata 中第 3~4 byte 為分頁偏移位址
shr eax, 0x10 ; 將 EAX 向右平移 16 個 bits
;
移去 AX([AH][AL]) 的內容
mov [task_code + 4], al ; 設定 taskcode 中第 5 byte 為分頁節段位址(低位元)
mov [task_code + 7], ah ; 設定 taskcode 中第 8 byte 為分頁節段位址(高位元)
mov [task_data + 4], al ; 設定 taskdata 中第 5 byte 為分頁節段位址(低位元)
mov [task_data + 7], ah ; 設定 taskdata 中第 8 byte 為分頁節段位址(高位元)

; 註:分頁數最多不超過 65535 (使用 2 bytes 的空間記錄分頁數)
mov eax, [ebp + s_task.taskPages] ; 設 EAX 為分頁數
mov [task_code], ax ; 設定 taskcode 中第 1~2 byte 為分頁數
mov [task_data], ax ; 設定 taskdata 中第 1~2 byte 為分頁數

mov [kernel_esp], esp ; 儲存核心目前的堆疊暫存器狀態到
; kernel_esp


; 將工作上一次執行時的暫存器狀態和旗標等設定,還原到目前的執行環境。
mov esp, [ebp + s_task.r_esp] ; 還原此工作的堆疊暫存器 ESP
mov ax, task_data - _GDT ; 計算 task_data 的偏移區段位址
; (參考 tables.asm 的結構)
mov ss, ax ; 設定 SS 堆疊區段為 AX

push DWORD [ds:ebp + s_task.r_eflags] ; 將此工作的旗標暫存器 EFLAG 存入堆疊
push DWORD (task_code - _GDT) ; 將 task_code 的偏移區段位址存入堆疊
; (參考 tables.asm 的結構)
push DWORD [ds:ebp + s_task.r_eip] ; 將此工作的指標暫存器 EIP 存入堆疊
; 目前堆疊內容:[r_eflags][task_code 的偏移區段位][r_eip]

mov eax, [ds:ebp + s_task.r_ebp] ; 設 EAX 為此工作的基底暫存器 EBP
push eax ; 將此工作的基底暫存器 EBP 存入堆疊
mov eax, [ds:ebp + s_task.r_eax] ; 還原此工作的累積暫存器 EAX
push eax ; 將累積暫存器 EAX 存入堆疊
; 目前堆疊內容:[r_eflags][task_code 的偏移區段位][r_eip][r_ebp][r_eax]

mov ebx, [ds:ebp + s_task.r_ebx] ; 還原此工作的基底暫存器 EBX
mov ecx, [ds:ebp + s_task.r_ecx] ; 還原此工作的計數暫存器 ECX
mov edx, [ds:ebp + s_task.r_edx] ; 還原此工作的資料暫存器 EDX
mov esi, [ds:ebp + s_task.r_esi] ; 還原此工作的來源索引暫存器 ESI
mov edi, [ds:ebp + s_task.r_edi] ; 還原此工作的目的索引暫存器 EDI

mov ax,task_data - _GDT ; 計算 task_data 的偏移區段位址
; (參考 tables.asm 的結構)
mov ds, ax ; 設資料區段 DS 為 AX
mov es, ax ; 設額外區段 ES 為 AX

pop eax ; 還原此工作的累積暫存器 EAX
pop ebp ; 還原此工作的基底暫存器 EBP
iretd ; 自堆疊取回 EIP,再取回 EFLAGS
; 轉移到該位址執行工作行程



後記

如同開頭所說的,完整的工作排程架構還涵蓋了 IRQ0 的計時器部份,而此部份程式碼於 irq_handlers.asm 之中,日後有空將再行註解。

另外,在這次 Schedule 系列函式中,我個人有一點小疑問在這行:

movzx eax, BYTE [eax]

在這行命令中只留下 EAX 最後 8bits(1 Byte) 資料,問題是為何要刻意取其中 8bits 出來呢?
就算沒有這行命令,我們還是可以正常的來計算記憶體偏移量,那此行命令不就多此一舉?希望知道的人能回應我的愚笨問題。