0% found this document useful (0 votes)
5 views26 pages

Understanding Threads in Operating Systems

Uploaded by

z01300585
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views26 pages

Understanding Threads in Operating Systems

Uploaded by

z01300585
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

CS350: Operating Systems

Lecture 3: Threads

Ali Mashtizadeh

University of Waterloo

1 / 26
Today: Threads

emacs

Process Threads Locks File I/O

Operating System

Hardware: CPU, Memory and Devices

2 / 26
Outline

1 Threads

2 Case Study: Go Language and Runtime

3 How to implement threads in OS/161

3 / 26
Threads

• A thread is a schedulable execution context


I Program counter, registers, stack (local variables) …
• Multi-threaded programs share the address space (global variables, heap, …)
4 / 26
Why threads?

• Most popular abstraction for concurrency


I Lighter-weight abstraction than processes
I All threads in one process share memory, file descriptors, etc.
• Allows one process to use multiple CPUs or cores
• Allows program to overlap I/O and computation
I Same benefit as OS running emacs & gcc simultaneously
I E.g., threaded web server services clients simultaneously:
for (;;) {
fd = accept_client ();
thread_create (service_client, &fd);
}
• Most kernels have threads, too
I Typically at least one kernel thread for every process
5 / 26
POSIX thread API

• int pthread_create (pthread_t *thr, pthread_attr_t *attr,


void *(*fn)(void *), void *arg);
I Create a new thread identified by thr with optional attributes, run fn with arg
• void pthread_exit(void *return_value);
I Destroy current thread and return a pointer
• int pthread_join(pthread_t thread, void **return_value);
I Wait for thread thread to exit and receive the return value
• void pthread_yield();
I Tell the OS scheduler to run another thread or process
• Plus lots of support for synchronization (next Lecture and see [Birell])

6 / 26
Kernel threads

• Can implement pthread_create as a system call


• To add pthread_create to an OS:
I Start with process abstraction in kernel
I pthread_create like process creation with features stripped out
. Keep same address space, file table, etc., in new process
. rfork/clone syscalls actually allow individual control

• Faster than a process, but still very heavy weight


7 / 26
Limitations of kernel-level threads

• Every thread operation must go through kernel


I create, exit, join, synchronize, or switch for any reason
I Syscall takes 100 cycles, function call 2 cycles
I Result: threads 10×–30× slower when implemented in kernel
I Worse today because of SPECTRE/Meltdown mitigations
• One-size fits all thread implementation
I Kernel threads must please all people
I Maybe pay for fancy features (priority, etc.) you don’t need
• General heavy-weight memory requirements
I E.g., requires a fixed-size stack within kernel
I Other data structures designed for heavier-weight processes

8 / 26
User threads

• An alternative: implement in user-level library


I One kernel thread per process
I pthread_create, pthread_exit, etc., just library functions

9 / 26
Implementing user-level threads

• Allocate a new stack for each pthread_create


• Keep a queue of runnable threads
• Replace blocking system calls (read/write/etc.)
I If operation would block, switch and run different thread
• Schedule periodic timer signal (setitimer)
I Switch to another thread on timer signals (preemption)
• Multi-threaded web server example
I Thread calls read to get data from remote web browser
I “Fake” read function makes read syscall in non-blocking mode
I No data? schedule another thread
I On timer or when idle check which connections have new data

10 / 26
Limitations of user-level threads

• Can’t take advantage of multiple CPUs or cores


• A blocking system call blocks all threads
I Can replace read to handle network connections
I But usually OSes don’t let you do this for disk
I So one uncached disk read blocks all threads
• A page fault blocks all threads
• Possible deadlock if one thread blocks on another
I May block entire process and make no progress
I [More on deadlock in future lectures.]

11 / 26
User threads on kernel threads

• User threads implemented on kernel threads


I Multiple kernel-level threads per process
I thread_create, thread_exit still library functions as before
• Sometimes called n : m threading
I Have n user threads per m kernel threads
(Simple user-level threads are n : 1, kernel threads 1 : 1)
12 / 26
Limitations of n : m threading
• Many of same problems as n : 1 threads
I Blocked threads, deadlock, …

• Hard to keep same # ktrheads as available CPUs


I Kernel knows how many CPUs available
I Kernel knows which kernel-level threads are blocked
I Tries to hide these things from applications for
transparency
I User-level thread scheduler might think a thread is
running while underlying kernel thread is blocked

• Kernel doesn’t know relative importance of threads


I Might preempt kthread in which library holds
important lock
13 / 26
Lessons

• Threads best implemented as a library


I But kernel threads not best interface on which to do this
• Better kernel interfaces have been suggested
I See Scheduler Activations [Anderson et al.]
I Maybe too complex to implement on existing OSes (some have added then
removed such features, now Windows is trying it)
• Today shouldn’t dissuade you from using threads
I Standard user or kernel threads are fine for most purposes
I Use kernel threads if I/O concurrency main goal
I Use n : m threads for highly concurrent (e.g,. scientific applications) with many
thread switches
• …though concurrency/synchronization lectures may
I Concurrency greatly increases the complexity of a program!
I Leads to all kinds of nasty race conditions
14 / 26
Outline

1 Threads

2 Case Study: Go Language and Runtime

