【xv6操作系统】Lab systems calls

news2024/11/15 17:33:38

一、实验前须知

阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件:

        系统调用的用户空间代码在 user/user.h 和 user/usys.pl 中。

        内核空间代码在 kernel/syscall.h 和 kernel/syscall.c 中。

        与进程相关的代码在 kernel/proc.h 和 kernel/proc.c 中。

二、基础知识

第二章 操作系统组织结构

一个操作系统必须满足3点要求:多路复用、隔离、交互;

Xv6采用宏内核设计;

xv6运行在多核risc-v微处理单元;

risc-v是一个64位cpu,并且xv6用”LP64”( L:long类型,P:pointer类型在c语言中是64位,int是32位)C写的。

(一)抽象物理资源

实现强隔离。例如,

1.unix应用与内存交互仅通过文件系统的open、read、write、close system call,而不是直接读写硬盘。

2.unix进程使用exec来构建他们的内存镜像,而不是直接与物理内存交互。这允许操作系统决定一个进程该在内存中的哪个位置。如果内存紧张,操作系统甚至可能将进程的一些数据存储到硬盘上。

许多形式的unix进程间交互通过文件描述符发生。

(二)user mode、supervisor mode、system calls

强隔离需要一个坚固的边界。如果程序出错,我们不想让操作系统失败,或其他应用失败。操作系统应该能够清理失败应用,并继续运行其他应用。为了实现强隔离,操作系统必须这么安排:应用不能更改(甚至读)操作系统的数据结构和指令,应用不能访问其他进程的内存。

cpu为强隔离提供硬件支持。例如:risc-v有3种cpu执行指令的模式:machine mode、supervisor mode、user mode。

在machine mode下执行的指令有全部权限。cpu从machine mode开始启动。machine mode绝大多数用于配置一台电脑。xv6在machine mode下执行几行代码,然后切换到supervisor mode。

在supervisor mode中,cpu被允许执行权限指令:例如,启用、禁用中断,读写寄存器(保存页表地址)等等。如果一个应用在user mode下尝试执行权限指令,cpu不会执行这个指令,而是切换到supervisor mode下终止应用,因为这个应用做了它不该做的事。在kernel space(或in supervisor mode)中运行的软件称作kernel。

将cpu从user mode切换到supervisor mode,从kernel声明的入口点处进入kernel(risc-v提供ecall指令达到这个目的)。一旦cpu切换到supervisor mode,kernel会校验system call参数,决定应用是否被允许执行请求操作,然后拒绝它或者执行它。kernel控制过渡到supervisor mode的入口是很重要的事;如果一个应用可以决定kernel入口,一个恶意程序可以进入到kernel中逃脱参数校验的点。

(三)kernel组织

设计问题的关键是:操作系统的哪部分应该运行在supervisor mode下。一种可能是整个操作系统搁置在kernel中,因此所有system calls的实现运行在supervisor mode下。这个组织叫做monolithic kernel。

在宏内核下,操作系统各部分之间协作更容易。例如:一个操作系统可能有一个buffer cache,它可以被file system和virtual memory system共享。

monolithic组织的一个缺点是:操作系统不同部分间的接口通常是复杂的(我们将在之后文中看到),因此操作系统开发人员很容易犯错。在monolithic kernel中,一个错误是致命的,因为一个supervisor mode下的错误通常将导致kernel挂掉。如果kernel挂掉了,计算机停止工作,因此所有应用也挂掉。计算机必须重启。

为了减少kernel错误风险,os设计者会最小化操作系统代码(运行在supervisor mode下)的体积,并在用户mode下执行操作系统块。这个kernel组织称作microkernel。

图2.1阐述了微内核设计。在这个图中,文件系统作为一个用户级别的进程运行。为了允许应用和file server交互,kernel提供一个内部进程交互机制来让用户进程之间发送消息。例如:如果一个像shell这样的应用想读写文件,它向file server发消息并等待响应。

因为xv6不提供一些服务,它的内核比一些微内核小,但xv6的理念是monolithic。

(四)代码:xv6组织

xv6 kernel源码在kernel子目录中。资源被分成多个文件,跟随了模块化理念。内部模块接口定义在defs.h(kernel/defs)中。

(五)进程概览

