====== Lecture 6 ====== =====Signals (continued)==== The operating system utilizes signals as a form of **asynchronous communication** between processes. They are the operating system's system for notifying a process that an event has occurred. Signals allow for the interrupt of user process as well as inter-process communication (IPC) such as Pipes. When a process receives a signal, it calls its function specific signal handler, generally setting a few flags and returning to previously executing instruction, unless the process has been killed. For example, if Process A calls write() and Process B calls read() through a pipe, when does Process B get the message? Process B only gets the message when it reads on the pipe file descriptor. __Why would Process B not be reading?__ * B is reading something else * B is executing a CPU bound computation * B called the sleep function __What if the message is urgent?__ In the worst case, Process B gets the message at most in 1 sec (arbitrary number). This occurs because even during busy waiting/polling, ever second Process B will check for a message. If there is no message it will continue with its busy work. **Need User Level Interrupts** User Level Interrupts preemptively interrupt a running process to execute a specific signal handler. Once the receiver of the signal receives the interrupt, it halts all computation and jumps to its specific signal handler function with its own stack. The specific function is specified in the interrupt command. kill ( pid_t p, int signo) This delivers a user level interrupt number “signo” to the process with id “p” Note: Kill must interrupt other system calls so the receiver of the signal, if it is in a system call, returns early. If it is the case that the process was interrupted while in a system call, then the system call will return -1 on the error and also will set the global variable ERRNO == EINTR. During the user level interrupt hardware and kernel interrupts are still active such as the timer interrupt. =====Scheduling===== **OS Goal:** To effectively __multiplex__ the physical machine's resources among the abstract machines A main task for the OS is to schedule the various processes on the machine and distribute the resources. The scheduling system must determine which of the runnable processes will run next and how much CPU time it will be given. There are numerous techniques to determine the order execution and amount of CPU time, which will be discussed below. **Process State Diagram** To schedule the various runnable processes, chunks of CPU time are assigned to each abstract machine. __Scheduling problems__ arise because of the variety of execution deadlines and the difficulty to meet each of them. {{.:process_state_diagram2.png|.:process_state_diagram2.png}} **Scheduling Metrics** Scheduling metrics are measurements that tell how good a schedule is. There are several different types of scheduling metrics: - __CPU Utilization__ is the fraction of time spent executing application code. It is denoted by ρ where 0<=ρ<=1. In this case, 0 represents low CPU utilization while 1 represents high CPU utilization. - __Responsiveness__ is the time between when a process enters the runnable state and when it starts running. It is denoted by w which stands for the average waiting time among all processes. We want w to be as low as possible (i.e. low latency). If we are measuring the waiting time of a single process i, then we use w(i) to represent its waiting time. - __Turnaround time__ is the time from when a process becomes runnable until it becomes blocked or has exited. It is denoted by TTRND which stands for the average turnaround time among all processes. We want TTRND to be as low as possible. If we are measuring the turnaround time of a single process i, then we use TTRND(i). - __Throughput__ is the mean number of jobs that can be performed per second. It is denoted by μ where μ = 1/TTRND. We want the throughput to be as high as possible. A job is a unit of work by a process (i.e. the period between when a process becomes RUNNABLE until it is blocked or has exited). Preferences: We want to influence the schedulers decision in order to achieve the following user goals: * We want to give some schedules priority over other schedules. * We want to make sure each process is finished by a given deadline. **Scheduling 4 jobs** Let's say we are given the following four jobs to schedule along with their service times T: ^Job ^Τ (time to complete)^ | A | 5 | | B | 2 | | C | 9 | | D | 4 | We assume that all processes start running at the same time. __First Come First Serve (FCFS)__ If we execute A, followed by B, followed by C, followed by D, we get the following Gantt Chart: ^0 ^5 ^7 ^16 ^20 |----A------|--B--|--------C----------|----D-----| Note: When we switch from one process to another we encounter a delay known as the contact switch time. We will denote the contact switch by using C. Analysis of responsiveness and turnaround time: ^ Job ^ W ^ TTRND ^ | A | 0 | 5 | | B | 5 + C | 7 + C | | C | 7 + 2C | 16 + 2C | | D | 16 + 3C | 20 + 3C | |Average | 7 + 1.5C | 12 + 1.5C | Analysis of CPU utilization: Since the CPU is not used during contact switches, the utilization is ρ = 20/(20+3C). __FCFS Predictions__ We have stable load which means that we can always get a good idea of the average TTRND. Lets says that L processes are waiting in a queue while 1 process is running. If we get a new job, we can easily estimate the new average turnaround time. Below is the formula that computes it: L*TTRND+TTRND+.5*TTRND The first term is the turnaround time of the L queued jobs. The second term is the turnaround time of the new job. The third term is the turnaround time of the current running job. __Shortest Job First (SJF)__ If we execute the jobs in order of the time they take, we get the following Gantt Chart: ^0 ^2 ^6 ^11 ^20 |--B---|----D----|-----A-----|---------C---------| Analysis of responsiveness and turnaround time: ^Job ^ W ^ TTRND ^ | A | 6 + 2C | 11 + 2C | | B | 0 | 2 | | C | 11 + 3C | 20 + 3C | | D | 2 + C | 6 + C | |Average | 4.75 + 1.5C | 9.75 + 1.5C | __Analysis of Shortest Job First Algorithm__ *The algorithm minimizes the average wait time **without preemption**. *A problem with the algorithm is **starvation** which means that you may never get to run a long job. Example of Starvation: ^Time ^Job:T ^ |0 |A:2 , B:1 | |1 |C:1 | |2 |D:1 | Since job A takes 2 units of time, it will be executed after job B. However, if jobs that take 1 unit of time keep appearing each second (like C and D), job A might never be executed. In this example, at time t, t+2 units of work will remain. This is known as CPU saturation. In practice, however, CPU starvation is generally unlikely. The FCFS and SJF algorithms are examples of cooperative scheduling. This works great for some cases but not for other cases such as the following: ^ Job ^ T ^Arriving Time ^ | A | 10100 | 0 | | B | 1 | 1 | In this case, we want job B to preempt or interrupt job A. **Preemptive Scheduling** *Quantum: a quantum is a measure of how many units of jobs run before preempted. __Round Robin__ *In round robin, we give time to each process in a given order, and every Q jobs we interrupt and switch to another process. Example:\\ If Q = 3,\\ We get the following Gantt Chart: ^0 ^3 ^5 ^8 ^11 ^13 ^16 ^17 ^20 |---A---|--B--|---C---|---D---|--A--|---C---|-D-|---C---| Analysis of responsiveness and turnaround time: ^Job ^ W ^ TTRND ^ | A | 0 | 13 + 4C | | B | 3 + C | 5 + C | | C | 5 + 2C | 20 + 7C | | D | 8 + 3C | 17 + 6C | |Average | 4 + 1.5C | 12.75 + 4.5C | If Q = 2,\\ We get:\\ \\ **W** = 3 + 1.5C\\ **TTRND** = 13.25 + 6C\\ *Quantum uses the clock timer interrupt frequency **Priority** *Priority allows the user to influence scheduling decisions *Smaller number = higher priority = more important Example:\\ While still using Q = 3,\\ We augment our process list:\\ ^Job ^Τ ^Priority ^ | A | 5 | 2 | | B | 2 | 2 | | C | 9 | 0 | | D | 4 | 1 | *We schedule the most important processes first, and do round robin for processes with the same priority level We get the following Gantt Chart: ^0 ^3 ^6 ^9 ^12 ^13 ^16 ^18 ^20 |---C---|---C---|---C---|---D---|-D-|---A---|--B--|---A---| Analysis of responsiveness and turnaround time: ^Job ^ W ^ TTRND ^ | A | 13 + 5C | 20 + 7C | | B | 16 + 6C | 18 + 6C | | C | 0 | 9 + 3C | | D | 9 + 3C | 13 + 4C | |Average | 9.5 + 3.5C | 15 + 5C | //__Multi-Level Priority System__// *Strict Priority *Run processes with smallest(highest) priority, using round-robin *Can lead to starvation (run a high priority job forever) //__Priority Inversion__// *What if C sends a message to A and wait for its response? *Then D runs, since prio(D) < Prio(A) *Problem: C is waiting for D, although prio(C) < prio(D) *Solution: When C wait for A, set effective priority *effprio(A) = min( prio(A), prio(C) ) //__Privilege and Priority__// *A, having prio(A), wants to set to priority "p" *if p < prio(A) => need privilege *if p ≥ prio(A) => no need for privilege //__Multi-Level Feedback Queues__// *Multiple priorities *Get feedback from CPU utilization to avoid starvation *More CPU utilization => larger priority number (lower priority) p_estcpu: process's estimated CPU utilization p_nice: normal priority p_usrpri = 1/4 * ( 50 + 1/4 * p_estcpu + 2 * p_nice )