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

Haiku POSIX Signals: Async-Safe Handlers and Deliberate Shutdown

Block shutdown signals before worker threads, use sigwait for orderly coordination, and separate Haiku's thread-directed APIs from POSIX handlers.

A signal handler is not a general-purpose callback. It can interrupt code while that code holds a lock, updates an allocator, or is halfway through changing an application invariant. Treating SIGINT or SIGTERM as permission to run ordinary cleanup from the handler can turn a routine shutdown request into deadlock or memory corruption.

For a multithreaded Haiku application, a dedicated signal-waiting thread is usually the clearer design: block the selected signals before creating workers, let the workers inherit that mask, and have a coordinator consume the requests with sigwait(). The coordinator is an ordinary thread, so it can hand shutdown into the application’s normal synchronization and cleanup path. This is different from installing an asynchronous sigaction() handler, and different again from Haiku’s native thread-directed signal API.

Keep disposition, mask, and destination separate

Signal handling involves three decisions that are easy to collapse into one. A signal disposition says what the process does when a signal is delivered: use the default action, ignore it, or invoke a handler. A signal mask says which signals a particular thread currently blocks. The sending API determines the intended recipient or delivery route.

POSIX signal dispositions are process-wide, while a thread’s mask is per-thread. A mask is not a second, thread-local handler table. When a newly created POSIX thread inherits a blocked mask, that can be used to ensure that a process-directed shutdown request is not accidentally delivered to an arbitrary worker. Keep the disposition question separate: if the design uses sigwait(), the waiting thread consumes a blocked signal synchronously; it does not need an asynchronous handler to do the same job.

Haiku exposes the familiar POSIX functions, including sigaction(), pthread_sigmask(), sigwait(), kill(), and pthread_kill(). Its public kernel OS.h also declares send_signal(thread_id, signal), which takes a Haiku thread ID. Do not treat that native API as a synonym for process-directed POSIX kill(pid, sig): they express different targeting contracts. Code using native thread IDs should follow the native API’s documented lifetime and error semantics rather than assuming POSIX process delivery rules.

Prefer a blocked-signal coordinator for orderly shutdown

Choose the shutdown signals your program owns (often SIGINT and SIGTERM) and block them in the initial thread before starting workers. POSIX threads created afterward inherit the creator’s signal mask. Start a coordinator that waits for those signals using sigwait(). When one arrives, the coordinator can set application state under a mutex, post to a condition variable, enqueue an event, or ask the main event loop to begin its normal shutdown sequence.

The following is a design sketch. request_orderly_stop() is application code that uses the program’s ordinary thread-safe notification mechanism; it must not be an asynchronous signal handler. Check every pthread return value directly because pthread APIs report an error number as their return value.

static sigset_t shutdownSignals;

static void *
waitForShutdown(void *unused)
{
	int received = 0;
	int error = sigwait(&shutdownSignals, &received);
	if (error != 0) {
		request_orderly_stop_with_error(error);
		return NULL;
	}

	request_orderly_stop(received);
	return NULL;
}

int
start_workers_and_shutdown_coordinator(void)
{
	sigemptyset(&shutdownSignals);
	sigaddset(&shutdownSignals, SIGINT);
	sigaddset(&shutdownSignals, SIGTERM);

	int error = pthread_sigmask(SIG_BLOCK, &shutdownSignals, NULL);
	if (error != 0)
		return error;

	pthread_t coordinator;
	error = pthread_create(&coordinator, NULL, waitForShutdown, NULL);
	if (error != 0)
		return error;

	/* Create workers only after the mask has been installed. */
	return start_application_workers();
}

The sample omits the application’s worker joins and coordinator lifetime policy. A production version should define what happens if thread creation fails, if sigwait() returns an error, or if a second request arrives during teardown. It should also decide whether the coordinator exits after the first request or continues draining an explicitly defined set. Do not make a worker unmask these signals casually: one unblocked thread can become an eligible recipient for a process-directed request, defeating the coordinator’s intended ownership. Also choose one consumption model per signal: POSIX specifies the result as unspecified if sigaction() and a sigwait() function are used concurrently in one process for the same signal.

