Time-slicing applications with SIGCONT and SIGSTOP
Python's subprocess.Popen class and signal library provide powerful ways to communicate with and send signals to a running subprocess.
We'll look at at using the
SIGSTOP and SIGCONT signals
to pause and resume the execution of a program. As an example,
we'll launch another Python process that runs a print_time
script.
The print_time
script takes two arguments, a time delta $n$ and a
total time $t$. The script prints its process ID and the Unix time every
$n$ seconds and completes after $t$ seconds.
The print_time
script looks like
import os
import sys
import time
if __name__ == "__main__":
sleep_time = float(sys.argv[-2])
run_time = float(sys.argv[-1])
end_time = time.time() + run_time
while time.time() < end_time:
print(f"{os.getpid()}: {time.time()}")
time.sleep(sleep_time)
Even though we're running other Python processes in this example, the concept of using signals applies to any program. The same script could've been implemented in any language or even with a mix of bash and Unix programs.
import signal
from subprocess import Popen, PIPE
import time
A few notes about the Popen
class which launches a running
subprocess. The first argument is a list of strings containing the
program to run and its arguments. This line
Popen(["python", "print_time.py", "1" "5"])
Is equivalent to opening your terminal and running
python print_time.py 1 5
The subprocess will begin running as soon as a Popen
object is
constructed and the Popen.wait()
method will wait for the subprocess
to terminate.
The stdout
argument sets where the process's output will be
written. By using subprocess.PIPE
, it will be written to the
Popen.stdout
attribute which can be read like a file. The encoding
argument sets stdout
to be interpreted as a stream of text rather
than a stream of bytes.
In the example below, we'll run the print_time
program for five seconds
with the Unix time printed at one second intervals. Two seconds in, we'll
use the SIGSTOP
signal to pause the program's execution and then resume
it two seconds later with the SIGCONT
signal.
p = Popen(["python", "print_time.py", "1", "5"], stdout=PIPE, encoding="utf-8")
time.sleep(2)
p.send_signal(signal.SIGSTOP)
time.sleep(2)
p.send_signal(signal.SIGCONT)
p.wait(); # Semi-colon is so the output of p.wait() is not printed
Note in the output below that the program runs for two seconds, pauses for a two second gap, then runs for two more seconds.
for line in p.stdout.readlines():
print(line.strip())
For a more involved example, we'll launch three processes
running print_time
and stop all of them immediately. We'll then
use SIGSTOP
and SIGCONT
to allow each of them to run one at a
time in round-robin fashion until they've all exited. This functions
like a time-slicing mechanism in which each program gets equal
execution time and none of the programs run concurrently.
processes = [
Popen(["python", "print_time.py", "1", "7"], stdout=PIPE, encoding="utf-8")
for _ in range(3)
]
for process in processes:
process.send_signal(signal.SIGSTOP)
The Popen.poll()
method returns None
if a process is still
running. We'll use this to check whether any of the processes
are still running and, if so, continue to run each process
one-by-one.
while any(process.poll() is None for process in processes):
for process in processes:
process.send_signal(signal.SIGCONT)
time.sleep(2)
process.send_signal(signal.SIGSTOP)
To see how the processes ran in a non-overlapping fashion, we can combine the output of each process into one list and then sort the list by the printed Unix time value. This will reconstruct which process was running at each timestamp.
lines = [
line.strip()
for process in processes
for line in process.stdout.readlines()
]
lines.sort(key=lambda line: float(line.split()[-1]))
In the output below, you can see each process run for two seconds before switching to a different process.
print("\n".join(lines))
Parting thoughts¶
The example above does, in a very rough way, what an ARINC 653 operating system does in partitioning applications by time.
A fun exercise would be to write a program that takes a config file containing programs with a time-slice allocation and coordinates their execution. It would be interesting to come up with techniques for precise timing control and measuring how well the OS and Python interpreter are able to switch between the running programs.
My idea for this came while watching this command-line environment lecture which does a great job giving an overview of signals and a number of other things. The entire Missing Semester lecture series is really fantastic and something that I wish I'd taken a college course on. I found the lectures on editors, data wrangling, and version control to be particularly useful.