xv6---Lab2: system calls

目录

参考资料:

2.1 抽象物理资源

2.2 特权模式与系统调用

2.3 内核的组织

2.5 进程概览

2.6 Code: 启动xv6,第一个进程和系统调用

4.2 Trap from user space

System call tracing 

关于syscall函数的代码

每个syscall是由usys.pl自动生成为usys.S

trace代码实现

系统调用流程

系统调用流程小结:

Sysinfo

实验目标

预备知识:

代码实现:syscall add sysinfotest func · zion6135/xv6@0b1cdcc · GitHub


参考资料:

2.1 抽象物理资源

  • 系统调用接口经过精心设计,既为程序员提供了方便,又提供了强大隔离的可能性。**Unix接口不是抽象资源的唯一方法,但它已被证明是一种非常好的方法。**
  • CPU为强隔离提供硬件支持。例如,RISC-V有三种CPU可以执行指令的模式:Machine mode、Supervior mode和User mode。 在机器模式下执行的指令具有完全权限;CPU以机器模式启动。机器模式主要用于配置计算机。xv6在Machine mode下执行几行,然后更改为Supervior mode。

2.2 特权模式与系统调用

  • 应用程序只能执行User mode指令,并被称为在用户空间中运行,而处于Supervior mode的软件可以执行特权指令,并被称为在内核空间中运行
  • 在Supervior mode中,CPU可以执行特权指令:例如,启用和禁用中断,读取和写入保存页表地址的寄存器等。如果User mode的应用程序尝试执行特权指令,则CPU不会执行该指令,但是会切换到Supervior mode,以便Supervior mode的代码可以终止应用程序。

2.3 内核的组织

  • 为了降低内核出错的风险,操作系统设计人员可以最小化在Supervior mode下运行的操作系统代码量,并在User mode下执行大部分操作系统代码。这种内核组织称为微内核

2.5 进程概览

  • xv6使用页表(硬件实现)来为每个进程提供其独有的地址空间。页表将虚拟地址映射为物理地址。xv6为每个进程维护不同的页表,一片地址空间包含了从虚拟地址0开始的用户内存。它的地址最低处放置进程的指令,接下来则是全局变量,栈区,以及一个用户可按需拓展的“堆”区。有许多因素限制了进程地址空间的最大大小,在Xv6这个值被定义为MAXVA

  • xv6内核为每个进程维护许多状态,并将其收集到struct proc中(kernel/proc.h:86)
  • 系统在进程之间切换实际上就是挂起当前运行的线程,恢复另一个进程的线程。线程的大多数状态(局部变量和函数调用的返回地址)都保存在线程的栈上。每个进程都有用户栈和内核栈(p->kstack)。
  • 在xv6中,一个进程由一个地址空间和一个线程组成。在实际的操作系统中,一个进程可能有多个线程来利用多个cpu。

2.6 Code: 启动xv6,第一个进程和系统调用

。。。

有三种事件会导致CPU不按照原先的执行顺序执行:系统调用(ecall)、异常、硬件中断

4.2 Trap from user space

uservec(trampoline.S) -> usertrap(trap.c) -> usertrapret(trap.c) -> userret(trampoline.S)

System call tracing 

  • 参考

xv6-lab2-syscall_Wound+=s的博客-CSDN博客

  • 实现一个系统调用的跟踪

