So in case you are all worried that you may not
be in the right class, you are still in 6.033.
If you're wondering what happened
to the Indian guy who normally lectures,
he's sitting right over there.
We are trading off lectures throughout the class.
I'm going to do about the next eight lectures
up until spring break, and then Hari will be back.
So, my name is Sam, and feel free
to address any questions about the lecture to me.
So today we are going to keep talking
about this concept of enforcing modularity that we started
talking about last time.
So last time we saw how we could use this notion of the client
service model in order to separate
two modules from each other.
So the idea was by running the client and the service
on separate machines, we can isolate these two things
from each other.
So we can make it so, for example, when the client wants
to invoke some operation on the server,
that the server can verify the request that the client makes,
and make sure that the client isn't asking
to do something malicious.
Similarly, so this is what we saw last time was this client
service model.
And so the other benefit of the client service model
was it meant that we could decouple
the client from the service.
So it meant that when the client issued a request
against the service, it didn't necessarily
mean that the client, if the service failed when
the client issued the request, it wasn't necessarily
the case that the client would also fail.
So the server might crash executing the request,
and then the client would get a timeout
and would be able to retry.
And similarly, if the client failed,
the server could continue handling requests
on the behalf of other clients.
OK, so that was a nice separation, the ability
to split the client and the server apart from each other.
That's a good thing.
But the problem with this approach
is that we had to have two separate machines.
The way we presented this was that the client was running
on one computer, and the server was
running on another computer.
And if you think about this, this
isn't exactly what we want, right?
Because that means, suppose we want
to build up a big, complicated computer system that's composed
of multiple different modules.
If we have to run each one of those modules
on a separate computer, that's not really very ideal, right?
To build up a big service, we're going
to need a whole lot of computers.
And clearly, that's not the way that computer systems
that we actually interact with work.
So what we've seen so far is this notion:
we have a module per computer.
OK, and what we're going to do today
is see how we can generalize this
so that instead of having one module per computer,
we can instead have multiple modules running
within a single computer.
But when we do that, we still want
to maintain these nice sort of protection benefits
that we had between the client and service
when these two things were running on separate computers.
OK, so the way we are going to do
that is by creating something we call a virtual computer.
So -- What we're going to do is we're going to create this
notion of multiple virtual computers.
They're all running on one single computer.
And then we're going to run each one of these modules
within a virtual computer.
OK, and I'll talk more about what
I mean by a virtual computer throughout the next couple
of lectures.
But the idea is that a virtual computer has exactly the same,
to a program that's running on it,
a virtual computers looks just like one
of these client or server computers might have looked.
Now, in particular, a virtual computer
has the same sorts of abstractions
that we studied in the previous lectures available to it
that a real computer has.
So a virtual computer has a virtual memory --
-- and a virtual processor.
So, today what we're going to talk about
is this notion of a virtual memory.
And so you guys have already probably seen
the term virtual memory in 6.004.
So, the word should be familiar to you.
And we're going to use exactly the same abstraction
that we used in 6.004.
So we're just going to show how this abstraction can
be used to provide this protection between the client
and server that we want.
OK, we're also going to introduce today this notion
of something called a kernel.
And a kernel is something that's in charge, basically,
of managing all of these different virtual computers
that are running on our one physical computer.
So the kernel is going to be the sort of system that
is going to be the piece of the system that actually knows
that there are multiple, virtual computers running
on this system.
OK, so I want to start off by first just talking
about why we want a virtualized memory, why virtualizing
memory is a good thing.
So I'm going to abbreviate virtual memory as VM.
So why would we want to virtualize memory?
Well, let's see what might happen if we build a computer
system with multiple modules running on it
at the same time that didn't have a virtualized memory.
So suppose we have some microprocessor
and we have some memory.
And within this memory, we have the code
and data for a couple of different modules is stored.
So we have, say, module A and module B,
and this is the things like the code and the data
that these modules are using to currently execute.
So, in this environment, suppose the module, A,
executes some instruction.
So if you think of this as memory,
let's say that module A begins at address A here,
and module B begins at address B here.
So, suppose that module A executes
some instruction like store some value, R1, in memory address,
B.
OK, so module A writes into memory address B here.
So, this can be a problematic thing,
right, because if the microprocessor just
allows this to happen, if it just
allows A to write into module B's memory,
then module B has no way of being isolated
from module A at all, right?
Module A can, for example, write some sort of garbage
into the memory of module B.
And then when module B tries to read from that memory address,
it will either, say, try and execute an illegal instruction,
or it will read in some data that doesn't make any sense.
So we've lost the separation, the isolation
that we had between if module A and module B are a client
and a service trying to interact with each other.
We've lost this sort of ability.
We've lost the ability to separate and isolate
each other, isolate them from each other
that we had when they were running on separate computers.
Furthermore, this sort of arrangement of virtual memory.
So one problem with this is A can overwrite B's memory.
But we have another problem, which
is that this sort of arrangement also is very, very
difficult to debug.
So, these kinds of bugs can be very hard to track down.
And really, when we're building computer systems,
we'd like to get rid of them.
So, for example, suppose that A stores into B's memory,
overwrites an instruction that somewhere sort of arbitrarily
within B's memory.
And then, 10,000 instructions later,
B tries to load in that memory address
and execute that instruction.
And it gets an illegal instruction.
Now, imagine that you have to debug this program.
So you sort of are sitting there,
and so you run application B, and it works just fine.
And then you run application A, and A overwrites part
of B's memory.
And now, when program B runs sometimes
long after program A, perhaps, is already terminated,
B mysteriously crashes on you.
So how are you going to track down the problem?
It's very tricky, and it's the kind of problem
that's extremely hard to debug these kinds of issues.
So we really would like to make it
so that A isn't able to simply overwrite arbitrary
things into B's memory.
We'd like to protect A from B.
OK, and that's what this virtual memory abstraction
that we're going to talk about today is going to do for us.
So, if you think about how you might protect A from B,
the solution that you sort of come up
with after thinking about this and staring
at this for a little bit is that you
want to verify that all of the memory
accesses that A does are actually valid memory
accesses that A should be allowed to make, so
for example, that refer to objects that are actually
that A has allocated or that A owns.
OK, so in order to do that, what we're going to need to do
is to interpose some sort of software
in between the memory accesses that each module makes,
and the actual memory access.
So the idea is as follows.
So this is the virtual memory abstraction.
OK, so the idea is as follows.
For each one of our modules, say A and B,
we're going to create what's known as an address space, OK?
And this address space is going to be, for example,
on a 32 bit computer, it's two to the 32 minus one,
it's going to be a 32 bit memory space.
So, this is the virtual address space of, say, a process, A.
So, process A can use any memory address
that's in this two to the 32 bit range, OK?
But this is going to be a virtual address space.
And what I mean by that is when process A refers
to some address within its memory space,
call it virtual address VA.
That address is going to be mapped by the virtual memory
system into a physical address, OK?
So this virtual address here is going to be mapped into some
physical address --
-- somewhere within the actual physical memory of the system,
OK?
So this physical memory of the system is also a 32 bit space.
But the sort of mapping from this virtual address
in A's memory into this physical address space
is going to be handled by this virtual memory system.
So now the idea is that each thing that, say,
a process A or B might want to reference
is going to be mapped into some different location
within this memory.
So, for example, the code from process A or from module A
can be mapped into one location, and the code from module B
is going to be mapped into a different location
in the physical address space.
OK, so we have this notion of address spaces.
Each module that is running on the system in a virtual memory
system is going to be allocated to one of these address spaces.
This address space is going to be,
and this address space is virtual in the sense
that these addresses are only valid in the context
of this given module, OK?
And, they translate into, each address in the virtual space
translates into some address in the physical space.
Now, if you look at this for a minute,
you might think, well, this is kind
of confusing because now B and A both have a 32 bit address
space.
And the computer itself only has a 32 bit address space.
So, there are sort of more addresses
between all the modules than the computer itself physically has.
And, if you remember back from 6.004, the story
that we told you about that was that, well,
some of these virtual addresses can actually
be mapped into the disk on the computer.
So in 6.004, virtual memory was presented as a way
to create a computer that appeared
to have more physical memory than it, in fact, did.
And the way that that was done was
to map some of these addresses onto disk.
So you might have addresses from B mapped onto disk.
And then, when B tries to reference an address that's
mapped onto disk, the system would load that data
from memory, load that data from disk and into memory.
So, we're not going to talk about that aspect
of virtual memory very much today.
The thing to remember is just that these virtual address
spaces, there may be parts of this virtual address
space in each of the modules that
isn't actually mapped into any physical address space.
So if one of these modules tries to use
one of these unmapped virtual address spaces,
it's not allowed to do that.
This virtual memory system will signal an error
when it tries to do that.
And on your computer, sometimes you'll
see programs that will mysteriously
crashed with things that say things like, tried to access
an illegal memory address.
When it does that, that's because the program
tried to access some memory location that wasn't mapped
by the virtual memory system.
OK, so let's dive a little more into actually
how this virtual memory abstraction works
so we can try to understand a little bit more
about what's going on.
So this is going to be a simplified VM hardware, OK?
It's a little bit simplified even
from what you learned about in 6.004.
So, the idea in this simplified hardware
is that we have our processor.
OK, and then we're going to have this VM system, which
is sometimes called a memory management unit, MMU.
And, this is a piece of hardware that's
going to help us to do this mapping
from these logical addresses in the modules' address spaces
into the physical memory.
And then, we're going to have the physical memory, OK?
Now the idea is that when an instruction tries
to access some virtual address, so for example
suppose we execute instruction load
some virtual address, load into R1 some virtual address, OK?
What's going to happen when we do
that is that the microprocessor is
going to send this virtual address to the VM system.
And then the VM system is going to translate that
into some physical address that can be resolved
within the memory itself.
OK, and the way that the virtual memory system is going
to decide this mapping between a virtual address
and a physical address is by using something
that we call a page map, OK?
OK, so this table is an example.
So this is a page map.
And what a page map basically has
is its just a table of virtual address to physical address
mappings.
So this is the virtual address to physical address.
So the idea is that when some virtual address comes in here,
the virtual memory manager looks up
that virtual address in this page map,
finds the corresponding physical address,
and then looks that physical address up
in the actual memory.
OK, so now there's one more detail that we need, right?
So what this gives us is we have this notion of a page map
that does this mapping for us.
But we're missing a detail that is, OK,
what we wanted was for each one of these different modules
that's running in the system to have a different address
space associated with it.
So what we want is we want to have separate page
maps for each of these different modules, so, for A and B,
OK, we're going to have a different page map.
And we're going to have this sort of same,
we might have multiple copies of a particular virtual address
in each one of these page maps.
And then what we're going to do is
we're going to allocate a special register
on the hardware, on the processor.
We're going to add a little register that's
going to allow us to keep track of which one of these page maps
we are currently looking at, OK?
So this thing is called the PMAR or the Page Map Address
Register.
OK, and the page map address register
simply points at one of these page maps.
OK, so what happens is that the virtual memory system, when
it wants to resolve a virtual address,
looks at this page map address register
and uses that to find a pointer to the beginning of the page
map that's currently in use.
And then he uses the page map that's
currently in use to resolve, to get
what physical address corresponds
to this logical address.
OK so this is really the core concept from virtual memory.
So what we have now is we have this page map address register
that can be used to select which one of these address spaces
we are currently using.
OK, and so when we have selected, for example,
the page map for module A, then module A
can only refer to virtual addresses
that are in its page map.
And those virtual addresses can only
map into certain physical addresses.
So, for example, suppose this block here
is the set of virtual addresses, the set of physical addresses
that correspond to the virtual addresses in A's page map.
OK, these are the only physical addresses
that A can talk about.
So if we, for example, have a different block of memory
addresses that correspond to the virtual addresses
that B can reference, we can see that there's
no way for module A to be able to reference any of the memory
that B uses.
So we are able to totally separate
the physical memory, pieces of physical memory
that A and B can talk about by using this page map mechanism
that virtual memory gives us.
So, basically what we've done is we've
sort of added this extra layer of indirection,
this virtual memory system, that gets
to map virtual addresses into physical addresses.
So the rest of this lecture is really
going to be details about how we make this work, about how
we actually decide, how we assign this PMAR register based
on which one of the modules is currently executing
about things like what the format of this page map table
is going to look like, OK?
So this is really the key concept.
So what I want to do now is sort of turn
to this second question I asked which is,
how does this page map thing actually work?
How is it actually represented?
So one very simple representation of a page map
might be that it simply is a pointer to,
the page map just says where A's memory begins on the processor,
right?
So it's just one value.
It says A's memory begins at this location
in the physical memory, and all virtual addresses
should be resolved relative to this beginning
location of A's memory.
The problem with that representation
is that it's not very flexible.
So, for example, suppose there's a third module, C,
which is laid out in memory right next So,
its storage is placed right next to A in memory.
And now, suppose that A wants to allocate some additional memory
that it can use.
Now, in order to do that, if the page map is simply
a single pointer to the beginning of A's address space,
we're kind of in trouble.
We can't just add memory onto the bottom
because then we would overlap C.
So we're going to have to move all
of A's memory around in order to be able to make space for this.
OK, so this seems like a little bit problematic simply
have it be a pointer.
The other thing we could do is suppose we could, instead,
have a different option where we could say,
for example, for every virtual address in A's address space,
so for each 32 bit value that A wants to resolve,
there might be an entry in this page map table, right?
So for every 32-bit virtual address,
there would be a corresponding 32-bit physical address.
And there would just be a one-to-one mapping
between all these things.
So, if A could reference a million blocks of memory,
there would be a million entries in this page map table.
So, if you think about this for a minute, that
sounds like kind of a bad idea, too,
because now these tables are totally huge, right,
and in fact they are almost as big as the memory
itself, right, because if I have a million entries, if A can
reference a million blocks, then I'm
going to need a million entries in the table.
So the table becomes just as big as the memory itself.
So we need some in between sort of alternative hybrid
between these two extremes.
And the idea, again, is very simple.
And you saw it in 6.004.
So the idea is to take this 32-bit virtual address.
So suppose this is our 32-bit virtual address.
Now what we're going to do is we're
going to split it up into two pieces, a page
number and an offset.
OK, and we're going to choose some size for these two things.
For now I'll just arbitrarily pick a 20 bit page
number and a 12 bit offset.
OK, so what this is going to do, so now what we're going to do
is instead of storing a single word of memory
at each entry in this table, we're
going to store a page of memory at each entry in this table.
So -- So this table is going to look like a mapping between
a page, and a physical address.
OK, so what a page is, so if the page number is 20 bits long,
then that means that each page is
going to be two to the 12th bits big, which is equal to, say,
4,096 words, OK?
So the idea is that we're going to have two
to the 20th pages within each address space,
and each page is going to map to one of these 4,096 byte blocks,
OK?
So, if we have our memory here this page, say, page one
is going to map into some physical address.
And page two is going to map into some other physical block,
OK, so each one of these things is now 4,096 bytes, each block
here, OK?
And so this, let's just expand this.
So this is now a page.
And this 12 bit offset is going to be used in order
to look up the word that we want to actually look up
in this page.
So, if the virtual address is, say, for example,
page one offset zero, what that's going to do
is we're going to look up in the page map.
We're going to find the page number that corresponds to,
we're going to find page number one.
We're going to find the physical address that corresponds to it,
we're going to go down here, and look at this block of memory.
And then within this 4,096 block memory,
we're going to take the address zero, the first word
within that thing, OK?
So now the size of these page tables is much smaller, right?
They're no longer two to the 30th bits.
Now they're two to the 20th bits,
which is some small number of megabytes big.
But we've avoided this problem that we have before.
We have some flexibility in terms of how each page maps
into the physical memory.
So I can allocate a third page to a process.
And I can map that into any 4,096 byte
block that's in memory that's currently unused.
OK, so I have flexibility.
I don't have this problem, say, where A and C
were colliding with each other.
OK, so this is sort of the outline
of how virtual memory works.
But what I haven't yet described to you
is how it is that we can actually
go about creating these different address spaces that
are allocated to the different modules,
and how we can switch between different modules using
this PMAR register.
So I've sort of described this as,
suppose these data structures exist,
now here's how we can use them.
But I haven't told you how these data structures all
get together, and created, and set up to begin with.
And I hinted at this in the beginning.
But in order to do this, what we're going to need
is some sort of a special supervisory module that's
able to look at the page maps for all
of these different, that's able to create new page maps
and look at the page maps for all
of the different modules that are within the system.
And the supervisory module is going
to be able to do things like add new memory to a page map
or be able to destroy, delete a particular module
and its associated page map.
So we need some sort of a thing that can
manage all this infrastructure.
So, this supervisory module --
-- is called the kernel.
OK, and the kernel is really, it's
going to be the thing that's going
to be in charge of managing all these data structures for us.
So -- So here's our microprocessor with its PMAR
register on it.
And, what we're going to do is we're
going to extend the microprocessor with one
additional piece of hardware, and this is the user kernel
bit, OK?
So, this is just a bit specifies whether we are currently
running a user module that is just a program that's
running on your computer, or whether the kernel is currently
executing, OK?
And the idea with this kernel bit
is that when this kernel bit is set, the code that is running
is going to have special privileges.
It's going to be able to manipulate
special things about the hardware and the processor.
And in particular, we're going to have
a rule that says that only the kernel can change the PMAR, OK?
So the PMAR is the thing that specifies which process
is currently running, and selects which address space we
want to be currently using.
And what we're going to use is we're going to use,
so what we're going to do is have the kernel
be the thing that's in charge of manipulating
the value of this PMAR register to select which thing
is currently being executed.
And we want to make it so that only the kernel can do this
because if we, for example, allowed
one of these other programs to be able to manipulate the PMAR,
right, then that other program might
be able to do something unpleasant to the computer,
right?
It changes the PMAR to point at some other program's memory.
And now, suddenly all the memory addresses in the system
are going to be resolved.
Then suddenly, we are going to be
sort of resolving memory addresses relative
to some other module's page map.
And that's likely to be a problematic thing.
It's likely to cause that other module to crash, for example,
because the processor is set up to be executing instructions
from the current program.
So we want to make sure that only something this kernel
can change the PMAR.
And this kernel is going to be this sort of supervisory module
that all of the other modules are
going to have to trust to kind of do the right thing
and manage the computer's execution for you.
And this kernel, except for this one difference
that a kernel can change the PMAR,
the kernel is, in all other respects,
essentially just going to be another module that's
running in the computer system.
So in particular, the kernel is also
going to have one of these 32-bit virtual address
spaces associated with it, OK?
But, what we're going to do is we're
going to say that the kernel within its address space
has all of the page maps of all the other programs that
are currently running on the system mapped into its address
space.
OK, so this is a little bit tricky
because I presented this as though A and B referenced
totally different pieces of memory.
So, I sort of have told you so far only
about modules that are referencing
disjoint sets of memory.
But in fact these page maps, right, they just
reside in memory somewhere.
And it's just fine if I, for example,
have multiple modules that are able to reference, that
have the same physical addresses mapped
into their virtual address space.
So it's very likely that I might want
to have something down here at the bottom
that both A and B can access.
So, this might be stuff that's shared between A and B.
I can map that into both A and B's memory.
In the same way, what I'm going to do is I'm going to map
these page maps, which are also stored in memory
into all of the page maps into the kernel's address space.
So the kernel is going to be able to reference the address
spaces of all the modules.
And the kernel is also going to keep
a little table of all the page maps for all of the currently
running programs on the system.
So these are, for example, page maps for A and B.
And this is a list of all the maps
that are currently running in the system.
So what's going to happen, now what
can happen is because the kernel is allowed to change the PMAR,
and because it knows about the location of all
the other address spaces that are in the system, when it
wants to start running one of these programs that's running
in the system, it can change the value
of the PMAR to be sort of the PMAR for A or the PMAR to B.
And, it can manipulate all the values
of the registers in the system so
that you can start executing code for one of these.
You can switch between one of these two modules.
So the actual process of switching between which module
is currently running.
We're going to focus on that more next time.
So, don't worry too much if you don't understand
the details of how you actually switch from executing
one program to another program.
But, you can see that the kernel can
switch which address space is currently
active by simply manipulating the value
of this PMAR register.
Furthermore, the kernel can do things
like it can create a new map.
So the kernel can simply allocate
one of these new tables, and it can set up
a mapping from a set of virtual addresses
to a set of physical addresses so that you
can create a new address space that a new module can
start executing within.
Similarly, the kernel can do things like allocate new memory
into one of these addresses.
So it can map some additional virtual addresses
into real physical memory so that when one of these modules,
say for example, requests additional memory to execute,
the kernel can add that memory, add an entry into the table
so that that new memory that the program has requested actually
maps into a valid, physical address.
OK, so the question, of course, then is,
so you can see the value of having this kernel module.
But the question is, how do we communicate
between these modules that are running these user level
modules that are running, and the kernel module that the user
modules need to invoke, for example, request new memory.
So the way that we are going to do this
is just like we've done everything else in this lecture
so far: by adding a little bit of extra,
by changing the processor just a little bit.
So in particular, what we're going to do --
What we're going to do is to is to add this notion
of a supervisor call.
Call that SVC.
So a supervisor call is simply a special instruction
that is on the processor that invokes the kernel.
So when the supervisor call instruction gets executed,
it's going to set up the state of these PMAR and user kernel
bit instructions so that the kernel can begin executing.
The supervisor call is also going.
But when the supervisor call instruction is executed,
we need to be a little bit careful because we also
need to decide somehow which code within the kernel
we want to begin executing when the supervisor call instruction
gets executed, right?
So what the supervisor call instruction does
is it accepts a parameter which is the name of a so-called gate
function.
So a gate function is a well defined entry point
into another module that can be used to invoke a piece of code
in that other module.
So, for example, the kernel is going
to have a particular gate function which
corresponds to allocating additional memory for a module.
So when a user program executes an instruction, supervisor
call, to gate say for example, this
might be some memory allocation code,
this special instruction is going
to do the following things.
First it's going to set the user kernel bit to kernel.
Then it's going to set the value of the PMAR register
to the kernel page map.
It's going to save the program counter for the currently
executing instruction, and then it's
going to set the program counter to be the address of the gate
function, OK?
So we've introduced a special new processor instruction
that takes these steps.
So when this instruction is executed,
we essentially switch into executing within the kernel's
address space.
OK, and this kernel, and we begin
executing within the kernel address space
at the address of this gate function within the kernel's
address space.
OK, so what this has done is this
is a well defined entry point.
If the program tries to execute this instruction
with the name of a gate function that doesn't exist,
then that program is going to get an error
when it tries to do that.
So the program can only name gate functions
that actually correspond to real things
that the operating system can do, that the kernel can do.
And so, the kernel is then going to be invoked and take
that action that was requested of it.
On return, essentially, so when the kernel
finishes executing this process, when
it finishes executing this, say, memory allocation instruction,
we are just going to reverse this step.
So we are just going to set the PMAR
to be, we're going to set the PMAR back
to the user's program.
We are going to set the user kernel bit back into user mode.
And then we're going to jump back to the saved return
address that we saved here, the saved program
counter address from the user's program.
So when the program finishes, when the kernel finishes
executing this service instruction,
the control will just return back
to the program in the place where the program
needs to begin executing.
OK, so now using this gate, so using this notion of,
so what we've seen so far now is the ability for the,
we can use the virtual memory abstraction
in order to protect the memory references of two modules
from each other.
And we see when we have this notion of the kernel that
can be used to, for example, that
can be used to manage these address spaces to allocate
new memory to these address spaces, and in general,
to sort of manage these address spaces.
So this kernel is also going to allow
us to do exactly what we set out trying to do,
which is to act as the so-called trusted intermediary between,
say for example, a client and a server running
on the same machine.
OK, so I trust that intermediary is just a piece of code.
So suppose we have a client and a server, right,
and these are two pieces of code written
by two different developers.
Maybe the two developers don't necessarily
trust that the other developer has written
a piece of code that's 100% foolproof because it doesn't
have any bugs.
But both of those developers may be
willing to say that they will trust
that the kernel is properly written
and doesn't have any bugs in it.
And so they are willing to allow the kernel
to sit in between those two programs
and make sure that neither of them
has any bad interactions with each other.
So let's see how we can use this kernel, the kernel combined
with virtual memory in order to be able to do this.
So suppose this is our kernel.
So the idea is that this kernel running has,
so suppose we have two processes A and B
that want to communicate with each other in some way.
And, we already said that we don't want these two processes
to be able to directly reference into each other's memory
because that makes them dependent on each other.
It means that if there's a bug in A,
that A can overwrite some memory in B's address space
and cause B to crash.
So, that's a bad thing.
So instead, what we are going to do is we're going to create,
well, one thing we can do is to create
a special set of these supervisor calls
that these two modules can use to interact with each other.
So in particular, we might within the kernel
maintain a queue of messages that these two programs are
exchanging with each other, a list of messages
that they're exchanging.
And then, suppose A is, we're going to call this guy,
A is a producer.
He's creating messages.
And B is a consumer.
A can call some function like Put which will --
supervisor call like Put -- which will cause a data item
to be put on this queue.
And then sometime later, B can call this supervisor call Get,
and pull this value out of the queue, OK?
So in this way, the producer and the consumer
can interact with each other.
They can exchange data with each other
and because we have this gate interface,
this well-defined gate interface with these Put
and Get calls being invoked by the kernel,
the kernel can be very careful, just
like in the case of the client and server running
on two different machines.
In this case, the kernel can carefully verify that these put
and get commands that these two different modules are calling
actually are correctly formed, that the parameters are valid,
that they're referring to valid locations in memory.
And therefore, the kernel can sort of
ensure that these two modules don't
do malicious things to each other
or cause each other to break.
So this is an example here of something
that's like an inter-process communication.
So you saw, for example, you've seen
an instance of inter-process communication
when he studied the UNIX paper.
We talked about pipes, and a pipe abstraction,
and how that works.
Well, pipes are sort of something
that's implemented by the kernel in UNIX
as a way for two programs to exchange data with each other.
And, there's lots of these kinds of services
that our people tend to push into the kernel
that the kernel provides to the other applications, the modules
that are running, so that these modules can,
for example, interact with hardware or interact
with each other.
So commonly within a kernel, you'd
find things like a file system, an interface to the network,
and you might, for example, find things like an interface
to the graphics hardware.
OK, there is some sort of --
so if you look at what's actually within a kernel,
there is a huge amount of code that's
going into these kernels.
So I think we talked earlier about how the Linux operating
system is many millions of lines of code.
If you go look at the Linux kernel,
the Linux kernel is probably today
on the order of about 5 million lines of code, most of which,
say two thirds of which, is related
to these so-called device drivers that
manage this low-level hardware.
So this kernel has gotten quite large.
And one of the side effects of the kernel getting large
is that maybe it's harder to trust it, right?
May be you sort of have less confidence
that all the code in the kernel is actually correct.
And you can imagine that if you don't trust the kernel then
the computer is not going to be as stable as you like to be.
And this is one argument for why Windows crashes all the time is
because it has all these drivers in it
and these drivers aren't necessarily all
perfectly written.
There are tens of millions of lines of code in Windows,
and some of them crash some of the time,
and that causes the whole computer to come down.
So there's a tension in the operating systems community
about whether you should execute these things, you should keep
these things look the file system or graphics
system within the kernel or whether you should move them
outside as separate services, which can be invoked
in the same way, for example, that A and B interact
with each other, by having some data structure that's stored
within the kernel that buffers the requests between, say,
the service and this, say, for example the graphics service
and the users' programs that want to interact with it.
OK, so that's basically it for today.
What I've shown you is, well, actually we
have a few more minutes.
Sorry.
[LAUGHTER] I know you're all getting up to leave,
but so, OK, I just want to quickly touch
on one last topic which is that, so, what I've
shown you so far is how we can use
the notion of virtual memory in order to protect the data,
protect two programs from each other
so that they can't necessarily interact
with each other's data.
But there is some situations in which we might actually
want to have two programs able to share
some data with each other.
So I don't know if you guys remember,
but when Hari was lecturing earlier,
he talked about how there are libraries
that get linked into programs.
And one of the common ways that libraries
are structured these days is a so-called shared library.
So a shared library is something that
is only stored in one location in physical memory.
But multiple different modules that
are executing on that system can call functions
within that shared library.
So in order to make this work, right, we
need to have mapped the memory for the shared library that
has all the functions that these other modules want
to call into the address spaces for both of these modules
so that they can actually execute the code that's there.
So the virtual memory system makes it very trivial
to do this.
So suppose I have my address space for some function, A,
and I have my address space for some function, B.
And suppose that function A references library
one and module B references libraries one and two.
OK, so using the virtual memory system, suppose we have,
so this is our physical memory.
So, suppose that module A, program A, is loaded.
And when it's loaded, the program
that runs other programs, the loader in this case,
is going to load this shared library, one,
as it loads for program A.
So, it's going to first load the code for A,
and then it's going to load the code for one.
And, these things are going to be sitting in memory somewhere.
So, within A's address space we're
going to have the code for A is going to be mapped
and the code for one is going to be mapped.
Now, when B executes, right, what we want to avoid,
so the whole purpose of shared libraries
is to make it so that when two programs are running
and they both use the same library
there aren't two copies of that library that
are in memory using twice as much of the memory up,
right, because there's all these libraries that all computer
programs share.
For example, on Linux there's the libc library
that implements all of the standard functions
that people use in C programs.
If there's 50 programs written in C on your Linux machine,
you don't want to have 50 copies of this libc library in memory.
You want to have just one.
So what we want is that when module B gets loaded,
we want it to map this code that's already
been mapped into the address space of A into its memory
as well, OK?
And then, of course, we're going to have
to load additional memory for B itself, and for the library two
which A hasn't already loaded.
So now, those are going to be loaded
into some additional locations in B's memory,
are going to be loaded into some additional locations
in the physical memory and mapped into A's memory.
So, this is just showing that we can actually
use this notion of address spaces,
in addition to using it to isolate
two modules from each other so they
can't refer to each other's memory,
we can also use it as a way to allow two modules to share
things with each other.
And in particular, this is a good idea in the case of things
like shared libraries where we have two things,
both programs need to be able to read the same data.
So they could use this to do that.
OK, so what we saw today was this notion of virtual memory
and address spaces.
We saw how we have the kernel that's
a trusted intermediary between two applications.
What we're going to see next time
is how we can take this notion of virtualizing a computer.
We saw how to virtualize the memory today.
Next time we're going to see how we virtualize the processor
in order to create the abstraction
of multiple processors running on just one
single, physical piece of hardware.
So I'll see you tomorrow.