CS 162, Spring 2007
Section notes for 30 January
2007
Sections 102/103
GSI: Thomas Kho <cs162-ta@inst.eecs.berkeley.edu> /
<tkho@eecs>
Based on notes by Hakim Weatherspoon and Karl Chen
AGENDA
-
Groups
-
Multiprogramming
-
Scheduling
-
Synchronization
ADMINISTRIVIA
-
2/6 7p, Nachos tutorial @ 306 Soda
-
2/13 11:59p, Nachos phase 1 initial design doc
-
2/20 11:59p, Nachos phase 1 code
-
2/22 11:59p, Nachos phase 1 final design doc
PROCESSES and THREADS
-
A "process" generally refers to a single program that you run,
with its own code, address space, etc.
-
A "thread" is an entity corresponding to a single flow of
control through that program
-
One process can have many threads - those threads all share the
same address space, but have their own stack, CPU registers, etc.
-
code section, data section, heap and OS resources (open
files, sockets, signals)
-
Each thread: Program Counter, Register
Set, Stack space
-
no protection between threads in a task, no problem because
threads should be cooperating
Multiprogramming
-
The illusion to support: Multiple programs on the same hardware
which each think they have infinite resources and the whole machine to
themselves
-
The reality: Single machine with limited resources (one CPU,
memory, network, disk, etc.)
-
How does it all work? The most important aspect of OS
design
-
A: Address spaces for isolation and abstraction
-
Give each program its own ADDRESS SPACE:
-
Range of "virtual addresses" (usually in range of 0 to 2^32)
which it can access
-
Program thinks that each virtual address corresponds to its
own "private memory"
-
Important as this ISOLATES processes
-
QUESTION: Apart from protection, how else are address spaces
useful?
-
ANSWER: Also lets programs be compiled to use virtual addresses
(i.e. compiler can place data variable at location "0x3000", without needing
to know where other programs are running on the machine)
-
OS maps virtual memory addresses to physical memory addresss,
with the help of the MMU (in hardware) more on h/w support later
-
QUESTION: What if we have less physical memory than virtual
memory?
-
ANSWER: Paging: OS swaps memory to and from disk
-
Get into all of the details later in the course
-
Regardless of paging, address spaces isolate the memory
accesses that a program can
make
-
Programs can only access their own memory, not that of other
processes or the OS
-
So if program X writes to "* 0xdeadabba" then program Y reads
from "* 0xdeadabba", X and Y really writing to different addresses
-
A nice feature to support for inter-process communication is
shared memory: Will be discussed later in the course
-
Also, if X reads/writes memory it doesn't have access to (e.g.
"null page") will get trapped by hardware and killed: segmentation fault:
core dumped !
• Supervisor/superuser bit
• How can programs access protected system services?
• Syscalls / interrupts
• How can a CPU run multiple programs?
-
Remember that only one program is really running at any
time
-
How do context switches work?
-
Registers, I/o descriptors, stack, PC, ...
-
CONTEXT SWITCH
-
OS does the following:
-
Save state of previous thread (Saves registers, I/O
descriptors, page table ptr, etc. in TCB)
-
Restores machine registers to state of new program (Sets up
stack pointer, etc.)
-
Sets up other state (I/O descriptors, etc.)
-
Sets up PAGE TABLES to map address space of new
program
-
Flushes TLB (QUESTION: Why? Answer: So that new
virtual->physical mappings are enforced)
-
Calls special instruction (sometimes) to "return from system
call": resume CPU from user program state at user's protection
level
-
Also: OS gets control whenever you do a system call, so you can
make scheduling decisions then
-
Also: Any time a hardware interupt occurs
-
QUESTION: So if I start running a program (i.e. Netscape), how
do I prevent it from spinning
on the CPU and just hanging the machine?
-
Note that Win 3.1 used to have this problem; programs
had to explicitly yield to
the OS
-
ANSWER: OS is interrupted by hardware timer
-
Define: PREEMPTION
-
Whenever timer goes off, OS gets control back from
program
-
Can decide to CONTEXT SWITCH to another program
• Yielding vs time-slice - CPU support for preemptive multitasking
Thread
Switch
-
Run thread
-
Choose next thread
-
Save state of current thread -> TCB
-
Restore state to new thread <- TCB
Stack View:
ComputePi()
yield()
kernel_yield() <-- kernel
space
run_new_thread()
switch()
Similar stack view for blocking on
IO
CopyFile()
read()
kernel_read() <-- kernel
space
run_new_thread()
switch()
And
Interrupt:
ComputePi()
TimerInterruptHandler() <-- kernel
space
run_new_thread()
switch()
• Little's formula: N = λ·F
N = average users in system
λ = arrival rate
F = mean flow time
(e.g. bandwidth-delay product in networks)
• Open/closed system
• Cooperating (to share resources and get speedup) vs independent processes
SCHEDULING
-
Metrics
-
Minimize response time
-
Maximize Throughput (minimize overhead, maximize efficiency)
-
Fairness (better average response time)
-
FCFS
-
Keep the CPU until thread blocks
-
Simple, but short jobs get stuck behind long ones (Convoy problem)
-
RR
-
Each thread runs for at most time quantum q, then preempted
-
q >> context-switch time, else too much overhead
-
Fair to short jobs (good response time)
-
SJF (Shortest Job First) (optimal if no preemption, unfair)
-
Shortest time to completion first
-
SRTF (Shortest Remaining Time First) (optimal, unfair)
-
Preemptive version of SJF
-
Adaptive SRTF
-
Estimate SRT based on previous bursts
-
Multi-Level Feedback Scheduling
-
Multiple queues, each with their own scheduling algorithm (e.g foreground
- RR, background FCFS)
-
Scheduling between queues: priority scheduling or time slice
-
Lottery Scheduling
-
At least one ticket assigned to each thread, more tickets to short running
jobs
-
Why at least one ticket? Prevent starvation
SCHEDULING SIMULATION!
• FCFS, RR (Q=2), SJF, SRPT (Q=2), SET (Q=2)
• Job, Arrival time, service time:
A 1 1
B 4 3
C 6 5
D 15 10
E 19 6
F 19 4
G 20 1
• Calculate: flow time, service time - mean, std dev
SYNCHRONIZATION
- Issues with threads and concurrency (things to think about)
- How to let threads interact safely? (SYNCHRONIZATION.)
- I.e. two threads each writing to the same shared
variable
- How to avoid having one thread overwrite the other's
value
(i.e. T1 does: local := shared; local
:= f(local); shared := local;
T2 does: local := shared;
local := g(local); shared := local;
RACE CONDITION: T1 writes new value
of 'shared' while T2 computing
g, so T2 writes 'shared' based on "old" value.)
- Critical Section, a piece of code that only one thread can execute at once.
Only one thread at a time will get into the section of code.
- Synchronization Requirements
- Mutual Exclusion, ensuring only one thread can execute in a critical
section at a time
- Progress, one thread must always be allowed to enter the critical
section
- Bounded waiting, a thread should not wait indefinitely to enter its
critical section
- Locks are used to implement mutual exclusion. All require some level of
hardware support.
- Atomic operations are a set of instructions which execute as one
uninterruptible unit. TestAndSet and Swap are atomic operations supported by
most hardwares.
/* returns the current value of lock and sets lock to true */
TestAndSet(lock)
TestAndSet = lock
lock = true
/* swaps two values
Swap(local(i), lock)
temp = lock;
lock = local(i)
local(i) = temp
Synchronization
---------------
• Critical section: abstract notion, lines of code
• Mutually exclusive lock
Atomicity
---------
• Single instructions
• Load, store (32 bits on 32-bit processor)
• Add, sub, xor, etc.
How to implement mutex lock?
----------------------------
• Need hardware support
• Atomic instructions: test&set, swap
class Lock {
acquire() { ... }
release() { ... }
}
• Naive implementation:
class Lock {
boolean flag = false;
acquire() { while (flag == false) /* twiddle */; flag = true; }
release() { flag = false; }
}
• What's wrong?
class Lock {
boolean flag = false;
acquire() {
start:
if flag == false: // needs to be atomic
goto start;
flag = true;
}
release() { flag = false; }
}
• Test&set:
class Lock {
boolean flag = false;
acquire() {
start:
if tset(flag) == false:
goto start;
}
release() { flag = false; }
}
class Lock {
boolean flag = false;
acquire() {
start:
while (tset(flag)) { /* twiddle */ }
}
release() { flag = false; }
}
• Works
• Busy waiting! => "spin lock"
• Disable interrupts
class Lock {
acquire() {
disableInterrupts();
}
release() {
enableInterrupts();
}
}
• How does this work?
• Problems?
• => only disableInterrupts for a SHORT time!
• Other threads should be allowed to run if using different lock
• Final solution?
• Use spinlock or interrupts-disabling to implement heavyweight but
non-busy-waiting lock, that uses queues
class Lock {
owner = null;
waitQ = {};
acquire() {
disableInterrupts();
if (owner != null) {
waitQ.add(currentThread());
KThread.sleep();
}
enableInterrupts();
}
release() {
disableInterrupts();
if (waitQ.notEmpty())
waitQ.pop().wake();
enableInterrupts();
}
}