CMSC 412 Project #4
Paging
Intermediate
submission due October 28, 2005 at 6pm
Final submission due November 11, 2005 at 6pm
Grading Criteria
Submission Instructions
Recitation material: proj4.ppt
Excerpt from Dave Hovemeyer's hacking guide GeekOS : paging.pdf
The IA-32 Intel(R) Architecture Software Developer's
Manual,Volume 3
Sections : 3.1, 3.6 (without subsections 3.6.1 and
3.6.2), 3.7.1, 3.7.6 (Figure 3-14 and associated bits only)
New project files
project4.tgz - new project
distribution
Introduction
The purpose of this project is to add paging to your GeekOS kernel.
This
will require many small, but difficult changes. More
than any previous project, it will be important to implement one thing,
test it, and then move to the next one. A successful implementation of
earlier projects is not required for
this project. You can implement Project 4 on top of any earlier version
of the kernel (or directly on the provided distribution).
This project will involve an intermediate submission and a final
poject submission. The intermediate submission should be your
implementation of the kernel memory mapping.
It is worth 10 points (out of 100 for the project). To submit the
intermediate
submission
just follow the same submission instructions as the final submission of
project 4. For details, check the Intermediate
submission
section in the grading criteria.
Paging
In this project you will add paging to the GeekOS kernel, combined with
the segmentation that is already part of the standard
GeekOS kernel. For detailed information on paging, segmentation, and
how they are combined on the Intel architecture, please refer to
sections 8.4-8.7 in the text (pps. 288-309), and the Intel
documentation specified above.
In a system that combines paging and segmentation, there are three
kinds of addresses: logical addresses, linear addresses, and physical
addresses. Logical addresses
are those issued by a process; these are mapped, via segmentation, to linear addresses by merely adding
the base address of the relevant process
segment to the logical address. A linear address is mapped to a physical address, which will be
used by the processor to actually read from main memory, by using page
tables set up and maintained by the operating system. Paging is
similar to segmentation because it allows each process to run in its
own memory space. However, paging allows a much finer granularity of
control by specifiying per-page mappings rather than a constant value
offset.
In GeekOS, your paging system will use a page directory
and page tables which form
what as known as a two-level page
table scheme (see Section 8.5.1, and notably Fig. 8.14, in the
text). Each memory address in the x86 system is 32 bits
long. To translate
a linear address to a physical one, the processor will refer to the
current page
directory (this is analagous to finding an LDT for a
process). It will then use the most significant 10 bits to index into
the page directory (10 bits allows you to write numbers between 0 and
1023 thus the page directory has 1024 entries). The page directory
entry is used to find the appropriate page table. The next 10 bits are
used to index the page table (again 10 bits allows you to write numbers
between 0 and 1023 thus the page table has 1024 entries). Finally, the
page table entry is used to find the base of the physical memory page.
The last 12 bits of the linear memory address is used to index into the
physical page (12 bits allows you to write numbers between 0 and 4095
thus a page is 4096 bytes in size).
Each page directory and page table are themselves pages in the
system. Each page directory/page table entry is 4 bytes and each table
contains 1024 entries (4096 bytes / 4 bytes). Each page directory
contains a pointer to a page table which in turn contains pointers to
the physical memory.
Part I
The first step is to modify your project to use page tables and
segmentation rather than just segments to provide memory protection.
The kernel starts running without paging enabled,
and the
segmentation hardware maps logical addresses directly to physical
addresses. Adding paging introduces a level in between: a logical
address is mapped by segmentation to a linear address, which paging
then maps to a physical address. As a first step, we will
introduce a page table that maps all linear addresses to themselves;
that is each linear address X is mapped to physical address X.
To set up page tables, you will need to allocate
a page directory
(via Alloc_Page) and then allocate page tables for the entire
region
that will be mapped into this memory context. You will need to fill out
the appropriate fields in the page tables and page directories.
The definition of paging tables and directories are to be found in
paging.h (structs pte_t and pde_t).
Finally, to enable paging for the first time, you will need to call
the routine Enable_Paging(pdbr) which is already defined for
you in
lowlevel.asm. It takes the base address
of your page directory as a parameter.
The final step of this function is to add
a handler for page faults. A default one named Page_Fault_Handler
in paging.c has been provided
for you. You should install it by calling Install_Interrupt_Handler.
You need to register this as a handler for interrupt 14.
You should then add a call the Init_VM function from your main.c
(after Init_Interrupts).
You should be able to do this step and test it by
itself by
temporarily giving user mode access to the to these pages - set the
flags fields to include VM_USER. Refer to item
1 in the grading criteria to understand what you should have running at
this point. Once you have this running, you can submit it as your
intermediate submission.
The next step is to make user processes have their
own linear address
space.
We will define a single page directory for all
kernel-only threads, and a
page directory for each user process. The page directory and page tables you set up for
part I will be used by the kernel. As such, you should change the
flags fields to not
include VM_USER.
The page directory for user processes will
contain entries mapping user logical memory to linear memory, and will also contain entries to
address the kernel memory.
This is not so user processes can access kernel memory directly (we
will set the
access flags of the memory to prevent them from doing so), but rather
so that when an interrupt occurs the page table does not need to be
changed for the kernel to access its own memory---it will simply use
the page table of the user process that was running when the interrupt
occurred.
The memory layout for a user process is shown
below, where the addresses on the left are linear addresses (which are
mapped to physical addresses by the paging system). User
processes still refer to logical addresses, which are mapped by the
segmentation system to the linear addresses shown here. In
particular,
a user process will still have the same addresses just as it has in
prior projects.
| VA 0x0000 0000 |
Kernel Memory |
Start of
kernel memory
(map all
physical memory here)
|
| VA 0x8000 0000 |
User Memory |
Data/Text start here
|
|
<gap>
|
|
| VA 0xFFFF E000 |
User Memory |
Initial stack at top of this page |
| VA 0xFFFF F000 |
User Memory |
Args in this page |
| VA 0xFFFF FFFF |
|
Memory space
ends here |
The next step is to modify your user processes
to use pages in
the
user range of memory. To help you do this, there is a new file called uservm.c
which will
replace your userseg.c from previous projects. You should
start by taking the implementations
from userseg.c and copying them to uservm.c and
then modifying them for paging.
There is a line in the Makefile in the build directory which
specifies whether it should
use userseg.c or uservm.c. You can switch between
them by
modifying USER_IMP_C := uservm.c to be USER_IMP_C :=
userseg.c and vice versa.
Setting up paging for user processes occurs in Load_User_Program,
and takes two steps.
First, you need to allocate a
page directory for the process. You should copy all of the
entries from the kernel page directory for the bottom 2 GB of linear
memory.
Next you need to allocate page table
entries for the user process' text, data, and stack regions. Each
of these regions will consist of some number of pages allocated by the
routine Alloc_Pageable_Page. This routine differs
from Alloc_Page in that
the allocated page it returns will have a
special flag PAGE_PAGEABLE set in the flags field of its entry in the
struct Page data structure (see mem.h). All pages (but not page
directories and page tables) for a user space process should be
allocated using this routine.
Contrast this with current implementation: for segmentation, one big
chunk of
memory was allocated for the
entire user process. Paging allows per-page mappings for user memory so
that each
page of the user process is now allocated and mapped individually, and
so it need not be contiguous.
As a suggestion, do this as follows: calculate the size of the text and
data segment of the process as is done now, without including the size
of the stack (but do round to PAGE_SIZE, though), and Malloc it. Load the
program into this memory, as now. Then copy
the image, page by page into the newly allocated pages. Then
allocate
two pages of memory at the end of the virtual address range (i.e.
the last two entries in the last page table). One is for the arguments,
the other one is for stack. Make sure the flags bits in
both the page
directory and page table entries allow user mode access (contain VM_USER
flag).
<>
Finally, you will need to tweak some aspects of the current
segmentation implementation so that it works with paging. The base
linear address for the user mode process should be
0x8000 0000 (USER_VM_START in paging.h), and the
limit should be 0xFFFF FFFF
(USER_VM_END in paging.h). You will also need
to add code to switch
the PDBR register. For this, in Switch_To_Address_Space
you should add a call to Set_PDBR
(provided for you in lowlevel.asm) as part of a context
switch,
after you load the LDT. You will use the pageDir field in the
userContext
structure that will store the address of the process's page directory.
At this point, you should test your memory
mapping by running bochs.
If you are able to load and run shell, you have completed this
correctly.
A nice benefit of paging is that it is
straightforward to add to the
memory allocation of a process by simply allocating a new (physical)
page and mapping it into a free region in the process's address
space. For example, this can be done to allow the process's stack
to grow beyond its initial allocation. To implement stack growth,
you must modify the default page fault handler. This handler is
called whenever the process tries to access an illegal address, and
currently it just terminates the process in this case, printing out the
address that caused the fault. The fault handler reads register
cr2 to determine the
faulting address. It also prints the errorCode defined in InterruptState and the fault
defined in
the struct faultcode_t in paging.h.
You will
modify this page fault handler to
determine an appropriate action to take based on the address. In
the case that the address is within one page of the current stack
limit, you should allocate a new page, map it into the appropriate
address, and then return normally from the handler. The program
will now be able to use the memory that you just allocated for
it. In order to test your new page fault handler, run the
provided program rec.c.
While paging is useful for efficiently managing
memory, the
processes that can be active are still limited by the amount of
physical memory. To remedy this, you will implement virtual memory
as part of GeekOS, in which memory pages can be temporarily stored on
disk so that the freed physical memory can be used by another
process.
To do this, the OS will need to pick a page to
evict from memory and write it to the paging
file, stored on disk. You
should implement a version of pseudo-LRU (see section 9.4.5 in
text, pp. 336). Use the accessed bit in the page tables to
keep
track of how frequently pages are accessed. To do
this, use the clock field in the struct Page in mem.h.
You should update the clock on every page fault for all pages that were
accessed since the last fault.
Alternatively, you can update the clocks in the body of the Idle
thread, i.e. walk thru the allocated
pages and for ones that were accessed, update their clocks. This will
avoid a heavy-weight page fault handler.
The
paging file consists of a group of consecutive 512 disk blocks of size SECTOR_SIZE
bytes.
Calling the routine Get_Paging_Device in vfs.h
will return
a Paging_Device structure containing the first disk block
number of the paging file and the number of disk
blocks in the paging file. Each page will consume 8 consecutive
disk
blocks (PAGE_SIZE/SECTOR_SIZE). To read/write the paging file,
use the functions Block_Read and
Block_Write provided in blockdev.h. These functions
write SECTOR_SIZE bytes at a time. How you manage your paging
file is completely up to you. A good idea would be to write
a Init_Pagefile function in paging.c and call it
from main.c
The code to page out a page is
implemented for you in Alloc_Pageable_Page in mem.c,
and works as follows:
Find a page to page out using Find_Page_To_Page_Out
which
you will implement in mem.c. (This function relies on the clock
field in the Page structure which you must manage).
Find space on the paging file using Find_Space_On_Paging_File
which you will implement in paging.c
Write the page to the paging file using Write_To_Paging_File
which you will implement in paging.c
Update the page table entry for the page to
clear the present
bit.
Update the pageBaseAddr in the page
table entry to be the
first disk block that contains the page.
Update the kernelInfo bits (3 bits
holding a number
from 0-7) in the page table entry to be KINFO_PAGE_ON_DISK
(used to indicate that the page is on disk rather than
not valid).
Flush the TLB using Flush_TLB from lowlevel.asm
Eventually,
the page that was put on disk will be needed by some process again. At
this point you will have to read it back off disk into memory (possibly
while paging out another page to fit it into memory). Since the page
that is paged out has its present bit set to 0 in the page
table, an access to
it will cause a page fault. Your page fault handler should then realize
that this page is actually stored on disk and bring it back from disk
(the kernelInfo field in the page table entry). When you
bring a page in off disk, you may free
the disk space used by the page. This will simplify
your paging system, but will require that when a page is removed from
memory it must always be written to the backing store (since even
pages that haven't been written since they were last brought into
memory from disk are not already on disk). You will rely on the
information stored when a page
is paged out to find it on disk and page it back in.
The following table summarizes the actions of
your page fault
handler.
| Cause |
Indication |
Action |
| Stack growing to new page |
Fault is within one page of
the current stack limit |
Allocate a new page and
continue. |
| Fault for paged out page |
Bits in page table indicate
page is on disk |
Read page from paging device
(sector indicated in PTE) and continue. |
| Fault for invalid address |
None of the other conditions
apply |
Terminate user process |
As part of process termination, you will need to
free the memory
associated with a process. This includes freeing the pages used
by the process, freeing the page tables and page directories. In
addition, you will need to release the backing store space used by any
pages of the terminating process. You should modify your Destroy_User_Context
function in uservm.c to
do this.
As mentioned in recitation (and in recitation
slides), you should use
Alloc_Page() for allocating page tables and page directories
and
Alloc_Pageable_Page() for everything else (program memory,
arguments, stack).
This is intended to make your task easier, since you know the page
tables are always in memory. For extra credit (10
points), try to make page tables pageable too, i.e. use Alloc_Pageable_Page()
for page tables.