XV6:增加系统调用


为XV6增加系统调用

XV6:增加系统调用

本文介绍了XV6操作系统(Unix Version 6)中系统调用的工作流程,此外也介绍了如何为XV6操作系统添加函数调用

1. 目标

稍后我们会讲解XV6中的系统调用的工作原理,包括如何从用户态跳转到内核态,系统调用功能号等等。

但是光听不练假把式,所以为了更好的能够去理解XV6中系统调用的工作原理,我们还要实践一下,也就是说为XV6增加新的系统调用。

本文中我们需要增加两个系统调用,分别是trace(const char *pathname)getcount()

这两个系统调用的功能是,trace接受一个文件路径,此后系统开始追踪该文件,被追踪的文件每被打开一次,那么计数器就会加一,而getcount将会返回该文件被打开的次数。

两个系统调用的函数原型如下:

int trace(const char *pathname)
int getcount(void)

接下来我们将首先介绍XV6中系统调用的原理,而后再增加这两个系统调用

2. 系统调用全流程

前面我们介绍了XV6项目的Makefile,我们从Makefilekernelfs.img这两个目标中就能够看出来,操作系统本身就是一个程序,而用户写的程序和操作系统这个大程序在地位上是相等的,大家都有main函数。

例如,内核的main函数如下

// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.

int
main(void)
{
  kinit1(end, P2V(4*1024*1024)); // phys page allocator
  kvmalloc();      // kernel page table
  mpinit();        // detect other processors
  lapicinit();     // interrupt controller
  seginit();       // segment descriptors
  picinit();       // disable pic
  ioapicinit();    // another interrupt controller
  consoleinit();   // console hardware
  uartinit();      // serial port
  pinit();         // process table
  tvinit();        // trap vectors      // == 中断向量表
  binit();         // buffer cache
  fileinit();      // file table
  ideinit();       // disk 
  startothers();   // start other processors
  kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
  userinit();      // first user process        // == 开启第一个用户进程
  mpmain();        // finish this processor's setup
}

而用户编写的ls这个程序中的main函数如下

int
main(int argc, char *argv[])
{
  int i;

  if(argc < 2){
    ls(".");
    exit();
  }
  for(i=1; i<argc; i++)
    ls(argv[i]);
  exit();
}

因此,操作系统和用户程序都是相互独立的、完整的程序。

那么就存在一个问题,系统调用究竟是如何工作的呢?

A. 不同视角下的系统调用

在讲解系统调用的工作原理之前,我们先分别从用户程序(程序员)操作系统(内核开发者)的角度分别来看看系统调用的样子

User Program’s (Developer’s) Perspective

我们首先从用户程序的视角来看看系统调用的样子。

test_trace.c是我在已经写好了需要新增加的两个系统调用之后为了测试写的一个用户程序。

// User program test_trace.c

#include "types.h"
#include "stat.h"
#include "user.h"
#include "fcntl.h"


int main(int argc, char *argv[])
{
  int stdout = 1;

  printf(stdout, "Start my syscall testing...\n");

  trace("ls");
  trace("pwd");
  trace("test");
  trace("666");


  printf(stdout, "ls count = %d\n", getcount("ls"));
  open("ls", O_RDONLY);
  printf(stdout, "ls count = %d\n", getcount("ls"));
  open("ls", O_RDONLY);
  printf(stdout, "ls count = %d\n", getcount("ls"));
  open("ls", O_RDONLY);
  printf(stdout, "ls count = %d\n", getcount("ls"));


  open("pwd", O_RDONLY);
  printf(stdout, "pwd count = %d\n", getcount("pwd"));

  open("test", O_RDONLY);
  printf(stdout, "test count = %d\n", getcount("test"));

  open("pwd", O_RDONLY);
  printf(stdout, "pwd count = %d\n", getcount("pwd"));

  open("666", O_RDONLY);
  printf(stdout, "666 count = %d\n", getcount("666"));

  printf(stdout, "test finished\n");

  exit();

}

从上面的代码中其实就能够看出来,tracegetcount虽然说是两个系统调用,可是在用户程序的视角看来,系统调用的外貌和函数没有什么两样。故

From the perspective of user program, system call are just like function calls

虽然在外形、使用上系统调用和和我们自己定义的函数差不多,但是我们需要知道,我们调用自己写的函数的时候,所有的功能其实都是我们自己写的。但是对于系统调用来说,功能都是由操作系统提供的,即:

