您的位置:首页 > 科技 > IT业 > 怎么让网站快速收录_站长统计黄页网站下载大全_地推团队联系方式_公众号推广平台

怎么让网站快速收录_站长统计黄页网站下载大全_地推团队联系方式_公众号推广平台

2025/3/15 15:09:48 来源:https://blog.csdn.net/weixin_46216674/article/details/142619892  浏览:    关键词:怎么让网站快速收录_站长统计黄页网站下载大全_地推团队联系方式_公众号推广平台
怎么让网站快速收录_站长统计黄页网站下载大全_地推团队联系方式_公众号推广平台

【xv6操作系统】系统调用与traps机制解析及实验设计

  • 系统调用
    • 相关理论
    • 系统调用追溯
    • 系统调用实验设计
    • Sysinfo
    • 🚩系统调用总结(结合trap机制)
  • trap
    • trap机制
    • trap代码流程
    • Backtrace实验
    • alarm实验

系统调用

相关理论

  隔离性(isolation): 类似的,操作系统某种程度上为所有的应用程序服务。当你的应用程序出现问题时,你会希望操作系统不会因此而崩溃。比如说你向操作系统传递了一些奇怪的参数,你会希望操作系统仍然能够很好的处理它们(能较好的处理异常情况)。所以,你也需要在应用程序和操作系统之间有强隔离性。

使用操作系统的一个原因,甚至可以说是主要原因就是为了实现multiplexing(CPU在多进程同分时复用)和内存隔离。如果你不使用操作系统,并且应用程序直接与硬件交互,就很难实现这两点。

  • 应用程序不能直接与CPU交互,只能与进程交互
  • 操作系统内核会完成不同进程在CPU上的切换
  • 操作系统不是直接将CPU提供给应用程序,而是向应用程序提供“进程”,进程抽象了CPU,这样操作系统才能在多个应用程序之间复用一个或者多个CPU。

硬件对于强隔离的支持

  当用户程序执行系统调用,会通过ECALL(详情结合下章trap)触发一个软中断(software interrupt),软中断会查询操作系统预先设定的中断向量表,并执行中断向量表中包含的中断处理程序。中断处理程序在内核中,这样就完成了user mode到kernel mode的切换,并执行用户程序想要执行的特殊权限指令。

  每一个进程都会有自己独立的page table,这样的话,每一个进程只能访问出现在自己page table中的物理内存。操作系统会设置page table,使得每一个进程都有不重合的物理内存,这样一个进程就不能访问其他进程的物理内存,因为其他进程的物理内存都不在它的page table中。

编译运行kernel:
  Makefile会为所有内核文件做相同的操作,比如说pipe.c,会按照同样的套路,先经过gcc编译成pipe.s,再通过汇编解释器生成pipe.o。之后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。

User/Kernel mode切换:

  • 用户的应用程序执行系统调用的唯一方法就是通过这里的ECALL指令。

  • 调用ECALL指令,并将fork对应的数字作为参数传给ECALL

  • 这里的数字参数代表了应用程序想要调用的System Call。

  • 可以通过系统调用或者说ECALL指令,将控制权从应用程序转到操作系统中

进程虚拟地址空间

  • 使用符号 p->xxx 来指代 proc 结构中的元素;struct proc 在 kernel/proc.h 文件第 86 行定义。

  • 每个进程有两个栈:一个用户栈 user stack 和一个内核栈 kernel stack ( p->kstack )

    enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
    // Per-process state
    struct proc {struct spinlock lock;			// 锁// p->lock must be held when using these:enum procstate state;        // Process statevoid *chan;                  // If non-zero, sleeping on chanint killed;                  // If non-zero, have been killedint xstate;                  // Exit status to be returned to parent's waitint pid;                     // Process ID// wait_lock must be held when using this:struct proc *parent;         // Parent process// these are private to the process, so p->lock need not be held.uint64 kstack;               // Virtual address of kernel stackuint64 sz;                   // Size of process memory (bytes)pagetable_t pagetable;       // User page tablestruct trapframe *trapframe; // data page for trampoline.Sstruct context context;      // swtch() here to run processstruct file *ofile[NOFILE];  // Open filesstruct inode *cwd;           // Current directorychar name[16];               // Process name (debugging)
    };
    

