Skip to content

Strategies for writing top/bottom half NIC interrupt handlers #212

@ghaerr

Description

@ghaerr

Pursuant to the discussion in ELKS ghaerr/elks#2506 (comment), I thought to discuss some ideas I've had about rewriting the NIC drivers for maximum performance, while keeping interruptions to other device drivers at a minimum.

The first concept I'm thinking would be to use two (new) bottom half handlers - NETRX_BH and NETTX_BH. when marked with mark_bh, these would run the bottom half receive or transmit handler registered by the individual driver from the init_bh call at init time. In this way, separate paths would be run for RX vs TX handling. (And some NICs don't use TX interrupts, right?)

As an example of how this might work, consider the current WD interrupt handler:

static void wd_int(int irq, struct pt_regs * regs)
{
    word_t stat;

    outb(0, WD_8390_PORT + EN0_IMR);/* Block interrupts,
                     * should not be required since the IRQ line
                     * is held high until all unmasked bits have
                     * been cleared. This is experimental.
                     */
    while (1) {
        stat = inb(WD_8390_PORT + EN0_ISR);
        //printk("/%02x;", stat&0xff);
        if (!(stat & ENISR_ALL))
            break;
        if (stat & ENISR_OFLOW) {
            printk(EMSG_OFLOW, dev_name, stat, netif_stat.oflow_keep);
            wd_clr_oflow(netif_stat.oflow_keep);
            netif_stat.oflow_errors++;
            continue; /* Everything has been reset, skip rest of the loop */
        }
        if (stat & ENISR_RX) {
            wake_up(&rxwait);
            outb(ENISR_RX, WD_8390_PORT + EN0_ISR);
        }
        if (stat & ENISR_TX) {
            wake_up(&txwait);
            //inb(WD_8390_PORT + EN0_TSR);  /* should be read every time */
            outb(ENISR_TX, WD_8390_PORT + EN0_ISR);
        }
        if (stat & ENISR_RX_ERR) {
            printk(EMSG_RXERR, dev_name, inb(WD_8390_PORT + EN0_RSR));
            netif_stat.rx_errors++;
            outb(ENISR_RX_ERR, WD_8390_PORT + EN0_ISR);
        }
        if (stat & ENISR_TX_ERR) {
            netif_stat.tx_errors++;
            printk(EMSG_TXERR, dev_name, inb(WD_8390_PORT + EN0_TSR));
            outb(ENISR_TX_ERR, WD_8390_PORT + EN0_ISR);
        }
        if (stat & (ENISR_RDC|ENISR_COUNTERS)) {  /* Remaining bits - should not happen */
            // FIXME: Need to add handling of the statistics registers
            // On 8216 cards, the RDC bit is set with every RX interrupt
            // even when the RDC interrupt has been masked.
            //printk("eth: RDC/Stat error, status 0x%x\n", stat);
            outb(ENISR_RDC|ENISR_COUNTERS, WD_8390_PORT + EN0_ISR);
        }
    }
    outb(ENISR_ALL, WD_8390_PORT + EN0_IMR);

}

This could be rewritten as:

static word_t stat; // save across top/bottom half execution

static void wd_int(int irq, struct pt_regs * regs)
{
    //outb(0, WD_8390_PORT + EN0_IMR); // Don't block interrupts, should not be needed

        stat = inb(WD_8390_PORT + EN0_ISR);
        if (stat & (ENISR_RX|ENISR_RX_ERR|ENISR_OFLOW) {
            mark_bh(NETRX_BH);
            //does this re-enable RX interrupts? If so, move it to bottom half
            //outb(ENISR_RX, WD_8390_PORT + EN0_ISR);
        }
        if (stat & ENISR_TX) {
            mark_bh(NETTX_BH);
            //does this re-enable TX interrupts? If so, move it to bottom half
            //outb(ENISR_TX, WD_8390_PORT + EN0_ISR);
        }
    //outb(ENISR_ALL, WD_8390_PORT + EN0_IMR); // should not be needed
}

The PIC prevents another NIC interrupt while the top half is running. If the NIC itself might interrupt again outside the top-half, before the bottom half executes, then separate rxstat and txstat variables are probably required. In general, anything that might change in the NIC controller register between the top and bottom half handlers will have to be saved by the top half. Ideally a structure is used so that the driver doesn't have tons of global variables.

If both an RX and TX PIC interrupt occur during kernel code execution, then both would run before either 1) returning to user mode from any interrupt, or 2) in the next call to schedule(), which occurs continuously if the idle task is running. If the interrupt occurred during kernel code execution, including during a bottom half execution, execution will be delayed until the kernel finally finishes up and returns to user mode via 1) above.

It doesn't appear that the WD driver disables interrupts while reading NIC data, unlike the NE2K and EL3 drivers. Is that because the WD has special hardware allowing for such operation, do the NE2K/EL3 drivers actually need interrupts disabled?

Metadata

Metadata

Assignees

No one assigned

    Labels

    DiscussionTechnical discussion related to other issues

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions