WEBVTT

00:00:01.120 --> 00:00:07.689
When a user-mode program wants to read a typed
character it executes a ReadKey() SVC.

00:00:07.689 --> 00:00:14.370
The binary representation of the SVC has an
illegal value in the opcode field, so the

00:00:14.370 --> 00:00:19.690
CPU hardware causes an exception, which starts
executing the illegal opcode handler in the

00:00:19.690 --> 00:00:21.390
OS.

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

00:00:27.220 --> 00:00:33.420
bits of the SVC instruction to determine which
sub-handler to call.

00:00:33.420 --> 00:00:38.219
Here's our first draft for the ReadKey sub-handler,
this time written in C.

00:00:38.219 --> 00:00:43.440
The handler starts by looking at the process
table entry for the current process to determine

00:00:43.440 --> 00:00:47.920
which keyboard buffer holds the characters
for the process.

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,

00:00:52.399 --> 00:00:56.980
which reads the character from the buffer
and uses it to replace the saved value for

00:00:56.980 --> 00:01:02.230
the user's R0 in the array holding the saved
register values.

00:01:02.230 --> 00:01:08.080
When the handler exits, the OS will reload
the saved registers and resume execution of

00:01:08.080 --> 00:01:13.690
the user-mode program with the just-read character
in R0.

00:01:13.690 --> 00:01:17.680
Now let's figure what to do when the keyboard
buffer is empty.

00:01:17.680 --> 00:01:22.530
The code shown here simply loops until the
buffer is no longer empty.

00:01:22.530 --> 00:01:27.460
The theory is that eventually the user will
type a character, causing an interrupt,

00:01:27.460 --> 00:01:31.890
which will run the keyboard interrupt handler
discussed in the previous section, which will

00:01:31.890 --> 00:01:35.490
store a new character into the buffer.

00:01:35.490 --> 00:01:40.850
This all sounds good until we remember that
the SVC handler is running with the supervisor

00:01:40.850 --> 00:01:46.200
bit (PC[31]) set to 1, disabling interrupts.

00:01:46.200 --> 00:01:47.729
Oops!

00:01:47.729 --> 00:01:53.090
Since the keyboard interrupt will never happen,
the while loop shown here is actually an infinite

00:01:53.090 --> 00:01:54.130
loop.

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

00:01:59.000 --> 00:02:05.830
will appear to hang, not responding to any
external inputs since interrupts are disabled.

00:02:05.830 --> 00:02:10.310
Time to reach for the power switch :)
We'll fix the looping problem by adding code

00:02:10.310 --> 00:02:15.410
to subtract 4 from the saved value of the
XP register before returning.

00:02:15.410 --> 00:02:17.930
How does this fix the problem?

00:02:17.930 --> 00:02:25.060
Recall that when the SVC illegal instruction
exception happened, the CPU stored the PC+4

00:02:25.060 --> 00:02:29.439
value of the illegal instruction in the user's
XP register.

00:02:29.439 --> 00:02:34.620
When the handler exits, the OS will resume
execution of the user-mode program by reloading

00:02:34.620 --> 00:02:40.680
the registers and then executing a JMP(XP),
which would normally then execute the instruction

00:02:40.680 --> 00:02:43.800
*following* the SVC instruction.

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.

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

00:02:56.400 --> 00:02:59.749
until the keyboard buffer is no longer empty.

00:02:59.749 --> 00:03:02.969
It's just a more complicated loop!

00:03:02.969 --> 00:03:08.519
But with a crucial difference: one of the
instructions - the ReadKey() SVC - is executed

00:03:08.519 --> 00:03:12.019
in user-mode with PC[31] = 0.

00:03:12.019 --> 00:03:17.310
So during that cycle, if there's a pending
interrupt from the keyboard, the device interrupt

00:03:17.310 --> 00:03:23.060
will supersede the execution of the ReadKey()
and the keyboard buffer will be filled.

00:03:23.060 --> 00:03:29.060
When the keyboard interrupt handler finishes,
the ReadKey() SVC will be executed again,

00:03:29.060 --> 00:03:33.060
this time finding that the buffer is no longer
empty.

00:03:33.060 --> 00:03:34.650
Yah!

00:03:34.650 --> 00:03:39.709
So this version of the handler actually works,
with one small caveat.

00:03:39.709 --> 00:03:45.579
If the buffer is empty, the user-mode program
will continually re-execute the complicated

00:03:45.579 --> 00:03:50.510
user-mode/kernel-mode loop until the timer
interrupt eventually transfers control to

00:03:50.510 --> 00:03:52.790
the next process.

00:03:52.790 --> 00:03:55.079
This seems pretty inefficient.

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

00:03:59.599 --> 00:04:02.810
a chance to run before we try again.

00:04:02.810 --> 00:04:04.459
This problem is easy to fix!

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

00:04:10.150 --> 00:04:12.169
re-executed.