xv6中的隔离单元是进程(正如其他unix中一样)。进程抽象阻止一个进程破坏、侦测另外一个进程的内存、cpu、文件描述符等等。也阻止一个进程破坏kernel本身,因此一个进程不能推翻kernel隔离机制。

为了帮助强制隔离,进程为程序提供一个私有内存系统或者地址空间(看起来是),其他进程不能读写。进程也给程序提供它自己的cpu(看起来是)来执行程序的指令。

xv6使用页表(硬件实现)来给每个进程分配自己的地址空间。risc-v页表翻译一个虚拟地址(risc-v指令操作的地址)到物理地址(cpu分配给主存的地址)。

xv6为每个进程保留一个独立的页表(定义了进程的地址空间)。正如图2.3阐述的那样,一个地址空间包括进程的用户内存(起始于虚拟内存地址0)。首先是指令,紧跟全局变量,然后栈,最后是一个堆(进程可以根据需要拓展)。

一些因素限制进程地址空间的最大尺寸:risc-v上面的指针是64位的;在页表中查找虚拟地址时,硬件仅使用低39位;xv6仅使用39位中的38位。因此最大地址是238-1=0x3fffffffff,这是最大虚拟地址(MAXVA max virtual address, kernel/riscv.h:348)。在xv6地址空间顶部保留一个page用作trampoline和一个page匹配进程的trapframe来切换到kernel,正如我们将在第四章解释的那样。

xv6 kernel给每个进程保留了许多个state,这些state集合在一起组成一个结构体struct_proc(kernel/proc.h)。

每个进程有一个执行线程,执行进程指令。一个线程可能会被挂起,然后再唤醒。在两个进程之间透明地切换,kernel挂起当前运行线程,并且唤醒另外一个进程的线程。thread的多个state(本地变量、函数调用返回地址)被存储在线程栈中。

每个进程有两个栈:一个用户栈,一个kernel栈。当进程正在执行用户指令,仅它的用户栈在使用,它的内核栈是空的。当进程进入kernel(system call或interrupt),kernel代码在进程kernel stack;当进程在kernel中时,它的用户栈仍然保留存储的数据,但不会被使用。因此kernel仍然可以执行,即使进程已经毁坏了它的用户stack。

一个进程通过执行risc-v的ecall指令可以做一个system call。这个指令提升硬件权利,改变程序计数器到一个kernel定义的入口。代码在入口处切换到kernel stack,并执行system call实现的kernel指令。当system call完成后,kernel切换回用户栈,通过调用sret指令返回到用户空间,这个指令拉低了硬件权利级别,并在system call指令之后恢复执行用户指令。

 当执行进程在用户空间中时,xv6引发paging硬件来使用一个进程的p->pagetable。

(六)现实世界

现代操作系统支持一个进程中多个线程,让一个进程利用多个cpu。进程支持多线程的机器(xv6不支持)有很多,包含潜在的接口改变(例如:linux的clone、fork变形),来控制进程(多线程共享)的各方面。

(七)traps和system calls

调用system call

用户代码为exec在寄存器a0和a1放置参数,并把system call序号放到a7。system call序号匹配入口在syscalls数组,一个函数指针表(kernel/syscall.c:108)。ecall指令traps into kernel并执行uservec、usertrap,然后syscall,正如我们上面看到的。

syscall(kernel/syscall.c:133)从trapframe存的a7取得system call序号,并用它在system calls中找出。对于第一个system call,a7包含SYS_exec(kernel/syscall.h:8),结果是调用system call 实现函数sys_exec。

当system call实现函数返回,syscall记录它的返回值在p->trapframe->a0这将导致原始用户空间call exec()返回该值,因为risc-v的c调用约定是将返回值放到a0。system call约定返回负值来表明错误,0或正值表明成功。如果system call序号无效,syscall打印一个错误,并返回-1。

system call参数

system call kernel实现需要找出user code传递的参数。kernel trap代码保存用户寄存器到当前进程的trap frame,kernel code可以访问。函数argint,argaddr,和argfd,从trap frame获取第n个system call参数,作为整数、指针、文件描述符。他们都调用argraw来获取对应保存参数的寄存器(kernel/syscall.c:35)。

一些system call传递指针作为参数,并且kernel必须使用那些指针来读取或写user内存。exec system call,传递kernel一个指针数组,来标识用户空间的字符串参数。这些指针有两个挑战。首先用户程序可能是有bug或恶意的,并且可能传给kernel一个无效指针或一个欺骗kernel访问kernel 内存(而不是user内存)的指针。第二,xv6 kernel page table映射不同于user page table映射,因此kernel不会使用原始指令来加载或存储用户提供地址。

The kernel implements functions that safely transfer data to and from user-supplied addresses. fetchstr是一个例子(kernel/syscall.c:25)。file system calls例如exec使用fetchstr从用户空间来获取字符串文件名参数。fetchstr调用copyinstr来做这个工作。

三、实验开始

新建文件夹,重新使用下面的命令下载代码作为实验 2 工作区。

$ git clone git://g.csail.mit.edu/xv6-labs-2020

$ cd xv6-labs-2020

$ git checkout syscall

四、System call tracing

1. 实验目的

添加一个系统调用跟踪功能,该功能可以在以后的实验中为你提供帮助。

你将创建一个新的 trace 系统调用来控制跟踪。

它应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。例如,为了跟踪 fork 系统调用,程序调用 trace (1 << SYS_fork) ,其中 SYS_fork 是来自 kernel/syscall.h 的系统调用号。

如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。

该行应包含 进程 ID 、系统调用名称 和 返回值 ;您不需要打印系统调用参数。 trace 系统调用应该为调用它的进程和它随后派生的任何子进程启用跟踪,但不应影响其他进程。

2. 实验要求及提示

将 $U/_trace 添加到 Makefile 的 UPROGS 中

运行 make qemu , 你将看到编译器无法编译 user/trace.c ,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到 user/user.h ,将存根添加到 user/usys.pl ,以及将系统调用号添加到 kernel/syscall.h 中。 Makefile 调用 perl 脚本 user/usys.pl ,它生成 user/usys.S ,实际的系统调用存根,它使用 RISC-V ecall 指令转换到内核。修复编译问题后,运行 trace 32 grep hello README ;它会失败,因为你还没有在内核中实现系统调用。

在 kernel/sysproc.c 中添加一个 sys_trace() 函数,该函数通过在 proc 结构中的新变量中记住其参数来实现新系统调用(请参阅 kernel/proc.h )。从用户空间检索系统调用参数的函数位于 kernel/syscall.c 中,你可以在 kernel/sysproc.c 中查看它们的使用示例。

修改 fork() (参见 kernel/proc.c )以将跟踪的掩码从父进程复制到子进程。

修改 kernel/syscall.c 中的 syscall() 函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。

3. 实验步骤

作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义,模仿已经存在的系统调用序号的宏定义,我们定义 SYS_trace 如下:

#define SYS_trace  22

查看了一下 user 目录下的文件,发现官方已经给出了用户态的 trace 函数( user/trace.c ),所以我们直接在 user/user.h 文件中声明用户态可以调用 trace 系统调用就好了。

但有一个问题,该系统调用的参数和返回值分别是什么类型呢?接下来我们还是得看一看 trace.c 文件,可以看到 trace(atoi(argv[1])) < 0 ,即 trace 函数传入的是一个数字,并将返回值和 0 进行比较,结合实验提示,我们知道传入的参数类型是 int ,返回值类型也是 int 。这样就可以把 trace 这个系统调用加入到内核中声明了:

接下来我们查看 user/usys.pl 文件,这里 perl 语言会自动生成汇编语言 usys.S ,是用户态系统调用接口。所以在 user/usys.pl 文件加入下面的语句:

entry("trace");

查看上一次实验编译后的 usys.S 文件,可以看到如下的代码块:

.global fork

fork:

 li a7, SYS_fork

 ecall

 ret

li a7, SYS_fork 指令就是把 SYS_fork 的系统调用号放入 a7 寄存器,使用 ecall 指令进入系统内核。

执行ecall指令之后,cpu跳转到 kernel/syscall.c 中 syscall 那个函数处,执行此函数。下面是 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]();

  } else {

    printf("%d %s: unknown sys call %d\n",

            p->pid, p->name, num);

    p->trapframe->a0 = -1;

  }

}