This approach also avoids one common cross-thread mistake. A volatile sig_atomic_t flag can be useful for communicating a minimal fact from an asynchronous handler to code that resumes in the interrupted execution context. It is not a general inter-thread synchronization primitive. If one thread writes it and other workers read it concurrently, use a suitable atomic or lock-based protocol instead. A sigwait() coordinator runs in normal thread context and can use the application’s normal synchronization objects.

If a handler is unavoidable, make it tiny

An event-loop application may already have a deliberate handler-based design. In that case use sigaction() rather than relying on historical signal() variation, zero-initialize the target action structure, set the handler, mask, flags, and any documented target-specific fields, and keep the handler within the async-signal-safe boundary. POSIX requires handler code to avoid functions that are not listed as async-signal-safe. In particular, do not call printf, allocate memory, acquire an application mutex, perform complex logging, or run C++ object destruction from the handler. A function being safe in an ordinary thread does not make it safe when an asynchronous signal interrupts arbitrary code.

For a narrowly scoped flag, the handler should record the request and return; the event loop notices the flag and performs cleanup later. Preserve and restore errno if the handler calls any permitted function that can change it. Keep the set of signals blocked during handler execution and the sa_flags choice intentional: sa_mask and flags affect when nested or interrupted work can occur. Avoid attempting to make a handler a second event loop.

Do not assume Haiku’s sigaction structure is byte-for-byte the same as another operating system’s. Haiku’s public header includes a sa_userdata field and describes additional handler arguments as BeOS-originated extensions to POSIX. Prefer the portable handler signature and fields in portable code. If an application uses Haiku-specific extensions, isolate that code and compile it against the target Haiku headers rather than copying a foreign platform’s structure layout or examples.

Haiku-specific signal numbers are not portable shutdown shortcuts

Haiku’s public signal.h comments identify SIGKILL as killing a team and mark it uncatchable. It describes SIGSTOP as suspending a thread and also marks it uncatchable; that comment should not be generalized into a claim about identical job-control behavior on every POSIX system. The header separately defines SIGKILLTHR with the explicit comment that it targets just the thread rather than the team. Those are not cooperative shutdown requests. They do not let application code flush state, release owned resources in order, or run its normal cleanup path.

Use portable SIGINT/SIGTERM handling for cooperative requests when those semantics fit, and use Haiku-native thread controls only when the application specifically needs a Haiku thread-level operation. A forced thread or team termination can interrupt code without completing its invariants. It is not a substitute for a signal coordinator, a cancellation protocol, or a worker join.

Test the boundaries on the target system

Exercise the actual binary on the Haiku release you ship. Verify that the mask is installed before thread creation; send SIGTERM while workers hold ordinary application locks; confirm that the coordinator receives the request and that workers leave through their normal stop path; and test the documented second-signal policy. Check that a worker cannot accidentally unblock the managed signals. Also test the handler variant separately if you support it, with diagnostics confirming that handler code does not log, allocate, lock, or invoke cleanup.

For API-specific cases, test invalid signal sets and errors from pthread_sigmask(), pthread_create(), and sigwait() rather than treating zero/nonzero as interchangeable across APIs. Inspect the exact Haiku headers and libc implementation corresponding to the supported release: current source is evidence of that implementation, not a promise that every historical Haiku or BeOS version has identical behavior. Do not test forced termination by assuming cleanup should run; the point of those APIs is that they do not provide a cooperative teardown contract.

The practical division is straightforward: install portable process dispositions only when a handler is really required; use per-thread masks to route the signals your design owns; prefer a blocked-signal waiter when orderly multithreaded shutdown matters; and keep Haiku’s native thread-directed controls distinct from the POSIX process model. That keeps cleanup in ordinary control flow, where locks, logs, and resource ownership can be handled deliberately.

Related:

Sources:

Comments