“how does an operating system track threads and processes”
traps and interrups
Bad problem: the operating system can’t be running when a user thread is running. We can’t do thread bookeeping if a user thread is running.
trap
a trap is a scheme to request OS attention explicitly from the user thread, swapping the user process off the CPU.
- system calls
- errors
- page fault (memory errors)
interrupt
a interrupt takes place outside the current thread, it forces the OS’ attention even if the user thread isn’t asking for it
- character typed at keyboard
- completion of a disk operations
- a hardware timer that fires an interrupt
interrupts enable preemption to happen, so see also preemption for interrupt handling pattern.
what if a timer goes off during an interrupt
interrupts are disabled during interrupt handling, otherwise, this causes an infinite loop.
solution: interrupts are disabled during timer handling.
this causes a problem: if you preempt into a brand new thread
main idea
- there are race condition we cannot solve with mutexes because we are the OS
- so, we implement mutexes by enabling/disabling interrupts
dispatcher
a dispatcher performs a context switch, which
context switch
- (in asm) push all registers except
%rsp
into the bottom of the old thread’s stack - store the stack pointer
%rsp
into the process control block for that process corresponding to thread - read the new thread’s stack pointer from the process control block, and load that into
%rsp
- (in asm) pop all registers stored on the bottom of our new stack back onto the registers
remember to push and pop the registers in the same order…. otherwise the registers won’t be in the right order.
this makes a context switch a function that calls on one thread and returns on another thread—“we start executing from one stack, and end executing from another”.
Example:
context switch
Notice that we only store callee saved registers because its the responsibility of whomever called context switch to save the register of the caller saved registers.
pushq %rbp
pushq %rbx
pushq %r14
pushq %r15
;; pushq all of em callee saved ...
movq %rsp, [somewhere in PCB, thread 1] ; the process control block
movq [somewhere else in PCB, thread 2], %rsp ; the stack is now somewhere else
;; now we pop backwards up from the stack
;; popq all of em calee saved ...
popq %r15
popq %r14
popq %rbx
popq %rbp
;; this will RETURN to the last call *or* top of context_switch() of the
;; **THREAD 2**, because we moved the stack pointer by movq into
;; %rsp, we will return to the NEW thread's last executed position
ret
what if the thread is new?
We can’t ret
to a function that never called context_switch
, which is the case for new threads.
To do this, we create a fake freeze frame on the stack for that new thread which looks like you are just about to call the thread function, and calls context_switch
normally.
yield
yield is a user function that one could implement, which acts like a blocking action, but instead of doing that we just add ourselves directly to the end of the ready queue again. (i.e. give up CPU voluntarily, but don’t block0.