\( \newcommand{\ess}{\epsilon} \newcommand{\RX}{\mathit{RX}} \newcommand{\TX}{\mathit{TX}} \newcommand{\rx}{\mathit{recv}} \newcommand{\tick}{\mathit{tick}}\)
Operating systems and distributed consensus algorithms are a good example of systems that are difficult to describe clearly except in a lot of code. We use pseudo-code, block diagrams, and explanations in natural languages but there is a big gap between “too detailed” and “too vague”. There has been no good method for doing back of the envelope calculations or sketching out a design in a way that would turn up problems before a lot of code gets written. Experience teaches that micro-kernels bog down in data movement, but clarifying the source of the problem would require us to be able to specify behavior and architecture on a high level without losing all detail. Or consider how hard it is to understand distributed consensus algorithms like Paxos which apparently need to be explained (and “verified” ) over and over.
The “temporal logics” seemed, at one time, to offer a solution, allowing specifications like “every process eventually runs” or “a process requesting a lock waits until the lock is released”. Even informally, these and the other temporal qualifiers provide a concise way of capturing some key systems properties. The methods I describe here are intended to get some of the intuitive leverage of the temporal logic within ordinary working mathematics and state machines and to add a strong approach to composition.
The mathematical basis is outlined in another note but the basic idea is simply to represent discrete state variables as depending on the sequence of events that have driven a system from its initial state. So \( status(s,p)\) might be the status of property \(p\) in the state reached by following the sequence of events \(s\) from the initial state. For now, suppose \(status(s,p)\in\{“ready”,”waiting”, “running”, “inactive”\}\). Let \(s.e\) be the sequence obtained by appending event \(e\) to \(s\) on the right, \(status(s.e,p)\) is the status of \(p\) in the state reached from the \(s\) state if an \(e\) event happens. Then
\[status(s,p) = “waiting” \mbox{ and }status(s.e,p)=”ready” \rightarrow WakeupPending(s,p)\]
says that if \(p\) is blocked in the current state and ready in the state after \(e\) happens, then there must be a wakeup pending for \(p\) in the current state.
\[blocked(s,p,l)\rightarrow (blocked(s.e,p,l)\mbox{ or }unlocked(s,l))\]
says if process \(p\) is blocked on lock \(l\) in the state determined by \(s\), it will stay blocked until the lock is unlocked. For composite systems, as we’ll see below, there is an analog of the chain rule from elementary calculus, but first let’s look at time and eventuality.
Suppose the event alphabet includes \(\tick\) which correspond to the passage of one unit of time (or maybe to the incrementing of a processor or virtual clock). Define \(waiting(s,p)\) as follows. In the initial state its value is zero: \(waiting(\ess,p)=0\). After an event \(s\) the count increments if and only if \(e=\tick\) and \(status(s,p)= “ready”\).
\[waiting(\ess,p)=0;\quad waiting(s.e,p) = \begin{cases} waiting(s,p)+1&\mbox{if }e=\tick \mbox{ and }status(s,p)=”ready”;\\waiting(s,p)&\mbox{otherwise}\end{cases}\]
Then \(waiting(s,p) < DELAY \) would be true for all \(s\) if and only if a ready process never takes more than DELAY time units to start running (or otherwise stop being ready to run). This is a good liveness condition.
To keep things simple, lets assume that there is at most one running process at any time:
\[\mbox{There is at most one }p\mbox{ s.t. } status(s,p)=”running”\]
Let \(running(s)\) equal \(p\) if \(status(s,p)=”running” \) and nullp if no process is running where \(nullp\) is not the id of any process.
At most one process becomes ready to run in any state
\[\mbox{There is at most one }p\mbox{ s.t. } status(s,p)\neq “ready” \mbox{ and }status(s.e,p)=”ready”\]
Let \(newready(s.e)=p\) if \(status(s,p)\neq “ready” \mbox{ and }status(s.e,p)=”ready”\) and let \(newready(s.e)=nullp\) if there is no process that becomes ready thanks to event \(e\).
Switch gears to an abstract representation of a fifo queue of max length \(k\). Let’s suppose \(Q(s)(i)\) is the contents of the \(i^{th}\) element of the queue where \(Q(s)(1)\) is the first element. The set of possible values is \(V\) plus a special null value \(nullv\notin V\) which indicates there is nothing held in that element of the queue. The events that can change state are “deq” and \(v\in V\) which enqueues value \(v\) at the end of the queue.
\[Q\mbox{ is a fifo queue of max length }k\mbox{ over }V\mbox{and }nullv\mbox{ if and only if } Q(\ess)(i) = nullv\]
\[ \mbox{and }Q(s.e)(i) = \begin{cases} v&\mbox{if }e=v\in V\mbox{ and }i>1\mbox{ and }Q(s)(i-1)\neq nullv\mbox{ and }Q(s)(i)=nullv\\ Q(i+1)(s)&\mbox{if }e=deq \mbox{ and } i < k\\nullv&\mbox{if }e=deq\mbox{ and }i=k\\ Q(i)(s)&\mbox{otherwise}\end{cases}\]
Now consider how we can embed a queue as the ready queue in our OS. Fifo queues are not real-time – time advances do not change state. In fact, the alphabet of events for the OS and the queue will be different so we need a map from OS event sequences \(s\) to queue event sequences \(u(s)\). To see the process it may help to look at a diagram. It turns out that the state variables (state map) used here are equivalent to state machines with output – also known as Moore machines. These can be defined separately and then interconnected
The variables defined above “status” and “waiting” depend on events from the OS event alphabet \(E_{os}\) while the queue has its own alphabet \(E_{Q}\). The queue can be defined on its own and then hooked into the OS so a sequence \(s\) over the event alphabet \(E_{OS}\) is mapped to a sequence \(u\) over \(E_{Q}\). Define \(u(\ess) = \ess\) so the queue is in the initial state when the OS is in the initial state. Here we want a fifo queue of length \(k\) over a set of process identifiers \(P\) and null element nullp. Then let’s require that

If the running process changes and a process \(p\) becomes ready, the queue advances by \(deq\) and then \(p\) to remove the new running process and queue up the new ready process.
If the running process changes and no new process becomes ready, the queue just advances by \(deq\).
If the running process does not change and a new process \(p\) becomes ready, the queue advances by \(p\).
otherwise the queue remains the same.
The \(\phi\) in the diagram to the left represents the “feedback” – the outputs of “status” and “Waiting” that can help determine the events applied to the embedded queue. To see how this all works out in terms of automata products, look at this note.
The method of connecting state variables by making sequence of events seen by components be a state variable depending on the input sequence seen by the higher level system and the values of some number of other state variables can be extended to any number of components and to multiple layers of components.
\[ u(s.e) = \begin{cases} u(s).p &\mbox{if }newready(s.e)=p\mbox{ and }Running(s)=Running(s.e)\\ u(s).deq&\mbox{if }running(s.e) \neq running(s)\mbox{ and }newready(s.e)=nullp\\ u(s).deq.p&\mbox{if }running(s)\neq running(s.e)\mbox{ and }p=newready(s.e)\\u(s).p&\mbox{if }p=newready(s.e)\neq nullp\mbox{ and }running(s.e)=running(s)\\u(s)&\mbox{otherwise}\end{cases}\]
Then we can require that:
\[ status(s.e,p)=”running”\rightarrow (status(s,p)=”running”\mbox{ or }p = Q(u)(1))\]