Linux

A 2.6 Driver and the Trail of Evidence

I ported a small driver experiment to a newer 2.6 kernel this week. The old code compiled after a few mechanical edits, so I congratulated myself much too early. Loading the module produced an oops during device removal, which is the kernel’s way of reviewing code without softening the language.

My first attempt was to stare at the final faulting line and add a null check. That stopped one crash but left the design wrong. The pointer was not unexpectedly null; it referred to an object whose lifetime had ended. A defensive condition would merely make the race less visible.

The 2.6 device model asks drivers to express relationships among buses, devices, and drivers. Registration is not just bookkeeping. It establishes ownership and determines which callbacks can run as devices appear and disappear. I had treated remove as the reverse spelling of probe, but not as the reverse sequence of acquired resources.

I wrote down each successful step in probe: enable the device, reserve its region, allocate private state, initialize synchronization, and expose the interface. Then I made every failure path unwind only the steps already completed. remove performs the same cleanup in reverse order after preventing new work from entering.

static int sample_probe(struct device *device)
{
        struct sample *sample;
        int error;

        sample = kmalloc(sizeof(*sample), GFP_KERNEL);
        if (!sample)
                return -ENOMEM;

        memset(sample, 0, sizeof(*sample));
        error = reserve_hardware(sample);
        if (error)
                goto free_sample;

        /* Published callbacks must be able to find fully initialized state. */
        dev_set_drvdata(device, sample);

        error = register_interface(sample);
        if (error)
                goto clear_drvdata;

        return 0;

clear_drvdata:
        dev_set_drvdata(device, NULL);
release_hardware:
        release_hardware(sample);
free_sample:
        kfree(sample);
        return error;
}

This is shortened code, but the order matters: private data is attached before register_interface() makes callbacks possible. If publication fails, the failure path clears that pointer before releasing and freeing the state. In C, a linear chain of labels is often clearer than nested cleanup conditionals.

Removal uses the other half of the same rule:

static int sample_remove(struct device *device)
{
        struct sample *sample = dev_get_drvdata(device);

        unregister_interface(sample);
        stop_hardware(sample);
        cancel_deferred_work(sample);
        dev_set_drvdata(device, NULL);
        release_hardware(sample);
        kfree(sample);
        return 0;
}

unregister_interface() first prevents new user callbacks. The hardware is then stopped and outstanding work is drained while drvdata still points to live memory. Only after no callback can use it do I detach and free it.

For evidence, I enabled the kernel debugging options relevant to memory and locking and built a kernel specifically for this work. They cost performance, but a development kernel is a measuring instrument, not a racing bicycle. I also kept the unstripped vmlinux and the exact .config beside the test build.

When the next oops appeared, the symbolic trace identified both the callback and its caller. printk() markers around acquisition and release confirmed that queued work could still run after teardown began. The repair was to stop new requests, cancel or drain deferred activity, and only then free the state it could reference.

The order now looks like this:

block new work
stop device activity
wait for deferred work
remove user-visible interface
release hardware resources
free private state

I tested failure paths too. Returning an artificial error after each acquisition showed whether cleanup restored the previous state. Module load and unload in a loop then exercised the unremarkable path repeatedly. Repetition is valuable because races dislike appointments.

I included reference counts in the review even though the first crash did not mention them. A reference is a claim that an object must remain alive, not merely an integer to increment near a pointer. Every path acquiring one needs a visible release, and teardown must prevent new claims before waiting for old ones. Counting without a shutdown rule only measures how confused the lifetime has become.

Lock choice followed the same reasoning. I did not replace every lock with a larger one to make the warning disappear. I listed which fields formed one invariant and which contexts accessed them. A spinlock suited the short state transition shared with interrupt context; work that could sleep stayed outside it. Narrow critical sections made both the rules and the traces easier to read.

