CSC546 - Operating Systems


Topics

Exit

Adjust Registers

Unix File System

Nachos File System

fileSystem

Nachos Write system call


Nachos Exit System Call

The assignment for due this week was to modify exception.cc to provide default implementations of each of the system calls and an initial implementation of the Exit system call. implementation

Default implementation means a placeholder for the implementation, but one that just calls ASSERT.

The implementation of Exit was to use Thread::Finish.

Although the initial implementation described last time should work for the suggested test.

Namely, you were to write and compile a "user" program that simply invoked the Nachos Exit system call. Then you were to run this "user" program, comparing the results with running the halt user program.


Problem

When a user program calls Exit, that user program should terminate, but this action should not in general halt the system.

However, in for the assignment, the system will indirectly halt as the Nachos system will:


Hidden Problem

The Exit system call is special among the other system calls simply because it will not return to the calling user thread.

For a system call like Write which will return to the caller, we have a problem.

The problem has to do with the value of the program counter.

The question is when the syscall instruction is executed (on the Mips simulator) and ExceptionHandler is invoked, has the Mips program counter been advanced already, or is it still pointing to the syscall instruction.


Problem Answer

The short answer is that the saved user program counter has not been advanced.

A Nachos system call like Write will return to the user. When the user program executes again, it will execute the same syscall instruction - an infinite loop.

So you need to adjust the program counter for the user in the implementation of the Write system call and calls like it.

Here is a function that will adjust the program counter(s). (See here for more on this.)

void AdjustPCRegs()
{
  int pc;

  pc = machine->ReadRegister(PCReg);
  machine->WriteRegister(PrevPCReg, pc);
  pc = machine->ReadRegister(NextPCReg);
  machine->WriteRegister(PCReg, pc);
  pc += 4;
  machine->WriteRegister(NextPCReg, pc);
  
}

When Not to Adjust

We are not worrying about memory management at this point. When virtual memory with paging is implemented, ExceptionHandler may be invoked, not because of a system call, but because of a page fault exception.

So in this case, the program counter is not pointing to a syscall instruction, but rather to a user code instruction that was not executed because a page fault occurred.

After ExceptionHandler "handles" the page fault, the user instruction which caused the page fault should be tried again.

So for page fault exceptions, the AdjustRegs procedures should not be called by the handler.


Unix File System Data Structures

Bach's book discusses (in several chapters - see 2.2.1) the Unix file system kernel data structures.

The principal ones structures are:


Unix File Descriptors

File descriptors are just integers. These integers are used by the kernel as indices into the user process's User File Descriptor Table.

In a Unix process, file descriptors 0, 1, and 2 are set up implicitly to be associated with standard input, standard output, and standard error, respectively.

The contents of an entry in the User File Descriptor Table points to an entry in the kernel's File Table.

  User
File Descriptor
Table
0
1
2
3
...
ptr0
ptr1
ptr2
NULL
...

The Kernel File Table

A kernel File Table entry stores:

Two different process could have the same file open. Each user process would get a file descriptor entry in its own table and a corresponding entry in the kernel's File Table.

The file descriptor used by one process may or may not be the same as that used by another process.

Even if the file descriptors are the same, the entry for each process at that descriptor index will point to different entries in the kernel's File Table. (An exception is for a parent its child process.)


Read/Write Unix System Calls

ssize_t read(int fildes, void *buf, size_t nbyte);

ssize_t write(int fildes, const void *buf, size_t nbyte);

When executing Unix read or write system calls on behalf of a user process, the system call is passed a file descriptor (an integer) as the first parameter, together with a buffer for the bytes to be read/written and the number of bytes to read/write.

The kernel routine uses the file descriptor to index into the user's File Descriptor table to get a pointer to an entry in the kernel's File Table.

The kernel uses the information in the File Table to perform the requested operation and updates that entry - e.g., to adjust the offset.

Note that the inode table contains information about creation times, modification times, access permissions, file size, and the location on disk where the file is stored.


Nachos File System

There are actually two Nachos File Systems in the program code.

The first is a "stub". It contains classes with member functions which just indirectly call the corresponding Unix file system functions.

The second file system is not fully implemented. It uses a large file as simulated disk and requires that you implement a directory structure, disk allocation for files, etc.

