Operating Systems: Three Easy Pieces学习笔记 - Virtualization - Free-Space Management

Free-Space Management

When the space that is managing consists of variable-sized units; this arises in a user-level memory-allocation library (as in malloc() and free()) and in an OS managing physical memory when using segmentation to implement virtual memory. Under this circumstance, external fragmentation occurs and subsequent requests may fail because there is no single contiguous space that can satisfy the request.
Crux: How to manage free space?

Assumptions

  • Two interfaces provided by a library: void *malloc(size_t size) and void free(void *ptr). Note that when freeing the space, the user does not have to inform the library of its size; the library must be able to figure how big a chunk of memory is when handed just a pointer to it.
  • The space that the library manages is the heap, and the generic data structure used to manage free space in the heap is some kind of free list. This structure contains references to all of the free chunks of space in the managed region of memory.
  • Once memory is handed out to a client, it cannot be relocated to another location in memory until the program returns it via a corresponding call to free(). Thus no compaction of free space is possible.
  • The allocator manages a contiguous region of bytes.

Low-level Mechanisms

Splitting and Coalescing

Assume the following 30-byte heap:

image.png

The free list for this heap should look like:
image.png

If there is a request for less than 10 bytes (1 byte, for example), the allocator will perform an action called splitting: it will find a free chunk of memory that can satisfy the request and split it into two. The first chunk it will return to the caller; the second chunk will remain on the list. In this example, the called to malloc() would return 20 (the address of the 1-byte allocated region) and the list would end up looking like this:
image.png

Take our example from above once more (free 10 bytes,
used 10 bytes, and another free 10 bytes). If an application calls free(10), the allocator would perform an action called coalescing, that is, when returning a free chunk in memory, look carefully at the addresses of the chunk and the nearby chunks of free space; if the newly-freed space sits right next to one (or two) existing free chunks, merge them into a single larger free chunk. Therefore, if an application calls free(10), the final list should look like this:
image.png

With coalescing, an allocator can better ensure that large free extents are available for the application.

Tracking The Size Of Allocated Regions

In order that the library can automatically determine the size of the region of memory being freed, most allocators store a little bit of extra information in a header block which is kept in memory, usually just before the handed-out chunk of memory. Look at this example:

image.png

Imagine the user called malloc() and stored the results in ptr, e.g., ptr = malloc(20);
The header minimally contains the size of the allocated region (20, in this case); it may also contain additional pointers to speed up deallocation, a magic number to provide additional integrity checking, and other information. Here we consider a simple header:

typedef struct __header_t {
    int size;
    int magic;
} header_t;

The memory looks like this:

image.png

When the user calls free(ptr), the library then uses simple pointer arithmetic to figure out where the header begins:

void free(void *ptr) {
    header_t *hptr = (void *)ptr - sizeof(header_t);
}

After obtaining such a pointer to the header, the library can easily determine whether the magic number matches the expected value as a sanity check and calculate the total size of the newly-freed region via simple math (adding the size of the header to size of the region). Notice that the size of the free region is the size of the header plus the size of the space allocated to the user.

Embedding A Free List

Within the memory-allocation library, we need to build the list inside the free space itself.
Assume we have a 4096-byte chunk of memory to manage. To manage this as a free list, we first have to initialize said list; initially, the list should have one entry, of size 4096 (minus the header size). Here is the description of a node of the list:

typedef struct __node_t {
    int size;
    struct __node_t *next;
} node_t;

The following code initializes the heap and puts the first element of the free list inside that space. The heap is built within some free space acquired via a call to the system call mmap():

// mmap() returns a pointer to a chunk of free space
node_t *head = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
                    MAP_ANON|MAP_PRIVATE, -1, 0);
head->size = 4096 - sizeof(node_t);
head->next = NULL;

After running this code, the heap should look like this:

image.png

When a chunk of memory is requested, 100 bytes, for example, the library will first find a chunk that is large enough to accommodate the request; because there is only one free chunk (size: 4088), this chunk will be chosen. Then, the chunk will besplit into two: one chunk big enough to service the request (and header, as described above), and the remaining free chunk. Assuming an 8-byte header (an integer size and an integer magic number), the space in the heap now looks like what you see in Figure 17.4:
image.png

The library actually allocated 108 bytes. Then it returns a pointer ptr to the request, stashes the header information immediately before the allocated space for later use upon free(), and shrinks the one free node in the list to 3980 (4088 - 108) bytes. Note that the location of the head pointer has changed.
Now consider that there are three allocated regions:
image.png

When the application is going to free the second chunk, it calls free(16500) (the value 16500 is arrived upon by adding the start of the memory region, 16384, to the 108 of the previous chunk and the 8 bytes of the header for this chunk).
The library immediately figures out the size of the free region, and then adds the free chunk back onto the free list. Assuming we insert at the head of the free list, the space now looks like this:
image.png

Growing The Heap

Most traditional allocators start with a small-sized heap and then re- quest more memory from the OS when they run out. Typically, this means they make some kind of system call (e.g.,sbrk in most UNIX systems) to grow the heap, and then allocate the new chunks from there.

Basic Strategies

There is no "best" approach. Any particular strategy can do quite badly given the wrong set of inputs.

Best Fit

Idea: First, search through the free list and find chunks of free memory that are as big or bigger than the requested size. Then, return the one that is the smallest in that group of candidates. One pass through the free list is enough to find the correct block to return.
Pro: It tries to reduce wasted space
Con: Performing an exhaustive search for the correct free block is highly time-consuming. Best fit also produces a lot of small free chunks.

Worst Fit

Idea: Find the largest chunk and return the requested amount; keep the remaining (large) chunk on the free list.
Pro: It leaves big chunks free instead of lots of small chunks
Con: A full search of free space is required, which is as time-consuming as best fit. Most studies also show that it performs badly, leading to excess fragmentation while still having high overheads

First Fit

Idea: Find the first block that is big enough and returns the requested amount to the user. The remaining free space is kept free for subsequent requests.
Pro: No exhaustive search of all the free spaces, therefore this strategy is fast
Con: It sometimes pollutes the beginning of the free list with small free chunks, we can use address-based ordering to alleviate this issue

Next Fit

Idea: Keep an extra pointer to the location within the list where one was looking last. The idea is to spread the searches for free space throughout the list more uniformly, thus avoiding splintering of the beginning of the list
Pro: High speed, similar to first fit

Other Approaches

Segregated Lists

Idea: If a particular application has one (or a few) popular-sized request that it makes, keep a separate list just to manage objects of that size; all other requests are forwarded to a more general memory allocator.
Pro: Fragmentation is much less of a concern. Allocation and free requests can be served quite quickly when they are of the right size as no complicated search of a list is required

Buddy Allocation

In a system with a binary buddy allocator, free memory is first conceptually thought of as one big space of size 2^N. When a request for memory is made, the search for free space recursively divides free space by two until a block that is big enough to accommodate the request is found. At this point, the requested block is returned to the user. Here is an example in search for a 7KB block:

image.png

Notice that this strategy can suffer from internal fragmentation.
Freeing memory chunk in such a system is very simple and fast as it only has to recursively go up to the root of the tree to coalesce neighbor free buddies, or it goes up until a buddy is in use and thus cannot be coalesced. Only one bit is needed to determine whether a chunk is free or not.

你可能感兴趣的:(Operating Systems: Three Easy Pieces学习笔记 - Virtualization - Free-Space Management)