在内核态中如何获取用户参数,如sleep(10),涉及一个拷贝

系统调用追溯

1. 实验要求: 添加一个trace system call调用,可以实现跟踪system call。此函数入参为一个数字,可以控制跟踪哪些system call。

如:

  • trace(1<<SYS_fork),trace(10b),trace(2)表示跟踪fork调用;
  • trace(1<<SYS_read),trace(10 0000b),trace(32),表示跟踪read调用;

2. 一些理论基础


  • initcode.S 将 exec 的参数放置在寄存器 a0 和 a1,系统调用编号放在 a7 中.

    # exec(init, argv)
    .globl start
    start:la a0, initla a1, argvli a7, SYS_exececall
    
  • sys_call返回值:sys_exec 返回时,系统调用会将其返回值记录在 p->trapframe->a0 中。如果系统调用编号无效,系统调用将打印错误并返回 −1

system call调用链路

1)在user/user.h做函数声明
2)Makefile调用usys.pl(perl脚本)生成usys.S,里面写了具体实现,通过ecall进入kernel,通过设置寄存器a7的值,表明调用哪个system call
3)ecall表示一种特殊的trap,转到kernel/syscall.c:syscall执行
4)syscall.c中有个函数指针数组,即一个数组中存放了所有指向system call实现函数的指针,通过寄存器a7的值定位到某个函数指针,通过函数指针调用函数

系统调用实验设计

核心1:内核函数调用过程

entry进入内核,调用syscall函数,通过a7获取函数指针数组的数值(即sys_call的入口地址)


核心2:内核获取用户参数

调用 argintargaddr函数,本质是从进程中获取


实验设计:

(1)链路配置:

  • user.h声明 trace函数

    int trace(int);
    
  • user.pl加入跳板函数

    entry("trace");
    
  • 在 syscalls 函数指针数组添加 sys_trace 数组,并在syscall.h中宏定义 SYS_trace

    // syscall.c
    [SYS_trace]   sys_trace,// syscall.h
    #define SYS_trace  22
    
  • 在syscall中外部函数声明,并在sys_call在sysproc.c中定义函数原型 sys_trace

    //sys_call.c
    extern uint64 sys_trace(void);//sysproc.c
    //Add a sys_trace() function 
    uint64 sys_trace(void)
    

(2)函数实现: 根据实验要求设计

  • 在 sys_trace 中获取用户系统调用参数 mask
    uint64 
    sys_trace(void)
    {int mask;if(argint(0, &mask) < 0)return -1;
    }
    
  • 通过在 proc 结构的新变量中记住其参数来实现新的系统调用,即 proc 结构体(proc.h)中添加新的变量 mask,以便子进程继承
    //Add a sys_trace() function 
    uint64 
    sys_trace(void)
    {int mask;if(argint(0, &mask) < 0)return -1;//获取当前进程struct proc *p = myproc();//需要在proc.h结构体中添加新的成员变量 maskp->trace_mask = mask;		return 0;
    }
    
  • 修改 fork() (kernel/proc.c) 以将跟踪掩码从父进程复制到子进程
    //copy the trace mask from the parent to the child process.
    np->trace_mask = p->trace_mask;
    
  • 修改 kernel/syscall.c 中的 syscall()函数,以打印跟踪输出

Sysinfo

  • 个人认为本题重点在于如何实现用户态与内核态的信息交互
    (调用、参数、返回值等)

添加一个sysinfo system call调用,可以实现打印可用空间(字节)、可用进程数