num = p->trapframe->a7;:从寄存器 a7 中读取系统调用号,放入num中。

接下来是 p->trapframe->a0 = syscalls[num](); 语句,通过调用 syscalls[num](); 函数,把返回值保存在了 p->trapframe->a0中。我们看看 syscalls[num](); 函数,这个函数在当前文件中。我们把新增的 trace 系统调用添加到其中:

static uint64 (*syscalls[])(void) = {

  ...

  [SYS_trace]   sys_trace,

};

接下来在文件开头(kernel/syscall.c)给内核态的系统调用 trace 加上声明

extern uint64 sys_trace(void);

在实现这个函数之前,我们可以看到实验最后要输出每个系统调用函数的调用情况,依照实验说明给的示例,可以知道最后输出的格式如下:

<pid>: syscall <syscall_name> -> <return_value>

其中, <pid> 是进程序号, <syscall_name> 是系统调用名称, <return_value> 是该系统调用的返回值。注意:冒号和 syscall 中间有个空格。

根据提示,我们的 trace 系统调用应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。所以,我们在 kernel/proc.h 文件的 proc 结构体中,新添加一个变量 mask ,使得每一个进程都有自己的 mask ,即要跟踪的系统调用。

struct proc {

  ...

  int mask;               // Mask

};

然后我们就可以在 kernel/sysproc.c 给出 sys_trace 函数的具体实现了,只要把传进来的参数给到现有进程的 mask 就好了:

uint64

sys_trace(void)

{

  int mask;

  // 取 a0 寄存器中的值返回给 mask

  if(argint(0, &mask) < 0)

    return -1;

  

  // 把 mask 传给现有进程的 mask

  myproc()->mask = mask;

  return 0;

}

接下来我们就要把输出功能实现,因为 RISCV 的 C 规范是把返回值放在 a0 中,所以我们只要在调用系统调用时判断是不是 mask 规定的输出函数,如果是就输出。

在proc 结构体(见 kernel/proc.h )自己定义一个数组,作为调用的系统调用名称。我这里直接在 kernel/syscall.c 中定义了,这里注意系统调用名字一定要按顺序,第一个为空,当然你也可以去掉第一个空字符串,但要记得取值的时候索引要减一,因为这里的系统调用号是从 1 开始的。

static char *syscall_names[] = {

  "", "fork", "exit", "wait", "pipe",

  "read", "kill", "exec", "fstat", "chdir",

  "dup", "getpid", "sbrk", "sleep", "uptime",

  "open", "write", "mknod", "unlink", "link",

  "mkdir", "close", "trace"};

进程序号直接通过 p->pid 就可以取到,函数名称需要从我们刚刚定义的数组中获取,即 syscall_names[num] ,其中 num 是从寄存器 a7 中读取的系统调用号,系统调用的返回值就是寄存器 a0 的值了,直接通过 p->trapframe->a0 语句获取即可。注意上面说的那个空格。

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((1 << num) & p->mask) {//p->mask为输入的掩码,num为当前系统调用的序号

      printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);

    }

  } else {

    printf("%d %s: unknown sys call %d\n",

            p->pid, p->name, num);

    p->trapframe->a0 = -1;

  }

}

然后在 kernel/proc.c 中 fork 函数调用时,添加子进程复制父进程的 mask 的代码:

int

fork(void)

{

  ...

  pid = np->pid;

  np->state = RUNNABLE;

  // 子进程复制父进程的 mask

  np->mask = p->mask;

  ...

}

最后在 Makefile 的 UPROGS 中添加:

UPROGS=\

  ...

  $U/_trace\

4. 实验结果

编译并运行 xv6 进行测试。

$ make qemu

只要和 2的系统调用号次幂相与之后为真(如2147483647与当前系统调用号相与),就会被打印出来

退出 xv6 ,运行单元测试检查结果是否正确。

五、Sysinfo

1.实验要求

在本实验中,您将添加一个系统调用 sysinfo ,它收集有关正在运行的系统信息。系统调用接受一个参数:一个指向 struct sysinfo 的指针(参见 kernel/sysinfo.h )。内核应该填写这个结构体的字段: freemem 字段应该设置为空闲内存的字节数, nproc 字段应该设置为状态不是 UNUSED 的进程数。我们提供了一个测试程序 sysinfotest ;如果它打印 “sysinfotest:OK” ,则实验结果通过测试。