例如,要跟踪fork系统调用,程序调用trace(1 << SYS_fork),其中SYS_fork是kernel/syscall.h中的一个系统调用号 [ #define SYS_fork    1 ]

  • 切到分支syscall

$ git fetch

$ git checkout syscall

$ make clean

关于syscall函数的代码

  •  以kill.c的系统调用为例:调用了syscall函数 kill() ,syscall定义在/user/user.h文件下。

每个syscall是由usys.pl自动生成为usys.S

  • Makefile的编译如下:usys.pl通过perl工具生成usys.S
$U/usys.S : $U/usys.pl
    perl $U/usys.pl > $U/usys.S
  • 做了些省略,以open函数为例
#!/usr/bin/perl -w

# Generate usys.S, the stubs for syscalls.

print "# generated by usys.pl - do not edit\n";

print "#include \"kernel/syscall.h\"\n";

sub entry {
    my $name = shift;
    print ".global $name\n";
    print "${name}:\n";
    print " li a7, SYS_${name}\n";
    print " ecall\n";
    print " ret\n";
}
	

entry("open");

  • 生成的usys.S如下:以open函数为例
# generated by usys.pl - do not edit
#include "kernel/syscall.h"
.global open
open:
 li a7, SYS_open
 ecall
 ret
  • trace代码实现

finish trace syscall funnction · zion6135/xv6@cbf80c3 · GitHub

  • 系统调用流程

用户空间:

  • 执行trace 32 grep hello README   

user/trace.c里执行grep hello README 并且执行trace 32, 而trace和grep都算系统调用。

上述usys.pl通过perl生成usys.S代码,可在其中找到trace和grep的汇编实现。以trace为例

.global trace
trace:
 li a7, SYS_trace  #等价于 li a7 22, 表示将22这个数字加载到寄存器a7
 ecall   #系统调用
 ret

而执行trace系统调用(li a7 22 和 ecall之后,),程序会跳转到syscall.c的void syscall(void)函数,这里就可理解为什么要把a7赋值为22了。

执行syscall函数的时候,会将num赋值为寄存器a7的值,并通过num找到syscalls中的系统调用号对应的函数指针。从而去执行sys_trace函数!!并将执行结果赋值给寄存器a0

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

至此可以去调用sys_trace函数了,这里需要关注:trace如何拿到传入的参数。可见argint(0. &n); 可将传入的int参数从寄存器p->trapframe->a0中拿到。

uint64
sys_trace(void)
{
  int n;
  //拿到trace传递的第一个参数,到变量n
  if(argint(0, &n) < 0)
    return -1;
  myproc()->trace_mask = n;
  return 0;
}

具体实现如下:argint用于拿到系统调用传递的参数,0-5的参数,对应寄存器a0-a5

// Fetch the nth 32-bit system call argument.
int argint(int n, int *ip) {
  *ip = argraw(n);
  return 0;
}
​
static uint64 argraw(int n) {
  struct proc *p = myproc();
  switch (n) {
  case 0:
    return p->trapframe->a0;
  case 1:
    return p->trapframe->a1;
  case 2:
    return p->trapframe->a2;
  case 3:
    return p->trapframe->a3;
  case 4:
    return p->trapframe->a4;
  case 5:
    return p->trapframe->a5;
  }
  panic("argraw");
  return -1;
}
  1. 用户空间:trace调用 
  2. (由perl生成的汇编函数:trace) 调用ecall +调用号(SYS_trace)存入寄存器a7 
  3. 进入trampline.S【通过ecall从用户态陷入内核态】的调用syscall()函数(syscall.c)
  4. 这里会去拿到寄存器a7的数据,并根据 a7去调用调用号对应的系统函数
  5. 执行系统调用的真正实现sys_trace

Sysinfo

实验目标

  • 实现一个系统调用sysinfo (struct sysinfo*)

  • struct sysinfo {

      uint64 freemem;   // 剩余可用的内存大小bytes

      uint64 nproc;     // 记录进程状态 != UNUSED的进程

    };

  • 本例子有一个测试程序user/sysinfotest.c ===>sysinfotest  打印sysinfotest ok即代表测试通过

预备知识:

  • kalloc.c数据结构
内核程序后的第一个地址, 由kernel.ld定义
extern char end[]; // first address after kernel.
                   // defined by kernel.ld.

struct run {
  struct run *next; 将内存分为一块一块的,直到没有内存为止
};

struct {
  struct spinlock lock;
  struct run *freelist;   指向可用内存的链表
} kmem;
  • kinit:初始化spinlock, 和将kernel后的第一个地址开始,全部整理到freelist链表。
void
kinit()
{
  initlock(&kmem.lock, "kmem");  // 初始化spinlock
  freerange(end, (void*)PHYSTOP); 初始化 从end到PHYSTOP的物理内存
}
  • kalloc:从freelist链表取PGSIZE大小的内存去使用。并更新freelist指向的链表头。
  • kfree:添加PGSIZE大小的数据到链表freelist。
  • freerange:从pa_start到pa_end以PGSIZE大小为单位作为链表节点添加到freelist。

每一页的大小为PGSIZE (#define PGSIZE 4096 // bytes per page)  大小为4K

void
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

注:kalloc.c操作的是直接的物理地址。并不等同于我们在系统之上分配的内存。

代码实现:syscall add sysinfotest func · zion6135/xv6@0b1cdcc · GitHub

  1. 实现sysinfo的系统调用函数接口,先return 0
  2. 在kalloc.c中添加获取可用内存大小
  3. 在proc中去遍历proc[NPROC]得到所有的UBUSED的进程,从而可以得到可用进程数
  4. sysinfo调用接口获取到struct sysinfo的内容
  5. 从内核拷贝数据到userspace

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值