尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MIT 6.S081 syscall 实验篇(lab2):Sysinfo (moderate)

MIT 6.S081 syscall 实验篇(lab2):Sysinfo (moderate) Sysinfo (moderate)实验目标本实验要新增一个系统调用sysinfo让用户程序能查询两项系统状态freemem当前空闲物理内存的字节数nproc当前处于活动状态的进程数量即UNUSED之外的进程数。教材已经在kernel/sysinfo.h把承载这两项数据的结构体定义好了我们要做的就是把数据填进去、并把它安全地送回用户进程/* * kernel/sysinfo.h */structsysinfo{uint64 freemem;// amount of free memory (bytes)uint64 nproc;// number of process};和 trace 实验的本质区别trace 只是在内核里printf打印数据不需要出内核而sysinfo要把内核采集到的数据跨越地址空间拷贝回用户进程——这正是本实验最关键、也最容易踩坑的地方。前置知识动手前先立住三个概念。1. 一个系统调用从用户态到内核态的完整链路用户程序调用sysinfo()后并不会直接进内核而是走这样一条路径用户程序 sysinfo() ↓ usys.S 里的跳板ecall 指令 ↓ trampoline.S uservec保存现场到 trapframe ↓ usertrap()kernel/trap.c ↓ syscall()kernel/syscall.c统一分发入口 ↓ sys_sysinfo()真正的内核处理函数kernel/sysproc.c ↓ 返回 syscall() → usertrapret() → userret → 回到用户态所以所有系统调用的总入口都是syscall()而具体干活的是sys_xxx。新增系统调用本质就是把一条新链路接进这套既有框架。2. 跨地址空间的数据传递本实验核心sysinfo的难点不在怎么算而在怎么把结果给用户用户程序传进来的struct sysinfo *是用户虚拟地址存在于该进程自己的用户页表里内核处理函数里声明的struct sysinfo info在内核地址空间内核不能直接往用户虚拟地址写因为两边地址空间不同。所以标准做法是structsysinfoinfo;// 1. 内核空间里准备好结构freebytes(info.freemem);// 2. 内核里填数据procnum(info.nproc);uint64 dstaddr;argaddr(0,dstaddr);// 3. 取出用户传来的指针用户虚拟地址copyout(myproc()-pagetable,dstaddr,(char*)info,sizeofinfo);// 4. 内核→用户 拷贝copyout内部会逐页把用户虚拟地址翻译成物理地址再memmove它替你处理了跨页、越界检查是内核安全地写用户内存的唯一正确姿势。3. 两个统计数据的来源数据存在哪怎么统计freememkernel/kalloc.c的空闲链表kmem.freelist遍历空闲页链表每个节点就是一页PGSIZE累加倍增nprockernel/proc.c的进程表proc[NPROC]遍历进程表凡是state ! UNUSED的都算活动实现思路顺着系统调用执行顺序需要改动的地方是一条连贯的链用户态要能调到sysinfo→user/user.h声明含struct sysinfo前向声明user/usys.pl生成跳板 Makefile把_sysinfotest编进镜像。内核要认得这个新系统调用→kernel/syscall.h分配号SYS_sysinfo 23。sysinfo的内核实现→kernel/sysproc.c里写sys_sysinfo()填info、取用户指针、copyout回用户。两个统计函数→freebytes()放kalloc.c离空闲链表最近procnum()放proc.c离进程表最近。让sys_sysinfo找到这两个函数→ 在kernel/defs.h补上extern风格的声明注意它们不是sys_开头所以不参与syscalls[]分发只需普通声明。把sys_sysinfo接进分发数组→kernel/syscall.c里extern 挂进syscalls[]sysnames[]加名字。一句话加用户接口 → 分配号 → 实现处理函数 → 实现两个统计 → 补声明 → 注册分发。代码实现kernel/sysinfo.h—— 结构体教材已给本实验的数据载体教材直接提供无需修改仅作引用/* * kernel/sysinfo.h */structsysinfo{uint64 freemem;// amount of free memory (bytes)uint64 nproc;// number of process};用户态三处修改( a )user/user.h—— 声明接口 前向声明结构体int sysinfo(struct sysinfo*)用到了struct sysinfo类型所以先在文件顶部加一句前向声明struct sysinfo;否则函数声明里的参数类型编译器不认识/* * user/user.h */structstat;structrtcdate;structsysinfo;// 添加前向声明// system callsintfork(void);...intuptime(void);inttrace(int);intsysinfo(structsysinfo*);// 用户态函数声明注意用户程序真正分配struct sysinfo info;变量时需要完整定义这由user/sysinfotest.c里的#include kernel/sysinfo.h提供user.h里只做函数声明所以前向声明struct sysinfo;就够用了。( b )user/usys.pl—— 生成用户态→内核态跳板usys.pl是 Perl 脚本编译期为每一行entry(xxx)生成一小段汇编usys.S用ecall陷入内核、把系统调用号塞进a7。补上sysinfo即可/* * user/usys.pl */...entry(uptime);entry(trace);entry(sysinfo);// sysinfo 系统调用的内核态跳板( c )Makefile—— 把测试程序编进内核镜像UPROGS列出所有随内核一起编译的用户程序。缺了_sysinfotestsysinfotest命令就不存在。/* * Makefile */...ifeq($(LAB),syscall)UPROGS\ $U/_trace\ $U/_sysinfotest endif...kernel/syscall.h—— 分配系统调用号为每个系统调用分配一个唯一整数编号。我们把它定为23紧接已有的SYS_trace 22/* * kernel/syscall.h */// System call numbers#defineSYS_fork1#defineSYS_exit2#defineSYS_wait3#defineSYS_pipe4#defineSYS_read5#defineSYS_kill6#defineSYS_exec7#defineSYS_fstat8#defineSYS_chdir9#defineSYS_dup10#defineSYS_getpid11#defineSYS_sbrk12#defineSYS_sleep13#defineSYS_uptime14#defineSYS_open15#defineSYS_write16#defineSYS_mknod17#defineSYS_unlink18#defineSYS_link19#defineSYS_mkdir20#defineSYS_close21#defineSYS_trace22#defineSYS_sysinfo23kernel/syscall.c—— 声明并注册处理函数本次改动有三处① 顶部extern声明sys_sysinfo② 挂进syscalls[]分发数组下标 调用号③sysnames[]加上名字供打印。/* * kernel/syscall.c */...externuint64sys_chdir(void);...externuint64sys_trace(void);externuint64sys_sysinfo(void);staticuint64(*syscalls[])(void){[SYS_fork]sys_fork,...[SYS_trace]sys_trace,[SYS_sysinfo]sys_sysinfo,};staticconstchar*sysnames[]{[SYS_fork]fork,...[SYS_close]close,[SYS_trace]trace,[SYS_sysinfo]sysinfo,};kernel/sysproc.c—— 实现sys_sysinfo核心本次改动是顶部#include sysinfo.h和文件末尾的sys_sysinfo。注意它先在内核空间填好info再用argaddr(0, dstaddr)取出用户指针最后copyout把数据送回用户进程——这就是「前置知识」里说的跨地址空间拷贝/* * kernel/sysproc.c */#includetypes.h#includeriscv.h...#includesysinfo.h// 文件开头加上sysinfo结构体的头文件...uint64sys_sysinfo(void){structsysinfoinfo;freebytes(info.freemem);// 获取空闲内存的字节数量procnum(info.nproc);// 获取处于活动状态的进程数量// 获取用户虚拟地址uint64 dstaddr;argaddr(0,dstaddr);// 从内核空间拷贝数据到用户空间if(copyout(myproc()-pagetable,dstaddr,(char*)info,sizeofinfo)0)return-1;return0;}user/sysinfotest.c调用sysinfo(info)时传入的info就是这个dstaddr。内核把统计好的info通过copyout写进用户那块内存用户程序才能真正读到freemem/nproc。这一来一回正是sysinfo与仅在内核打印的trace最本质的区别。kernel/kalloc.c—— 实现freebytes统计空闲内存xv6 用空闲链表管理空闲物理页每个空闲页自身的前几个字节被复用成struct run节点r-next串起下一页kmem.freelist是链表头。遍历链表、每跳一步加一个PGSIZE就是空闲字节数。记得加kmem.lock因为kalloc/kfree会并发改这条链表不加锁会读到正在变化的指针/* * kernel/kalloc.c */...voidfreebytes(uint64*dst){*dst0;structrun*pkmem.freelist;acquire(kmem.lock);// 加锁保证线程安全while(p){*dstPGSIZE;// 统计空闲的字节数pp-next;}release(kmem.lock);}kernel/proc.c—— 实现procnum统计活动进程proc[NPROC]是全局进程表每个进程的state字段标明了它是否在使用。遍历这张表凡是state ! UNUSED的都算活动进程。下面贴的是proc.c完整文件本次只新增了文件末尾的procnum其余为既有代码/* * kernel/proc.c */...voidprocnum(uint64*dst){*dst0;structproc*p;for(pproc;pproc[NPROC];p){if(p-state!UNUSED){(*dst);}}}...该实现遍历进程表时没有加p-lock。在评测环境sysinfotest下单次调用、没有并发创建/销毁进程的竞争所以能稳定通过若在真实多核高并发场景下理论上有极小概率读到不一致的中间状态——生产代码通常要持锁或配合原子操作。面试时展示我清楚这里的权衡。kernel/defs.h—— 声明两个统计函数freebytes/procnum不是sys_开头的系统调用不参与syscalls[]分发所以只需在defs.h里做普通声明sysproc.c就能找到它们。/* * kernel/defs.h */...// kalloc.cvoid*kalloc(void);voidkfree(void*);voidkinit(void);voidfreebytes(uint64*dst);...// proc.cintcpuid(void);voidexit(int);intfork(void);intgrowproc(int);pagetable_tproc_pagetable(structproc*);voidproc_freepagetable(pagetable_t,uint64);intkill(int);structcpu*mycpu(void);structcpu*getmycpu(void);structproc*myproc();voidprocinit(void);voidscheduler(void)__attribute__((noreturn));voidsched(void);voidsetproc(structproc*);voidsleep(void*,structspinlock*);voiduserinit(void);intwait(uint64);voidwakeup(void*);voidyield(void);inteither_copyout(intuser_dst,uint64 dst,void*src,uint64 len);inteither_copyin(void*dst,intuser_src,uint64 src,uint64 len);voidprocdump(void);voidprocnum(uint64*dst);...验证回到 xv6 目录编译并运行syscall分支的评测指定只测sysinfo子项$ ./grade-lab-syscall sysinfoTest sysinfotestsysinfotest: OK(6.1s)只要返回sysinfotest: OK即代表freemem、nproc两项统计与跨地址空间拷贝都正确。复盘本实验解决了什么在系统调用统一分发框架内新增了sysinfo它能安全地把内核视角的两项全局状态空闲内存、活动进程数通过copyout跨越地址空间传递给用户进程。设计上的关键点是sys_sysinfo不参与打印而是采集 回拷——这正是它和同分支trace实验最本质的区别。与 trace 实验对比维度tracesysinfo数据去向内核printf直接打印copyout回拷到用户内存是否需要跨地址空间否是核心难点新增状态字段进程级trace_mask无读全局链表/表处理函数落点sysproc.csysproc.ckalloc.cproc.c收获内核如何安全暴露数据给用户必考copyout/copyin机制能讲清用户虚拟地址 ≠ 内核地址、必须逐页翻译越界检查会非常加分。空闲内存统计xv6 空闲链表是把空闲页自身当节点struct run的巧妙做法能口述遍历kmem.freelist、每节点 PGSIZE即可。进程表遍历proc[NPROC]state ! UNUSED计数顺带讲清进程状态机UNUSED→RUNNABLE→RUNNING→SLEEPING→ZOMBIE更佳。新增系统调用 6 步模板本实验 trace 实验合起来就是加系统调用的完整范本用户接口 → 分配号 → 处理函数 → 关联辅助函数 → 头文件声明 → 注册分发后续 labs 反复用到。
返回列表