By using these special function (System Call), the user program can leverage functionalities offered by the kernel

Kernel’s (Operating System’s) Perspective

我们上面讲解了我们程序员在写代码时候所看到的系统调用的样子,接下来看看操作系统眼中的系统调用是什么样子。

我们以kill这个系统调用为例进行讲解。上面说到,系统调用的功能由操作系统提供,因此kill的函数定义在内核的源代码proc.c

// proc.c

// Kill the process with the given pid.
// Process won't exit until it returns
// to user space (see trap in trap.c).
int
kill(int pid)
{
  struct proc *p;

  acquire(&ptable.lock);
  for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
    if(p->pid == pid){
      p->killed = 1;
      // Wake process from sleep if necessary.
      if(p->state == SLEEPING)
        p->state = RUNNABLE;
      release(&ptable.lock);
      return 0;
    }
  }
  release(&ptable.lock);
  return -1;
}

所以,其实站在内核的角度,所谓的系统调用只不过是内核程序中定义的函数而已,即:

So, from the perspective of kernel, system call are just normal functions defined in kernel.

可是,这样就存在一个很关键的问题:我们在上面说过,操作系统和用户程序都是独立的程序。而我们在编译的时候是分别编译的用户程序和内核,我们并没有将两者链接到一起。但是我们却能够在用户程序中,调用在操作系统这个独立的程序中定义的函数。

C. 从用户程序到内核

为了解决上面讲的我们,我们接下来讲解从用户程序到内核的全过程。

1) usys.S

System call的第一步就是usys.S这个程序。

usys.S是一个AT&T格式的汇编程序,其中主要定义了一个宏函数SYSCALL

#include "syscall.h"
#include "traps.h"

#define SYSCALL(name) \
  .globl name; \
  name: \
    movl $SYS_ ## name, %eax; \
    int $T_SYSCALL; \
    ret

SYSCALL(fork)
SYSCALL(exit)
SYSCALL(wait)
SYSCALL(pipe)
SYSCALL(read)
SYSCALL(write)
SYSCALL(close)
SYSCALL(kill)
SYSCALL(exec)
SYSCALL(open)
SYSCALL(mknod)
SYSCALL(unlink)
SYSCALL(fstat)
SYSCALL(link)
SYSCALL(mkdir)
SYSCALL(chdir)
SYSCALL(dup)
SYSCALL(getpid)
SYSCALL(sbrk)
SYSCALL(sleep)
SYSCALL(uptime)

汇编中##是字符拼接操作符, 所以如果我们把fork作为SYSCALL的参数传入的话,宏函数的内容就会像下面这样

; SYSCALL(fork) will expand as

#define SYSCALL(fork) \
  .globl fork; \
  fork: \
    movl $SYS_fork, %eax; \
    int $T_SYSCALL; \
    ret

$SYS_call这个符号变量定义在 syscall.h

// syscall.h

// System call numbers
#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
#define SYS_pipe    4
#define SYS_read    5
#define SYS_kill    6
#define SYS_exec    7
#define SYS_fstat   8
#define SYS_chdir   9
#define SYS_dup    10
#define SYS_getpid 11
#define SYS_sbrk   12
#define SYS_sleep  13
#define SYS_uptime 14
#define SYS_open   15
#define SYS_write  16
#define SYS_mknod  17
#define SYS_unlink 18
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21

而符号变量T_SYSCALL的值是64,定义在traps.h

// These are arbitrarily chosen, but with care not to overlap
// processor defined exceptions or interrupt vectors.
#define T_SYSCALL       64      // system call
#define T_DEFAULT      500      // catchall

总的来说,宏函数SYSCALL干的是就是把系统调用号(System Call Number)存到eax寄存器中,而后使用软中断指令int调用64号中断。

In summary, the macro function SYSCALL defines a assembly function that store the system call number into eax register and then call #64 interrput.

And usys.S simply defines assembly functions for all system call.

因此,usys.S这个程序中其实就是为每个系统调用定义了一个汇编函数,汇编函数的作用就是把系统调用号放入eax

2) vector.S

系统调用的下一步就是在vector.S中。vector.S中定义了所有的中断处理函数。

SYSCALL宏的最后跳转到了64号中断,64号中断的定义如下

.globl vector64
vector64:
  pushl $0
  pushl $64
  jmp alltraps

