UEFI不再为开发者提供中断支持,但在UEFI内部还是使用了时钟中断,所有的异步操作都要通过事件(Event)完成。在Legacy BIOS中,要处理异步事件或实现一些特定的功能,需要通过中断模式
传统的中断模式
常用中断分为硬件中断和软件中断
硬件中断
- 敲键盘事件:
- 敲键盘→键盘信号传输给EC→EC产生硬件中断 (IRQ1)到CPU→中断信号输入到CPU的中断控制器→OS执行对应的软件代码来获取键盘按键
- 插入AC事件:
- 插入AC→信号传输给EC→EC产生硬件中断 (SCI)到CPU的特定GPIO pin→中断信号输入到CPU的中断控制器→OS执行对应的软件代码来获取AC状态
软件中断
- 比如调用显示服务,通过INT10h中断
显示服务(Video Service – INT10H)
功能号 | 功能 | ||
---|---|---|---|
00H | 设置显示器模式 | 0CH | 写图形像素 |
01H | 设置光标形状 | 0DH | 读图形像素 |
02H | 设置光标位置 | 0EH | 在Teletype模式下显示字符 |
03H | 读取光标信息 | 0FH | 读取下面是其模式 |
04H | 读取光笔位置 | 10H | 颜色 |
05H | 设置显示页 | 11H | 字体 |
06H、07H | 初始化或滚屏 | 12H | 显示器的配置 |
08H | 读光标出的字符及其属性 | 13H | 在Teletype模式下显示字符串 |
09H | 在光标处按指定属性显示字符串 | 1AH | 读取、设置显示组合编码 |
0AH | 在当前光标处显示字符串 | 1BH | 读取功能、状态信息 |
0BH | 设置调色板,背景色或者边框 | 1CH | 保存、恢复显示器状态 |
Event Overview
- UEFI中没有使用中断
- UEFI是一个单线程的系统,如果没有中断,则所有UEFI任务只能被依次执行,类似一个FIFO的任务队列。
- Event (事件)就是当某个条件满足时, 就去执行某一段代码, 比如:
- 当某个protocol被install时, 去执行一段代码
- 当定时器每一秒时, 去执行一段代码
- 当手动触发某一个事件时, 去执行一段代码
- 当代码执行完成, 就回到原来被中断的代码处继续执行
- Event机制也有一些扩展功能,比如在特定的条件下,一次性执行多个任务(EventGroup),节约系统资源和简化调用复杂度。
- Event机制通过设定任务优先级(TPL)来控制任务执行时机
事件类型 | 事件特征 |
---|---|
EVT_TIMER | 定时器事件。普通Timer事件,没有Notification函数。生成事件后需调用SetTimer服务设置时钟属性。事件可以:通过SetTimer()设置等待事件、到期后通过SignalEvent()触发、通过WaitForEvent()等待事件被触发、通过CheckEvent()检査状态 |
EVT_NOTIFY_WAIT | 普通事件。这个事件有一个Notification函数,当这个事件通过CheckEventO检査状态或通过WaitForEvent()等待时,这个Notification函数会被放到待执行队列 gEventQueue[Event->NotifyTpl]中 |
EVT_NOTIFY_SIGNAL | 普通事件。这个事件有一个Notification函数,当这个事件通过SignalEvent()被触发时,这个Notification函数会被放到待执行队列gEventQueue[Event->NotifyTpl]中等待执行 |
0x00000000 | 普通事件。此类事件没有Notification函数。事件可以: 通过SignalEvent()被触发、通过WaitForEvent()等待事件被触发 、通过CheckEventO检査状态 |
EVT_TIMER EVT_NOTIFY_WAIT | 带Notification函数的定时器事件。此类事件除了具有EVT_TIMER的特性外, 还有EVT_NOTIFY_WAIT的特性,即到期后通过SignalEvent()触发。当事件通过CheckEventO检査状态或通过WaitForEvent()等待时,这个Notification 函数会被放到待执行队列 gEventQueue[Event->NotifyTpl]中 |
EVT_TIMER EVT_NOTIFY_SIGNAL | 带Notification函数的定时器事件。此类事件除了具有EVT_TIMER的特性外, 还有EVT_NOTIFY_WAIT的特性,即到期后通过SignalEvent()触发。当事件通过SignalEvent()被触发时,这个Notification函数会被放到待执行队 列 gE ventQueue [ Event->N otify Tpl]中 |
Event Function
启动服务为开发者提供了下表所示的函数,用于操作事件、定时器及TPL (任务优先 级)。这些函数可以分为三类:事件相关函数、定时器相关函数及TPL相关函数。
函数名 | 作 用 |
---|---|
CreateEvent | 生成一个事件对象 |
CreateEventEx | 生成一个事件对象并将该事件加人到一个组内 |
CloseEvent | 关闭事件对象 |
SignalEvent | 触发事件对象 |
WaitForEvent | 等待事件数组中的任一事件触发 |
CheckEvent | 检査事件状态 |
SetTimer | 设置定时器属性 |
RaiseTPL | 提升任务优先级 |
RestoreTPL | 恢复任务优先级 |
//生成一个事件
typedef
EFI_STATUS (EFIAPI *EFI_CREATE_EVENT)(
IN UINT32 Type, //事件类型
IN EFI_TPL NotifyTpl, //事件 Notification 函数的优先级
IN EFI_EVENT_NOTIFY NotifyFunction, OPTIONAL //事件 Notification 函数
IN VOID *NotifyContext, OPTIONAL //传给事件 Notification 函数的参数
OUT EFI_EVENT * Event //生成的事件
);
NotifyFunction
- 当Event类型为EVT_NOTIFY_WAIT或EVT_NOTIFY_SIGNAL时,此function有效。
- EVT_NOTIFY_WAIT类型的Event,在等待过程中,NotifyFunction被调用。
- EVT_NOTIFY_SIGNAL类型的Event,Event被Signal时,NotifyFunction被调用。
- NotifyFunction原型如下:
typedef VOID(EFIAPI *EFI_EVENT_NOTIFY)(
IN EFI_EVENT Event, //拥有此函数的事件
IN VOID *Context //上下文指针:此指针在CreateEvent时设置
);
CreateEventEx
- 相比较CreateEvent,它多一个EventGroup参数。其它的参数均和CreateEvent一致。
- 它会将创建成功的Event存在这个EventGroup(一个GUID类型的指针)。
- 当组内的任意Event被触发,则所有的Event都会被触发,所有的NotifyFunction函数都会被加入到执行队列,并按优先级高低来执行。
- 当传入的EventGroup为空时,则退化为CreateEvent函数。
- EVT_SIGNAL_EXIT_BOOT_SERVICES和EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE这两种Type是无效的,因为已经有这两个EventGroup。
// 为 EventGroup 生成一个 Event
typedef EFI_STATUS (EFIAPI *EFI_CREATE_EVENT_EX)(
IN UINT32 Type, //事件类型
IN EFI_TPL NotifyTpl, //事件 Notification 函数的优先级
IN EFI_EVENT_NOTIFY NotifyFunction, OPTIONAL //事件 Notification 函数
IN VOID *NotifyContext, OPTIONAL //传给事件 Notification 函数的参数
IN CONST EFI_GUID *EventGroup OPTIONAL, //事件组
OUT EFI_EVENT *Event //生成的事件
UEFI目前预定了四种EventGroup:
- EFI_EVENT_GROUP_EXIT_BOOT_SERVICES
- EFI_EVENT_GROUP_VIRTUAL_ADDRESS_CHANGE
- EFI_EVENT_GROUP_MEMORY_MAP_CHANGE
- EFI_EVENT_GROUP_READY_TO_BOOT
- 也可以自定义EventGroup
- 比如使用EFI_ACPI_TABLE_GUID,当ACPI Table data更改,执行InstallConfigurationTable()时,所有该组的Event都会被触发
CheckEvent
typedef EFI_STATUS (EFIAPI *EFI_CHECK_EVENT) ( IN EFI_EVENT Event);
- 用来Check某个Event的触发状态。
- 如果是EVT_NOTIFY_SIGNAL类型的Event,则返回EFI_INVALID_PARAMETER。
- 如果Event处于触发态,则返回EFI_SUCCESS,并且返回前重置为非触发态。
- 如果Event处于非触发态且无NotifyFunction,则返回EFI_NOT_READY。
- 如果Event处于非触发态且有NotifyFunction,则将NotifyFunction加入执行队列,之后重新check状态,如果执行NotifyFunction导致Event处于触发态,则重置状态后返回EFI_SUCCESS,否则返回EFI_NOT_READY
SignalEvent
typedef EFI_STATUS SignalEvent ( IN EFI_EVENT Event );
- 用于将给定的Event置为触发态。如果Event类型为EVT_NOTIFY_SIGNAL,则将其NotifyFunction加入到执行队列,如果该Event属于某个EventGroup,则将该Group里的所有Event都置为触发态,且将所有的NotifyFunction都加入到执行队列。
- 当需要Signal一个EventGroup时,可以给该Group创建一个Event,之后去Signal此Event,最后再将此Event Close掉,从Group中移除
CloseEvent
typedef EFI STATUS CloseEvent ( IN EFI EVENT Event);
- 当Event使用完毕后,必须调用CloseEvent来关闭Event。
- 当某个Event被close之后,如果它属于某个EventGroup,则会从该Group中被移除。
- 当Event被Close之后,会释放掉它的内存,并从内核队列中被移除,Event将不再有效,也不会被任何队列调用。
Event Use
ReadyToBootEvent
- ReadyToBootEvent是一个UEFI预定义的EventGroup。它用来给UEFI在Boot OS之前做最后的操作使用。
- Boot OS之前,也需要将整个EventGroup signal。
RegisterProtocolNotify - 它用来创建一个Event,使得当给定的Protocol被install或ReInstall的时候,对应的Event被触发
它的用法一般分两步:- 创建一个需要被执行的Event,以及它的NotifyFunction。
- 使用RegisterProtocolNotify来指定特定的Protocol,以便Protocol被Install时可以触发Event。
下面是一个实例 - 当gEfiLenovoEaiaInterfaceProtocolGuid protocol在EaiaDxeEntryPoint()里被Install的时候,会触发ProtocolEvent,随之该Event的NotifyFunction(InitRemovableBattery)被加入执行队列执行。
EfiNamedEventListen/ EfiNamedEventSignal
- 前者的作用是创建一个Event,并使用RegisterProtocolNotify将这个Event Register,Event在等待指定的Protocol被Install。
- 后者的作用是Install前者指定的Protocol,使创建的Event被触发,以执行Event的NotifyFunction。两者成对出现。
TPL
优先级用来区分事件的先后执行顺序,它可以是0 ~ 31的一个整数。UEFI预定义了以下4个优先级。
UEFI不提供中断接口,但是UEFI又是一个单线程的,所以为了更好的协调各个任务,利用CPU等各类资源,UEFI引入了任务优先级的概念。
- 比如定时器Event的NotifyFunction的优先级要大于一般的UEFI_APPLICATION。
- gBS->Stall这种和时钟有关的任务要大于任何任务的优先级。
- UEFI使用RaiseTPL/RestoreTPL来提升和恢复任务的优先级。
- RaiseTPL和RestoreTPL一般成对出现,并且一旦任务优先级被Raise之后,要尽快的Restore,以免影响其他任务的执行。
- 当任务的优先级被提升至TPL_HIGH_LEVEL时,时钟中断将被关闭,等优先级恢复后,中断会被重新打开。
- 任务的优先级恢复到原优先级之前,所有高于原优先级的处于触发态事件的NotifyFunction都要被执行完毕。
#define TPL_APPLICATION 4
#define TPL_CALLBACK 8
#define TPL_NOTIFY 16
#define TPL_HIGH_LEVEL 31
优先级 | 用法 | 相关函数 |
---|---|---|
TPL_APPLICATION | 这 是 预 定 义 的 4 个 级 别 中 最 低 的 一个优先级。应用程序运行(包括 Boot Manager 和 OS Loader)在这个级 别。当程序运行在这个级别时,任务 队列中没有任何处于就绪状态的事件 Notification 函数 | 下列函数运行在此级别:ExitBootServices() > WaitForEvent()、User Manager Protocol/Identify() Form Browser2 Protocol/SendForm下列函数运行在此级别或更低级别:Simple Input Protocol |
TPL一CALLBACK | 比较耗时的操作通常在这个优先级 执行,如文件系统、磁盘操作等 | 下列函数运行在此级别或更低级别:Exit();Serial I/O Protocol、Unloadlmage()、Variable Services、 Network Service Binding. Network Protocol下列函数运行在低于8的级别:Loadlmage()、Startlmage() |
TPL_NOTIFY | 运行在这个级别的程序不允许阻塞, 必须尽快执行完毕并且返回。如果需要更多操作,则需要使用Event由内核重新调度。通常,底层的IO操作允 许在这个级别,例如UEFI内核中读取键盘状态的代码。 | 大部分Event的Notification函数允许在这个级别下列函数运行在此级别或更低级别:Protocol Handler Services、Memory Allocation Services、Simple Text Output Protocol、ACPI TableProtocol、User Manager Protocol、User Credential Protocolx User Info Protocolx Authentication Info、 Device Path Utilities、Device Path From Text、 EDID Discovered、EDID Active> Graphics OutputEDID OverrideiSCSI Initiator Name、Tape IO、 Deferred Image Load Protocol、HII ProtocolssDriver Health下列函数运行在低于16的级别:ACPI Table Protoco |
TPL_HIGH_LEVEL | 优先级最高级别。在此级别,中断 被禁止。 | UEFI内核全局变量的修改需 要允许在这个级别下列函数运行在此级别或更低级别:SignalEvent()、Stall()下列函数运行在低于31的级别:CheckEvent()、CloseEvent()> CreateEvent()、 SetTimer()5Event Notification Levels 运行在(TPL_ APPLICATION, TPL_HIGH_LEVEL)区间的优 先级上 |
RaiseTPL用来将任务优先级提升至NewTPL,返回值为任务原优先级。
RestoreTPL用于恢复(通常是降低)任务优先级至原来的优先级。
typedef EFI_TPL (EFIAPI *EFI_RAISE_TPL) ( IN EFI_TPL NewTpl );
//恢复当前任务任务优先级至原来的OldTpl
typedef VOID (EFIAPI *EFI_RESTORE_TPL) ( IN EFI_TPL OldTpl )
Timer
定时器是一类特殊的事件,生成定时器事件后,可以通过SetTimer服务设置定时器属性。
/**设置定时器属性(类型及时间>
Qretval EFI_SUCCESS 属性设置成功
Qretval EFI_INVALID_PARAMETER 参数Event不是EVT_TIMER,或参数Type非法
**/
typedef EFI_STATUS (EFIAPI *EFI
IN EFI_EVENT Event, // Timer 事件
IN EFI_TIMER_DELAY Type,//定时器类别
IN UINT64 TriggerTime //定时器过期时间,100ns为一个单位
);
定时器的类别有三种:TimerCance、TimerPeriodic 、TimerRelative。
Type取值 | 作 用 |
---|---|
TimerCancel | 用于取消定时器触发时间。设置后定时器不再触发 |
TimerPeriodic | 重复型定时器。每TriggerTime* 100ns,定时器触发一次 |
TimerRelative | 一次性定时器。TriggerTime* 100ns时触发 |
- 定时器事件的类型必须是EVT_TIMER
- 如果调用SetTimer时
- 定时器类型为TimerPeriodic,且TriggerTime为0,则Event在每个时钟Tick时被触发一次。
- 定时器类型为TimerRelative,且TriggerTime为0,则Event在下一次始终Tick时被触发。 - 使用定时器事件一般分两步:
- 使用CreateEvent创建一个EVT_TIMER类型的Event。
- 使用SetTimer设置此Event的定时器属性。 - 定时器事件也可以和EVT_NOTIFY_SIGNAL类型搭配使用。
- 创建Event的时候,Type类型设定为EVT_TIMER|EVT_NOTIFY_SIGNAL。
//生成Timer事件,i设置触发函数
Status = gBS->CreateEvent(EVT_TIMER | EVT_NOTIFY_SIGNAL, TPL_CALLBACK, EFI_EVENT一NOTIFY)myEventNoify30, (VOID*)LnHello, Time Out!nr &myEvent);
//设置"Timer^待时间为10秒,属性为循环等待
Status = gBS->SetTimer(myEvent,TimerPeriodic , 10 * 1000 * 1000);
WaitKey();
Status = gBS->CloseEvent(myEvent);
系统时钟中断驱动的定时器及相关函数
DXE Foundation 提供了时钟中断的服务例程,但设计上DXE Foundation 是硬件无关的,所以它不能直接操作系统时钟中断,而是依赖Timer Architectural Protocol 来设置自己的时钟中断服务。位于DxeProtocolNotify.c
//
// Do special operations for Architectural Protocols
//
if (CompareGuid (Entry->ProtocolGuid, &gEfiTimerArchProtocolGuid)) {
//
// Register the Core timer tick handler with the Timer AP
//
gTimer->RegisterHandler (gTimer, CoreTimerTick);
}
从上面这段代码 可以看出,当DXE Foundation 发现Timer Architectural Protocol 被安装,就会调用Timer Artchitectural Protocol 的RegisterHandler() 函数去注册自己的时钟中断服务函数CoreTimerTick() , 该函数会在每次时钟中断发生时被调用 。
UEFI 的时钟处理函数是 CoreTimerTick,位于 MdeModulePkg\Core\Dxe\Event\Event. c文件中。它在时钟中断中调用,是时钟中断处理函数的主体。该函数执行期间必须关中 断并且不能被其他任何任务干扰,因而进入函数时需要加锁,离开函数时需要解锁。它的 主要功能是维持系统时间,检查定时器事件列表中是否有到期的事件。
VOID
EFIAPI
CoreTimerTick (
IN UINT64 Duration
)
{
IEVENT *Event;
//
// Check runtiem flag in case there are ticks while exiting boot services
//
CoreAcquireLock (&mEfiSystemTimeLock);
//
// Update the system time
//
mEfiSystemTime += Duration;
//
// If the head of the list is expired, fire the timer event
// to process it
//
if (!IsListEmpty (&mEfiTimerList)) {
Event = CR (mEfiTimerList.ForwardLink, IEVENT, Timer.Link, EVENT_SIGNATURE);
if (Event->Timer.TriggerTime <= mEfiSystemTime) {
CoreSignalEvent (mEfiCheckTimerEvent);
}
}
CoreReleaseLock (&mEfiSystemTimeLock);
}
首先,CoreTimerTick() 取得保护mEfiSystemTime 全局变量的锁。DXE Foundation 用mEfiSystemTime 全局变量追踪时钟中断启动以来经过的时间(以100 ns) 。每次时钟中断发生时,此次时钟中断距上次时钟中断的时间差被加入mEfiSystemTime.