What even is Xv6? What is a system call? Why such a random operating system to work with in the first place?
What is Xv6?
Imagine you have successfully managed to trick some students to sign up as computer science majors at your university and now you need to offer an Operating System class just like any other university. What do you do in that class? Teach them how operating systems do what they do to a reasonable depth. But you can't bring up Linux's entire source code and expect them to understand and furthermore implement on top of it. You need something simple and student-friendly. Something that its compilation does not blow half your class time away.
That's when Xv6 comes into play. Xv6 is an operating system developed by MIT in 2006 as a teaching tool. It's designed to be simple, yet comprehensive enough to illustrate some of the key concepts of an operating systems. Think of it like a stripped-down version of Unix, which is an operating system that's been around for ages.
Now, what's cool about Xv6 is that it's built on top of another operating system called Unix Version 6 (hence the name "Xv6"). Unix Version 6 was developed in the 1970s, and it's considered one of the early versions of Unix. So, Xv6 is like a modernized, simplified version of this old-school Unix system.
This thing is so light-weight that you can build the entire operating system in less than 10 seconds. To put it into perspective, the building process of the Linux kernel could take you about 30-60 minutes depending on your machine. Obviously, there is going to be no GUI or anything fancy for Xv6, just a shell that understands a very limited list of commands. Here is the full list of c
programs that already exist in Xv6 when you set it up.
cat
echo
grep
init
kill
ln
ls
mkdir
rm
sh
stressfs
usertests
wc
zombie
Yeah... that's it.
What is a syscall?
A syscall, short for "system call", is like a secret handshake between a running program and the operating system. When a program needs to do something that only the operating system can do—like reading from a file, creating a new process, or even just exiting—it can't just do it on its own. Instead, it has to ask the operating system nicely by making a syscall as the OS has higher privileges.
So, think of syscalls as requests that programs send to the operating system for help with tasks that require special privileges or access to system resources. The operating system then handles these requests, performs the requested action, and returns the result to the program. It's like the program saying, "Hey OS, can you do this thing for me?" and the OS responding, "Sure" if allowed, or brutally killing the program in case it's trying to do something sneaky and malicious.
Implementing the getreadcount
syscall
We are going to implement the getreadcount
syscall. What it does is very simple: It keeps track of how many times the read
syscall has been invoked starting from the compilation of the kernel.
Idea behind the implementation
It's actually very straightforward when you think about it for a second. You need a global variable (let's call it readcount
) that is incremented by one every time the syscall read
is called, and you also need to define the syscall called getreadcount
that returns the value of readcount
. That's it.
Where? How?
There are multiple files (5 to be exact) that you need to modify in order for your system call to be defined properly. Here is the list:
syscall.c
syscall.h
sysfile.c
user.h
usys.S
// <- Add this line
syscall.c
changes
// rest of the file
extern int sys_read(void);
extern int sys_sbrk(void);
extern int sys_sleep(void);
extern int sys_unlink(void);
extern int sys_wait(void);
extern int sys_write(void);
extern int sys_uptime(void);
extern int sys_getreadcount(void); // <- Add this line
extern int readcount; // <- Add this line
static int (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
[SYS_getreadcount] sys_getreadcount, // <- Add this line
};
void
syscall(void)
{
int num;
struct proc *curproc = myproc();
num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
if (num == SYS_read) readcount++; // <- Add this line
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}
// rest of the file
extern int sys_getreadcount(void);
declares a system call function for getting the read count.extern int readcount;
declares a global variable to keep track of the number of reads.[SYS_getreadcount] sys_getreadcount,
adds thegetreadcount
system call to the syscall table, which allows it to be called by user programs.if (num == SYS_read) readcount++;
incrementsreadcount
every time theread
system call is executed.
syscall.h
changes
// rest of the file
#define SYS_getreadcount 22 // <- Add this line
Assuming you have not previously added any other syscalls, and you have the default number of syscalls when setting up Xv6 (21 syscalls), this one is going to be numbered 22. If not, you can change the number accordingly.
sysfile.c
changes
#include "types.h"
#include "defs.h"
#include "param.h"
#include "stat.h"
#include "mmu.h"
#include "proc.h"
#include "fs.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "fcntl.h"
int readcount; // <- Add this line
int // <- Add this line
sys_getreadcount(void) { // <- Add this line
return readcount; // <- Add this line
} // <- Add this line
// rest of the file
int readcount;
declares a global variablereadcount
.int sys_getreadcount(void) {...}
defines a new system call functionsys_getreadcount
that, when called, returns the current value ofreadcount
.
readcount
declared globally twice? Once with the extern
keyword and once without, in another file?Declaring a variable with extern
in one file and defining it globally in another is pretty common in C for managing global variables. The extern
keyword in syscall.c
basically tells the compiler the following:
Hey, trust me the variable
readcount
is defined elsewhere. Don't allocate space for it again but recognize its existence and allow linkage for now. When the program is fully compiled and linked later, everything is going to make sense as all files come together.
user.h
changes
struct stat;
struct rtcdate;
// system calls
int fork(void);
int exit(void) __attribute__((noreturn));
int wait(void);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
int getreadcount(void); // <- Add this line
// rest of the file
usys.S
changes
// rest of the file
SYSCALL(getreadcount) // <- Add this line
SYSCALL(getreadcount)
registers the syscall so it can be invoked from user space.
There you go! That's it. You have successfully implemented the system call.
readcount
to 0
? We never did that, right? How does the computer know what number to start incrementing that variable from?The answer is simple. The readcount
variable automatically initializes to 0
due to its placement in the BSS segment, which stands for "Block Starting Symbol." The BSS segment is a special area in memory that holds uninitialized global and static variables and fills them with zeroes at program start. And since our variable is defined in the BSS segment, we don't need to manually initialize readcount
to 0
!
Test your syscall implementation
In your Xv6 main folder, create a new file called readSyscallTest.c
and copy-paste the following code inside:
#include "types.h"
#include "user.h"
#include "param.h"
#define assert(x) \
if (x) \
{ /* pass */ \
} \
else \
{ \
printf(1, "assert failed %s %s %d\n", #x, __FILE__, __LINE__); \
exit(); \
}
void readfile(char *file, int howmany)
{
int i;
int fd = open(file, 0);
char buf;
for (i = 0; i < howmany; i++)
(void)read(fd, &buf, 1);
close(fd);
}
int main(int argc, char *argv[])
{
int rc1 = getreadcount();
printf(1, "Read count %d\n", rc1);
int rc = fork();
if (rc < 0)
{
printf(1, "Fork failed!\n");
exit();
}
readfile("README", 5);
if (rc > 0)
{
wait();
int rc2 = getreadcount();
printf(1, "Read count %d\n", rc2);
assert((rc2 - rc1) == 10);
printf(1, "TEST PASSED\n");
}
exit();
}
Then make the following changes to your Makefile
:
UPROGS=\
_cat\
_echo\
_forktest\
_grep\
_init\
_kill\
_ln\
_ls\
_mkdir\
_rm\
_sh\
_stressfs\
_usertests\
_wc\
_zombie\
_readSyscallTest\ # <- Add this section
# rest of the file
EXTRA=\
mkfs.c ulib.c user.h cat.c echo.c forktest.c grep.c kill.c\
ln.c ls.c mkdir.c rm.c stressfs.c usertests.c wc.c zombie.c\
printf.c umalloc.c\
readSyscallTest.c\ # <- Add this section
README dot-bochsrc *.pl toc.* runoff runoff1 runoff.list\
.gdbinit.tmpl gdbutil\
# rest of the file
Make sure you save the changes on the newly created file as well as Makefile
.
Run make clean
and then make qemu
and try running the program after the build is successful using readSyscallTest
.
In case of any errors occurring, make sure you don't have any typos, and have not missed any steps in the instructions. Cheers.