MIT6.S081 - Lab2: system calls

Lab2: system calls

预备知识

执行一次系统调用的流程:

USER MODE

step1:系统调用声明

  • user/user.h:系统调用函数(如 int fork(void))

step2:ecall 进入内核态

  • user/usys.S(该文件由 user/usys.pl 生成,后续添加函数可以在这里添加):执行如下命令
 .global fork
 fork:
   li a7, SYS_fork
   ecall
   ret
  • 将系统调用的编号(在kernel/syscall.h中定义)写入a7寄存器
  • ecall进入中断处理函数

KERNEL MODE

step3:保存数据并跳转到中断判断函数

  • kernel/trampoline.S:在uservec中保存寄存器、切换到内核栈、切换栈指针SP等,最后跳转到内核指定的中断判断函数usertrap(在kernel/trap.c中)
    • 每一个进程都对应一个状态结构体proc(在kernel/proc.h中定义这个结构体),将数据存储在这里

step4:中断判断函数

  • kernel/trap.c:中断判断函数usertrap用于处理来自用户态的中断、异常或系统调用,在此处判断是否是系统调用,如果是,则执行响应函数syscall

step5:执行对应的系统调用

  • kernel/syscall.c:响应函数syscall用于对号入座,根据给出的syscalls表(将系统调用编号与执行函数相对应)获取系统调用类型(系统调用编号从a7寄存器中读取),执行相应的函数(进入kernel/sysproc.c中);系统调用的返回值传递给了a0,后续会回传给用户态

step6:系统调用函数

  • kernel/sysproc.c:保存着系统调用的执行函数,如果系统调用有参数,需要用argraw(读取参数本质上是从内存中恢复,见kernel/syscall.c)取出保存的寄存器数据,然后函数最终都将调用相对应的执行函数(在kernel/proc.c中)

step7:系统调用核心功能

  • kernel/proc.c:这里的函数用于执行核心功能,如创建进程等

添加系统调用需要考虑的地方

  1. user/user.h中添加系统调用的函数声明,并在user中创建相应的系统调用(*.c文件)(和lab1类似)

  2. user/usys.pl中添加系统调用项

  3. kernel/syscall.h添加系统调用的编号

  4. kernel/syscall.c中的syscalls表中添加映射关系,指出需要执行的函数

  5. kernel/sysproc.ckernel/proc.c中实现系统调用的执行函数

Part1:System call tracing

实现功能

  1. 添加一个系统调用的trace功能,在命令前输入trace <mask>,能够打印出该命令使用的系统调用

  2. mask = 1 << SYS_name为一个整数,它能够指定跟踪哪个系统调用,SYS_name是来自kernel/syscall.h的一个系统调用号,mask可以等于1 << SYS_name | 1 << SYS_other_name

  3. 如果在输入命令时使用trace <mask>,则在使用指定系统调用后应该返回一行包括进程id、系统调用名称和返回值的打印信息

  4. trace要求能够跟踪进程以及进程派生出的子进程,且不影响其他进程

实验提示

  1. user/user.huser/usys.plkernel/syscall.h添加系统调用以及编号

  2. kernel/sysproc.c添加一个sys_trace函数实现新的系统调用,并在状态结构体proc中插入一个新的变量(对该进程进行跟踪的mask掩码),系统调用的执行函数参考kernel/sysproc.c

  3. 此外需要对kernel/proc.cfork函数进行修改,因为调用了trace系统调用,会在当前进程状态proc(在kernel/proc.h中)中修改mask掩码的设置,同时每次fork时也需要对相关的子进程同步相关的设置

  4. 需要修改kernel/syscall.c下的syscall使其打印追踪信息;为了打印系统调用名称,需要额外创建字符串数组

