Problem Set 6 Harvard Extension School CSCI E-92: Principles of Operating Systems Spring 2024 Due: April 28, 2024 at Midnight ET (Eastern Time) Total of 200 Points 1. (200 Points) Freescale K70 Programming. Write a C program (with embedded assembler as necessary) for the Freescale K70/MK70FN1M0 Tower Development Kit that implements the following constructs: a. (120 Points) Use interrupts from the SysTick timer (See B3.3 on page B3-676 in the ARMv7-M Architecture Reference Manual, ARM DDI 0403E.b; See B3.3 on page B3-744 in the ARMv7-M Architecture Reference Manual, Errata markup, ARM DDI 0403Derrata 2010_Q3) to implement a round-robin scheduler. Determine an appropriate quantum given our processor speed and an approximate number of cycles required per average instruction. In order to schedule processes, your operating system should maintain a process list with a process control block (PCB) for each process. The process list should be organized as (one or more) circular linked lists of PCB's. Each process should be identified by a process id (PID) number. The PID number is returned when a new process is created and is passed to all supervisor calls that act on processes (e.g. to wait for a process, to kill a process). Each PCB should contain sufficient information to save and restore the state of the process (see below), to identify the process (the PID), to indicate the state of the process (running, ready, blocked), to point to the storage allocated for that process' stack and the size of that stack, to track the CPU time attributed to that process, and to maintain information required by the operating system on a per-process basis (such as open streams). To be clear, this means that each process must have its own private set of open streams. If you have chosen to implement streams in the style of Unix/Linux, then, for example, each process will have its own stdin, stdout, and stderr. The state of the process will include all registers that processes are allowed to change (possibly R0, R1, R2, R3, R12, SP (R13), LR (R14), xPSR, PC (R15), the remaining registers: R4 through R11, and, if you're using floating-point hardware, the floating-point registers: S0 through S15 and the FPSCR), the stack, etc. You should save all of the registers (except, obviously, for the SP) on the stack rather than in the PCB. Then, the SP would be saved into the PCB. Keep in mind that each process will have its own stack. Note that all memory allocated for the newly-spawned process (such as the PCB and stack) must be owned by that process (i.e., the PID in the malloc data structure should be the PID of the new process, not the PID of the process that spawned it). Note that during processor reset, the ARM is running on the Main stack (See B1.4.1 on page B1-572 in the ARMv7-M Architecture Reference Manual, ARM DDI 0403E.b; See B1.4.1 on page B1-623 in the ARMv7-M Architecture Reference Manual, Errata markup, ARM DDI 0403Derrata 2010_Q3). This implies that both process code and interrupt handlers will be running on the same stack. In CodeWarrior, the SP is initialized in __thumb_startup in startup.c (found at C:\Freescale\CW MCU v10.2\MCU\ ARM_EABI_Support\ewl\EWL_Runtime\Runtime_ARM\Source\startup.c in a default installation). b. (60 Points) Each process should be identified by a process id (PID) number. The PID number is returned when a new process is created and is passed to all supervisor calls that act on processes (e.g. to wait for a process, to kill a process). Implement the supervisor calls to create a process (spawn), to prematurely yield the remainder of your quantum to the next process (yield), to cause your process to block (block), to awaken a blocked process (wake), to kill a process (kill), and to wait for a process to terminate (wait). Remember that when a new process is created, a new PCB and stack must be created for that process. Similary, when a process ends (naturally or when killed), any open streams need to be closed and the storage used for its PCB and for its stack must be reclaimed. In addition, all dynamically-allocated (malloc'ed) storage owned by the process that is ending needs to be freed. Here are prototype declarations of these supervisor calls: typedef uint32_t pid_t; (1 Point) pid_t pid(void); /* returns pid of current process */ (20 Points) int spawn(int main(int argc, char *argv[]), int argc, char *argv[], uint32_t stackSize, pid_t *spawnedPidPtr); /* returns indication of success */ /* main is the function to be run by the newly created process */ /* argc is the argc to be passed to main */ /* argv is the argv to be passed to main; NOTE: the argv array and the strings pointed to by argv need to be copied into newly allocated memory that will be owned by the new process */ /* stackSize is the size of the stack to be allocated for the new process */ /* sets spawnedPid to pid of spawned process */ (10 Points) void yield(void); /* yields remaining quantum */ (2 Points) void block(void); /* sets the current process to blocked state */ (2 Points) int blockPid(pid_t targetPid); /* sets the targetPid process to blocked state */ /* returns indication of success */ (5 Points) int wake(pid_t targetPid); /* sets the targetPid process to ready state */ /* returns indication of success */ (15 Points) int kill(pid_t targetPid); /* prematurely terminates the targetPid process */ /* returns indication of success */ (5 Points) void wait(pid_t targetPid);/* waits for the targetPid process to end execution (naturally or prematurely) */ To avoid having to deal with the spawn supervisor call having more than four parameters, you may choose to redefine the spawn SVC to reduce the number of parameters. You might accomplish this by passing a pointer to a struct as one of the parameters where the struct would contain additional parameters. If you use the prototype declaration for spawn given above (with five parameters), then this is how the parameters are passed: main passed in r0 argc passed in r1 argv passed in r2 stackSize passed in r3 spawnedPidPtr passed on stack at TOS before call c. (10 Points) Using the interrupt-driven I/O drivers that you've already developed, write an application invoked by a new shell command "multitask" that takes a single filename argument and that tests the supervisor calls above by creating three processes to accomplish the following tasks, one per process: (First process) copying from UART5 input to the named filename, (Second process) sending a message over UART5 output whenever pushbutton S2 is depressed, (Third process) using the supervisor call for user timer events, flash the orange LED on and off every half a second (the LED will light once a second). In all cases, any I/O operations should be performed using device independent supervisor calls (fopen, fgetc, fputc, and fclose, or your equivalents). The first process (the one that performs input from UART5), will terminate when control-D is entered. When the shell's multitask command determines that that process has terminated, it will kill the other two processes. d. (5 Points) Implement a "ps" shell command that lists all processes in the PCB chain: the PID, the program name (i.e., argv[0]), the process state, and CPU time used. Keep in mind that you need to lock the PCB list when traversing it to create the information to be output by ps. You must not disable interrupts while performing the output from ps. Instead, while traversing the PCB list you should store the relevant information in malloc'ed memory that will be output once you re-enable interrupts. e. (4 Points) Implement a "spawn" shell command that spawns off a new process for the following commands that you've already written: touch2led from PS4 pb2led from PS4 flashled from PS5 The spawn command will take a single argument which is one of touch2led, pb2led, or flashled and will spawn off a process to run that code. f. (1 Points) Implement a "kill" shell command that takes a single argument which is the PID of the process to be killed. Last revised 29-Mar-24