| CPSC 331 | Operating Systems | Spring 2026 |
This project has several goals:
Your primary task is to implement lottery scheduling for xv6, as described in the project README and below. As part of this, you will add two system calls to support lottery scheduling and write a simple version of the utility ps. There again isn't a lot of code to write; as with the previous project, the crux of the assignment is understanding what is going on in the relevant portions of xv6.
Review the course policy on academic integrity.
Certain uses of AI are permitted on this assignment. AI use is not
required.
The basic rule: you may use the code completion features of Copilot but may not use features (such as the coding agent) where code is generated from English language prompts. It is also essential that you understand and think critically about any code suggested by Copilot, both to help develop your C programming skills and because while the code suggestions are often uncannily on target, they are not always exactly what you want.
Using the code explanation features of Copilot Chat is permitted, though be careful that this doesn't spill over into code generation.
Review the policy on late work.
Revise and resubmit applies for this assignment. Review the details of how revise and resubmit works.
To hand in your project:
Make sure that you have included a comment at the beginning of your program containing your name and a description of the program, and that you have auto-formatted it.
Copy your entire scheduling-xv6-lottery directory to your handin directory (/classes/cs331/handin/username).
Check that the handin was successful and that the directory structure is correct: your handin folder /classes/cs331/handin/username should contain a folder scheduling-xv6-lottery which in turn contains a src directory.
Do the steps outlined in reddish boxes below before you start writing code.
Copy the scheduling-xv6-lottery directory (and all of its contents) from /classes/cs331 to your workspace folder (~/cs331/workspace). Make sure that you end up with the scheduling-xv6-lottery directory inside your workspace folder, don't just copy its contents.
Very important! One of the things the autoformatter can do is reorganize the #include directives in your code. While useful in some circumstances, this will break the xv6 code. You should have taken care of this as part of the setup for the previous xv6 project. (If you didn't, go back and look at that now.)
This is largely the same as with the previous xv6 project. The one difference is that if you want to run the provided tests directly (instead of using make test), do so with:
./test-lottery.sh
from the top-level scheduling-xv6-lottery directory.
The provided test cases focus on the system calls. A test program schedtest is provided to help you test your scheduler, but this will need to be run manually. From the xv6 shell prompt in QEMU:
schedtest <n> <ticks> <tickets1> [<tickets2> <tickets3> ...]
where n is the number of processes to start, ticks is the duration (in ticks, which are approximately 10ms each) to let the processes run, and tickets1 etc are the number of tickets assigned to process 1, process 2, and so forth. Note that you do not type the <> or [] symbols — <> denotes something you replace with an actual value and [] denotes optional elements.
Of particular relevance to this project:
scheduling in xv6: Scheduling (in particular, the section on proc.c)
system calls: review the posted slides from class and the references provided in the previous xv6 project relating to system calls
navigating the xv6 source: review the information on grep and VSCode's "Find in Folder" feature from the previous xv6 project
Headers for the available user library routines can be found in include/user.h.
You have one main task: implement lottery scheduling. The project README provides an overview, though note that your implementation should follow what is described below — there are some modified specifications as well as more specifics about how to achieve what the README describes.
To do this:
Work through the sections below, in order. For each, read through the whole section to see what it contains before getting started writing code. There are a number of specific directions and hints that will make things a great deal easier if you are aware of them rather than just launching straight into trying to achieve the specifications.
The first step is to add support for getting information about processes — a system call int getpinfo(struct pstat *) to retrieve the info from the OS and a user command ps that can be run from the shell. (This isn't needed for the scheduler as such, but it will make it possible to see whether the scheduler is working correctly.) To do this:
getpinfo needs to retrieve a lot of information, so we need to define a data structure to hold that info. We'll call this data type pstat, for "process statistics". Use the definition given below rather than the one in the README. Put it in the file include/pstat.h.
Add the getpinfo system call. The header should be int getpinfo(struct pstat *). This will be similar to getreadcount so proceed like you did in the previous project. However, when you get to the body of sys_getpinfo things will be a little different because getpinfo takes a parameter and getreadcount did not. See the discussion below for more on this, and for info on implementing the functionality of getpinfo.
Implement ps.
Definition for pstat:
#ifndef _PSTAT_H_
#define _PSTAT_H_
#include "param.h"
struct pstat {
int inuse[NPROC]; // whether this slot of the process table is in use (1 or 0)
int pid[NPROC]; // the PID of each process
char name[NPROC][16]; // the name of each process
int state[NPROC]; // the state of each process (as int because enum procstate is not visible in user space)
uintp sz[NPROC]; // the size of each process's memory (bytes)
};
#endif // _PSTAT_H_
System calls with parameters: As with getreadcount, it is helpful to look for an example already in the code that is similar to what you want to do. Since you are implementing a system call, look over the headers in include/user.h to find anotehr system call with similar parameters to use as a guide — getpinfo takes a struct, so fstat (which takes a different struct) is a good example.
Looking at the body of sys_fstat, you'll see that it does...something...and then calls a helper function filestat. filestat does the real work of the system call; the something before that call is what deals with the system call's parameters. Remember the flow of control when a user program calls a library routine: the handling of the subroutine call involves pushing the subroutine's arguments onto the stack. So that pstat (well, the address of the pstat — struct pstat * means we've passed a pointer) is on the stack, and the body of sys_getpinfo needs to retrieve it. Also, since we're now in kernel mode, the OS should thoroughly check that the parameters are valid. That's what is going on in sys_fstat — a local variable is declared for each of the system call's parameters and argfd, argptr, and argint (not used by sys_fstat) retrieve various types of parameters from the stack into local variables. Read more about this in the following (start with the section on "fetchint" and continue through "argstr").
Follow a similar pattern for getpinfo — retrieve the parameter values and do basic validity checking in the body of sys_getpinfo (return -1 if there is an error), then call a helper to do the real work. The helper should be implemented in kernel/proc.c — if you look for where filestat is implemented, you'll notice that the convention for file-related system calls is to put the sys_ handler in kernel/sysfile.c and the helper in kernel/file.c. Process-related system calls go into sysproc.c and proc.c.
Finally, on to the actual functionality of getpinfo! First check any preconditions (e.g. that the parameter passed in isn't NULL), then go through the processes in the process table (ptable.proc) and copy the relevant information into the pstat parameter. The return value should follow the usual conventions: return -1 if there is any error (such as a violated precondition) and 0 on success.
Some notes and tips:
xv6 doesn't have a definition for NULL so use the value 0 instead.
The global ptable variable is the process table, and it has a field ptable.proc which is an array holding the per-process state for each process. NPROC is a global constant defining the number of slots in the array. The per-process state is in a structure of type struct proc, which you can see defined in kernel/proc.h.
Since the process table holds an array of processes, not all of the slots in the array may be in use at a given time. A process state of UNUSED indicates an unused slot; the inuse field of pstat should be a boolean reflecting whether this slot of the process table is in use (state != UNUSED) or not.
Care is needed when copying strings — a string variable is of type char * and an assignment statement just copies the pointer. Use safestrcpy to copy the process name. Locate where it is implemented in the xv6 code for a brief comment explaining it; you can look up the standard C strncpy in the man pages to understand the reference to strncpy as xv6's version of that works the same. In short, your usage will be something like the following:
safestrcpy(ps->name[i], ptable.proc[i].name, sizeof(ps->name[i]));
The parameters are the destination (where to copy the string), the source (the string to copy), and the number of characters to copy. sizeof returns the size of what it is passed; the idea here is to only copy as many characters as fit in the space a pstat has for the process names.
Be sure to #include "pstat.h" at the beginning of .c files where you reference the struct pstat type so that the compiler can find the definition. In header files (include/user.h and include/defs.h) add struct pstat at the beginning instead.