ยง2.2โ€“2.3Virtualizing Memory โ€ฆ Concurrency

Intro OSTEP pp. 7โ€“10 ยท ~9 min read

  • pid
  • address space
  • concurrency
  • thread
  • atomic

The CPU demo showed one resource being multiplied into many. This page runs the same experiment on memory โ€” and then breaks something, on purpose, with threads.

2.2 Virtualizing Memory

The model of physical memory presented by modern machines is very simple: memory is just an array of bytes. To read, you name an address; to write (or update), you name an address and supply data. Thatโ€™s the whole interface.

Programs use it constantly. Every data structure a program keeps lives in memory, reached by loads, stores, and other memory-touching instructions. And donโ€™t forget the program itself lives there too โ€” the fetch in fetchโ€“decodeโ€“execute is a memory access on every single instruction.

The bookโ€™s second program, mem.c (Figure 2.3), allocates memory and slowly counts in it:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int
main(int argc, char *argv[])
{
    int *p = malloc(sizeof(int));                        // a1
    assert(p != NULL);
    printf("(%d) address pointed to by p: %p\n",
            getpid(), p);                                // a2
    *p = 0;                                              // a3
    while (1) {
        Spin(1);
        *p = *p + 1;
        printf("(%d) p: %d\n", getpid(), *p);            // a4
    }
    return 0;
}

Figure 2.3: A Program That Accesses Memory (mem.c)

It allocates an int (line a1), prints the address of that memory (a2), stores zero into it (a3), then loops: sleep a second, increment, print (a4). Each print also includes the PID โ€” the process identifier, unique per running program:

prompt> ./mem
(2134) address pointed to by p: 0x200000
(2134) p: 1
(2134) p: 2
(2134) p: 3
(2134) p: 4
(2134) p: 5
^C

Unremarkable โ€” the memory landed at address 0x200000 and counts upward. Now run two instances at once:

prompt> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) address pointed to by p: 0x200000
(24114) address pointed to by p: 0x200000
(24113) p: 1
(24114) p: 1
(24114) p: 2
(24113) p: 2
(24113) p: 3
(24114) p: 3
(24113) p: 4
(24114) p: 4
...

Figure 2.4: Running The Memory Program Multiple Times

Both processes allocated memory at the same address โ€” 0x200000 โ€” and yet each updates its value independently, as if the other didnโ€™t exist. Watch it happen:

Figure 2.4, animated: two copies of mem.c, each incrementing "its own" 0x200000
t =
PID 24113
PID 24114
tick 1 / 8 ยท one 1-second time slice per tickrunning (increments its p)ready โ€” not running

PID 24113 runs for a second, increments its p, and prints "(24113) p: 1".

This is the OS virtualizing memory โ€” the same trick as virtualizing the CPU, applied to a different resource. Each process gets its own private virtual address space (usually just address space), which the OS somehow maps onto the machineโ€™s physical memory. An address like 0x200000 is only meaningful within a process; the running program believes it has physical memory all to itself. The reality: physical memory is one shared resource, managed by the operating system.

The illusion: two private memoriesโ€ฆPID 24113 โ€” virtual address space0x200000 : p = 3โ€all of memory is mineโ€PID 24114 โ€” virtual address space0x200000 : p = 2โ€all of memory is mine, tooโ€OS maps virtual โ†’ physical(same virtual address, different physical homes)24113โ€™s p24114โ€™s pphysicalโ€ฆโ€ฆaddressesโ€ฆthe reality: one shared physical memory, managed by the OS

Virtualizing memory: both processes see a private 0x200000 (orange = virtual addresses); the OS maps each onto a different region of the one shared physical memory. How this mapping works is the heart of chapters 13โ€“23.

Aside: Try this at home? Disable ASLR first

For both instances to print the same 0x200000, address-space randomization must be disabled. Modern systems randomize where allocations land as a defense against certain security exploits (such as stack-smashing attacks), so out of the box youโ€™ll see two different addresses โ€” which spoils the illusion but proves the same point: the address a process sees is virtual, not physical.

2.3 Concurrency

The third theme: concurrency โ€” the bookโ€™s umbrella term for the host of problems that arise when working on many things at once in the same program. These problems appeared first inside the OS itself (as you just saw, it juggles many processes at once), but they are no longer the OSโ€™s private headache: modern multi-threaded programs hit exactly the same issues.

Meet threads.c (Figure 2.5):

#include <stdio.h>
#include <stdlib.h>
#include "common.h"

volatile int counter = 0;
int loops;

void *worker(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
        counter++;
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "usage: threads <value>\n");
        exit(1);
    }
    loops = atoi(argv[1]);
    pthread_t p1, p2;
    printf("Initial value : %d\n", counter);

    Pthread_create(&p1, NULL, worker, NULL);
    Pthread_create(&p2, NULL, worker, NULL);
    Pthread_join(p1, NULL);
    Pthread_join(p2, NULL);
    printf("Final value   : %d\n", counter);
    return 0;
}

Figure 2.5: A Multi-threaded Program (threads.c)

main creates two threads โ€” think of a thread as a function running within the same memory space as other functions, with more than one active at a time. Both threads run worker(), which just increments a shared counter loops times. (Pthread_create, with a capital P, is the bookโ€™s wrapper that calls pthread_create and checks the return code.)

Each of the two workers increments the counter loops times, so with loops = 1000 youโ€™d predict a final value of 2000 โ€” and youโ€™d be right:

prompt> gcc -o thread thread.c -Wall -pthread
prompt> ./thread 1000
Initial value : 0
Final value   : 2000

With loops = N, the answer should always be 2N. Should be. Turn the dial up:

prompt> ./thread 100000
Initial value : 0
Final value   : 143012     // huh??
prompt> ./thread 100000
Initial value : 0
Final value   : 137298     // what the??

Not 200,000 โ€” and a different wrong answer each run. (Run it enough times and it will occasionally even be right.) Nothing in the source changed; only timing did.

The culprit is the innocent-looking counter++. It is not one operation but three instructions: load the counter from memory into a register, increment the register, store it back. Because those three do not execute atomically โ€” all at once, indivisibly โ€” a switch between threads at the wrong moment silently loses an update. Step through one such moment:

Why threads.c loses updates: counter++ is three instructions, and a badly timed switch interleaves them
t =
Thread 1
Thread 2
counter (memory)
tick 1 / 6 ยท one machine instruction per tickexecuting an instructionpaused โ€” not runningvalue of counter in memory

Thread 1 starts an increment: it loads counter (50) from memory into a register.

With 100,000 loops per thread, the timer interrupt lands mid-increment over and over โ€” each time erasing one update. How many land there differs run to run, which is exactly why the wrong answers differ run to run.

The Crux: How To Build Correct Concurrent Programs

When there are many concurrently executing threads within the same memory space, how can we build a correctly working program? What primitives are needed from the OS? What mechanisms should the hardware provide? How can we use them to solve the problems of concurrency? Part II of the book (chapters 25โ€“34) is the answer.

Next: the third and final piece โ€” what happens when the power goes out, and why file systems exist.

Check yourself

1.Both copies of mem.c print "address pointed to by p: 0x200000", yet each counts independently. How is that possible?

2.In the output "(24113) p: 1", what is 24113?

3.threads.c with loops = 100000 printed 143012 on one run and 137298 on the next. Why a different wrong answer each time?

4.Predict from the interleaving above: counter is 50, and BOTH threads load it before either stores. After both finish their store, counter isโ€ฆ

5.What property is counter++ missing that makes it unsafe to run in two threads at once?

5 questions