2.实验提示

将 $U/_sysinfotest 添加到 Makefile 的 UPROGS 中。

运行 make qemu , 你将看到编译器无法编译 user/sysinfotest.c 。添加系统调用 sysinfo ,按照与之前实验相同的步骤。要在 user/user.h 中声明 sysinfo() 的原型,您需要预先声明 struct sysinfo :

struct sysinfo;

int sysinfo(struct sysinfo *);

修复编译问题后,运行 sysinfotest 会失败,因为你还没有在内核中实现系统调用。

sysinfo 需要复制一个 struct sysinfo 返回用户空间;有关如何使用 copyout() 执行此操作的示例,请参阅 sys_fstat() ( kernel/sysfile.c ) 和 filestat() ( kernel/file.c )。

要收集空闲内存量,请在 kernel/kalloc.c 中添加一个函数。

要收集进程数,请在 kernel/proc.c 中添加一个函数。

3.实验步骤

跟上个实验一样,首先定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义 SYS_sysinfo 如下:

#define SYS_sysinfo  23

在 user/usys.pl 文件加入下面的语句:

entry("sysinfo");

然后在 user/user.h 中添加 sysinfo 结构体以及 sysinfo 函数的声明:

struct stat;

struct rtcdate;

// 添加 sysinfo 结构体

struct sysinfo;

// system calls

...

int sysinfo(struct sysinfo *);

在 kernel/syscall.c 中新增 sys_sysinfo 函数的定义:

extern uint64 sys_sysinfo(void);

在 kernel/syscall.c 中函数指针数组新增 sys_trace :

[SYS_sysinfo]   sys_sysinfo,

记得在 kernel/syscall.c 中的 syscall_names 新增一个 sys_trace :

static char *syscall_names[] = {

  "", "fork", "exit", "wait", "pipe",

  "read", "kill", "exec", "fstat", "chdir",

  "dup", "getpid", "sbrk", "sleep", "uptime",

  "open", "write", "mknod", "unlink", "link",

  "mkdir", "close", "trace", "sysinfo"};

接下来我们就要开始写相应的函数实现了。

首先我们写获取可用进程数目的函数实现。通过阅读 kernel/proc.c 文件可以看到下面的语句:

struct proc proc[NPROC];

这是一个进程数组的定义,这里保存了所有的进程。

我们再阅读 kernel/proc.h 查看进程结构体的定义:

enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

// Per-process state

struct proc {

  struct spinlock lock;

  // p->lock must be held when using these:

  enum procstate state;        // Process state

  struct proc *parent;         // Parent process

  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

  // 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 mask;                    // Mask

};

可以看到,进程里面已经保存了当前进程的状态,所以我们可以直接遍历所有进程,获取其状态判断当前进程的状态是不是为 UNUSED 并统计数目就行了。当然,通过 proc 结构体的定义,我们知道使用进程状态时必须加锁,我们在 kernel/proc.c 中新增函数 nproc 如下,通过该函数以获取可用进程数目:

// Return the number of processes whose state is not UNUSED

uint64

nproc(void)

{

  struct proc *p;

  // counting the number of processes

  uint64 num = 0;

  // traverse all processes

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

  {

    // add lock

    acquire(&p->lock);

    // if the processes's state is not UNUSED

    if (p->state != UNUSED)

    {

      // the num add one

      num++;

    }

    // release lock

    release(&p->lock);

  }

  return num;

}

接下来我们来实现获取空闲内存数量的函数。可用空间的判断在 kernel/kalloc.c 文件中。

这里定义了一个链表,每个链表都指向上一个可用空间,这里的 kmem 就是一个保存最后链表的变量。

struct run {

  struct run *next;

};

struct {

  struct spinlock lock;

  struct run *freelist;

} kmem;

继续分析文件kernel/kalloc.c可知,这里把从 end (内核后的第一个地址) 到 PHYSTOP (KERNBASE + 128*1024*1024) 之间的物理空间以 PGSIZE 为单位全部初始化为 1 ,然后每次初始化一个 PGSIZE,就把上一次初始化好的页放到当前页的下一个,然后把当前页挂在了 kmem.freelist 上,所以 kmem.freelist 永远指向最后一个可用页,那我们只要顺着这个链表往后走,直到 NULL 为止。所以我们就可以在 kernel/kalloc.c 中新增函数 free_mem ,以获取空闲内存数量

