Jim Plank, Rich Wolski
Lab 5 -- Simple time-shared multiprogramming
In this lab, you will add simple, time-shared multiprogramming to
JOS. Specifically, you will divide the user's memory into
eight equal parts, and you will allow up to eight user processes to
be in memory at any one time. There will be a timer that you can
use to perform time-slicing.
Also, you will be able to compile and execute C programs under
JOS that make use of malloc() and the standard I/O
library.
When you are done, you will have
implemented the following system calls:
- ioctl() (in limited form)
- fstat() (in limited form)
- getpagesize()
- sbrk()
- execve()
- getpid()
- fork()
- getppid()
- wait()
- getdtablesize()
- close() (in limited form)
Moreover, you'll be able to execute a shell and use it to run
user programs (although file indirection and pipes won't work).
Changes to the simluator
- For this lab, you need to include the header file
/lymon/homes/cs560/labs/lab5/simulator.h
in your code. Note that this is different from the simulator.h in
lab 4.
Also, you will need to link your code with
main_lab2.o and
libsim.a. You should know where to find these by now.
-
For this lab, there are two special simulator variables:
extern int User_base;
extern int User_limit;
These define what JOS addresses the user sees as
its memory. When a user job is executing, its address 0 is
JOS's (main_memory + User_Base), and it can use
up to User_Limit bytes
of memory. It is JOS's job to partition the memory that it has
allocated for users to multiple processes in the correct way. A user
process cannot access any memory outside of the current values of
User_Base and (User_Base + User_Limit). Otherwise
a segmentation violation will occur.
-
User_Base and User_Limit also change the semantics
of load_user_program(). It now loads the specified program starting
at address (main_memory + User_Base), and will fail if the
program contains more than int User_Limit bytes.
It returns -1 on a failure. On success, it returns the size of the
program loaded. You can use this as the end of the heap (i.e.
the initial sbrk() pointer).
-
There is now a timer interrupting facility: There is a procedure call
start_timer(int ticks), which tells the simulator to generate
the TimerInt interrupt every ticks ticks of the clock.
You may not call start_timer() more than once. The default
is no timer.
Your Job in this Lab
Basically, we want to do four things in this lab:
- Allow the user to compile and run programs that use the standard
I/O library and malloc().
- Implement the parts of JOS that enable a simple shell to work:
fork(), execve() and wait().
- Implement process id's.
- Make time-slicing work.
I will describe each of these in turn.
Allow the user to compile and run programs that use the standard
I/O library and malloc().
As before, set up JOS so that it loads the program
a.out as its user process when it starts up. What we first
want to do is allow these a.out programs to use the standard
I/O library and malloc().
To do this, we need to do some busy work and implement some system calls
that normally we would not care about. The first few are:
getpagesize(), getdtablesize(), and a simple close().
Getpagesize() should return the value of the PageSize
variable in simulator.h. Getdtablesize() should return 64.
Close() should be implemented so that it returns an error (that's
not really how to implement close(), but it will suffice for this
lab).
We also have to implement one case of ioctl() and one
case of fstat(). Read the man page on ioctl(). I
don't expect you to understand much about ioctl() except
for its syntax. You are going to need to implement ioctl()
when the first argument is 1 and the second argument is
JOS_TCGETP. Your job is to fill in the third argument which
is a pointer to a (struct JOStermios). You do this by calling
ioctl_console_fill(char *addr), where addr is the
JOS address of the third argument. Then return zero.
This is probably confusing, but it must be done.
Fstat() is called by the standard I/O library in order to find
out how it should buffer I/O on file descriptors 0, 1 and 2. You will
service these system calls by
calling stat_buf_fill(char *addr, int blk_size), where
addr is the JOS address of the stat struct that the user
passed to fstat(), and blk_size is the amount of buffering
that the file descriptor should allow. For file descriptor zero,
this should be one. For file descriptors one and two, try 256.
Last, you must implement sbrk(), which you should remember from
cs360. Read the man page for more detail. As before, the stack
and the heap can crash if the user messes up. However, sbrk()
should not be allowed to increase past User_limit.
When you have implemented all of these system calls, the a.out
programs should be allowed to use the standard I/O library (most notably
printf() and malloc()). Look at the programs in
the test code directory,
and try ones like hw2.c that use
printf(). You can write, compile and run your own programs
too -- just follow the compilation steps in the lab
4 handout
Implement the parts of JOS that enable a simple shell to work:
fork(), execve() and wait()
This means you must do multiprogramming. In the last lab, you allowed
one user program to execute and gave it all of memory. This time, you
should divide memory into 8 equal parts and when you create a new process,
it will use one of those parts. Context switching will now involve
saving/restoring the registers, User_base
and User_limit.
You'll need to modify the exit() system call so that instead of
halting JOS, it simply terminates a process. Fork() and
execve() are rather straightforward. You should ignore the third
argument to execve() -- we won't have any environment variables.
You also must implement wait() and process id's, which should
work as in Unix. In particular, processes have parents; otherwise
they are orphans. If a process exits and its parent hasn't called
wait() it becomes a zombie -- it releases its memory but
maintains a PCB until its parent either dies or calls wait().
Orphans must do the correct thing when they die.
To test this, copy or link jsh from the test_execs
directory to a.out and run it. You should be able to run
any program in the test_execs directory from the shell. You
should also be able to run programs in the background. Jsh
does allow you to do file indirection and pipes, but you do not
have those implemented, so they will not work. That is ok.
Implement process id's
Finally, you need to implement getpid() and getppid()
to work with your process id's. This is straightforward. I have no
process with process id 0. Orphans return pid 0 as their parent.
Make time-slicing work.
This is straightforward -- set the timer for some number of ticks, and
then reschedule the CPU when you get the interrupt.
What I did
As in the last lab, I have a detailed list of the things I did to get this
lab working. This is in the file
WHAT_DR_PLANK_DID.
As before, it is not mandatory that you do things the same way that I
did. However, it will make your life easier.