Supplementary lecture 2 cs4411 – Operating Systems Practicum September 30, 2011 Zhiyuan Teo

advertisement
cs4411 – Operating Systems Practicum
Supplementary lecture 2
September 30, 2011
Zhiyuan Teo
Today’s lecture
•Administrative Information
•Common mistakes on Project 1
•Project 2 FAQ
•Technical supplement 1
•Discussion
Administrative Information
•Project 1 has been graded – please review comments
and make amendments.
- Regrade requests: through CMS or in person.
•Project 2 deadline has been extended to 11.59pm
5th October.
Project 1: general
•We prize correctness, but also elegance.
•Don’t need provide as much abstraction as in data
structure courses.
•Comments and README.
•zip files and file content.
- don’t create an extra folder to hold the content.
- submit only changed source files, in addition to README.
- don’t tar and rezip.
- don’t encrypt!
Project 1: general
•Not checking for errors.
- malloc()
- queue_new(), queue_delete(), etc.
- asserts are not checks!
- should not exit unless there is no way for even the user to recover.
•Excessive debug output.
•Indentation and code readability.
Common mistakes: queues
•queue_append/prepend
- forgetting to set next or prev pointers.
•queue_delete/dequeue
- bugs when deleting from head/tail: pointers not updated or illegal access.
- not returning -1 when item to delete isn’t found in queue.
- not freeing up queue node after updating list.
- forgetting to update head/tail pointers.
•queue_iterate
- not checking the return value from function pointer.
- short-circuit evaluation.
Common mistakes: semaphores
•semaphore_create vs semaphore_initialize
- creating the blocked queue in semaphore_initialize.
•semaphore_V
- relying on a blind dequeue or queue length to unblock threads.
•semaphore_destroy
- forgetting to free up the blocked queue.
•TAS locks
- yielding instead of spinning.
- homemade TAS code.
- non-atomic clears.
Common mistakes: minithreads
•minithread_fork/create
- reimplementing create in fork (by cutting and pasting code).
•minithread_yield
- self-switching.
•minithread_stop
- beware of the semantics.
- comments for minithread_stop indicate that it is used for blocking threads.
- not to be used in place of minithread_yield, even if scheduling results are
correct.
Common mistakes: minithreads
•cleanup handler
- directly starting the cleanup thread through minithread_start.
- forcing the cleanup thread to run immediately (via context switch).
- forcing the next yield to run the cleanup thread.
•cleanup thread
- blindly calling dequeue before semaphore_P.
- freeing multiple items per semaphore_V called on it.
•global wait queue
- replicating functionality already achieved in semaphores.
- inefficient to delete.
Project 2 FAQ
•malloc and library calls
- all library calls are safe: interrupts are automatically disabled upon calling.
interrupts will be restored to its original state (enabled/disabled) after the call.
•units of time
- PERIOD is defined as 50 ms, which is 50000 as a constant.
- alarm and wakeup delays are specified in milliseconds.
- you have to convert units; don’t blindly subtract PERIOD.
•irregular/random clock interrupts
- this is normal.
- be careful of introducing heisenbugs because of your debug statements.
When to disable interrupts
•When you need to do something that must be done
atomically.
•Typically manipulations on shared data structures.
- data structures that can be accessed by multiple threads ‘simultaneously’.
- eg. modifying the cleanup queue, ready queue, alarm list.
•Trivial way of achieving correctness: disable interrupts
for everything.
-Why is this a bad idea?
Semaphores revisited
•typical sem_P code:
while (TAS(&lock) == 1);
sem->counter--;
if (sem->counter < 0)
{
append thread to blocked queue
atomically unlock and stop
}
else
{
atomic_clear(&lock);
}
Semaphores revisited
•typical sem_V code:
while (TAS(&lock) == 1);
sem->counter++;
if (sem->counter <= 0)
{
take one thread from blocked queue
start the thread
}
atomic_clear(&lock);
Semaphore design considerations
•Interrupts are not disabled in sem_P and sem_V.
•…but semantics must still be correct even if used in an
interrupt-enabled environment.
•Semaphores should also be safely usable from
places where interrupts are disabled.
•Why not just disable interrupts inside sem_P/V?
Semaphore use in user threads
•Interrupts can arrive at any time.
•If interrupts arrive while a TAS lock is held:
- another thread that tries to acquire the TAS lock will spin until its time quanta
is exhausted.
- thread holding the TAS lock will eventually regain control and make progress.
- progress ensures other threads can eventually get the TAS lock.
semaphore_P
semaphore_V
while (TAS(&lock) == 1);
while (TAS(&lock) == 1);
sem->counter--;
if (sem->counter < 0)
{
append thread to blocked queue
atomically unlock and stop
}
else
atomic_clear(&lock);
sem->counter++;
if (sem->counter <= 0)
{
take one thread from blocked queue
start the thread
}
atomic_clear(&lock);
Semaphore use in the kernel
•Typically used to block some thread and wake it up
on some condition.
- minithread_sleep_with_timeout()
- wake up the thread after the elapsed time.
•Sleep with timeout requires calling sem_P on the TCB’s
sleep semaphore. Done in user space.
•Waking up requires calling sem_V on that sleep
semaphore. Done in kernel space with interrupts
disabled.
struct tcb {
int tid;
…
semaphore_t sleep_sem;
};
An unfortunate interleaving
•What if user calls sleep_with_timeout(0) ?
- sem_P is called, and thread blocks itself.
•What if sem_P was interrupted just after placing
thread on blocked queue but before clearing TAS lock?
user calls sleep_with_timeout(0)…
…clock handler tries to wake that thread up
while (TAS(&lock) == 1);
while (TAS(&lock) == 1);
sem->counter--;
if (sem->counter < 0)
{
append thread to blocked queue
sem->counter++;but interrupts are disabled
if (sem->counter
<= clock
0) handler!
in the
{
take one thread from blocked queue
start the thread
}
clock interrupt!
atomically unlock and stop
}
else
atomic_clear(&lock);
atomic_clear(&lock);
Solution
•Disable interrupts for sem_P and sem_V.
- atomicity: sem_P will be done with everything before an interrupt can
possibly arrive.
- if interrupt arrives, acquisition of TAS lock is guaranteed in kernel space.
“You should disable interrupts first before making
calls to semaphore_P from within your kernel code.”
-project 2 slides
•What about sem_V?
- sem_V is called from interrupt handler.
- interrupts are already disabled in the handler.
When is this applicable?
•If semaphore will be used in portions of your kernel
where interrupts are disabled.
- right now: only the sleep semaphore.
•What about cleanup semaphore?
- Cleanup semaphore is not signaled from any place where interrupts are
disabled.
•Cleanup code should only disable interrupts while
accessing the cleanup queue, not for semaphore
signaling.
Why spin on TAS? Why not yield?
•Seems logical to yield if TAS lock cannot be acquired.
- behavior would still be correct in user threads.
semaphore_P
semaphore_V
while (TAS(&lock) == 1)
1);yield();
while (TAS(&lock) == 1)
1);yield();
sem->counter--;
if (sem->counter < 0)
{
append thread to blocked queue
atomically unlock and stop
}
else
atomic_clear(&lock);
sem->counter++;
if (sem->counter <= 0)
{
take one thread from blocked queue
start the thread
}
atomic_clear(&lock);
Why spin on TAS? Why not yield?
•But interrupts are enabled after the yield.
- not a problem in user threads, since they should run with interrupts enabled.
- presents problems in the kernel where you want interrupts to stay disabled
when using semaphores.
semaphore_P
semaphore_V
while (TAS(&lock) == 1) yield();
while (TAS(&lock) == 1) yield();
sem->counter--;
if (sem->counter < 0)
{
append thread to blocked queue
atomically unlock and stop
}
else
atomic_clear(&lock);
sem->counter++;
if (sem->counter <= 0)
{
take one thread from blocked queue
start the thread
}
atomic_clear(&lock);
No longer safe from interrupts;
interrupt handler can be interrupted.
How a problem may arise
•Suppose interrupts are not disabled before calling
sem_P/V in the kernel.
1. Suppose clock handler was invoked to service a clock
interrupt. It runs with interrupts disabled.
clock handler
void clock_handler() {
disable_interrupts();
while(sleeping_threads > 0) {
if (time_to_wake_thread)
sem_V(&tcb->sleep_semaphore);
}
restore_interrupt_level();
}
How a problem may arise
2. Clock handler is unable to acquire TAS lock, so it
yields.
semaphore_V()
while (TAS(&lock) == 1) yield();
sem->counter++;
if (sem->counter <= 0)
{
take one thread from blocked queue
start the thread
}
atomic_clear(&lock);
How a problem may arise
3. Control returns after yield. Interrupts are now enabled.
Suppose TAS lock succeeds (previous thread gave it up).
semaphore_V()
while (TAS(&lock) == 1) yield();
sem->counter++;
if (sem->counter <= 0)
{
take one thread from blocked queue
start the thread
}
atomic_clear(&lock);
How a problem may arise
4. A network interrupt arrives and calls sem_V because
it wants to wake up the thread.
semaphore_V()
network handler
while (TAS(&lock) == 1) yield();
void network_handler() {
network interrupt!
sem->counter++;
if (sem->counter <= 0)
{
take one thread from blocked queue
start the thread
}
atomic_clear(&lock);
disable_interrupts();
sem_V(&net_tcb->sleep_semaphore);
restore_interrupt_level();
}
How a problem may arise
5. Network handler yields because it cannot acquire TAS
lock.
No amount of yielding will allow progress in this scenario. Why?
Nested interrupts
0x7fffffff
Thread A
Clock interrupt handler
•An interrupt
•Network
interrupt
is like handler
an
is
unscheduled
waiting for clock
function
handler
call.to
finish, but this is impossible.
Network interrupt handler
•Only way to do this is for
network interrupt handler to
return.
•Yielding does not unwind the
call stack.
Program text, heap, etc.
0x00000000
Q & A for Project 2
Technical supplement roadmap
•How pointer casts work.
•How the stack works; function calls and context switches;
anatomy of buffer overflow & stack smashing exploits.
•x86 history and a (very short) introduction to x86
assembly.
•None of these are examinable nor strictly needed for
4410/4411 but are good to know as OS designers.
Pointer casting
•A pointer has two components:
- starting address of the data being pointed to.
- size of data type associated with this memory location.
int a = 5;
int *x = &a;
0x00001000
0
0
x = 0x00001004
*x = 0x00000005
pointer x: data starts from 0x00001004 and spans 4 bytes
0
0
0
0
0
0x00001004
5
0
0
0
0
0
0
0
0
Pointer casting
•Casting the pointer changes only the way the memory is
interpreted.
•It does not change the memory contents nor the address
stored in the pointer.
x
*x
y
*y
int
a = 5;
int *x = &a;
char *y;
y = (char*) x;
0x00001000
0
0
=
=
=
=
0x00001004
0x00000005
0x00001004
0x00
pointer y:
x: data starts from 0x00001004 and spans 41 bytes
byte
0
0
0
0
0
0x00001004
5
0
0
0
0
0
0
0
0
Pointer casting
•We can change the values from the casted pointer
if we like.
•No array bounds check in C!
x
*x
y
*y
int
a = 5;
int *x = &a;
char *y = (char*) x;
*y
= 1;
y[2]
= 3;
=
=
=
=
0x00001004
0x01000305
0x00001004
0x01
0x00001000
0
0
0
0
0
1
0
0
3
0x00001004
5
0
0
0
0
0
0
0
0
Structures
base struct
struct queueable {
struct queueable* next;
struct queueable* prev;
};
‘derived’ struct
struct tcb {
struct queueable reserved;
int tid;
semaphore_t sleep_sem;
};
struct queueable* queue_node;
struct tcb*
idle_tcb;
pointer queue_node: data starts from 0x00001000 and spans 8 bytes (struct is aligned)
0
0
0x00001000
next
0
0
0
0
prev
0
0
0
0
0
0
0
0
0
0
Structures
base struct
struct queueable {
struct queueable* next;
struct queueable* prev;
};
‘derived’ struct
struct tcb {
struct queueable reserved;
int tid;
semaphore_t sleep_sem;
};
struct queueable* queue_node;
struct tcb*
idle_tcb;
pointer idle_tcb: data starts from 0x00001000 and spans 16 bytes (struct is aligned)
0
0
0x00001000
0
0
0
reserved
0
0
0
0
0
0
tid
0
0
0
0
sleep_sem
0
Common questions on casting
•Casting from a derived struct pointer to the base struct
pointer is safe.
- any operation that works on data members of the base struct will not write
outside of the space allocated for the derived struct.
struct queue_node* node = (struct queue_node*) idle_tcb;
node->prev = 0xdead; node->next = 0xbeef;
pointer idle_tcb:
otherdata
fields
starts
in the
from
tcb 0x00001000
struct (tid and
and
sleep_semaphore)
spans 16 bytes (struct
are not
is aligned)
affected.
d
c
e
a
af
d
e
b
e
a
b
e
ef
0
b
0
a
0
a
0
d
0f
0
0
0
d
pointer queue_node: data starts from 0x00001000 and spans 8 bytes.
manipulation of next and prev fields in queue_node will only affect the first 8 bytes
of the tcb structure.
Common questions on casting
•What about casting from base struct to derived struct?
- unless you are very sure you will not touch data fields other than those
in the base struct.
- but if that is the case, why did you perform the cast?
struct tcb* some_tcb = (struct tcb*) queue_node;
some_tcb->reserved.next = 0xdead;
some_tcb->reserved.prev = 0xbeef;
some_tcb->tid = 1;
•Writing outside of known bounds may cause
unpredictable behavior.
- may overwrite another variable.
- may overwrite stack (and thus modify return address; a common exploit).
- usually just crashes.
void* pointers
•Review: a pointer has two components.
- starting address of the data being pointed to.
- size of data type associated with this memory location.
•void* pointers are not associated with any data type.
- has only a starting address.
- no known size, therefore not possible to tell where data ends in memory.
- malloc() is a good example.
•casting to void* from other pointer types.
- automatic, no need to prepend (void*).
- other pointers can easily cast to void*; just ignore the size of the data type.
•casting to other pointer types from void*.
- explicit cast required.
- destination data type supplies the size of the data type.
Q&A
Download