// Return the number of bytes of free memory

uint64

free_mem(void)

{

  struct run *r;

  // counting the number of free page

  uint64 num = 0;

  // add lock

  acquire(&kmem.lock);

  // r points to freelist

  r = kmem.freelist;

  // while r not null

  while (r)

  {

    // the num add one

    num++;

    // r points to the next

    r = r->next;

  }

  // release lock

  release(&kmem.lock);

  // page multiplicated 4096-byte page

  return num * PGSIZE;

}

然后在 kernel/defs.h 中添加上述两个新增函数的声明:

// kalloc.c

...

uint64          free_mem(void);

// proc.c

...

uint64          nproc(void);

接下来我们按照实验提示,添加 sys_sysinfo 函数的具体实现,这里提到 sysinfo 需要复制一个 struct sysinfo 返回用户空间,根据实验提示使用 copyout() 执行此操作,我们查看 kernel/sysfile.c 文件中的 sys_fstat() 函数,如下:

uint64

sys_fstat(void)

{

  struct file *f;

  uint64 st; // user pointer to struct stat

  if(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)

    return -1;

  return filestat(f, st);

}

这里可以看到调用了 filestat() 函数,该函数在 kernel/file.c 中,如下:

// Get metadata about file f.

// addr is a user virtual address, pointing to a struct stat.

int

filestat(struct file *f, uint64 addr)

{

  struct proc *p = myproc();

  struct stat st;

  

  if(f->type == FD_INODE || f->type == FD_DEVICE){

    ilock(f->ip);

    stati(f->ip, &st);

    iunlock(f->ip);

    if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)

      return -1;

    return 0;

  }

  return -1;

}

我们可以知道,复制一个 struct sysinfo 返回用户空间需要调用 copyout() 函数,上面是一个例子,我们来查看一下 copyout() 函数的定义( kernel/vm.c ):

// Copy from kernel to user.

// Copy len bytes from src to virtual address dstva in a given page table.

// Return 0 on success, -1 on error.

int

copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)

{

  uint64 n, va0, pa0;

  while(len > 0){

    va0 = PGROUNDDOWN(dstva);

    pa0 = walkaddr(pagetable, va0);

    if(pa0 == 0)

      return -1;

    n = PGSIZE - (dstva - va0);

    if(n > len)

      n = len;

    memmove((void *)(pa0 + (dstva - va0)), src, n);

    len -= n;

    src += n;

    dstva = va0 + PGSIZE;

  }

  return 0;

}

该函数其实就是把在内核地址 src 开始的 len 大小的数据拷贝到用户进程 pagetable 的虚地址 dstva 处,所以 sys_sysinfo 函数实现里先用 argaddr 函数读进来我们要保存的在用户态的数据 sysinfo 的指针地址,然后再把从内核里得到的 sysinfo 开始的内容以 sizeof(info) 大小的的数据复制到这个指针上。模仿上面filestat()函数,我们在 kernel/sysproc.c 文件中添加 sys_sysinfo 函数的具体实现如下:

// add header

#include "sysinfo.h"

uint64

sys_sysinfo(void)

{

  // addr is a user virtual address, pointing to a struct sysinfo

  uint64 addr;

  struct sysinfo info;

  struct proc *p = myproc();

  

  if (argaddr(0, &addr) < 0)

  return -1;

  // get the number of bytes of free memory

  info.freemem = free_mem();

  // get the number of processes whose state is not UNUSED

  info.nproc = nproc();

  if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)

    return -1;

  

  return 0;

}

最后在 user 目录下添加一个 sysinfo.c 用户程序:

#include "kernel/param.h"

#include "kernel/types.h"

#include "kernel/sysinfo.h"

#include "user/user.h"

int

main(int argc, char *argv[])

{

    // param error

    if (argc != 1)

    {

        fprintf(2, "Usage: %s need not param\n", argv[0]);

        exit(1);

    }

    struct sysinfo info;

    sysinfo(&info);

    // print the sysinfo

    printf("free space: %d\nused process: %d\n", info.freemem, info.nproc);

    exit(0);

}

