Only the portions of the solution which represent additions or significant changes to the solution for the second programming assignment are described here. Everything not described here is essentially identical the the solutions for the first or second programming assignments.
Probably the most important decision to be made is the page replacement algorithm, but even here the interface to the algorithm is simple and fixed, which means it's easy to just cobble something up to meet the interface spec using, for example, random page replacement. Once everything else is in place and working, the replacement algorithm can be upgraded to something more acceptable, should that be necessary.
Another interesting design problem is determining how to wake up the user process that threw the page fault once the page lands in primary store. This would be a difficult design problem if it wasn't for the design of the disk manager done in the second assignment. However, the situations in both cases are the same, and so the solution used for the disk manager - callbacks - can also be used here.
That pretty much wraps the major design issues associated with the page manager proper. There are some minor, local design issues remaining, but these are best handled when they come up during code development below.
In the overall scheme of things, the page manager fits between the disk driver and the process manager, and receives its input from the interrupt handler on addressing faults. The page manager uses the disk driver to page to and from the disk, and uses the process manager to suspend and resume processes involved in a page fault.
<page manager interface declarations>= (U->) [D->] void initialize(unsigned page_file_start);
When process pid
throws a page fault by accessing unmapped virtual address
vaddr
, the page manager needs to page-in the proper page. When the page
has landed in primary store, the page manager calls f(arg)
.
<page manager interface declarations>+= (U->) [<-D->] typedef void (* callback)(void *); void page_in(unsigned pid, address vaddr, callback f, void * arg);
When process pid
exits, its mapped in pages have to be freed to make them
available for other processes.
<page manager interface declarations>+= (U->) [<-D] void clear_process(unsigned pid);
A primary-store page frame is either free, allocated, or in transition from being free to being allocated.
<page manager data structures>= (U->) [D->] enum page_frame_state { free_pf, transitional_pf, allocated_pf }; static page_frame_state page_frame_states[pages_in_user_space];
Definespage_frame_state
,page_frame_states
(links are to index).
The number of the fist block in the paging area on disk.
<page manager data structures>+= (U->) [<-D->] static unsigned page_file_start;
Definespage_file_start
(links are to index).
When a requested page-in or page-out completes, the disk driver calls back to the page manager, passing along the following information:
ptep
- a pointer to the page frame receiving the page.
vaddr
- the virtual address that caused the page fault.
f
- the call-back to use when the page-in is complete.
arg
- the argument to pass along in the call back.
<page manager data structures>+= (U->) [<-D] struct page_request_info { pte_ptr ptep; address vaddr; page_mgr::callback f; void * arg; }; typedef page_request_info * pri_ptr;
Definespage_request_info
,pri_ptr
(links are to index).
Return a pointer to a page frame suitable for receiving a page. A free page frame is most suitable; a page frame in transition (that is, in the process of receiving a page from disk) is completely unsuitable. Of the remaining page frames, a page frame that's been unaccessed for the longest period of time is must suitable.
<page manager procedures>= (U->) [D->] static pte_ptr allocate_page_frame(void) { int oldest = -1; for (unsigned i = 0; i < pages_in_user_space; i++) { if (page_frame_states[i] == free_pf) { oldest = i; break; } if (page_frame_states[i] == transitional_pf) continue; if ((oldest < 0) || (mass.memory->page_table[i].accessed_at < mass.memory->page_table[oldest].accessed_at)) oldest = i; } if (oldest < 0) panic("can't find an available page frame"); page_frame_states[oldest] = transitional_pf; return mass.memory->page_table + oldest; }
Definesallocate_page_frame
(links are to index).
When a page-in request is complete, the disk driver will call page_in_cb()
to complete the page-in, which involves completing the page-table
initialization and calling the requester of the page in.
<page manager procedures>+= (U->) [<-D->] static void page_in_cb(status::responses s, void * arg) { if (s != status::ok) panic("page-in disk read failed with status %d", s); pri_ptr prip = reinterpret_cast<pri_ptr>(arg); prip->ptep->valid = true; prip->ptep->accessed_at = prip->ptep->modified_at = 0; prip->ptep->process_id = prip->ptep->process_id; prip->ptep->page_number = vaddr_page_number(prip->vaddr); if (prip->f) (prip->f)(prip->arg); page_frame_states[prip->ptep - mass.memory->page_table] = allocated_pf; delete prip; }
Definespage_in_cb
(links are to index).
Schedule a page-in request with the disk driver, using the information pointed
to by prip
.
<page manager procedures>+= (U->) [<-D->] static void schedule_page_in(pri_ptr prip) { const unsigned pno = vaddr_page_number(prip->vaddr); disk_dvr::request( device::read, process::get_page_block(prip->ptep->process_id, pno), page_frame_address(prip->ptep), page_in_cb, prip); debugp(dbp_paging, "At time %4d, load process %3d page %2d at %4d for address %4d.\n", read_clock(), prip->ptep->process_id, pno, page_frame_address(prip->ptep), prip->vaddr); }
Definesschedule_page_in
(links are to index).
Schedule a page-out request for the page being held in the page frame
associated with ptep
.
If this is the first time this page's getting paged out, the process's page-block location for this page points to the page's location in the program file. Bump the page-block location up to move the page to the paging area.
The page-out request doesn't include a call-back function, which means the page manager won't know when the page-out has been completed. This turns out not to be a problem because the disk driver handles requests in FIFO order; as long as the page-in request is issued after the page-out request, disk I-O is handled in the proper order. This is, however, an incredibly dangerous assumption to make, and represents bad, bad programming.
<page manager procedures>+= (U->) [<-D] static void schedule_page_out(pte_ptr ptep) { unsigned db_no = process::get_page_block(ptep->process_id, ptep->page_number); if (db_no < page_file_start) { db_no += page_file_start; process::put_page_block(ptep->process_id, ptep->page_number, db_no); } debugp(dbp_paging, "At time %4d, flush process %2d page %2d at %4d to %3d.\n", read_clock(), ptep->process_id, ptep->page_number, page_frame_address(ptep), db_no); disk_dvr::request( device::write, db_no, page_frame_address(ptep), 0, 0); }
Definesschedule_page_out
(links are to index).
Page manager initialization involves remembering the first disk block of the paging area and setting the page table state.
<page manager interface procedures>= (U->) [D->] void initialize(unsigned pfs) { if (2*pfs > disk_size) panic("the disk contains insufficient paging space"); page_file_start = pfs; for (unsigned i = 0; i < pages_in_user_space; i++) { page_frame_states[i] = free_pf; mass.memory->page_table[i].valid = false; } }
Definespage_mgr::initialize
(links are to index).
Page in the virtual address vaddr
on behalf of the process pid
. If the
allocated page frame has a dirty page, schedule a page out before the page in.
<page manager interface procedures>+= (U->) [<-D->] void page_in(unsigned pid, address vaddr, callback f, void * arg) { pri_ptr prip = new page_request_info; prip->ptep = allocate_page_frame(); if (prip->ptep->valid && prip->ptep->modified_at > 0) schedule_page_out(prip->ptep); prip->ptep->valid = false; prip->ptep->process_id = pid; prip->f = f; prip->arg = arg; prip->vaddr = vaddr; schedule_page_in(prip); }
Definespage_mgr::page_in
(links are to index).
The process with process number pid
has exited. Free up all the page
frames allocated to the process, and mark them all as being invalid.
<page manager interface procedures>+= (U->) [<-D] void clear_process(unsigned pid) { for (unsigned i = 0; i < pages_in_user_space; i++) if (mass.memory->page_table[i].process_id == pid) { page_frame_states[i] = free_pf; mass.memory->page_table[i].valid = false; } }
Definespage_mgr::clear_process
(links are to index).
<page-mgr.cc
>=
#include <set>
#include "page-mgr.h"
#include "disk-dvr.h"
#include "process.h"
#include "os.h"
<page manager data structures>
<page manager procedures>
namespace page_mgr {
<page manager interface procedures>
}
Definespage-mgr.cc
(links are to index).
<page-mgr.h
>=
#ifndef _page__mgr_h_
#define _page__mgr_h_
#include "mass.h"
namespace page_mgr {
<page manager interface declarations>
};
#endif
Definespage-mgr.h
(links are to index).
To keep things simple, the program loader hangs all programs on the ready queue
before running any of them. Because the program loader runs to completion and
stops, there's no longer any need for the done()
predicate and poke()
procedure in the interface; only the init()
procedure remains as the
program loader's interface.
There is, however, another reason besides simplicity for delaying program execution until all programs have been loaded. Because the paging area follows the programs on disk, the page manager can't be initialized until the total size of the programs are known.
Keeping track of a program on disk involves recording its starting block and the number of blocks it occupies, something for which a pair of unsigneds is well suited. Keeping track of all such programs on the disk is easiest done with a vector of such pairs.
<program loader data structures>= (U->) typedef std::pair<unsigned, unsigned> program_info; static std::vector<program_info> program_list;
Definesprogram_info
,program_list
(links are to index).
Because processes don't run until all programs are read, the program loader can read disk blocks into primary store without clobbering some process's pages. This is actually half-way to pre-fetching; the other half is keeping track in the page table where each initial block lands in primary store. However, that's too much work, so the page manager just reads into the first block of User Space and lets it go at that.
<program loader macros>= (U->) const address pl_buffer = usr_base; #define do_disk_read() \ disk_dvr::request(device::read, next_block_to_read, pl_buffer, get_block, 0)
Definesdo_disk_read
,pl_buffer
(links are to index).
get_block()
handles a block that's been read from the disk. If the block
is the first block in a program, save the program information and adjust the
next-block address. If there's more programs to read, then issue the next read
request; otherwise, initialize the page manager and hand the programs over to
the process manager using the auxiliary procedure start_program()
. Just
in case there's no programs on the disk, the program loader should be prepared
to shut down the system.
<program loader procedures>= (U->) static void start_program(program_info pi) { process::add(pi.first, pi.second); } static void get_block(status::responses r, void *) { if (r != status::ok) panic("disk read error %R in program_loader::get_block()", r); word psize; fetch_mem(pl_buffer, psize, handle_first_block); if (static_cast<unsigned>(psize) > disk_size) panic("invalid program size %d in handle_first_block()", psize); if (psize != 0) { program_list.push_back(std::make_pair(next_block_to_read, psize)); next_block_to_read += psize; } if ((psize != 0) && (next_block_to_read < disk_size)) do_disk_read(); else { page_mgr::initialize(next_block_to_read); std::for_each(program_list.begin(), program_list.end(), start_program); if (process::done()) process::halt(); } }
Definesget_block
,start_program
(links are to index).
To initialize the program loader, set the next disk-block to read to the first disk block and go read it.
<program loader interface procedures>= (U->) void init(void) { next_block_to_read = 0; do_disk_read(); }
Definesprogram_loader::init
(links are to index).
There are a couple of ways the disk manager can implement the physical-virtual address translation, but the easiest way is to assign a one-block disk buffer to the disk manager. The disk manager does all disk I-O to and from the its buffer, and does virtual-physical address translation when copying data between its (physical address) buffer and the process's (virtual) address space.
There's plenty of free device space available, so allocate a one-block buffer there.
<os buffer definitions>= (U->) const address dm_buffer = dev_top - disk_block_size;
Definesdm_buffer
(links are to index).
The buffer scheme has the great virtue of being (relatively) simple, but it does have a number of drawbacks. First, the buffer becomes a bottleneck between processes and the disk because the buffer must be involved in every disk I-O operation. This can be relieved somewhat by increasing the number of buffers available to the disk manager, but as a practical matter the number of buffers is finite and you can't outrun the bottleneck problem.
The disk manager also has to keep a queue of disk I-O requests because the buffer is a serially reusable resource available to only one process at a time (this is the source of the bottleneck). Also, the need to copy between the buffer and the process essentially doubles the I-O time.
The interface presented by the disk manager is unchanged; only the internals need to be changed to accommodate virtual memory.
The disk manager needs to be able to queue up requests and issue them one at a time when the buffer is available.
<disk manager data structures>= (U->) struct req { req(device::operations d, address l, unsigned b, process::id p) : dop(d), loc(l), bno(b), pid(p) { } const device::operations dop; const address loc; const unsigned bno; const process::id pid; }; static std::queue<req> pending;
Definespending
,req
(links are to index).
If the disk manager's working on a disk I-O request, or if there's no pending requests, then the disk manager shouldn't start a new request. Otherwise, the disk manager should start the disk I-O operation at the head of the pending queue.
<disk manager procedures>= (U->) [D->] static void poke(void) { if (in_progress || pending.empty()) return; const req & r = pending.front(); in_progress = true; if (r.dop == device::write) write_dm_buffer(0); else if (r.dop == device::read) disk_dvr::request(device::read, r.bno, dm_buffer, read_buffer_cb, 0); else panic("impossible occurrence in disk-mgr::poke()"); }
Definespoke
(links are to index).
When the current disk I-O request has completed, its request should be popped off the head of the pending queue, the disk manager should be set to idle, and next pending request should be started.
<disk manager procedures>+= (U->) [<-D->] static void next_disk_request(void) { pending.pop(); in_progress = false; poke(); }
Definesnext_disk_request
(links are to index).
The disk manager needs two steps to perform a write from a process to the disk:
The second step is easy, involving a call to disk_dvr::request()
. The
first step is more complicated, because it may involving paging.
A disk block is the same size as a page (what a coincidence!), so the data block to write may be split across at most two pages, which means the copy to the buffer may require at most two page-ins. When copying the data, the disk manager has to first check each virtual address to make sure it's resident in primary store, and page it in if it's not.
The analysis given in the previous paragraph is a bit simple minded because it's only necessary to check at most two addresses: the address at the start of the data block and the address on the other side of the page boundary crossed by the data block (if it doesn't fit all in one page). However, exploiting this fact complicates the code, so it will be ignored in what follows.
write_dm_buffer()
is responsible for performing a disk write; it also is
the call-back for the page manager on page ins. The pass-along argument is the
offset into the data block being written; the rest of the arguments are kept at
the head of the pending queue. Once the data block has been copied to the
buffer, the actual disk write can begin, and the disk write request is
wrapped-up by write_buffer_cb()
.
<disk manager procedures>+= (U->) [<-D->] static void write_dm_buffer(void * arg) { unsigned offset = reinterpret_cast<unsigned>(arg); const req & r = pending.front(); while (offset < disk_block_size) { address pa; if (!(mass.memory->va2pa(r.pid, r.loc + offset, pa, 'r'))) { page_mgr::page_in(r.pid, (address) (r.loc + offset), write_dm_buffer, reinterpret_cast<void *>(offset)); return; } word w; fetch_mem(pa, w, write_dm_buffer); store_mem(dm_buffer + offset, w, write_dm_buffer); offset++; } disk_dvr::request(device::write, r.bno, dm_buffer, write_buffer_cb, 0); }
Defineswrite_dm_buffer
(links are to index).
When a process's write request has completed, store the status and make the process ready to run. Then go start the next disk I-O request, if any.
<disk manager procedures>+= (U->) [<-D->] static void write_buffer_cb(status::responses s, void *) { assert(!pending.empty()); const process::id pid = pending.front().pid; process::put_register(pid, 0, s); process::ready(pid); next_disk_request(); }
Defineswrite_buffer_cb
(links are to index).
A disk read is handled much like a disk write, except the steps are reversed:
first read the disk into the buffer and then copy the buffer into the process's
address space. read_dm_buffer()
assumes the read's already been done and
the data's ready to be copied into the process's address space. Once the copy
is complete, the process can be made ready to run and the disk manager can
begin de novo.
<disk manager procedures>+= (U->) [<-D->] static void read_dm_buffer(void * arg) { unsigned offset = reinterpret_cast<unsigned>(arg); assert(!pending.empty()); const req & r = pending.front(); while (offset < disk_block_size) { address pa; if (!(mass.memory->va2pa(r.pid, r.loc + offset, pa, 'w'))) { page_mgr::page_in(r.pid, (address) (r.loc + offset), read_dm_buffer, reinterpret_cast<void *>(offset)); return; } word w; fetch_mem(dm_buffer + offset, w, read_dm_buffer); store_mem(pa, w, read_dm_buffer); offset++; } process::ready(r.pid); process::put_register(r.pid, 1, r.bno); next_disk_request(); }
Definesread_dm_buffer
(links are to index).
The requested disk read was either successful, in which case control moves on to copying the data from the buffer to the process, or it failed, in which case the requesting process wakes up immediately with the bad news.
<disk manager procedures>+= (U->) [<-D] static void read_buffer_cb(status::responses s, void *) { assert(!pending.empty()); const req & r = pending.front(); process::put_register(r.pid, 0, s); if (s != status::ok) { process::ready(r.pid); next_disk_request(); } else read_dm_buffer(0); }
Definesread_buffer_cb
(links are to index).
The implementation of the disk-manager interface procedures is unchanged,
except for operation()
, which now hangs the I-O request on the pending
queue, rather than issuing the disk I-O commands directly.
<disk manager interface procedures>= (U->) void operation( system_call::system_calls op, word dev_id, address loc, unsigned n) { assert(is_valid_id(dev_id)); assert((op == system_call::read) || (op == system_call::write)); if (!legal_range(loc, disk_block_size)) { store_mem(0, status::bad_address, disk_mgr::operation); return; } pending.push(req((op == system_call::read ? device::read : device::write), loc, n, process::block())); poke(); }
Definesdisk_mgr::operation
(links are to index).
Unlike the disk, from which a process can only read a fixed-sized data block, the terminal can supply a process with an arbitrarily long sequence of words. This complicates buffering. On the other hand, also unlike the disk, data come from the terminal a word at a time, which, assuming you're not interested in high performance, simplifies buffering.
The interface presented to by the terminal manager is unchanged.
The terminal manager now needs to deal with call-backs from the page manager in
addition to interrupts from the terminal. It seems a wise design decision to
devote a procedure to handle each. The write-interrupt handler, for example,
look as it did before, except the actual character handling code has been moved
to character_from_process()
, which also handles the page-manager call-back.
<terminal manager procedures>= (U->) [D->] static void write_terminal_ih(void) { check_tty(write_terminal_ih); if (request_chcnt > 0) character_from_process(0); else wake_up_process(); }
Defineswrite_terminal_ih
(links are to index).
character_from_process()
fetches the next character from the process (which
may involve requesting a page in) and writes it to the terminal.
<terminal manager procedures>+= (U->) [<-D->] static void character_from_process(void *) { address pa; assert(request_chcnt > 0); if (!(mass.memory->va2pa(tty_proc, request_chaddr, pa, 'r'))) page_mgr::page_in(tty_proc, request_chaddr, character_from_process, 0); else { word w; fetch_char(pa, w, character_from_process); store_mem(terminal_data_register, w, character_from_process); tty_op(write, character_from_process); request_chaddr++; request_result_cnt++; request_chcnt--; } }
Definescharacter_from_process
(links are to index).
read_terminal_ih()
reads the incoming character from the terminal and, if
it doesn't indicate the end-of-file, passes it on to character_to_process()
for for storage, which may involve a page in.
<terminal manager procedures>+= (U->) [<-D->] static void read_terminal_ih(void) { check_tty(read_terminal_ih); word w; fetch_mem(terminal_data_register, w, read_terminal_ih); if (w == device::eof_char) wake_up_process(); else character_to_process(reinterpret_cast<void *>(w)); }
Definesread_terminal_ih
(links are to index).
Store the character arg
read from the terminal, which may involve
requesting a page in.
<terminal manager procedures>+= (U->) [<-D] static void character_to_process(void * arg) { address pa; if (!(mass.memory->va2pa(tty_proc, request_chaddr, pa, 'w'))) page_mgr::page_in(tty_proc, request_chaddr, character_to_process, arg); else { word w = reinterpret_cast<word>(arg); store_char(pa, w, character_to_process); request_chaddr++; request_chcnt--; request_result_cnt++; if (request_chcnt == 0) wake_up_process(); else tty_op(read, character_to_process); } }
Definescharacter_to_process
(links are to index).
When a process throws a bad (virtual) address, it could be because the address is outside the range of legal addresses for the process, or because the address isn't resident in physical storage. In the latter case, schedule a page in for the address; end the process in the former case.
<interrupt handlers>= (U->) [D->] static void badaddr_ih(void) { word w; fetch_mem(ia_register, w, badaddr_ih); if (legal_range(w, 1)) { const process::id pid = process::block(); page_mgr::page_in(pid, (address) w, page_fault_cb, reinterpret_cast<void *>(pid)); } else process::exit(); }
Definesbadaddr_ih
(links are to index).
When a page lands in primary store, restart the process that caused the page fault.
<interrupt handlers>+= (U->) [<-D] static void page_fault_cb(void * arg) { process::ready(reinterpret_cast<process::id>(arg)); }
Definespage_fault_cb
(links are to index).
A new process is defined by its starting location on the disk and the number of disk blocks it occupies.
<process manager interface declarations>= (U->) [D->] void add(unsigned bno, unsigned size);
Given the number of a page belonging to a process, the page manager needs to be able to indicate in which disk block the page is stored, and retrieve the disk block number holding a process's page.
<process manager interface declarations>+= (U->) [<-D] unsigned get_page_block(id pid, unsigned pno); void put_page_block(id pid, unsigned pno, unsigned dbn);
A process-table entry needs to be extended by a page map, which maps between a process's pages and the location of the pages on disk.
<process manager data structures>= (U->) struct process_table_entry { static process::id next_id; const process::id id; process_status status; word registers[next_register]; std::vector<unsigned> page_map; process_table_entry(unsigned bno, unsigned size) : id(next_id++), page_map(std::vector<unsigned>(size)) { assert((size > 0) && (bno + size <= disk_size)); for (unsigned i = 0; i < size; i++) page_map[i] = bno + i; } void save_state(void) { for (address a = 0; a < next_register; a++) fetch_mem(a, registers[a], save_state); } void restore_state(void) const { for (address a = 0; a < next_register; a++) store_mem(a, registers[a], restore_state); } };
Definesprocess_table_entry
(links are to index).
The procedures to read and write a process's page map.
<process manager interface procedures>= (U->) [D->] unsigned get_page_block(id pid, unsigned pno) { ptab_iter pti = find_proc(pid); assert(pno < pti->page_map.size()); return pti->page_map[pno]; } void put_page_block(id pid, unsigned pno, unsigned dbn) { ptab_iter pti = find_proc(pid); assert((pno < pti->page_map.size()) && (dbn < disk_size)); pti->page_map[pno] = dbn; }
Definesprocess::get_page_block
,process::put_page_block
(links are to index).
The process::add()
procedure changes slightly to set the register context
properly for virtual memory.
<process manager interface procedures>+= (U->) [<-D->] void add(unsigned bno, unsigned size) { process_table_entry new_pte(bno, size); new_pte.registers[base_register] = vmem_base; new_pte.registers[top_register] = vmem_base + size*disk_block_size; new_pte.registers[pc_register] = vmem_base + 1; new_pte.registers[sw_register] = 0; put_swr_field(new_pte.registers[sw_register], vmem, 1); put_swr_field(new_pte.registers[sw_register], pid, new_pte.id); ptab.push_front(new_pte); debugp(dbp_execute, "At time %4d, process %d created.\n", read_clock(), new_pte.id); ready(new_pte.id); }
Definesprocess::add
(links are to index).
The exit()
procedure needs to call the page manager to clear the exiting
process's pages from the page frame.
<process manager interface procedures>+= (U->) [<-D] void exit(void) { debugp(dbp_execute, "At time %4d, process %d exits.\n", read_clock(), running_process->id); page_mgr::clear_process(running_process->id); ptab.erase(running_process); poke(); }
Definesprocess::exit
(links are to index).
disk-dvr.cc
>: D1
disk-dvr.h
>: D1
disk-mgr.cc
>: D1
disk-mgr.h
>: D1
mpool.cc
>: D1
mpool.h
>: D1
os.cc
>: D1
os.h
>: D1
os-utils.cc
>: D1
os-utils.h
>: D1
page-mgr.cc
>: D1
page-mgr.h
>: D1
process.cc
>: D1
process.h
>: D1
program-loader.cc
>: D1
program-loader.h
>: D1
terminal-mgr.cc
>: D1
terminal-mgr.h
>: D1
This page last modified on 17 January 2002.