EDK2源码架构:DXE Dispatcher驱动加载顺序与Protocol Database

搜"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 DispatcherDXE 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里调用了InstallProtocolInterfacegEfiPciPlatformProtocolGuid——但这个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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

媳妇爱吃酸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值