Memory Operations

Operating Systems — Lecture 03

Why care about memory?

$ ps -e -o pid,comm,rss --sort -rss | head -3
    PID COMMAND           RSS
  33283 chrome          1657396        1.6 GB, one process of dozens

$ time ./row        0.05 s             same instructions,
$ time ./column     0.46 s             nine times slower

$ valgrind --leak-check=full ./task
39,556 bytes in 4,000 blocks are definitely lost

Functionality · correctness · security · performance · efficiency.

The story

Memory is an array of bytes

An address is the index. A pointer is a variable holding one.

Fast is small, big is slow

size a byte takes
cache KB–MB nanoseconds
memory GB ~100 ns
disk TB µs–ms

The cache hides memory from the CPU; the buffer cache hides the disk from programs.

Allocate marks, it does not create

An area = start + size. Allocate: new area, or extend one. Free: remove, or shrink.

Read, write, execute — or fault

$ ./basic
----- Allocate memory -----
----- Write to memory -----
----- Read from memory -----
----- Execute memory -----
Segmentation fault (core dumped)      SEGV_ACCERR, in the heap, rw-

Every area has permissions. The hardware checks them. There is no third option.

A process is a list of areas

pmap, line by line

$ pmap -x $(pidof see-pmap-static)
0000000000400000       4 r---- see-pmap-static     ELF header
0000000000401000     564 r-x-- see-pmap-static     .text  (libc inside)
000000000048e000     172 r---- see-pmap-static     .rodata
00000000004be000      24 rw--- see-pmap-static     .data
00000000004c4000      40 rw---   [ anon ]          .bss
0000000034c5d000     136 rw---   [ anon ]          heap
00007ffd49694000     136 rw---   [ stack ]         stack

Static: from the file, fixed. Dynamic: made at run time, zero-filled, resizable.

The loader builds the map

$ size see-pmap-static
   text    data     bss      .bss: 38 KB in memory, 0 bytes in the file
 749834   41848   39016

Who decided the address?

$ ./allocation-types
ro_data: 0x402020               .rodata   the linker, before the run
init_data: 0x408080             .data
local_scope_data: 0x4080a0      .data     static local: load time, not "global"
non_init_data: 0x40c0e0         .bss
heap_data: 0x316932c0           heap      malloc(), at run time
stack_buffer: 0x7ffd4f5633f0    stack     the call, at run time
mapped_data: 0x7c51ef5c0000     mapping   mmap(), at run time

nm allocation-types already knows the first four.

The stack grows by itself, the heap on request

[ stack ]   136 KB  ->  420 KB     100 calls deep; the kernel extended it on a fault
heap        132 KB  ->  268 KB     4 x malloc(64 KB)
            268 KB  ->  140 KB     4 x free()
mapping          +  64 KB          mmap(), then munmap()

Behind malloc()

Ten malloc()s

$ strace -e trace=brk,mmap ./malloc_kb 100       # 10 x 100 KB
brk(0x62161b3d6000)   brk(0x62161b408000)   brk(0x62161b43a000)
brk(0x62161b46c000)   brk(0x62161b49e000)   brk(0x62161b4d0000)
brk(0x62161b4b7000)                              <- free(): the heap shrinks

$ strace -e trace=brk,mmap ./malloc_kb 1000      # 10 x 1000 KB
mmap(NULL, 1028096, ...) = 0x765b4323e000        x 10
munmap(0x765b4272d000, 1028096)

Six crossings for ten requests; or one each, when the block is worth it.

A process sees its memory as a list of areas.

Static ones are built from the file before main(); dynamic ones grow while it runs — the stack by itself, the heap and mappings through an allocator, over brk() and mmap().

Everything is an address into one of them. Anything else is a fault.