UEFI 软中断
背景
熟悉linux的朋友会知道linux对于中断实现方法。简单讲linux 把一个外部中断分为两部分顶半部和底半部,这样设计目的是因为中断来了,需要快速响应,会抢占当前的进程,但是中断一般需要和外设打交道,这样会导致他们执行需要执行很长时间,有的还需要睡眠,这样会导致系统的吞吐量下降,所以linux 为了解决这个问题把中断分为两部分,顶半部分负责响应中断,底半部分负责执行中断的大量处理函数,并且顶半部分不允许中断嵌套,底半部分允许中断嵌套。软中断是linux对于底半部分的一种实现方式。比如内核定时器是借助硬件中断和软中断的实现,Tasklet是借助于软中断实现的,所以参考内核定时器和Tasklet,就会发现UEFI 里面的timer,event 和这两个长的挺像的。软中断顾名思义是用软件模拟硬件中断,是纯软件的东西和硬件架构无关,所以它有很好的拓展性,兼容性。
UEFI 采用了这种思想,它的Timer也是用硬件计时器中断+软中断实现,Event 和linux tasklet 类似用的软中断实现。所以UEFI 保留了一个计时器硬件中断,其他都用软中断实现。
原理
UEFI 软中断优先级目前代码里用三种:
- TPL_CALLBACK , TPL_NOTIFY , TPL_HIGH_LEVEL - 1 (Timer 用)
而其他TPL_APPLICATION(普通程序), TPL_HIGH_LEVEL(一般用作互斥)
- TPL_CALLBACK , TPL_NOTIFY , TPL_HIGH_LEVEL - 1 (Timer 用)
UEFI 软中断触发方法:
- 显示调用SignalEvent
UEFI 软中断执行时机
- 中断上下文中硬件中断将要退出的时候,打开中断(因为硬件中断不允许嵌套所以进入执行硬中断程序时候是需要关闭外部中断),执行软中断处理程序。
- 普通程序上下文发生优先级从高到低变化的时候
抢占
- 硬件中断优先级最高,可以抢占软中断,普通程序
- 软中断可以抢占优先级低于自己的软中断或普通程序
- 因为有了抢占就会涉及竞态那么需要锁,不过UEFI 是单核在跑,程序也不大,也不会太在意锁的效率,所以它的锁的机制比较简单,就是根据抢占规则提升相应的优先级,例如提升到TPL_HIGH_LEVEL 的同时会关闭硬件中断,相当于关闭抢占。
有了上面几点的知识背景再来看代码就知道程序为什么那么写,锁的位置怎么加,用什么优先级的锁。
定时器
前面有提到,定时器是用到硬件中断和软中断来实现的,代码如下。CoreTimerTick 是在硬件中断上下文中,回顾一下,UEFI 中断处理流程 (1)中断处理函数开头关闭中断(2)增加系统时间 (3)检测定时器列表中是否有定时时间到Timer Event, 如果有触发一个软中断(4)在硬中断函数末尾打开中断并且执行软中断(软中断是可以嵌套)。
CoreAcquireLock, CoreReleaseLock 锁的机制前面也有提,UEFI 是通过提高当前任务的优先级来阻止抢占从而保护临界资源。下面代码mEfiSystemTimeLock设定的优先级是TPL_HIGH_LEVEL也就是说在下面代码中CoreAcquireLock, CoreReleaseLock之间是禁止抢占的,那么执行软中断程序和开中断步骤UEFI设计者把它放到CoreReleaseLock里的RestoreTPL中了。
MdeModulePkg/Core/Dxe/Event/Timer.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);
}
接下来看一下CoreSignalEvent(代码如下),这个function可以在中断上下文执行,也可以在普通程序里显示调用CoreSignalEvent, 因为两个执行上下文的优先级不同,会发生抢占,所以这个function 里处理临界区的值需要上锁,因为发生抢占是硬中断抢占普通程序,这里需要用到锁需要把优先级调到合适的位置。CoreSignalEvent里面用到的锁是CoreAcquireEventLock 优先级是TPL_HIGH_LEVEL。
下面就是软件模拟硬件发中断,他会把这些软中断挂在panding 这个全局变量上,panding 会跟据这个软中断的优先级把相应的bit 置起来,用来模拟几号中断正在触发,所有的软中断共享相应的中断号(优先级)。接下来看看下面锁的应用1和4 上面有提到保护临界区的数据,2和3 为啥要先释放锁再获取呢? 其实看过CoreNotifySignalList 就知道了,这个function 也是会在中断上下文和普通程序上下文里被调用,所以为了保护临界区的数据CoreNotifySignalList 内部也调用CoreAcquireEvent, 所以在CoreNotifySignalList 之前先把锁释放掉。
EFI_STATUS
EFIAPI
CoreSignalEvent (
IN EFI_EVENT UserEvent
)
{
IEVENT *Event;
Event = UserEvent;
...
1: CoreAcquireEventLock ();
if (Event->SignalCount == 0x00000000) {
Event->SignalCount++;
if ((Event->Type & EVT_NOTIFY_SIGNAL) != 0) {
if ((Event->ExFlag & EVT_EXFLAG_EVENT_GROUP) != 0) {
2: CoreReleaseEventLock ();
CoreNotifySignalList (&Event->EventGroup);
3: CoreAcquireEventLock ();
} else {
CoreNotifyEvent (Event);
}
}
}
4: CoreReleaseEventLock ();
return EFI_SUCCESS;
}
回过头来再来分析CoreTimerTick() 中的CoreSignalEvent (mEfiCheckTimerEvent); 查代码可以知道这个mEfiCheckTimerEvent 是TPL_HIGH_LEVEL-1 比较特殊,因为其他Event 优先级只能用TPL_CALLBACK_LEVEL / TPL_NOTIFY_LEVEL中的一个,这样做目的就是保证TimerEvent 的处理程序优先级比普通Event 优先级高,就能抢占其他Event或在普通程序,同时它的优先级不会导致硬件计时器中断被关闭。下面简单分析一下,这个mEfiCheckTimerEvent挂的handler(CoreCheckTimers() ),这个function 还是比较简单,主要就是检查Timer队列中有没有到期的,如果有把相应的软中断使能起来,如果是周期性的timerEvent 就把它的下一次触发时间计算一下,再一次加入Timer队列中,我们可以看到这个函数里用到的锁是 mEfiTimerLock 它的优先级是TPL_HIGH_LEVEL-1 也就是可以阻止除了硬件中断之外包括自己的抢占。那么CoreCheckTimers运行期间硬件中断会有机会抢占进来,但是还是小概率,因为定时器延时时间相较于代码执行时间还是太长了,说不定不到硬件中断来,CoreCheckTimers已经执行结束了。
其实到这里UEFI Event基本分析结束,原理性东西弄明白了,包括UEFI Event设计模型思想,抢占类型,我们可以很好应用它,写出比较健壮的Event, 因为我们要考虑优先级,临界区资源的保护,锁的应用等等。
其他Event的操作,CreateEvent,CheckEvent,WaitforEvent,看看代码就好了,不是重点。但是其中有一个个人觉得比较有意思的Event是gIdleLoopEvent,这个是EventGroup,由Platform Code 去实现一个,主要是让CPU进入Idel 状态,或者其他Cx 状态,原来不仅仅操作系统有Idel 进程,UEFI 也弄了一个Idel Event 让CPU 进入相应的省电模式或者低延时唤醒状态。