Chapter 10 Flashcards
The ability to execute a program that is only partially in memory would
confer many benefits
A program would no longer be constrained by the amount of physical
memory that is available. Users would be able to write programs for an
extremely large virtual address space, simplifying the programming task.
* Because each program could take less physical memory, more programs
couldbe runat the same time,witha correspondingincrease inCPU utilization
and throughput but with no increase in response time or turnaround
time.
* Less I/O would be needed to load or swap portions of programs into
memory, so each program would run faster.
Virtual memory
involves the separation of logical memory as perceived
by developers from physical memory. This separation allows an extremely
large virtual memory to be provided for programmers when only a smaller
physical memory is available (Figure 10.1). Virtual memory makes the task of
programming much easier, because the programmer no longer needs to worry
about the amount of physical memory available; she can concentrate instead
on programming the problem that is to be solved.
The virtual address space
of a process refers to the logical (or virtual) view
of how a process is stored in memory. Typically, this view is that a process
begins at a certain logical address—say, address 0—and exists in contiguous
memory, as shown in Figure 10.2. Recall from Chapter 9, though, that in fact
physical memory is organized in page frames and that the physical page
frames assigned to a process may not be contiguous. It is up to the memory management unit (MMU) to map logical pages to physical page frames in
memory.
Note in Figure 10.2 that we allow the heap to grow upward in memory as
it is used for dynamic memory allocation. Similarly, we allow for the stack to
grow downward in memory through successive function calls. The large blank
space (or hole) between the heap and the stack is part of the virtual address
space but will require actual physical pages only if the heap or stack grows.
Virtual address spaces that include holes are known as sparse address spaces.
Using a sparse address space is beneficial because the holes can be filled as the
stack or heap segments grow or if we wish to dynamically link libraries (or
possibly other shared objects) during program execution.
In addition to separating logical memory from physical memory, virtual
memory allows files and memory to be shared by two or more processes
through page sharing (Section 9.3.4). This leads to the following benefits:
- System libraries such as the standard C library can be shared by several
processes through mapping of the shared object into a virtual address
space. Although each process considers the libraries to be part of its virtual
address space, the actual pages where the libraries reside in physical
memory are shared by all the processes (Figure 10.3). Typically, a library is
mapped read-only into the space of each process that is linked with it. - Similarly, processes can share memory. Recall from Chapter 3 that two
or more processes can communicate through the use of shared memory.
Virtual memory allows one process to create a region of memory that it can
share with another process. Processes sharing this region consider it part
of their virtual address space, yet the actual physical pages of memory are
shared, much as is illustrated in Figure 10.3. - Pages can be shared during process creation with the fork() system call,
thus speeding up process creation.
We further explore these—and other—benefits of virtual memory later in
this chapter. First, though, we discuss implementing virtual memory through
demand paging.
Demand Paging
Consider how an executable program might be loaded from secondary storage
into memory. One option is to load the entire program in physical memory
at program execution time. However, a problem with this approach is that we may not initially need the entire program in memory. Suppose a program
starts with a list of available options from which the user is to select. Loading
the entire program into memory results in loading the executable code for all
options, regardless of whether or not an option is ultimately selected by the
user.
An alternative strategy is to load pages only as they are needed. This technique
is known as demand paging and is commonly used in virtual memory
systems. With demand-paged virtual memory, pages are loaded only when
they are demanded during program execution. Pages that are never accessed
are thus never loaded into physical memory. A demand-paging system is similar
to a paging system with swapping (Section 9.5.2) where processes reside in
secondary memory (usually an HDD or NVM device). Demand paging explains
one of the primary benefits of virtual memory—by loading only the portions
of programs that are needed, memory is used more efficiently.
Basic Concepts
The general concept behind demand paging, as mentioned, is to load a page in
memory only when it is needed. As a result,while a process is executing, some
pageswill be in memory, and some will be in secondary storage. Thus, we need
some form of hardware support to distinguish between the two. The valid–
invalid bit scheme described in Section 9.3.3 can be used for this purpose. This time, however, when the bit is set to “valid,” the associated page is both legal
and in memory. If the bit is set to “invalid,” the page either is not valid (that
is, not in the logical address space of the process) or is valid but is currently in
secondary storage. The page-table entry for a page that is brought into memory
is set as usual, but the page-table entry for a page that is not currently in
memory is simply marked invalid. This situation is depicted in Figure 10.4.
(Notice that marking a page invalid will have no effect if the process never
attempts to access that page.)
But what happens if the process tries to access a page that was not brought
into memory?Access to a page marked invalid causes a page fault. The paging
hardware, in translating the address through the page table,will notice that the
invalid bit is set, causing a trap to the operating system. This trap is the result
of the operating system’s failure to bring the desired page into memory. The
procedure for handling this page fault is straightforward (Figure 10.5):
1. We check an internal table (usually kept with the process control block)
for this process to determine whether the reference was a valid or an
invalid memory access.
2. If the reference was invalid, we terminate the process. If it was valid but
we have not yet brought in that page, we now page it in.
3. We find a free frame (by taking one from the free-frame list, for example).
4. We schedule a secondary storage operation to read the desired page into
the newly allocated frame.
5. When the storage read is complete,we modify the internal table keptwith
the process and the page table to indicate that the page is now inmemory.
6. We restart the instruction that was interrupted by the trap. The process
can now access the page as though it had always been in memory.
In the extreme case, we can start executing a process with no pages in
memory. When the operating system sets the instruction pointer to the first
instruction of the process, which is on a non-memory-resident page, the process
immediately faults for the page. After this page is brought into memory, the
process continues to execute, faulting as necessary until every page that it
needs is in memory. At that point, it can execute with no more faults. This
scheme is pure demand paging: never bring a page into memory until it is
required.
Theoretically, some programs could access several new pages of memory
with each instruction execution (one page for the instruction and many for
data), possibly causing multiple page faults per instruction. This situation
would result in unacceptable system performance. Fortunately, analysis of
running processes shows that this behavior is exceedingly unlikely. Programs
tend to have locality of reference, described in Section 10.6.1, which results in
reasonable performance from demand paging.
The hardware to support demand paging is the same as the hardware for
paging and swapping:
* Page table. This table has the ability to mark an entry invalid through a
valid–invalid bit or a special value of protection bits.
* Secondary memory. This memory holds those pages that are not present
in main memory. The secondary memory is usually a high-speed disk or
NVM device. It is known as the swap device, and the section of storage
used for this purpose is known as swap space. Swap-space allocation is
discussed in Chapter 11.
A crucial requirement for demand paging is the ability to restart any
instruction after a page fault. Because we save the state (registers, condition
code, instruction counter) of the interrupted process when the page fault
occurs, we must be able to restart the process in exactly the same place and
state, except that the desired page is now in memory and is accessible. In most
cases, this requirement is easy to meet. Apage fault may occur at any memory
reference. If the page fault occurs on the instruction fetch, we can restart by
fetching the instruction again. If a page fault occurs while we are fetching an
operand, we must fetch and decode the instruction again and then fetch the
operand.
As a worst-case example, consider a three-address instruction such as ADD
the content of Ato B, placing the result in C. These are the steps to execute this
instruction:
1. Fetch and decode the instruction (ADD).
2. Fetch A.
3. Fetch B.
4. Add Aand B.
5. Store the sum in C.
If we fault when we try to store in C (because C is in a page not currently
in memory), we will have to get the desired page, bring it in, correct the
page table, and restart the instruction. The restart will require fetching the
instruction again, decoding it again, fetching the two operands again, and
then adding again. However, there is not much repeated work (less than one
complete instruction), and the repetition is necessary only when a page fault
occurs.
The major difficulty arises when one instruction may modify several different
locations. For example, consider the IBM System 360/370 MVC (move
character) instruction, which can move up to 256 bytes from one location to
another (possibly overlapping) location. If either block (source or destination)
straddles a page boundary, a page fault might occur after the move is partially
done. In addition, if the source and destination blocks overlap, the source
block may have been modified, in which case we cannot simply restart the
instruction.
This problem can be solved in two different ways. In one solution, the
microcode computes and attempts to access both ends of both blocks. If a page
fault is going to occur, it will happen at this step, before anything is modified.
The move can then take place; we know that no page fault can occur, since all
the relevant pages are in memory. The other solution uses temporary registers
to hold the values of overwritten locations. If there is a page fault, all the old
values arewritten back into memory before the trap occurs. This action restores
memory to its state before the instruction was started, so that the instruction
can be repeated.
This is by no means the only architectural problem resulting from adding
paging to an existing architecture to allow demand paging, but it illustrates
some of the difficulties involved. Paging is added between the CPU and the
memory in a computer system. It should be entirely transparent to a process.
Thus, people often assume that paging can be added to any system. Although
this assumption is true for a non-demand-paging environment, where a page
fault represents a fatal error, it is not true where a page fault means only that
an additional page must be brought into memory and the process restarted.
Free-Frame List
When a page fault occurs, the operating system must bring the desired page
from secondary storage into main memory. To resolve page faults, most operating
systems maintain a free-frame list, a pool of free frames for satisfying
such requests (Figure 10.6). (Free frames must also be allocated when the stack
or heap segments from a process expand.) Operating systems typically allocate free frames using a technique known as zero-fill-on-deman . Zero-fillon-
demand frames are “zeroed-out” before being allocated, thus erasing their
previous contents. (Consider the potential security implications of not clearing
out the contents of a frame before reassigning it.)
When a system starts up, all available memory is placed on the free-frame
list. As free frames are requested (for example, through demand paging), the
size of the free-frame list shrinks. At some point, the list either falls to zero or
falls below a certain threshold, at which point it must be repopulated.We cover
strategies for both of these situations in Section 10.4.
Performance of Demand Paging
Demand paging can significantly affect the performance of a computer system.
To see why, let’s compute the effective access time for a demand-paged memory.
Assume the memory-access time, denoted ma, is 10 nanoseconds. As long
as we have no page faults, the effective access time is equal to the memory
access time. If, however, a page fault occurs, we must first read the relevant
page from secondary storage and then access the desired word.
Let p be the probability of a page fault (0 ≤ p ≤ 1). We would expect p to
be close to zero—that is, we would expect to have only a few page faults. The
effective access time is then
effective access time = (1 − p) × ma + p × page fault time.
To compute the effective access time, we must know how much time is
needed to service a page fault. A page fault causes the following sequence to
occur:
1. Trap to the operating system.
2. Save the registers and process state.
3. Determine that the interrupt was a page fault.
4. Check that the page referencewas legal, and determine the location of the
page in secondary storage.
5. Issue a read from the storage to a free frame:
a. Wait in a queue until the read request is serviced.
b. Wait for the device seek and/or latency time.
c. Begin the transfer of the page to a free frame.
6. While waiting, allocate the CPU core to some other process.
7. Receive an interrupt from the storage I/O subsystem (I/O completed).
8. Save the registers and process state for the other process (if step 6 is
executed).
9. Determine that the interrupt was from the secondary storage device.
10. Correct the page table and other tables to show that the desired page is
now in memory.
11. Wait for the CPU core to be allocated to this process again.
12. Restore the registers, process state, and new page table, and then resume
the interrupted instruction.
Not all of these steps are necessary in every case. For example,we are assuming
that, in step 6, the CPU is allocated to another process while the I/O occurs.
This arrangement allows multiprogramming to maintain CPU utilization but
requires additional time to resume the page-fault service routine when the I/O
transfer is complete.
In any case, there are three major task components of the page-fault service
time:
1. Service the page-fault interrupt.
2. Read in the page.
3. Restart the process.
The first and third tasks can be reduced, with careful coding, to several
hundred instructions. These tasks may take from 1 to 100 microseconds each.
Let’s consider the case of HDDs being used as the paging device. The pageswitch
time will probably be close to 8 milliseconds. (A typical hard disk has
an average latency of 3 milliseconds, a seek of 5 milliseconds, and a transfer
time of 0.05 milliseconds. Thus, the total paging time is about 8 milliseconds,
including hardware and software time.) Remember also that we are looking at
only the device-service time. If a queue of processes is waiting for the device,
we have to add queuing time as we wait for the paging device to be free to
service our request, increasing even more the time to page in.
With an average page-fault service time of 8 milliseconds and a memoryaccess
time of 200 nanoseconds, the effective access time in nanoseconds is
effective access time = (1 − p) × (200) + p (8 milliseconds)
= (1 − p) × 200 + p × 8,000,000
= 200 + 7,999,800 × p.
We see, then, that the effective access time is directly proportional to the
page-fault rate. If one access out of 1,000 causes a page fault, the effective access
time is 8.2 microseconds. The computer will be slowed down by a factor of 40
because of demand paging! If we want performance degradation to be less than
10 percent, we need to keep the probability of page faults at the following level:
220 > 200 + 7,999,800 × p,
20 > 7,999,800 × p,
p < 0.0000025.
That is, to keep the slowdown due to paging at a reasonable level,we can allow
fewer that one memory access out of 399,990 to page-fault. In sum, it is important
to keep the page-fault rate low in a demand-paging system. Otherwise,
the effective access time increases, slowing process execution dramatically.
An additional aspect of demand paging is the handling and overall use of
swap space. I/O to swap space is generally faster than that to the file system. It
is faster because swap space is allocated in much larger blocks, and file lookups
and indirect allocation methods are not used (Chapter 11). One option for the system to gain better paging throughput is by copying an entire file image into
the swap space at process startup and then performing demand paging from
the swap space. The obvious disadvantage of this approach is the copying of
the file image at program start-up. A second option—and one practiced by
several operating systems, including Linux andWindows—is to demand-page
from the file system initially but to write the pages to swap space as they are
replaced. This approach will ensure that only needed pages are read from the
file system but that all subsequent paging is done from swap space.
Some systems attempt to limit the amount of swap space used through
demand paging of binary executable files. Demand pages for such files are
brought directly from the file system. However, when page replacement is
called for, these frames can simply be overwritten (because they are never
modified), and the pages can be read in from the file system again if needed.
Using this approach, the file system itself serves as the backing store. However,
swap space must still be used for pages not associated with a file (known as
anonymous memory); these pages include the stack and heap for a process.
This method appears to be a good compromise and is used in several systems,
including Linux and BSD UNIX.
As described in Section 9.5.3, mobile operating systems typically do not
support swapping. Instead, these systems demand-page from the file system
and reclaim read-only pages (such as code) from applications if memory
becomes constrained. Such data can be demand-paged from the file system if
it is later needed. Under iOS, anonymous memory pages are never reclaimed
from an application unless the application is terminated or explicitly releases
the memory. In Section 10.7, we cover compressed memory, a commonly used
alternative to swapping in mobile systems.
Copy-on-Write
In Section 10.2, we illustrated how a process can start quickly by demandpaging
in the page containing the first instruction. However, process creation
using the fork() system call may initially bypass the need for demand paging
by using a technique similar to page sharing (covered in Section 9.3.4). This
technique provides rapid process creation and minimizes the number of new
pages that must be allocated to the newly created process.
Recall that the fork() system call creates a child process that is a duplicate
of its parent. Traditionally, fork() worked by creating a copy of the parent’s
address space for the child, duplicating the pages belonging to the parent.
However, considering that many child processes invoke the exec() system call
immediately after creation, the copying of the parent’s address space may be
unnecessary. Instead, we can use a technique known as copy-on-write, which
works by allowing the parent and child processes initially to share the same
pages. These shared pages are marked as copy-on-write pages, meaning that
if either process writes to a shared page, a copy of the shared page is created.
Copy-on-write is illustrated in Figures 10.7 and 10.8, which show the contents
of the physical memory before and after process 1 modifies page C.
For example, assume that the child process attempts to modify a page
containing portions of the stack, with the pages set to be copy-on-write. The
operating system will obtain a frame from the free-frame list and create a copy
of this page, mapping it to the address space of the child process. The child
process will then modify its copied page and not the page belonging to the
parent process. Obviously, when the copy-on-write technique is used, only the
pages that are modified by either process are copied; all unmodified pages
can be shared by the parent and child processes. Note, too, that only pages
that can be modified need be marked as copy-on-write. Pages that cannot
be modified (pages containing executable code) can be shared by the parent
and child. Copy-on-write is a common technique used by several operating
systems, including Windows, Linux, and macOS.
Several versions of UNIX (including Linux, macOS, and BSD UNIX) provide
a variation of the fork() system call—vfork() (for virtual memory fork)—
that operates differently from fork() with copy-on-write. With vfork(), the
parent process is suspended, and the child process uses the address space of
the parent. Because vfork() does not use copy-on-write, if the child process
changes any pages of the parent’s address space, the altered pages will be
visible to the parent once it resumes. Therefore, vfork() must be used with
caution to ensure that the child process does not modify the address space of
the parent. vfork() is intended to be used when the child process calls exec()
immediately after creation. Because no copying of pages takes place, vfork() is an extremely efficient method of process creation and is sometimes used to
implement UNIX command-line shell interfaces.
Page Replacement
In our earlier discussion of the page-fault rate, we assumed that each page
faults at most once,when it is first referenced. This representation is not strictly
accurate, however. If a process of ten pages actually uses only half of them, then
demand paging saves the I/O necessary to load the five pages that are never
used. We could also increase our degree of multiprogramming by running
twice as many processes. Thus, if we had forty frames, we could run eight
processes, rather than the four that could run if each required ten frames (five
of which were never used).
If we increase our degree of multiprogramming, we are over-allocating
memory. If we run six processes, each of which is ten pages in size but actually
uses only five pages, we have higher CPU utilization and throughput, with
ten frames to spare. It is possible, however, that each of these processes, for
a particular data set, may suddenly try to use all ten of its pages, resulting in a
need for sixty frames when only forty are available.
Further, consider that system memory is not used only for holding program
pages. Buffers for I/O also consume a considerable amount of memory. This use
can increase the strain on memory-placement algorithms. Deciding how much
memory to allocate to I/O and how much to program pages is a significant
challenge. Some systems allocate a fixed percentage of memory for I/O buffers,
whereas others allow both processes and the I/O subsystem to compete for all
system memory. Section 14.6 discusses the integrated relationship between I/O
buffers and virtual memory techniques.
Over-allocation of memory manifests itself as follows. While a process is
executing, a page fault occurs. The operating system determines where the
desired page is residing on secondary storage but then finds that there are
no free frames on the free-frame list; all memory is in use. This situation is
illustrated in Figure 10.9, where the fact that there are no free frames is depicted
by a question mark.
The operating system has several options at this point. It could terminate
the process. However, demand paging is the operating system’s attempt to
improve the computer system’s utilization and throughput. Users should not
be aware that their processes are running on a paged system—paging should
be logically transparent to the user. So this option is not the best choice.
The operating system could instead use standard swapping and swap
out a process, freeing all its frames and reducing the level of multiprogramming.
However, as discussed in Section 9.5, standard swapping is no longer
used by most operating systems due to the overhead of copying entire processes
between memory and swap space. Most operating systems now combine
swapping pages with page replacement, a technique we describe in detail
in the remainder of this section.
Basic Page Replacement
Page replacement takes the following approach. If no frame is free,we find one
that is not currently being used and free it. We can free a frame by writing its contents to swap space and changing the page table (and all other tables) to
indicate that the page is no longer in memory (Figure 10.10). We can now use
the freed frame to hold the page for which the process faulted.We modify the
page-fault service routine to include page replacement:
1. Find the location of the desired page on secondary storage.
2. Find a free frame:
a. If there is a free frame, use it.
b. If there is no free frame, use a page-replacement algorithm to select
a victim frame.
c. Write the victim frame to secondary storage (if necessary); change
the page and frame tables accordingly.
3. Read the desired page into the newly freed frame; change the page and
frame tables.
4. Continue the process from where the page fault occurred.
Notice that, if no frames are free, two page transfers (one for the page-out
and one for the page-in) are required. This situation effectively doubles the
page-fault service time and increases the effective access time accordingly.
We can reduce this overhead by using a modify bit (or dirty bit). When this
scheme is used, each page or frame has a modify bit associated with it in the
hardware. The modify bit for a page is set by the hardware whenever any byte
in the page is written into, indicating that the page has been modified. When
we select a page for replacement, we examine its modify bit. If the bit is set, we know that the page has been modified since it was read in from secondary
storage. In this case, we must write the page to storage. If the modify bit is not
set, however, the page has not been modified since itwas read intomemory. In
this case,we need notwrite thememory page to storage: it is already there. This
technique also applies to read-only pages (for example, pages of binary code).
Such pages cannot be modified; thus, they may be discarded when desired.
This scheme can significantly reduce the time required to service a page fault,
since it reduces I/O time by one-half if the page has not been modified.
Page replacement is basic to demand paging. It completes the separation
between logicalmemory and physicalmemory.With this mechanism, an enormous
virtual memory can be provided for programmers on a smaller physical
memory.With no demand paging, logical addresses are mapped into physical
addresses, and the two sets of addresses can be different. All the pages of a
process still must be in physical memory, however. With demand paging, the
size of the logical address space is no longer constrained by physical memory.
If we have a process of twenty pages, we can execute it in ten frames simply by
using demand paging and using a replacement algorithm to find a free frame
whenever necessary. If a page that has been modified is to be replaced, its
contents are copied to secondary storage. A later reference to that page will
cause a page fault. At that time, the page will be brought back into memory,
perhaps replacing some other page in the process.
We must solve two major problems to implement demand paging: we must
develop a frame-allocation algorithm and a page-replacement algorithm.
That is, if we have multiple processes in memory, we must decide how many
frames to allocate to each process; and when page replacement is required,
we must select the frames that are to be replaced. Designing appropriate algorithms
to solve these problems is an important task, because secondary storage I/O is so expensive. Even slight improvements in demand-paging methods
yield large gains in system performance.
There are many different page-replacement algorithms. Every operating
system probably has its own replacement scheme. How do we select a particular
replacement algorithm? In general, we want the one with the lowest
page-fault rate.
We evaluate an algorithm by running it on a particular string of memory
references and computing the number of page faults. The string of memory
references is called a reference string. We can generate reference strings artificially
(by using a random-number generator, for example), or we can trace
a given system and record the address of each memory reference. The latter
choice produces a large number of data (on the order of 1 million addresses
per second). To reduce the number of data, we use two facts.
First, for a given page size (and the page size is generally fixed by the hardware
or system), we need to consider only the page number, rather than the
entire address. Second, if we have a reference to a page p, then any references
to page p that immediately follow will never cause a page fault. Page p will
be in memory after the first reference, so the immediately following references
will not fault.
For example, ifwe trace a particular process, we might record the following
address sequence:
0100, 0432, 0101, 0612, 0102, 0103, 0104, 0101, 0611, 0102, 0103,
0104, 0101, 0610, 0102, 0103, 0104, 0101, 0609, 0102, 0105
At 100 bytes per page, this sequence is reduced to the following reference
string:
1, 4, 1, 6, 1, 6, 1, 6, 1, 6, 1
To determine the number of page faults for a particular reference string and
page-replacement algorithm, we also need to know the number of page frames
available. Obviously, as the number of frames available increases, the number
of page faults decreases. For the reference string considered previously, for
example, if we had three or more frames, we would have only three faults—
one fault for the first reference to each page. In contrast, with only one frame
available, we would have a replacement with every reference, resulting in
eleven faults. In general, we expect a curve such as that in Figure 10.11. As the
number of frames increases, the number of page faults drops to some minimal
level. Of course, adding physical memory increases the number of frames.
We next illustrate several page-replacement algorithms. In doing so, we
use the reference string
7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
for a memory with three frames.
FIFO Page Replacement
The simplest page-replacement algorithm is a first-in, first-out (FIFO) algorithm.
AFIFO replacement algorithm associates with each page the time when
that page was brought into memory.When a page must be replaced, the oldest
page is chosen. Notice that it is not strictly necessary to record the time when a page is brought in.We can create a FIFO queue to hold all pages inmemory.We
replace the page at the head of the queue.When a page is brought into memory,
we insert it at the tail of the queue.
For our example reference string, our three frames are initially empty. The
first three references (7, 0, 1) cause page faults and are brought into these empty
frames. The next reference (2) replaces page 7, because page 7 was brought in
first. Since 0 is the next reference and 0 is already in memory, we have no fault
for this reference. The first reference to 3 results in replacement of page 0, since
it is now first in line. Because of this replacement, the next reference, to 0, will
fault. Page 1 is then replaced by page 0. This process continues as shown in
Figure 10.12. Every time a fault occurs, we show which pages are in our three
frames. There are fifteen faults altogether.
The FIFO page-replacement algorithm is easy to understand and program.
However, its performance is not always good. On the one hand, the page
replaced may be an initialization module that was used a long time ago and is
no longer needed. On the other hand, it could contain a heavily used variable
that was initialized early and is in constant use.
Notice that, even if we select for replacement a page that is in active use,
everything still works correctly. After we replace an active page with a new one, a fault occurs almost immediately to retrieve the active page. Some other
page must be replaced to bring the active page back into memory. Thus, a bad
replacement choice increases the page-fault rate and slows process execution.
It does not, however, cause incorrect execution.
To illustrate the problems that are possible with a FIFO page-replacement
algorithm, consider the following reference string:
1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5
Figure 10.13 shows the curve of page faults for this reference string versus the
number of available frames. Notice that the number of faults for four frames
(ten) is greater than the number of faults for three frames (nine)! This most
unexpected result is known as Belady’s anomaly: for some page-replacement
algorithms, the page-fault rate may increase as the number of allocated frames
increases. We would expect that giving more memory to a process would
improve its performance. In some early research, investigators noticed that this
assumption was not always true. Belady’s anomaly was discovered as a result.
Optimal Page Replacement
One result of the discovery of Belady’s anomaly was the search for an optimal
page-replacement algorithm—the algorithm that has the lowest page-fault
rate of all algorithms and will never suffer from Belady’s anomaly. Such an
algorithm does exist and has been called OPT or MIN. It is simply this:
Replace the page that will not be used for the longest period of time.
Use of this page-replacement algorithm guarantees the lowest possible pagefault
rate for a fixed number of frames.
For example, on our sample reference string, the optimal page-replacement
algorithmwould yield nine page faults, as shown in Figure 10.14. The first three
references cause faults that fill the three empty frames. The reference to page
2 replaces page 7, because page 7 will not be used until reference 18, whereas
page 0 will be used at 5, and page 1 at 14. The reference to page 3 replaces
page 1, as page 1 will be the last of the three pages in memory to be referenced
again. With only nine page faults, optimal replacement is much better than
a FIFO algorithm, which results in fifteen faults. (If we ignore the first three,
which all algorithms must suffer, then optimal replacement is twice as good as
FIFO replacement.) In fact, no replacement algorithmcan process this reference
string in three frames with fewer than nine faults.
Unfortunately, the optimal page-replacement algorithm is difficult to
implement, because it requires future knowledge of the reference string.
(We encountered a similar situation with the SJF CPU-scheduling algorithm in
Section 5.3.2.) As a result, the optimal algorithm is used mainly for comparison
studies. For instance, it may be useful to know that, although a new algorithm
is not optimal, it is within 12.3 percent of optimal at worst and within 4.7
percent on average.
LRU Page Replacement
If the optimal algorithm is not feasible, perhaps an approximation of the optimal
algorithm is possible. The key distinction between the FIFO and OPT algorithms
(other than looking backward versus forward in time) is that the FIFO
algorithm uses the time when a page was brought into memory, whereas the
OPT algorithm uses the time when a page is to be used. If we use the recent past
as an approximation of the near future, then we can replace the page that has
not been used for the longest period of time. This approach is the least recently
used (LRU) algorithm.
LRU replacement associates with each page the time of that page’s last use.
When a page must be replaced, LRU chooses the page that has not been used
for the longest period of time. We can think of this strategy as the optimal
page-replacement algorithm looking backward in time, rather than forward.
(Strangely, if we let SR be the reverse of a reference string S, then the page-fault
rate for the OPT algorithm on S is the same as the page-fault rate for the OPT
algorithm on SR. Similarly, the page-fault rate for the LRU algorithm on S is the
same as the page-fault rate for the LRU algorithm on SR.)
The result of applying LRU replacement to our example reference string is
shown in Figure 10.15. The LRU algorithm produces twelve faults. Notice that
the first five faults are the same as those for optimal replacement. When the
reference to page 4 occurs, however, LRU replacement sees that, of the three
frames in memory, page 2 was used least recently. Thus, the LRU algorithm
replaces page 2, not knowing that page 2 is about to be used. When it then faults
for page 2, the LRU algorithm replaces page 3, since it is now the least recently
used of the three pages in memory. Despite these problems, LRU replacement
with twelve faults is much better than FIFO replacement with fifteen.
The LRU policy is often used as a page-replacement algorithm and is considered
to be good. The major problem is how to implement LRU replacement.
An LRU page-replacement algorithm may require substantial hardware assistance.
The problem is to determine an order for the frames defined by the time
of last use. Two implementations are feasible:
* Counters. In the simplest case, we associate with each page-table entry a
time-of-use field and add to the CPU a logical clock or counter. The clock is
incremented for every memory reference.Whenever a reference to a page
is made, the contents of the clock register are copied to the time-of-use
field in the page-table entry for that page. In this way, we always have
the “time” of the last reference to each page. We replace the page with the
smallest time value. This scheme requires a search of the page table to find
the LRU page and a write to memory (to the time-of-use field in the page
table) for each memory access. The times must also be maintained when
page tables are changed (due to CPU scheduling). Overflow of the clock
must be considered.
* Stack. Another approach to implementing LRU replacement is to keep a
stack of page numbers.Whenever a page is referenced, it is removed from
the stack and put on the top. In this way, the most recently used page is
always at the top of the stack, and the least recently used page is always
at the bottom (Figure 10.16). Because entries must be removed from the
middle of the stack, it is best to implement this approach by using a doubly
linked list with a head pointer and a tail pointer. Removing a page and
putting it on the top of the stack then requires changing six pointers at
worst. Each update is a little more expensive, but there is no search for
a replacement; the tail pointer points to the bottom of the stack, which is
the LRU page. This approach is particularly appropriate for software or
microcode implementations of LRU replacement.
Like optimal replacement, LRU replacement does not suffer from Belady’s
anomaly. Both belong to a class of page-replacement algorithms, called stack
algorithms, that can never exhibit Belady’s anomaly. A stack algorithm is an
algorithm for which it can be shown that the set of pages in memory for n
frames is always a subset of the set of pages that would be in memory with n
+ 1 frames. For LRU replacement, the set of pages in memory would be the n
most recently referenced pages. If the number of frames is increased, these n
pages will still be the most recently referenced and so will still be in memory.
Note that neither implementation of LRU would be conceivable without
hardware assistance beyond the standard TLB registers. The updating of the
clock fields or stack must be done for every memory reference. If we were
to use an interrupt for every reference to allow software to update such data
structures, it would slow every memory reference by a factor of at least ten,
hence slowing every process by a factor of ten. Few systems could tolerate that
level of overhead for memory management.
LRU-Approximation Page Replacement
Not many computer systems provide sufficient hardware support for true LRU
page replacement. In fact, some systems provide no hardware support, and
other page-replacement algorithms (such as a FIFO algorithm) must be used.
Many systems provide some help, however, in the form of a reference bit. The
reference bit for a page is set by the hardware whenever that page is referenced
(either a read or a write to any byte in the page). Reference bits are associated
with each entry in the page table.
Initially, all bits are cleared (to 0) by the operating system. As a process
executes, the bit associated with each page referenced is set (to 1) by the
hardware. After some time, we can determine which pages have been used and
which have not been used by examining the reference bits, although we do not
know the order of use. This information is the basis for many page-replacement
algorithms that approximate LRU replacement.