首先阅读测试案例:user/sysinfotest.c:main,需结合程序

  • testcall:测试 sysinfo调用失败,用户传递非法内存地址,因为Risc-v只支持39位 在下一章页表中涉及
  • testmem:使用sbrk分配物理内存页,测试剩余分配量,释放测试
  • testproc:测试有多少进程未使用,测试fork,使用 + 1,未使用 -1

实验设计: 构建 sysinfo 系统调用链路,略,参考System call tracing 中的实验设计的第一步:链路配置

程序设计:
1. 获取用户sysinfo参数

uint64 addr;             // user pointer to struct stat
// step1: copy a struct sysinfo back to user space
if(argaddr(0, &addr) < 0)return -1;

2. 获取未使用的空间和进程数存入 struct sysinfo 结构体

struct sysinfo info;        // sysinfo struct// step2 get freemen 
info.freemem = acquire_freemen();	//kalloc.c
// step3 get unused number of processes
info.nproc = acquire_npro();		//proc.c

3. 功能函数实现
(1) 获取未使用的空间,参考 kalloc.c: kalloc,遍历空页表,计算页数,返回 页数 * 每一页的字节数,这里涉及到一点下一章的页表知识

// 参考 kalloc 
uint64 acquire_freemen(void)
{struct run *r;        //listuint64 cnt = 0;acquire(&kmem.lock);  r = kmem.freelist;//遍历链表求长度 页表while(r){r = r->next;cnt++;}release(&kmem.lock);return cnt * PGSIZE;    // 页表页数 * 每一页的字节数
}

(2) 获取未使用的进程数,遍历进程,判断进程状态

uint64 acquire_npro(void)
{struct proc *p;   // 进程指针int cnt = 0;// 遍历所有进程for(p = proc; p < &proc[NPROC]; p++) {acquire(&p->lock);          // get lock//进程状态为使用if(p->state == UNUSED) {cnt++;} release(&p->lock);        // release lcok  }return cnt;
}

4. 内核数据拷贝给用户,利用 copyout 函数将内核地址的数据拷贝给用户态
函数定义:

int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)

数据拷贝实现

struct proc *p = myproc();  // process p
copyout(p->pagetable, addr, (char *)&info, sizeof(info));

5. 完整 sys_sysinfo 函数实现:

//Add a sys_sysinfo() function 
uint64 
sys_sysinfo(void)
{struct sysinfo info;        // sysinfo structuint64 addr;                // user pointer to struct statstruct proc *p = myproc();  // process p// step2 get parainfo.freemem = acquire_freemen();// step3 get number of processesinfo.nproc = acquire_npro();// step1: copy a struct sysinfo back to user spaceif(argaddr(0, &addr) < 0)return -1;if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)return -1;//printf("call sys_sysinfo Hi\n");return 0;
}

🚩系统调用总结(结合trap机制)

  • 这里在学习页表、trap后总结



trap

  大佬认为: 本部分主要内容其实都在lecture里(lecture 5、lecture 6),实验不是非常复杂但是以理解概念为重,trap机制、trampoline作用、函数calling convention、调用栈、特权模式、riscv汇编,这些即使都不知道可能依然能完成 lab。但是不代表这些不重要,相反这些才是主要内容,

参考大佬的博文:[mit6.s081] 笔记 Lab4: Traps | 中断陷阱

trap机制

参考博文: Mit6.S081学习笔记

  • 程序执行系统调用
  • 程序出现了类似page fault、运算时除以0的错误
  • 一个设备触发了中断使得当前程序运行需要响应内核设备驱动

都会发生用户空间和内核空间的切换,这种切换通常被称为trap


对比 (二)ARM寄存器组织与异常处理 中的异常处理学习,这是ARM寄存器组织异常处理过程:

  • 拷贝之前的模式及状态
  • 切换模式 用户模式 -> 异常模式
    • 禁止相应的中断
    • 修改模式位
    • 修改状态位
  • 保存返回地址
  • 设置 PC 为相应的中断异常向量表的值


