CS 537: Fall 2007
Programming Assignment 3: A "Better" Malloc


Due: Tuesday, November 20 at 9 pm.
This project must be implemented in C with your project partner

Test Script Results

To see how you compare to others in the class, link your library with the object file testmem-submit.o provided in ~cs537-1/public/Projects/P3. It operates very similarly to testmem.c, but has some of the command line options hardcoded in. It takes only three command line options: -S [0,1,2] -O [0,1] -P [0,1]. If your library works correctly, the times will be recorded and your login name anonymized. You can then check out these 12 web pages to see how you are doing relative to everyone else in the class. (BTW, "iktb" corresponds to the default C library malloc; therefore, there are no times for it for -P 1.)

Have fun!


Contents


Objectives

There are three objectives to this assignment:

  1. To understand the nuances of building a memory allocator.
  2. To understand the art of performance tuning for different workloads.
  3. To create a shared library.

Overview

In this project, you will be implementing a memory allocator for the heap of a user-level process. Your functions will be similar to those provided by malloc() and free(), but a little more interesting.

Memory allocators have two distinct tasks. First, the memory allocator asks the operating system to expand the heap portion of the process's address space by calling either sbrk or mmap. Second, the memory allocator doles out this memory to the calling process. This involves managing a free list of memory and finding a contiguous chunk of memory that is large enough for the user's request; when the user later frees memory, it is added back to this list.

This memory allocator is usually provided as part of a standard library and is not part of the OS. To be clear, the memory allocator operates entirely within the address space of a single process and knows nothing about which physical pages have been allocated to this process or the mapping from logical addresses to physical addresses.

When implementing this basic functionality in your project, we have a few guidelines. First, when requesting memory from the OS, you must use mmap (which we think is easier to use than sbrk). Second, although a real memory allocator requests more memory from the OS whenever it can't satisfy a request from the user, your memory allocator must call mmap only one time (when it is first initialized). Third, you are free to use any data structures you want to manage the free list as well as any policy for choosing a chunk of memory. Note that you will be graded partially on performance in this project, so think about these aspects very carefully.

Your memory allocator will be more interesting than the traditional malloc and free int that your allocator will be flexible in how the user can specify what memory should be freed. For comparison, the traditional malloc() and free() are defined as follows.

Your implementations of Mem_Alloc(int size) and Mem_Free(void *ptr) are identical, except the ptr passed to Mem_Free does not have to have been previously returned by Mem_Alloc; instead, ptr can point to any valid range of memory returned by Mem_Alloc. We will refer to this range as an "object". For example, the following code sequence is valid with your allocator, but not with the traditional malloc and free:
int *ptr;

// The returned memory object is between ptr and ptr+49
if ((ptr = (int *)Mem_Alloc(50 * sizeof(int))) == NULL) exit(1);

// Could replace 30 with any value from 0 to 49..
Mem_Free(ptr+30);  

Thus, in your implementation, you will need to have a more sophisticated data structure than the traditional malloc to track the regions of memory allocated by Mem_Alloc. Specifically, this data structure will allow you to efficiently map any address to the corresponding memory object or to determine that there is no corresponding object. Designing this data structure is up to you. You will also provide supporting functions, Mem_GetSize(void *ptr) and Mem_IsValid(void *ptr), described below; again, for both functions, ptr can point to any portion of a memory object.

Program Specifications

For this project, you will be implementing several different routines as part of a shared library. Note that you will not be writing a main() routine for the code that you handin (but you should implement one for your own testing). We have provided the prototypes for these functions in the file mem.h (which is available from ~cs537-1/public/Projects/P3); you should include this header file in your code to ensure that you are adhering to the specification exactly. You should not change mem.h in any way!

We now define each of these routines more precisely.

You must provide these routines in a shared library named "libmem.so". Placing the routines in a shared library instead of a simple object file makes it easier for other programmers to link with your code. There are further advantages to shared (dynamic) libraries over static libraries. When you link with a static library, the code for the entire library is merged with your object code to create your executable; if you link to many static libraries, your executable will be enormous. However, when you link to a shared library, the library's code is not merged with your program's object code; instead, a small amount of stub code is inserted into your object code and the stub code finds and invokes the library code when you execute the program. Therefore, shared libraries have two advantages: they lead to smaller executables and they enable users to use the most recent version of the library at run-time.

To create a shared library named libmem.so, use the following commands (assuming your library code is in a single file "mem.c"):

gcc -c -fpic mem.c
gcc -shared -o libmem.so mem.o
To link with this library, you simply specify the base name of the library with "-lmem" and the path to find the library "-L.".
gcc mymain.c -lmem -L. -o myprogram
Of course, these commands should be placed in a Makefile.

Before you run "myprogram", you will need to set the environment variable, LD_LIBRARY_PATH, so that the system can find your library at run-time. Assuming you always run myprogram from this same directory, you can use the command:

setenv LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:.

Unix Hints

We are providing you with a lot of flexibility in how you implement this project. In particular, you can choose any suitable data structure for tracking memory objects (i.e., mapping address ranges to the corresponding object) and any policy for choosing the memory allocated to each request. The place you don't have any flexilibity is: you must use mmap() for allocating more space on the heap for this process.

In project 2, you saw how a web server can use mmap to map a file into its address space; then, the server read the file by simply accessing the memory where that file had been mapped. In this project, you will use mmap to map zero'd pages (i.e., allocate new pages) into the address space of the calling process. Note there are a number of different ways that you can call mmap to achieve this same goal; we give one working example here:

    // open the /dev/zero device 
    int fd = open("/dev/zero", O_RDWR); 

    // size (in bytes) needs to be evenly divisible by the page size
    void *ptr = mmap(NULL, sizeOfRegion, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); 
    if (ptr == MAP_FAILED) { 
        perror("mmap"); 
        return -1;
    } 

    // close the device (don't worry, mapping should be unaffected) 
    close(fd); 
    return 0; 

Grading

Your implementation will be graded along three main axes: We will provide you with sample workloads so that you can optimize your memory allocator appropriately. Note that you should feel free to implement policies that adapt to the workload; that is, you can use a different policy when your memory allocator observes different requests. However, so that you tune your implementation to general workload characteristics (and not any specific anomalies in these workloads), the exact workload traces that we use for grading your project will be slightly different (e.g., the requested sizes or the order of requests will be changed slightly). The sample workloads are available from ~cs537-1/public/Projects/P3/testmem.c.

The following is a short description of the workloads. These descriptions should be sufficient for you to optimize your design. Each workload can be characterized by three properties: the size of the requested objects, the order of allocates and frees, and whether pointers are always to the beginning of the object or not.

The workloads vary the size of their requests in three different ways:

The workloads vary the order of allocates and frees in two different ways:

The workloads vary whether or not they use only the pointer returned by Mem_Alloc:

In summary, to test your code, we will construct 12 different workloads (3 * 2 * 2) that combine each of these characteristics.

Handing in your Code

Hand in your source code and a README file. We will create a directory ~cs537-1/handin/p2/NAME, where NAME is the login name of one person in your group.

You should copy all of your server source files (*.c and *.h) and a Makefile to your p3 handin directory. Do not submit any .o files.

In your README file you should have the following four sections:

After the deadline for this project, you will be prevented from making any changes in these directory. Remember: No late projects will be accepted!