最后在 Makefile 的 UPROGS 中添加:

$U/_sysinfotest\

$U/_sysinfo\

4.实验结果

编译并运行 xv6 进行测试。

$ make qemu

退出 xv6 ,运行单元测试检查结果是否正确。

./grade-lab-syscall sysinfo

通过测试样例。

六、参考文档

[1] 操作系统实验Lab 2:system calls(MIT 6.S081 FALL 2020)_编写 system_call.c 文件输出进程号 pid-CSDN博客

[2] Russ Cox, Frans Kaashoek, Robert Morris, xv6: A simple, Unix-like teaching operating system, 2020.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1502548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

iOS-系统弹窗调用

代码&#xff1a; UIAlertController *alertViewController [UIAlertController alertControllerWithTitle:"请选择方式" message:nil preferredStyle:UIAlertControllerStyleActionSheet];// style 为 sheet UIAlertAction *cancle [UIAlertAction actionWithTit…

GitHub和Gitee的基本使用和在IDEA中的集成

文章目录 【1】GitHub1.创建仓库2.增加和修改文件3.创建分支4.删除仓库5.远程仓库下载到本地 【2】Gitee1.创建仓库2.远程仓库下载到本地. 【3】IDEA集成GitHub【4】IDEA集成Gitee1.在Gitee中修改&#xff0c;同步到本地2.从Gitee中下载项目 【1】GitHub 1.创建仓库 先登陆这…

基于Token的身份验证:安全与效率的结合

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

阿里云服务器“地域”是什么?怎么选择比较好?

阿里云服务器地域选择方法&#xff0c;如何选择速度更快、网络延迟更低的地域节点&#xff0c;地域指云服务器所在的地理位置区域&#xff0c;地域以城市划分&#xff0c;如北京、杭州、深圳及上海等&#xff0c;如何选择地域&#xff1f;建议根据用户所在地区就近选择地域&…

【Multisim】关于导入TI的SPICE模型发生的报错

关于如何在Multisim导入TI提供的SPICE模型&#xff0c;该篇博客描述的很清楚&#xff1a;在Multisim导入TI提供的SPICE模型_multisim如何导入元器件的仿真模型-CSDN博客 但是使用13或14版本的Multisim在操作过程中可能会遇到如下问题&#xff1a; The model contains multiple …

STM32的启动流程分析 和 一些底层控制的原理

阅读引言&#xff1a; 阅读本文之后&#xff0c; 你将对单片机&#xff0c; 甚至是嵌入式系统&#xff0c; 或者是传统的PC机系统的启动流程有一个大致的了解&#xff0c; 本文更加偏向于单片机的启动流程分析。 目录 一、基础知识 1.STM32系列的微控制器&#xff08;mcu&…

PDF控件Spire.PDF for .NET【安全】演示:使用时间戳服务器对 PDF 进行数字签名

Spire.PDF for .NET 是一款独立 PDF 控件&#xff0c;用于 .NET 程序中创建、编辑和操作 PDF 文档。使用 Spire.PDF 类库&#xff0c;开发人员可以新建一个 PDF 文档或者对现有的 PDF 文档进行处理&#xff0c;且无需安装 Adobe Acrobat。 E-iceblue 功能类库Spire 系列文档处…

阿里云服务器买哪个地区比较好?2024阿里云服务器地域怎么选择?

阿里云服务器地域选择方法&#xff0c;如何选择速度更快、网络延迟更低的地域节点&#xff0c;地域指云服务器所在的地理位置区域&#xff0c;地域以城市划分&#xff0c;如北京、杭州、深圳及上海等&#xff0c;如何选择地域&#xff1f;建议根据用户所在地区就近选择地域&…

物联网在智慧城市建设中的关键作用:连接、感知、智能响应

一、引言 随着信息技术的飞速发展&#xff0c;物联网&#xff08;IoT&#xff09;技术已经渗透到我们生活的方方面面&#xff0c;特别是在智慧城市建设中发挥着至关重要的作用。智慧城市是指通过运用先进的信息和通信技术&#xff0c;实现城市基础设施、公共服务、交通管理、环…

