1 00:00:01,120 --> 00:00:07,689 When a user-mode program wants to read a typed character it executes a ReadKey() SVC. 2 00:00:07,689 --> 00:00:14,370 The binary representation of the SVC has an illegal value in the opcode field, so the 3 00:00:14,370 --> 00:00:19,690 CPU hardware causes an exception, which starts executing the illegal opcode handler in the 4 00:00:19,690 --> 00:00:21,390 OS. 5 00:00:21,390 --> 00:00:27,220 The OS handler recognizes the illegal opcode value as being an SVC and uses the low-order 6 00:00:27,220 --> 00:00:33,420 bits of the SVC instruction to determine which sub-handler to call. 7 00:00:33,420 --> 00:00:38,219 Here's our first draft for the ReadKey sub-handler, this time written in C. 8 00:00:38,219 --> 00:00:43,440 The handler starts by looking at the process table entry for the current process to determine 9 00:00:43,440 --> 00:00:47,920 which keyboard buffer holds the characters for the process. 10 00:00:47,920 --> 00:00:52,399 Let's assume for the moment the buffer is *not* empty and skip to the last line, 11 00:00:52,399 --> 00:00:56,980 which reads the character from the buffer and uses it to replace the saved value for 12 00:00:56,980 --> 00:01:02,230 the user's R0 in the array holding the saved register values. 13 00:01:02,230 --> 00:01:08,080 When the handler exits, the OS will reload the saved registers and resume execution of 14 00:01:08,080 --> 00:01:13,690 the user-mode program with the just-read character in R0. 15 00:01:13,690 --> 00:01:17,680 Now let's figure what to do when the keyboard buffer is empty. 16 00:01:17,680 --> 00:01:22,530 The code shown here simply loops until the buffer is no longer empty. 17 00:01:22,530 --> 00:01:27,460 The theory is that eventually the user will type a character, causing an interrupt, 18 00:01:27,460 --> 00:01:31,890 which will run the keyboard interrupt handler discussed in the previous section, which will 19 00:01:31,890 --> 00:01:35,490 store a new character into the buffer. 20 00:01:35,490 --> 00:01:40,850 This all sounds good until we remember that the SVC handler is running with the supervisor 21 00:01:40,850 --> 00:01:46,200 bit (PC[31]) set to 1, disabling interrupts. 22 00:01:46,200 --> 00:01:47,729 Oops! 23 00:01:47,729 --> 00:01:53,090 Since the keyboard interrupt will never happen, the while loop shown here is actually an infinite 24 00:01:53,090 --> 00:01:54,130 loop. 25 00:01:54,130 --> 00:01:59,000 So if the user-mode program tries to read a character from an empty buffer, the system 26 00:01:59,000 --> 00:02:05,830 will appear to hang, not responding to any external inputs since interrupts are disabled. 27 00:02:05,830 --> 00:02:10,310 Time to reach for the power switch :) We'll fix the looping problem by adding code 28 00:02:10,310 --> 00:02:15,410 to subtract 4 from the saved value of the XP register before returning. 29 00:02:15,410 --> 00:02:17,930 How does this fix the problem? 30 00:02:17,930 --> 00:02:25,060 Recall that when the SVC illegal instruction exception happened, the CPU stored the PC+4 31 00:02:25,060 --> 00:02:29,439 value of the illegal instruction in the user's XP register. 32 00:02:29,439 --> 00:02:34,620 When the handler exits, the OS will resume execution of the user-mode program by reloading 33 00:02:34,620 --> 00:02:40,680 the registers and then executing a JMP(XP), which would normally then execute the instruction 34 00:02:40,680 --> 00:02:43,800 *following* the SVC instruction. 35 00:02:43,800 --> 00:02:49,790 By subtracting 4 from the saved XP value, it will be the SVC itself that gets re-executed. 36 00:02:49,790 --> 00:02:56,400 That, of course, means we'll go through the same set of steps again, repeating the cycle 37 00:02:56,400 --> 00:02:59,749 until the keyboard buffer is no longer empty. 38 00:02:59,749 --> 00:03:02,969 It's just a more complicated loop! 39 00:03:02,969 --> 00:03:08,519 But with a crucial difference: one of the instructions - the ReadKey() SVC - is executed 40 00:03:08,519 --> 00:03:12,019 in user-mode with PC[31] = 0. 41 00:03:12,019 --> 00:03:17,310 So during that cycle, if there's a pending interrupt from the keyboard, the device interrupt 42 00:03:17,310 --> 00:03:23,060 will supersede the execution of the ReadKey() and the keyboard buffer will be filled. 43 00:03:23,060 --> 00:03:29,060 When the keyboard interrupt handler finishes, the ReadKey() SVC will be executed again, 44 00:03:29,060 --> 00:03:33,060 this time finding that the buffer is no longer empty. 45 00:03:33,060 --> 00:03:34,650 Yah! 46 00:03:34,650 --> 00:03:39,709 So this version of the handler actually works, with one small caveat. 47 00:03:39,709 --> 00:03:45,579 If the buffer is empty, the user-mode program will continually re-execute the complicated 48 00:03:45,579 --> 00:03:50,510 user-mode/kernel-mode loop until the timer interrupt eventually transfers control to 49 00:03:50,510 --> 00:03:52,790 the next process. 50 00:03:52,790 --> 00:03:55,079 This seems pretty inefficient. 51 00:03:55,079 --> 00:03:59,599 Once we've checked and found the buffer empty, it would be better to give other processes 52 00:03:59,599 --> 00:04:02,810 a chance to run before we try again. 53 00:04:02,810 --> 00:04:04,459 This problem is easy to fix! 54 00:04:04,459 --> 00:04:10,150 We'll just add a call to Scheduler() right after arranging for the ReadKey() SVC to be 55 00:04:10,150 --> 00:04:12,169 re-executed. 56 00:04:12,169 --> 00:04:16,810 The call to Scheduler() suspends execution of the current process and arranges for the 57 00:04:16,810 --> 00:04:21,029 next process to run when the handler exits. 58 00:04:21,029 --> 00:04:25,569 Eventually the round-robin scheduling will come back to the current process and the ReadKey() 59 00:04:25,569 --> 00:04:28,440 SVC will try again. 60 00:04:28,440 --> 00:04:33,909 With this simple one-line fix the system will spend much less time wasting cycles checking 61 00:04:33,909 --> 00:04:40,770 the empty buffer and instead use those cycles to run other, hopefully more productive, processes. 62 00:04:40,770 --> 00:04:45,620 The cost is a small delay in restarting the program after a character is typed, 63 00:04:45,620 --> 00:04:51,180 but typically the time slices for each process are small enough that one round of process 64 00:04:51,180 --> 00:04:56,460 execution happens more quickly than the time between two typed characters, so the extra 65 00:04:56,460 --> 00:04:58,699 delay isn't noticeable. 66 00:04:58,699 --> 00:05:05,020 So now we have some insights into one of the traditional arguments against timesharing. 67 00:05:05,020 --> 00:05:07,180 The argument goes as follows. 68 00:05:07,180 --> 00:05:13,820 Suppose we have 10 processes, each of which takes 1 second to complete its computation. 69 00:05:13,820 --> 00:05:18,849 Without timesharing, the first process would be done after 1 second, the second after 2 70 00:05:18,849 --> 00:05:21,730 seconds, and so on. 71 00:05:21,730 --> 00:05:27,650 With timesharing using, say, a 1/10 second time slice, all the processes will complete 72 00:05:27,650 --> 00:05:31,870 sometime after 10 seconds since there's a little extra time needed for 73 00:05:31,870 --> 00:05:36,740 the hundred or so process switches that would happen before completion. 74 00:05:36,740 --> 00:05:42,699 So in a timesharing system the time-to-completion for *all* processes is as long the worst-case 75 00:05:42,699 --> 00:05:46,060 completion time without time sharing! 76 00:05:46,060 --> 00:05:48,770 So why bother with timesharing? 77 00:05:48,770 --> 00:05:51,980 We saw one answer to this question earlier in this slide. 78 00:05:51,980 --> 00:05:57,439 If a process can't make productive use of its time slice, it can donate those cycles 79 00:05:57,439 --> 00:06:00,169 to completion of some other task. 80 00:06:00,169 --> 00:06:05,380 So in a system where most processes are waiting for some sort of I/O, timesharing is actually 81 00:06:05,380 --> 00:06:09,090 a great way of spending cycles where they'll do the most good. 82 00:06:09,090 --> 00:06:15,490 If you open the Task Manager or Activity Monitor on the system you're using now, you'll see 83 00:06:15,490 --> 00:06:21,220 there are hundreds of processes, almost all of which are in some sort of I/O wait. 84 00:06:21,220 --> 00:06:26,590 So timesharing does extract a cost when running compute-intensive computations, but in an 85 00:06:26,590 --> 00:06:32,240 actual system where there's a mix of I/O and compute tasks, time sharing is the way to 86 00:06:32,240 --> 00:06:33,240 go. 87 00:06:33,240 --> 00:06:38,129 We can actually go one step further to ensure we don't run processes waiting for an I/O 88 00:06:38,129 --> 00:06:40,300 event that hasn't yet happened. 89 00:06:40,300 --> 00:06:46,800 We'll add a status field to the process state indicating whether the process is ACTIVE (e.g., 90 00:06:46,800 --> 00:06:51,599 status is 0) or WAITING (e.g., status is non-zero). 91 00:06:51,599 --> 00:06:57,729 We'll use different non-zero values to indicate what event the process is waiting for. 92 00:06:57,729 --> 00:07:01,900 Then we'll change the Scheduler() to only run ACTIVE processes. 93 00:07:01,900 --> 00:07:05,639 To see how this works, it's easiest to use a concrete example. 94 00:07:05,639 --> 00:07:12,940 The UNIX OS has two kernel subroutines, sleep() and wakeup(), both of which require a non-zero 95 00:07:12,940 --> 00:07:14,099 argument. 96 00:07:14,099 --> 00:07:18,169 The argument will be used as the value of the status field. 97 00:07:18,169 --> 00:07:19,940 Let's see this in action. 98 00:07:19,940 --> 00:07:25,250 When the ReadKey() SVC detects the buffer is empty, it calls sleep() with an argument 99 00:07:25,250 --> 00:07:30,469 that uniquely identifies the I/O event it's waiting for, in this case the arrival of a 100 00:07:30,469 --> 00:07:32,300 character in a particular buffer. 101 00:07:32,300 --> 00:07:37,879 sleep() sets the process status to this unique identifier, then calls Scheduler(). 102 00:07:37,879 --> 00:07:44,509 Scheduler() has been modified to skip over processes with a non-zero status, not giving 103 00:07:44,509 --> 00:07:46,090 them a chance to run. 104 00:07:46,090 --> 00:07:51,300 Meanwhile, a keyboard interrupt will cause the interrupt handler to add a character to 105 00:07:51,300 --> 00:07:57,600 the keyboard buffer and call wakeup() to signal any process waiting on that buffer. 106 00:07:57,600 --> 00:08:02,449 Watch what happens when the kbdnum in the interrupt handler matches the kbdnum in the 107 00:08:02,449 --> 00:08:04,470 ReadKey() handler. 108 00:08:04,470 --> 00:08:09,800 wakeup() loops through all processes, looking for ones that are waiting for this particular 109 00:08:09,800 --> 00:08:11,139 I/O event. 110 00:08:11,139 --> 00:08:17,690 When it finds one, it sets the status for the process to zero, marking it as ACTIVE. 111 00:08:17,690 --> 00:08:22,200 The zero status will cause the process to run again next time the Scheduler() reaches 112 00:08:22,200 --> 00:08:25,250 it in its round-robin search for things to do. 113 00:08:25,250 --> 00:08:31,340 The effect is that once a process goes to sleep() WAITING for an event, it's not considered 114 00:08:31,340 --> 00:08:37,760 for execution again until the event occurs and wakeup() marks the process as ACTIVE. 115 00:08:37,760 --> 00:08:39,380 Pretty neat! 116 00:08:39,380 --> 00:08:45,029 Another elegant fix to ensure that no CPU cycles are wasted on useless activity. 117 00:08:45,029 --> 00:08:50,540 I can remember how impressed I was when I first saw this many years ago in a (very) 118 00:08:50,540 --> 00:08:51,819 early version of the UNIX code :)