先认识一些内核模式下的寄存器,留个印象:

  • scause: trap类型(sys_call, page fault)
  • sststus: 保存状态模式(User / supervisor mode)
  • scratch: trapframe地址
  • sepc: 保存当前PC的值
  • stvec: trap handler地址
  • csr: 控制状态寄存器,csrr,csrw,csrrw指令

用户下的内存块:

  • trampoline:存放切换代码的地方 (内核下也有)
  • trapframe:类似于上下文保存的结构 (内核下没有)

  需要清楚如何让程序的运行,从只拥有user权限并且位于用户空间的程序,切换到拥有supervisor权限的内核。在这个过程中,硬件的状态将会非常重要,因为很多的工作都是将硬件从适合运行用户应用程序的状态,改变到适合运行内核代码的状态。

用户应用程序可以使用全部的32个寄存器,很多寄存器都有特殊的作用。其中:

  • 程序计数寄存器PC(Program Counter Register)
  • 表明当前mode的标志位,这个标志位表明了当前是 supervisor mode 还是 user mode
  • 还有一堆控制CPU工作方式的寄存器,比如 SATP(Supervisor Address Translation andProtection)寄存器,包含了指向page table的物理内存地址
  • STVEC(Supervisor Trap Vector Base Address Register)寄存器,指向了内核中处理trap的指令的起始地址
  • SEPC(Supervisor Exception Program Counter)寄存器,在trap的过程中保存程序计数器的值
  • SSCRATCH(Supervisor Scratch Register)寄存器,这也是个非常重要的寄存器

这些寄存器表明了执行系统调用时计算机的状态。


trap代码流程

用户程序执行系统调用函数(实际上通过执行ECALL指令来执行系统调用)

  用户程序→ ECALL→uservec(在trampoline中)→usertrap(在trap.c中)→ syscall → sys xxx(对应的系统调用)一执行结果返回给syscall→usertrapret(在trap.c中)→userret(在trampoline中)→系统调用完成,返回到用户空间,恢复ECALL之后的用户程序的执行