We will use the stub system. This allows us to implement multiprogramming and virtual memory without having to first implement the file system on which they depend.


Nachos File System Classes/Functions

Here is the "stub" Nachos File System C++ class OpenFile:

class OpenFile {
  public:
    OpenFile(int f) { file = f; currentOffset = 0; }    // open the file
    ~OpenFile() { Close(file); }                        // close the file

    int ReadAt(char *into, int numBytes, int position) {
                Lseek(file, position, 0);
                return ReadPartial(file, into, numBytes);
                }
    int WriteAt(char *from, int numBytes, int position) {
                Lseek(file, position, 0);
                WriteFile(file, from, numBytes);
                return numBytes;
                }
    int Read(char *into, int numBytes) {
                int numRead = ReadAt(into, numBytes, currentOffset);
                currentOffset += numRead;
                return numRead;
                }
    int Write(char *from, int numBytes) {
                int numWritten = WriteAt(from, numBytes, currentOffset);
                currentOffset += numWritten;
                return numWritten;
                }

    int Length() { Lseek(file, 0, 2); return Tell(file); }

  private:
    int file;
    int currentOffset;
};

Nachos File System Classe (continued)

Here is the Nachos File System C++ class, FileSystem. There is a global pointer,

FileSystem *fileSystem;

which is initialized to point to an object of this type, and can be used to access the Nachos ("stub") file system.

class FileSystem {
  public:
    FileSystem(bool format) {}

    bool Create(char *name, int initialSize) {
        int fileDescriptor = OpenForWrite(name);

        if (fileDescriptor == -1) return FALSE;
        Close(fileDescriptor);
        return TRUE;
        }

    OpenFile* Open(char *name) {
          int fileDescriptor = OpenForReadWrite(name, FALSE);

          if (fileDescriptor == -1) return NULL;
          return new OpenFile(fileDescriptor);
      }

    bool Remove(char *name) { return Unlink(name) == 0; }

};

Example: StartProcess

The function StartProcess in userprog/progtest.cc is called when the -x option is used to execute a user program.

This function must open the specified Mips executable file.

Here is how it does so in StartProcess:

void
StartProcess(char *filename)
{
    OpenFile *executable = fileSystem->Open(filename);
...
}

Nachos Write System Call

For the next assignment, it is desirable to be able to get some output from the user programs, so the assignment is to implement an initial version of the Nachos Write system call; write, compile, and test user program(s) that call Write.

Here is the Nachos Write (declared in threads/syscall.h)

/* Write "size" bytes from "buffer" to the open file. */
void Write(char *buffer, int size, OpenFileId id);

The initial implementation should check that the file descriptor passed to Write is 1.

A Nachos user thread could maintain a "descriptor" table of pointers to OpenFile objects.

An OpenFile object to represent standard output could be created like this:

OpenFile* ofptr = new OpenFile(1); // 1 for Standard output

Then a kernel routine could write to standard output by:

ofptr->Write(buf, nbytes);

where buf is character buffer holding the bytes to be written and nbytes is the number of bytes to write.


Outline of Nachos Write System Call

1. Read the 3 parameters passed to Write by the user program. They will be in registers 4, 5, and 6.

2. The first parameter will be a "Mips address". This is a virtual address that is for the moment the same as the "Mips physical address"; i.e., an index into the array that simulates the Mips machine's memory. (It is not a virtual address or a physical address on the hawk.)

3. So the actual bytes to write are in the Mip memory. These bytes could be copied or read from the user's Mips memory by using the machine's ReadMem member function:

int byteval;
char buf[...];

machine->ReadMem(strvaddr, 1, &byteval)
buf[n++] = (char) byteval;

where this quirky function reads 1 byte at the Mips virtual address, vaddr, and stores it in the int byteval.

This int value should then be converted back to a byte and stored in a char array.

4. The other two parameters are integers. Size is the number of bytes to write, and id should be 1 for standard output since we aren't worrying about any other output files yet. The size is used to control how many times ReadMem must be called to read each byte one at a time.

5. After copying the bytes from the user program space, you could call

ofptr->Write(buf, nbytes);

to write the bytes to standard output.

6. Finally, you need to call AdjustPCRegs (or similar code) to adjust the program counter(s) before returning.