实验代码

  1. user/user.h中添加int trace(int);的声明(trace接受一个整型的参数mask

  2. user/usys.pl中添加entry("trace");,这是进入内核态的入口

  3. Makefile中的UPROGS添加_trace

  4. 进入内核态,在kernel/syscall.h添加系统调用的编号#define SYS_trace 22

  5. kernel/syscall.c中添加系统调用的映射extern uint64 sys_trace(void);[SYS_trace] sys_trace

  6. kernel/proc.h中的proc中添加int mask

  7. kernel/sysproc.c中添加进入系统调用的函数

uint64
sys_trace(void){   // 参考sys_wait函数
  uint64 p;
  if(argaddr(0, &p) < 0)   // 获取trace的参数(只有一个)
    return -1;    
  return trace(p);  // 系统调用的执行函数
}
  1. kernel/proc.c 实现 trace 的系统调用执行函数(仅仅是进行一个掩码的赋值)
int 
trace(int mask){
  struct proc *p = myproc();
  p->mask = mask;     // 将掩码赋值给结构体的mask 
  return 0;
}
  1. kernel/defs.h(这个里面包含了 kernel 中常用函数的原型声明)中加入函数的声明 int trace(int)
  2. kernel/proc.c 中的 fork 函数中加一行代码 np->mask = p->mask;,将父进程的 mask 赋值给子进程的 mask
  3. kernel/syscall.c 中对 syscall 进行修改,打印追踪信息;并且为了打印系统调用的名称,创建一个字符串数组
char 
*sys_name[] = {
"",      "fork",  "exit",   "wait",   "pipe",  "read",  "kill",   "exec",
"fstat", "chdir", "dup",    "getpid", "sbrk",  "sleep", "uptime", "open",
"write", "mknod", "unlink", "link",   "mkdir", "close", "trace"
};   

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();  // 返回值
    if(p->mask >> num & 1){   // 判断是否有mask输入
      // 打印进程id、系统调用的名称和返回值
      printf("%d: syscall %s -> %d", p->pid, sys_name[num], p->trapframe->a0);   // 这里注意使用的prinrf是因为xv6的kernel中专门定义了一个printf的函数,在Linux内核中只能使用prinrk打印
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

p->mask >> num & 1:p->mask 为 1<<SYS_read,num 为从 a7 寄存器中读出来的 SYS_read,p->mask >> num 能够将 mask 还原为 SYS_read,如果没有 mask,则不打印

实验感悟

  1. 在阅读源码的过程中,看到有些地方涉及到汇编语言,之后要把这块给学习一下,要不然很影响阅读体验
  2. 虽然按照系统调用的步骤能把整个流程走下来,但是每个函数以及它们之间的关系实际上还没有特别清楚,需要再进行梳理(将每个函数的作用都搞清楚)
  3. 在进行 proc 修改的时候,要考虑到 fork 函数的子进程是否需要复制参数

Part2: Sysinfo

实现功能

  1. 添加一个系统调用 sysinfo,通过系统调用将收集正在运行的程序的信息
  2. 在这个系统调用中将传入一个结构体指针 sysinfo,结构体定义在 kernel/sysinfo.h 中,其中 freemem 应该设置为空闲内存的字节数,nproc 应该设置为进程状态不为 UNUSED 的进程数

实验提示

  1. user/user.h 中声明 sysinfo() 的原型,你需要预先声明结构体 sysinfo 的存在:struct sysinfo;int sysinfo(struct sysinfo *);
  2. sysinfo 需要将 struct sysinfo 复制回用户空间;参考 sys_fstat() (kernel/sysfile.c)filestat() (kernel/file.c) 学习使用 copyout()
  3. 要收集空闲内存量,可以在 kernel/kalloc.c 中添加一个函数
  4. 要收集进程数,请在 kernel/proc.c 中添加一个函数

实验代码

  1. user/user.h 添加 sysinfo 结构体和函数的声明 struct sysinfo;int sysinfo(struct sysinfo *);
  2. 分别在 user/usys.plMakefilekernel/syscall.hkernel/syscall.c 执行与上一个实验一样的步骤
  3. kernel/kalloc.c 中实现空闲内存大小的查找
uint64 
getfreemen(void){    // 获取空闲内存数量
  struct run *rp;
  uint64 result = 0;
  acquire(&kmem.lock);  // 考虑到并发问题,上锁
  rp = kmem.freelist;
  while(rp){
    result += 1;
    rp = rp->next;
  }
  release(&kmem.lock);
  return result * PGSIZE;   //一个内存页的大小为PGSIZE(4096)
}
  1. kernel/proc.c 中实现进程数的统计
int 
getproc(void){
  struct proc *p;
  int result = 0;
  for(p = proc; p < &proc[NPROC]; p++){
    acquire(&p->lock);   //上锁
    if(p->state != UNUSED){
      result += 1;
    }
    release(&p->lock);
  }
  return result;
}

上述两个步骤都需要在 kernel/defs.h 中添加函数声明

  1. kernel/sysproc.c 中实现执行函数 sys_sysinfo 的功能,记得添加 #include "sysinfo.h" 来使用 sysinfo 结构体
uint64   
sys_sysinfo(void){   
  struct sysinfo info;
  struct proc *p;
  uint64 addr;
  if(argaddr(0, &addr) < 0)    // 获取系统的指针参数
    return -1;
  p = myproc();
  info.freemem = getfreemen();
  info.nproc = getproc();
  // 从内核copy到用户
  if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)  //参考sys_fstat(在kernel/sysfile.c中)
    return -1;                
  return 0;
}

实验感悟

  1. 这个实验实现的内存大小和进程数的统计函数需要了解了 kalloc.c 和 proc.c 的函数后才能写出来,我两个文件看的还不太完全,之后需要再看看
  2. 这个实验中也使用了指针,这一块使用还不太熟练,C 语言还需要精进
posted @ 2024-04-23 10:37  jll133688  阅读(63)  评论(0编辑  收藏  举报