In this Lab I'll use
./build_modified_nachos.sh
to build the docker container. And I'll add user program incode/test/
and usecode/userprog/nachos
to test.As usual, use
-d S
to show the system call debug message. And-x executable
to run user program.
In the MIPS Instruction Reference, it describe the MIPS system call instruction.
SYSCALL -- System call
- Description: Generates a software interrupt.
- Operation: advance_pc (4);
- Syntax: syscall
- Encoding:
0000 00-- ---- ---- ---- ---- --00 1100
Read the system call related code, understand current Nachos system call mechanism
code/userprog/syscall.h
code/userprog/exception.cc
code/test/start.s
We've met the "exception handler" in the virtual memory Lab.
The system calls are considered as a kind of exception in Nachos.
When Machine::OneInstruction
(in code/machine/mipssim.cc
) decodes a OP_SYSCALL
(define in code/machine/mipssim.h
as #define OP_SYSCALL 61
)
void
Machine::OneInstruction(Instruction *instr)
{
switch (instr->opCode){
...
case OP_SYSCALL:
RaiseException(SyscallException, 0);
return;
...
}
...
It will call Machine::RaiseException
and then finally into the ExceptionHandler
that we've seen before.
There is a important thing to mention that when handling the system call, we need to make sure we've advanced the program counter. This is also mentioned in Machine::OneInstruction
's comment.
// On a syscall,
// the OS software must increment the PC so execution begins
// at the instruction immediately after the syscall.
I've also taken some notes here
There is a macro IN_ASM
.
In code/userprog/syscall.h
, there is a condition #ifndef IN_ASM
followed by the declaration of all the ten syscall supported by Nachos. And the definition of these syscall are in code/machine/sysdep.cc
. Most of them were been packed in the UNIX standard library.
But in code/test/start.s
, it will define IN_ASM
first and then include the code/userprog/syscall.h
. This time it will define all the system call in assembly way. Including setting the syscall type in the $2
register.
For example of Halt()
and also the comments:
/* -------------------------------------------------------------
* System call stubs:
* Assembly language assist to make system calls to the Nachos kernel.
* There is one stub per system call, that places the code for the
* system call into register r2, and leaves the arguments to the
* system call alone (in other words, arg1 is in r4, arg2 is
* in r5, arg3 is in r6, arg4 is in r7)
*
* The return value is in r2. This follows the standard C calling
* convention on the MIPS.
* -------------------------------------------------------------
*/
.globl Halt
.ent Halt
Halt:
addiu $2,$0,SC_Halt
syscall
j $31
.end Halt
Each of them corresponding to a system call code define in code/userprog/syscall.h
/* system call codes -- used by the stubs to tell the kernel which system call
* is being asked for
*/
#define SC_Halt 0
#define SC_Exit 1
#define SC_Exec 2
#define SC_Join 3
#define SC_Create 4
#define SC_Open 5
#define SC_Read 6
#define SC_Write 7
#define SC_Close 8
#define SC_Fork 9
#define SC_Yield 10
That's why in the exception handler, when we want to know which type of the syscall trigger the exception, we will check the r2 register.
Such like this in code/userprog/exception.cc
:
//----------------------------------------------------------------------
// ExceptionHandler
// Entry point into the Nachos kernel. Called when a user program
// is executing, and either does a syscall, or generates an addressing
// or arithmetic exception.
//
// For system calls, the following is the calling convention:
//
// system call code -- r2
// arg1 -- r4
// arg2 -- r5
// arg3 -- r6
// arg4 -- r7
//
// The result of the system call, if any, must be put back into r2.
//
// And don't forget to increment the pc before returning. (Or else you'll
// loop making the same system call forever!
//
// "which" is the kind of exception. The list of possible exceptions
// are in machine.h.
//----------------------------------------------------------------------
void
ExceptionHandler(ExceptionType which)
{
// System Call
// The system call codes (SC_[TYPE]) is defined in userprog/syscall.h
// The system call stubs is defined in test/start.s
int type = machine->ReadRegister(2); // r2: the standard C calling convention on the MIPS
if (which == SyscallException) {
switch(type) {
case: ...
}
}
}
Note that I don't use switch case in practice in the
ExceptionHandler()
.
I've left the API when doing Lab 4 in code/userprog/exception.cc
.
void
ExceptionHandler(ExceptionType which)
{
...
// System Call
// The system call codes (SC_[TYPE]) is defined in userprog/syscall.h
// The system call stubs is defined in test/start.s
int type = machine->ReadRegister(2); // r2: the standard C calling convention on the MIPS
if (which == SyscallException) {
if (type == SC_Halt) {
DEBUG('a', "Shutdown, initiated by user program.\n");
PrintTLBStatus(); // TLB debug usage
interrupt->Halt();
} else if (type == SC_Exit || type == SC_Exec || type == SC_Join) {
// Address Space Control (Process Management) System Calls
AddressSpaceControlHandler(type);
} else if (type == SC_Create || type == SC_Open || type == SC_Write || type == SC_Read || type == SC_Close) {
// File System System Calls
FileSystemHandler(type);
} else if (type == SC_Fork || type == SC_Yield) {
// User-level Threads System Calls
UserLevelThreadsHandler(type);
}
// Increment the Program Counter before returning.
IncrementPCRegs();
return;
}
}
For this part, I'll put the handler in FileSystemHandler
.
And because we need to advance the program counter after handling the system call. I've made this function and called after handling any syscall.
//----------------------------------------------------------------------
// IncrementPCRegs
// Because when Nachos cause the exception. The PC won't increment
// (i.e. PC+4) in Machine::OneInstruction in machine/mipssim.cc.
// Thus, when invoking a system call, we need to advance the program
// counter. Or it will cause the infinity loop.
//----------------------------------------------------------------------
void IncrementPCRegs(void) {
// Debug usage
machine->WriteRegister(PrevPCReg, machine->ReadRegister(PCReg));
// Advance program counter
machine->WriteRegister(PCReg, machine->ReadRegister(NextPCReg));
machine->WriteRegister(NextPCReg, machine->ReadRegister(NextPCReg)+4);
}
On the analogy of the Halt implementation, complete the following system calls
- Create
- Open
- Close
- Write
- Read
There are basic description of the system calls in
code/userprog/syscall.h
In code/userprog/syscall.h
, the definition is as following
/* File system operations: Create, Open, Read, Write, Close
* These functions are patterned after UNIX -- files represent
* both files *and* hardware I/O devices.
*
* If this assignment is done before doing the file system assignment,
* note that the Nachos file system has a stub implementation, which
* will work for the purposes of testing out these routines.
*/
/* A unique identifier for an open Nachos file. */
typedef int OpenFileId;
/* when an address space starts up, it has two open files, representing
* keyboard input and display output (in UNIX terms, stdin and stdout).
* Read and Write can be used directly on these, without first opening
* the console device.
*/
#define ConsoleInput 0
#define ConsoleOutput 1
And the operations
/* Create a Nachos file, with "name" */
void Create(char *name);
/* Open the Nachos file "name", and return an "OpenFileId" that can
* be used to read and write to the file.
*/
OpenFileId Open(char *name);
/* Write "size" bytes from "buffer" to the open file. */
void Write(char *buffer, int size, OpenFileId id);
/* Read "size" bytes from the open file into "buffer".
* Return the number of bytes actually read -- if the open file isn't
* long enough, or if it is an I/O device, and there aren't enough
* characters to read, return whatever is available (for I/O devices,
* you should always wait until you can return at least one character).
*/
int Read(char *buffer, int size, OpenFileId id);
/* Close the file, we're done reading and writing to it. */
void Close(OpenFileId id);
Because these two functions needs to get the file name by reading memory.
So I've made this read file name helper function.
// Helper function to get file name using ReadMem for Create and Open syscall
char* getFileNameFromAddress(int address) {
int position = 0;
int data;
char name[FileNameMaxLength + 1];
do {
// each time read one byte
bool success = machine->ReadMem(address + position, 1, &data);
ASSERT_MSG(success, "Fail to read memory in Create syscall");
name[position++] = (char)data;
ASSERT_MSG(position <= FileNameMaxLength, "Filename length too long")
} while(data != '\0');
name[position] = '\0';
return name;
}
And here is the Create
syscall (type SC_Create
)
int address = machine->ReadRegister(4); // memory starting position
DEBUG('S', COLORED(GREEN, "Received Create syscall (r4 = %d): "), address);
char* name = getFileNameFromAddress(address);
bool success = fileSystem->Create(name, 0); // initial file length set 0
DEBUG('S', success ? COLORED(GREEN, "File \"%s\" created.\n") : COLORED(FAIL, "File \"%s\" fail to create.\n"), name);
Here is the Open
syscall (type SC_Close
)
int address = machine->ReadRegister(4); // memory starting position
DEBUG('S', COLORED(GREEN, "Received Open syscall (r4 = %d): "), address);
char* name = getFileNameFromAddress(address);
OpenFile *openFile = fileSystem->Open(name);
DEBUG('S', COLORED(GREEN, "File \"%s\" opened.\n"), name);
machine->WriteRegister(2, (OpenFileId)openFile); // return result
Closing the file is simply de-allocate the open file pointer.
Here is the Close
syscall (type SC_Close
)
OpenFileId id = machine->ReadRegister(4); // OpenFile object id
DEBUG('S', COLORED(GREEN, "Received Close syscall (r4 = %d): "), id);
OpenFile* openFile = (OpenFile*)id; // transfer id back to OpenFile
delete openFile; // release the file
DEBUG('S', COLORED(GREEN, "File has closed.\n"));
This part is more complicated, because we need to read and write the buffer one by one byte. And handle the "data type transform" problem
Here is the Read
syscall (type SC_Read
)
int address = machine->ReadRegister(4); // memory starting position
int size = machine->ReadRegister(5); // read "size" bytes
OpenFileId id = machine->ReadRegister(6); // OpenFile object id
DEBUG('S', COLORED(GREEN, "Received Read syscall (r4 = %d, r5 = %d, r6 = %d): "), address, size, id);
OpenFile* openFile = (OpenFile*)id; // transfer id back to OpenFile
char* buffer = new char[size];
int numBytes = openFile->Read(buffer, size);
for (int i = 0; i < numBytes; i++) { // each time write one byte
bool success = machine->WriteMem(address + i, 1, (int)buffer[i]);
if (!success) {
i--;
}
}
DEBUG('S', COLORED(GREEN, "Read %d bytes into buffer.\n"), numBytes);
machine->WriteRegister(2, numBytes); // Return the number of bytes actually read
Finally, here is the Write
syscall (type SC_Write
)
int address = machine->ReadRegister(4); // memory starting position
int size = machine->ReadRegister(5); // read "size" bytes
OpenFileId id = machine->ReadRegister(6); // OpenFile object id
DEBUG('S', COLORED(GREEN, "Received Write syscall (r4 = %d, r5 = %d, r6 = %d): "), address, size, id);
char* buffer = new char[size];
for (int i = 0; i < size; i++) { // each time write one byte
bool success = machine->ReadMem(address + i, 1, (int*)&buffer[i]);
if (!success) {
i--;
}
}
OpenFile* openFile = (OpenFile*)id; // transfer id back to OpenFile
int numBytes = openFile->Write(buffer, size);
DEBUG('S', COLORED(GREEN, "Write %d bytes into file.\n"), numBytes);
machine->WriteRegister(2, numBytes); // Return the number of bytes actually write
Write user program and invoke the system calls implemented in the Exercise 2 and test the correctness.
I've made the user program code/test/filesyscall.c
which will create a file call "test.txt" then write string and then read from it. Finally use the exit state to show how many bytes it has read.
/* filesyscall.c
* Simple program to test file system syscall (Lab 6)
*/
#include "syscall.h"
#define BUFFER_SIZE 11
int main() {
char data[9]; // as file name and content
char buffer[9];
OpenFileId fid_write;
OpenFileId fid_read;
int numBytes;
data[0] = 't';
data[1] = 'e';
data[2] = 's';
data[3] = 't';
data[4] = '.';
data[5] = 't';
data[6] = 'x';
data[7] = 't';
data[8] = '\0';
Create(data);
fid_write = Open(data);
fid_read = Open(data);
Write(data, 8, fid_write);
numBytes = Read(buffer, 8, fid_read);
Close(fid_write);
Close(fid_read);
Exit(numBytes);
}
Why I have to define the string in that way: here
$ docker run -it nachos nachos/nachos-3.4/code/userprog/nachos -d S -x nachos/nachos-3.4/code/test/filesyscall
Received Create syscall (r4 = 1464): File "test.txt" created.
Received Open syscall (r4 = 1464): File "test.txt" opened.
Received Open syscall (r4 = 1464): File "test.txt" opened.
Received Write syscall (r4 = 1464, r5 = 8, r6 = 151082128): Write 8 bytes into file.
Received Read syscall (r4 = 1480, r5 = 8, r6 = 151082144): Read 8 bytes into buffer.
Received Close syscall (r4 = 151082128): File has closed.
Received Close syscall (r4 = 151082144): File has closed.
User program exit with status 8
And it will generate the "test.txt" file in docker under code/userprog
.
Implement the following system calls
- Exec
- Fork
- Yield
- Join
- Exit
There are basic description of the system calls in
code/userprog/syscall.h
The API of each of them defined as
/* Address space control operations: Exit, Exec, and Join */
/* This user program is done (status = 0 means exited normally). */
void Exit(int status);
/* A unique identifier for an executing user program (address space) */
typedef int SpaceId;
/* Run the executable, stored in the Nachos file "name", and return the
* address space identifier
*/
SpaceId Exec(char *name);
/* Only return once the the user program "id" has finished.
* Return the exit status.
*/
int Join(SpaceId id);
/* User-level thread operations: Fork and Yield. To allow multiple
* threads to run within a user program.
*/
/* Fork a thread to run a procedure ("func") in the *same* address space
* as the current thread.
*/
void Fork(void (*func)());
/* Yield the CPU to another runnable thread, whether in this address space
* or not.
*/
void Yield();
I've implemented the initial version Exit syscall in virtual memory Lab. But this time, it requires not only receiving the return value but also the thread control.
Here is the following procedure that a Exit syscall need to do
TBD
TBD
Write user program and invoke the system calls implemented in the Exercise 4 and test the correctness.
In this test I'll create a child thread using Exec
to execute the code/test/exit
user program that I've made for Lab4 Exercise5.
And then, after the child thread finish, it will Join
back to the parent thread, then Exit
and return the result state.
/* execjoin.c
* Simple program to test the thread syscall (Lab 6) using the
* previous made "exit" program.
* This is used to test the address space control operations
* This will create executable Exec child with it and the parent will Join
* the thread and then Exit with the result
*/
#include "syscall.h"
int main() {
char executable[5];
int exitCode;
SpaceId sp;
executable[0] = 'e';
executable[1] = 'x';
executable[2] = 'i';
executable[3] = 't';
executable[4] = '\0';
sp = Exec(executable);
exitCode = Join(sp);
Exit(exitCode);
}
Result:
TBD
In this test program, I'll first create a file called "text.txt" and Fork
two threads.
These two threads will write character 'a' and 'b' in to the file for 20 and 10 times respectively.
/* forkyield.c
* Simple program to test the thread syscall (Lab 6)
* This is used to test the user-level thread operations
* This example will create two thread and each of them
* will write something in the file.
*/
#include "syscall.h"
char file[9];
void ThreadA() {
char ch = 'a';
int i;
OpenFileId fd;
fd = Open(file);
for(i = 0; i < 20; i ++) {
Write(&ch, 1, fd);
Yield();
}
}
void ThreadB() {
char ch = 'b';
int i;
OpenFileId fd;
fd = Open(file);
for(i = 0; i < 10; i ++) {
Write(&ch, 1, fd);
Yield();
}
}
int main() {
file[0] = 't';
file[1] = 'e';
file[2] = 's';
file[3] = 't';
file[4] = '.';
file[5] = 't';
file[6] = 'x';
file[7] = 't';
file[8] = '\0';
Create(file);
Fork(ThreadA);
Fork(ThreadB);
}
Result:
TBD (Debug message of -d S
)
TBD (the "test.txt" content)
If define a string like this
char data[5] = "test";
It will cause Assertion failed: line 286, file "../machine/mipssim.cc"
// ReadMem assumes all 4 byte requests are aligned on an even
// word boundary. Also, the little endian/big endian swap code would
// fail (I think) if the other cases are ever exercised.
ASSERT((tmp & 0x3) == 0);
But if using this way, then it can prevent from it.
char data[5];
data[0] = 't';
data[1] = 'e';
data[2] = 's';
data[3] = 't';
data[4] = '\0';
- I'm not sure the filesys syscall test should use
code/filesys/nachos
orcode/userprog/nachos
.- Using the first one will cause "Unable to open file ..." error
- Using the second one, because the
code/userprog/Makefile
useFILESYS_STUB
, not sure if it is legal, but it will enter the exception handler, so not quite sure.
TODO
- Maybe print something when
Read
andWrite
- Check if the
if (!success) i--;
is necessary inRead
andWrite
. - More detail notes and deeper implementation.
- Exercise 5 test result when finish Exercise 4
CSC546 - Operating Systems
- Lecture 4
- Exit
- Adjust Registers
- Unix File System
- Nachos File System
- fileSystem
- Nachos Write system call
- Lecture 7
- Exec
- StartProcess
- Thread::Fork
- AddrSpace
- AddrSpace::AddrSpace(char * name, OpenFile* executable)
- AddrSpace::mapPages()
- AddrSpace::loadpage(int vpn, int ppn)
- Lecture 8
- Exec
- Thread 'name' Data Member
- Next Assignment
- Unix fork and wait
- Join
- Lecture 9
- Join
- Thread Class Additions
- List Class Additions
- rcsmerge
- Join Details
- Exec Changes
- Exit Changes
- Assignment: Implementing Join
CPS 110 / EE 153 Operating Systems (Duke University)
- nachos-Lab6實習報告 - This guy written wrong things = =
- SergioShen/Nachos comment: Finish lab6 exercise3: Syscall: Create, Open, Close, Write, Read
- SergioShen/Nachos comment: Finish lab6 - Exec, Fork, Yield, Join
- SergioShen/Nachos exception.cc
Others
Yet Another
Just.. a little bit related