1.1 Processes and memory
An xv6 process consists of user-space memory (instructions, data, and stack)
and per-process state private to the kernel.
Xv6
time-shares
processes: it transparently switches the available CPUs
among the set of processes waiting to execute.
When a process is not executing, xv6 saves the process’s CPU registers,
restoring them when it next runs the process.
The kernel associates a process identifier, or
PID
,
with each process.
System call | Description |
int fork() | Create a process, return child’s PID. |
int exit(int status) | Terminate the current process; status reported to wait(). No return. |
int wait(int *status) | Wait for a child to exit; exit status in *status; returns child PID. |
int kill(int pid) | Terminate process PID. Returns 0, or -1 for error. |
int getpid() | Return the current process’s PID. |
int sleep(int n) | Pause for n clock ticks. |
int exec(char *file, char *argv[]) | Load a file and execute it with arguments; only returns if error. |
char *sbrk(int n) | Grow process’s memory by n zero bytes. Returns start of new memory. |
int open(char *file, int flags) | Open a file; flags indicate read/write; returns an fd (file descriptor). |
int write(int fd, char *buf, int n) | Write n bytes from buf to file descriptor fd; returns n. |
int read(int fd, char *buf, int n) | Read n bytes into buf; returns number read; or 0 if end of file. |
int close(int fd) | Release open file fd. |
int dup(int fd) | Return a new file descriptor referring to the same file as fd. |
int pipe(int p[]) | Create a pipe, put read/write file descriptors in p[0] and p[1]. |
int chdir(char *dir) | Change the current directory. |
int mkdir(char *dir) | Create a new directory. |
int mknod(char *file, int, int) | Create a device file. |
int fstat(int fd, struct stat *st) | Place info about an open file into *st. |
int link(char *file1, char *file2) | Create another name (file2) for the file file1. |
int unlink(char *file) | Remove a file. |
A process may create a new process using the
fork
system call.
fork
gives the new process an exact copy of the calling
process’s memory: it copies the instructions, data, and
stack of the calling process into the new process’s memory.
fork
returns in both the original and new processes.
In the original process, fork
returns the new process’s
PID.
In the new process, fork
returns zero.
The original and new processes are often called the
parent
and
child.
For example, consider the following program fragment written in the C programming language [4]:
1 int pid = fork();
2 if(pid > 0){
3 printf("parent: child=%d\n", pid);
4 pid = wait((int *) 0);
5 printf("child %d is done\n", pid);
6 } else if(pid == 0){
7 printf("child: exiting\n");
8 exit(0);
9 } else {
10 printf("fork error\n");
11 }
The
exit
system call causes the calling process to stop executing and
to release resources such as memory and open files.
Exit takes an integer status argument,
conventionally 0 to indicate success and 1 to indicate failure.
The
wait
system call returns the PID of an exited (or killed) child of the
current process and copies the exit status of the child to the address
passed to wait; if none of the caller’s children
has exited,
wait
waits for one to do so.
If the caller has no children, wait
immediately
returns -1.
If the parent doesn’t care about the exit status of a child, it can
pass a 0 address to
wait
.
In the example, the output lines
1 parent: child=1234
2 child: exiting
might come out in either order (or even intermixed), depending on whether the
parent or child gets to its
printf
call first.
After the child exits, the parent’s
wait
returns, causing the parent to print
1
parent: child 1234 is done
Although the child has the same memory contents as the parent initially, the
parent and child are executing with separate memory and separate registers:
changing a variable in one does not affect the other. For example, when the
return value of
wait
is stored into
pid
in the parent process,
it doesn’t change the variable
pid
in the child. The value of
pid
in the child will still be zero.
The
exec
system call
replaces the calling process’s memory with a new memory
image loaded from a file stored in the file system.
The file must have a particular format, which specifies which part of
the file holds instructions, which part is data, at which instruction
to start, etc. Xv6
uses the ELF format, which Chapter 3 discusses in
more detail.
Usually the file is the result of compiling a program’s source code.
When
exec
succeeds, it does not return to the calling program;
instead, the instructions loaded from the file start
executing at the entry point declared in the ELF header.
exec
takes two arguments: the name of the file containing the
executable and an array of string arguments.
For example:
1 char *argv[3];
2
3 argv[0] = "echo";
4 argv[1] = "hello";
5 argv[2] = 0;
6 exec("/bin/echo", argv);
7 printf("exec error\n");
This fragment replaces the calling program with an instance
of the program
/bin/echo
running with the argument list
echo
hello
.
Most programs ignore the first element of the argument array, which is
conventionally the name of the program.
The xv6 shell uses the above calls to run programs on behalf of
users. The main structure of the shell is simple; see
main
(user/sh.c:146).
The main loop reads a line of input from the user with
getcmd
.
Then it calls
fork
,
which creates a copy of the shell process. The
parent calls
wait
,
while the child runs the command. For example, if the user
had typed
“echo hello
”
to the shell,
runcmd
would have been called with
“echo hello
”
as the argument.
runcmd
(user/sh.c:55)
runs the actual command. For
“echo hello
”,
it would call
exec
(user/sh.c:79).
If
exec
succeeds then the child will execute instructions from
echo
instead of
runcmd
.
At some point
echo
will call
exit
,
which will cause the parent to return from
wait
in
main
(user/sh.c:146).
You might wonder why
fork
and
exec
are not combined in a single call; we will see later that
the shell exploits the separation in its implementation of
I/O redirection.
To avoid the wastefulness of
creating a duplicate process and then immediately replacing it (with exec
),
operating kernels optimize the implementation of
fork
for this use case by using virtual memory techniques such as
copy-on-write (see Section 4.6).
Xv6 allocates most user-space memory
implicitly:
fork
allocates the memory required for the child’s copy of the
parent’s memory, and
exec
allocates enough memory to hold the executable file.
A process that needs more memory at run-time (perhaps for
malloc
)
can call
sbrk(n)
to grow its data memory by
n
zero
bytes;
sbrk
returns the location of the new memory.