------------------------------ MODULE sched ---------------------------------
EXTENDS Naturals, Sequences, FiniteSets, TLC
CONSTANTS CPU,
THREADS
(* --algorithm sched {
variables
running = THREADS;
current = "idle";
worked = [t \in THREADS |-> FALSE];
define {
SchedInv == /\ \A t \in running : current \in {t, "idle"}
/\ current # "idle" => Cardinality(running) <= 1
AllWorked == <>(\A t \in THREADS : worked[t])
}
procedure do_work(thread)
{
do_work:
worked[thread] := TRUE;
return;
}
procedure do_handler()
{
do_handler:
skip; \* some work
return;
}
procedure schedule()
{
schedule:
\* pick any thread, even the current one
with (t \in THREADS) {
running := {t};
current := t;
};
return;
}
\* Threads
process (Thread \in running)
{
init_thread:
\* only a single thread allowed at the same time
running := {self};
current := self;
start_thread:
assert self \in running;
main:
while (TRUE)
call do_work(self);
}
\* Interrupts
process (Interrupt = CPU)
{
interrupt:
while (TRUE) {
\* stop current thread
running := {};
handler:
call do_handler();
resched:
call schedule();
}
}
} *)
==============================================================================
SPECIFICATION Spec
CONSTANT defaultInitValue = defaultInitValue
\* Add statements after this line.
CONSTANTS CPU = p1
THREADS = {t1, t2}
INVARIANTS SchedInv
\*PROPERTY AllWorked
On 12 Jan 2018, at 15:09, Catalin Marinas <catalin...@gmail.com> wrote:
Hi,
The usual disclaimer for a first post to this group, I’m new to TLA+ (and I already found it useful discovering a couple of software bugs [1]).
What I’m trying to model now is OS thread preemption on a single CPU [2] (uniprocessor; my final model has multiple CPUs). Basically only one thread can run at a time (and modify the CPU state) while it can be interrupted and scheduled out at any point (well, based on the labels granularity). Modelling each thread as a TLA+ process matches closely the way Linux kernel programmers think about concurrency and preemption. The interrupt handler is modelled as yet another TLA+ process. All these processes (threads and interrupt handler) race to modify some CPU state (not defined here).
--
You received this message because you are subscribed to the Google Groups "tlaplus" group.
To unsubscribe from this group and stop receiving emails from it, send an email to tlaplus+u...@googlegroups.com.
To post to this group, send email to tla...@googlegroups.com.
Visit this group at https://groups.google.com/group/tlaplus.
For more options, visit https://groups.google.com/d/optout.
- I turned your procedures into macros: that's not essential, but since there is no recursion, the procedure bodies contain simple straight-line code, and you have a label in front of every call anyway, it looks more natural to me, the translation to TLA+ gets simpler, and the number of states is reduced a lot due to the rather useless intermediate states.
- I removed the startup code from the threads: I didn't understand why the initial processor assignment would be based on a race rather than on a decision by the scheduler (through the "schedule" operation). This allowed me to get rid of your hack with making ProcSet dynamic: a thread can execute only if it is the chosen one, although the set of potentially executable threads is static.
- Consequently, I initialized "running" to the empty set. I also rewrote the invariant to a formulation that looks easier to me.
For your module, verification of liveness fails because the generated fairness condition is/\ \A self \in running : WF_vars(Thread(self)) /\ WF_vars(do_work(self))
and TLC can only handle quantifiers over constant sets in liveness checking. With the above modifications, the bound of the quantifier is THREADS and liveness checking becomes possible.
However, TLC produces a counter-example in which a terminated thread gets scheduled repeatedly although there is some non-terminated thread.I therefore rewrote the schedule operation so that it will only schedule threads that have not finished (and "idle" if all threads have finished). Now, assuming weak fairness we still get a counter-example in which an executable thread gets scheduled but pre-empted before it does any work. This happens because the thread is not persistently executable (it can't execute while the interrupt handler is active). Using strong fairness for the threads the liveness condition is checked.
------------------------------ MODULE sched2 --------------------------------
EXTENDS Naturals, Sequences, FiniteSets, TLC
CONSTANTS CPU, THREADS
(* --algorithm sched {variables
current = "idle"; in_interrupt = FALSE; interrupts_on = FALSE;
worked = [t \in THREADS |-> FALSE];
thread_pc = [t \in THREADS |-> "start_thread"]; thread_stack = [t \in THREADS |-> << >>];
\* concurrency test count = 0;
define { \* Handle interrupt. Save current thread context and invoke handler HandleInt(self) == /\ interrupts_on \* only when interrupts are enabled /\ interrupts_on' = FALSE \* turn interrupts off /\ in_interrupt' = TRUE /\ thread_pc' = [thread_pc EXCEPT ![current] = pc[self]] /\ thread_stack' = [thread_stack EXCEPT ![current] = stack[self]] /\ pc' = [pc EXCEPT ![self] = "interrupt_handler"] /\ UNCHANGED <<current, worked, stack, count>>
\* Return from interrupt. Restore current thread context ReturnInt(self) == /\ pc[self] = "ret_from_interrupt" /\ pc' = [pc EXCEPT ![self] = thread_pc[current]] /\ stack' = [stack EXCEPT ![self] = thread_stack[current]] /\ in_interrupt' = FALSE /\ interrupts_on' = TRUE \* re-enable interrupts /\ UNCHANGED <<current, worked, thread_pc, thread_stack, count>>
Interrupt(self) == HandleInt(self) \/ ReturnInt(self)
TypeInv == /\ current \in THREADS \cup {"idle"} /\ worked \in [THREADS -> BOOLEAN]
SchedInv == count < Cardinality(THREADS)
AllWorked == <>(\A t \in THREADS : worked[t])}
procedure interrupt(){interrupt_handler: \* re-schedule with (t \in THREADS) current := t;ret_from_interrupt: \* Never pass this point await FALSE;}
procedure thread(){start_thread: interrupts_on := TRUE;main_loop: while (TRUE) { assert ~in_interrupt; worked[current] := TRUE;
\* Concurrency test (should not trigger if ~interrupts_on) interrupts_on := FALSE; count := count + 1;maybe_preempt: count := count - 1; interrupts_on := TRUE; }}
process (Processor = CPU){start_cpu: with (t \in THREADS) { current := t; call thread(); };}} *)\* BEGIN TRANSLATION\* removed\* END TRANSLATION
PreemptNext == Interrupt(CPU) \/ Next
PreemptSpec == Init /\ [][PreemptNext]_vars==============================================================================
Mixing TLA+ and PlusCal looks a bit adventurous to me. If you start along these lines, why not go all the way to a TLA+ spec?
But then, it seems to me that you just model a non-deterministic choice between invoking the interrupt handler or continuing processing the current thread, and PlusCal's either-or statement can be used for that.
Below is a spec along these lines, including relevant fairness properties that ensure that (i) the interrupt handler is invoked from time to time and (ii) that every unfinished thread will eventually be scheduled. These guarantee the liveness property that you want.