Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku Device Driver Readiness: select(), poll(), and Waiter Lifetimes

Implement Haiku device readiness notifications with selectsync, B_SELECT events, race-safe registration, and cleanup across select() and poll().

A device read or write operation can block until hardware or buffered data changes. A caller that repeatedly retries the operation wastes CPU and still does not know when progress is possible. Haiku’s device interfaces expose readiness hooks so userland can wait for events with select() or poll() and attempt I/O when the driver reports a relevant transition.

This is a narrow part of the device contract, not a substitute for a driver lifecycle or an asynchronous transfer API. The driver must define what “ready” means for its device, register waiters without losing a transition, notify them when the condition changes, and remove registrations safely. A readiness notification is permission to try an operation; it is not a reservation of data or buffer space.

Two device interfaces expose readiness hooks

Haiku’s device-manager documentation describes select() and deselect() as operations in the user-facing device module interface. The legacy driver ABI also has select and deselect entries in device_hooks. In the current public Drivers.h, the legacy hook signatures include the open cookie, event, a ref value for selection, and an opaque selectsync*; deselection receives the cookie, event, and sync object. The current legacy adapter invokes the driver’s selection hook with ref set to zero, so a driver using that path should not rely on ref as a persistent waiter identifier.

The current header defines B_SELECT_READ, B_SELECT_WRITE, and B_SELECT_ERROR for basic readiness notifications. It also declares priority-read, priority-write, high-priority, and disconnected event values; the header specifically marks B_SELECT_PRI_READ as additional poll() support. Do not assume that every device or target release delivers each less-common event through both APIs. selectsync is opaque to the driver. notify_select_event(sync, event) is the public notification function; treat the pointer as an opaque sync token, not as a structure to inspect or retain beyond the registered lifetime.

There is an important documentation-history caveat. The legacy Be Book’s select_hook() page says the hooks were reserved for future use and should be null. Current Haiku headers, the device-manager documentation, and the kernel’s legacy-driver dispatch show a later supported path. Do not take old BeOS advice as a statement about every Haiku revision; check the public headers and implementation for the exact target release and ABI you are building against.

Define readiness as a device-specific predicate

For a stream-like device, read readiness often means that a nonblocking read can return bytes, EOF, or a terminal device condition without waiting indefinitely. Write readiness often means that at least some output can be accepted. Those are useful patterns, not universal promises for every hardware class. A block device, sensor, serial port, and event queue can have different buffering and terminal-state rules. Document the actual predicate beside the driver’s read and write contract.

Do not report a descriptor writable merely because the driver implements write(), or readable merely because the device is open. Conversely, a disconnected or failed device may need to wake a blocked waiter so that the next I/O attempt returns the appropriate error. Keep the readiness predicate and the subsequent I/O operation consistent: if readiness says “read will not block,” the read path must not then wait for new data behind the caller’s back.

select() and poll() do not move data. Their result can become stale as soon as another thread or process consumes a shared queue. Applications should open a device in nonblocking mode when appropriate, call the I/O operation after readiness, and handle a would-block result without treating it as corruption. Haiku’s device header includes blocking-mode controls; the exact policy still belongs to the device and its supported flags.

Register first, then close the readiness race

A classic lost-wakeup bug occurs when the driver checks “not ready,” the device becomes ready, and only afterward the driver registers the wait object. The notification has already happened, so the caller can sleep despite available data. The inverse race is also possible: registering first and then inspecting state without a synchronization rule can miss or duplicate a transition.

Use one lock or an equivalent atomic protocol to coordinate the readiness predicate and waiter list. Under that synchronization, install the waiter and inspect current state; if the event is already ready, arrange a notification before the selection operation completes. When hardware or a producer changes state later, update the state and notify the waiters registered for the affected event. Do not notify for an unrelated event simply because the same device changed internally.

The public interface does not expose the internals of selectsync, so do not copy another operating system’s structure layout or dereference the pointer. Keep the driver’s own registration records small: event, sync token, and only the state needed to remove that registration. If a device has a queue shared by several opens, distinguish per-open policy from device-wide readiness and make notification fan-out deliberate.

Make deselection and teardown idempotent

The selection and deselection hooks are a lifecycle pair. A timeout, a caller changing its interest set, descriptor closure, or device removal can cause a registration to stop mattering. Remove the matching waiter and ensure that a later producer does not notify freed per-open state. Do not assume that close() is the only cleanup boundary: the kernel’s device path has a distinct deselect operation, while open-cookie teardown has its own close/free rules.

Protect the waiter list and the device state with a clear lock order. Keep interrupt-context work bounded; if notification or list manipulation cannot safely happen in the hardware interrupt, record the event and defer processing to a context allowed by the driver. During removal, prevent new registrations, wake waiters with an appropriate disconnect/error event where the device and target API support it, drain callbacks, and free state only after neither a waiter nor a worker can reference it. Never hold a driver lock while blocking for the user’s next read or write.

The current devfs implementation also has a fallback when a character device has no selection hook: it immediately notifies non-output-only events and returns B_UNSUPPORTED. That is a kernel compatibility path, not a readiness policy for a real device. A driver that needs accurate write or read readiness should implement and test the hook instead of assuming the fallback means the device is genuinely ready.

Validate observable behavior, not just callback wiring

Test the driver and a userland client together on the target Haiku build. For a queue-based device, verify that an empty read wait times out, a producer then makes data available and wakes the waiter, and a second reader consuming the data does not make a stale notification look like a successful transfer. Exercise the already-ready-at-registration case to detect lost wakeups. Also cover multiple waiters, timeout and interest-set removal, closing one of several opens, write-buffer full and drain transitions, and device removal while a waiter is blocked.

Test the requested event classes rather than assuming every driver supports every poll() flag. If priority data, high-priority conditions, or disconnect events are not meaningful for the device, document that limitation. Use finite timeouts in diagnostic programs and record the exact event mask, I/O mode, status code, and target revision. A smoke test that only calls select() on an idle descriptor does not prove the notification path works.

Haiku’s readiness API is small, but its correctness depends on synchronization and lifetime discipline. Define readiness from the driver’s real state, close the register-versus-transition race, and treat each selectsync as a temporary opaque waiter. That lets applications wait efficiently without confusing notification with successful I/O or a guarantee that the device will remain ready.

Related:

Sources:

Comments