3 How to implement threads in OS/161

15 / 26
Go Routines

• Go routines are very light-weight


I Running 100k go routines is practical
I Custom compiler enables stack segmentation, preemption, and garbage collection
I Runs on segmented stack – stack allocated on demand to avoid memory use
I OS thread typically allocate 2 MiB fixed stacks

• Go routines on top of Kernel threads (n:m Model)


I Multi-core scalability and efficient user-level threads
I One pthread (kernel-level thread) per CPU core
I Supports many user-level threads as you like

16 / 26
Go Routine Continued

• Each kernel-level thread finds and runs a go routine (user-level thread)

• Every logical core is owned by a kernel thread when running

• Convert blocking system calls (when possible):


I Converted to non-blocking by in the runtime yielding the CPU to another core
I Cores poll using kernel event API poll, epoll, or kqueue
• Blocking system calls:
I Release the ”CPU” to another kernel-level thread before the call
I Let the kernel thread sleep
I Regain the ”CPU” thread when done

17 / 26
Go Channels

• Go routine communicate and synchronize through channels


func worker(done chan bool) {
// Notify the main routine
done <- true
}

func main() {
// Create a channel to notify us
done := make(chan bool, 1)

// Create go routine
go worker(done)

// Block until we receive a message


<-done
}
18 / 26
Outline

1 Threads

2 Case Study: Go Language and Runtime

3 How to implement threads in OS/161

19 / 26
Background: MIPS calling conventions
• Registers divided into 2 groups
I Functions free to clobber caller-saved regs Call
(%t0–%t9 on MIPS) arguments
I But must restore callee-saved ones to original value
return addr
upon return (%s0–%s7, %fp)
old frame ptr
• %sp register always base of stack fp
callee-saved
I Frame pointer (%fp) is old %sp
registers
• Local variables stored in registers and on stack
Local vars
• Function arguments go in caller-saved regs and and temps
on stack sp
I First four arguments in %a0–%a3
I Remaining arguments on stack
• Return value %v0 and %v1
20 / 26
Background: procedure calls

• Some state saved on stack


I Return address, caller-saved registers
• Some state not saved
I Callee-saved regs, global variables, stack pointer
21 / 26
Threads vs. procedures

• Threads may resume out of order:


I Cannot use LIFO stack to save state
I General solution: one stack per thread
• Threads switch less often:
I Don’t partition registers (why?)
• Threads can be involuntarily interrupted:
I Synchronous: procedure call can use compiler to save state
I Asynchronous: thread switch code saves all registers
• More than one than one thread can run at a time:
I Procedure call scheduling obvious: Run called procedure
I Thread scheduling: What to run next and on which CPU?

22 / 26
OS/161 Kernel Threads

• OS/161 supports fork, exec, exit, and wait


I You will implement these functions in Assignments 2a/2b
• One thread per process
int thread_fork(const char *name,
struct proc *proc,
void (*entrypoint)(void *data1, unsigned long data2),
void *data1, unsigned long data2);
• OS/161 supports kernel threads (no user-level threading)
• Create a kernel thread with: thread_fork()
• Bad nameing: Not fork() this is actually pthread_create!

23 / 26
Switching Threads

• All thread switches go through thread_yield() and thread_switch()


• thread_switch() calls switchframe_switch generates switchframe
• switchframe_switch switches from one stack to other

General (from Kernel) Hardware Interrupt (typically Timer)

...

struct trapframe
...
trap()
thread_yield()
interrupt handler
thread_switch()
thread_yield()
switchframe
thread_switch()
...
struct switchframe

...

24 / 26
OS/161 switchframe_switch – save old thread

From OS/161 kern/arch/mips/thread/switch.S


1 switchframe_switch:
2 /* a0: switchframe pointer to old thread */
3 /* a1: switchframe pointer to new thread */
4 /* Allocate space for saving 10 registers. 10*4 = 40 */
5 addi sp, sp, -40
6
7 sw ra, 36(sp) /* Save callee save registers */
8 sw gp, 32(sp) /* Caller saved registers saved by thread_switch() */
9 sw s8, 28(sp)
10 sw s6, 24(sp)
11 sw s5, 20(sp)
12 sw s4, 16(sp)
13 sw s3, 12(sp)
14 sw s2, 8(sp)
15 sw s1, 4(sp)
16 sw s0, 0(sp)
17
18 /* Store the old stack pointer in the old thread */
19 sw sp, 0(a0) 25 / 26
OS/161 switchframe_switch – restore new thread

1 /* Get the new stack pointer from the new thread */


2 lw sp, 0(a1)
3 nop /* Delay slot for load */
4
5 lw s0, 0(sp) /* Now, restore callee saved registers */
6 lw s1, 4(sp) /* Caller saved registers restored by thread_switch() */
7 lw s2, 8(sp)
8 lw s3, 12(sp)
9 lw s4, 16(sp)
10 lw s5, 20(sp)
11 lw s6, 24(sp)
12 lw s8, 28(sp)
13 lw gp, 32(sp)
14 lw ra, 36(sp)
15 nop /* Delay slot for load */
16
17 j ra /* jump register to return address. */
18 addi sp, sp, 40 /* Fix sp in delay slot for j */

26 / 26

You might also like