6.S081——Lab2——system calls

0.Briefly Speaking

这是6.S081的第二个实验,目的主要是自己实现一些系统调用,上一个实验更多的是使用已有的设施去实现一些功能各异的程序,这需要我们对系统调用的过程有更加深入的理解。当然整个过程要全部了解,可能还需要做完Lab4:Traps Lab,在那个实验中将会具体探究xv6是如何借助陷阱来实现系统调用的。本实验的实验指导书在这里

因为系统调用的具体实现逻辑在内核内部,所以在实现一个系统调用时需要修改和增加一些东西,帮助内核能够正确索引到对应的系统调用,具体来说添加一个系统调用有以下文件需要修改:

1.首先,系统调用必须提供用户态下的调用接口,所以必须在user.h文件下添加用户态函数原型

2.在usys.pl中添加一个stub,我们在Makefile中检查一个这个文件,发现usys.pl会用来生成usys.S文件,这个文件专门用来生成汇编代码段来将对应的系统调用号读入a7寄存器

$U/usys.S : $U/usys.pl
	perl $U/usys.pl > $U/usys.S

3.接下来需要在kernel/syscall.h文件中加入一个新的系统调用号,内核需要借助这个调用号索引到对应的内核例程。还需要在kernel/syscall.c文件中的syscall中加入从系统调用号到函数的索引关系,如下所示:

static uint64 (*syscalls[])(void) = {
    
    
[SYS_fork]    sys_fork,
[SYS_exit]    sys_exit,
[SYS_wait]    sys_wait,
[SYS_pipe]    sys_pipe,
[SYS_read]    sys_read,
[SYS_kill]    sys_kill,
[SYS_exec]    sys_exec,
[SYS_fstat]   sys_fstat,
[SYS_chdir]   sys_chdir,
[SYS_dup]     sys_dup,
[SYS_getpid]  sys_getpid,
[SYS_sbrk]    sys_sbrk,
[SYS_sleep]   sys_sleep,
[SYS_uptime]  sys_uptime,
[SYS_open]    sys_open,
[SYS_write]   sys_write,
[SYS_mknod]   sys_mknod,
[SYS_unlink]  sys_unlink,
[SYS_link]    sys_link,
[SYS_mkdir]   sys_mkdir,
[SYS_close]   sys_close,
[SYS_trace]   sys_trace,	// 注意新加入的两个系统调用和对应关系
[SYS_sysinfo] sys_sysinfo,
};

4.最后在sysproc文件中实现对应的系统调用(sys_trace,sys_sysinfo等),完成具体的功能。
以上的4个步骤是添加一个系统调用所必需的步骤,在完成本实验时要注意检查。在下面的记录中,就不再一一将上面的步骤描述出来,而是聚焦于问题本身的实现思路。

1.System call tracing (moderate)

第一个实验是实现一个可以追踪任意系统调用的系统调用(看起来有点拗口…),当设置了追踪掩码mask之后,对应编号的系统调用在触发时都会被追踪。并打印出如下的信息:

进程ID号:syscall xxx -> 返回值

实验书中指明,xv6已经实现了一个文件trace.c,这个文件和我们在上一个实验中实现的用户态程序是一样的,trace.c会调用trace系统调用来跟踪后面具体执行的命令行,所以这里也需要将_trace加入UPROGS字段,否则找不到这个应用程序。下面来具体实现一下。

其实指导书中暗示的都差不多了,我们就一步步照指示做事就好:
1.首先在表示每个进程的结构体proc(kernel/proc.h)中加入一个表示用于进程追踪的掩码,见下面的结构体尾部

struct proc {
    
    
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int 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 stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
  int TraceMask;               // <在这里加入一个新的属性,用于表示要追踪的进程掩码>
};

2.然后就是在sysproc.c的文件中真正地实现sys_trace的逻辑,这里涉及到用户态向内核态传递参数,这个过程中需要使用一些函数(argint, argaddr,argstr等)从trapframe中将用户态下传递的参数读取到内核态。所以在内核态下的系统调用函数,都是没有参数的,因为它们的参数都是从用户态下读进来的。上述的这些函数的关系如下,本质上都调用了argraw来解析参数
在这里插入图片描述
argraw的逻辑非常简单,就是从对应的trapframe中返回a0-a7寄存器,因为根据RISC-V的calling convention,头几个寄存器是用来存放函数参数的。

// 以64位无符号整数返回寄存器a0-a7中的值
// 当n大于5的时候,函数错误,陷入panic
static uint64
argraw(int n)
{
    
    
  struct proc *p = myproc();
  switch (n) {
    
    
  case 0:
    return p->trapframe->a0;
  case 1:
    return p->trapframe->a1;
  case 2:
    return p->trapframe->a2;
  case 3:
    return p->trapframe->a3;
  case 4:
    return p->trapframe->a4;
  case 5:
    return p->trapframe->a5;
  }
  panic("argraw");
  return -1;
}

