pete > courses > CS 315 Fall 26 > lecture 07: malloc
Lecture 07: malloc
Goals
- conclusively determine whether an arbitrary function is a system call
- explain why malloc is not a system call
- describe the behavior of the brk and sbrk system calls
- explain how one might implement malloc using brk and sbrk
- explain alignment in the context of memory
(following notes are necessarily short because much of the class time was discussion that led to the following conclusions…)
question: is malloc a system call?
some intuition both ways:
- since it asks some entity (?) for a chunk of memory, that entity might very well be the kernel, and system calls are how we ask the kernel for stuff, so it might be a syscall
- on the other hand, requests for memory with malloc are often small and frequent: a few bytes at a time, but a bunch of times throughout the lifetime of a process, which suggests it might be inefficient as a syscall
(the latter intuition is similar to the reasons for not using read(2) and write(2) directly)
how might you find out for sure?
- you could see what section of the manual it’s described in (it’s in 3, which suggests it’s not a syscall)
- you could write a program that calls malloc and then run it with strace to see if malloc shows up in the sequence of system calls that are invoked
here’s a program we can use to implement the latter strategy: simple-malloc.c
and a Makefile that includes a handy recipe to run strace for us
$ make simple-malloc.strace gcc -Wall -pedantic -g -o simple-malloc simple-malloc.c strace -o simple-malloc.strace ./simple-malloc
(recall: normally, strace will send its record of syscalls to the terminal; the -o option is used to send that record to the named file)
now, if we look in simple-malloc.strace, we see all the boilerplate we’ve seen before
and, at the very end, in the collection of syscalls that are invoked as a result of the code in our main function, we see
brk(NULL) = 0x5631192ad000 brk(0x5631192ce000) = 0x5631192ce000
huh. I wonder what the brk syscall does.
to the manpage!
brk() and sbrk() change the location of the program break, which defines the end of the process's data segment (i.e., the program break is the first location after the end of the uninitialized data segment). Increasing the program break has the effect of allocating memory to the process; decreasing the break deallocates memory.
these words may all make sense, but it may be difficult to see how they relate to the Heap (which, recall, is the name for the region of memory from which malloc allocates memory)
to put it into context, let us return to (a simplified version of) the diagram that shows the memory layout of a process:
0x7ff..fff +---------------------------+
| |
| stack |
| | |
| v |
+---------------------------+
| |
| |
| |
| |
| |
| |
| |
+---------------------------+
| ^ |
| | |
| heap |
| |
| |
+---------------------------+
| |
| instructions |
| |
+---------------------------+
| |
| |
| |
| |
| |
0x000..000 +---------------------------+
the stack grows down, the heap grows up, the instructions are more or less static
the upper border of the Heap is what the manage is referring to as the program break
and the brk and sbrk system calls are used to move it up (make it bigger) and down (make it smaller) as necessary
brk allows you to specify exactly the address at which you want the heap to end
sbrk allows you to specify the desired change in the size of the heap (positive or negative)
in other words: brk is absolute, sbrk is relative
so if we ask for memory with malloc, that function will turn around and call brk or sbrk to make the heap bigger and thus make room for the new chunk of memory requested
this may feel weird: what’s in the memory beforehand? what about malloc "gives" a certain chunk of memory to the caller?
the heap is a bit strange: it’s not organized at all, even though you might think it would be
it’s just a region of memory out of which chunks are carved and returned by malloc
there is no notion of boundaries between those chunks; malloc just makes sure that, when you call it, it returns the address of the beginning of a chunk of memory that hasn’t been returned by any other call to malloc
(setting aside for the time being the behavior of free, which explicitly marks an allocation as no longer being used)
so how might we go about implementing malloc using brk and/or sbrk ?
if the user calls malloc(12) is it enough to just call sbrk(12) ?
no, for (at least) two reasons:
- this would be painfully inefficient for a bunch of small allocations: similar to myread/mywrite, we want to get a big chunk of memory with brk/sbrk and then carve individual allocations out of that
- if the user later calls free on that allocation, we need to know how much room is now available (recall that free takes as its parameter only the pointer that identifies the beginning of the memory being allocated)
the latter point implies that we need some bookkeeping information in the heap to keep track of all the allocations made so that we can do the right thing when free is called
it makes sense that we would have to store some information about every allocation, because every allocation might be different and free’ing each would need to be handled differently
where to store this bookkeeping information?
not the stack: if the frames get popped, the information is lost
not the instruction section: because heap metadata is data
logical choice is within the heap itself
conclusion: the heap consists of both the chunks of memory being allocated with malloc as well as the metadata about those chunks that allow malloc and free to manage them
what sort of data structure would we use to keep track of all this information about the allocations?
an array kind of makes sense, because we expect to have a bunch of presumably-structs, one for each allocation
except arrays are fixed-size: we’d have to set aside a specific amount of space at the beginning and hope it’s enough to manage all the allocations
additionally, if a program makes very few allocations, much of the memory consumed by the array would go unused, which is wasteful
is there something similar to an array that can grow and shrink dynamically?
why yes, yes there is: a linked list
modern heap implementations are super complex (I’ve linked some really good articles in the description for assignment 3) but most of them do indeed use a linked list (or several) to keep track of allocations
one way (which, hint hint, you could implement for assignment 3) is to set up the heap such it’s a sequence of "struct A, allocation A, struct B, allocation B, struct C, allocation C, etc"
whereby the struct describing a particular allocation immediately precedes the allocated memory itself
how about free ?
it could just mark an allocation as no longer being used
but then what if malloc is called again and the now-unused chunk is an appropriate size to be allocated?
this is something you’ll have to figure out in the assignment
you’re going to get very comfortable with pointers and gdb on this one
as we’ve beaten into the ground many times, we can think of memory as a massive array
where each element of the array holds a single byte of data (and is consequently referred to as byte-addressable memory)
deep inside the hardware, there are physical devices that store these bits and connected physical devices that transfer these bits from memory to the CPU for to make computation
transferring a single byte at a time is horrendously inefficient, especially when we consider that many of the values we work with are larger (32-bit integers, 64-bit floats, variable-length strings, etc)
therefore, a straightforward optimization that pretty much all hardware uses these days is to transfer more than a single byte at a time
the wiring between memory (or cache, to be absolutely precise) and CPU is often set up such that the path is the same width as (or an integer multiple of) the word size
recall that the word size is a parameter of the hardware design, and is the number of bits used to store a memory address
so in modern machines, the wiring between memory (cache) and CPU allows us to transfer 8 bytes (64 bits) of data at a time
there’s actually a subtlety here: we can’t transfer just any contiguous group of 8 bytes at once
in effect, memory is broken up into consecutive 8-byte chunks: the first chunk consists of bytes 0 through 7; the second of bytes 8 through 15, etc
therefore, if the 8-byte value upon which I wish to compute is stored in addresses 256-263, I can transfer it all at once
because the starting address is evenly divisible by 8
rephrased, because the starting address mod 8 is 0
such a value is said to be aligned
an unaligned value is one that spans multiple 8-byte chunks
for instance, if my 8-byte value starts on address 100 (and therefore lives in address 100-107), then its first half will be in the 96-103 chunk and its second half will be in the 104-111 chunk
reading this value from memory is much less efficient: two memory operations, then some shifting and combining
as a result, for efficiency, it behooves programs to operate on aligned values whenever they can
it turns out that some software even expects memory returned by malloc to be aligned
so if your implementation returns addresses that are not aligned, weird behavior could ensue, quite often leading to a crash
in fact, the libc version of malloc returns addresses that are always aligned to 16-byte boundaries
when I modified my implementation to do the same, the crashes went away
take-home message: make sure your implementation of malloc returns addresses that are evenly divisible by 16 (ie, address mod 16 is 0)