Implementing Asynchronous I/O Callbacks in UEFI
The Unified Extensible Firmware Interface (UEFI) operates in a
single-threaded, cooperative execution environment, yet it frequently
requires non-blocking hardware operations to maintain performance. This
article explains how UEFI drivers implement asynchronous I/O completion
callbacks using the native EFI_EVENT architecture, Task
Priority Levels (TPL), and token-based driver protocols. By leveraging
these primitives, drivers can initiate hardware requests, register
completion handlers, and invoke callbacks when data transfers finish
without stalling execution.
The UEFI Event-Driven Architecture
Because UEFI does not support native preemptive multithreading,
asynchronous operations rely entirely on an event-driven architecture
managed by UEFI Boot Services. The foundation of this system is the
EFI_EVENT type.
To implement an asynchronous model, a driver or caller creates an event associated with a notification function (the callback) and assigns it a specific Task Priority Level (TPL). The firmware core executes this callback only when the event is explicitly signaled and the current execution context operates at a lower priority level.
Creating and Registering Completion Events
An asynchronous workflow begins with the creation of an event using
the CreateEvent or CreateEventEx Boot Services
API:
EFI_STATUS Status;
EFI_EVENT CompletionEvent;
Status = gBS->CreateEvent (
EVT_NOTIFY_SIGNAL,
TPL_CALLBACK,
MyIoCompletionCallback,
ContextData,
&CompletionEvent
);EVT_NOTIFY_SIGNAL: Specifies that the event's callback function will execute when the event transitions to the signaled state viagBS->SignalEvent().TPL_CALLBACK: Sets the execution priority of the callback. UEFI defines hierarchical levels (TPL_APPLICATION,TPL_CALLBACK,TPL_NOTIFY, andTPL_HIGH_LEVEL). UsingTPL_CALLBACKensures the completion routine can execute safely without interrupting critical system state changes or high-level timer interrupts.MyIoCompletionCallback: The function pointer executed upon completion.ContextData: A pointer to caller-defined state information passed directly to the callback routine.
Token-Based Asynchronous Protocols
Modern UEFI specifications standardize asynchronous operations using
token structures. Protocols such as EFI_BLOCK_IO2_PROTOCOL,
EFI_DISK_IO2_PROTOCOL, and
EFI_SIMPLE_NETWORK_PROTOCOL implement this pattern.
For example, EFI_BLOCK_IO2_PROTOCOL defines operations
that take an EFI_BLOCK_IO2_TOKEN:
typedef struct {
EFI_EVENT Event;
EFI_STATUS TransactionStatus;
} EFI_BLOCK_IO2_TOKEN;When an asynchronous read or write is issued:
- The consumer allocates the token, initializes
Token->Event(either via a registered callback or as an event that can be polled/waited upon), and setsToken->TransactionStatustoEFI_NOT_READY. - The consumer calls the protocol method (e.g.,
ReadBlocksEx), passing the token. - The underlying driver enqueues the request to the hardware queue
(e.g., NVMe submission queue or AHCI command list) and returns
EFI_SUCCESSimmediately, signaling that the operation has been queued rather than completed.
Hardware Monitoring and Completion Signaling
Because traditional hardware interrupts (IRQs) are often disabled or severely restricted in UEFI environments, drivers typically monitor asynchronous requests using periodic timer events or driver-level polling hooks.
Timer-Based Polling: When an asynchronous transaction is queued, the underlying driver maintains an internal list of pending requests. The driver registers a periodic timer event (
EVT_TIMER) that fires atTPL_NOTIFY.Checking Hardware Status: Inside the timer callback, the driver queries the hardware's completion ring or status register (such as an NVMe Completion Queue).
Updating Status and Signaling: When the hardware indicates the transfer has completed, the driver updates
Token->TransactionStatuswith the final status code (such asEFI_SUCCESSorEFI_DEVICE_ERROR) and calls:gBS->SignalEvent(Token->Event);
Callback Execution Lifecycle
Once gBS->SignalEvent is called:
- The UEFI core places the associated callback function on the event
queue at the priority level defined during
CreateEvent(e.g.,TPL_CALLBACK). - If the processor is currently running at
TPL_APPLICATION, the core immediately raises the TPL and dispatches the callback function. - If the processor is executing at an equal or higher TPL (such as
inside the driver's polling routine at
TPL_NOTIFY), the callback remains queued until the TPL drops back belowTPL_CALLBACK. - The callback executes with the provided
ContextData, allowing the consumer to process received data, handle errors, free resources, or trigger subsequent I/O operations.