Class 15 CS 202 On the board ------------ 1. Last time 2. Context switches (WeensyOS) 3. User-level threading, intro 4. Context switches (user-level threading) -swtch() -yield() -I/O 5. Cooperative multithreading 6. Preemptive user-level multithreading 7. mmap() --------------------------------------------------------------------------- 1. Last time Context switch review. 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. 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. 4. 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. ) 5. 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. 6. 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. 7. mmap() Plays a role in lab5. Also, cool way to bring some ideas together. --recall some syscalls: fd = open(pathname, mode) write(fd, buf, sz) read(fd, buf, sz) --we've seen fds before, but what's an fd? --indexes into a table maintained by the kernel on behalf of the process --syscall: void* mmap(void* addr, size_t len, int prot, int flags, int fd, off_t offset); --means, roughly, "map the specified open file (fd) into a region of my virtual memory (close to addr, or at a kernel-selected place if addr is 0), and return a pointer to it" [see handout] NOTE: the "disk image" here is the file we've mmap()'ed, not the process's usual backing store. The idea is that mmap() lets the programmer "inject" pages from a regular file on disk into the process's backing store (which would otherwise be part of a swap file). --after this, loads and stores to addr[x] are equivalent to reading and writing to the file at offset+x. --why is this cool? - example: mmap enables copying a file to stdout without transferring data to user space see handout NOTE: the process never itself dereferences a pointer to memory containing file data. NOTE: this saves a memory-to-memory copy versus the "naive" solution of read()ing into a buffer in user space, and then write()ing. (the data goes right from buffer cache to terminal buffer, instead of buffer cache --> user space --> terminal buffer) [Also, a well-tuned buffer cache manages which file pages are kept in RAM, rather than leaving the app developer to have to explicitly try to manage that (and potentially have the OS page replacement algorithm underneath make conflicting decisions).] - other examples: - reading big files. map the whole thing, rely on the paging mechanism to bring the needed pieces into memory as necessary - shared data structures, when flag is MAP_SHARED - file-based data structures: - load data from file, update it, write it back - this is implemented entirely with loads/stores Question: how does the OS ensure that it's only writing back modified pages? --how's mmap implemented?! (answer: through virtual memory, with the VA being addr [or whatever the kernel selects] and the PA being what? answer: the physical address storing the given page in the kernel's buffer cache). --have to deal with eviction from buffer cache, so kernel will need a data structure that maps from: Phys page --> {list of (proc,va) pairs} note that the kernel needs this data structure anyway: when a page is evicted from RAM, the kernel needs to be able to invalidate the given virtual address in the page table(s) of the process(es) that have the page mapped.