-
Program segments (e.g., the code segment) must be loaded into
contiguous memory.
-
Consecutive memory locations are required because the compiler
generates code for "sequential statements":
- sequential statements
- "if statments"
- "loops" that generate
jump or branch instructions to addresses
all expect instructions to be stored in consecutive
memory locations.
-
In particular, when an instruction is fetched from the memory address
in the PC register (%eip on Intel IA32), the PC is then incremented. This assumes
that the next instruction is in the next memory address.
Programs must therefore be loaded into contiguous memory blocks large enough to
hold the entire program.
For an operating system embedded in a single purpose device with no external storage, this may be ok.
In a multiprogramming operating system, many processes will be loaded
into memory.
Memory allocation occurs when a process is created and
deallocation occurs when the process terminates.
However, the order of
deallocation is not predictable from the order of allocation!
So in general, there must be a free list of the memory blocks
not in use and available for allocation.
This list needs to keep
track of the beginning address of each free memory block and the size
of the block.
Memory allocation will select a free memory block (or part of one)
which is big enough for the memory request and remove
the block (or part of it) from the free list.
Process termination requires returning the memory to the free list.
Internal fragmentation is memory that is allocated to a process
but not used (for data or code).
This may occur if memory is allocated in minimum units such as
multiples of 4K bytes. A program that requries 150K + 1 bytes would be
allocated 154K bytes. Of this 4095 bytes (4k - 1 bytes) would be
internal fragmentation.
Internal fragmentation represents some inefficiency in the use of memory.
External fragmentation is free memory that is broken into
contiguous memory blocks so small that they cannot contain any program
and so cannot be allocated.
Three typical memory allocation algorithms are:
First Fit Memory blocks are ordered.
Choose the first block large enough.
Best Fit Choose the smallest free block which is large
enough.
Worst Fit Choose the largest block.
In all cases, if the chosen free block is not the exact size, split
off the amount needed, leaving a smaller free block of the extra
amount. This eliminates all internal fragmentation.
Unfortunately, these allocation strategies cannot avoid external fragmentation.
A memory management system is working with a total memory size of
700K and uses contiguous allocation and variable sized partitions so
that no internal fragmentation occurs. It currently has three free
blocks available. Their sizes are 50K, 155K, and 100K, respectively,
and their location in the memory is as below. The order of the free
list is just the increasing order of the beginning addresses of the
free blocks.
0 --- increasing addresses --> 700K
+-------+-----+------+------+------+------+------+
| used | 50K | used | 155K | used | 100K | used |
+-------+-----+------+------+------+------+------+
- Show how First-Fit would allocate memory to 3 new processes of
size 90K, 100K, and 60K (in that order).
- Show how Best-Fit would allocate memory for the same three
processes.
Request | First-Fit | Best-Fit |
| 50,155,100 | 50,155,100 |
90 | 50,65,100 | 50,155,10 |
100 | 50,65 | 50,55,10 |
60 | 50, 5 | wait |
At the third request (for 60K), both First Fit and Best Fit has a
free list with a total size of 115K, more that enough. However, for
Best Fit this 115K is broken (fragmented) into three pieces of size
50K, 55K, and 10K. None of these 3 blocks is big enough for the
request. So the request must wait until some process terminates and
releases more memory.
Memory must be allocated in contiguous memory blocks large enough to
hold the entire program (or at least each entire segment - code, data, etc.)
Internal and external memory fragmentation can result. Each of these
represents unused memory which limits the number and/or size of
simultaneous processes.
Memory allocation algorithms that split off the exact memory request
size from free blocks can eliminate internal fragmentation.
However, these algorithms (first fit, best fit, worst fit) all
suffer from external fragmentation.
The memory model using an implicit free list consists of
- prolog block
- regular blocks (some free, some allocated)
- epilog block
Prolog block |
|
Epilog block |
hd |
ft |
hd |
free |
ft |
hd |
alloc |
ft |
hd |
alloc |
ft |
hd |
free |
ft |
hd |
ft |
All blocks have a 4 8 byte header and
a 4 8 byte footer
The header and footer have the same value.
The value is in two parts:
The block size is in the upper 29 60 bits of the header/footer
and the 3 4 lower bits of the size are implied and are 0. This
means the block size is a multiple of 8 16.
The payload is the portion of the block between the
header and footer.
The payload must begin at an 8 16 byte address boundary; that
is, the payload address must be a multiple of 8
16.
The prolog block has a header and a footer and an empty payload and so the size of the
prolog block is 8 16.
The prolog block is marked as allocated.
So in binary, the header is
0000 0000 0000 0000 .... 0000 0000 0001 0001
As an integer, this looks like 17, but the lower order 4 bits
are not part of the size. The size has 4 implied (not stored) low
order bits equal to 0:
0000 0000 0000 0000 0000 0000 00010000
which is the value 16
It is convenient to write the header value as 16/1:
size 16 and allocated bit = 1 or 16/0 for a free block of
size 16..
The epilog block also only has a header and footer with empty
payload.
The header value for the eplilog block is
0/1
size 0 and allocated bit = 1.
The actual size of the epilog is really 8 (no footer), but setting the
size value in the header to 0 makes it unique among all blocks
and so it can be used to detect the end of the heap area when
searching the blocks.
The functions in the memlib.c file for managing the heap:
- void mem_init(int size)
- void * mem_sbrk(int incr)
- void * mem_heap_lo()
- void * mem_heap_hi()
- size_t mem_heapsize()
You should not change anything in these functions.
The mm.c file is the only file you will change.
You make work in teams of 1 or at most 2 people.
This file has a C struct in which you should insert your name
(or names if two people are working together).
You can omit the email addresses:
team_t team = {
/* Team name */
"asmith+asaini",
/* First member's full name */
"Afred Smith",
/* First member's email address */
"",
/* Second member's full name (leave blank if none) */
"Atul Saini",
/* Second member's email address (leave blank if none) */
""
};
- void * mm_malloc(size_t sz)
- void mm_free(void *ptr)
- mm_realloc(void * ptr, size_t sz)
but mm_init is called first and then these call other
functions. You may also need to add some
functions and some macros!
- int mm_init(void)
- void *find_fit(size_t asize)
- void place(void *bp, size_t asize)
- void *extend_heap(size_t words)
- void *coalesce(void *bp)
- void mm_checkheap(int verbose)
A very simple initial version of mm_malloc allocates 8 extra bytes
for the header and footer in addition to the payload:
/*
* mm_malloc - Allocate a block by incrementing the brk pointer.
* Always allocate a block whose size is a multiple of the
alignment.
*/
void *mm_malloc(size_t size)
{
int newsize = ALIGN(size + 8);
void *p = mem_sbrk(newsize);
if ((int)p == -1)
return NULL;
else {
*(size_t *)p = (newsize | 1);
*(size_t *)(p + newsize - 4) = (newsize | 1);
return (void *)(p + 4);
}
}
A correspondingly simple version of mm_free is:
/*
* mm_free - Freeing a block does nothing.
*/
void mm_free(void *ptr)
{
}
The text has a much better version of mm_malloc (page
750).
This file is also available in the file mm.c provided in
the tar file for lab2.
The initial versions of mm_malloc, mm_free, and mm_realloc are
provided in the file mm.c. Here is that version of mm_malloc:
1 void *mm_malloc(size_t size)
2 {
3 size_t asize; /* adjusted block size */
4 size_t extendsize; /* amount to extend heap if no fit */
5 char *bp;
6
7 /* Ignore spurious requests */
8 if (size <= 0)
9 return NULL;
10
11 /* Adjust block size to include overhead and alignment reqs. */
12 if (size <= DSIZE)
13 asize = DSIZE + OVERHEAD;
14 else
15 asize = DSIZE * ((size + (OVERHEAD) + (DSIZE-1)) / DSIZE);
16
17 /* Search the free list for a fit */
18 if ((bp = find_fit(asize)) != NULL) {
19 place(bp, asize);
20 return bp;
21 }
22
23 /* No fit found. Get more memory and place the block */
24 extendsize = MAX(asize,CHUNKSIZE);
25 if ((bp = extend_heap(extendsize/WSIZE)) == NULL)
26 return NULL;
27 place(bp, asize);
28 return bp;
29 }
The file mm.c with the "better" version of mm_malloc also
contans some auxilliary functions you can use including:
- extend_heap (p 748)
- coalesce (p 749)
static void *extend_heap(size_t words)
{
char *bp;
size_t size;
/* Allocate an even number of words to maintain alignment */
size = (words % 2) ? (words+1) * WSIZE : words * WSIZE;
if ((int)(bp = mem_sbrk(size)) < 0)
return NULL;
/* Initialize free block header/footer and the epilogue header */
PUT(HDRP(bp), PACK(size, 0)); /* free block header */
PUT(FTRP(bp), PACK(size, 0)); /* free block footer */
PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1)); /* new epilogue header */
/* Coalesce if the previous block was free */
return coalesce(bp);
}
static void *coalesce(void *bp)
Handles the 4 cases that arise depending on whether the two
neighbors of the block to be freed are themselves free or allocated.
Frees the block pointed to by bp in constant time and returns
a pointer to the free block after it has been merged (coalesced)
with surrounding blocks if they were already free.
Here is the corresponding mm_free:
1 void mm_free(void *bp)
2 {
3 size_t size = GET_SIZE(HDRP(bp));
4
5 PUT(HDRP(bp), PACK(size, 0));
6 PUT(FTRP(bp), PACK(size, 0));
7 coalesce(bp);
8 }
The find_fit function in this initial version is the same as
the practic problem in
chapter 10:
static void *find_fit(size_t asize)
{
void *bp;
/* first fit search */
for (bp = heap_listp; GET_SIZE(HDRP(bp)) > 0; bp = NEXT_BLKP(bp)) {
if (!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp)))) {
return bp;
}
}
return NULL; /* no fit */
}
When the find_fit returns a pointer to a free block that is
big enough, it should be usually be split into the size
requested and the rest should be a smaller free block.
The size of the part allocated should be adjusted up if
smaller than some minimum size, and rounded so that the 8 byte
boundary alignment is satisfied.
In the "better" malloc.c file, the place function
handles the splitting of a block of size csize
in order to satisfy a request of size asize:
static void place(void *bp, size_t asize)
{
size_t csize = GET_SIZE(HDRP(bp));
if ((csize - asize) >= (DSIZE + OVERHEAD)) {
PUT(HDRP(bp), PACK(asize, 1));
PUT(FTRP(bp), PACK(asize, 1));
bp = NEXT_BLKP(bp);
PUT(HDRP(bp), PACK(csize-asize, 0));
PUT(FTRP(bp), PACK(csize-asize, 0));
}
else {
PUT(HDRP(bp), PACK(csize, 1));
PUT(FTRP(bp), PACK(csize, 1));
}
}
The Makefile will allow you to build an executable named
mdriver which is compiled with
- mdriver.c
- memlib.c
- mm.c (Your functions)
- several files for timing
Each time you modify mm.c, you should recompile mdriver:
make
Your heap management functions (mm_maloc, mm_free, and
mm_realloc) will be tested on 11 trace files
consisting of heap memory requests.
Some of these trace files come from actual programs, while
some have been artificially constructed.
Some the files are relatively large so rather having a copy
in every user's directory, they are located in the readable directory:
~glancast/374class/malloclab/traces
The makefile, Makefile, has a target named test, test01,
test02, ... test11.
To run tests, type
- Run all tests
make test
- Run only the 3rd test
make test03
- Run the n-th test (n = 01 to 11)
make testn
The malloc.c file contains a partial check that a pointer is
actually pointing to the "payload" portion of a block on the
heap.
static void checkblock(void *bp)
{
if ((size_t)bp % 8)
printf("Error: %p is not doubleword aligned\n", bp);
if (GET(HDRP(bp)) != GET(FTRP(bp)))
printf("Error: header does not match footer\n");
}
The void
mm_free(void *bp)
function should also check that the pointer bp is
actually pointing to the "payload" of an allocated
block on the heap before executing code to free it.
Otherwise, the heap could become corrupted!
The problem with the implicit free list is that searches for
a free block of the right size must examine allocated as well
as free blocks.
One improvement would be to have an explicit
list consisting of free blocks only.
So we should review options for constructing linked
lists.
- Each free block could contain the address of the next free
block - the "next" link. (singly linked list)
- Should we also have a "prev" link? (doubly linked list)
- How do we know if the list is empty?
- Should we have head node and/or and trailer node?
- Should the list be circular?
If a linked list is doubly linked, then deleting a
block pointed to by bp takes constant time:
after = NEXT_FREE(bp);
before = PREV_FREE(bp);
NEXT_FREE(before) = after;
PREV_FREE(after) = before;
We would need to write these two macros:
The code for deleting block from the free list above will not
work if the free list is empty! So check first.
However, the code make also not work at the two ends of the
heap or if there is only 1 free block left on the free list!
You could check for those special cases.
Adding special nodes (blocks) to mark the beginning and the
end of a linked list can make the code above work in all
cases.
With Header and Trailer "free" blocks, every actual free
block will have a previous block and a next block and
so the deletion code will work in all cases including the
problem boundary cases.
Instead of having both a Header and a Trailer Node, you can
get by with only a single Header Node if you make the list be
circular.
The last free block will have its next block be the Head
"free" block.
The Head node will have its previous block be the last free
block.
If the free list is empty, you still have a Head "free"
node.
For an empty free list, the Head "free" node will have its
hext and its prev equal to itself.
Using a pointer with value 0 (NULL pointer value) to access
memory will cause a segmentation fault.
If a doubly linked circular list with Header Node is used, no
links in the free blocks will ever be NULL.
To maintain the free blocks in an explicit free list, a
doubly linked circular list with header has advantages over
other choices. But...
- The Head Nodes need to be in the heap along with the free
blocks.
- Finding a suitable free block to use will only require
looking at free blocks, not allocated blocks.
- Deletion operations can be constant
time once the right free block to use has been found.
- Insertion of a block onto the free list can be constant
time, if it is simply inserted at the beginning of the
explicit free list.
First Fit and Next Fit are both easy to implement once the
linked list manipulation code is available.
Best Fit or worst fit require more coding and more time to
keep the free list sorted.
Alternatively some data structure other than a linked list
might be more efficient for best fit or worst fit.
Where are insertions into the free list?
LIFO
If insertions are made at the front, the time is constant
independent of the number of free blocks. Fast!
Recently freed blocks will be the first candidates to be reused.
Address Order
It is more work to keep the list of free blocks in order by
their addresses.
However, it is claimed that this results in less external
memory fragmentation.