@mnl@hachyderm.io

Sketch of a shared DMA buffer async IO setup

This post is mostly written by GPT, which is a technique I'm going to try to use more often to get technical information out there.

This is a sketch of how to do async IO with an ethernet card DMA-ing received data into a buffer shared between userland and kernel.

Sync vs. Async IO: The Basics

Synchronous IO

In synchronous IO, when a userland process requests data from a device (like reading from a disk), the process is blocked until the operation is completed. This means that to further use the CPU, a separate thread of execution needs to be run. Standard functions like read() in many programming environments are examples of synchronous IO operations.

Asynchronous IO

Asynchronous IO, on the other hand, allows a process to request data and then continue with other tasks without waiting for the IO operation to complete. The process is notified asynchronously when the data is ready. This model can be highly efficient, particularly in IO-heavy applications, as it ensures better utilization of the CPU.

The iocomplete/asyncio_notify Setup

In our specific setup, we explore an Ethernet card performing DMA. Here, the data is transferred directly into a memory buffer shared between the kernel and userland, bypassing the CPU. The userland application then uses an async_io_notify system call to register a callback function, io_complete, which is triggered when the DMA transfer is complete.

Kernel Side Implementation

On the kernel side, async_io_notify involves several key steps:

  1. Validation and Device Retrieval: The kernel validates the provided file descriptor and retrieves the corresponding device.

  2. Tracking Async Operation: A structure is created to track this async operation, which includes storing the callback and user context.

  3. Configuring DMA: The Ethernet card is configured for DMA, including setting up a mechanism to notify the kernel upon completion.

  4. Handling Completion: When DMA completes, an internal handler is triggered, which then calls the io_complete function in userland.

Pseudocode for Kernel Implementation

function async_io_notify(fd, user_callback, user_context):
    device = validate_and_get_device(fd)
    async_op = setup_async_operation(fd, user_callback, user_context)
    configure_dma(device, async_op)
    return success

function dma_complete_handler(async_op_id):
    async_op = get_async_operation(async_op_id)
    unblock_userland_process(async_op)
    cleanup_async_op(async_op)

Userland Implementation

In the userland:

  1. Opening Device: The Ethernet device is opened and a file descriptor is obtained.

  2. Registering Callback: The async_io_notify call is used to register io_complete.

  3. Blocking: The process uses pause() or similar to block until the callback signals completion.

  4. Handling Completion: Upon notification, the io_complete function processes the completed IO operation.

Sample Userland Code

// Callback function
void io_complete(fd, context) {
    // Signal main process to continue
}

main() {
    fd = open_ethernet_device()
    async_io_notify(fd, io_complete, context)
    pause()  // Wait for completion
    close(fd)
}

Conclusion

By allowing DMA to directly transfer data into memory and leveraging async IO for notifications, we minimize CPU idle time and enhance overall system performance. This paradigm is particularly beneficial in environments where IO operations are frequent and substantial.