DEV Community

Yutaka Hirata
Yutaka Hirata

Posted on

[1/4] The Driver Had Stopped. Had Its Poll Function Finished?

Disabling NAPI does not necessarily mean its poll callback has returned. In r8169, I trace the difference between releasing NAPI ownership and finishing the callback, then follow the RCU region that covers the final interrupt enable.

Around 2001, Linux was entering business systems in Japan alongside commercial UNIX systems such as HP-UX. Getting packets through was only the beginning. Could the driver stop cleanly?

We would load and unload modules overnight with insmod and rmmod. I also read shutdown paths on paper. Twenty-five years have passed since I first began working with the Linux kernel. While preparing my Linux device driver book for publication on Kindle, I returned to the kernel source after a long time away. This time, I read my local Linux 7.1.3 and 7.3 trees, including r8169.

What does “complete” mean?

NAPI batches packet processing after an interrupt. In rtl8169_poll(), finishing the batch allows interrupts again:

if (work_done < budget && napi_complete_done(napi, work_done))
        rtl_irq_enable(tp);
Enter fullscreen mode Exit fullscreen mode

Shutdown in rtl8169_cleanup() contains:

napi_disable(&tp->napi);
synchronize_net();
rtl8169_irq_mask_and_ack(tp);
Enter fullscreen mode Exit fullscreen mode

The NAPI documentation says disabling NAPI waits for ownership release, not poll return. Could the final enable undo shutdown's interrupt mask?

I initially imagined this ordering: napi_disable() returns, shutdown masks interrupts, and the unfinished poll then enables them again.

My initial hypothesis, later rejected: synchronization was assumed not to wait for the poll tail

Figure 1 records my initial understanding, not an observed failure. I had assumed that synchronize_net() would not wait for the remaining poll code. Following its implementation changed that picture.

A wait hidden in plain sight

The comment above synchronize_net() mentions transmit work. Its effect is broader: it waits for an RCU grace period, covering earlier protected readers, including ordinary softirq NAPI execution.

These snippets do not establish the bug I suspected.

The corrected sequence puts the wait before shutdown masks interrupts. While shutdown waits, the existing poll enables interrupts and returns. Synchronization with its enclosing RCU region completes before the final mask.

The corrected ordering: RCU synchronization waits for the existing poll tail before shutdown masks interrupts

Figure 2 shows one representative ordering. Poll may finish earlier, and unrelated readers can extend the wait. The key correction is that the final interrupt mask follows the poll's enable, rather than preceding it.

The function's comment also says it does not block later packets from starting. That does not mean this NAPI starts polling again. Here, napi_disable() prevents new invocations; synchronization waits for the existing poll's tail.

Finding the wait was a start. Now I follow the caller of poll to see why RCU covers its final instructions.

The shutdown path contains a wait. Now I want to know why that wait covers a function it never receives as an argument. I return to the code that calls poll.

RCU is not looking for a function named poll

I still wanted to know why this wait covered the last interrupt enable. A function name was not enough evidence.

RCU, Read-Copy-Update, lets an updater defer reclaiming old data until earlier readers have finished. A typical reader uses rcu_read_lock() and rcu_read_unlock(). After removing old data from new readers' reach, an updater can call synchronize_rcu() to wait for a grace period.

But explicit RCU locks are not the only protected regions. The comment above synchronize_rcu() in Linux 7.3 also includes regions with interrupts, preemption, or softirqs disabled. Preemption is the mechanism that lets another task take over the CPU.

On an ordinary kernel, a CPU executing a softirq cannot simply report that all earlier work is gone while that handler is still running. RCU waits for suitable boundaries called quiescent states. They do not mean the CPU has stopped. They establish that earlier readers are no longer outstanding.

So this is an indirect wait: RCU waits for the region containing poll, rather than finding poll by name.

Follow the caller, too

In the ordinary receive-softirq path, the relevant calls are:

handle_softirqs()
  softirq_handle_begin()
    net_rx_action()
      napi_poll()
        __napi_poll()
          rtl8169_poll()
            napi_complete_done()
            rtl_irq_enable()
            return
  softirq_handle_end()
Enter fullscreen mode Exit fullscreen mode

Receive-softirq registration selects net_rx_action(). It takes a queued NAPI instance and eventually calls its registered driver callback through n->poll(). For r8169, that callback is rtl8169_poll().

On a non-RT kernel, the outer begin/end operations update the softirq execution count. Returning NAPI ownership does not end this outer region. The interrupt enable and poll return still occur inside it.

Quiescent-state reporting is not necessarily performed by the end operation alone. The ksoftirqd path also calls rcu_softirq_qs() after its group of softirq handlers returns. That location is after poll, too.

Shutdown can therefore wait while the existing poll finishes its tail, then mask interrupts. There is no second poll invocation in this explanation.

Releasing NAPI ownership and leaving the enclosing execution region are different events. I had started reading with a suspected bug and found a wait that prevented it. Being wrong felt rather good this time: I could see why the shutdown ordering held.

Then I remembered that NAPI can run in a dedicated thread. That route does not enter through the softirq handler whose protection I had just traced. What keeps the final interrupt enable covered there? That is the next caller I follow, including on a real-time kernel.

Top comments (0)