OS
reference:
https://github.com/s-matyukevich/raspberry-pi-os
為了避免頻繁的插、拔 SD card 進行 image 的更新,在 image 最一開始設置一個 bootloader,監聽 UART 一定的秒數,秒數內能夠進行新的 kernel image upload
樹莓派開始接收數據,並將數據保存到 sd 卡上第一個 fat32 分區的 root,覆蓋掉原有的kernel.img
FSBL(第一階段bootloader)位於ROM,由各個hardware vendor來實現。
它會加載一個image到內存(這個image可以是第二階段bootloader,例如grub、lilo等,也可以是kernel),並將 CPU 的控制權移交
AUX_MU_IO
[read/write]AUX_MU_LSR[1]
表示有無數據可讀AUX_MU_LSR[6]
表示數據是否已滿,能否繼續寫入使用 system timer
ARMv8
定義了四個例外層級。EL0-EL3,數字越大權限(privilege)越高。
EL0
用於應用程式。無特權模式(unprivileged) EL0
上執行每個 user porcess。EL1
用於作業系統本身。作業系統核心模式(OS kernel mode)EL2
用於使用虛擬機監控程式的場景。虛擬機器監視器模式(Hypervisor mode)EL3
TrustZone® monitor mode確認 CurrentEL
system register (utils.S)
在尚未進行任何切換動作,當前的 Exception Level 應該為 3
在 ARM architecture
下只有當發生異常時,才能夠更改當前的 EL
。執行某些非法指令(例如: access 不存在的 address)則可以觸發。另外還有 interrupts
也被視為特殊類型的異常。
每當有異常時就會觸發下列的動作
ELR_ELn
register (Exception link register
)SPSR_ELn
register (Saved Program Status Register
)exception handler
執行所需的任何工作exception handler
呼叫 eret
指令。該指令從 SPSR_ELn
恢復處理器狀態,並且從保存在 ELR_ELn
register 中的 address 開始恢復執行需要注意的點是 exception handler
並沒有一定要返回到異常所源自的相同位置。若有必要,exception handler
能夠對 ELR_ELn
& SPSR_ELn
的內容進行修改。
EL1
EL1
層具有執行所有常見 OS
任務的正確特權(privilege)集合,所以將作業系統切換到 EL1
是很直觀的
檢視 boot.S
如上 master
function 主要進行一些 system register 的設置
SCTLR_EL1, System Control Register (EL1), Page 2654 of AArch64-Reference-Manual.
SCTLR_I_CACHE_DISABLED (0 << 12)
與 SCTLR_D_CACHE_DISABLED (0 << 2)
禁用指令與數據 cacheSCTLR_MMU_DISABLED (0 << 0)
禁用 MMUHCR_EL2, Hypervisor Configuration Register (EL2), Page 2487 of AArch64-Reference-Manual.
EL1
的執行狀態。0
為 AArch32
,1
為 AArch64
SCR_EL3, Secure Configuration Register (EL3), Page 2648 of Arch64-Reference-Manual.
SPSR_EL3, Saved Program Status Register (EL3), Page 389 of Arch64-Reference-Manual.
eret
後會恢復該狀態。當 EL3
發生異常會自動保存 spsr_el3
。而其為 writeable,所以可以透過重寫它來達成切換 EL
的目的SPSR_MASK_ALL (7 << 6)
切換成 EL1
後,所有類型的中斷都將被禁用(masked)
FIQ mask bit
, IRQ mask bit
, SError interrupt mask bit
SPSR_EL1h (5 << 0)
設置為 EL1h
模式
0
bit 用來選擇 SP
。 "0 means the SP is always SP0" & "1 means the exception SP is determined by the EL"ELR_EL3, Exception Link Register (EL3), Page 351 of AArch64-Reference-Manual.
elr_el3
保存了 address,在執行 eret
後則會返回到該 addressel1_entry (即執行任務的 function)
寫入上述 addressARM.v8 architecture
有四種類型的異常
str
將數據寫入不存在的記憶體位置)Synchronous exception
相反,它始終是由 external hardware 產生,而不是處理器本身生成的normal
,其他中斷配置成 fast
。Linux 中不使用 FIQ
。IRQ
, FIQ
一樣為 asynchronous,由 external hardware 產生。與 IRQ
, FIQ
不同的是它永遠表示某種錯誤情況每個異常類型都需要有自己的 handler。以異常 handler 的角度來看有四種執行狀態(execution states),以在 EL1
工作,執行狀態可以定義如下
EL1
與 EL0
共享 stack pointer)
SPSel
register 值為 0 時會發生SPSel
register 值為 1 時會發生(也是我們當前使用的 mode)總共需要定義 16 個 exception handler
(4 exception levels X 4 execution states),而一個保存所有 exception handler
address 的特殊結構稱為 exception vector table
(exception table
)
參考 page 1876 of the AArch64-Reference-Manual,每個異常佔用 0x80
bytes
透過以下 macro ventry
建立異常 exception table
中的 entries
.align 7
的目的是將所有的異常向量 offset 對齊 0x80
bytes(如上圖所示)entry.S 定義了 16 個異常向量。目前雖然只處理 EL1h
的 IRQ
,仍舊需要定義總共 16 個的 handler,目的是希望看到有意義的錯誤訊息,避免出現問題。針對目前不關注的錯誤型態透過 macro handle_invalid_entry
來處理
show_invalid_entry_message
x0
: 主用使用 entry.h中所定義的 0 ~ 15 的編號(index),使得能夠準確的知道執行了哪個異常 processx1
: 最重要的參數,ESR
(Exception Syndrome Register)實際來自 esr_el1
register,它包含了有關導致異常的原因與詳細訊息x2
: 為在同步異常的情況下很重要的關鍵,它來自前面有提過的 elr_el1
register,其中包含生成異常時已執行的指令 address。在同步異常中,該指令也就是導致異常的指令show_invalid_entry_message
能夠在畫面中顯示當前的異常當 exception handler
處理完成後會希望所有 general register
保持與生成異常前相同的值,若不實現此功能可能會產生不可預測的錯誤。於是透過 macro kernel_entry
來實現
x0 - x30
保存到 stackkernel_exit
從 stack 恢復先前保存的 processor 狀態準備完 exception table
後需將其 address 設置於 vbar_el1
(Vector Base Address Register)
在特定的程式碼段絕對不能被異步中斷攔截。例如 kernel_entry
中進行 general register 的狀態保存,若執行到一半被攔截中斷,processor 的狀態將會被覆蓋、遺失。因此,每當執行 exception handler 時 processor 會自動禁用所有類型的 interrupts
irq.S 中的兩個函式負則 masking & unmasking
ARM processor state 有 4 位,負責控制不同類型中斷的 mask 狀態
synchronous exceptions
SErrors
。之所以命名為 A
是因為 SError
也被稱為 asynchronous aborts
IRQs
FIQs
daifclr
& daifset
都設置 #2
的原因是目前只設置 I
位
interrupt controller
能夠啟用/禁用 hardware 發送的 interrupt。
ENABLE_IRQS_1
register 控制啟用/禁用 interrupts
在 handler 中透過 IRQ_PENDING_1
register 來獲取 0-31
的中斷狀態。透過該 register 能夠檢查當前的中斷是由 timer 產生或者其他 devices,進一步呼叫特定的中斷 handler。
多個中斷可能同時產生,因此每個中斷的 handler 必須確認是否已經完成對於中斷的處理。
Raspberry Pi 的 system timer 具有連接到中斷控制器的 4 條中斷線和 4 個相應的 compare register。當計數器的值等於儲存在 compare register 之一中的值時,就會觸發相對應的中斷。因此需要對 compare register 之一進行非零值的初始化。
第一步更新下一次觸發中斷的時間。
再來將 1
寫入 TIMER_CS
register(稱為 "Timer Control/Status" register),用於確認來自 4 條中斷線中第 1
條的中斷
Process scheduling 是核心任務之一。Scheduling 指的是一個 OS 應該要能夠實現不同 processes 共享 CPU time。其中最困難的事一個 process 不知道 scheduling 的發生,也就是説它將自己視為唯一佔用 CPU 的 process
如果要管理 processes,首先要做的就是建立一個描述 process 的 struct,而在 Linux 中就具有這樣的 struct,它稱為 task_struct
。參考與模仿 Linux 中的實現方式如下
這個 struct 具有以下內容:
cpu_context
x19-x30
(fp: x29
, pc: x30
) 和 sp
的原因是根據 ARM 的調用約定,x0-x18
registers 可以被調用的函式覆蓋,所以不得假定這些 registers value 在調用結束後仍然存在state
counter
priority
priority
複製到 counter
processor time
preempt_count
kernel 啟動後只有一個任務是執行中的狀態,也就是 kernel_main
function。稱其為 "init task" ,在開啟 scheduling 之前,首先必須填充 "init task" 相對應的 task_struct
所有任務存放在 task
。另外透過一個 struct pointer current
指向 init task
kernel_main
function需要關注的重點
copy_process()
使用兩個參數,新 thread 中要執行的函式以及傳遞給該函式的參數。copy_process
會建立新的 task_struct
使其能夠被 scheduler 調度schedule()
是主要的調度 process 功能。它檢查是否有新任務需要搶佔當前的任務。timer interrupt handler 也會呼叫 schedule
呼叫了兩次 copy_process()
都使用 process
函式傳入。process
會不斷 print 出傳入的參數
每個任務都應該具有屬於自己的獨立 stack,因此在建立新任務需要進行正確的記憶體分配。透過以下的方法進行記憶體分配
mem_map
array 保存了每一個記憶體頁面的使用狀態。每當要分配新的頁面時 loop through 這個 array 獲取第一個空閒頁面HIGH_MEMORY
: 系統中的記憶體總量為 1 GB
減去最後的 device registers 保留位LOW_MEMORY
: 前 4 MB
的記憶體保留給 kernel image 和 init task。所有的 memory allocation 由此開始PAGE_SIZE
定義為 4 KB
透過 copy_process
來建立新任務
preempt_diable()
禁用"搶佔",避免在執行過程被切換到其他任務get_free_page()
分配一個尚未被使用的 page。在此 page 底部放置新任務的 task_struct
task_struct
後,初始化其屬性
priority
和 counter
根據當前任務的 priority
來設置state
設置為 TASK_RUNNING
,表示新任務已經準備好開始preempt_count
設置為 1
,表示在執行任務之後,在完成特定初始化工作之前,不進行任務的切換cpu_context
THEAD_SIZE
= PAGE_SIZE
= 4 KB
)pc
設置為 ret_from_fork
functionret_from_fork
function
schedule_tail
function 來啟用搶佔x20
register 中的參數來執行 x19
register 的函式ret_from_fork
之前,從 cpu_context
恢復 x19
, x20
以上過程僅僅是建立新任務到 task array,完成創建後並不會發生切換任務,待後續 schedule
才會執行新任務
schedule
functionschedule
呼叫被加入 timer interrupt handler 中,來定時執行timer_tick
被 interrupt handler 呼叫
counter
減一,若 counter
大於 0
或者當前當前任務禁用搶佔,則返回(不做任何事),否則呼叫 schedule
並啟用中斷。schedule
演算法參考 Linux kernel 第一個發行版的作法
工作原理如下
for
loop through 所有任務,目的是找到 counter
最大處於 TASK_RUNNING
狀態的任務。若找到該條件任務則會跳出外部 while
,然後切換到該任務。for
沒找到符合條件任務,有兩種可能
TASK_RUNNING
狀態任務
counter
皆為 0
for
,對每個任務增加其 counter
for
迭代次數越多,其 counter
越高counter
限制不會大於 2 * priority
while
執行,
TASK_RUNNING
狀態,則 while
迭代的二次就會結束,因為第一次的迭代讓所有任務 counter
不為 0
TASK_RUNNING
,則 while
將會不斷運行直到某個任務轉換成 TASK_RUNNING
狀態。需要思考的部分是若在單個 CPU 上運行,在執行這個 while
時該如何更改任務狀態?
這就能夠理解為什麼執行 schedule
時必須啟用中斷(enable_irq
),中斷會發生執行 schedule
期間,interrupt handler 可以更改任務的狀態,更改某些在等待中斷的任務狀態
找到 counter
非零且處於 TASK_RUNNING
狀態的任務後,呼叫 switch_to
來切換任務
實際的切換工作呼叫了 cpu_switch_to
THREAD_CPU_CONTEXT
為 task_struct
中 cpu_context
結構的偏移量,其為 constant 0
add x8, x0, x10
x0
指向第一個參數的 pointer,也就是當前任務的 task_sturct
x8
指向當前任務的 cpu_context
mov x9, sp
將 x9
register 指向當前 stack pointercallee-saved
registers 按照 cpu_context
的順序保存
stp x19, x20, [x8], #16
從 x19
開始寫回 cpu_context
,依照 cpu_context
順序stp x29, x9, [x8], #16
將 x9
寫回 cpu_context
的 sp
(x29
為 frame pointer)str x30, [x8]
從 x30
寫回 cpu_context
的 pc
(保存了函式返回的 address)callee-saved
registers 按照 cpu_context
的順序恢復
x1
下一個任務 address 加上 THREAD_CPU_CONTEXT
保存到 x8
ldp x19, x20, [x8], #16
從 x19
開始讀回 cpu_context
sp
指向下一個任務 cpu_context
中的 sp
ret
後返回到 link register (x30
) 所指向的 addressret_from_fork
。copy_process
創建新任務時,執行了 pc
的初始化p->cpu_context.pc = (unsigned long)ret_from_fork
pc
)原本的 kernel_entry & kernel_exit 進行 registers 的保存和恢復。加入 scheduler
後能夠將 interrupt handler 視為一個 task,能夠在處理 interrupt 時切換任務,而 interrupt 的返回 eret
依賴以下兩個 registers
elr_el1
: 返回的 addressspsr_el1
: 處理器狀態因此要在處理 interrupt 中切換任務,需要進行以上兩個 registers 和 gerneral registers 的保存與恢復
elr_el1
address 寫入對應 cpu_context
結構的 pc
spsr_el1
狀態寫入對應 task_struct
結構的 state
為了 processes 的隔離,需要將所有 user processes 移至 EL0,這能夠限制它們對 privileged processor operations
的訪問
另外提供 API(system call
) 給 user processes 來 print 出相關訊息
每個 system call
都是 synchronous exception
。如果 user process 需要執行 system call
必須準備必要的參數,然後呼叫 svc
command。
svc
會生成 synchronous exception
,並由 OS 在 EL1 處理。目前實作的 OS 定義 4 個 system call
:
write
: 使用 UART device 在畫面上顯示出訊息clone
: 創建一個新的 user threadmalloc
: 為 user process 分配新的 memory pageexit
: 每個 process 執行完後必須呼叫此 system call
,進行必要的清理system calls
在 sys.csystem calls
handlerswrite
為例
system call
的 index 存放在 w8
registersvc
生成 synchronous exception
x0 ~ x7
用於呼叫 system call
的參數, x8
or w8
用於呼叫 system call
的 index產生 synchronous exception
後,會呼叫已註冊的對應 handler
kernel_entry
esr_el1
(Exception Syndrome Register)。該 register 包含 "exception class",如果 "exception class" == ESR_ELx_EC_SVC64
表示當前的異常由 svc
產生ESR_ELx_EC_SVC64
會進一步執行 el0_svc
,否則顯示錯誤el0_svc
執行的工作
system call
table 加載到 stbl
system call
index 加載到 scno
enable_irq
sc_nr
),若不合法則報錯system call
handler 存入 x16
(stbl + (scno * 8))ret_from_syscall
ret_from_syscall
首先禁用中斷,然後將 x0
register value 存入 stack
(??? 因為接下來要執行的 kernel_exit
將從 stack 恢復 general register value,而希望將原 x0
value 回傳到 user code)
兩個 macro kernel_entry
& kernel_exit
更新為能夠接受一個傳入參數
EL0
與 EL1
使用不同的 stack,此處 EL0
使用了原始 stack sp_el0
,因此在發生異常後 stack pointe 會被覆蓋,所以需要在發生異常的前/後,保存/恢復該 register 的值EL1
中獲取異常不恢復 stack pointer,原因是在異常處理過程中發生 context switch,在 kernel_exit
時, sp 已經被 cpu_switch_to
切換eret
前指定需要返回的 exception level,因為相個資訊儲存在 spsr_el1
中,永遠都會返回到發生異常的 level使用任何 system call
之前,首先需要有在 user mode
下執行的任務。能夠將 kernel task 移動至 user mode
來達成
kenel_main
函式中,創建一個新的 kernel thread,將在 scheduler 開始後,在 kernel model 執行 kernel_process
函式
先前創建新任務在 stack 頂部保留一個區域 (pt_regs 區域
),將會在這裡被使用到
pt_regs
struct 的初始化 (這個區域與 kernel_exit
期望的格式相符合)
pc
指向需要在 user mode 下執行的工作。kernel_exit
將把 pc
複製到 elr_el1
register 確保從異常返回後回到 pc
addresspstate
由 kernel_exit
複製到 spsr_el1
,異常返回後成為處理器狀態。 PSR_MODEL_EL0t
constant 將異常返回到 EL0
stack
為 user stack 分配一個 page,並將 sp
指到 page 頂部task_pt_regs
用於計算 pt_regs
區域位置user_process
透過 move_to_user_mode
後在 user mode 下執行
使用 clone
system call 來啟用兩個任務。其中 clone
需要搭配 malloc
獲取的 stack address
x0-x2
sys_clone
sys_clone
的返回值,
0
,表示當前正在新創建的 kernel thread 中0
,表示其為任務 PID
,在這裡即會直接返回copy_process
現在能夠處理 kernel
or user
thread,clone user thread 時會有新的處理
struct pt_regs * cur_regs = task_pt_regs(current)
第一件事是訪問處理器狀態,該狀態由 kernel_entry
保存*childregs = *cur_regs
將當前處理器狀態複製到新任務的狀態。新狀態下 x0
設置為 0,因為調用者會將 x0
解釋為 system call 的返回值(使用此值來確定是否仍在執行原始的 thread 一部分 or 新 thread)sp
指向新 user stack 的頂部每個任務完成後,透過 exit
system call,其呼叫了 exit_process
,負責停止任務
TASK_ZOMBIE
。如此 schedule 不會在執行該任務。(這種作法使得 parent process
即使在 child process
完成後也能夠查詢相關訊息)