[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

2. Project 0: Introducing Pintos

In this assignment, you will learn about the existing functionality in Pintos, and add two small features to the system: a more efficient implementation of sleep, and the ability to pass command line arguments to user programs.

You will be working in the threads directory for the first part of this assignment (with some work in the devices directory on the side), and modify the file userprog/process.c in the second part.

The tests for Project 0 are executed by changing the working directory to intro and running make followed by make check.

Before you read the description of this project, you should read all of the following sections: 1. Introduction, B. Coding Standards, D. Debugging Tools, and E. Development Tools. You should at least skim the material from A.1 Loading through A.5 Memory Allocation, especially A.3 Synchronization.


2.1 Understanding Threads

The first step is to read and understand the code for the initial thread system. Pintos already implements thread creation and thread completion, a simple scheduler to switch between threads, and synchronization primitives (semaphores, locks, condition variables, and optimization barriers).

Some of this code might seem slightly mysterious. If you haven't already compiled and run the base system, as described in the introduction (see section 1. Introduction), you should do so now. You can read through parts of the source code to see what's going on. If you like, you can add calls to printf() almost anywhere, then recompile and run to see what happens and in what order. You can also run the kernel in a debugger and set breakpoints at interesting spots, single-step through code and examine data, and so on.

When a thread is created, you are creating a new context to be scheduled. You provide a function to be run in this context as an argument to thread_create(). The first time the thread is scheduled and runs, it starts from the beginning of that function and executes in that context. When the function returns, the thread terminates. Each thread, therefore, acts like a mini-program running inside Pintos, with the function passed to thread_create() acting like main().

At any given time, exactly one thread runs and the rest, if any, become inactive. The scheduler decides which thread to run next. (If no thread is ready to run at any given time, then the special "idle" thread, implemented in idle(), runs.) Synchronization primitives can force context switches when one thread needs to wait for another thread to do something.

The mechanics of a context switch are in threads/switch.S, which is 80x86 assembly code. (You don't have to understand it.) It saves the state of the currently running thread and restores the state of the thread we're switching to.

Using the GDB debugger, slowly trace through a context switch to see what happens (see section D.5 GDB). You can set a breakpoint on schedule() to start out, and then single-step from there.(1) Be sure to keep track of each thread's address and state, and what procedures are on the call stack for each thread. You will notice that when one thread calls switch_threads(), another thread starts running, and the first thing the new thread does is to return from switch_threads(). You will understand the thread system once you understand why and how the switch_threads() that gets called is different from the switch_threads() that returns. See section A.2.3 Thread Switching, for more information.

Warning: In Pintos, each thread is assigned a small, fixed-size execution stack just under 4 kB in size. The kernel tries to detect stack overflow, but it cannot do so perfectly. You may cause bizarre problems, such as mysterious kernel panics, if you declare large data structures as non-static local variables, e.g. int buf[1000];. Alternatives to stack allocation include the page allocator and the block allocator (see section A.5 Memory Allocation).


2.1.1 Source Files

Here is a brief overview of the files in the threads directory. You will not need to modify most of this code, but the hope is that presenting this overview will give you a start on what code to look at.

loader.S
loader.h
The kernel loader. Assembles to 512 bytes of code and data that the PC BIOS loads into memory and which in turn finds the kernel on disk, loads it into memory, and jumps to start() in start.S. See section A.1.1 The Loader, for details. You should not need to look at this code or modify it.

start.S
Does basic setup needed for memory protection and 32-bit operation on 80x86 CPUs. Unlike the loader, this code is actually part of the kernel. See section A.1.2 Low-Level Kernel Initialization, for details.

kernel.lds.S
The linker script used to link the kernel. Sets the load address of the kernel and arranges for start.S to be near the beginning of the kernel image. See section A.1.1 The Loader, for details. Again, you should not need to look at this code or modify it, but it's here in case you're curious.

init.c
init.h
Kernel initialization, including main(), the kernel's "main program." You should look over main() at least to see what gets initialized. You might want to add your own initialization code here. See section A.1.3 High-Level Kernel Initialization, for details.

thread.c
thread.h
Basic thread support. Much of your work will take place in these files. thread.h defines struct thread, which you are likely to modify in all three projects. See A.2.1 struct thread and A.2 Threads for more information.

switch.S
switch.h
Assembly language routine for switching threads. Already discussed above. See section A.2.2 Thread Functions, for more information.

palloc.c
palloc.h
Page allocator, which hands out system memory in multiples of 4 kB pages. See section A.5.1 Page Allocator, for more information.

malloc.c
malloc.h
A simple implementation of malloc() and free() for the kernel. See section A.5.2 Block Allocator, for more information.

interrupt.c
interrupt.h
Basic interrupt handling and functions for turning interrupts on and off. See section A.4 Interrupt Handling, for more information.

intr-stubs.S
intr-stubs.h
Assembly code for low-level interrupt handling. See section A.4.1 Interrupt Infrastructure, for more information.

synch.c
synch.h
Basic synchronization primitives: semaphores, locks, condition variables, and optimization barriers. You will need to use these for synchronization in all four projects. See section A.3 Synchronization, for more information.

io.h
Functions for I/O port access. This is mostly used by source code in the devices directory that you won't have to touch.

vaddr.h
pte.h
Functions and macros for working with virtual addresses and page table entries. These will be more important to you in project 2. For now, you can ignore them.

flags.h
Macros that define a few bits in the 80x86 "flags" register. Probably of no interest. See [ IA32-v1], section 3.4.3, "EFLAGS Register," for more information.


2.1.1.1 devices code

The basic threaded kernel also includes these files in the devices directory:

timer.c
timer.h
System timer that ticks, by default, 100 times per second. You will modify this code in this project.

vga.c
vga.h
VGA display driver. Responsible for writing text to the screen. You should have no need to look at this code. printf() calls into the VGA display driver for you, so there's little reason to call this code yourself.

serial.c
serial.h
Serial port driver. Again, printf() calls this code for you, so you don't need to do so yourself. It handles serial input by passing it to the input layer (see below).

block.c
block.h
An abstraction layer for block devices, that is, random-access, disk-like devices that are organized as arrays of fixed-size blocks. Out of the box, Pintos supports two types of block devices: IDE disks and partitions.

ide.c
ide.h
Supports reading and writing sectors on up to 4 IDE disks.

partition.c
partition.h
Understands the structure of partitions on disks, allowing a single disk to be carved up into multiple regions (partitions) for independent use.

kbd.c
kbd.h
Keyboard driver. Handles keystrokes passing them to the input layer (see below).

input.c
input.h
Input layer. Queues input characters passed along by the keyboard or serial drivers.

intq.c
intq.h
Interrupt queue, for managing a circular queue that both kernel threads and interrupt handlers want to access. Used by the keyboard and serial drivers.

rtc.c
rtc.h
Real-time clock driver, to enable the kernel to determine the current date and time. By default, this is only used by thread/init.c to choose an initial seed for the random number generator.

speaker.c
speaker.h
Driver that can produce tones on the PC speaker.

pit.c
pit.h
Code to configure the 8254 Programmable Interrupt Timer. This code is used by both devices/timer.c and devices/speaker.c because each device uses one of the PIT's output channel.


2.1.1.2 lib files

Finally, lib and lib/kernel contain useful library routines. (lib/user will be used by user programs, starting in project 2, but it is not part of the kernel.) Here's a few more details:

ctype.h
inttypes.h
limits.h
stdarg.h
stdbool.h
stddef.h
stdint.h
stdio.c
stdio.h
stdlib.c
stdlib.h
string.c
string.h
A subset of the standard C library. See section B.2 C99, for information on a few recently introduced pieces of the C library that you might not have encountered before. See section B.3 Unsafe String Functions, for information on what's been intentionally left out for safety.

debug.c
debug.h
Functions and macros to aid debugging. See section D. Debugging Tools, for more information.

random.c
random.h
Pseudo-random number generator. The actual sequence of random values will not vary from one Pintos run to another, unless you do one of three things: specify a new random seed value on the -rs kernel command-line option on each run, or use a simulator other than Bochs, or specify the -r option to pintos.

round.h
Macros for rounding.

syscall-nr.h
System call numbers. Not used until project 2.

kernel/list.c
kernel/list.h
Doubly linked list implementation. Used all over the Pintos code, and you'll probably want to use it a few places yourself.

kernel/bitmap.c
kernel/bitmap.h
Bitmap implementation. You can use this in your code if you like, but you probably won't have any need for it in project 0 and project 1.

kernel/hash.c
kernel/hash.h
Hash table implementation.

kernel/console.c
kernel/console.h
kernel/stdio.h
Implements printf() and a few other functions.


2.1.2 Synchronization

Proper synchronization is an important part of the solutions to these problems. Any synchronization problem can be easily solved by turning interrupts off: while interrupts are off, there is no concurrency, so there's no possibility for race conditions. Therefore, it's tempting to solve all synchronization problems this way, but don't. Instead, use semaphores, locks, and condition variables to solve the bulk of your synchronization problems. Read the tour section on synchronization (see section A.3 Synchronization) or the comments in threads/synch.c if you're unsure what synchronization primitives may be used in what situations.

In the Pintos projects, the only class of problem best solved by disabling interrupts is coordinating data shared between a kernel thread and an interrupt handler. Because interrupt handlers can't sleep, they can't acquire locks. This means that data shared between kernel threads and an interrupt handler must be protected within a kernel thread by turning off interrupts.

This project only requires accessing a little bit of thread state from interrupt handlers. For the alarm clock, the timer interrupt needs to wake up sleeping threads. In the advanced scheduler, the timer interrupt needs to access a few global and per-thread variables. When you access these variables from kernel threads, you will need to disable interrupts to prevent the timer interrupt from interfering.

When you do turn off interrupts, take care to do so for the least amount of code possible, or you can end up losing important things such as timer ticks or input events. Turning off interrupts also increases the interrupt handling latency, which can make a machine feel sluggish if taken too far.

The synchronization primitives themselves in synch.c are implemented by disabling interrupts. You may need to increase the amount of code that runs with interrupts disabled here, but you should still try to keep it to a minimum.

Disabling interrupts can be useful for debugging, if you want to make sure that a section of code is not interrupted. You should remove debugging code before turning in your project. (Don't just comment it out, because that can make the code difficult to read.)

There should be no busy waiting in your submission. A tight loop that calls thread_yield() is one form of busy waiting.


2.1.3 Development Suggestions

In the past, many groups divided the assignment into pieces, then each group member worked on his or her piece until just before the deadline, at which time the group reconvened to combine their code and submit. This is a bad idea. We do not recommend this approach. Groups that do this often find that two changes conflict with each other, requiring lots of last-minute debugging. Some groups who have done this have turned in code that did not even compile or boot, much less pass any tests.

Instead, we recommend integrating your team's changes early and often, using the source code control system git. This is less likely to produce surprises, because everyone can see everyone else's code as it is written, instead of just when it is finished. These systems also make it possible to review changes and, when a change introduces a bug, drop back to working versions of code.

You should expect to run into bugs that you simply don't understand while working on this and subsequent projects. When you do, reread the appendix on debugging tools, which is filled with useful debugging tips that should help you to get back up to speed (see section D. Debugging Tools). Be sure to read the section on backtraces (see section D.4 Backtraces), which will help you to get the most out of every kernel panic or assertion failure.


2.2 Understanding User Programs

The tests for both the alarm clock assignment in Project 0, and the priority scheduler in Project 1, run as part of the operating system kernel, with full access to privileged parts of the system. Once we start running user programs on top of the operating system, this is no longer true.

We allow more than one process to run at a time. Each process has one thread (multithreaded processes are not supported). User programs are written under the illusion that they have the entire machine. This means that when you load and run multiple processes at a time, you must manage memory, scheduling, and other state correctly to maintain this illusion.

In Project 2, we will test your operating system by running user programs. This gives you much greater freedom. You must make sure that the user program interface meets the specifications described here, but given that constraint you are free to restructure or rewrite kernel code however you wish.


2.2.1 Source Files

process.c
process.h
Loads ELF binaries and starts processes.

pagedir.c
pagedir.h
A simple manager for 80x86 hardware page tables. Although you probably won't want to modify this code for this project, you may want to call some of its functions. See Page Tables, for more information.

syscall.c
syscall.h
Whenever a user process wants to access some kernel functionality, it invokes a system call.

exception.c
exception.h
When a user process performs a privileged or prohibited operation, it traps into the kernel as an "exception" or "fault."(2) These files handle exceptions. In project 2, you will need to modify the page fault handler to support lazy page loading and stack growth.

gdt.c
gdt.h
The 80x86 is a segmented architecture. The Global Descriptor Table (GDT) is a table that describes the segments in use. These files set up the GDT. You should not need to modify these files for any of the projects. You can read the code if you're interested in how the GDT works.

tss.c
tss.h
The Task-State Segment (TSS) is used for 80x86 architectural task switching. Pintos uses the TSS only for switching stacks when a user process enters an interrupt handler, as does Linux. You should not need to modify these files for any of the projects. You can read the code if you're interested in how the TSS works.


2.2.2 Using the File System

You will need to interface to the file system code, because user programs are loaded from the file system and most of the system calls you must implement deal with the file system. You will want to look over the filesys.h and file.h interfaces to understand how to use the file system, and especially its many limitations.

There is no need to modify the file system code in this course, and so we recommend that you do not. Working on the file system is likely to distract you from the project's foci.

You will have to tolerate the following limitations, however:

One important feature is included:

You need to be able to create a simulated disk with a file system partition. The pintos-mkdisk program provides this functionality. From the userprog/build directory, execute pintos-mkdisk filesys.dsk --filesys-size=2. This command creates a simulated disk named filesys.dsk that contains a 2 MB Pintos file system partition. Then format the file system partition by passing -f -q on the kernel's command line: pintos -f -q. The -f option causes the file system to be formatted, and -q causes Pintos to exit as soon as the format is done.

You'll need a way to copy files in and out of the simulated file system. The pintos -p ("put") and -g ("get") options do this. To copy file into the Pintos file system, use the command pintos -p file -- -q. (The -- is needed because -p is for the pintos script, not for the simulated kernel.) To copy it to the Pintos file system under the name newname, add -a newname: pintos -p file -a newname -- -q. The commands for copying files out of a VM are similar, but substitute -g for -p.

Incidentally, these commands work by passing special commands extract and append on the kernel's command line and copying to and from a special simulated "scratch" partition. If you're very curious, you can look at the pintos script as well as filesys/fsutil.c to learn the implementation details.

Here's a summary of how to create a disk with a file system partition, format the file system, copy the echo program into the new disk, and then run echo, passing argument x. (Argument passing won't work until you implemented it.) It assumes that you've already built the examples in examples and that the current directory is userprog/build:

 
pintos-mkdisk filesys.dsk --filesys-size=2
pintos -f -q
pintos -p ../../examples/echo -a echo -- -q
pintos -q run 'echo x'

The three final steps can actually be combined into a single command:

 
pintos-mkdisk filesys.dsk --filesys-size=2
pintos -p ../../examples/echo -a echo -- -f -q run 'echo x'

If you don't want to keep the file system disk around for later use or inspection, you can even combine all four steps into a single command. The --filesys-size=n option creates a temporary file system partition approximately n megabytes in size just for the duration of the pintos run. The Pintos automatic test suite makes extensive use of this syntax:

 
pintos --filesys-size=2 -p ../../examples/echo -a echo -- -f -q run 'echo x'

You can delete a file from the Pintos file system using the rm file kernel action, e.g. pintos -q rm file. Also, ls lists the files in the file system and cat file prints a file's contents to the display.


2.2.3 How User Programs Work

Pintos can run normal C programs, as long as they fit into memory and use only the system calls you implement. Notably, malloc() cannot be implemented because none of the system calls required for this project allow for memory allocation. Pintos also can't run programs that use floating point operations, since the kernel doesn't save and restore the processor's floating-point unit when switching threads.

The src/examples directory contains a few sample user programs. The Makefile in this directory compiles the provided examples, and you can edit it compile your own programs as well. Some of the example programs will not work with the current implementation of Pintos.

Pintos can load ELF executables with the loader provided for you in userprog/process.c. ELF is a file format used by Linux, Solaris, and many other operating systems for object files, shared libraries, and executables. You can actually use any compiler and linker that output 80x86 ELF executables to produce programs for Pintos. (We've provided compilers and linkers that should do just fine.)

You should realize immediately that, until you copy a test program to the simulated file system, Pintos will be unable to do useful work. You won't be able to do interesting things until you copy a variety of programs to the file system. You might want to create a clean reference file system disk and copy that over whenever you trash your filesys.dsk beyond a useful state, which may happen occasionally while debugging.


2.2.4 Virtual Memory Layout

Virtual memory in Pintos is divided into two regions: user virtual memory and kernel virtual memory. User virtual memory ranges from virtual address 0 up to PHYS_BASE, which is defined in threads/vaddr.h and defaults to 0xc0000000 (3 GB). Kernel virtual memory occupies the rest of the virtual address space, from PHYS_BASE up to 4 GB.

User virtual memory is per-process. When the kernel switches from one process to another, it also switches user virtual address spaces by changing the processor's page directory base register (see pagedir_activate() in userprog/pagedir.c). struct thread contains a pointer to a process's page table.

Kernel virtual memory is global. It is always mapped the same way, regardless of what user process or kernel thread is running. In Pintos, kernel virtual memory is mapped one-to-one to physical memory, starting at PHYS_BASE. That is, virtual address PHYS_BASE accesses physical address 0, virtual address PHYS_BASE + 0x1234 accesses physical address 0x1234, and so on up to the size of the machine's physical memory.

A user program can only access its own user virtual memory. An attempt to access kernel virtual memory causes a page fault, handled by page_fault() in userprog/exception.c, and the process will be terminated. Kernel threads can access both kernel virtual memory and, if a user process is running, the user virtual memory of the running process. However, even in the kernel, an attempt to access memory at an unmapped user virtual address will cause a page fault.


2.2.4.1 Typical Memory Layout

Conceptually, each process is free to lay out its own user virtual memory however it chooses. In practice, user virtual memory is laid out like this:

 
   PHYS_BASE +----------------------------------+
             |            user stack            |
             |                 |                |
             |                 |                |
             |                 V                |
             |          grows downward          |
             |                                  |
             |                                  |
             |                                  |
             |                                  |
             |           grows upward           |
             |                 ^                |
             |                 |                |
             |                 |                |
             +----------------------------------+
             | uninitialized data segment (BSS) |
             +----------------------------------+
             |     initialized data segment     |
             +----------------------------------+
             |           code segment           |
  0x08048000 +----------------------------------+
             |                                  |
             |                                  |
             |                                  |
             |                                  |
             |                                  |
           0 +----------------------------------+

In this project, the user stack is fixed in size, but in project 2 it will be allowed to grow. Traditionally, the size of the uninitialized data segment can be adjusted with a system call, but you will not have to implement this.

The code segment in Pintos starts at user virtual address 0x08084000, approximately 128 MB from the bottom of the address space. This value is specified in [ SysV-i386] and has no deep significance.

The linker sets the layout of a user program in memory, as directed by a "linker script" that tells it the names and locations of the various program segments. You can learn more about linker scripts by reading the "Scripts" chapter in the linker manual, accessible via info ld.

To view the layout of a particular executable, run objdump (80x86) or i386-elf-objdump (SPARC) with the -p option.


2.2.5 Accessing User Memory

As part of a system call, the kernel must often access memory through pointers provided by a user program. The kernel must be very careful about doing so, because the user can pass a null pointer, a pointer to unmapped virtual memory, or a pointer to kernel virtual address space (above PHYS_BASE). All of these types of invalid pointers must be rejected without harm to the kernel or other running processes, by terminating the offending process and freeing its resources.

There are at least two reasonable ways to do this correctly. The first method is to verify the validity of a user-provided pointer, then dereference it. The second method is to check only that a user pointer points below PHYS_BASE, then dereference it. An invalid user pointer will cause a "page fault" that you can handle by modifying the code for page_fault() in userprog/exception.c. This technique is normally faster because it takes advantage of the processor's MMU, so it tends to be used in real kernels (including Linux). It is also the way access to user pointers is implemented in the Pintos version provided.

In either case, one needs to make sure not to "leak" resources. For example, suppose that your system call has acquired a lock or allocated memory with malloc(). If you encounter an invalid user pointer afterward, you must still be sure to release the lock or free the page of memory. If you choose to verify user pointers before dereferencing them, this should be straightforward. It's more difficult to handle if an invalid pointer causes a page fault, because there's no way to return an error code from a memory access.


2.3 Requirements


2.3.1 Design Document

Before you turn in your project, you must copy the project 0 design document template into your source tree under the name pintos/src/intro/DESIGNDOC and fill it in. We recommend that you read the design document template before you start working on the project. See section C. Project Documentation, for a sample design document that goes along with a fictitious project.


2.3.2 Alarm Clock

Reimplement timer_sleep(), defined in devices/timer.c. Although a working implementation is provided, it "busy waits," that is, it spins in a loop checking the current time and calling thread_yield() until enough time has gone by. Reimplement it to avoid busy waiting.

Function: void timer_sleep (int64_t ticks)
Suspends execution of the calling thread until time has advanced by at least x timer ticks. Unless the system is otherwise idle, the thread need not wake up after exactly x ticks. Just put it on the ready queue after they have waited for the right amount of time.

timer_sleep() is useful for threads that operate in real-time, e.g. for blinking the cursor once per second.

The argument to timer_sleep() is expressed in timer ticks, not in milliseconds or any another unit. There are TIMER_FREQ timer ticks per second, where TIMER_FREQ is a macro defined in devices/timer.h. The default value is 100. We don't recommend changing this value, because any change is likely to cause many of the tests to fail.

Separate functions timer_msleep(), timer_usleep(), and timer_nsleep() do exist for sleeping a specific number of milliseconds, microseconds, or nanoseconds, respectively, but these will call timer_sleep() automatically when necessary. You do not need to modify them.

If your delays seem too short or too long, reread the explanation of the -r option to pintos (see section 1.1.4 Debugging versus Testing).

The tests for the 2.3.2 Alarm Clock assignment are executed by changing the working directory intro. The run make to build the Pintos kernel. Finally run make check to run the tests, followed by make grade to obtain your score.

The alarm clock implementation is not needed for later projects.


2.3.3 Argument Passing

Currently, process_execute() does not support passing arguments to new processes. Implement this functionality, by extending process_execute() so that instead of simply taking a program file name as its argument, it divides it into words at spaces. The first word is the program name, the second word is the first argument, and so on. That is, process_execute("grep foo bar") should run grep passing two arguments foo and bar.

Within a command line, multiple spaces are equivalent to a single space, so that process_execute("grep foo bar") is equivalent to our original example. You can impose a reasonable limit on the length of the command line arguments. For example, you could limit the arguments to those that will fit in a single page (4 kB). (There is an unrelated limit of 128 bytes on command-line arguments that the pintos utility can pass to the kernel.)

You can parse argument strings any way you like. If you're lost, look at strtok_r(), prototyped in lib/string.h and implemented with thorough comments in lib/string.c. You can find more about it by looking at the man page (run man strtok_r at the prompt).

See section 2.5.1 Program Startup Details, for information on exactly how you need to set up the stack.


2.4 FAQ

How much code will I need to write?

Here's a summary of our reference solution, produced by the diffstat program. The final row gives total lines inserted and deleted; a changed line counts as both an insertion and a deletion.

The reference solution represents just one possible solution. Many other solutions are also possible and many of those differ greatly from the reference solution. Some excellent solutions may not modify all the files modified by the reference solution, and some may modify files not modified by the reference solution.

 
 devices/timer.c    |   40 +++++++++++-
 threads/thread.h   |    3 +
 userprog/process.c |  148 ++++++++++++++++++++++++++++++-----------
 3 files changed, 150 insertions(+), 41 deletions(-)


2.4.1 Threads FAQ

How do I update the Makefiles when I add a new source file?

To add a .c file, edit the top-level Makefile.build. Add the new file to variable dir_SRC, where dir is the directory where you added the file. For this project, that means you should add it to threads_SRC or devices_SRC. Then run make. If your new file doesn't get compiled, run make clean and then try again.

When you modify the top-level Makefile.build and re-run make, the modified version should be automatically copied to threads/build/Makefile. The converse is not true, so any changes will be lost the next time you run make clean from the threads directory. Unless your changes are truly temporary, you should prefer to edit Makefile.build.

A new .h file does not require editing the Makefiles.

What does warning: no previous prototype for `func' mean?

It means that you defined a non-static function without preceding it by a prototype. Because non-static functions are intended for use by other .c files, for safety they should be prototyped in a header file included before their definition. To fix the problem, add a prototype in a header file that you include, or, if the function isn't actually used by other .c files, make it static.

What is the interval between timer interrupts?

Timer interrupts occur TIMER_FREQ times per second. You can adjust this value by editing devices/timer.h. The default is 100 Hz.

We don't recommend changing this value, because any changes are likely to cause many of the tests to fail.

How long is a time slice?

There are TIME_SLICE ticks per time slice. This macro is declared in threads/thread.c. The default is 4 ticks.

We don't recommend changing this value, because any changes are likely to cause many of the tests to fail.

How do I run the tests?

See section 1.2.1 Testing.

See section D.4 Backtraces, for more information.


2.4.2 Alarm Clock FAQ

Do I need to account for timer values overflowing?

Don't worry about the possibility of timer values overflowing. Timer values are expressed as signed 64-bit numbers, which at 100 ticks per second should be good for almost 2,924,712,087 years. By then, we expect Pintos to have been phased out of the curriculum.


2.4.3 Userprog FAQ

The kernel always panics when I run pintos -p file -- -q.

Did you format the file system (with pintos -f)?

Is your file name too long? The file system limits file names to 14 characters. A command like pintos -p ../../examples/echo -- -q will exceed the limit. Use pintos -p ../../examples/echo -a echo -- -q to put the file under the name echo instead.

Is the file system full?

Does the file system already contain 16 files? The base Pintos file system has a 16-file limit.

The file system may be so fragmented that there's not enough contiguous space for your file.

When I run pintos -p ../file --, file isn't copied.

Files are written under the name you refer to them, by default, so in this case the file copied in would be named ../file. You probably want to run pintos -p ../file -a file -- instead.

You can list the files in your file system with pintos -q ls.

All my user programs die with page faults.

This will happen if you haven't implemented argument passing (or haven't done so correctly). The basic C library for user programs tries to read argc and argv off the stack. If the stack isn't properly set up, this causes a page fault.

How can I disassemble user programs?

The objdump (80x86) or i386-elf-objdump (SPARC) utility can disassemble entire user programs or object files. Invoke it as objdump -d file. You can use GDB's disassemble command to disassemble individual functions (see section D.5 GDB).

Why do many C include files not work in Pintos programs?
Can I use libfoo in my Pintos programs?

The C library we provide is very limited. It does not include many of the features that are expected of a real operating system's C library. The C library must be built specifically for the operating system (and architecture), since it must make system calls for I/O and memory allocation. (Not all functions do, of course, but usually the library is compiled as a unit.)

The chances are good that the library you want uses parts of the C library that Pintos doesn't implement. It will probably take at least some porting effort to make it work under Pintos. Notably, the Pintos user program C library does not have a malloc() implementation.

How do I compile new user programs?

Modify src/examples/Makefile, then run make.

Can I run user programs under a debugger?

Yes, with some limitations. See section D.5 GDB.

How can I run user programs that need more than 4 kB stack space?

You may modify the stack setup code to allocate more than one page of stack space for each process. In project 2, you will implement a better solution.

What happens when an open file is removed?

You should implement the standard Unix semantics for files. That is, when a file is removed any process which has a file descriptor for that file may continue to use that descriptor. This means that they can read and write from the file. The file will not have a name, and no other processes will be able to open it, but it will continue to exist until all file descriptors referring to the file are closed or the machine shuts down.


2.4.4 Argument Passing FAQ

Isn't the top of stack in kernel virtual memory?

The top of stack is at PHYS_BASE, typically 0xc0000000, which is also where kernel virtual memory starts. But before the processor pushes data on the stack, it decrements the stack pointer. Thus, the first (4-byte) value pushed on the stack will be at address 0xbffffffc.

Is PHYS_BASE fixed?

No. You should be able to support PHYS_BASE values that are any multiple of 0x10000000 from 0x80000000 to 0xf0000000, simply via recompilation.


2.5 80x86 Calling Convention

This section summarizes important points of the convention used for normal function calls on 32-bit 80x86 implementations of Unix. Some details are omitted for brevity. If you do want all the details, refer to [ SysV-i386].

The calling convention works like this:

  1. The caller pushes each of the function's arguments on the stack one by one, normally using the PUSH assembly language instruction. Arguments are pushed in right-to-left order.

    The stack grows downward: each push decrements the stack pointer, then stores into the location it now points to, like the C expression *--sp = value.

  2. The caller pushes the address of its next instruction (the return address) on the stack and jumps to the first instruction of the callee. A single 80x86 instruction, CALL, does both.

  3. The callee executes. When it takes control, the stack pointer points to the return address, the first argument is just above it, the second argument is just above the first argument, and so on.

  4. If the callee has a return value, it stores it into register EAX.

  5. The callee returns by popping the return address from the stack and jumping to the location it specifies, using the 80x86 RET instruction.

  6. The caller pops the arguments off the stack.

Consider a function f() that takes three int arguments. This diagram shows a sample stack frame as seen by the callee at the beginning of step 3 above, supposing that f() is invoked as f(1, 2, 3). The initial stack address is arbitrary:

 
                             +----------------+
                  0xbffffe7c |        3       |
                  0xbffffe78 |        2       |
                  0xbffffe74 |        1       |
stack pointer --> 0xbffffe70 | return address |
                             +----------------+


2.5.1 Program Startup Details

The Pintos C library for user programs designates _start(), in lib/user/entry.c, as the entry point for user programs. This function is a wrapper around main() that calls exit() if main() returns:

 
void
_start (int argc, char *argv[])
{
  exit (main (argc, argv));
}

The kernel must put the arguments for the initial function on the stack before it allows the user program to begin executing. The arguments are passed in the same way as the normal calling convention (see section 2.5 80x86 Calling Convention).

Consider how to handle arguments for the following example command: /bin/ls -l foo bar. First, break the command into words: /bin/ls, -l, foo, bar. Place the words at the top of the stack. Order doesn't matter, because they will be referenced through pointers.

Then, push the address of each string plus a null pointer sentinel, on the stack, in right-to-left order. These are the elements of argv. The null pointer sentinel ensures that argv[argc] is a null pointer, as required by the C standard. The order ensures that argv[0] is at the lowest virtual address. Word-aligned accesses are faster than unaligned accesses, so for best performance round the stack pointer down to a multiple of 4 before the first push.

Then, push argv (the address of argv[0]) and argc, in that order. Finally, push a fake "return address": although the entry function will never return, its stack frame must have the same structure as any other.

The table below shows the state of the stack and the relevant registers right before the beginning of the user program, assuming PHYS_BASE is 0xc0000000:

Address Name Data Type
0xbffffffc argv[3][...] bar\0 char[4]
0xbffffff8 argv[2][...] foo\0 char[4]
0xbffffff5 argv[1][...] -l\0 char[3]
0xbfffffed argv[0][...] /bin/ls\0 char[8]
0xbfffffec word-align 0 uint8_t
0xbfffffe8 argv[4] 0 char *
0xbfffffe4 argv[3] 0xbffffffc char *
0xbfffffe0 argv[2] 0xbffffff8 char *
0xbfffffdc argv[1] 0xbffffff5 char *
0xbfffffd8 argv[0] 0xbfffffed char *
0xbfffffd4 argv 0xbfffffd8 char **
0xbfffffd0 argc 4 int
0xbfffffcc return address 0 void (*) ()

In this example, the stack pointer would be initialized to 0xbfffffcc.

As shown above, your code should start the stack at the very top of the user virtual address space, in the page just below virtual address PHYS_BASE (defined in threads/vaddr.h).

You may find the non-standard hex_dump() function, declared in <stdio.h>, useful for debugging your argument passing code. Here's what it would show in the above example:

 
bfffffc0                                      00 00 00 00 |            ....|
bfffffd0  04 00 00 00 d8 ff ff bf-ed ff ff bf f5 ff ff bf |................|
bfffffe0  f8 ff ff bf fc ff ff bf-00 00 00 00 00 2f 62 69 |............./bi|
bffffff0  6e 2f 6c 73 00 2d 6c 00-66 6f 6f 00 62 61 72 00 |n/ls.-l.foo.bar.|


2.5.2 System Call Details

We already know one way that the operating system can regain control from a user program: interrupts from timers and I/O devices. These are "external" interrupts, because they are caused by entities outside the CPU (see section A.4.3 External Interrupt Handling).

The operating system also deals with software exceptions, which are events that occur in program code (see section A.4.2 Internal Interrupt Handling). These can be errors such as a page fault or division by zero. Exceptions are also the means by which a user program can request services ("system calls") from the operating system.

In the 80x86 architecture, the int instruction is the most commonly used means for invoking system calls. This instruction is handled in the same way as other software exceptions. In Pintos, user programs invoke int $0x30 to make a system call. The system call number and any additional arguments are expected to be pushed on the stack in the normal fashion before invoking the interrupt (see section 2.5 80x86 Calling Convention).

Thus, when the system call handler syscall_handler() gets control, the system call number is in the 32-bit word at the caller's stack pointer, the first argument is in the 32-bit word at the next higher address, and so on. The caller's stack pointer is accessible to syscall_handler() as the esp member of the struct intr_frame passed to it. (struct intr_frame is on the kernel stack.)

The 80x86 convention for function return values is to place them in the EAX register. System calls that return a value can do so by modifying the eax member of struct intr_frame.

You should try to avoid writing large amounts of repetitive code for implementing system calls. Each system call argument, whether an integer or a pointer, takes up 4 bytes on the stack. You should be able to take advantage of this to avoid writing much near-identical code for retrieving each system call's arguments from the stack.


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by on March, 6 2012 using texi2html