This web page shows color, animated versions of the memory maps in the 0019 CW4 handout, along with their accompanying text.
Once you've cloned your repository from GitHub to your working
environment, you can build the initial version of WeensyOS we've given
you by issuing the shell command make run
in your CW4 directory.
You should see something like the below, which shows four processes
running in parallel, each running a version of the program in
p-allocator
:
The animated image above loops forever; in an actual run, the bars will move to the right and stay there. Don't worry if your image has different numbers of K's or otherwise has different details.
If your bars run painfully slowly, edit the p-allocator.c
source file and reduce the ALLOC_SLOWDOWN
constant.
Stop now to read and understand p-allocator.c
.
Here’s how to interpret the memory map display:
p-allocator.c
), but
linked to use a different region of memory.
Here are two labeled memory maps showing what the characters mean and how memory is arranged.
The virtual memory display is similar:
We describe below the five implementation stages you must complete in CW4: what you need to implement in each, and hints on how to do so.
In the starting code we've given you, WeensyOS processes could stomp
all over the kernel's memory if they wanted to. Better prevent
that. Change kernel()
, the kernel initialization
function, so that kernel memory is inaccessible to applications,
except for the memory holding the CGA console (the single page at
(uintptr_t) console == 0xB8000
.) Making the
console accessible in this way, by making the range of RAM where the
contents of the display are held directly accessible to applications,
is a throwback to the days of DOS, whose applications typically
generated console output in precisely this way. DOS couldn't run more
than one application at once, so there wasn't any risk of multiple
concurrent applications clobbering one another's display writes to the
same screen locations. We borrow this primitive console design to keep
WeensyOS simple and compact.
When you are done, WeensyOS should look like the below. In the virtual map, kernel memory is no longer reverse-video, since the user can't access it. Note the lonely CGA console memory block.
Hints:
virtual_memory_map()
. A description of this function is in
kernel.h
. You will benefit from reading all the function
descriptions in kernel.h
. You can supply NULL
for
the allocator
argument for now.
virtual_memory_map()
, it is
in k-hardware.c
, along with many other hardware-related
functions.
perm
argument to virtual_memory_map()
is a
bitwise-or of zero or more
PTE
flags: PTE_P
,
PTE_W
, and PTE_U
.
PTE_P
marks Present pages (pages
that are mapped). PTE_W
marks
Writable pages. PTE_U
marks
User-accessible pages--pages accessible by
applications. You want kernel memory to be mapped with permissions
PTE_P | PTE_W
, which will
prevent applications from reading or writing the memory, while
allowing the kernel to both read and write.
sys_page_alloc()
system call
preserves kernel isolation: Applications shouldn’t be able to use
sys_page_alloc()
to screw up
the kernel.
Implement process isolation by giving each process its own independent page table. Your OS memory map should look like this when you're done:
That is, each process only has permission to access its own pages. You can tell this because only its own pages are shown in reverse video.
What goes in per-process page tables:PROC_START_ADDR
should be
copied from those in kernel_pagetable
. You
can use a loop with virtual_memory_lookup()
and
virtual_memory_map()
to copy them. Alternately, you can
copy the mappings from the kernel's page table into the new page
tables; this is faster, but make sure you copy the right data!
PROC_START_ADDR
--should be
inaccessible to user processes (i.e., PTE_U
should not be set for these PTEs). In our solution (shown above),
these addresses are totally inaccessible (so they show as
blank), but you can also change this so that the mappings are still
there, but accessible only to the kernel, as in this diagram:
The reverse video shows that this OS also implements process isolation correctly.
How to implement per-process page tables:
process_setup()
to create
per-process page tables.
copy_pagetable(x86_64_pagetable
*pagetable, int8_t owner)
function that allocates
and returns a new page table, initialized as a full copy of
pagetable
(including all mappings from
pagetable
). This function will be useful in Stage 5. In
process_setup()
you can modify the page table
returned by copy_pagetable()
according to the
requirements above. Your function can use pageinfo[]
to find
free pages to use for page tables. Read about pageinfo[]
at the top of kernel.c
.
allocator
function suitable for passing to
virtual_memory_map()
.
0...MEMSIZE_VIRTUAL - 1
)
yourself; then you don’t need an allocator function.
pageinfo[PAGENUMBER].refcount == 0
. Look at the other code in kernel.c
for
some hints on how to examine the pageinfo[]
array.
P
's page table pages must have
pageinfo[...].owner == P
or WeensyOS's consistency-checking functions will
fail. This will affect your allocator function. (Hint: Don't forget
that global variables are allowed in your code!)
If you create an incorrect page table, WeensyOS might crazily
reboot. Don't panic! Add log_printf()
statements. Another
useful technique that may at first seem counterintuitive: add
infinite loops to your kernel to track down exactly where a fault
occurs. (If the OS hangs without crashing once you've added an
infinite loop, then the crash you're debugging must occur at a point
in the kernel's execution after your infinite loop's place in the
code.)
Thus far in CW4, WeensyOS processes have used physical page
allocation: the page with physical address X is used to
satisfy the sys_page_alloc(X)
allocation request for virtual address X. This strategy is
inflexible and limits utilization. Change the implementation of the
INT_SYS_PAGE_ALLOC
system call so that it can use any free physical
page to satisfy a sys_page_alloc(X)
request.
Your new INT_SYS_PAGE_ALLOC
code must perform the following tasks:
pageinfo[]
array. Return -1
to the application if you can't find
one. Use any algorithm you'd like to find a free physical page; in
our model solution, we just return the first one we find.
pageinfo[]
.
Don't modify the physical_page_alloc()
helper function,
which is also used by the program loader. You can write a new function
if you need to.
Here's how our OS looks after this stage:
Now the processes are isolated, which is excellent. But they're still not taking full advantage of virtual memory. Isolated address spaces can use the same virtual addresses for different physical memory. There's no need to keep the four processes' address spaces disjoint.
In this stage, change each process's stack to start from address
0x300000 == MEMSIZE_VIRTUAL
. Now the processes have enough
heap room to use up all of physical memory! Here's how the memory map
will look after you've done it successfully:
If there's no physical memory available, sys_page_alloc()
should return an error to the caller
(by returning -1). Our model solution additionally prints
"Out of physical memory!
" to the console when this happens;
you don't need to.
The fork()
system call is one of Unix's great ideas. It
starts a new process as a "copy" of an existing one. The
fork()
system call appears to return twice, once to each
process. To the child process, it returns 0. To the parent process, it
returns the child's process ID.
Run WeensyOS with make run
or make run-console
. At
any time, press the "f
" key. This will soft-reboot WeensyOS
and cause it to run a single process from the p-fork
application, rather than the gang of allocator
processes. You
should see something like this in the memory map:
That's because you haven't implemented fork()
yet.
How to implement fork()
:
fork()
, look for a free process
slot in the processes[]
array. Don’t use slot 0. If no free
slot exists, return -1
to the caller.
current->p_pagetable
, the
forking process’s page table, using your function from earlier.
fork()
must examine every virtual
address in the old page table. Whenever the parent process has an
application-writable page at virtual address V
, then
fork()
must allocate a new physical page P
; copy
the data from the parent's page into P
using
memcpy()
; and finally map page P
at address
V
in the child process's page table. (There's a Linux man
page for memcpy()
.)
reg_rax
.
virtual_memory_lookup()
to query the mapping between virtual and physical addresses in a
page table.
When you're done, you should see something like the below after
pressing "f
":
An image like the below, however, means you forgot to copy the data for some pages, so the processes are actually "sharing" stack and/or data pages when they should not: