系统调用分析

xv6的系统调用

系统调用就是内核提供出系统调用接口,然后供开发者进行使用,你无需关心里面的实现方法,只需要按照规定去进行调用使用即可。但是,为了深入了解一下,在这里概述一下系统调用的过程

代码分析

内核与用户程序的桥梁

  1. 在学习了一段时间后,我们可以看到xv6里面有着许多系统调用,在lab1里面我们就做过forkexecsleep之类的系统调用。可以看到的是,在user目录下,只有许多这些系统调用函数的定义,具体细节并没有实现,而为了实现系统调用,我们就不得不去转到kernel里面去执行,那么这个过程是怎么发生的呢?

  2. 这里就用到了我们的桥梁,也就是user/usys.pl,这是一个自动脚本,我们可以来看一下里面的内容

    #!/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";
    ​
    # 这是我们的系统调用存根
    # 会生成一个汇编文件
    # 其中 li 指令是加载立即数,也就是把 SYS_name 的系统调用号加载到a7寄存器里面
    # 然后就去调用ecall命令,这个命令会提高运行的权限,为后续进入内核做准备
    # 然后执行完退出就行
    ​
    sub entry {
        my $name = shift;
        print ".global $name\n";
        print "${name}:\n";
        print " li a7, SYS_${name}\n";
        print " ecall\n";
        print " ret\n";
    }
    ​
    # 在这个文件里面添加存根(stub),生成我们的系统调用汇编文件
    ​
        
    entry("fork");
    entry("exit");
    entry("wait");
    entry("pipe");
    entry("read");
    entry("write");
    entry("close");
    entry("kill");
    entry("exec");
    entry("open");
    entry("mknod");
    entry("unlink");
    entry("fstat");
    entry("link");
    entry("mkdir");
    entry("chdir");
    entry("dup");
    entry("getpid");
    entry("sbrk");
    entry("sleep");
    entry("uptime");
    entry("trace");
    ​

    其中,我们重点来看一下entry的部分,这个脚本会接受一个参数,也就是系统调用的名称,然后去生成汇编文件,也就是说,当我们调用系统调用sleep(举个例子),便会生成这个汇编文件

    .global sleep
    sleep:
        li a7, SYS_sleep
        ecall
        ret

    这几行汇编的意思就是把sleep系统调用号(在syscall.h里面定义)存储到a7寄存器里面,然后执行ecall指令,提高运行权限,由user model转到监管模式下,在此,我们便成功进入了内核。同时,在执行的时候,我们可以去使用make qemu去编译一下,查看一下生成的usys.S文件。PS(有时候会看到汇编文件是大写的S而不是小写的s,这是因为S里面还包含一些预处理指令)。

  3. ecall指令干了什么 了解到了一些关于ecall指令的细节。刚开始一直在想,为什么执行个ecall就可以去跳转到内核里面了。今天来细细说一下ecall指令干的事。

    • 首先提升权限,这个是很明显的

    • 其次是把next pc值存储到sepc(Smode exception pc S态异常处理pc),因为处理完还要跳转回来嘛

    • 然后跳转到STVEC(Smode Trap Vector)寄存器那,这个存储的就是我们的异常处理程序的开端,也是一个很大的数

    • stvec寄存器里面存储的就是我们蹦床(trampoline)的地址,蹦床我觉得就是一个过度机制,因为你进入了内核,但是你不能在内核直接执行c代码,因为现在的这些寄存器里面存储的还是用户态的数据,不能直接去执行c函数,你还得把这些数据存储下来,存储到别的地方去,而蹦床就提供了一个缓冲机制

    • 之后就开始我们参数的传递了

