process = program in execution, running in a virtual machine
provided by the OS.
Last time: looked at a simple
program that used the UNIX fork system
call to get its whole address space copied, ending up with two executions
running on the system together with completely separate memory images, but
identical at the moment of the fork. So
both resulting processes can see the old memory values, but not each other’s
changes to them.
Execution of a program across a call to fork()—
<picture
of user address space, kernel in cloud>
At fork, second address space
shows up, a copy of first:
<picture
of two user address spaces, kernel in cloud>
After the fork, parent and
child are completely separate in terms of available memory—they can’t spy on
each other. This is true between any two
user processes.
The mystery of the addresses in user spaces: the copied program uses exactly the same addresses
as the original, but they point to different values in memory after one of the
processes has change a value. How can this work?
Answer: we will cover this
carefully in the memory management topic. The secret is in memory mapping: we
aren’t really accessing memory location 0x40000 or whatever, but rather some
mapped location in physical memory. The
two processes have different memory maps and are using separate pages in physical
memory, i.e., separate memory chips.
Question: can UNIX processes
ever actively share memory?
Answer: yes, via special
“shared memory” system calls, considered advanced programming. The memory
involved would be in that big area above the heap and below the stack, where
the DLLs hang out. DLLs themselves are
largely shared between processes (all the code).
Question: fork()
produces another process, what about threads?
Answer: threads share the
same process image, so thread programming is very tricky—it’s easy for one thread
to damage data needed by another thread. The processes are “bottled up”
from one another each in their own address space. Java supports threads and the synchronization
features needed. More on this later.
Execve, or exec for short, replaces the current process
image with one constructed from the contents of an executable file, i.e. it
runs a program in the current process. This means the old program in the
process is wiped out, replaced by the one specified as an argument to the exec
system call. A simple (UNIX) program would be:
int
main(int argc, char *argv[])
{
int status;
if (fork()) {
waitpid(-1,
&status, 0); /* parent waits for child */
printf("status=%d\n",status);
}
else { /* run date in the child */
execve("/usr/bin/date", argv, 0);
/* never get here, unless execve fails */
}
}
Here we are passing argv along from our old process to be the argv of the new image, since execve
wants a non-trivial argv. Waitpid(-1, ...) waits for
any child to terminate, so the parent waits for date to run, print out the
date, and exit, and then prints out “status = 0”, the success status.
Note that the stdout, stdin,
and stderr fd’s
pass live through the exec, so date has a good stdout
to use and we see the output of date on the screen.
Parent processes are expected
to pick up the child exit status—it’s their “parental responsibility”. One way
to do this is to wait for any-child termination by calling system call waitpid, as shown above.
The parent can do real work before calling waitpid. If the child exits while the parent is still
working, that’s no real problem. It becomes a “zombie”, a runt process that
just holds the exit status. The zombie disappears entirely when the parent
picks up its exit status.
We realize that the parent
and child are running completely separately (in terms of accessible memory)
after the fork. The child doesn’t “live at home” under the direct supervision
of the parent. Instead it takes off immediately and rattles around on its own,
and when it dies, it’s as if the system sends a postcard to the parent about
its exit status. The parent is expected to be still alive when the child exits,
and pick up the exit status. If the parent creates a lot of children and then
ignores them, the system can get clogged up with thousands of zombies.
Stripped-down shell: look at code in Tan., pg.
54. Similar to above with loop around it and general
command instead of just “date.”
Tanenbaum’s address space pictures—addresses run from bottom to
top, unlike my pictures on the board that run from left to right--
Pg. 51: Highest address is
FFFFFFFF: 32 bit system
Pg. 56: Highest address is
FFFF: 16-bit system.
We have covered UNIX process
management system calls: fork, exec, waitpid, exit. Windows has no fork or exec system calls, only
the combination CreateProcess system call that
takes the executable file as one of many parameters, and creates a new process
running that program. We could write a “wshell”,
something like the UNIX shell with CreateProcess
instead of fork and exec, and WaitForSingleObject
(using the process handle returned by CreateProcess)
after the CreateProcess to make the parent wait for
termination of the program, itself done by ExitProcess.
Look at the system call
conversion table, pg. 61:
The OS provides a virtual
machine for programs
UNIX and Windows have similar
virtual machine setups—
User programs execute in CPU
user mode, interrupts enabled. They can’t do the privileged instructions. They
are “bottled up” in their virtual machines.
System call instruction: each instruction set has a special instruction,
generic TRAP in Tanenbaum, INT n/SYSENTER/SYSCALL for
x86, SWI for ARM.
·
changes the CPU
from user mode to kernel/supervisor mode, enabling the privileged instructions
such as cli
·
saves the old CPU
state, so the old user execution can be resumed at the next instruction in user
code (by IRET on x86)
·
works like an interrupt cycle, except it doesn’t change IF.
Gets the handler address from IDT and puts it in EIP, causing the execution of
the system call handler in the kernel.
Below the OS, the hardware,
so the OS code needs to run the hardware, including of course the interrupt
system.
Also, don’t forget 2 ways to
look at system call—function prototype, part of system
API, and single special instruction in user program image.
Idea of starting new job,
finding out they are using a special OS, say for embedded programming.
What do you ask? Does it provide a flat address space? timesharing? automatic stack? and what are the system calls?
sequential process = process, with emphasis on its single
program counter showing where the CPU is in the code—the CPU just “follows its
nose” through the program code
multiprogramming = multitasking = OS method of switching one CPU from
process to process, so that for each process seems to have its own (slower)
CPU. Almost synonymous with “timesharing”, except that it is more
specifically single-CPU-oriented. Of course a system could have several
CPUs each doing multiprogramming.
How Timesharing works
Each process gets to use the
CPU for a while, maybe 50 ms (milliseconds), then the
OS reassigns it to another process to use.
Thus multiple processes can make progress in their programs alongside
each other on the system.
Look at Fig. 2-1(c). Shows 4 processes executing in “round-robin” fashion—over and over
in some pattern. Of course the pattern is usually more
irregular. Each little interval of running is typically about 50 ms long,
the “quantum” of CPU for the process.
These four processes are all running code that computes constantly
(maybe “ray tracing”), so they all want to use the CPU as much as possible.
Processes
vs. programs. Programs are
algorithms embodied in machine code and usually stored in an executable
file. Processes are alive on the system and are using memory, CPU, etc.
Example of
program that forks. End up
with two processes each running the same program. For Windows, could run
the same program twice and get the same situation.
All processes are born by the
process-create syscall (fork, CreateProcess),
except the very first. On bootup, the kernel is
running as a standalone program, and morphs itself into a proper process.
Process Termination:
usually by (exit, ExitProcess). These both have
a parameter for an error/success code. (In UNIX, this code is reported to
the parent in waitpid, and then forgotten by the
kernel. In Win2K, it is reported via the process handle—I’m not sure how
it is ever garbage-collected)
Other ways processes
terminate:
Zombies. If a process exits in UNIX, and its parent does
not pick up its exit status, it stays as a “zombie” in the system. It no
longer has a virtual machine, so it isn’t using resources, but you can see it
in “ps –a” output with a Z in
the “S” (process state) column. It will stay there until the system is
rebooted. Like a proper zombie, you can’t kill it by normal
methods. To avoid zombies, make your parent processes
do a wait or waitpid to pick up the status.