diff --git a/simso/configuration/Configuration.py b/simso/configuration/Configuration.py index 4c0f2d7..15b3598 100644 --- a/simso/configuration/Configuration.py +++ b/simso/configuration/Configuration.py @@ -281,7 +281,8 @@ def add_task(self, name, identifier, task_type="Periodic", abort_on_miss=True, period=10, activation_date=0, n_instr=0, mix=0.5, stack_file="", wcet=0, acet=0, et_stddev=0, deadline=10, base_cpi=1.0, followed_by=None, - list_activation_dates=[], preemption_cost=0, data=None): + list_activation_dates=[], preemption_cost=0, data=None, + m=1, k=1): """ Helper method to create a TaskInfo and add it to the list of tasks. """ @@ -292,7 +293,7 @@ def add_task(self, name, identifier, task_type="Periodic", activation_date, n_instr, mix, (stack_file, self.cur_dir), wcet, acet, et_stddev, deadline, base_cpi, followed_by, list_activation_dates, - preemption_cost, data) + preemption_cost, data, m=m, k=k) self.task_info_list.append(task) return task diff --git a/simso/core/Job.py b/simso/core/Job.py index 4708fd3..4aa3be9 100644 --- a/simso/core/Job.py +++ b/simso/core/Job.py @@ -2,7 +2,7 @@ from SimPy.Simulation import Process, hold, passivate from simso.core.JobEvent import JobEvent -from math import ceil +from math import floor, ceil class Job(Process): @@ -29,6 +29,8 @@ def __init__(self, task, name, pred, monitor, etm, sim): """ Process.__init__(self, name=name, sim=sim) self._task = task + self._m = task._task_info.m + self._k = task._task_info.k self._pred = pred self.instr_count = 0 # Updated by the cache model. self._computation_time = 0 @@ -49,6 +51,11 @@ def __init__(self, task, name, pred, monitor, etm, sim): self.context_ok = True # The context is ready to be loaded. + self.instance_num = int(name.split("_")[1]) + self.cri = floor(ceil(self.instance_num * self._m/ self._k) * (self._k / self._m)) + self.mandatory = self.instance_num == self.cri + self.optional = not self.mandatory + def is_active(self): """ Return True if the job is still active. diff --git a/simso/core/Task.py b/simso/core/Task.py index da5ad2d..25b5dcf 100644 --- a/simso/core/Task.py +++ b/simso/core/Task.py @@ -20,7 +20,8 @@ class TaskInfo(object): def __init__(self, name, identifier, task_type, abort_on_miss, period, activation_date, n_instr, mix, stack_file, wcet, acet, et_stddev, deadline, base_cpi, followed_by, - list_activation_dates, preemption_cost, data): + list_activation_dates, preemption_cost, data, + m=1, k=1): """ :type name: str :type identifier: int @@ -63,6 +64,9 @@ def __init__(self, name, identifier, task_type, abort_on_miss, period, self.data = data self.preemption_cost = preemption_cost + self.m = m + self.k = k + @property def csdp(self): """ @@ -272,7 +276,7 @@ def create_job(self, pred=None): directly by a scheduler. """ self._job_count += 1 - job = Job(self, "{}_{}".format(self.name, self._job_count), pred, + job = Job(self, "{}_{}".format(self.name, self._job_count - 1), pred, #-1 to be zero indexed monitor=self._monitor, etm=self._etm, sim=self.sim) if len(self._activations_fifo) == 0: diff --git a/simso/schedulers/MK_DBP.py b/simso/schedulers/MK_DBP.py new file mode 100644 index 0000000..397c62e --- /dev/null +++ b/simso/schedulers/MK_DBP.py @@ -0,0 +1,158 @@ +""" +@author Jonathan Tan (@jona1115 on GitHub) +@date 12/09/2025 +""" + +from collections import deque + +from simso.core import Scheduler +from simso.schedulers import scheduler + + +@scheduler("simso.schedulers.MK_DBP") +class MK_DBP(Scheduler): + """ + Distance Based Priority scheduler for (m,k)-firm tasks. + + Based on: Joël Goossens. (m, k)-firm constraints and DBP scheduling: impact of the initial + k-sequence and exact schedulability test. 16th International Conference on Real-Time and + Network Systems (RTNS 2008), Isabelle Puaut, Oct 2008, Rennes, France. ⟨inria-00336461⟩ + + This is coded to work on Uniprocessor simulations! + """ + + def init(self): + self.ready_list = [] + self._k_histories = {} + for task in self.task_list: + self._k_histories[task] = self._init_history(task) + + def on_activate(self, job): + self.ready_list.append(job) + job.cpu.resched() + + def on_terminated(self, job): + self._update_history(job) + if job in self.ready_list: + self.ready_list.remove(job) + else: + job.cpu.resched() + + def _should_run(self, candidate, current): + if current is None: + return True + if current.mandatory and candidate.optional: + return False + return self._priority_tuple(candidate) < self._priority_tuple(current) + + def _priority_tuple(self, job): + return ( + job.optional, # mandatory > optional + self._compute_distance(job.task), # distance + job.absolute_deadline, # on tie use EDF + job.period # then RMS + ) + + def _priority_distance(self, job): + if job is None: + return float("inf") + return self._compute_distance(job.task) + + def _compute_distance(self, task): + """ + Distance, as per Goossens 2008, is defined as starting from time now, the amount of + deadline we can affort to miss before we violate the (m,k)-firm requirement. + + In practice, we achieve this but keeping track of the history of the task as a list. + if successes is < m the distance is 0 because it is already violated (m,k)-firm + requirement. Else, it append a hypothetical miss into the list, then it runs the check + again. + """ + history = self._k_histories.get(task) + if history is None: + history = self._init_history(task) + self._k_histories[task] = history + m = getattr(task._task_info, "m", 1) + if m <= 0: + return float("inf") + sequence = list(history) + successes = sum(sequence) + if successes < m: + return 0 + allowed = 0 + current = sequence[:] + k = len(current) + while allowed < k: + current = current[1:] + [0] + allowed += 1 + if sum(current) < m: + return allowed - 1 + return allowed + + def _init_history(self, task): + k = max(1, getattr(task._task_info, "k", 1)) + initial = None + if task.data and isinstance(task.data, dict): + initial = task.data.get("initial_k_sequence") + values = self._normalize_init_sequence(initial) + history = deque(maxlen=k) + slice_start = max(0, len(values) - k) + for value in values[slice_start:]: + history.append(value) + while len(history) < k: + history.appendleft(1) + return history + + @staticmethod + def _normalize_init_sequence(initial): + if initial is None: + return [1] + result = [] + if isinstance(initial, str): + for char in initial: + if char == "1": + result.append(1) + elif char == "0": + result.append(0) + elif isinstance(initial, (list, tuple, deque)): + for value in initial: + result.append(1 if value else 0) + elif isinstance(initial, bool): + result.append(1 if initial else 0) + elif isinstance(initial, (int, float)): + result.append(1 if initial else 0) + else: + result.append(1) + if not result: + result.append(1) + return result + + def _update_history(self, job): + task = job.task + history = self._k_histories.get(task) + if history is None: + history = self._init_history(task) + self._k_histories[task] = history + history.append(0 if job.exceeded_deadline else 1) + + def schedule(self, cpu): + if not self.ready_list: + return None + + key = lambda x: ( + 0 if x.running is None else 1, + -self._priority_distance(x.running) if x.running else 0, + 0 if x is cpu else 1, + ) + cpu_min = min(self.processors, key=key) + + job = min(self.ready_list, key=self._priority_tuple) + + if self._should_run(job, cpu_min.running): + self.ready_list.remove(job) + if cpu_min.running: + self.ready_list.append(cpu_min.running) + print(self.sim.now() / 1000000, job.name, cpu_min.name) + return (job, cpu_min) + + return None \ No newline at end of file diff --git a/simso/schedulers/MK_EDF.py b/simso/schedulers/MK_EDF.py new file mode 100644 index 0000000..ef320df --- /dev/null +++ b/simso/schedulers/MK_EDF.py @@ -0,0 +1,49 @@ +""" +@author Jonathan Tan (@jona1115 on GitHub) +@date 12/09/2025 +""" + +from simso.core import Scheduler +from simso.schedulers import scheduler + +@scheduler("simso.schedulers.MK_EDF") +class MK_EDF(Scheduler): + """Earliest Deadline First (EDF) with (m,k)-firm requirements""" + def on_activate(self, job): + job.cpu.resched() + + def on_terminated(self, job): + job.cpu.resched() + + def schedule(self, cpu): + # List of ready jobs not currently running: + ready_jobs = [t.job for t in self.task_list + if t.is_active() and not t.job.is_running()] + + if ready_jobs: + # Select a free processor or, if none, + # the one with the greatest deadline (self in case of equality): + key = lambda x: ( + 1 if not x.running else 0, + x.running.absolute_deadline if x.running else 0, + 1 if x is cpu else 0 + ) + cpu_min = max(self.processors, key=key) + + + # Select the job with the least priority: + job = min(ready_jobs, key=lambda x: (x.optional, x.absolute_deadline, x.period)) + + if ((# If there is nothing running rn, or + (cpu_min.running is None) or + # if the current running is optional and the job is mandatory, also if the current running deadline < job deadline, or + ((cpu_min.running is not None and job is not None) and (cpu_min.running.optional and job.mandatory) and (cpu_min.running.absolute_deadline < job.absolute_deadline)) or + # if the current running deadline is > the job's deadline + (cpu_min.running.absolute_deadline > job.absolute_deadline) + ) and + # finally we make sure a mandatory task don't get preempted by a optional task + not ((cpu_min.running is not None and cpu_min.running.mandatory) and job.optional) + ): + + print(self.sim.now()/1000000, job.name, cpu_min.name) + return (job, cpu_min) diff --git a/simso/schedulers/MK_RMS.py b/simso/schedulers/MK_RMS.py new file mode 100644 index 0000000..bab402d --- /dev/null +++ b/simso/schedulers/MK_RMS.py @@ -0,0 +1,50 @@ +""" +@author Jonathan Tan (@jona1115 on GitHub) +@date 12/09/2025 +""" + +from simso.core import Scheduler +from simso.schedulers import scheduler + +@scheduler("simso.schedulers.MK_RMS") +class MK_RMS(Scheduler): + """ Rate monotonic (RMS) with (m,k)-firm requirements """ + def init(self): + self.ready_list = [] + + def on_activate(self, job): + self.ready_list.append(job) + job.cpu.resched() + + def on_terminated(self, job): + if job in self.ready_list: + self.ready_list.remove(job) + else: + job.cpu.resched() + + def schedule(self, cpu): + decision = None + if self.ready_list: + ############### As far as I can tell this chunk is just selecting a CPU, so for uniprocessor, this chunk is junk ############### + # Get a free processor or a processor running a low priority job. + key = lambda x: ( # this lambda func aparently returns a 3 element tuple + 0 if x.running is None else 1, + -x.running.period if x.running else 0, + 0 if x is cpu else 1 + ) + cpu_min = min(self.processors, key=key) # with one cpu, cpu min will just be the only cpu + ####################################################### end junk chunk ####################################################### + + # Job with highest priority. + job = min(self.ready_list, key=lambda x: (x.optional, x.period)) + + if (((cpu_min.running is None) or (cpu_min.running.period > job.period)) and + not ((cpu_min.running is not None and cpu_min.running.mandatory) and job.optional)): # this check is to prevent a optional job preempting a mandatory job + + self.ready_list.remove(job) + if cpu_min.running: + self.ready_list.append(cpu_min.running) # if it got preempted, add it back to the que + decision = (job, cpu_min) + print(self.sim.now()/1000000, job.name, cpu_min.name) + + return decision