00:04:12.169 --> 00:04:16.810
The call to Scheduler() suspends execution
of the current process and arranges for the

00:04:16.810 --> 00:04:21.029
next process to run when the handler exits.

00:04:21.029 --> 00:04:25.569
Eventually the round-robin scheduling will
come back to the current process and the ReadKey()

00:04:25.569 --> 00:04:28.440
SVC will try again.

00:04:28.440 --> 00:04:33.909
With this simple one-line fix the system will
spend much less time wasting cycles checking

00:04:33.909 --> 00:04:40.770
the empty buffer and instead use those cycles
to run other, hopefully more productive, processes.

00:04:40.770 --> 00:04:45.620
The cost is a small delay in restarting the
program after a character is typed,

00:04:45.620 --> 00:04:51.180
but typically the time slices for each process
are small enough that one round of process

00:04:51.180 --> 00:04:56.460
execution happens more quickly than the time
between two typed characters, so the extra

00:04:56.460 --> 00:04:58.699
delay isn't noticeable.

00:04:58.699 --> 00:05:05.020
So now we have some insights into one of the
traditional arguments against timesharing.

00:05:05.020 --> 00:05:07.180
The argument goes as follows.

00:05:07.180 --> 00:05:13.820
Suppose we have 10 processes, each of which
takes 1 second to complete its computation.

00:05:13.820 --> 00:05:18.849
Without timesharing, the first process would
be done after 1 second, the second after 2

00:05:18.849 --> 00:05:21.730
seconds, and so on.

00:05:21.730 --> 00:05:27.650
With timesharing using, say, a 1/10 second
time slice, all the processes will complete

00:05:27.650 --> 00:05:31.870
sometime after 10 seconds
since there's a little extra time needed for

00:05:31.870 --> 00:05:36.740
the hundred or so process switches that would
happen before completion.

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

00:05:42.699 --> 00:05:46.060
completion time without time sharing!

00:05:46.060 --> 00:05:48.770
So why bother with timesharing?

00:05:48.770 --> 00:05:51.980
We saw one answer to this question earlier
in this slide.

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

00:05:57.439 --> 00:06:00.169
to completion of some other task.

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

00:06:05.380 --> 00:06:09.090
a great way of spending cycles where they'll
do the most good.

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

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.

00:06:21.220 --> 00:06:26.590
So timesharing does extract a cost when running
compute-intensive computations, but in an

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

00:06:32.240 --> 00:06:33.240
go.

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

00:06:38.129 --> 00:06:40.300
event that hasn't yet happened.

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.,

00:06:46.800 --> 00:06:51.599
status is 0) or WAITING (e.g., status is non-zero).

00:06:51.599 --> 00:06:57.729
We'll use different non-zero values to indicate
what event the process is waiting for.

00:06:57.729 --> 00:07:01.900
Then we'll change the Scheduler() to only
run ACTIVE processes.

00:07:01.900 --> 00:07:05.639
To see how this works, it's easiest to use
a concrete example.

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

00:07:12.940 --> 00:07:14.099
argument.

00:07:14.099 --> 00:07:18.169
The argument will be used as the value of
the status field.

00:07:18.169 --> 00:07:19.940
Let's see this in action.

00:07:19.940 --> 00:07:25.250
When the ReadKey() SVC detects the buffer
is empty, it calls sleep() with an argument

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

00:07:30.469 --> 00:07:32.300
character in a particular buffer.

00:07:32.300 --> 00:07:37.879
sleep() sets the process status to this unique
identifier, then calls Scheduler().

00:07:37.879 --> 00:07:44.509
Scheduler() has been modified to skip over
processes with a non-zero status, not giving

00:07:44.509 --> 00:07:46.090
them a chance to run.

00:07:46.090 --> 00:07:51.300
Meanwhile, a keyboard interrupt will cause
the interrupt handler to add a character to

00:07:51.300 --> 00:07:57.600
the keyboard buffer and call wakeup() to signal
any process waiting on that buffer.

00:07:57.600 --> 00:08:02.449
Watch what happens when the kbdnum in the
interrupt handler matches the kbdnum in the

00:08:02.449 --> 00:08:04.470
ReadKey() handler.

00:08:04.470 --> 00:08:09.800
wakeup() loops through all processes, looking
for ones that are waiting for this particular

00:08:09.800 --> 00:08:11.139
I/O event.

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.

00:08:17.690 --> 00:08:22.200
The zero status will cause the process to
run again next time the Scheduler() reaches

00:08:22.200 --> 00:08:25.250
it in its round-robin search for things to
do.

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

00:08:31.340 --> 00:08:37.760
for execution again until the event occurs
and wakeup() marks the process as ACTIVE.

00:08:37.760 --> 00:08:39.380
Pretty neat!

00:08:39.380 --> 00:08:45.029
Another elegant fix to ensure that no CPU
cycles are wasted on useless activity.

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)

00:08:50.540 --> 00:08:51.819
early version of the UNIX code :)