Class 17 CS 202 31 March 2021 On the board ------------ 1. Last time 2. Context switches (WeensyOS) 3. Synchronous vs async I/O 4. User-level threading, intro 5. Context switches (user-level threading) -swtch() -yield() -I/O 6. Cooperative multithreading 7. Preemptive user-level multithreading --------------------------------------------------------------------------- 1. Last time lab 4 I/O (CPU/device) today, a mixture of topics 2. Context switches in WeensyOS - on interrupt, hardware saves "trapframe": %rip/%rsp/%eflags and lots of other things saves all of that on the *kernel's stack* at a well-known place in kernel memory - %rdi set equal to stack pointer - %rdi shows up, in C code, as the argument to exception() - exception does a brute force struct copy into process-specific memory in kernel space - now all of the process's registers just live in the PCB / struct proc. - kernel does its thing. - kernel gets ready to choose another process - remember, that process had the same thing happen - so all of *the new process's* registers are sitting in the same kind of memory mentioned above. - now, exception_return (&p->p_registers) - note: %rdi holds address of saved registers - set the stack pointer equal to that address - that means that popq will do the "right" thing - pop the saved registers into the CPU's - add 16 to skip past saved codes (error code if pg fault handler and int code in all cases). - now stack pointer is pointing to the trapframe at the time of the trap - that trapframe includes %rip and trap-time %rsp - iretq brings us back into user space with the %rip,%rsp at the time of the trap 3. Synchronous vs asynchronous I/O - A question of interface - NOTE: kernel never blocks when issuing I/O. We're discussing the interface presented to user-level processes. - Synchronous I/O: system calls block until they're handled. - Asynchronous I/O: I/O doesn't block. for example, if a call like read() or write() _would_ block, then it instead returns immediately but sets a flag indicating that it _would_ have blocked. Process discovers that data is ready either by making another query or by registering to be notified by a signal (discuss signals later) - Annoyingly, standard POSIX interface for files is blocking, always. Need to use platform-specific extensions to POSIX to get async I/O for files. (Although below, we will assume a non-blocking read(). This isn't a total abuse because read() can be set to be non-blocking, if the fd represents a device, pipe, or socket.) - Pros/cons: - blocking interface leads to more readable code, when considering the code that invokes that interface - but blocking interfaces BLOCK, which means that the code _above_ the interface cannot suddenly switch to doing something else. if we want concurrency, it has to be handled by a layer _underneath_ the blocking interface. - We'll see an example of this below. 4. User-level threading Setting: there's a _threading package_ --Review: what *is* a kernel-managed thread? (We refer to that as "kernel-level threading.") --basically same as a process, except two threads in the same process have the same value for %cr3 --recall: kernel threads are always preemptive --We can also have *user*-level threading, in which the kernel is completely ignorant of the existence of threading. [draw picture] T1 T2 T3 thr package OS H/W --in this case, the threading package is the layer of software that maintains the array of TCBs (thread control blocks) --threading package has other responsibilities as well: --make a new stack for each new thread. --scheduling! --user-level threading can be non-preemptive (cooperative) or preemptive. we'll look at both. --but first, revisit context switches, this time for user-space. 5. Context switches in user-space Note confusion: a "context switch" really is two things: - switching the view of memory (%cr3) - switching the registers The first one isn't relevant for user-level threading. Workhorse: swtch() switches registers [draw pictures; see handout] swtch() is called by yield() [see handout] yield() is called by any thread that couldn't make further progress. Good example of simultaneous use of synchronous and asynchronous interface. [see handout] (Kernels also need swtch(), to switch the *kernel* stack. [for those who want to learn more: here is an example in an instructional OS, see MIT's sv6 x86 version: https://pdos.csail.mit.edu/6.828/2018/xv6.html https://pdos.csail.mit.edu/6.828/2018/xv6/xv6-rev7.pdf RISC version: https://pdos.csail.mit.edu/6.828/2019/xv6.html search for uses of swtch(). ] WeensyOS doesn't use this mechanism because there is only a single kernel stack. ) 6. Cooperative multithreading --This is also called *non-preemptive multithreading*. --It means that a context switch takes place only at well-defined points: when the thread calls yield() and when the thread would block on I/O. 7. Preemptive multithreading in user-level How can we build a user-level threading package that does context switches at any time? Need to arrange for the package to get interrupted. How? Signals! Deliver a periodic timer interrupt or signal to a thread scheduler [setitimer() ]. When it gets its interrupt, swap out the thread, run another one Makes programming with user-level threads more complex -- all the complexity of programming with kernel-level threads, but few of the advantages (except perhaps performance from fewer system calls). in practice, systems aren't usually built this way, but sometimes it is what you want (for example, if you're simulating some OS-like thing inside a process, and you want to simulate the non-determinism that arises from hardware timer interrupts). A larger point: signals are instructive, and are used for many things. What a signal is really doing is abstracting a key hardware feature: interrupts. So this is another example of the fact that the OS's job is to give a user-space process the illusion that it's running on something like a machine, by creating abstractions. In this example, the abstraction is the signal, and the thing that it's abstracting is an interrupt.