I put packet bookkeeping into a network device’s interrupt handler because the information was conveniently available there. Under load, the machine reminded me that convenience inside interrupt context is borrowed at unreasonable interest.

The interrupt handler should acknowledge the device, collect enough status to know what happened, move completed descriptors along, and arrange deferred processing where appropriate. It cannot sleep, and time spent there delays other interrupt work. Long parsing, allocation loops, or diagnostic printing are especially poor guests.

Linux 2.4 offers deferred mechanisms including tasklets and the network receive path’s softirq processing. A driver can prepare received skbs and pass them to netif_rx(), allowing the protocol stack to process packets outside the hard interrupt handler. For driver-specific deferred work, I put the tasklet in the device’s private state:

struct sample_dev {
        struct tasklet_struct rx_tasklet;
        /* rings, locks, and device state */
};

static void sample_tasklet(unsigned long data)
{
        struct sample_dev *dev = (struct sample_dev *)data;

        service_completed_packets(dev);
}

static void sample_init_tasklet(struct sample_dev *dev)
{
        tasklet_init(&dev->rx_tasklet, sample_tasklet,
                     (unsigned long)dev);
}

sample_init_tasklet() runs after dev is allocated and before the interrupt can schedule dev->rx_tasklet. Teardown disables that interrupt source, calls tasklet_kill(&dev->rx_tasklet), and only then frees dev. The callback therefore never receives the null value that a static DECLARE_TASKLET(..., 0) would have supplied.

Shared rings and status fields need protection across the interrupt, deferred function, transmit entry point, and process-context control operations. I use spinlocks where the data can be touched in interrupt context and choose the interrupt-disabling variant when the same processor could otherwise interrupt a lock holder. I keep the protected section short and never call a function that can sleep while holding it.

There is a balance. Deferring every tiny operation adds overhead and can complicate ordering, while doing everything in the handler harms latency. I measure under sustained receive and transmit load, watch dropped packets, and check that another device’s interrupts are not starved.

Interrupt acknowledgement order is device-specific and worth documenting beside the handler. A level-triggered interrupt that remains asserted can immediately enter again; acknowledging too early on other hardware may lose status that has not been copied. I read the device manual rather than deriving this order from whichever experiment happened not to hang.

Shutdown receives the same scrutiny. The driver disables the device’s interrupt source, prevents new deferred work, frees the IRQ with the required synchronization, and only then releases rings and private data. Otherwise a late tasklet can faithfully process memory that has already acquired a new purpose.

I want the hard interrupt path to be visibly bounded. One caveat: printk can completely distort timing here. A hundred helpful messages per second soon become the performance problem, a promotion few debug statements deserve.