If you're not already familiar with UNIX fork system call, here is it's function description and its entry on Wikipedia.
Basically, in sys_fork
, we need to do the follow things:
- Copy parent's trap frame, and pass it to child thread
- Copy parent's address space
- Create child thread (using
thread_fork
) - Copy parent's file table into child
- Parent returns with child's pid immediately
- Child returns with 0
So, let's get started.
Pass parent's trap frame to child thread
Trap frame (struct trapframe
) records the exact state (e.g. registers, stack,
etc.) of parent when
it call fork. Since we need the child exactly the same with parent (except for
return value of fork), we need child thread to start run with parent's trap
frame.
So we need to pass parent's trapframe
pointer to sys_fork
, and store a full
copy of it in kernel heap (i.e., allocated by kmalloc
). Then pass the
pointer to child's fork entry function (I called it child_forkentry
).
Copy parent's address space
We can use the as_copy
facility to do this. Note that as_copy
will allocate
a struct addrspace
for you and also copy the address space contents, so you
don't need to call as_create
by yourself.
Create Child Thread
thread_fork
will create a new child thread structure and copy various fields
of current thread to it. Again, you don't need to call thread_create
by
yourself, thread_fork
will call it for you. You can get the pointer of child's
thread structure by the last argument of thread_fork
.
Parent's and Child's fork
return different values
This is the trickiest part. You may want to take a look at the end of syscall
to find out the convention of return values. That is: on success, $a3
stores
0, and $v0
stores return value (or $v0:$v1
if retval is 64-bit); on failure, $a3
stores 1, and $v0
store error code.
Parent part is quite easy, after call thread_fork
, just copy current thread's
file table to child, and other book-keeping stuff you need to do, and finally,
return with child's pid, and let syscall
deal with the rest.
Child part is not that trivial. In order to let child feel that fork
returns
0, we need to play with the trapframe a little bit. Remember that when we call
thread_fork
in parent's sys_fork
, we need to pass it with an entry point
together with two arguments (void* data1, unsigned long data2
). As said before,
I name the entry point as child_forkentry
, then what should we pass to it?
Obviously, one is parent's trapframe copy (lies in kernel heap buffer) and
another is parent's address space!
Once we've decided what to pass, how to pass is depend on your preference. One
way is to pass trapframe pointer as the data1
, and address space pointer as
data2
(with explicit type-case, of course). Another way may be we pass trapframe pointer
as data1
, and assign the address space pointer to $a0
since we know fork
takes
no arguments.
child_forkentry
Ok, now child_forkentry
becomes the first function executed when child
thread got run. First, we need to modify parent's trapframe's $v0
and $a3
to make child's fork looks success and return 0. Also, don't forget to
forward $epc by 4 to avoid child keep calling fork. (BTW, we don't need
to do this in parent since syscall
will take care of this.).
Then we
need to load the address space into child's curthread->t_addrspace
and
activate it using as_activate
. Finally, we need to call mips_usermode
to return to user mode. But before that, we need to_ copy the modified
trapframe from kernel heap to stack_ since mips_usermode
check this
(KASSERT(SAME_STACK(cpustacks[curcpu->c_number]-1, (vaddr_t)tf))
. How? Before
call mips_usermode
, just declare a struct trapframe
(note: not pointer) and copy the content
into it, then use its address as parameter to call mips_usermode
.
Synchronization
Note that thread_fork
will set newly created child thread runnable and try to
switch to it immediately. So it's highly possible that before thread_fork
returns, the child thread is already running. This is not desired since we
need to copy other stuff, like file table, to child thread after
thread_fork
. We definitely don't want the child thread running without a
file table. So we need to prevent child thread from running until parent
thread set everything up.
So we need to disable interrupts before thread_fork
using splhigh
, and
restore the old interrupt level using splx
after parent thread is done.
Update: Disable interrupt does not necessarily stop child from running. If you adopt this approach, you need to use some synchronization primitives to coordinate between parent and child.
Or better, you can modify thread_fork
, copy whatever you need to copy (e.g.,
file table) before thread_make_runnable
. Thus you won't have synchronization
issue.