Event和Timer在UEFI當中是怎么實現的以及原理,我們先從Timer開始,然后細細的撥開隱藏在底層的實現。
先說Timer,那什么是Timer呢?其實在中文里面我們把它叫做定時/計數器,但是我的理解它不僅僅是一個定時/計數器硬件而是一個被程序設計者設定為工作在特殊模式下的 做定時/計數器 ,僅僅是一個硬件的定時器還不能算是Timer。定時/計數器在幾乎所有的數字處理器系統當中都是一個必備的設備,沒有它我們的各種運行在cpu上的系統軟件都會癱瘓,他們就會變成生活在桃花源當中的世外人一樣,完全沒有時間參考,不知世事更替,所有的原來的秩序都會變得混亂,所以來說他應該是我們系統軟件人員必須要關注和處理的一個設備。
說到 做定時/計數器 ,最經典的當數intel兼容的8253/8254定時器,它幾乎是所有的PC必須兼容支持的一顆IC,當然在其他的微處理器系統當中也是支持的,比如MSC8051,以及其他的微處理器當中。不過Timer不止一種在PC當中有許多的定時器可以作為Timer來使用,比如ACPI timer,HPET timer等等,雖然他們叫法不一,功能強弱不一,所提供給系統軟件設計者的編程接口不完全相同,但是他們都提供了一個最基本的功能,那就是定時,計數功能。幾乎所有的Timer都能在不需要外力干涉的情況下在系統的時鍾脈沖的驅動之下,自動計數,並且在計數值到達預設值的時候會通知cpu去執行特定的動作,並且在處理完之后能自動接着計數,不辭辛勞做着機械性的無差錯的動作。這一點非常有意思,也正是這一點特性,改變了系統軟件的格局,當然這里面也包括了UEFI的核心程序的格局。
再說Event,在上篇當中提到了Event的作用,也就是實現所謂的線程(暫且這么叫,其實它並不是傳統意義的線程,只是有類似多線程的,異步與消息機制,但實際上是單線程工作的)間的同步與消息機制,這里線程的實現靠的就是Timer來實現,由Timer來驅動消息的傳遞和callback服務的相互調用,資源互斥,資源鎖等的實現。
在PC/AT架構UEFI當中我們通常會使用8254來作為核心心跳Timer,它工作在mode 3,時鍾頻率是1.1931816MHz,設定的tick(mTimerPeriod)間隔,默認是1ms(10000個tick基礎計數單位=10000*100ns),並且打開CPU中斷IRQ0。當計數器減到0的時候,就會通過OUT pin給8259的IRQ0發送中斷,這個時候CPU就會中斷當前操作,進行Timer的中斷處理服務。當然前提是我們在此之前有准備好cpu archprotocol和Legacy8259Protocol,設置好中斷向量表,當中斷到來的時候,cpu會從IDT表中依據中斷號,調用Timer中斷服務TimerInterruptHandler。
看下進入Timer中斷前我們需要准備哪些東西,在系統進入DXE階段之后(只有在DXE及后續階段才需要Timer),會處理以下幾件事情:
1.確認中斷是關的,並關掉中斷Cli
2.設置GDT表,根據系統的設定是IA32模式還是X64模式,選擇在EfiruntimeServicesData內存創建並加載不同的GDT表,並且通過相應的跳轉指令,跳轉到相應的段當中去,一般設置兩個段,Code和Data段。
3.分配EfiBootServicesData類型內存,來保存異常向量表,以及異常服務指針數組ExternalVectorTable(默認為0),同時讓設置IDT表中的異常服務地址指向該數組,當異常發生的時候,就能夠調用相對應的服務。此時中斷還是關閉的。
4.install CpuArch protocol,為后續的driver提供cpu相關的訪問方法,比如安裝Timer中斷的中斷向量
5.注冊一個idle event group,Event類型EVT_NOTIFY_SIGNAL,callback服務是一個待機服務,當Event在Timer中斷當中被Signal的時候,如果沒有可用的其他的Event,執行CPU的hlt指令,等待cpu被喚醒。
在CPU的異常向量入口設置一個256項的interrup cate descriptor,在X64保護模式下,填充如下的數據結構,當然在IA32下,IDT只有8Bytes,對應於C語言當中的結構體如下:
typedef union {
struct {
UINT32 OffsetLow:16; ///< Offset bits 15..0.
UINT32 Selector:16; ///< Selector.
UINT32 Reserved_0:8; ///< Reserved.
UINT32 GateType:8; ///< Gate Type. See #defines above.
UINT32 OffsetHigh:16; ///< Offset bits 31..16.
UINT32 OffsetUpper:32; ///< Offset bits 63..32.
UINT32 Reserved_1:32; ///< Reserved.
} Bits;
struct {
UINT64 Uint64;
UINT64 Uint64_1;
} Uint128;
} IA32_IDT_GATE_DESCRIPTOR;
在實際的操作過程當中我們使用匯編來實現一個模板,數據結構表述如下,每一項8Bytes,當cpu發生異常的時候,就會自動把PC指針指向這256項的中斷向量表當中。當PC把下面的A位置的數據讀取到PC當中並執行的時候,就會發生跳轉call到公共異常處理服務
ComInterrEntry當中,同時在跳轉之前會把異常向量B.壓入堆棧當中(其實入棧的並不是函數返回地址,而是中斷向量號),然后再后續的過程當中來處理各種異常。在ComInterrEntry當中我們需要調用我們注冊的256個異常服務,我們會創建一個異常服務數組,使用異常向量來作為索引值來索引,調用不同類型的服務,當然這里面就包括我們的Timer服務。
struct
{
A.) call ComInterrEntry ;1+4(Opcode +ComInterrEntry)=5 Bytes
B.) dw VecNum ;vector number,2Bytes
C.) nop ;1Bytes
}
現在再來看Timer中斷異常是怎么做的,它會提供哪些服務,以下是簡單的列舉。
1.關中斷
2.清除中斷源
3.獲取系統時間鎖(關中斷,雖然中斷此時是關的)
4.調用注冊的具體的Timer服務,這里我們稱之為CoreTimerTick(mTimerPeriod)/CoreTimerTick(100ms),它是在TimerArchprotocol被install的時候由EVT_NOTIFY_SIGNAL類型的Event來觸發注冊的。
5.全局Sytem time累加mEfiSystemTime =mEfiSystemTime+ 1Tick(100ms)
6. 在timer event list雙向鏈表當中查找已經到時間了的event,如果找到,就使用BS->SignalEvent去signal它們。
BS->SignalEvent--->CoreNotifyEvent-->CoreRestoreTpl-->CoreDispatchEventNotifies-->A.如果是EVT_NOTIFY_SIGNAL類型,就清除count(Event->SignalCount = 0),然后嵌套直接調用Event->NotifyFunction (Event, Event->NotifyContext)函數,此時中斷是開的,然后清除該Tpl的gEventPending mask bit,清除該event。
7.釋放系統時間鎖(開中斷,雖然中斷此時是關的)
8.開中斷
注:在完成上面的步驟之前,我們需要先初始化一些數據結構:
1. gEventQueue--- 雙向鏈表數組,A list of event's to notify for each priority level
2. mEfiTimerList----雙向鏈表,所有的等待timer觸發的event列表。
3. gEventSignalQueue ---雙向鏈表A list of events to signal based on EventGroup type
4. 對於EVT_NOTIFY_WAIT類型的event,我們使用BS-> CoreCheckEvent->CoreNotifyEvent-->CoreRestoreTpl-->CoreDispatchEventNotifies來激活,它同樣是往 gEventQueue插入IEvent的節點。
參考資料:
1.event.c,tpl.c,timer.c,cpudxe.c,DxeProtocolNotify.c,CpuAsm.asm,IvtAsm.asm
2.IA32/x64編程指導手冊
3.IA32/X64 calling conventions
X86有很多種工作模式這個我們應該都是知道的,但是我們做BIOS的大概之后關注到幾個模式:實模式,保護模式。模式切換大概就是從實模式 -->保護模式(32bit)-->保護模式(32bit flat 模式)-->保護模式(X64 flat);在post的過程中,在不同的階段cpu會出在不同的模式,一般來說在SEC階段是實模式,在PEI階段是32bit flat 模式,而在DXE及后續階段是保護模式的X64 flat模式。同時還需要注意的是在post過程中雖然說我們現代的CPU都是多核心,但是我們一般都是讓CPU工作在單核心模式下,並且除了一個定時器中斷外,所有的中斷都是關掉的,這里需要注意的是這里說的中斷是指普通的IRQ至於說NMI,SMI這種是不算在之列的。
好了說了半天的模式問題,現在我們正式來聊下今天的主題Event和Timer,其實timer(核心心跳時鍾)的存在主要目的也是為Event來服務所以我們就直接來說Event,timer我們可以在接下來的部分慢慢聊。說白了Event也就是在我們的UEFI的工作環境下提供一個異步的事件通知機制,以此來實現一個類似於操作系統里面的多任務機制。它主要完成以下的幾個常見的任務:
2.Notification when ExitBootServices() is called by an OS Loader or OS Kernel so UEFI Drivers can place devices in a quiescent state or a state that is requiredfor OS compatibility.
3. Notification when SetVirtualAddressMap() is called by an OS Loader or OSKernel so a UEFI Runtime Driver can translate physical addresses to virtual addresses.
4.Timer events used to periodically poll for I/O completion and/or detect timeout conditions.
5.Implementation of protocols that provide non-blocking I/O capabilities where notification of an I/O completion utilizes an EFI_EVENT
BS提供了三個級別的服務CreateEvent(), CreateEventEx(), and CloseEvent(),他們只要來實現對兩種基本的服務的操作:EVT_NOTIFY_SIGNAL,EVT_NOTIFY_WAIT這兩種Event的最大差別在於他們的Notify function在何時被執行。
EVT_NOTIFY_WAIT:當使用CheckEvent() or WaitForEvent()服務來檢測Event的狀態的時候,就會被執行。這種機制用的最多的地方是當BIOS在post過程中需要偵測用戶輸入的時候,這個情況下我們先creat一個EVT_NOTIFY_WAIT類型的Event,然后再使用CheckEvent() or WaitForEvent()服務來檢查是否有用戶輸入的數據存在。如Simple Text Input
Protocols, Pointer Protocols, Simple Network Protocol等等。
有時候我們會希望在OS接管系統的控制權之前在BIOS里面做點什么特別東西,那么我們怎么才能知道OS何時接管系統呢,當然我們有ExitBootServices()服務,這這個服務被調用之后我們的所有的BS的服務和BS data都會消失,那么我們就需要在這個之前來做點特別的事情,這個時候Event就派上用處了,我們可以里面注冊一個Event,在OS loader調用ExitBootServices()服務的時候,它會通知所有的監聽這個Event的特殊服務去做一些事情。同樣UEFI Runtime
Driver也可能需要在SetVirtualAddressMap()服務被調用的時候,做一些特殊的動作,這個時候我們也需要Event。我們有些UEFI driver可能需要偵測設備的狀態,這個時候使用Event會提高cpu的工作效率,減少等待的時間。所以看起來Event在UEFI的架構中充當了一個比較重要的角色。
1.If a UEFI Driver creates events in its driver entry point, those events must be closed with CloseEvent() in the UEFI Driver's Unload() function
2.If a UEFI Driver creates events in its Driver Binding Protocol Start()function associated with a device, those events must be closed with CloseEvent() in its Driver Binding Protocol Stop() function.
3.If a UEFI Driver creates events as part of an I/O operation, the event should be closed with CloseEvent() when the I/O operation is completed.

SignalEvent()服務:
通過它把Event狀態設置為signaled狀態。廣泛用於Simple Text Input Protocols, Pointer Protocols, Simple Network Protoco以及non-blocking I/O驅動當中.
CheckEvent()服務:
用來檢測Event是處於waiting 狀態 或者signaled狀態,這些狀態通常是被SignalEvent()所改變,下面是EVT_TIMER Event的例子,使用CheckEvent()來循環查詢等待Event的狀態被Timer去signal
SetTimer()服務:

Stall()服務:
在實際的driver或者app開發過程中經常需要延時等待的情況,這個時候就需要使用延時服務,當然延時服務我們可以使用上面的單次觸發的Timer event來實現,但是如果我們需要的延時是小於10ms的話,使用timer event就不能實現,這個時候我們就可以使用stall服務。
stall服務是基於定時器的計數延時來實現的,它既可以是用和我們的核心心跳時鍾定時器來實現,也可以使用其他的更高的精度的定時器來實現,比如HPET,PM timer等等來實現,但是需要注意的是在使用這些timer來實現stall服務的時候,可能會關掉中斷,這就會影響到Event的觸發,所以這種情況下,就應該使用Event來實現Stall服務,它也可以使用軟件延時來實現比如while(delay),具體得看code的實現方式。stall服務在32bit環境下可以提供1us到1hr的定時,在64bit模式下可以實現1us到500,000
years的延時,但是這個應該是沒有必要這么久的延時的。我們應該讓延時盡可能的短,這樣才不會影響到boot到OS的時間,畢竟這個才是重點。
上面提到了各種Event在創建的時候都會有一個優先級,關於這個參數我們一直都還沒提到,這里來看下什么是優先級,有那些優先級。一般來說我們的優先級有以下幾種,優先級從低到高依次如下:
TPL_APPLICATION, TPL_CALLBACK, TPL_NOTIFY, and TPL_HIGH_LEVEL,我們一般的app和driver初始的時候是運行在TPL_APPLICATION上,當event 的notification function被觸發的時候,就可以把優先級遷移到高的優先級上去,而這一般是由像Event發信號來觸發的。當timer中斷發生的時候,如果event列表里面的某個event的時間到期了,這個時候event所注冊的notification function就會,中斷低優先級的任務,執行高優先級的任務,來做一些特殊的事情,比如檢測設備狀態,它也可以signal其他的Event,當所有的pending
event notification執行完之后,就會返回到TPL_APPLICATION。我們希望他們盡可能的運行在低的TPL上,花盡可能短的時間運行在高TPL上,同時使用RaiseTPL()的時候輸入的TPL的優先級必須是高於當前的TPL。但是由於以下的一些原因我們需要把當前的運行優先級使用BS提供的RaiseTPL服務來提高其TPL:
置,這個可以在UEFI 2.3.1 SPC的6.1章節,表22查的到,下圖是部分:

2. UEFI Driver that needs to implement a simple lock, or critical section, on global data structures maintained by the UEFI Driver
2. UEFI Driver that needs to implement a simple lock, or critical section, on global data structures maintained by the UEFI Driver
在UefiLib使用的信號鎖服務,也是用TPL來實現的:
