You might do forks a lot in GitHub. If so, it'll be easy for you to understand fork in the OS world. Today we will implement a fork in our own OS.
fork has some friends, they are exec, wait and exit. We'll implement them too.
So far all the processes are hard coded. Doing fork implies we will create/destroy processes dynamically. Before starting, let's recall the data structures related to a process:
process-related stuff: 16 0 H +----------+ : : +----------+ |ldt desc | 16 0 |for proc j| H +-------------+ / H +-----+ +----------+ : : | |Stack| : : / +-------------+ | +-----+ +----------+ | | ldts[RW] |---. | : : | ldt desc +--->-| +-------------+ +--->-| +-----+ .--> |for proc i| X | | ldts[C] |---` Z | |Data | | +----------+ \ +-------------+ | +-----+ | : : | ldt_sel +--. | |Text | | L +----------+ +-------------+ | \ L +-----+ | GDT : STACK FRAME : | process image | L +-------------+ | | proc_table[i] | | | `-----------------------------------------` Y
There are three data structures: GDT
, proc_table[]
and the process image.
There are three relations between them: X, Y and Z.
In the past, all of them are prepared before the processes run:
GDT
is prepared inkernel/main.c
proc_table[]
is prepared inkernel/global.c
- process images are the compiled binaries of the process functions (e.g.
task_fs()
for FS) - relations X is prepared in
init_proc()
- relations Y and Z are prepared in
kernel_main()
We can see that although we will create processes dynamically, some of the above works can still be prepared beforehand:
- We can reserve slots in
proc_table[]
andGDT
for new processes - We can set all
ldt_sel
s inproc_table[]
properly (relation Y) - We can set all ldt descriptors in
GDT
properly (relation X)
The memory allocation for the process image and the relation Z must be done when doing fork.
See a/
for more details.
If process A calls exit()
, then MM will do the following:
- inform FS so that the fd-related things will be cleaned up
- free A's memory
- set A
.exit_status
, which is for the parent - depends on parent's status. if parent (say P) is:
WAITING
- clean P's
WAITING
bit, and - send P a message to unblock it
- release A's
proc_table[]
slot
- clean P's
- not
WAITING
- set A's
HANGING
bit
- set A's
- iterate
proc_table[]
, if proc B is found as A's child, then:- make INIT the new parent of B, and
- if INIT is
WAITING
and B isHANGING
, then:- clean INIT's
WAITING
bit, and - send INIT a message to unblock it
- release B's
proc_table[]
slot
- clean INIT's
If process P calls wait()
, then MM will do the following:
- iterate
proc_table[]
, if proc A is found as P's child and it isHANGING
- reply to P
- release A's
proc_table[]
entry
- if no child of P is
HANGING
- set P's
WAITING
bit
- set P's
- if P has no child at all
- reply to P with error
Glossary:
HANGING
: everything except theproc_table
entry has been cleaned upWAITING
: a process has at least one child, and it is waiting for the child(ren) toexit()
See b/
for details.
Once we implement exec()
, we can write programs for our OS (see c/
and d/
).
The snapshot below is the result of a fork()
followed by an execl("/echo", "echo", "hello", "world", 0)
.
Calling execl("/echo", "echo", "hello", "world", 0)
is supposed to be the same as executing echo hello world
in a shell, except that we have no shell yet.
The "hello world
" appears. That means the fork and exec succeeded.
We didn't have a shell when we called execl()
, but we will soon.
Armed with fork()
and exec()
, it's easy to write a simple shell.
Let's call it shabby_shell (see e/
).
Look, shabby_shell is running: