搜"EDK2 DXE"的人很多,但大部分文章只讲概念。本文从Dispatcher.c源码出发,把五级优先级、Depex后缀表达式评估、Protocol三层结构、Notify订阅机制全部拆清楚,配实战踩坑案例。
一、DXE Core烧的"三把火"
公司换了个新老板,第一件事不是开会,是搞清楚三件事:手头有哪些资源、怎么分配任务、定一套规矩。DXE Core也是这个逻辑。
PEI把"家底"(HOB List)交过来之后,DXE Core烧三把火:建协议数据库(让所有驱动能互相找到)、调度驱动加载(决定谁先跑)、管理硬件资源(内存、IO、中断、DMA)。
PEI和DXE最大的区别:PEI是"临时工"——跑完就扔,模块卸载了内存回收。DXE是"正式员工"——驱动一直驻留在内存直到OS启动,驱动之间通过Protocol永久互连。
二、DXE Dispatcher:和PEI Dispatcher的根本不同
调度主循环
// MdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.c
VOID CoreDispatcher (IN EFI_STATUS *ReturnStatus)
{
// 第一步:找到Busy状态的FV,注册所有DXE驱动到调度队列
DiscoverFvAndRegisterDrivers ();
// 第二步:进入调度循环——和PEI不同,这是多轮循环
do {
Status = CoreDispatchOneDriver ();
} while (Status != EFI_NOT_FOUND);
// 第三步:通知Dispatcher Architecture Protocol的注册者
Dispatcher = CoreLocateProtocol (&gEfiDispatcherArchProtocolGuid);
if (Dispatcher != NULL) {
Dispatcher->DispatcherLoaded (Dispatcher);
}
}
PEI vs DXE Dispatcher对比
| 方面 | PEI Dispatcher | DXE Dispatcher |
|---|---|---|
| 调度对象 | PEIM(每个只跑一次) | DXE Driver(驻留内存) |
| 依赖表达 | PPI GUID布尔表达式 | Protocol GUID布尔表达式 + 优先级 |
| 调度策略 | 纯Depex驱动 | Depex + Driver Binding + 优先级排序 |
| FV发现 | 静态(BFV + PPI通知) | 动态(FV HOB + DXE Driver主动通知) |
| 执行后 | PEIM卸载 | 驱动Stay Resident,安装Protocol |
三、五级优先级体系
DXE Dispatcher不是"谁的Depex先满足谁先跑"这么简单。它有五级优先级:
// MdePkg/Include/Pi/PiDxeCis.h
#define DXE_DRIVER_PRIORITY_PLATFORM_PROCESSOR 0x03000000 // 最早
#define DXE_DRIVER_PRIORITY_PLATFORM_CHIPSET 0x03010000
#define DXE_DRIVER_PRIORITY_BUS 0x03020000
#define DXE_DRIVER_PRIORITY_DEVICE 0x03030000
#define DXE_DRIVER_PRIORITY_PLATFORM_APPLICATION 0x03040000 // 最晚
优先级写在每个DXE驱动的PE/COFF头或FV文件扩展头里。Dispatcher先扫一遍所有注册的驱动,按优先级分组,每组内再按Depex评估。
这个设计解决了PEI阶段一个头疼的问题:PEI里只能靠Apriori强制排序,DXE里不需要了——优先级表达得更自然。
四、Depex评估:后缀表达式
DXE驱动的Depex比PEI的复杂,支持Protocol GUID和SOR(Scheduled Order Rule):
// USB键盘驱动的Depex示例
AND
gEfiUsbIoProtocolGuid // 必须有USB IO Protocol
gEfiSimpleTextInputProtocolGuid // 且已有至少一个输入设备
END
Depex是后缀表达式(逆波兰表示法),评估器维护一个操作数栈:
// MdeModulePkg/Core/Dxe/Dispatcher/Dependency.c
BOOLEAN CoreEvaluateDependency (IN EFI_CORE_DRIVER_ENTRY *DriverEntry)
{
// EFI_DEP_PUSH_GUID → 查Protocol是否存在,压栈
// EFI_DEP_AND → 弹两个操作数,逻辑与
// EFI_DEP_OR → 弹两个操作数,逻辑或
// EFI_DEP_NOT → 弹一个操作数,逻辑非
// EFI_DEP_TRUE/FALSE → 压常量
// EFI_DEP_END → 评估结束,栈顶就是结果
}
五、Protocol Database:DXE的"黄页"
三层结构
如果你用过COM、D-Bus或Android的Binder IPC,看到DXE Protocol Database会觉得似曾相识。三层结构:
Handle Database (全局链表)
└── IHANDLE (句柄)
├── 唯一Handle ID
└── PROTOCOL_INTERFACE 链表
├── Protocol GUID
├── Protocol Interface 指针
└── 注册这个Protocol的Agent Handle
// MdeModulePkg/Core/Dxe/Hand/Handle.h
typedef struct {
UINTN Signature; // "hList"
LIST_ENTRY AllHandles; // 连到全局Handle List
LIST_ENTRY Protocols; // 这个Handle上装的所有Protocol
UINTN LocateRequest;
UINT64 Key; // 唯一ID
} IHANDLE;
typedef struct {
UINTN Signature; // "pI/F"
LIST_ENTRY Link; // 连到IHANDLE.Protocols
LIST_ENTRY ByProtocol; // 连到PROTOCOL_ENTRY.AllEntries
IHANDLE *Handle;
PROTOCOL_ENTRY *Protocol;
VOID *Interface; // Protocol具体数据指针
PROTOCOL_NOTIFY *Notify;
} PROTOCOL_INTERFACE;
typedef struct {
UINTN Signature; // "PEnT"
LIST_ENTRY AllEntries; // 连到全局Protocol Entry List
EFI_GUID ProtocolID; // GUID
LIST_ENTRY Protocols; // 所有装了这个Protocol的PROTOCOL_INTERFACE
LIST_ENTRY Notify; // RegisterProtocolNotify的注册者
} PROTOCOL_ENTRY;
双向索引:从Handle能找到它装了什么Protocol;从Protocol GUID能找到所有装了它的Handle。
六、InstallProtocolInterface:驱动"挂牌营业"
安装流程
每个DXE驱动跑起来后,第一件事是安装自己提供的Protocol。这是链式调用:
InstallProtocolInterface
→ CoreInstallProtocolInterfaceNotify
→ InsertTailList // 挂到IHANDLE
→ InsertTailList // 挂到PROTOCOL_ENTRY
→ CoreNotifyProtocolEntry // 通知所有等待者
// MdeModulePkg/Core/Dxe/Hand/Handle.c
EFI_STATUS EFIAPI CoreInstallProtocolInterface (
IN OUT EFI_HANDLE *UserHandle,
IN EFI_GUID *Protocol,
IN EFI_INTERFACE_TYPE InterfaceType,
IN VOID *Interface
)
{
return CoreInstallProtocolInterfaceNotify (
UserHandle, Protocol, InterfaceType, Interface,
TRUE // Notify=TRUE,安装后立刻通知等待者
);
}
InstallMultipleProtocolInterfaces
日常开发没人直接调InstallProtocolInterface——太啰嗦。用InstallMultipleProtocolInterfaces一次装好几个:
// 典型调用:一个Handle上装两个Protocol
EFI_HANDLE HostBridgeHandle = NULL;
Status = gBS->InstallMultipleProtocolInterfaces (
&HostBridgeHandle,
&gEfiDevicePathProtocolGuid, &DevicePath,
&gEfiPciHostBridgeResourceAllocationProtocolGuid, &ResAlloc,
NULL // 参数列表结束
);
ReinstallProtocolInterface
需要更新已安装的Protocol时不能直接改Interface指针(在跑的驱动可能正在用),必须走Reinstall——三步走:通知断开 → 换指针 → 通知重连。
七、LocateProtocol:驱动找服务
安装是"挂牌",查找是"翻黄页":
// MdeModulePkg/Core/Dxe/Hand/Locate.c
EFI_STATUS CoreLocateProtocol (
IN EFI_GUID *Protocol,
IN VOID *Registration,
OUT VOID **Interface
)
{
ProtEntry = CoreFindProtocolEntry (Protocol, FALSE);
// Protocol可能装了不止一份,返回第一个找到的Interface
Prot = CR(ProtEntry->Protocols.ForwardLink,
PROTOCOL_INTERFACE, ByProtocol,
PROTOCOL_INTERFACE_SIGNATURE);
*Interface = Prot->Interface;
return EFI_SUCCESS;
}
LocateHandleBuffer用于遍历所有装了某个Protocol的Handle(比如枚举所有PCI设备)。
八、Notify机制:等Protocol的"订阅-发布"
有的驱动依赖的Protocol可能还没安装——比如USB键盘驱动需要EFI_USB_IO_PROTOCOL,但USB控制器驱动还在后面排着队。RegisterProtocolNotify派上用场:
// MdeModulePkg/Core/Dxe/Hand/Notify.c
EFI_STATUS CoreRegisterProtocolNotify (
IN EFI_GUID *Protocol,
IN EFI_EVENT Event,
OUT VOID **Registration
)
{
ProtEntry = CoreFindProtocolEntry (Protocol, TRUE);
// 分配PROTOCOL_NOTIFY,挂在ProtEntry的Notify链表
ProtNotify = AllocatePool (sizeof(PROTOCOL_NOTIFY));
ProtNotify->Event = Event; // Protocol安装时Signal这个Event
InsertTailList (&ProtEntry->Notify, &ProtNotify->Link);
// 防竞态:万一本Protocol在Register之前刚好装了
if (!IsListEmpty (&ProtEntry->Protocols)) {
CoreSignalEvent (Event); // 已经有了,直接通知
}
return EFI_SUCCESS;
}
当任何驱动调用InstallProtocolInterface时,CoreNotifyProtocolEntry遍历Notify链表,Signal每个Event。被卡住的驱动从WaitForEvent返回,用LocateProtocol获取刚装好的Interface。
九、实战踩坑:Dispatcher死循环
某次发布前紧急修了一个PCIe驱动bug,重新编译FV。烧进去系统卡在DXE Dispatcher死循环——日志疯狂刷"Dispatch 0xXXXX Status SUCCESS",但就是不走下一个驱动。
根因
修复后的PCIe驱动Depex是AND gEfiPciRootBridgeIoProtocolGuid gEfiPciPlatformProtocolGuid END。每次调度执行后,它在Entry里调用了InstallProtocolInterface装gEfiPciPlatformProtocolGuid——但这个Protocol之前已经装过了!CoreInstallProtocolInterface返回EFI_ACCESS_DENIED(不允许在一个Handle上装同名Protocol两次)。
驱动作者没检查返回值。Dispatcher继续扫描,发现下一个驱动的Depex需要这个Protocol的新版本,但装不上去。这个驱动被放回等待队列,PCIe驱动又被重新调度——形成调度-失败-重调度死循环。
修复
在InstallProtocolInterface之后加ASSERT_EFI_ERROR(Status)。如果有EFI_ACCESS_DENIED,说明设计有问题——一个Protocol不应该被同一个驱动重复安装。
教训:DXE驱动的Entry Point必须检查InstallProtocolInterface的返回值。 PEI阶段也许可以懒一点,DXE阶段不行——Dispatcher会把失败吞掉然后重试,让你以为一切正常。
十、实战踩坑总结
| 坑 | 现象 | 根因 | 经验 |
|---|---|---|---|
| Dispatcher死循环 | 日志疯狂刷Dispatch SUCCESS | 重复安装同名Protocol返回ACCESS_DENIED没检查 | Entry里ASSERT_EFI_ERROR |
| Depex评估误解 | 驱动提前执行导致崩溃 | 以为AND是短路评估 | 实际是全量评估后逻辑与 |
| Notify竞态 | 驱动等不到Protocol通知 | Register前Protocol刚装好 | RegisterProtocolNotify内部有二次检查 |
| Reinstall没通知 | 使用者拿到旧Interface | 直接改指针没走Reinstall | 必须三步:通知断开→换指针→通知重连 |
| 优先级设错 | 总线驱动比平台驱动先跑 | 优先级宏填反 | Platform Processor最早,Application最晚 |
源码路径索引
| 内容 | 关键文件 |
|---|---|
| DXE Dispatcher主循环 | MdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.c |
| Depex评估 | MdeModulePkg/Core/Dxe/Dispatcher/Dependency.c |
| 驱动注册与FV发现 | MdeModulePkg/Core/Dxe/Dispatcher/Driver.c |
| Protocol安装/重安装 | MdeModulePkg/Core/Dxe/Hand/Handle.c |
| Protocol查找 | MdeModulePkg/Core/Dxe/Hand/Locate.c |
| Notify机制 | MdeModulePkg/Core/Dxe/Hand/Notify.c |
| DXE服务表 | MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c |
| PI规范 | PI Specification Volume 1, Chapter 7-10 |
914

被折叠的 条评论
为什么被折叠?