一、当一个trap来临((ecall指令)RISC-V硬件做了什么

  1. 如果是设备中断,并且状态SIE位(sststus中的标志位)被清空,不执行以下操作
  2. 清除SIE以禁用中断(disable interrupts by clearing SlE)(ECALL禁用,防止切换到其他进程,并重新trap,覆盖sepc
  3. 保存pc 的值到sepc(epc = PC)
  4. 保存当前所处模式到SPP位(sststus中的标志位) (user mode -> supervisor mode
  5. 设置scause表明 trap类型(Set scause to reflect the trap’s cause)
  6. 切换到supervisor模式(Set the mode to supervisor)
  7. stvec寄存器里面的值复制到pc(Copy the stvec to the pc)
  8. pc上取值执行(Start executing at the new pc)

所以现在,ecall在硬件上帮我们做了一点点工作,但是实际上我们离执行内核中的C代码还差的很远。接下来:

  • 我们需要保存32个用户寄存器的内容,这样当我们想要恢复用户代码执行时,我们才能恢复这些寄存器的内容。(保存现场)
  • 因为现在我们还在user page table,我们需要切换到kernel page table
  • 我们需要创建或者找到一个kernel stack,并将Stack Pointer寄存器的内容指向那个kernel stack。这样才能给C代码提供栈
  • 我们还需要跳转到内核中C代码的某些合理的位置。

ecall并不会为我们做这里的任何一件事。

GDB过程:

二、uservec函数

  • 保存现场(32个通用寄存器)
  • 把内核的page table,内核的stack、当前执行该进程的CPU号装载到寄存器里
  • 跳转到usertrap继续执行

三、usertrap函数

  • 分情况,执行系统调用/中断/异常的处理逻辑

    • scause == 8 -> syscall -> num = p->trapframe->a7 -> syscalls表驱动 -> sys_write(16)
  • 修改了stvec的值,还可能会修改sepc的值

    void usertrap(void)
    {int which_dev = 0;if((r_sstatus() & SSTATUS_SPP) != 0)panic("usertrap: not from user mode");// send interrupts and exceptions to kerneltrap(),// since we're now in the kernel.w_stvec((uint64)kernelvec);// save user program counter.p->trapframe->epc = r_sepc();if(r_scause() == 8) {// system call// sepc points to the ecall instruction,// but we want to return to the next instruction.p->trapframe->epc += 4;// an interrupt will change sstatus &c registers,// so don't enable until done with those registers.intr_on();syscall();} // give up the CPU if this is a timer interrupt.if(which_dev == 2) {yield();}usertrapret();
    }
    

四、usertrapret函数

  • 填入了trapframe的内容,这样下一次从用户空间转换到内核空间时可以用到这些数据
    • 存储 kernel page table 的指针
    • 存储当前用户进程的 kernel stack
    • 存储 usertrap 函数的指针,这样trampoline代码才能跳转到这个函数
    • 从tp寄存器中读取当前的CPU核编号,并存储在trapframe中
    • 恢复stvec、sepc的值(supervisor mode register)

五、userret函数

  • 恢复现场

  • 把用户空间的 page table、用户空间的 stack 装载到寄存器里

  • 执行sret指令(相对于ecall指令

六、sret指令

  • 程序会切换回user mode
  • SEPC寄存器的数值会被拷贝到PC寄存器(程序计数器)
  • 重新打开中断

Backtrace实验

添加 backtrace 功能,打印出调用栈,用于调试

实验步骤:

  • 在 defs.h 中添加声明 backtrace,并在 print 中定义

  • 在 riscv.h 中添加获取当前 fp(frame pointer)寄存器的方法:

    static inline uint64
    r_fp()
    {uint64 x;asm volatile("mv %0, s0" : "=r" (x) );return x;
    }
    
  • 获取当前栈帧指针

    void backtrace(void)
    {//并在 Backtrace 中调用r_fp以读取当前帧指针//此函数使用内联汇编来读取 s0。uint64 fp = r_fp();       // 当前栈帧指针
    
  • lecture notes have a picture of the layout of stack frames. The return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.

  • Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp)

  • 程序设计

    void backtrace(void)
    {//并在 Backtrace 中调用r_fp以读取当前帧指针//此函数使用内联汇编来读取 s0。uint64 fp = r_fp();       // 当前栈帧指针uint64 return_address;    // 函数返回地址 printf("backtrace:\n");// 判断是否已经到达栈底while (fp != PGROUNDUP(fp)){// 获取每个栈的返回地址return_address = *(uint64*)(fp - 8);// 更新 fp 获取上个栈的栈帧地址fp = *(uint64*)(fp - 16);printf("%p:\n",return_address);}
    }
    

函数调用栈(Stack)

  • 栈由高地址往低地址增长
  • 在xv6里,有一页大小(4KB)
  • 栈指针(stack pointer)保存在 sp 寄存器里

栈帧(Stack Frame)

  • 当前栈帧的地址保存在 s0/fp 寄存器里

  • 当前栈帧的地址也叫栈帧的指针(frame pointer, fp),指向该栈帧的最高处

  • 栈帧指针往下偏移8个字节是函数返回地址 return address

  • 往下偏移16个字节是上一个栈帧的栈帧指针 previous frame pointer

  • fp 指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。
  • 栈从高地址往低地址生长,所以 fp 虽然是帧开始地址,但是地址比 sp 高
  • 栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
  • 栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
  • 剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。
  • 在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底

查看 call.asm,可以看到,一个函数的函数体最开始首先会扩充一个栈帧给该层调用使用,在函数执行完毕后再回收,例子:

int g(int x) {0:	1141                  addi  sp,sp,-16  // 扩张调用栈,得到一个 16 字节的栈帧2:	e422                  sd    s0,8(sp)   // 将返回地址存到栈帧的第一个 8 字节中4:	0800                  addi  s0,sp,16return x+3;
}6:	250d                  addiw a0,a0,38:	6422                  ld    s0,8(sp)   // 从栈帧读出返回地址a:	0141                  addi  sp,sp,16   // 回收栈帧c:	8082                  ret              // 返回

注意栈的生长方向是从高地址到低地址,所以扩张是 -16,而回收是 +16。

alarm实验

  该实验需要实现 sigalarm和 sigreturn 两个系统调用,为用户进程添加定期通知功能,使得进程在一段时间内使用 CPU 后,会被定期“提醒”,类似于一种用户态的中断处理,用来模拟用户级的异常处理。

作用:

  • 对于希望限制其占用CPU时间的计算密集型进程,或者希望进行计算但也希望采取一些定期行动的进程可能很有用。
  • 更一般来说,你将实现一种原始的用户级中断/故障处理程序;例如,你可以使用类似的机制来处理应用程序中的页面错误

实验要求与实现:

0. 核心参数设置:

struct proc{...// alarm test0int ticks;                    // 报警间隔 interval for the alarmuint64 handler;               // call functionint ticks_count;              // how many ticks right now//test1 test2 struct trapframe *save_trap_frame;  // 保存现场int is_handling;                    // 是否正在中断 防止二次打断...
}

1. 添加新的系统调用:

  1. user.h 声明系统调用
  2. usys.pl添加入口
  3. syscall.h函数编号
  4. syscall.c更加系统调用,做函数表映射

2. 保存 sigalarm 的报警间隔与 handler 指针保存在 struct proc 中新的字段,并在proc.c中allocproc初始化字段

  • 核心代码:

    sys_sigalarm:
    获取用户参数
    argint(0, &ticks);
    argaddr(1, &handler);
    // 保存参数
    p->ticks = ticks;
    p->handler = handler;
    p->ticks_count = 0;sys_sigreturn:
    p->is_handling = 0;      	清空中断标记位  
    memmove(p->trapframe, p->save_trap_frame, PGSIZE);	中断返回 保存现场的地址 
    
  • 完整代码

  • 系统调用函数实现:
    (1)sys_sigalarm 获取用户数据(间隔时间与处理函数handler)并保存到进程中

    uint64 
    sys_sigalarm(void)
    {int ticks;uint64 handler;// 进入中断// 获取进程struct proc *p = myproc();// 获取用户参数argint(0, &ticks);argaddr(1, &handler);// 保存参数p->ticks = ticks;p->handler = handler;p->ticks_count = 0;return 0;
    }

    (2)sys_sigreturn 中断返回保存现场,清空中断标志位

    uint64 sys_sigreturn(void){  //恢复现场// 获取进程struct proc *p = myproc();// 清空异常标志p->is_handling = 0;        // 中断返回 保存现场的地址memmove(p->trapframe, p->save_trap_frame, PGSIZE);        return 0;}
    
  • proc/allocproc函数初始化成员变量

      // init p->ticks = 0;p->ticks_count = 0;p->handler = 0;
    

3. 在kernel/trap.c中的实现该时钟中断的代码usertrap

  • 核心代码:

    保存中断现场 
    memmove(p->save_trap_frame, p->trapframe, PGSIZE);
    handler存入epc寄存器,破坏了现场,因此这两行代码不可先后更换
    p->trapframe->epc = p->handler;   
    
  • 完整代码:

      // give up the CPU if this is a timer interrupt.if(which_dev == 2){if(p->ticks > 0){p->ticks_count++;//时间到 并且无其他中断if(p->ticks_count > p->ticks && p->is_handling == 0){p->ticks_count = 0;//保存中断现场 memmove(p->save_trap_frame, p->trapframe, PGSIZE);//执行函数的地址入口p->trapframe->epc = p->handler;   // handler存入epc寄存器p->is_handling = 1;               // 标记正在中断}}yield();}
    

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com