6.824 2005 Lecture 4: Event-Driven Programming Remember we were trying to get I/O concurrency for servers Overlap CPU, disk, network activity Serve many clients at once One plan: multiple server processes One per active client [draw it] One process may "block" reading, O/S switches to another process Problems: space (stacks), switching overhead, no sharing Flash paper talks about these problems A better plan: event-driven server One process, so low overhead and one address space Starts I/O w/o waiting Receives I/O completion notifications from O/S Starts the next I/O, &c Example loop: while(1){ read next event; if connection arrives: create client state, start read if req arrives: start open() if open() done: start file read() if file read done: start net write() if net write done: start file read() Ideal O/S interface: All system calls should return immediately All calls should indicate "done" w/ events Unified "wait for next event" So process can wait for any of many potential operations Why are non-blocking system calls hard in general? Typical system call implementation, inside the kernel: [Example 1] Can we just return to user program instead of wait_for_disk? No: how will kernel know where to continue? Big problem: keeping state for multi-step operations. Lesson: not easy to make most system calls non-blocking and continuable. Options: New operating system with totally different syscall interface. One system call per non-blocking sub-operation. So kernel doesn't need to keep state across multiple steps. Flash/AMPED helper processes Microkernel: no system calls, only messages to servers. and non-blocking communication Really a generalization of Flash/AMPED What does UNIX provide? Non-blocking operations for communication (sockets &c) select() to wait for event on any communication fd *No* UNIX non-blocking disk operations! open(), read(), stat() &c will block if data not in cache Due to relative speed of disk, CPU, network in 1980s Why non-blocking I/O is easy for communication. Socket reads are essentially atomic -- one step. Data has already arrived, or not; read() doesn't have to pull data. [Example 2] Don't need to block before decision about whether data is waiting. I.e. nothing like asking the disk to start a read. So select() can tell ahead of time if data is waiting. This is true for all communcation-like read()s: network sockets, pipes, terminals, mouse select() [write prototype on the board: nfds, reads, writes, excepts, time] Condition is actually "read() would not block"; might be EOF. select() blocks (if timeout > 0) to avoid wasteful polling. this is important; you *do* want to block in select(). **************************** Remember the event loop. What if your program does many things? Hard to be modular if event loop knows about all activities. And knows how to consult all state. Solution: event-driven programming library. Library provides main loop. Modules can register "callbacks" to handle certain events [Example 3] How can we write programs in the event driven style? Suppose we're starting with this code [Example 4] wait_rpc_reply() is not good, can't block, must return to top level. which means get() *and* fn() must return before done. How will fn() remember key? How will fn() indicate what to do next? wrap(fn, a, b) generates a closure. That is, a function pointer plus an environment of saved values. cb() calls fn(a, b) cb(x) calls fn(a, b, x) Now we can turn Example 4 into event code: [Example 5] The wrap()s are continuations! How does wrap work internally? And what is the callback data type? Why does libasync supply reference counted garbage collection? str, for example, and callback<>::ref In Example 3, x is saved on stack across call to f2() Space for those values is freed automatically on function return. In Example 5, f1() returns but f1_cb() will still need x. wrap() stores values in a closure, uses when you call the callback. How do we know when we've used a callback the last time? That's why they're reference counted.