系统调用详解:以Nachos为例实现系统调用

2023-10-21 11:36:01 浏览数 (1)

用户程序

要实现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()开始执行程序。

代码语言:javascript复制
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()移动时钟周期。

代码语言:javascript复制
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,操作码opcodersrt两个操作数寄存器和rd一个结果寄存器以及extra字段的解析。

代码语言:javascript复制
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对应的操作。

代码语言:javascript复制
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)函数请求系统调用资源。

代码语言:javascript复制
#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当中被定义,将两个操作数相加。

代码语言:javascript复制
int SysAdd(int op1, int op2)
{
  return op1   op2;
}

分析到这里了,接下来我们回到刚刚讲的用户程序的执行步骤,来将二者串联。

我们前面提到的Mipssim.cc中模拟机器指令执行的函数OneInstruction(),根据不同的操作码执行不同的操作。函数中当操作码为系统调用OP_SYSCALL时,如下所示。使用RaiseException来引发异常,向操作系统发出一个信号,可以理解为我们前面讲到的陷入指令。

代码语言:javascript复制
case OP_SYSCALL:
	RaiseException(SyscallException, 0);
	return;

RasieException的函数定义如下,第一个参数为异常类型,也就是陷入核心态的原因,第二个参数为引发陷入的虚拟地址。然后调用kernel->interrupt->setStatus(SystemMode)中断将处理器状态设为核心态,然后使用ExceptionHandler(which)将异常类型传入并处理该异常,最后回到用户态。

代码语言:javascript复制
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存入的系统调用标识符。然后根据传入的异常类型和系统调用的标识符执行相应的操作。

代码语言:javascript复制
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的内核函数,最后将结果存入寄存器,系统调用就完成了。接下来{}当中的内容用于完成中断的进程上下文恢复。

代码语言:javascript复制
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

0 人点赞