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

📅 2026/8/4 5:45:09 👁️ 阅读次数 📝 编程学习
MIT 6.S081 syscall 实验篇(lab2):Sysinfo (moderate)

Sysinfo (moderate)


实验目标

本实验要新增一个系统调用sysinfo,让用户程序能查询两项系统状态:

教材已经在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的难点不在"怎么算",而在"怎么把结果给用户":

所以标准做法是:

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的都算"活动"

实现思路

顺着"系统调用执行顺序",需要改动的地方是一条连贯的链:

  1. 用户态要能调到sysinfouser/user.h声明(含struct sysinfo前向声明)+user/usys.pl生成跳板 +Makefile_sysinfotest编进镜像。
  2. 内核要认得这个新系统调用kernel/syscall.h分配号SYS_sysinfo = 23
  3. sysinfo的内核实现kernel/sysproc.c里写sys_sysinfo():填info、取用户指针、copyout回用户。
  4. 两个统计函数freebytes()kalloc.c(离空闲链表最近),procnum()proc.c(离进程表最近)。
  5. sys_sysinfo找到这两个函数→ 在kernel/defs.h补上extern风格的声明(注意它们不是sys_开头,所以不参与syscalls[]分发,只需普通声明)。
  6. sys_sysinfo接进分发数组kernel/syscall.cextern+ 挂进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_sysinfo23

kernel/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 */#include"types.h"#include"riscv.h"...#include"sysinfo.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){*dst=0;structrun*p=kmem.freelist;acquire(&kmem.lock);// 加锁保证线程安全while(p){*dst+=PGSIZE;// 统计空闲的字节数p=p->next;}release(&kmem.lock);}

kernel/proc.c—— 实现procnum(统计活动进程)

proc[NPROC]是全局进程表,每个进程的state字段标明了它是否在使用。遍历这张表,凡是state != UNUSED的都算"活动进程"。下面贴的是proc.c完整文件,本次只新增了文件末尾的procnum,其余为既有代码:

/* * kernel/proc.c */...voidprocnum(uint64*dst){*dst=0;structproc*p;for(p=proc;p<&proc[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 sysinfo
==Test sysinfotest==sysinfotest: OK(6.1s)

只要返回sysinfotest: OK,即代表freememnproc两项统计与跨地址空间拷贝都正确。


复盘

本实验解决了什么

在系统调用统一分发框架内新增了sysinfo,它能安全地把内核视角的两项全局状态(空闲内存、活动进程数)通过copyout跨越地址空间传递给用户进程。设计上的关键点是:sys_sysinfo不参与"打印",而是"采集 + 回拷"——这正是它和同分支trace实验最本质的区别。

与 trace 实验对比

维度tracesysinfo
数据去向内核printf直接打印copyout回拷到用户内存
是否需要跨地址空间(核心难点)
新增状态字段进程级trace_mask无(读全局链表/表)
处理函数落点sysproc.csysproc.c+kalloc.c+proc.c

收获