A network controller does not know which operating system is driving it. Its registers, its descriptor rings, its interrupt behaviour and its quirks are the same whether the code above them is a Linux net_device or a Windows NDIS miniport. That is the good news about porting a driver between the two, and it is why the first step of any port is to separate the code that knows about the hardware from the code that knows about the kernel.
The bad news is that everything else changes, and the changes are not cosmetic. The two kernels have different ideas about what a request is, when your code can sleep, how memory reaches a device, and how a driver gets from a build machine onto a user’s computer. Engineers who are expert on one side reliably fall into the same traps on the other. Here is the map we use, and the traps.
The side-by-side map
| Concept | Linux | Windows (WDM / KMDF) |
|---|---|---|
| Device discovery and binding | Bus match tables (PCI IDs, device-tree compatible strings, ACPI IDs), probe() / remove() | Plug and Play manager with hardware IDs in an INF file; AddDevice or KMDF EvtDriverDeviceAdd; PnP IRPs or KMDF PnP callbacks |
| Unit of work | Subsystem objects: sk_buff, bio/request, urb, i2c_msg | I/O Request Packets (IRPs), wrapped as WDFREQUEST in KMDF; subsystem-specific packets (NET_BUFFER_LIST, SCSI_REQUEST_BLOCK) |
| Interrupt handling | request_irq(); hard IRQ handler in atomic context; threaded IRQ or tasklet/workqueue for the rest | ISR runs at the device’s DIRQL with the interrupt spin lock held; deferred work in a DPC at DISPATCH_LEVEL; further deferral to a work item at PASSIVE_LEVEL |
| “Can I sleep here?” | Process context yes; atomic context (IRQ, softirq, under spinlock) no | Determined by IRQL: PASSIVE_LEVEL yes; APC_LEVEL limited; DISPATCH_LEVEL and above never, and no paged memory either |
| Locking | spinlock_t, mutex, RCU; lockdep validates ordering | KSPIN_LOCK (raises to DISPATCH_LEVEL), fast mutexes, executive resources, interrupt spin locks; Driver Verifier and Static Driver Verifier validate |
| Memory allocation | kmalloc with GFP flags; vmalloc; per-CPU | ExAllocatePool2 with pool type (non-paged vs paged) and tag; lookaside lists |
| DMA | DMA API: dma_map_single, dma_alloc_coherent, scatterlists; IOMMU handled by the API; explicit sync for non-coherent platforms | DMA adapter objects; KMDF WDFDMAENABLER and WDFDMATRANSACTION; MDLs describe buffers; scatter-gather lists; map registers handle bounce buffering |
| Power management | Runtime PM, system suspend/resume callbacks, device links | D-states driven by the power manager; KMDF EvtDeviceD0Entry / EvtDeviceD0Exit, idle and wake settings; power IRPs in WDM |
| User-space interface | Character devices, ioctls, sysfs, netlink, subsystem interfaces | Device interfaces (GUIDs), IOCTL codes via DeviceIoControl, WMI; subsystem interfaces |
| Subsystems | netdev, block/SCSI, DRM/KMS, ALSA, V4L2, input, USB core | NDIS, StorPort, WDDM, PortCls/AVStream, KS, HIDClass, USB stack (USBD) |
| Errors | Negative errno values | NTSTATUS codes; success is STATUS_SUCCESS, and several non-error statuses exist (STATUS_PENDING) |
| Asynchronous completion | Completion callbacks on the subsystem object, waitqueues, completions | Return STATUS_PENDING, complete the IRP later from a DPC; cancellation must be handled explicitly |
| Debugging | printk, dynamic debug, ftrace, kgdb, KASAN | DbgPrintEx, WPP tracing, WinDbg over network or serial, Driver Verifier |
| Distribution | Upstream in the kernel tree, or out-of-tree with DKMS; no signing required | Must be signed; attestation signing or WHQL through the Hardware Dev Center; distributed via Windows Update or an installer |
The traps, in the direction they usually catch people
Linux engineers arriving on Windows
- IRQL is not a suggestion. Touching paged memory or waiting on anything at DISPATCH_LEVEL does not fail gracefully; it produces a bug check, and only sometimes, depending on whether the page happened to be resident. Every function needs a known maximum IRQL, and the Driver Verifier option that forces paged memory out is mandatory during development.
- The ISR does almost nothing. Acknowledge the device, capture what must be captured, queue a DPC. Real work in the ISR blocks every interrupt at or below that DIRQL on that processor.
- Cancellation is your problem. A pending IRP can be cancelled at any time; the driver must either complete it promptly or register a cancel routine, and get the race between completion and cancellation right. KMDF removes most of this pain if you let it own the request queues.
- PnP and power states are a state machine you do not own. Surprise removal, stop-and-restart with new resources, and power transitions arrive as requests you must handle in order. Again, KMDF exists so that you implement callbacks instead of the state machine.
- There is no
dma_map_single. DMA goes through an adapter object and, on 64-bit systems with 32-bit-capable devices, through map registers that bounce for you. Skipping the DMA API and handing a physical address to the device works on your test machine and corrupts memory on the one with an IOMMU.
Windows engineers arriving on Linux
- Atomic context is implicit. There is no IRQL to read; whether you may sleep depends on how you were called.
might_sleep()and lockdep will tell you, but only at runtime and only on the path that was exercised. - The subsystem owns the request model. There is no generic IRP; a network driver receives
sk_buffs through the netdev API and a block driver receivesrequests from the block layer, with different rules for each. The subsystem’s expectations about locking and context are documented in its headers and in existing drivers, not in one place. - Cache coherency is platform-dependent. On x86 the DMA API is nearly a no-op; on many ARM platforms
dma_sync_*calls are what makes the data visible, and forgetting them produces bugs that never appear on the development PC. Windows’ DMA abstractions handle much of this under the covers; Linux expects the driver to call the API at the right moments. - Upstream has opinions. A driver destined for the mainline kernel will be reviewed for style, for use of existing helpers instead of reimplementations, and for device-tree binding correctness. Budget for that review; it is where most of the schedule goes on a first upstream submission.
- There is no INF. Device binding is by ID tables in code or by device tree; user-visible configuration is sysfs or module parameters. Installers are a distribution’s business, not the driver’s.
How we structure a port
- Extract the hardware layer first. Register definitions, ring management, command encoding, firmware loading and the device’s state machine become OS-independent C with a small interface: read and write registers, allocate DMA memory, log, delay. On the existing driver this is a refactor with the existing tests as a safety net.
- Port the tests before the driver. Whatever exercised the hardware layer on the source OS (loopback tests, register self-tests, firmware load checks) is ported to run against the new OS’s glue first. It tells you that the DMA and interrupt plumbing works before the driver logic obscures the result.
- Write the OS glue thin, and in the OS’s idiom. A KMDF driver that looks like a Linux driver wearing a costume will fight the framework at every power transition. Use the request queues, the DMA enabler and the power callbacks the way the samples do. The same holds in reverse: a Linux driver that reimplements a PnP state machine will be rejected upstream.
- Verify with the OS’s verifier. Driver Verifier with the strict options on Windows; lockdep, KASAN and sparse on Linux. Both find the context and locking mistakes that the other OS would have let you make.
- Document the mapping. A short table, like the one above but specific to this driver, of which source-side concept became which target-side construct. The next engineer will otherwise re-derive it.
Takeaways
- The hardware layer ports; the OS layer is rewritten. Separate them before you start.
- Windows: IRQL, DPCs, cancellation and PnP/power state machines are the traps; let KMDF own the queues and states.
- Linux: implicit atomic context, subsystem-specific request models, platform-dependent DMA coherency and upstream review are the traps.
- Port the tests first, write the glue in the target’s idiom, run the target’s verifier, and leave a mapping table behind.