AT&T格式的汇编中,立即数前要加$,因此64号中断的内容就是先把0压入栈,然后再把64压入栈。最后再跳转到alltraps函数中

3) trapasm.S

alltraps是一个汇编函数,定义在trapasm.S中。alltraps函数的定义如下

.globl alltraps
alltraps:
  # Build trap frame.
  pushl %ds
  pushl %es
  pushl %fs
  pushl %gs
  pushal

  # Set up data segments.
  movw $(SEG_KDATA<<3), %ax
  movw %ax, %ds
  movw %ax, %es

  # Call trap(tf), where tf=%esp
  pushl %esp
  call trap
  addl $4, %esp

alltraps函数干了三件事情:

  • 首先把dsesfsgs等段寄存器的值压入栈中,然后再把通用寄存器的值压入栈中
  • 重新设置数据为内核数据段
  • 调用trap函数

4) trap.c

trap是一个C语言函数,定义在trap.c中。

这里我们首先需要关注的是:

trap函数接受一个struct trapframe *类型的参数tf

C语言间相互调用函数时候传递参数是非常简单的,例如我们在my_trap_call_test.c文件中想要调用trap函数的话,直接以trap(mytf)这样的形式调用即可。

可问题是,trap这个C语言函数是被trapasm.S汇编程序中alltraps汇编函数调用的。所以关键问题就在于:汇编语言调用C语言函数的时候,该如何传参?

//PAGEBREAK: 41
void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_SYSCALL){
    if(myproc()->killed)
      exit();
    myproc()->tf = tf;
    syscall();
    if(myproc()->killed)
      exit();
    return;
  }

  switch(tf->trapno){
  case T_IRQ0 + IRQ_TIMER:
    if(cpuid() == 0){
      acquire(&tickslock);
      ticks++;
      wakeup(&ticks);
      release(&tickslock);
    }
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_IDE:
    ideintr();
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_IDE+1:
    // Bochs generates spurious IDE1 interrupts.
    break;
  case T_IRQ0 + IRQ_KBD:
    kbdintr();
    lapiceoi();
    break;
  case T_IRQ0 + IRQ_COM1:
    uartintr();
    lapiceoi();
    break;
  case T_IRQ0 + 7:
  case T_IRQ0 + IRQ_SPURIOUS:
    cprintf("cpu%d: spurious interrupt at %x:%x\n",
            cpuid(), tf->cs, tf->eip);
    lapiceoi();
    break;

  //PAGEBREAK: 13
  default:
    if(myproc() == 0 || (tf->cs&3) == 0){
      // In kernel, it must be our mistake.
      cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n",
              tf->trapno, cpuid(), tf->eip, rcr2());
      panic("trap");
    }
    // In user space, assume process misbehaved.
    cprintf("pid %d %s: trap %d err %d on cpu %d "
            "eip 0x%x addr 0x%x--kill proc\n",
            myproc()->pid, myproc()->name, tf->trapno,
            tf->err, cpuid(), tf->eip, rcr2());
    myproc()->killed = 1;
  }

解决汇编传参给C语言函数的关键就是使用栈,即:

  • 当我们在C语言间调用函数的时候,编译器会自动帮我们把主调函数中的参数值压入栈中
  • 而函数调用本质上是通过jmp指令实现的,因此spss这两个保存了栈顶和栈底的寄存器的值是没有改变的
  • 因此,在被调函数中通过出栈就可以获得主调函数中传递的参数值

通过栈这种方式,C语言实现了主调函数为被调函数中的形参赋值。而编译器则帮助我们完成了压栈和出栈的过程。

因此,现在我们在汇编中调用C语言函数的话,我们需要自己把参数全部压入到栈中。由于压栈和出栈的顺序是反过来的,所以我们需要去看一下trapframe这个结构体的内容。

trapframe的定义和alltraps压栈的顺序如下:

// definition of trapframe in x86.h

//PAGEBREAK: 36
// Layout of the trap frame built on the stack by the
// hardware and by trapasm.S, and passed to trap().
struct trapframe {
  // registers as pushed by pusha
  uint edi;
  uint esi;
  uint ebp;
  uint oesp;      // useless & ignored
  uint ebx;
  uint edx;
  uint ecx;
  uint eax;

  // rest of trap frame
  ushort gs;
  ushort padding1;
  ushort fs;
  ushort padding2;
  ushort es;
  ushort padding3;
  ushort ds;
  ushort padding4;
  uint trapno;

