Networks & Operating Systems Essentials 2 (NOSE 2)
Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit
Networks & Operating Systems Essentials 2 (NOSE 2)
Assessed Exercise 2 : Scheduling
The aim of this exercise is to have students use the knowledge they have acquired so far in the second part of the course to implement a set of scheduling algorithms.
You will be using and extending a simple discrete event simulator written in Python, creating classes that simulate the scheduling and dispatching of processes performed by four scheduling algorithms: First-Come- First-Served (FCFS), Shortest-Job- First (SJF), Round- Robin (RR), and Shortest- Remaining-Time- First (SRTF).
This assessed exercise will also act as a gentle introduction to such concepts as queuing theory, discrete event simulation, and object-oriented software engineering.
Object-Oriented Software Engineering Primer
You should already be familiar with the basic concepts of object-oriented programming in Java from the JP2 course. Specifically, you should already know:
• What a class is and what member variables and functions/methods are.
• What an object (or instance of a class) is.
• How to create (instantiate) an object of a class.
• How to refer to member variables/methods using dot notation (e.g., ClassX.func()).
• What the meaning of ‘this’ is in Java.
• What inheritance is and how you can override class methods in a subclass.
• What encapsulation is – the act of bundling together data/variables and functions/methods that operate on said data/variables in a single unit: the object (or instance of a class) .
Python also allows object-oriented programming with classes . In Python, a class is defined using the keyword ‘class’, like so:
|
class Animal: … |
The equivalent of Java’s ‘this’ is Python’s ‘self’. As Python is not a pure object- oriented programming language, all class methods need to have ‘self’ as their first argument. All variables and methods defined in this class (i.e., indented one level – and indention is not optional in Python) are class variables and class methods. Also, the constructor of a class does not have the same name as the class but is rather called ‘__init__’ in Python. Furthermore, in Python, it is customary to create and initialise instance variables in a class’s __init__ method.
Subclassing, or inheritance, is one of the mainstays of many object-oriented programming languages. Each subclass S (sometimes also referred to as a child class) of a Class C has access to all members and methods of its parent class (a .k.a. superclass).
In Python, subclassing is done by including the name of the parent class in parentheses after the name of the new child class. When a subclass S has a method M with the exact same name and input parameters as those of a superclass C, we say that S’s M overrides (in essence, provides a new implementation of) C’s M. In other words, if one creates an object of S and calls M on it, it will be S’s implementation of M that will be executed, not C’s M. While other object-oriented programming languages offer mechanisms for controlling the access to a parent class’s members and methods, Python doesn’t offer such a feature. However, by convention subclasses shouldn’t use
and/or access members/methods whose name starts with one or more underscores.
Putting all this together, have a look at the example code below:
|
class Bird: def __init__ (self, species): self.species = species def what_am_i(self): print("I am a " + self.species) def can_fly(self): print("I can't fly") def can_swim(self): print("I can't swim") class Parrot (Bird): def __init__ (self): self.species = "Parrot" def can_fly(self): print("I can fly") class Penguin(Bird): def __init__ (self): self.species = "Penguin" def can_swim(self): print("I can swim") birds = [Eagle(),Penguin()] for bird in birds: bird.what_am_i() bird.can_fly() bird.can_swim() |
If you save it in a file and execute it, the output should be:
|
I am a Parrot I can fly I can't swim I am a Penguin I can't fly I can swim |
Discrete Event Simulation Primer
A discrete event simulation (DES) simulates the operation of a system via a series of discrete events ordered in time. That is, given a set of events ordered by time of occurrence from earliest to latest, the internal clock of the simulator does not take on every possible value but rather jumps from one event to the next. The simulated system is considered to be in a constant state in between any pair of successive events, while events trigger a possible change to the system’s state. The DES, in essence, implements the following algorithm:
|
Queue event_queue # Assume a queue of events, ordered by time system_time = 0 while event_queue is not empty: current_event = event_queue.remove_first_event () if current_event.time > system_time: system_time = current_event.time service(current_event) |
where service(current_event) may end up adding new events to the queue.
Note that it is not necessary that events that are removed from the queue and serviced lie in the future, but they can and, if they do, it moves the system time along.
In the simple discrete event simulator provided for this assessed exercise, we have three types of events:
• PROC_ ARRIVES: A new process arrives in the system.
• PROC_CPU_REQ: An existing process requests access to the CPU .
• PROC_CPU_DONE: A process has run its course and, thus, terminates.
Similarly, simulated processes can be in one of the following states:
• NEW: A new process arrives in the system at some point.
• READY: A process is waiting for the CPU to be allocated; note that this can be a new process that just arrived, an older process that was never scheduled, or an older process that was pre-empted.
• RUNNING: A process currently executes on the CPU.
• TERMINATED: A process has used up its service time and, thus, terminates.
Originally the simulator creates a list of processes to be simulated . Each process has the following attributes:
• Process ID: A number uniquely identifying the process in the system. As all processes are added to a table (aka the process table), their ID is simply the table index for the cell at which their info is stored, starting from 0.
• Arrival time: The time of arrival of the process.
• State: The state in which the process currently is, as discussed above.
• Service time: The total amount of CPU time required by the process.
• Remaining time: The remaining CPU burst time for the process
Each process keeps track of its execution time and offers a set of utility functions, comprising:
• A function that returns its departure time, after the process has terminated.
• A function that computes and returns its total waiting time.
• A function that computes and returns its turnaround time.
• A function that executes the process for up to a specified amount of time.
All processes start off in the NEW state. Along the same lines, the simulator populates the event queue with PROC_ARRIVES events for all processes to be simulated. The simulator’s service function then looks like the following (in abstract terms):
|
service(current_event): for process in list_of_processes: if process.arrival_time <= system_time: process.state = READY process_to_run = scheduler_func(current_event) if process_to_run != currently_executing_process: system_time += context_switch_time currently_executing_process = process_to_run new_event = dispatcher_func(process_to_run) if new_event.type != PROC_CPU_DONE: event_queue.insert (new_event) system_time = new_event.time |
The service function makes use of two more functions, printed in italics above: scheduler_func(event) and dispatcher_func(process). The former takes the current event into consideration and selects the next process to be given access to the CPU, based on the scheduling policy/algorithm. The latter takes as an argument the process selected by the scheduler and executes it on the CPU; this translates to transitioning the process to the RUNNING state, allowing it to run for a specific amount of time (dependent on the scheduling algorithm used), then transitioning it to either the READY or the TERMINATED state (depending on whether it used up its service time), generating and returning an appropriate event (PROC_CPU_REQ in the former case, PROC_CPU_DONE in the latter case). If the event is a PROC_CPU_REQ one, it is added to the events queue, as it will require further processing in the future. Finally, the simulator’s internal (simulated) clock is updated to the time of the returned event. With this in hand, you should be able to implement simple process scheduling algorithms, as outlined below.
The design of the simple discrete event simulator provided for this assessed exercise, follows an object-oriented approach. The information and methods required for events and processes are implemented in a set of classes (Event, EventTypes, Process, ProcessStates). The basic simulator logic is also implemented in a class of its own – namely, SchedulerDES. Then, four new scheduler-specific classes are provided; namely, FCFS, SJF, RR, and SRTF – one for each of the scheduling algorithms to be implemented. The classes are defined as subclasses of SchedulerDES.
Method overriding is used in the provided source code to allow you to implement the various scheduling algorithms without having to touch the discrete event simulator implementation. You will notice that the source code of the latter includes skeleton definitions for the scheduler and dispatcher functions discussed above, and that these same functions are defined in the subclasses as well. You only need to edit the latter, as it is these implementations that will be used by the main function of the simulator. Remember that you still have full access to all methods/members of SchedulerDES, yet you should only really use those without one or more leading underscores.
Scheduling Algorithms
As part of this assessed exercise, you are asked to implement the following scheduling algorithms:
• FCFS/FIFO (non-pre-emptive): Processes should be executed in the order in which they arrive at the system. Conceptually, when a process arrives, it is added to a queue. The scheduling algorithm will always pick the first process in the queue and will execute it to completion. It will then proceed with the next process in the queue, and so on.
• SJF (non-pre-emptive): Processes are prioritised based on their service time. Conceptually, on arrival, processes are added to a priority queue, which is sorted in ascending order of service time. The scheduling algorithm will then always remove the first process in the queue and will execute it to completion. It will then proceed with the next process in the queue, and so on.
• RR (pre-emptive): On arrival, processes are added to a queue. Conceptually, the algorithm will pick the first process in the queue, execute it for a specified amount of time (also known as a time slice or quantum), and, if the process needs more time, it will be added to the end of the queue again.
• SRTF (pre-emptive): This is a pre-emptive version of the SJF algorithm above. Conceptually, whenever a change occurs in the system (i.e., a new process arrives, a running process terminates, etc.), the scheduler is called to select the process among those in the READY state with the minimum remaining execution time. This process might then be pre-empted when a new change occurs that results in another process a.) being ready and b.) having a lower remaining execution time than the one currently executing.
Pseudo- Random Number Generation
The main simulator code makes use of a pseudo-random number generator (PRNG) to generate the process arrival and service times, following the M/M/1 queue equations (as briefly discussed on last week’s lab sheet). The “pseudo“ in PRNG denotes that these generators do not really produce completely random numbers.
Instead, they compute a new “random” output based on a deterministic computation on state kept internally by the PRNG implementation. Consequently, if one could control the original internal state, they would be able to force the PRNG into producing repeatable sequences of “random” outputs. Most implementations of PRNGs support this functionality through seeding; in other words, they provide a method that allows the user to define an initial seed (usually taking the form of an integer), based on which the PRNG initialises its internal state in a deterministic manner. For example, execute the following code in your favourite Python environment:
|
for repetition in range (3): print ("Repetition {}:".format(repetition)) for i in range (10): random.random() |
You should get three sequences of ten random floats, each different to the next. Then, execute the following code:
|
for repetition in range (3): print ("Repetition {}:".format(repetition)) random.seed(1) for i in range (10): random.random() |
You should now get three sequences of ten random floats, but all three sequences are identical to each other. This is because we have reset the seed to the same value (1) before the generation of each of these sequences.
This is used in the provided implementation to allow for repeatable experiments . The PRNG defaults to a random initial seed. This seed is printed on the screen when the simulator starts. The program also features a command-line argument that allows you to set the seed to a user-defined value, as well as arguments to enable verbose logging of the internal state of the simulator. You can use this whenever a run of the simulator produces counter-intuitive results, to allow you to dig into the problem and identify the cause of the discrepancy between actual and expected results.
For your consideration, here is a set of “interesting” seeds: 1797410758, 2688744162, 3399474557. These are interesting in the sense that the respective workloads make some algorithms win over others. What you should do in these cases, is execute the program with debug logging on, examine the sequence of events/scheduling decisions and address the points laid out in the spec below.
Miscellanea
Start by reading and trying to understand the provided code. Please feel free to ask us if you have any questions that cannot be answered by a simple online search. You should not need to read up on the concepts outlined earlier (queuing theory, discrete event simulation, object-oriented programming) much, but feel free to do so if you deem it necessary to understand the provided code (or if you are interested).
You should only need to change the code in schedulers.py. The list of imports in said file includes all imports that you should need for the assessed exercise. Please ask us if you think more imports are necessary, as this might indicate a misunderstanding.
Please make sure that your code is well formatted and documented, and that an appropriate function/variable naming scheme has been used.
The main function (main.py) provides several command-line options that allow you to change various parameters of the system. Executing the program with ‘ -h’ or ‘ --help’ will print a help message that explains the supported options.
A set of sane defaults is provided in the source code, but feel free to play around with other values. You can do so by either executing the simulator on the command line and providing different arguments, by changing the defaults in the source code (main.py), or by providing your own parameters as input to the parser.parse_args(…) function; as an example of the latter, to execute the program with the command-line arguments -S 851898649, do the following:
|
args = parser.parse_args(['-S', '851898649']) |
To print the help message, do the following:
|
args = parser.parse_args |
2022-11-16