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 pid Process identifier โ a unique number naming a running process.
defined in ch. 2 โ open in glossary
โ 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:
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 address space A process's private virtual view of memory, which the OS maps onto physical memory.
defined in ch. 2 โ open in glossary
(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.
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 same0x200000, 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 concurrency The problems that arise when working on many things at once within the same memory space. defined in ch. 2 โ open in glossary โ 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 thread A function running within the same memory space as other functions, with more than one active at a time.
defined in ch. 2 โ open in glossary
โ 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 atomic Executing all at once, indivisibly โ what a multi-instruction sequence like counter++ is not.
defined in ch. 2 โ open in glossary
โ all at once, indivisibly โ a switch
between threads at the wrong moment silently loses an update. Step through
one such moment:
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?