  // below here defined by x86 hardware
  uint err;
  uint eip;
  ushort cs;
  ushort padding5;
  uint eflags;

  // below here only when crossing rings, such as from user to kernel
  uint esp;
  ushort ss;
  ushort padding6;
};


// push in alltraps
.globl alltraps
alltraps:
  # Build trap frame.
  pushl %ds
  pushl %es
  pushl %fs
  pushl %gs
  pushal

  # Set up data segments.
  movw $(SEG_KDATA<<3), %ax
  movw %ax, %ds
  movw %ax, %es

  # Call trap(tf), where tf=%esp
  pushl %esp
  call trap
  addl $4, %esp

我们可以看到对应如下:

  • alltraps压栈的内容和trapframe中25行以前的内容一一对应。
  • 26行的trapno是在vector.S中压栈的,其值就是中断号的值,即64号中断

最关键的是,目前我们的程序已经运行到了trap.c中,而trap.c是内核程序的一部分,换而言之,我们此刻正在执行内核代码,即:

  1. 我们已经进入了内核态
  2. 我们从用户态跳转到了内核态

因此,操作系统使用三个汇编语言函数来实现从用户程序跳转到内核程序中,从而使得程序A中运行定义在程序B中的函数称为可能。而实现跳转的关键,就是通过中断

话说回来,上面我们说到,运行到trap函数其实就已经在执行内核代码。

我们精简一下trap函数,主要看看其中的逻辑。很明显,就是根据trapno来判断要执行那些动作:

  • 时钟中断发生了之后,调用操作系统的Scheduler来调度下一个程序
  • IO中断发生了之后,调用驱动和硬件交互
  • 处理系统调用中断

因此trap函数里面的逻辑主要就是根据不同的中断号来执行不同的代码

//PAGEBREAK: 41
void
trap(struct trapframe *tf)
{
  if(tf->trapno == T_SYSCALL){
    ...
  }

  switch(tf->trapno){
      case T_IRQ0 + IRQ_TIMER:
        ...
      break;

    case T_IRQ0 + IRQ_IDE:
        ...
      break;

    case T_IRQ0 + IRQ_IDE+1:
        // Bochs generates spurious IDE1 interrupts.
        break;

    case T_IRQ0 + IRQ_KBD:
        ...
        break;

    case T_IRQ0 + IRQ_COM1:
            ...
      break;

    case T_IRQ0 + 7:
      case T_IRQ0 + IRQ_SPURIOUS:
        ...
      break;

      //PAGEBREAK: 13
      default:
        ...
}

我们这里因为关注如何给XV6增加一个系统调用,所以我们其实真正关注的内容就是第一个if中的内容,即

if (tf->trapno == T_SYSCALL)
{
    if (myproc()->killed)
        exit();
    myproc()->tf = tf;
    syscall();
    if (myproc()->killed)
        exit();
    return;
}

我们在上面说过trapno中的值是在vector.S中压入栈的,而vector64就将trapno的值设置为了64,和T_SYSCALL的值一样。

因此,操作系统就明白了此时发生了系统调用,于是就调用syscall去处理系统调用

D. Syscall函数

接下来,我们关注syscall函数。syscall函数是一个C语言函数,定义在syscall.c中。

相关的代码如下

// syscall.c

extern int sys_chdir(void);
extern int sys_close(void);
extern int sys_dup(void);
extern int sys_exec(void);
extern int sys_exit(void);
extern int sys_fork(void);
extern int sys_fstat(void);
extern int sys_getpid(void);
extern int sys_kill(void);
extern int sys_link(void);
extern int sys_mkdir(void);
extern int sys_mknod(void);
extern int sys_open(void);
extern int sys_pipe(void);
extern int sys_read(void);
extern int sys_sbrk(void);
extern int sys_sleep(void);
extern int sys_unlink(void);
extern int sys_wait(void);
extern int sys_write(void);
extern int sys_uptime(void);

static int (*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,
};

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

  num = curproc->tf->eax;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    curproc->tf->eax = syscalls[num]();
  } else {
    cprintf("%d %s: unknown sys call %d\n",
            curproc->pid, curproc->name, num);
    curproc->tf->eax = -1;
  }
}