One subtle lesson was to distrust logs as timing-neutral. A printk() can alter scheduling enough to hide a race. I used it to establish broad ordering, then relied on assertions, debug checks, and repeated tests rather than declaring victory when extra logging made the crash vanish.

An oops after testing still required a disciplined capture. I saved the complete report, not only the final address, and matched it to the exact kernel image. A decoded call chain is context: it shows how execution arrived, which often matters more than the instruction that finally touched invalid memory.

The kernel APIs will continue to evolve, and copying a driver from an older tree will continue to be tempting. The durable knowledge is not a particular field name. It is the discipline of checking the current in-tree callers, understanding callback context, defining object lifetime, and making cleanup the exact inverse of setup.

I still prefer user-space debugging, where a bad pointer usually ruins one process rather than the afternoon. The kernel does leave a trail if the build preserves symbols and the code preserves invariants. That’s better than adding null checks until the machine goes quiet.

Sleeping in the Kernel Without Regret

A driver experiment needed to do a little cleanup after an interrupt. I put the cleanup directly in the interrupt handler because it was only a little work. Then that little work called something that could sleep, and the kernel objected with considerably more confidence than I had shown.

The naive model says an interrupt handler is just a function the kernel calls quickly. The missing detail is context. Hard interrupt context cannot sleep: there is no process there to suspend in the normal sense, and holding up interrupt handling harms the rest of the machine.

The useful pattern is to split the operation. The handler acknowledges the device, records the minimal state under the right lock, and schedules deferred work. A workqueue function later runs in process context and may use operations that can sleep.

static void device_work(void *data)
{
        struct sample_device *dev = data;

        /* Slow cleanup is safe in process context. */
        finish_request(dev);
}

The exact initialization APIs deserve a check against the headers for the kernel being built; this area has changed before and surely will again. The invariant matters more than memorizing one macro: know which context executes the callback, what locks are held, and whether every function it calls may sleep.

Allocation flags make the distinction concrete. Code in a context that cannot sleep must not request an allocation mode that may block. Moving slow work to a workqueue lets that callback use normal process-context operations, but it does not make shared state safe automatically.

Teardown is the other half. Before freeing the device state, the driver must stop scheduling work and ensure already queued callbacks have finished. Otherwise the deferred function merely postpones the crash until after the object is gone.

I added an explicit comment at the scheduling point and reduced the handler to the smallest useful unit. Latency improved, but the larger gain was conceptual. Once context is part of the function’s contract, calls that looked harmless become obviously wrong.

Kernel C is not difficult because the syntax is exotic. It is difficult because an ordinary-looking call carries constraints about context, ownership, and concurrency. Ignoring those constraints produces bugs that wait patiently for the least convenient machine. Mine at least had the courtesy to complain immediately.

The Boring Desktop Test

I spent this week using KDE 3.3 on a machine that is deliberately uninteresting: modest graphics, an ordinary IDE disk, and no patience for experiments during working hours. The trigger was a recurring claim that a Linux desktop is only comfortable after endless adjustment. I wanted to count the adjustments.

My first approach spoiled the test. I copied my entire home directory from another machine, including years of stale application settings. Konqueror opened with an absurd toolbar, file associations contradicted one another, and the panel had ideas of its own. This proved only that old configuration can travel.

With a clean account, I changed the keyboard layout, terminal font, and focus behavior. That was nearly all. KDE remembered sessions, mounted removable media predictably, and kept common actions in familiar places. More importantly, applications failed in ordinary application-sized ways. A text editor problem did not rearrange the whole desktop.

For development, the useful measure is interruption. Can I keep a terminal, editor, API documentation, and a debugger arranged for several days? Do copied selections behave consistently? Does the session return after a kernel reboot? KDE 3.3 did these things without becoming the day’s project.

I added one less visible test: I watched memory use across repeated opening and closing of the applications I rely on. A desktop can feel quick for ten minutes while quietly accumulating baggage. Nothing in this informal check grew without bound, and leaving the session running overnight did not turn the next morning into an archaeological dig through swap.

