Month: January 2025

  • riscv-xv6系统调用的实现以及trap发生处理过程分析

    这篇文章介绍riscv-xv6的操作系统接口(operating system interface)的实现,系统调用实现了用户态和内核态的隔离,也为实现了不同进程之间的隔离提供了条件(还有其他的机制如虚拟内存和页表等机制一起为实现不同进程的隔离提供了完整解决方案)。在riscv-xv6的user目录下,一般是用户程序的实现源码,如cat,echo, sh等,在这些程序的实现过程中,会调用内核态执行的代码逻辑,如文件读写,fork,exec等系统调用,下面就简要介绍一下添加一个系统调用的实现方法以及系统调用等trap发生时的处理过程逻辑分析。 1、在user/user.h中定义系统调用的prototype(signature),在usys.pl定义添加的系统调用的entry(通过编译过程会生成usys.S,用于生成系统调用的stubs,桩函数作为占位符,通常用于测试、RPC 代理,或者未完成的函数。)sutb里定义了通过指令ecall以及系统调用的参数和系统调用函数指针数组index等信息传递到内核去调用系统调用函数的实现(将系统调用的函数序号索引放入a7寄存器,参考引文[2])。 2、用户程序调用系统调用函数时触发stub函数,会执行ecall指令, 触发异常(trap),进入 S-mode(Supervisor Mode)。ecall 不会自动保存寄存器状态,需要软件(操作系统)来管理寄存器的保存与恢复。当 ecall 发生时,CPU 会保存当前 sepc(保存陷入前的 PC);设置 scause(记录 trap 原因,ecall 在 S-mode 下的 scause=8);切换到 stvec 处理 trap(uservec代码段);清除 SIE 位(即 sstatus.SIE = 0,禁用中断);更新 sstatus.SPP(记录陷入前的特权级)。 3、uservec代码段的功能为:(1)、保存当前用户态执行的cpu的寄存器到trapframe结构中;(2)并将trapframe中保存的内核的栈指针,页表地址等进行加载,并最后调用usertrap函数,具体实现参看引文[3]。usertrap最后调用syscall实现系统函数在内核中的真正执行,usertrap的代码实现根据不同的trap发生的条件(系统调用,硬件中断,时钟定时器中断)做对应的处理,具体可以参考引文[4]。几个关键的寄存器:(1)、stvec(// Supervisor Trap-Vector Base Address)存放发生trap时的代码起始地址,在用户态时应指向uservec[5],在内核态时应为kernelvec[6];kerneltrap发生的情形有外部硬件按中断和时钟中断以及一些运行时错误等(由于已经在内核态,不存在系统调用的的情况。进入 kerneltrap() 前,RISC-V 硬件自动清除 sie,确保不会在处理中断时发生新的中断抢占。sie 负责控制中断使能,如果 sie=0,新的中断不会打断当前的 kerneltrap 处理流程[8]。kerneltrap的处理过程代码及分析参考[12]。 4、usertrapret[7]为从usertrap执行完准备返回用户态时需要执行的代码,主要的逻辑有:关闭硬件中断;设置stvec为uservec;设置寄存器为用户态权限;执行寄存器状态恢复等操作(userret代码段[9])。 5、在创建用户进程的时候相关的函数调用路径有: allocproc([10]) -> forkret([11])-> usertrapret-> w_stvec(trampoline_uservec)([7]),可以看到创建用户进程时子进程被调度开始运行时将uservec代码段起始地址设置到stvec寄存器的问题。关于设置stvec为uservec代码段的流程的更多的分析:(1)、fork() 创建子进程,并设置其 state =…

  • riscv-xv6的页表机制

    本篇文章将简要介绍内核页表和用户进程页表的机制,页表实现了内存的虚拟地址到物理地址的转换,虚拟地址机制实现了进程之间物理存储空间的隔离。在os中,物理内存一般以页(一页,一般为4096字节)进行分配和管理。 在riscv-xv6中实现了三级页表机制:level2,level1,level0。sv39的寻址方式中,virtual address由9+9+9(三级页表项,每一级页表的directory有2^9=512项PTE条目,PTE的组成为(reserved(10bit)+PPN(44bit)+flags(10bit))。其中标记位为PTE_R,PTE_W,PTE_X等,如果这三项都没有标记,则表示该PTE不是叶子PTE,可以通过该PTE进一步检索下一级页表,否则该PPN(对齐到4096,即4K字节)+12位的offset即为具体的物理地址。其中虚拟地址翻译的细节图解如下图所示。 内核页表和用户进程的页表的虚拟地址到物理地址的转换机制相同,都是通过上述的地址转换细节实现。两者细节分别如下: 内核页表机制:低地址一般为外围硬件的寄存器和低地址物理内存对应,其地址映射图示为,其中PLIC( platform-level interrupt controller (PLIC) ),UART0定义为UART的寄存器对应的物理内存地址。VIRTIO为 virtio mmio interface,其中KERNBASE(0x80000000)到PHYSTOP(0x88000000=PHYSTOP (KERNBASE + 128*1024*1024))为xv6所支持的实际物理内存空间 其中kvmmake函数内核映射的代码可以参考引文[1]。其中代码段中下面的两行实现内核的代码和剩余空间的映射,确保内核可以管理所有物理内存。 其中通过kinit函数将内核代码数据段之上的物理内存空间按照物理页的粒度进行初始化并形成链表,kfree实现以页为单位的内存空间的释放,kalloc实现以页为单位的内存空间的申请(提供了锁机制实现了多进程的互斥访问),释放和申请相应的也需要对链表进行操作,具体实现可以参考引文[2]。从上图可以看出,kstack0,kstack1等为用户进程所对应的内核栈,也在kvmmake函数里实现 用户进程的地址空间通过fork、exec和sbrk等系统调用来生成,其中fork创建子进程,复制父进程地址空间的内容(会申请新的页表和PTE对应的物理内存空间,并通过函数memmove将父进程的内容进行复制,包括增加文件描述符的引用计数等),exec通过加载可行性的elf格式的文件,加载代码段数据段到用户页表里(生成了新的页表并释放就的页表空间,具体见引文[3]),然后生成stack空间,堆空间的生成通过sbrk系统调用来生成,其中栈空间的基地址为低地址,栈顶指针为高地址,栈空间从上到下维护程序代码执行的生命周期,包括参数,临时变量等等。 几个问题: 1、用户进程和其对应的内核栈如何实现的关联?回答:初始化进程表的时候实现的关联,具体可以参考引文[4]的代码实现。 2、在用户进程的用户态申请内存时如何实现的?是基于srbk的系统调用吗?回答:扩展堆区的大小可以用srbk系统调用来生成,进一步调用growproc函数和uvmalloc来实现进程的用户空间的存储空间的增长或缩减,可以参考引文[5]的具体实现。 References

  • 大家新年好

    大家新年好,正月初一,给大家拜年了。 这几天deep seek开源了很有影响力的大模型产品,带来了很大的冲击,好的有影响力的技术的产生有一些偶然因素,但更多的是创新者的创新精神和长久持续的行业技术的积累和深度的思考,以及较为强大的驱动力。就像深度学习在2012年左右的突破,带来了长达10几年持续的技术和软硬件产业革新,同时也带动其他行业和学科的综合发展向前,人类不竭的探索精神,科学技术人文的不断发展,希望带给人类可以无限畅想的且可以逐渐变为现实的更加美好的未来。 刚才打开了一下博客,发现有一段时间没有更新了,今年打算继续不定期的写一些短文, 也希望通过记录对相关技术研发,产业应用思考和实践,不断提升技术技能,深化行业思考,落地产业应用。这些短文包括技术文章总结汇总,生活哲学感悟,以及平时的思考、自省和问题记录等等,希望能有合适的更新频率,一年下来也希望有一些文字积累,也是一年生活工作轨迹的记录,以便回顾复盘总结,更好的设计计划执行前行的路,等更久的以后也可以作为温馨的回忆来重温。 祝大家巳巳如意,幸福安康!