• 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
    1. $U/usys.S : $U/usys.pl
    2.     perl $U/usys.pl > $U/usys.S
    • 做了些省略,以open函数为例
    1. #!/usr/bin/perl -w
    2. # Generate usys.S, the stubs for syscalls.
    3. print "# generated by usys.pl - do not edit\n";
    4. print "#include \"kernel/syscall.h\"\n";
    5. sub entry {
    6. my $name = shift;
    7. print ".global $name\n";
    8. print "${name}:\n";
    9. print " li a7, SYS_${name}\n";
    10. print " ecall\n";
    11. print " ret\n";
    12. }
    13. entry("open");
    • 生成的usys.S如下:以open函数为例
    1. # generated by usys.pl - do not edit
    2. #include "kernel/syscall.h"
    3. .global open
    4. open:
    5. li a7, SYS_open
    6. ecall
    7. 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为例

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

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

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

    1. void
    2. syscall(void)
    3. {
    4. int num;
    5. struct proc *p = myproc();
    6. num = p->trapframe->a7;
    7. if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    8. p->trapframe->a0 = syscalls[num]();
    9. } else {
    10. printf("%d %s: unknown sys call %d\n",
    11. p->pid, p->name, num);
    12. p->trapframe->a0 = -1;
    13. }
    14. }

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

    1. uint64
    2. sys_trace(void)
    3. {
    4. int n;
    5. //拿到trace传递的第一个参数,到变量n
    6. if(argint(0, &n) < 0)
    7. return -1;
    8. myproc()->trace_mask = n;
    9. return 0;
    10. }

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

    1. // Fetch the nth 32-bit system call argument.
    2. int argint(int n, int *ip) {
    3. *ip = argraw(n);
    4. return 0;
    5. }
    6. static uint64 argraw(int n) {
    7. struct proc *p = myproc();
    8. switch (n) {
    9. case 0:
    10. return p->trapframe->a0;
    11. case 1:
    12. return p->trapframe->a1;
    13. case 2:
    14. return p->trapframe->a2;
    15. case 3:
    16. return p->trapframe->a3;
    17. case 4:
    18. return p->trapframe->a4;
    19. case 5:
    20. return p->trapframe->a5;
    21. }
    22. panic("argraw");
    23. return -1;
    24. }
    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数据结构
    1. 内核程序后的第一个地址, 由kernel.ld定义
    2. extern char end[]; // first address after kernel.
    3. // defined by kernel.ld.
    4. struct run {
    5. struct run *next; 将内存分为一块一块的,直到没有内存为止
    6. };
    7. struct {
    8. struct spinlock lock;
    9. struct run *freelist; 指向可用内存的链表
    10. } kmem;
    • kinit:初始化spinlock, 和将kernel后的第一个地址开始,全部整理到freelist链表。
    1. void
    2. kinit()
    3. {
    4. initlock(&kmem.lock, "kmem"); // 初始化spinlock
    5. freerange(end, (void*)PHYSTOP); 初始化 从end到PHYSTOP的物理内存
    6. }
    • kalloc:从freelist链表取PGSIZE大小的内存去使用。并更新freelist指向的链表头。
    • kfree:添加PGSIZE大小的数据到链表freelist。
    • freerange:从pa_start到pa_end以PGSIZE大小为单位作为链表节点添加到freelist。

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

    1. void
    2. freerange(void *pa_start, void *pa_end)
    3. {
    4. char *p;
    5. p = (char*)PGROUNDUP((uint64)pa_start);
    6. for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    7. kfree(p);
    8. }

    注: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

  • 相关阅读:
    stencilJs学习之构建 Drawer 组件
    JUC学习笔记——共享模型之内存
    Modbus在Java中使用总结
    文件系统考古4:如何支持多个文件系统
    Spring中Bean的作用域
    美创科技获浙江省网络空间安全协会多项荣誉认可
    互联网摸鱼日报(2022-11-25)
    【Linux 06】项目自动化构建工具 make / makefile
    蓝眼开源云盘部署全过程(手动安装)
    SpringCloud微服务治理技术入门(SCN)
  • 原文地址:https://blog.csdn.net/m0_37844072/article/details/127885416