函数的参数传递

  1. 在执行完trampoline后,我们就开始执行trapframe里面的东西了,这个结构体在kernel/proc.h里面保存,目的就是为了去保存用户寄存器里面的数据,可以看到

    struct trapframe {
      /*   0 */ uint64 kernel_satp;   // kernel page table
      /*   8 */ uint64 kernel_sp;     // top of process's kernel stack
      /*  16 */ uint64 kernel_trap;   // usertrap()
      /*  24 */ uint64 epc;           // saved user program counter
      /*  32 */ uint64 kernel_hartid; // saved kernel tp
      /*  40 */ uint64 ra;
      /*  48 */ uint64 sp;
      /*  56 */ uint64 gp;
      /*  64 */ uint64 tp;
      /*  72 */ uint64 t0;
      /*  80 */ uint64 t1;
      /*  88 */ uint64 t2;
      /*  96 */ uint64 s0;
      /* 104 */ uint64 s1;
      /* 112 */ uint64 a0;
      /* 120 */ uint64 a1;
      /* 128 */ uint64 a2;
      /* 136 */ uint64 a3;
      /* 144 */ uint64 a4;
      /* 152 */ uint64 a5;
      /* 160 */ uint64 a6;
      /* 168 */ uint64 a7;
      /* 176 */ uint64 s2;
      /* 184 */ uint64 s3;
      /* 192 */ uint64 s4;
      /* 200 */ uint64 s5;
      /* 208 */ uint64 s6;
      /* 216 */ uint64 s7;
      /* 224 */ uint64 s8;
      /* 232 */ uint64 s9;
      /* 240 */ uint64 s10;
      /* 248 */ uint64 s11;
      /* 256 */ uint64 t3;
      /* 264 */ uint64 t4;
      /* 272 */ uint64 t5;
      /* 280 */ uint64 t6;
    };

    这就是这些寄存器,比较奇怪的是,前面还有着偏移量,我现在来解释一下这个偏移量是什么意思,在进入用户空间之前(无论是由于进程启动还是从中断中恢复),内核会先设置sscratch寄存器指向trapframe,它是每个进程都有的一个用于存储所有用户寄存器的结构体,并且,它也被映射到用户页表的TRAPFRAME部分,位于TRAMPOLINE的下面,所以也可以通过用户页表访问

  2. 在系统调用的时候,我们还要传入参数嘛,比如sleep里面的休眠时间就是一个参数,在传递参数的时候,通常是由寄存器来保存我们的参数的,其中还有着一个复杂的函数调用约定,就是要求了哪个寄存器去保存我们的第一个参数、第二个参数、函数返回参数等等,在调用函数接口的时候,就已经把参数给保存好了。

  3. 这就又引出了一个问题,我们在函数的参数都在用户态的时候保存下来了,那么进入内核态后我们怎么传递参数呢。这里就封装出了另一个函数,这个函数就在syscall.c里面,你可以把它理解为就是从寄存器里面取出参数的函数

  4. 现在我们来了解一下什么是trampoline,这个意思直译过来就是蹦床的意思,在我们执行系统调用后,每个系统调用的接口其实就是汇编函数的体现,就像是上面那个sleep汇编。执行ecall指令后,提高运行权限。。。

系统调用图例

  1. 这张图片真是完美的解释了系统调用的全流程,在执行完trap后,就会去执行syscall函数,此时,先在syscall函数里面去找到我们要执行的系统调用编号(存储在a7寄存器里面),然后去执行,执行完后,把返回值存储在a0寄存器里面,这样,我们就可以实现系统调用的跟踪过程,然后返回。

  2. 现在我们来分析一下怎么实现syscall里面的trace,这个功能就是去跟踪调用,首先这个系统调用有一个参数,就是一个系统调用的掩码,(1<<SYS_name)这样的形式。因为syscall这个函数是所有系统调用的抽象接口,这个函数里面封装了一个系统调用函数的结构体,通过传进来的系统调用号(a7寄存器)我们就可以直接去在数组里面调用这个函数,然后调用完后把返回值保存在我们的a0寄存器里面,这个寄存器里面存储的就是我们的返回值,调用完这个函数之后,就返回陷阱处理函数,然后再逐渐的进行返回。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值