Basic process memory layout
The basic process memory layout is the result of "linking" (combining the
various .o and .a (or .so) files into an executable program format) plus
arranging the disk file's data into main memory (allocating data storage and
such).
The gcc command performs the linking process (by running ld).
Then we have a file on the disk which we can execute with an exec() function.
The following notes are primarily about what happens to memory layout at the
time of exec() and after.
Process memory is divided into a "text segment" and a "data segment".
The "text segment" is the code, the machine language instructions that are
your program.
It is made read-only via the MMU.
The "data segment" is more complicated.
It is not read-only, far from it; it
changes as your program executes.
Some of its components are:
- argv (this is treated somewhat specially)
- the environment (ditto)
- these days it usually does not include string literals, which are put
into the text segment to make them unwritable
- all variables
- some variables' initial values are determined by values in the a.out
file; this is loaded first
- the remaining variables are initialized to all bytes zero and thus need
not be stored in the a.out file format. All we need to store is the total
number of bytes needed (a single number).
This is known as the "BSS" area.
(If the programming language guarantees initial values for these
variables, it may or may not need to initialize them in a loop before
calling main(). On most architectures, for example, all bytes zero
will be a null pointer; if not, global-lifetime pointer variables in
C without explicit initializers need to be initialized in an explicit
loop emitted by the compiler.)
- the "stack", or this may be a separate segment; anyway, it's described
below
- the "heap", which consists of whatever data space is acquired during
process execution by raising the top limit of data space with sbrk().
Malloc calls sbrk().
Variables with global lifetime are allocated in the data segment.
Scope is an error-checking issue for the C compiler; global versus
stack is a lifetime issue. Function-static is in the global area.
But
"auto" variables can't have a fixed address. They are allocated on the stack.
(Insert (virtually speaking) brief reminder of CSC 258 material regarding
stack use for modern-style procedures (functions))
The text segment is shared between multiple processes executing the same program!
This was a useful memory savings in the early unix days. Still nice.
Not a problem because the text segment is unmodifiable.
The stack may be a separate segment. Whether it's part of the data segment or
separate, it is private.
Each process gets its own.
Virtual memory
What happens when memory fills up? lots of processes... not all running...
-
paging: writing some of it out to disk
-
implements "virtual memory" -- seem to have more memory than you really do.
-
multiplexing virtual memory onto real memory
(analogy: timesharing and CPU.)
-
Set aside an area of the disk -- traditionally not part of a filesystem.
The
"MMU" maps addresses, a "page" at a time. It sits between the CPU and the
main memory unit, conceptually;
these days typically built in to the CPU.
- presents a uniform address space; no need to write relocatable code anymore
- "virtual address" vs "physical address"
- "locality of reference"
paging stuff out: write to disk
- when out, unmap page
- if unmapped and accessed, page fault occurs
- this is an interrupt
- "page fault handler" (an ISR in the OS)
may read from disk, remap, and RTI, thus restarting the
instruction that faulted, but this time with it pointing to the actual data
- works for code too. Unix: code segment not writable! So we only
have to page in, not out. Try to write to an a.out file which is in use,
get error "text segment busy". Nasty interaction with NFS though; try it on a
local filesystem (e.g. /tmp).
The MMU enforces
access restrictions implicitly. You can't refer to a memory
address which is part of a different unix process. Except by using numbers
below zero or above the size of your process.
In that case, the page-fault handler signals segfault.
i.e. the page-fault handler can signal an OS-level page fault if it's a
virtual page fault and not just a physical page fault.
"Copy-on-write": after a fork(), typically one process does an exec(), or an
exit(), pretty soon, most of the time. So in
the interim, don't duplicate all the data pages. (not an issue for code
pages!)
Mark the data pages as read-only in the MMU configuration.
If a process tries to write, this generates a hardware page fault;
the OS duplicates that particular page and then marks them both as read-write.
Much more about all this in CSC 369.
sbrk(): allocates more pages at the end of your data segment. sbrk() system
call returns the new top of memory. So
malloc() begins by calling sbrk(0) to
find out where the end of memory is. It uses this as the "heap":
heap is managed by complex algorithms... fragmentation issue
Can write your own memory allocator! minimal kernel! sbrk() means increase
the data size allocated to this process; that's all. sbrk() requires MMU
adjustments, thus restricted to kernel. All else is userland. Malloc is in
the C library, just calls sbrk().
(Actually, sbrk() is in the C library and the kernel entry point in
question is brk(), but that's a minor point.)
System calls
So, how does that syscall work anyway?
-
"kernel entry point"
-
machine-language-level mechanism (either explicit support for OS calls or
"software interrupts")
-
"glue routine" in C library
-
errno
- #include <errno.h>
- man 2 intro (unfortunately only in unix, not linux)
[list of topics covered]
[course notes available so far]
[main course page]