用户程序
要实现Nachos的系统调用,必须先弄清楚Nachos用户态程序的运行步骤。
在main.cc中,当我们选择-x选项时,这段代码将-x之后的参数设置为userProgName,即我们需要执行的用户程序。
代码语言:javascript复制else if (strcmp(argv[i], "-x") == 0)
{
ASSERT(i 1 < argc);
userProgName = argv[i 1];
i ;
}
然后再下面这段代码中,首先给程序执行分配资源空间,然后执行用户程序。
代码语言:javascript复制// finally, run an initial user program if requested to do so
if (userProgName != NULL)
{
AddrSpace *space = new AddrSpace;
ASSERT(space != (AddrSpace *)NULL);
if (space->Load(userProgName))
{ // load the program into the space
space->Execute(); // run the program
ASSERTNOTREACHED(); // Execute never returns
}
}
在addrspace.cc中,Load函数如下。将程序加载并为程序分配内存空间。
代码语言:javascript复制bool AddrSpace::Load(char *fileName)
{
OpenFile *executable = kernel->fileSystem->Open(fileName);
NoffHeader noffH;
unsigned int size;
if (executable == NULL)
{
cerr << "Unable to open file " << fileName << "n";
return FALSE;
}
executable->ReadAt((char *)&noffH, sizeof(noffH), 0);
if ((noffH.noffMagic != NOFFMAGIC) &&
(WordToHost(noffH.noffMagic) == NOFFMAGIC))
SwapHeader(&noffH);
ASSERT(noffH.noffMagic == NOFFMAGIC);
#ifdef RDATA
// how big is address space?
size = noffH.code.size noffH.readonlyData.size noffH.initData.size
noffH.uninitData.size UserStackSize;
// we need to increase the size
// to leave room for the stack
#else
// how big is address space?
size = noffH.code.size noffH.initData.size noffH.uninitData.size UserStackSize; // we need to increase the size
// to leave room for the stack
#endif
numPages = divRoundUp(size, PageSize);
size = numPages * PageSize;
ASSERT(numPages <= NumPhysPages); // check we're not trying
// to run anything too big --
// at least until we have
// virtual memory
DEBUG(dbgAddr, "Initializing address space: " << numPages << ", " << size);
// then, copy in the code and data segments into memory
// Note: this code assumes that virtual address = physical address
if (noffH.code.size > 0)
{
DEBUG(dbgAddr, "Initializing code segment.");
DEBUG(dbgAddr, noffH.code.virtualAddr << ", " << noffH.code.size);
executable->ReadAt(
&(kernel->machine->mainMemory[noffH.code.virtualAddr]),
noffH.code.size, noffH.code.inFileAddr);
}
if (noffH.initData.size > 0)
{
DEBUG(dbgAddr, "Initializing data segment.");
DEBUG(dbgAddr, noffH.initData.virtualAddr << ", " << noffH.initData.size);
executable->ReadAt(
&(kernel->machine->mainMemory[noffH.initData.virtualAddr]),
noffH.initData.size, noffH.initData.inFileAddr);
}
#ifdef RDATA
if (noffH.readonlyData.size > 0)
{
DEBUG(dbgAddr, "Initializing read only data segment.");
DEBUG(dbgAddr, noffH.readonlyData.virtualAddr << ", " << noffH.readonlyData.size);
executable->ReadAt(
&(kernel->machine->mainMemory[noffH.readonlyData.virtualAddr]),
noffH.readonlyData.size, noffH.readonlyData.inFileAddr);
}
#endif
delete executable; // close file
return TRUE; // success
}
之后继续调用addrspace.cc中的Execute函数。使用this->InitRegisters()
初始化寄存器的值,之后调用this->RestoreState()
保存进程的分页表,完成这两项准备工作之后使用kernel->machine->Run()
开始执行程序。
void AddrSpace::Execute()
{
kernel->currentThread->space = this;
this->InitRegisters(); // set the initial register values
this->RestoreState(); // load page table register
kernel->machine->Run(); // jump to the user progam
ASSERTNOTREACHED(); // machine->Run never returns;
// the address space exits
// by doing the syscall "exit"
}
在mipssim.cc中定义的Run函数如下,使用软件模拟硬件执行指令。调用setStatus
函数将处理器状态设置为用户态,表示执行的是用户程序。然后使用OneInstruction(instr)
执行指令,再使用OneTick()
移动时钟周期。
void Machine::Run()
{
Instruction *instr = new Instruction; // storage for decoded instruction
if (debug->IsEnabled('m'))
{
cout << "Starting program in thread: " << kernel->currentThread->getName();
cout << ", at time: " << kernel->stats->totalTicks << "n";
}
kernel->interrupt->setStatus(UserMode);
for (;;)
{
OneInstruction(instr);
kernel->interrupt->OneTick();
if (singleStep && (runUntilTime <= kernel->stats->totalTicks))
Debugger();
}
}
继续深入分析OneInstruction
函数,由于这个函数源代码比较长,所以从中截取关键部分分析。
下面这部分代码完成取指和译码的过程
代码语言:javascript复制if (!ReadMem(registers[PCReg], 4, &raw))
return; // exception occurred
instr->value = raw;
instr->Decode();
译码函数如下,在这里完成对Instruction
的二进制表示value
,操作码opcode
,rs
、rt
两个操作数寄存器和rd
一个结果寄存器以及extra
字段的解析。
void Instruction::Decode()
{
OpInfo *opPtr;
rs = (value >> 21) & 0x1f;
rt = (value >> 16) & 0x1f;
rd = (value >> 11) & 0x1f;
opPtr = &opTable[(value >> 26) & 0x3f];
opCode = opPtr->opCode;
if (opPtr->format == IFMT)
{
extra = value & 0xffff;
if (extra & 0x8000)
{
extra |= 0xffff0000;
}
}
else if (opPtr->format == RFMT)
{
extra = (value >> 6) & 0x1f;
}
else
{
extra = value & 0x3ffffff;
}
if (opCode == SPECIAL)
{
opCode = specialTable[value & 0x3f];
}
else if (opCode == BCOND)
{
int i = value & 0x1f0000;
if (i == 0)
{
opCode = OP_BLTZ;
}
else if (i == 0x10000)
{
opCode = OP_BGEZ;
}
else if (i == 0x100000)
{
opCode = OP_BLTZAL;
}
else if (i == 0x110000)
{
opCode = OP_BGEZAL;
}
else
{
opCode = OP_UNIMP;
}
}
}
回到OneInstruction函数继续分析,这里截取了switch-case
代码段的一部分。根据不同的操作码opcode,执行对应的操作,以OP_ADD这一个操作码为例,使用指令sum = registers[instr->rs] registers[instr->rt]
计算rs和rd两个寄存器内操作数的和,然后使用registers[instr->rd] = sum
将结果存入到rd寄存器当中。在这之后还定义了许多opcode对应的操作。
switch (instr->opCode)
{
case OP_ADD:
sum = registers[instr->rs] registers[instr->rt];
if (!((registers[instr->rs] ^ registers[instr->rt]) & SIGN_BIT) &&
((registers[instr->rs] ^ sum) & SIGN_BIT))
{
RaiseException(OverflowException, 0);
return;
}
registers[instr->rd] = sum;
break;
接下来就是一步步执行编译完成的用户程序的对应的模拟机器指令即可。整个Nachos的用户程序执行的过程就是这样。
系统调用
什么是系统调用
操作系统作为硬件与用户之间的接口,需要为用户提供一些简单易用的服务,包括命令接口与程序接口。程序接口由一组系统调用实现。操作系统提供这种系统调用,当用户进程想要使用这个资源,就必须对通过系统调用向操作系统发出请求,由操作系统会对这些请求进行协调与管理。
用户态与核心态
在操作系统当中,有两种指令——特权指令与非特权指令,两种处理器状态——核心态与用户态。区分这些的原因很好理解。指令特权指令指具有特殊权限的指令。这类指令只用于操作系统或其他系统软件,一般不直接提供给用户使用。这些处理如果交由用户程序随意使用,必然会导致未知的安全问题与风险,因此某些特权指令需要在核心态下完成。
中断与陷阱
用户态与核心态的转变,只能通过中断实现。发生中断,CPU立即进入核心态。中断是CPU进入核心态的唯一途径。
陷阱是一种由执行指令触发的同步事件,通常用于实现系统调用和异常处理等功能。陷阱是由执行特殊的软件中断指令或硬件陷阱指令引起的。当执行这些指令时,处理器会从用户态切换到内核态,同时保存当前执行进程的上下文信息,然后跳转到系统调用或异常处理程序中。陷阱的处理方式和中断类似,不同的是引起的方式不同。
系统调用的过程
系统调用相关处理涉及系统资源的管理,对进程的管理,这些处理需要一些特权指令才能完成,因此系统调用相关操作需要在核心态下完成。
首先用户态程序发出系统调用请求,执行陷入指令(只能在用户态执行)引发中断并保存用户态进程的上下文进入核心态,然后执行系统调用的相关服务,最后返回用户态,回复用户态进程上下文信息。
Nachos如何实现系统调用
了解完系统调用的有关内容,接下来分析Nachos如何实现的系统调用。
以示例程序add.c为例,Add(42, 23)
函数请求系统调用资源。
#include "syscall.h"
int main()
{
int result;
result = Add(42, 23);
Halt();
/* not reached */
}
在test目录下的MIPS汇编文件start.s中,实现了Add的系统调用。将标识符SC_ADD加载到寄存器$2当中,使用 syscall 指令来发出系统调用请求。
代码语言:javascript复制Add:
addiu $2,$0,SC_Add
syscall
j $31
.end Add
.globl Exit
.ent Exit
在syscall.h当中使用#define SC_Add 42
定义了SC_ADD标识符,为42。SysAdd()这个函数在ksyscall.h当中被定义,将两个操作数相加。
int SysAdd(int op1, int op2)
{
return op1 op2;
}
分析到这里了,接下来我们回到刚刚讲的用户程序的执行步骤,来将二者串联。
我们前面提到的Mipssim.cc中模拟机器指令执行的函数OneInstruction(),根据不同的操作码执行不同的操作。函数中当操作码为系统调用OP_SYSCALL
时,如下所示。使用RaiseException来引发异常,向操作系统发出一个信号,可以理解为我们前面讲到的陷入指令。
case OP_SYSCALL:
RaiseException(SyscallException, 0);
return;
RasieException的函数定义如下,第一个参数为异常类型,也就是陷入核心态的原因,第二个参数为引发陷入的虚拟地址。然后调用kernel->interrupt->setStatus(SystemMode)
中断将处理器状态设为核心态,然后使用ExceptionHandler(which)
将异常类型传入并处理该异常,最后回到用户态。
void Machine::RaiseException(ExceptionType which, int badVAddr)
{
DEBUG(dbgMach, "Exception: " << exceptionNames[which]);
registers[BadVAddrReg] = badVAddr;
DelayedLoad(0, 0); // finish anything in progress
kernel->interrupt->setStatus(SystemMode);
ExceptionHandler(which); // interrupts are enabled at this point
kernel->interrupt->setStatus(UserMode);
}
exception.cc中的 ExceptionHandler() 函数部分代码如下所示。从寄存器$2取出type
变量也就是我们在start.s存入的系统调用标识符。然后根据传入的异常类型和系统调用的标识符执行相应的操作。
void ExceptionHandler(ExceptionType which)
{
int type = kernel->machine->ReadRegister(2);
DEBUG(dbgSys, "Received Exception " << which << " type: " << type << "n");
switch (which)
{
case SyscallException:
switch (type)
所以执行系统调用Add,那么which的值为SyscallException
,type的值为SC_ADD
,因此的执行操作如下。从寄存器当中取出操作数,然后调用SysAdd的内核函数,最后将结果存入寄存器,系统调用就完成了。接下来{}当中的内容用于完成中断的进程上下文恢复。
case SC_Add:
DEBUG(dbgSys, "Add " << kernel->machine->ReadRegister(4) << " " << kernel->machine->ReadRegister(5) << "n");
/* Process SysAdd Systemcall*/
int result;
result = SysAdd(/* int op1 */ (int)kernel->machine->ReadRegister(4),
/* int op2 */ (int)kernel->machine->ReadRegister(5));
DEBUG(dbgSys, "Add returning with " << result << "n");
/* Prepare Result */
kernel->machine->WriteRegister(2, (int)result);
/* Modify return point */
{
/* set previous programm counter (debugging only)*/
kernel->machine->WriteRegister(PrevPCReg, kernel->machine->ReadRegister(PCReg));
/* set programm counter to next instruction (all Instructions are 4 byte wide)*/
kernel->machine->WriteRegister(PCReg, kernel->machine->ReadRegister(PCReg) 4);
/* set next programm counter for brach execution */
kernel->machine->WriteRegister(NextPCReg, kernel->machine->ReadRegister(PCReg) 4);
}
return;
ASSERTNOTREACHED();
break;
到这里,整个Nachos的系统调用的流程就很清楚了。
实现步骤
实验要求实现求减法、乘法,除法、乘方四种系统调用。
修改start.s中的内容
代码语言:javascript复制Add:
addiu $2,$0,SC_Add
syscall
j $31
.end Add
.globl Mul
.ent Mul
Mul:
addiu $2,$0,SC_Mul
syscall
j $31
.end Mul
.globl Div
.ent Div
Div:
addiu $2,$0,SC_Div
syscall
j $31
.end Mul
.globl Pow
.ent Pow
Pow:
addiu $2,$0,SC_Pow
syscall
j $31
.end Pow
.globl Sub
.ent Sub
Sub:
addiu $2,$0,SC_Sub
syscall
j $31
.end Sub
.globl Exit
.ent Exit
然后再syscall.h文件当中添加相对应的标识符
代码语言:javascript复制#define SC_Add 42
#define SC_Sub 43
#define SC_Mul 44
#define SC_Div 45
#define SC_Pow 46
在ksyscall.h当中定义系统提供的程序接口,减法、乘法,除法、乘方。
代码语言:javascript复制int SysMul(int op1, int op2)
{
return op1 * op2;
}
int SysSub(int op1, int op2)
{
return op1 - op2;
}
int SysDiv(int op1, int op2)
{
return op1 / op2;
}
int SysPow(int op1, int op2)
{
int num = 1;
for (size_t i = 0; i < op2; i )
{
num = op1 * num;
}
return num;
}
完成相关的定义之后,在exception.cc文件当中的switch-case代码段仿照SC_ADD的处理完成对相对应的标识符的处理扩展即可。
最后在test目录下新建自己的用户态程序文件,如下。然后修改Makefile。
代码语言:javascript复制#include "syscall.h"
int main()
{
int result;
// 加法
result = Add(8, 2);
// 减法
result = Sub(8, 2);
// 乘法
result = Mul(8, 2);
// 除法
result = Div(8, 2);
// 乘方
result = Pow(8, 2);
Halt();
/* not reached */
}
最后在test目录下重新编译一下用户程序,然后回到build.linux目录下重新编译Nachos操作系统即可。
在test目录下执行下面命令验证一下,成功。
代码语言:javascript复制../build.linux/nachos -x test.noff -d u