当前位置: 代码迷 >> 综合 >> UEFI EVENT 全解
  详细解决方案

UEFI EVENT 全解

热度:23   发布时间:2024-02-20 22:13:38.0

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    

 

UEFI当中,我们的代码运行在一个什么样的环境当中,刚开始接触UEFI的时候也很疑惑我们每天看到的代码到底是在一个什么样的硬件平台和模式下工作,经过多方查找资料和请教高人之后得到了一些结果,下面来和大家分享下,这里以X86架构为例。
    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的工作环境下提供一个异步的事件通知机制,以此来实现一个类似于操作系统里面的多任务机制。它主要完成以下的几个常见的任务:

1.Implementation of protocols that produce an EFI_EVENT to inform protocol consumers when input is available.
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

从上面的三条来看主要完成了,事件同步;事件通知;周期执行任务;周期检测设备状态然后通知driver来完成操作等功能。

Event服务:
BS提供了三个级别的服务CreateEvent(), CreateEventEx(), and CloseEvent(),他们只要来实现对两种基本的服务的操作:EVT_NOTIFY_SIGNAL,EVT_NOTIFY_WAIT这两种Event的最大差别在于他们的Notify function在何时被执行。

EVT_NOTIFY_SIGNAL:当使用SignalEvent()服务使Event处于signaled 状态的时候就会被执行。
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的架构中充当了一个比较重要的角色。

    Event 虽然说很有用,但是乱用也会导致很多的异常状况发生,一旦发生问题,就很难去定位所以我们必须保持一些使用的原则,也就是需要和CloseEvent服务成对出现:


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.

下面是EVT_NOTIFY_WAIT类型,一个键盘读取按键的例子:

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()服务:

    如上图SetTimer()服务主要是用来设置软件Timer的触发时间间隔和触发类型(周期触发还是单次触发)。

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的时间,毕竟这个才是重点。

Task Priority Level(TPL) Services:
    上面提到了各种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:

1. the implementation of a service of a specific protocol requires execution at a specific TPL to be UEFI conformant.不同的drive需要运行在不同的TPL上,我们需要对不同的driver做不同的设
    置,这个可以在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

在UefiLib使用的信号锁服务,也是用TPL来实现的:

 

 

  相关解决方案