The common Qt foundation deserves some credit. Shared dialogs, font handling, and event conventions reduce the small disagreements that make a collection of programs feel improvised. Integration is not only visual consistency; it is predictable behavior under the same input.

There are still annoyances. Some control modules expose every possible knob, and a few dialogs feel designed by committees that never met. Yet the foundation is settled enough that these are complaints about furniture, not the building.

My conclusion is unfashionably positive: boring is a technical feature. A mature desktop preserves context and lets me concentrate on C and C++. If I cannot remember what the desktop did all afternoon, it probably did it correctly.

X.Org Configuration With Less Guessing

I switched one machine to X.Org and immediately lost the mouse wheel. Naturally, I responded by changing three unrelated options in xorg.conf at once. The wheel remained decorative, and I no longer knew which edit had caused the pointer acceleration to feel strange.

Starting over one change at a time worked better. X.Org’s log records each configuration source, loaded module, detected device, warning, and error. The prefixes are particularly useful: (EE) deserves immediate attention, while (WW) is a clue rather than a conviction.

$ less /var/log/Xorg.0.log

The mouse was using the wrong protocol. Once the InputDevice section selected the correct protocol and enabled ZAxisMapping, wheel events arrived as expected. I confirmed that with xev, which is not beautiful but is wonderfully literal: move, click, or type, and it shows what the X server reports.

Display problems benefit from the same restraint. Modelines are timings, not decorative strings, and monitor ranges are safety limits rather than values to invent competitively. I let detection provide what it could, compared the resulting mode against the log, and added explicit values only when I had the monitor’s specifications in front of me.

This also clarified the layers for me. The kernel exposes the input device, the X server translates input and drives the display, and KDE consumes the resulting events. Poking a KDE setting cannot repair an X protocol mismatch. Likewise, changing an X option will not help if the kernel never reports the device.

I kept a minimal known-good configuration and added only the monitor ranges and device sections I actually needed. Generated configurations tend to contain enough commented examples to make every problem look plausible. More text is not more diagnosis.

Keeping the known-good file also made testing reversible without making every edit timid. I could change one section, restart the server, inspect the new log, and return to a working baseline if the result was worse. That is a much tighter loop than reconstructing yesterday’s guesses from memory.

The move to X.Org has been less dramatic than I expected, which is favorable. It feels familiar, the source is moving in an open direction, and the logging is adequate when I resist random editing. My new rule is log first, xev second, configuration change third. Rebooting repeatedly and hoping the mouse develops self-awareness is no longer on the list.

Following the Device Through sysfs

A USB card reader prompted today’s trip through /sys. I wanted one answer: which physical device sits behind the block-class entry /sys/block/sda?

The useful relationship is the device symlink. /sys/block/sda presents the disk as a member of the block class; its device link points into the physical device hierarchy. Those are two views of one kernel object, not two devices.

I started at the block entry and followed its device link rather than guessing from the node name:

$ readlink -f /sys/block/sda/device
/sys/devices/pci0000:00/0000:00:10.4/usb2/2-2/2-2:1.0/host1/1:0:0:0

The exact target is machine-specific. Here it says that the block disk came through a SCSI host attached to USB interface 2-2:1.0, itself below a PCI USB controller. On another machine the chain will differ, but the link still joins the class view to the device that implements it.

Walking upward from that target lets me inspect the parents that actually own bus attributes. The block entry doesn’t need to copy USB details into its own directory; the parent links already express the relationship.

I don’t parse the punctuation in the resolved path. Directory names and topology are useful while diagnosing a machine, but exported links and attributes are the interface. Hard-coding every component would just trade /dev/sda for a longer accidental name.

This also explains why /dev/sda isn’t a hardware biography. The node names a block interface; sysfs supplies the link back to the device model when I need to ask where that interface came from.

The service corridors are a little dim, but this one symlink answers the question without guesswork.