In this course we will implement a big project in subsequent phases. In each project we will be adding some new functionality to GeekOS. GeekOS is a tiny operating system kernel for x86 PCs. Its main purpose is to serve as a simple but realistic example of an OS kernel running on real hardware. To run our OS, we need to either use an actual machine (which must be an x86 machine) or the other (simpler) idea is to use an emulator; we will use QEMU.
The purpose of this assignment (project0) is to get you familiar with the GeekOS development environment, including the QEMU x86 emulator and the debugger gdb. The assignment is to modify GeekOS to impose resource limits on GeekOS processes.
$ /c/b.exe 1 2 3 4And it would print out the four arguments that you provided. The shell also takes a number of directives. For example, typing exit will terminate the shell (and your interaction with the OS). Look at src/user/shell.c to understand the other features of the shell.
In writing an operating system, you want to make a distinction between the activities that operating systems code are allowed to do and the activities user programs are allowed to do. The goal is to protect the system from incorrect or malicious code that a user may try to run. Bad things a program could do include:
Preventing these sorts of mistakes or attacks is accomplished by controlling the parts of memory that can be accessed when user code is running and limiting the set of machine operations that the user code can execute. The x86 processor provides the operating system with facilities to support these controls. A program that is running in this sort of controlled way is said to be running in user mode.
In GeekOS, there is a distinction between Kernel Threads and User Threads. As you would expect, kernel activities (that is, processes) run as kernel threads, while user programs (or rather, user processes) run in user threads. A kernel thread is represented by a Kernel_Thread structure (in include/geekos/kthread.h)
struct Kernel_Thread { ulong_t esp; /* offset 0 */ volatile ulong_t numTicks; /* offset 4 */ int priority; DEFINE_LINK(Thread_Queue, Kernel_Thread); void* stackPage; struct User_Context* userContext; struct Kernel_Thread* owner; int refCount; ... }
The kernel thread contains all of the mechanisms necessary to run and schedule a process. In order to represent user processes, GeekOS includes an extra field in the Kernel_Thread structure that points to a User_Context structure. In a thread representing a kernel process, the User_Context will be NULL. The User_Context is defined in include/geekos/user.h:
struct User_Context { /* We need one LDT entry each for user code and data segments. */ #define NUM_USER_LDT_ENTRIES 2 /* * Each user context contains a local descriptor table with * just enough room for one code and one data segment * describing the process's memory. */ struct Segment_Descriptor ldt[NUM_USER_LDT_ENTRIES]; struct Segment_Descriptor* ldtDescriptor; /* The memory space used by the process. */ char* memory; ulong_t size; /* Selector for the LDT's descriptor in the GDT */ ushort_t ldtSelector; ... };
Most of the information in the User_Context structure has to do with the memory layout for the process, and you don't need to understand that for now (not until project 2). You will have to modify User_Context to contain some bookkeeping information as part of this project.
User threads are created using the routine Start_User_Thread. This takes as its first argument the User_Context to run within that thread. This context is created by the Sys_Spawn system call. A system call is like a function call that a user program makes to the OS kernel, to ask it to perform some service. System calls are a proper topic we'll cover shortly in the course, but we'll tell you now what you need to know about them for this project.
The operating system kernel presents an interface to user processes for the services it will perform on their behalf. These are essentially functions. However, rather than literally performing a function call to access them, user processes have to use a special mechanism, called a system call. System calls are designed to protect the kernel's memory from malicious processes. On Intel's x86 processor, a user process issues a system call via a trap, which indicates that a system call is being requested; the identity of the system call routine and/or the arguments to pass to it are stored in the processor registers. This mechanism is hidden from the typical C programmer, since it is typically used only in the standard C library routines.
In GeekOS, when a user process issues a system call, the trap causes the routine Syscall_Handler in geekos/trap.c to be invoked. This routine examines the current value in the register eax and calls the appropriate system routine to handle the syscall request. The value in eax is called the Syscall Number. The routine that handles the syscall request is passed a copy of the caller's context state, so the values in general registers (ebx, ecx, edx) can be used by the user program to pass parameters to the handler routine and can be used to return values to the user program. This state is defined in the struct Interrupt_State, defined in geekos/int.h.
The routines that implement GeekOS system calls are in geekos/syscall.c. The Sys_Spawn code is implemented here, along with code for other system calls, like Sys_Exit for the system call used by a process to terminate itself. If you are curious how system calls are invoked from user processes, take a look at src/user/shell.c in your project distribution; you'll be modifying this program in your next project.
The purpose of this assignment is to modify GeekOS to impose resource limits on GeekOS processes:
Finally, you need to create a userlevel process that calls some system calls a desired number of times.
In order to limit the number of system calls, you need to modify the User_Context structure (defined in include/geekos/user.h) to include a counter for the number of system calls the user process has performed. Then you should modify the kernel to update this counter each time it performs a system call on this process's behalf. For the system call that exceeds the limit (i.e., the N+1st system call), Sys_Exit should be called instead to kill the process.
The system call should return a negative value for invialid paramters to the limit system call including requesting a limit on a resource other than 0, and for a negative limit value.
Look at the code provided in src/user/* to see how to use the system call interface provided. Write a user-level process called syscall.exe that takes as two arguments L and P where L is the limit on the number of system calls for that process (i.e. call Limit(0, L)) and P is the number of times to call the null system call should be made.
Unpack and compile correctly and we can see that you've made a good effort at the project | 20 |
Adding a system call to limit the number of system calls correctly | 30 |
Limiting number of system calls correctly | 20 |
Testing syscall.exe L P | 30 |
Total | 100 |