CS360 Lecture notes -- Malloc Lecture #2

  • Jim Plank
  • Directory: /home/plank/cs360/notes/Malloc2
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Malloc2/lecture.html

    More on malloc() and free().

    Last class I discussed how malloc keeps a big buffer of memory in the heap and carves it up and doles it out whenever the user calls malloc(). If there is not enough room in the buffer for what the user desires, then sbrk() is called to get heap storage for a big enough buffer.

    Malloc actually works in a different way. What it really does is maintain a linked list of free memory. When malloc() is called, it looks on its list for a piece of memory that is big enough. If it finds one, then it removes that memory from the linked list and returns it to the user. When free() is called, the memory is put back on the linked list. Now, to be efficient, if there is a chunk of memory on the free list that much bigger than what is requested, then it breaks up that chunk into two chunks -- one which is the size of the request (padded to a multiple of 8), and the remainder. The remainder is put on the free list and the one the size of the request is returned to the user. This is the standard way that you view malloc -- it manages a free list of memory. Malloc() takes memory from the free list and gives it to the user, and free() gives memory back to the free list.

    Initially, the free list is empty. When the first malloc() is called, we call sbrk() to get a new chunk of memory for the free list. This memory is split up so that some is returned to the user, and the rest goes back onto the free list.

    Before going into an example, I should say something about how the free list is implemented. There will be a global variable malloc_head, which is the head of the free list. Initially, malloc_head is NULL. When malloc() is first called, sbrk() is called so that some memory can be put on the free list. The way to turn memory into a free list element is to use the first few bytes as a list structure. In other words, if you have a chunk of memory on the free list and that chunk always has at least 12 bytes, then you can treat the first twelve bytes of the memory as a list structure. I.e.:

    How do you do this? You set up a typedef like the following:
    typedef struct flist {
       int size;
       struct flist *flink;
       struct flist *blink;
    } *Flist;
    
    And when you need to treat a chunk of memory starting at location s (where s is a (char *) or (void *) or caddr_t) as a free list element, you cast it:
      Flist f;
      
      f = (Flist) s;
    

    So, you start off as follows. malloc_head == NULL. I.e. the free list is empty. Suppose the user calls malloc(16). Your malloc program sees that there is no memory on the free list, so it calls sbrk(8192) to get 8K of heap storage for free memory. Suppose sbrk(8192) returns 0x6100. Then you have the following view of memory:
              malloc_head == NULL
    
             |---------------|
             |               | 0x6100 (start of heap)
             |               | 0x6104
             |               | 0x6108
             |               | 0x610c
             |               | 0x6120
             |               |
                  .....      |
             |               |
             |---------------| 0x8100 (end of heap -- sbrk(0))
    
    To put this hunk of memory on the free list, you link it onto the list. Memory looks as follows:
              malloc_head == 0x6100
    
             |---------------|
             |    8192       | 0x6100 (start of heap)
             |    NULL       | 0x6104
             |    NULL       | 0x6108
             |               | 0x610c
             |               | 0x6110
             |               |
                  .....      | 
             |               |
             |---------------| 0x8100 (end of heap -- sbrk(0))
    
    You need to satisfy the user's request for 16 bytes. You split this chunk of memory into two chunks -- one that is 24 bytes (16 for the user and 8 for bookkeeping), and one that is the remaining 8192-24 = 8168 bytes. You put the latter chunk onto the free list, and return a pointer to the 16 bytes allocated for the user to the user:
              malloc_head == 0x6118
    
             |---------------|
             |     24        | 0x6100 (start of heap)
             |               | 0x6104
             |               | 0x6108  <-- beginning of 16 bytes for the user
             |               | 0x610c
             |               | 0x6110
             |               | 0x6114
             |     8168      | 0x6118
             |     NULL      | 0x611c
             |     NULL      | 0x6120
                  .....      | 
             |               |
             |---------------| 0x8100 (end of heap -- sbrk(0))
    
    0x6108 is returned to the user.
    
    Suppose the user calls malloc(8). You do the same thing -- carve 16 bytes off of the chunk of memory in the free list, and return a pointer to 8 of them to the user. The other 8 are used for bookkeeping. The remaining 8152 bytes are put back on the free list. In other words, after malloc(8) is called the heap looks like:
              malloc_head == 0x6128
             |---------------|
             |     24        | 0x6100 (start of heap)
             |               | 0x6104
             |               | 0x6108  
             |               | 0x610c
             |               | 0x6110
             |               | 0x6114
             |     16        | 0x6118
             |               | 0x611c
             |               | 0x6120
             |               | 0x6124
             |     8152      | 0x6128
             |     NULL      | 0x612c
             |     NULL      | 0x6130
                  .....      | 
             |               |
             |---------------| 0x8100 (end of heap -- sbrk(0))
    
    0x6120 is returned to the user.  
    
    Suppose the user calls free(0x6108). In other words, the user wants to free the chunk of memory returned by the first malloc call. This means to put those 24 bytes that were carved off of the free list back onto the free list. Free() will do that by turning those 24 bytes into a free list node, and then making that node the first one in the free list. When free() is done, memory will look as follows:
              malloc_head == 0x6100
             |---------------|
             |     24        | 0x6100 (start of heap)
             |   0x6128      | 0x6104
             |   NULL        | 0x6108  
             |               | 0x610c
             |               | 0x6110
             |               | 0x6114
             |     16        | 0x6118
             |               | 0x611c
             |               | 0x6120
             |               | 0x6124
             |     8152      | 0x6128
             |     NULL      | 0x612c
             |    0x6100     | 0x6130
                  .....      |
             |               |
             |---------------| 0x8100 (end of heap -- sbrk(0))
    
    In other words, the free list has two nodes now -- one starting at 0x6100 and containing 24 bytes, and one starting at 0x6128 and containing 8152 bytes.

    Suppose now the user calls malloc(24). Malloc now tries to find a chunk in the free list that has 32 bytes (24 for the user and 8 for bookkeeping). Thus, it rejects the first chunk in the free list, because it only has 24 bytes, and instead carves 32 bytes off the second. When it is done, memory should look as follows:

              malloc_head == 0x6100
             |---------------|
             |     24        | 0x6100 (start of heap)
             |   0x6148      | 0x6104
             |   NULL        | 0x6108  
             |               | 0x610c
             |               | 0x6110
             |               | 0x6114
             |     16        | 0x6118
             |               | 0x611c
             |               | 0x6120
             |               | 0x6124
             |     32        | 0x6128
             |               | 0x612c
             |               | 0x6130
             |               | 0x6134
             |               | 0x6138
             |               | 0x613c
             |               | 0x6140
             |               | 0x6144
             |     8130      | 0x6148
             |     NULL      | 0x614c
             |     0x6100    | 0x6150
                  .....      |
             |               |
             |---------------| 0x8100 (end of heap -- sbrk(0))
    
    And the value 0x6130 is returned to the user -- this is 24 bytes of memory allocated for the user.

    Finally, suppose now the user calls malloc(8). We want to find a free chunk of 16 bytes (8 for the user and 8 for bookkeeping) in our free list. The first chunk will do as it has 24 bytes, however there is a question -- should we carve off 16 bytes from it, or just use all 24. The answer is that we should use all 24 bytes (give the user 16 bytes even though he'll only think it's 8). Why? Because if we carve off 16 bytes, we'll only have 8 leftover, and that's not enough to store the pointers to hook it into the free list. Thus, we take the entire 24 bytes off the free list and give it to the user:

              malloc_head == 0x6148
             |---------------|
             |     24        | 0x6100 (start of heap)
             |               | 0x6104
             |               | 0x6108
             |               | 0x610c
             |               | 0x6110
             |               | 0x6114
             |     16        | 0x6118
             |               | 0x611c
             |               | 0x6120
             |               | 0x6124
             |     32        | 0x6128
             |               | 0x612c
             |               | 0x6130
             |               | 0x6134
             |               | 0x6138
             |               | 0x613c
             |               | 0x6140
             |               | 0x6144
             |     8130      | 0x6148
             |     NULL      | 0x614c
             |     NULL      | 0x6150
                  .....      |
             |               |
             |---------------| 0x8100 (end of heap -- sbrk(0))
    
    0x6108 is returned to the user.  
    
    Note how malloc_head and the pointers in the chunk starting at 0x6148 are changed to reflect the chunk being removed from the free list.

    More on free()

    Free() must be called with addresses that have been returned from malloc(). This should be obvious. Why? Because free expects that eight bytes behind the address will contain the size of the memory to be freed. If you call free with an address that wasn't allocated with malloc(), then free() may well do something strange.

    For example, look in badfree.c. It calls free() with an address in the middle of an array, and after a while, this causes malloc() to dump core. It takes a while since malloc() seems to allocate from its buffer first, before attempting to use the (bogus) free'd blocks.

    In class it was suggested that malloc() keep track of the addresses that it allocates, and free() should check its argument to see whether or not it is a valid one. This is a good suggestion. However, malloc() will have to keep track of these addresses in a red-black tree structure or a hash table to make lookup fast. Otherwise, free() will take too long. You should note that red-black trees as implemented in libfdr.a cannot be used for this purpose, because the jrb routines call malloc() and free().

    Another way for free() to do error checking is to use the bookkeeping bytes to help you. You'll notice that the word 4 bytes before the address returned by malloc() is unused. What you can do is set that word to a checksum when you call malloc(). Then when free() is called, it will check that word to see if it has the desired checksum. If not, it will know that free() is being called with a bad address and can flag the error.

    Coalescing free blocks

    When you call free(), you put a chunk of memory back on the free list. There may be times when the chunk immediately before it in memory, and/or the chunk immediately after it in memory are also free. If so, it makes sense to try to merge the free chunks into one free chunk, rather than having three continguous free chunks on the free list. This is called ``coalescing'' free chunks.

    Here is one way that you can perform coalescing. (You shoudl not do this in your lab). However, if you want to give it a try on your own, it is a very good exercise in hacking. Instead of having all 8 of your bookkeeping bytes before the memory allocated for the user, keep 4 before and 4 after. When a memory chunk is allocated, both words are set to be the size of the chunk.

    Free chunks on the free list have a different layout. They must be at least 16 bytes in size. The first 12 bytes should be as described above, except (-size) should be kept in the first four bytes instead of size. The last 4 bytes of the chunk should also hold -size.

    What this lets you do is the following. When you free a chunk, you can look 8 bytes behind the pointer passed to free(). That should be either:

    In other words, if the word 8 bytes before the given pointer is positive, then the chunk before the one being freed has been allocated. Otherwise, it is free, and can be coalesced. If the word after the current chunk is negative, then the subsequent chunk is free and it can be coalesced. Otherwise, it has been allocated. Cool, no?

    Obviously, you need to take care at the beginning and end of the heap to make sure that your checks all work. This is not difficult.