至于用户态下的参数如何被放到trapframe里,发生系统调用后又如何从用户态过渡到内核态的,这个过程后面做到对应实验时再分析源码。sys_trace的实现非常简单,直接将用户态下传进来的追踪掩码存入当前进程的TraceMask(之前刚加入的变量属性)即可。

uint64
sys_trace(void)
{
    
    
  int n;
  if(argint(0, &n) < 0)             // 将trapframe->a0读入
    return -1;
  myproc()->TraceMask = n;          // set the TraceMask in proc struct
  return 0; 
}

3.因为trace系统调用还要能够追踪当前进程的子进程,所以在进程调用fork时,子进程应该将TraceMask一并复制过去,加入的代码我已经在下面用中文标出了。

int
fork(void)
{
    
    
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // Allocate process.
  if((np = allocproc()) == 0){
    
    
    return -1;
  }

  // Copy user memory from parent to child.
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    
    
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;

  // <在这加一行代码,将TraceMask复制到子进程中去>
  np->TraceMask = p->TraceMask;

  // copy saved user registers.
  *(np->trapframe) = *(p->trapframe);

  // Cause fork to return 0 in the child.
  np->trapframe->a0 = 0;

  // increment reference counts on open file descriptors.
  for(i = 0; i < NOFILE; i++)
    if(p->ofile[i])
      np->ofile[i] = filedup(p->ofile[i]);
  np->cwd = idup(p->cwd);

  safestrcpy(np->name, p->name, sizeof(p->name));

  pid = np->pid;

  release(&np->lock);

  acquire(&wait_lock);
  np->parent = p;
  release(&wait_lock);

  acquire(&np->lock);
  np->state = RUNNABLE;
  release(&np->lock);

  return pid;
}

4.最后实现具体的追踪功能,因为追踪时需要打印出具体的系统调用名称,所以在syscall.h中还需要加一个系统调用号到名称的映射表

static char* SyscallName[] = {
    
    
[SYS_fork]    "fork",
[SYS_exit]    "exit",
[SYS_wait]    "wait",
[SYS_pipe]    "pipe",
[SYS_read]    "read",
[SYS_kill]    "kill",
[SYS_exec]    "exec",
[SYS_fstat]   "fstat",
[SYS_chdir]   "chdir",
[SYS_dup]     "dup",
[SYS_getpid]  "getpid",
[SYS_sbrk]    "sbrk",
[SYS_sleep]   "sleep",
[SYS_uptime]  "uptime",
[SYS_open]    "open",
[SYS_write]   "write",
[SYS_mknod]   "mknod",
[SYS_unlink]  "unlink",
[SYS_link]    "link",
[SYS_mkdir]   "mkdir",
[SYS_close]   "close",
[SYS_trace]   "trace",
[SYS_sysinfo] "sysinfo",
};