其中,比较关键的如下:

  • 变量 syscalls是一个函数指针数组,即数组中的每一个元素都是一个指向函数的指针。 而SYS_xxx这些宏常量的值都定义在syscall.h中 (参考 syscall.h)
  • 此外,系统调用号(Syatem Call Number)存储在eax中 (参考 usys.S).

因此,syscall函数首先通过当前进程的eax寄存器获得系统调用号(line #55),然后根据系统调用号去从syscalls函数数组中选择对应的函数去执行(line

57)

需要注意的是,因为函数数组中的函数都是在别的文件中定义的,所以需要在最前面使用 extern关键字去声明一下外部符号常量,例如:sys_exitsys_closesys_mkdir

E. sys_XXX系统调用

最后,我们看一下sys_xxx之类的系统调用的定义(以sys_kill为例)

sys_kill定义在sysproc.c

// sysproc.c
int
sys_kill(void)
{
  int pid;

  if(argint(0, &pid) < 0)
    return -1;
  return kill(pid);
}

kill则定义在在proc.c中。

// proc.c

// Kill the process with the given pid.
// Process won't exit until it returns
// to user space (see trap in trap.c).
int
kill(int pid)
{
  struct proc *p;

  acquire(&ptable.lock);
  for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
    if(p->pid == pid){
      p->killed = 1;
      // Wake process from sleep if necessary.
      if(p->state == SLEEPING)
        p->state = RUNNABLE;
      release(&ptable.lock);
      return 0;
    }
  }
  release(&ptable.lock);
  return -1;
}

F. 传递参数

XV6的实现中,syscall中使用函数数组就是为了方便未来扩展系统调用,这样未来我们如果自己想要添加系统调用的话,直接给数组中新增加函数就行了。可是这样做又会造成新的问题,即参数传递问题。

用户想要使用系统调用的话,需要include user.h这个头文件,其中提供了所有系统调用的函数原型

// user.h

// system calls
int fork(void);
int exit(void) __attribute__((noreturn));
int wait(void);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);

可以有一个问题,我们上面在syscall.csyscalls函数数组中所有的函数都是int xxxx(void),即所有的函数都是不接受参数的(参考 syscall.c

extern int sys_chdir(void);
extern int sys_close(void);
extern int sys_dup(void);
extern int sys_exec(void);
extern int sys_exit(void);
extern int sys_fork(void);
extern int sys_fstat(void);
extern int sys_getpid(void);
extern int sys_kill(void);
extern int sys_link(void);
extern int sys_mkdir(void);
extern int sys_mknod(void);
extern int sys_open(void);
extern int sys_pipe(void);
extern int sys_read(void);
extern int sys_sbrk(void);
extern int sys_sleep(void);
extern int sys_unlink(void);
extern int sys_wait(void);
extern int sys_write(void);
extern int sys_uptime(void);

但是很多系统调用都是需要参数的,例如kill就需要进程的PID。因此,为了解决参数传递问题,XV6中又提供了新的机制。

前面解释过,C语言通过栈传递参数。而我们C语言再声明函数的时候,通过int xxx(char a, int b)来通知编译器,主调函数调用函数前把一个char类型的数据(一般是一个字节)先压入栈中,而后再把一个int类型的数据(一般是四个字节)压入栈中。而后进入被调函数之后就从栈中弹出五个字节即可。

因此,我们其实在知道的了一个函数接受的所有的参数的数据类型的情况下,我们其实可以手动从栈中去读取数据

XV6中提供了两个函数去从当前栈(中间spss一直没变)中获取参数。

这两个函数分别是argstrargint,定义在syscall.c中。

// Fetch the int at addr from the current process.
int
fetchint(uint addr, int *ip)
{
  struct proc *curproc = myproc();

  if(addr >= curproc->sz || addr+4 > curproc->sz)
    return -1;
  *ip = *(int*)(addr);
  return 0;
}

// Fetch the nul-terminated string at addr from the current process.
// Doesn't actually copy the string - just sets *pp to point at it.
// Returns length of string, not including nul.
int
fetchstr(uint addr, char **pp)
{
  char *s, *ep;
  struct proc *curproc = myproc();

  if(addr >= curproc->sz)
    return -1;
  *pp = (char*)addr;
  ep = (char*)curproc->sz;
  for(s = *pp; s < ep; s++){
    if(*s == 0)
      return s - *pp;
  }
  return -1;
}

// Fetch the nth 32-bit system call argument.
int
argint(int n, int *ip)
{
  return fetchint((myproc()->tf->esp) + 4 + 4*n, ip);
}

// Fetch the nth word-sized system call argument as a string pointer.
// Check that the pointer is valid and the string is nul-terminated.
// (There is no shared writable memory, so the string can't change
// between this check and being used by the kernel.)
int
argstr(int n, char **pp)
{
  int addr;
  if(argint(n, &addr) < 0)
    return -1;
  return fetchstr(addr, pp);
}

argint和fetchint函数

argint函数用于从程序的栈中获取第nint数据。而fetchint则是给定一个地址,而后在进行完地址检查之后,从该地址中取一个int出来。

因此argint中计算完偏移量之后,直接把程序当前的栈顶地址+偏移量传给fectint函数。而fetchint函数中直接进行的值复制,因此最后传入argintip(integer· pointer)就储存了整数值的拷贝。

而在argstr函数中,首先调用了argint。而调用argint的目的是:

  • 利用argint计算偏移地址,从而得到字符串的开头地址,而后通过fetchint检查字符串的开头地址是否非法(地址高于栈底地址)。

最后,argstr调用fetchstr将字符串指针pp指向了字符串开头的地址。

到此为止,我们就介绍完了XV6中系统调用的全部流程以及必要的函数

3. 增加tracegetcount系统调用

接下来,我们就将添加tracegetcount这两个系统调用

1. 增加系统调用号

syscall.h中定义了所有的系统调用号,而系统调用号将会在syscall.cusys.S中用的

// my syscall
#define SYS_trace  22
#define SYS_getcount  23

增加两个系统调用号

2. 增加SYSCALL宏函数

接下来要做的,就是在usys.S文件中为我们的系统调用增加宏函数。

SYSCALL(trace)
SYSCALL(getcount)

增加SYSCALL宏函数

3. syscall.c中定义函数原型

接下来我们要做的就是在syscall.c中增加我们的函数到syscalls函数数组中去

// my syscall
extern int sys_trace(void);
extern int sys_getcount(void);

static int (*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_getcount] sys_getcount
};