四桥臂三相逆变器动态电压恢复器(DVR)MATLAB仿真

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 简介 四桥臂三相逆变器 电路 的一般形式如图 1&#xff0c;为 便于分析 &#xff0c;将其等效成图所示的电路 。以直流母线电压Ud的 1&#xff0f;2处为参考点 &#xff0c;逆变器三相和零线相 输 出可等效成…

Git 远程操作

1.分布式版本控制系统 我们目前所说的所有内容&#xff08;工作区&#xff0c;暂存区&#xff0c;版本库等等&#xff09;&#xff0c;都是在本地&#xff01;也就是在你的笔记本或者计算机上。而我们的 Git 其实是分布式版本控制系统&#xff01;什么意思呢 可以简单理解为&am…

2024届 C++ 刷题 笔试强训 Day 01

选择题 01 以下for循环的执行次数是&#xff08;&#xff09; for(int x 0, y 0; (y 123) && (x < 4); x); A 是无限循环 B 循环次数不定 C 4次 D 3次 解题思路&#xff1a; 我们直接来看本道题中最关键的部分&#xff1a;(y 123) && (x < 4)。 (y…

在NAT模式下,局域网内的其他电脑要访问使用NAT模式的虚拟机或设备

在NAT模式下&#xff0c;局域网内的其他电脑要访问使用NAT模式的虚拟机或设备 可以通过以下几种方式&#xff1a; 1、端口映射&#xff1a;在NAT模式下&#xff0c;你可以配置端口映射&#xff0c;类似于Docker中的端口映射。这样&#xff0c;局域网内的其他电脑就可以通过特…

RabbitMQ篇

1.初始MQ 1.1. 同步和异步通讯 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c;打电话可以立即得到响应&#xff0c;…

产品推荐 - 基于星嵌 OMAPL138+国产FPGA的DSP+ARM+FPGA三核开发板

1 评估板简介 基于TI OMAP-L138&#xff08;定点/浮点DSP C674xARM9&#xff09; FPGA处理器的开发板&#xff1b; OMAP-L138是TI德州仪器的TMS320C6748ARM926EJ-S异构双核处理器&#xff0c;主频456MHz&#xff0c;高达3648MIPS和2746MFLOPS的运算能力&#xff1b; FPGA…

数据中台驱动:高效交付之道

如何保证数据中台高效交付&#xff1f; 在数据行业中&#xff0c;项目交付难题尤为突出&#xff0c;尤其在数据中台领域。数据中台项目交付面临诸多挑战&#xff0c;若不妥善解决&#xff0c;将会降低服务质量&#xff0c;影响企业数字化建设的顺利开展&#xff0c;甚至影响项目…

K8s-MySQL主从集群

K8s-MySQL主从集群 引言 该案例代码均可从https://github.com/WeiXiao-Hyy/k8s_example 获取&#xff0c;欢迎Star&#xff01; 需求 一个“主从复制”的MySQL集群有一个主节点Master有多个从节点Slave从节点需要能水平扩展所以写操作只能在主节点上执行读操作可以在所有节点…

批量PDF转HTML:高效管理与优化文档格式

随着数字化时代的快速发展&#xff0c;PDF文件因其跨平台兼容性和良好的排版效果而被广泛应用。然而&#xff0c;在文本批量管理的场景中&#xff0c;我们可能需要将PDF文件转换为HTML格式&#xff0c;以便更好地进行编辑、搜索和定制。本文将为您介绍一种高效的方法&#xff0…

C语言之练手题

题目1&#xff1a; 思路&#xff1a;我们定义两个变量left和right分别为数组的左端下标和右端下标。 左端下标的元素为奇数时&#xff0c;left继续往前走&#xff0c;为偶数时就停下 右端下标的元素为偶数时&#xff0c;right- -往回走&#xff0c;为奇数时停下 停下后对应的元…

【C++】手撕string类(超实用!)

前言 一、标准库中的string类 1.1 string类介绍 1.2 string的常用接口 1.2.1 常用的构造函数 1.2.2 容量操作接口 &#xff08;1&#xff09;size &#xff08;2&#xff09;capacity &#xff08;3&#xff09;empty &#xff08;4&#xff09;clear &#xff08…