然后就是修改执行系统调用的具体函数syscall:

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]();
    // 如果当前系统调用需要被追踪,则打印出对应的调用信息
    // 用&运算判断对应的位是否同为1
    if(p->TraceMask & (1 << num)){
    
                                                           
      printf("%d: syscall %s -> %d\n", p->pid, SyscallName[num], p->trapframe->a0);    
    }
  } else {
    
    
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

到此为止,以上4步就已经很好地完成了这个任务,验证一下实现是否正确
在这里插入图片描述
这就是Lab2第一个小任务的具体实现思路。

2.Sysinfo (moderate)

这个任务是实现一个系统调用统计一些系统信息,包括:空闲内存的数量和不处于UNUSED状态的进程数量。和trace一样的,这里提供了一个用户态下的应用程序user/sysinfotest.c用来检测我们编写的系统调用的正确性。可以阅读一下测试程序的逻辑,学习一下,这里不再展开了。

还是要强调一下,不要忘记实现一个系统调用的必经4步骤,这些在最上面已经列出了,这里不再展开赘述。sysinfo暴露在用户态下的编程接口原型如下,需要在user.h中加入这个接口

int sysinfo(struct sysinfo *);

struct sysinfo是一个结构体,它定义在kernel/sysinfo.h中,定义如下:

// sysinfo结构体中定义了空闲内存和不处于UNUSED态进程的数量
struct sysinfo {
    
    
  uint64 freemem;   // amount of free memory (bytes)
  uint64 nproc;     // number of process
};

下面直接进入到系统调用的实现,首先给出sysinfo系统调用整体的实现思路:

uint64
sys_sysinfo(void)
{
    
    
  uint64 UserSysinfo;
  struct sysinfo KernelSysinfo;
  if(argaddr(0, &UserSysinfo) < 0)
    return -1;
  KernelSysinfo.freemem = kcountFreeMemory();    // 统计空闲内存的量
  KernelSysinfo.nproc = countProc();             // 统计状态不是UNUSED的进程数量

  /* 将sysinfo结构体拷贝回用户态下 */
  struct proc *p = myproc(); 
  if(copyout(p->pagetable, UserSysinfo, (char*)&KernelSysinfo, sizeof(struct sysinfo)) < 0)
    return -1;
  return 0;
}

首先通过argaddr系统调用,将用户传进来的指向结构体的指针读入变量UserSysinfo,记录这个地址是因为后面还要使用copyout函数将此变量从内核态拷贝回用户态。其中分别调用了两个函数分别用来计算空闲内存的数量和进程数量,下面来具体看看这两个函数的实现:

1.kcountFreeMemory函数
在实现这个函数之前,需要先阅读其他内存管理函数的实现,大致弄清xv6中物理内存的管理机制
首先来看内存管理需要的两个数据结构,run和kmem。稍微有些数据结构基本功的人就可以看出这就是一个很简单的链表,事实上,xv6就是将空闲内存块串成了一个链表来管理,如下所示:
在这里插入图片描述

// 一个链表,这个链表每个结点都指向了一个空闲内存块的开头地址
struct run {
    
    
  struct run *next;
};

// 一把自旋锁,防止并发时的访问冲突
// 空闲内存块组成的列表
struct {
    
    
  struct spinlock lock;
  struct run *freelist;
} kmem;

接下来主要阅读一下kmalloc和kfree函数的实现:

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
    
    
  // 声明一个run类型的指针r
  struct run *r;

  // 在从空闲链表中获取空闲内存块时,首先加一把自旋锁
  acquire(&kmem.lock);
  r = kmem.freelist;
  // 如果r不为空,则空闲链表还没有到达结尾
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);
  
  // 随意填一些数据将当前页面填满
  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  
  // 返回新申请的页面
  return (void*)r;
}

kmalloc的实现思路非常简单,就是从一个空闲链表中取一块空闲内存,稍作初始化之后返回。
接下来看看kfree函数,也非常简单,就是将空闲页面使用“头插法”插入回空闲列表

// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
// 此函数一次释放一整个页面(4096bytes)大小的内存
void
kfree(void *pa)
{
    
    
  struct run *r;
  // 如果pa不能被页面大小整除,或范围超出合法边界,则陷入panic
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  // 随便填一些数字将准备收回的页面填充
  memset(pa, 1, PGSIZE);
  
  // 接下来这段代码就是使用“头插法”来将空闲节点插回链表
  r = (struct run*)pa;

  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

看懂了基本的物理内存管理方式,下面就可以看看kcountFreeMemory的实现,基本思路就是遍历freelist链表直至结尾即可,直接贴出代码:

uint64
kcountFreeMemory()
{
    
    
  struct run* r;
  uint64 FreeMemory = 0;
  acquire(&kmem.lock);
  for(r = kmem.freelist ; r ; r = r->next)
    FreeMemory += PGSIZE;
  release(&kmem.lock);

  return FreeMemory;
}

2.countProc函数
这个函数最重要的就是在kernel/proc.c文件中,学习如何遍历当前进程列表。事实上,在同一文件下的其他函数中,经常能看到类似于以下代码的实现:

for(p = proc; p < &proc[NPROC]; p++) {
    
    
	...
}

这里的proc[NPROC]就是xv6的进程组,以上代码就完成了所有进程的遍历。据此,我们可以写出如下代码来统计进程组中正在被使用的进程,如下所示:

uint64
countProc()
{
    
    
  struct proc* p;
  uint64 NumberOfProcess = 0;
  for(p = proc ; p < &proc[NPROC] ; p++){
    
    
    acquire(&p->lock);
    if(p->state != UNUSED)
      NumberOfProcess++;
    release(&p->lock);
  }
  return NumberOfProcess;
}

至此我们就已经完整实现了sysinfo的所有功能,在此测试一下:
在这里插入图片描述
表明上述实现是正确的。

3.小结

本实验的目的是在xv6中实现系统调用,这个过程中涉及到一些xv6中系统调用的基本机制,如系统调用号、如何从用户态向内核传参数、如何将结果从内核空间再传回用户空间(copyout)等。但要更加深入的理解系统调用的全过程,还需要在后面继续完成实验和研究内核源码

猜你喜欢

转载自blog.csdn.net/zzy980511/article/details/129752061