4. 实现tracegetcount

最后,我们要实现这tracegetcount这两个系统调用,因为需要和open系统调用配合,所以就在 sysfile.c中定义了

// sysfile.c

// my syscall data structure
typedef struct trace_file
{
  char filename[256];
  int open_counter;
  int trace_enabled;
} trace_file;

int num_traced;
trace_file tracing_file;

int sys_trace(void) {
  // get param from stack char *path;
  char *path;
  int length = argstr(0, &path);
  if (length < 0)
    return -1;

  int k = 0;
  for (k = 0; k < length; k++)
    tracing_file.filename[k] = path[k];
  tracing_file.filename[k] = '\0';
tracing_file.open_counter = 0;
  tracing_file.trace_enabled = 1;
  return 0;
}


int sys_getcount(void){
  if (tracing_file.trace_enabled == 1)
    return tracing_file.open_counter;
  else
    return 0;
}

最后修改 sys_open使得每次打开都会增加计数。

int
sys_open(void)
{
  char *path;
  int fd, omode;
  struct file *f;
  struct inode *ip;

  // check if trace enabled and add counter
  int length = argstr(0, &path);

  if(length < 0 || argint(1, &omode) < 0)
    return -1;

  char *ppath = path;
  char *ptf = tracing_file.filename;
  if (strncmp(ppath, ptf, length) == 0)
    if (tracing_file.trace_enabled)
      tracing_file.open_counter += 1;


  begin_op();

  if(omode & O_CREATE){
    ip = create(path, T_FILE, 0, 0);
    if(ip == 0){
      end_op();
      return -1;
    }
  } else {
    if((ip = namei(path)) == 0){
      end_op();
      return -1;
    }
    ilock(ip);
    if(ip->type == T_DIR && omode != O_RDONLY){
      iunlockput(ip);
      end_op();
      return -1;
    }
  }

  if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
    if(f)
      fileclose(f);
    iunlockput(ip);
    end_op();
    return -1;
  }
  iunlock(ip);
  end_op();

  f->type = FD_INODE;
  f->ip = ip;
  f->off = 0;
  f->readable = !(omode & O_WRONLY);
  f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
  return fd;
}

增加sys_trace和sys_getcount

修改sys_open


文章作者: Jack Wang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Jack Wang !
  目录