Landin library reference discuss source

Shared: available on every enabled target.

Signals as events: the concept a provider that takes turns supplies when it delivers the system's signals to a fibre that waits for them.

Call What it does
signal.watch(host, which) Take a signal from the system's default disposition and deliver it as an event from then on.
signal.next_signal(host) Wait, taking turns, for the next arrival of a watched signal: a signal.event.
signal.stop_watching(host) Give every watched signal back to its default disposition, and wake every waiting fibre, whose wait fails io_failed.

A signal is named portably by a signal.kind, and an arrival says which one came and the number the system gave it:

Kind Signal
signal.interrupt SIGINT, the terminal's Ctrl-C
signal.terminate SIGTERM, a request to end
signal.hangup SIGHUP, the terminal gone, or a daemon's cue to reload
signal.child SIGCHLD, a child ended, stopped or went on
signal.user1, signal.user2 SIGUSR1 and SIGUSR2
  • A signal never runs a handler of Landin code. The provider reads it as it reads any other event, through signalfd on Linux and kqueue's signal filter on Darwin and FreeBSD, and wakes the fibre that waits.
  • signals refines fibre.turns: only a provider whose fibres take turns supplies it, so a fibre waiting for a signal lets the others run. hosted/events supplies it; over hosted/blocking a program cannot ask for it, and a signal there keeps the system's default disposition: SIGTERM ends the process.
  • Each arrival is read once, by one waiter. Two arrivals of one signal before it is read may be one event, as the systems keep them.
  • A fibre waiting for a signal is stopped as any waiting fibre is, by failing its wait: stop_watching does that.

The fake

signals_over makes a fake.signals over the caller's signals_space and a fibre.turns it waits on, whose start, finished, wait and yield it passes on, so it is a provider of turns as the concept asks. A test raises a signal into it with fake.raise(host, which), and the fibre waiting in next_signal receives it after its next turn.

  • A raised signal nobody watches is counted in ignored and dropped, where a system would act on it.
  • A signal raised again while it waits to be read is one event.
  • The fake numbers each signal as Linux does on x86-64 and arm64.
  • raised, delivered and turns, the turns its waits took, count the rest.

The fake runs on Cortex-M0, over a round of turns.

Items

Concepts 1

Types 2

Also on this page

public signals: type = concept (provider: type) is fibre.turns

Deliver which as an event from now on.

    watch: (self: provider, which: kind) -> none ! io.io_failed

Wait, taking turns, for the next arrival of a watched signal.

    next_signal: (self: provider) -> (received: event) ! io.io_failed

Give every watched signal back to its default disposition, and wake every waiter, failing its call.

    stop_watching: (self: provider) -> none ! io.io_failed
end signals

A provider that delivers signals to a fibre as events (D449). Only a provider of fibre.turns supplies it (D477): a fibre waiting for a signal lets the provider's other fibres run, where over a provider that runs each task to its end it would hold the program before it served anything. A program that waits for a signal asks for signals, and over the blocking host it cannot be written, so a signal there keeps the system's default disposition: SIGTERM ends the process.

watch takes a signal from the system's default disposition and delivers it as an event from then on. next_signal waits as a receive does, taking turns, for the next arrival of a watched signal; each arrival is read once, by one waiter, and two arrivals of one signal before it is read may be one event, as the systems' own queues keep them. stop_watching gives every watched signal back to its default disposition and wakes every fibre waiting in next_signal, whose call fails io_failed, which is how a program stops the fibre that waits (D449). next_signal is the only entry that may switch.

public event: type = struct
    which: kind
    number: u32
end event

One arrival of a watched signal: its portable name and the number the system gave it, as a log or kill would show it.

public kind: type = interrupt | terminate | hangup | child | user1 | user2

The signals a provider of signals delivers, by portable names: each system numbers them its own way, and a program that names one here means the same signal on every system.

public child: atom

A child process ended, stopped or went on, SIGCHLD.

public hangup: atom

The controlling terminal has gone, SIGHUP; a daemon, which has none, commonly takes it to read its configuration again.

public interrupt: atom

The terminal's interrupt, SIGINT: someone pressed Ctrl-C.

public terminate: atom

A request to end, SIGTERM: what a service manager sends to stop a server, and what kill sends when it is not told otherwise.

public user1: atom

The first signal left to programs, SIGUSR1.

public user2: atom

The second signal left to programs, SIGUSR2.

public next_signal: (provider: type is signals, host: provider)
                    -> (received: event) ! io.io_failed

Wait, taking turns, for the next arrival of a watched signal.

stop_watching

function core/io/signal/signal.ldn:83 ↑ index
public stop_watching: (provider: type is signals, host: provider)
                      -> none ! io.io_failed

Stop delivering signals as events, and wake every fibre waiting for one, whose call fails io_failed.

public watch: (provider: type is signals, host: provider, which: kind)
              -> none ! io.io_failed

Deliver which as an event through signals evidence.

Example program

This complete program is one of the compiler's runtime tests. View source.

The program, 110 lines
compiler/tests/fixtures/runtime/signals-fake-firmware/main.ldn110 lines
--  D449, D477: core/io/fake's signals over a round of turns, within a
--  32 KiB firmware image.  A fibre written against `any signal.signals`
--  watches SIGUSR1 and SIGTERM and waits for each in turn; another fibre
--  takes a turn and then raises SIGUSR1, SIGHUP, which nobody watches and
--  is dropped, and SIGTERM twice, the second while the first waits to be
--  read and so one event with it.  The waiter hears SIGUSR1 and then
--  SIGTERM, each with the number the fake gives it.  Last, a fibre
--  waiting for a signal is woken by `stop_watching`, its wait failing
--  `io_failed`.
import core/failure
import core/fibre
import core/fibre/stacks
import core/io
import core/io/fake
import core/io/signal

--  What the waiter heard: each signal's number, in order, and `s` when a
--  wait failed because watching stopped.
mut heard: [8]u32 = zeroed
mut count: usize = 0

note: (number: u32) -> none =
    heard[count] = number
    inc count
end note

--  Every task's state, one type so the group's `admit` is one routine in
--  the image: the source as any provider of signals, and as the fake a
--  test raises signals into.
waiting: type = struct
    signals: any signal.signals
    source: fake.signals
end waiting

--  Hear signals until SIGTERM.  Nothing in it knows the source is a fake.
until_terminate: (inout state: waiting) -> none ! failure.reason =
    try state.signals.watch(signal.user1)
    try state.signals.watch(signal.terminate)
    loop do
        received: signal.event = try state.signals.next_signal()
        note(received.number)
        break when received.which == signal.terminate
    end loop
end until_terminate

--  Take a turn, so the waiter waits first, and raise four signals.
raise_four: (inout state: waiting) -> none ! failure.reason =
    fibre.yield(state.source)
    fake.raise(state.source, signal.user1)
    fake.raise(state.source, signal.hangup)
    fake.raise(state.source, signal.terminate)
    fake.raise(state.source, signal.terminate)
end raise_four

--  Wait for a signal that never comes.
wait_in_vain: (inout state: waiting) -> none ! failure.reason =
    try state.signals.watch(signal.hangup)
    received: signal.event = state.signals.next_signal() else (problem)
        if problem == io.io_failed then note(0) end if
        return
    end
    _ = received
end wait_in_vain

--  Module storage, so the stacks are no frame's: Cortex-M0's reset stack
--  is 4 KiB, and its 12 KiB of RAM below that hold two stacks of 4 KiB.
fixed if compiler.word_size == 32 then
    mut backing: [2 * 4096]u8 = zeroed
else
    mut backing: [2 * 8192]u8 = zeroed
end if

run: (signals: fake.signals, counts: ptr fake.signals_space)
     -> none ! failure.reason =
    runs: any fibre.turns = any(addr signals)
    mut tasks := try fibre.turns_over(runs, backing, 2)
    defer fibre.join(tasks) else none
    parts: waiting = (signals: any(addr signals), source: signals)
    try fibre.admit(tasks, until_terminate, parts)
    try fibre.admit(tasks, raise_four, parts)
    try fibre.join(tasks)
    fail io.io_failed when counts.val.raised <> 4
      or counts.val.ignored <> 1 or counts.val.delivered <> 2
      or counts.val.turns == 0
    try fibre.admit(tasks, wait_in_vain, parts)
    fibre.yield(signals)
    try signal.stop_watching(signals)
    try fibre.join(tasks)
end run

public main: () -> (code: i32) =
    code = 1
    mut state: stacks.round_space = zeroed
    mut entries: [3]usize = zeroed
    idler: stacks.spin = zeroed
    round := stacks.round_over(state, entries, any(addr idler)) else (problem)
        _ = problem
        return
    end
    mut space: fake.signals_space = zeroed
    signals := fake.signals_over(space, any(addr round))
    run(signals, addr space) else (problem)
        _ = problem
        return
    end
    code = 2
    return when count <> 3 or heard[0] <> 10 or heard[1] <> 15
      or heard[2] <> 0
    code = 42
end main