当前位置: 首页 > news >正文

【6S.081】Lab2 System Calls

【6S.081】Lab2 System Calls

注意,在开始本章实验前,你应该将代码切换到syscall分支

每次做完试验后,下一次实验都应该切换到对应的分支

$ git fetch
$ git checkout `your lab`
$ make clean

实验1 System call tracing(难度:Moderate)

实验要求

在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在以后调试实验时对您有所帮助。您将创建一个新的trace系统调用来控制跟踪。它应该有一个参数,这个参数是一个整数“掩码”(mask),它的比特位指定要跟踪的系统调用。例如,要跟踪fork系统调用,程序调用trace(1 << SYS_fork),其中SYS_fork是***kernel/syscall.h***中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改xv6内核,以便在每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值;您不需要打印系统调用参数。trace系统调用应启用对调用它的进程及其随后派生的任何子进程的跟踪,但不应影响其他进程。

输入与预期输出

$ trace 32 grep hello README  //第一个示例
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README  //第二个示例
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README  //第三个示例
$
$ trace 2 usertests forkforkfork  //第四个示例
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$

解释:

在上面的第一个例子中,trace调用grep,仅跟踪了read系统调用。321<<SYS_read

在第二个示例中,trace在运行grep时跟踪所有系统调用;2147483647将所有31个低位置为1。

在第三个示例中,程序没有被跟踪,因此没有打印跟踪输出。

在第四个示例中,在usertests中测试的forkforkfork中所有子孙进程的fork系统调用都被追踪。

提示

  • 在*Makefile*UPROGS中添加$U/_trace
  • 运行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指令转换到内核。一旦修复了编译问题(注:如果编译还未通过,尝试先make clean,再执行make qemu),就运行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()函数以打印跟踪输出。您将需要添加一个系统调用名称数组以建立索引。

实验思路与操作

首先,在这一节的实验中,我们需要很详细地查看题目所给出的示例以及提示,只有读懂了它们才知道 What should we do

我们先来看题目要求:我们需要添加一个trace功能,用来控制跟踪。那么具体它是怎么个实现方法呢?我们就来看它给出的例子。

我们发现输入进去的信息是按照以下格式:

trace + 系统调用编号 + 操作名 + 操作对象

输出的信息是按照以下格式:

syscall + 系统调用名称 -> 返回值

好,我们来研究下输入和输出之间的关系:首先我们进行了trace也就是我们要实现的跟踪操作,然后带了一个编号,实际上这个编号就是对应了我们需要做的操作,也就响应了我们输出的系统调用名称将会是什么。那么我们来看看这个编号在哪。打开kernel/syscall.h,我们将看到:

image-20250319093027670

这就是操作对应的编号,前面是系统调用操作名称。

好,那么既然我们需要实现trace操作,我们照葫芦画瓢写上去即可。

#define SYS_trace  22 //追踪函数的调用序号

接下来,我们直接看提示中的第一条:

将系统调用的原型添加到user/user.h*,存根添加到user/usys.pl*,以及将系统调用编号添加到kernel/syscall.h*Makefile*调用perl脚本user/usys.pl*,它生成实际的系统调用存根user/usys.S*,这个文件中的汇编代码使用RISC-V的ecall指令转换到内核。

按照上方指示继续照葫芦画瓢。

首先。系统调用编号已经添加。然后是存根添加到user/usys.pl\,如下:

image-20250319093509752

这里说明一下:

存根指的是系统调用的占位符函数。这些函数在用户空间中定义,并通过特定的指令将控制权转移到内核,以执行实际的系统调用。存根函数的主要作用是提供一个接口,使用户程序可以方便地调用内核提供的系统服务。

没错,而这里的entry也可以很明显看出它的作用。

当我们编译后查看usys.S文件,我们就可以看到存根已经将系统调用号放入了寄存器的指令中。

image-20250319093820694

对于这里的寄存器指令,对接的是syscall.c。我们前去查看。

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;}
}

我们先直接去看syscall这个函数(如上),它是直接对应寄存器指令的。

然后我们先添加几个东西:

  • 一个数组中加一个sys_trace元素,这个数组包含了一系列系统调用的指针。——syscalls[ ]

image-20250319095845802

这个数组主要直接对接相应的系统调用。注意:这里数组中元素的顺序需要与上个数组一样,否则会出现打印的操作名称和相应具体操作实现不一样的情况。

  • 声明。

image-20250319095654728

好,到这里,我们对于系统调用的大致过程已经设置完成了,

需要注意,我们要始终知道我们学习的这门课是操作系统,那么上述对于系统调用的过程,实际上是非常重要的,这也就和平常我们的写代码不一样的地方。

接下来就要具体实现trace这个操作。

首先,我们来看下user中,已经有了trace.c,并且发现这个函数传入的是一个数字,一个整型int,那么我们直接对其进行声明,在user.h中。

int trace(int);

然后,对于这个操作,我们需要有一个整数参数,也就是mask掩码,用来指定要跟踪的系统调用。具体提示如下:

image-20250319101648958

我们查看proc.h,上面说要将参数保存到proc这个结构体的一个新变量,这个新变量指的就是mask,我们添加进去。

image-20250319101834501

好,然后我们直接在sysproc.c中实现sys_trace这个函数就行。按照它说的,把传进来的参数给现有进程的mask即可。

uint64
sys_trace(void)
{int mask;if (argint(0, &mask) < 0) //如果获取参数失败return -1;myproc()->mask = mask;  //否则将进程的权限掩码设置为 maskreturn 0;
}

好,mask我们传进来了,但我们还要是实现对应格式的输出。

我们又回到syscall.c中。

我们将对应格式的输出添加到syscall这个函数中。

void
syscall(void)
{int num;struct proc *p = myproc();num = p->trapframe->a7; //从寄存器a7中获取系统调用号if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {p->trapframe->a0 = syscalls[num]();//接下来是打印相应格式的输出if((1 << num)&p->mask){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;}
}

以下是对该函数的解释:

这段代码实现了一个系统调用处理函数 syscall

首先,函数定义了一个整数变量 num,用于存储系统调用号。然后,通过调用 myproc() 函数获取当前进程的指针 p接着,从当前进程的 trapframe 结构体中的寄存器 a7 中读取系统调用号,并将其存储在 num 变量中。注意:这里刚好告诉了我们该函数对应了文件Usys/S。

接下来,代码检查 num 是否在有效范围内(大于 0 且小于 syscalls 数组的元素数量),并且 syscalls 数组中对应的系统调用函数指针是否有效。如果条件满足,则调用对应的系统调用函数,并将返回值存储在 trapframea0 寄存器中。如果当前进程的 mask 中对应系统调用号的位被设置,则打印系统调用的信息,包括进程 ID、系统调用名称和返回值

总体来说,这段代码实现了一个基本的系统调用处理机制,通过读取系统调用号并调用相应的系统调用函数来处理用户程序的请求。

然后我们还要添加一个字符指针数组中的元素trace。——syscall_names[ ]

image-20250319095541124

这个代码片段定义了一个静态字符指针数组 syscall_names,用于存储系统调用的名称。每个数组元素都是一个字符串,对应一个系统调用的名称。数组的索引与系统调用的编号相对应。主要用于打印时告诉我们对应的系统调用名称

ok,该实验的主要部分已经全部完成,接下来就是要:

image-20250319102917257

image-20250319103020835

再在Makefile添加该操作的目录即可。

大功告成,测试一下,

make qemu,出现以下:

image-20250319103252001

输入对应案例:trace 32 grep hello README(都可以),出现以下:

image-20250319103329325

退出xv6,进行单元测试,make grade一下:

image-20250319103440143

发现完美。

对于该实验,在做完之后可以收获以下的知识:

加深对系统调用机制的理解
通过添加新的系统调用,你会亲自体验用户态与内核态之间的转换,了解系统调用的参数传递和返回过程,从而把理论知识转化为实际操作。

熟悉内核内部结构和进程管理
修改内核源码(如 kernel/syscall.ckernel/proc.c)让你深入了解内核的架构设计、进程控制块(PCB)的管理以及进程创建、复制(fork)等核心概念。

提升编写和调试内核代码的能力
实验过程中需要调整 Makefile、编译调试内核代码等,这对培养你在系统级编程中的工程实践能力和问题排查能力非常有帮助。

锻炼系统调试与追踪技能
实现系统调用跟踪功能不仅能帮助你在实验中调试代码,也为你以后在实际开发中分析和解决系统问题提供了有力工具。

实验2 Sysinfo(难度:Moderate)

实验要求

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

提示

  • 在*Makefile*UPROGS中添加$U/_sysinfotest
  • 当运行make qemu时,**user/sysinfotest.c*将会编译失败,遵循和上一个作业一样的步骤添加sysinfo系统调用。要在user/user.h***中声明sysinfo()的原型,需要预先声明struct sysinfo的存在:
struct sysinfo;
int sysinfo(struct sysinfo *);

一旦修复了编译问题,就运行sysinfotest;但由于您还没有在内核中实现系统调用,执行将失败。

  • sysinfo需要将一个struct sysinfo复制回用户空间;请参阅sys_fstat()(*kernel/sysfile.c*)和filestat()(*kernel/file.c)以获取如何使用copyout()执行此操作的示例。
  • 要获取空闲内存量,请在*kernel/kalloc.c*中添加一个函数
  • 要获取进程数,请在***kernel/proc.c***中添加一个函数

实验思路

根据提示,前期工作已经很明了:

我们先添加MakefileUPROGS

image-20250319160839203

然后在user.h声明我们需要编写的函数:

struct sysinfo;
int sysinfo(struct sysinfo *);

然后我们根据上个实验的经验,也在syscall.h添加系统调用序号::

#define SYS_sysinfo 23

usys.pl中添加调用入口:

entry("sysinfo");

syscall.c中新增函数定义:

extern uint64 sys_sysinfo(void);

相信可能大多数人跟我一样,在刚看这个定义的时候有点不太懂它的意思,这里解释一下:

extern
表示该函数在别处(通常是在另一个源文件中)定义,而不是在当前文件中定义。它告诉链接器需要在其他目标文件中查找该函数的实现。

uint64
是一个数据类型,代表 64 位无符号整数。在 xv6 中通常在类型定义文件(比如 types.h)中定义,通常是通过 typedef 将“unsigned long long”重命名为 uint64。

好,我们继续照葫芦画瓢:

syscall.c中函数指针数组新增一个元素:sys_info,同时在syscall_names中新增一个sys_info

[SYS_sysinfo]   sys_sysinfo,
...
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"};

ok,前期工作都做完了,现在开始写函数。

我们要实现的功能是什么?我们先来看下题目,得知:我们需要写一个能够收集有关正在运行系统的信息的系统调用,它叫做sysinfo

同时根据题目我们得知在 kernel/sysinfo.h 中已经定义了一个结构体(struct sysinfo),它有两个字段:

  • freemem:用来存储系统当前空闲内存的字节数
  • nproc:用来存储当前处于“使用中”(状态不为 UNUSED)的进程数量

看一下就知道了。

image-20250319162444644

确实。那么接下来我们就需要在内核代码中实现一个名为sys_sysinfo的函数,它应该有以下功能:

  • 接收一个指针,指向上面提到的 struct sysinfo
  • 统计系统中空闲的内存,并把数值填到 freemem 字段中
  • 遍历进程表,统计那些状态不是 UNUSED 的进程数量,并把结果填到 nproc 字段中

这些功能基于题目中的需求:

image-20250319162732985

那么第一个功能很好实现,在后续该函数编写的时候直接调用即可。

我们先来分别实现后面两个功能。

功能的分别实现

首先来看第二个,统计系统中空闲的内存,并把数值填到 freemem 字段中

既然我们需要统计空闲内存,那么就先要知道这些内存保存在哪。提示告诉我们了:要获取空闲内存量,请在*kernel/kalloc.c*中添加一个函数

好,我们直接去看这个文件并分析一下:这段代码实现了 xv6 中的物理内存分配器

struct run
{struct run *next;
};struct
{struct spinlock lock;struct run *freelist;
} kmem;

这里实现了一个链表,kmem用来保存最后链表的变量。

  • 内存初始化

    void kinit()
    {initlock(&kmem.lock, "kmem"); //这里有上锁操作freerange(end, (void *)PHYSTOP);
    }
    

    在 kinit() 中,通过 initlock 初始化 kmem 的锁,然后调用 freerange() 来把从 end 到 PHYSTOP 范围内的所有物理页都“释放”掉,也就是把它们加入到空闲页链表(freelist)中。

  • 释放内存页

    void freerange(void *pa_start, void *pa_end)
    {char *p;p = (char *)PGROUNDUP((uint64)pa_start);for (; p + PGSIZE <= (char *)pa_end; p += PGSIZE)kfree(p);
    }void kfree(void *pa)
    {struct run *r;if (((uint64)pa % PGSIZE) != 0 || (char *)pa < end || (uint64)pa >= PHYSTOP)panic("kfree");// 检测是否合法,并将页面填充为垃圾数据memset(pa, 1, PGSIZE);r = (struct run *)pa;acquire(&kmem.lock);r->next = kmem.freelist;kmem.freelist = r;release(&kmem.lock);
    }
    

    freerange() 以页为单位遍历内存区间,每一页调用 kfree()。在 kfree() 中,先检查页地址是否合法,然后用 memset 将页面填充为垃圾数据(以便调试时能发现错误使用),再将该页面结构化为 struct run 类型,最后在持锁的情况下将其插入到空闲页链表中。

  • 分配内存页
    kalloc() 也是持锁操作,从空闲链表中取出一个页,如果成功则将其填充成特定数据(也便于调试),最后返回该页的指针。

总而言之,我们最后可以得知空闲的内存量的freelist永远指向最后一个可用页。

那么如果需要一个获取空闲内存量的函数free_mem,只需要遍历这个链表往前走就可以找到。以下是具体实现:

uint64 free_mem(void) {uint64 freePages = 0;struct run *r;acquire(&kmem.lock);for(r = kmem.freelist; r; r = r->next)freePages++;release(&kmem.lock);return freePages * PGSIZE;
}

好,接下来,看后面那个功能:遍历进程表,统计那些状态不是 UNUSED 的进程数量,并把结果填到 nproc 字段中

直接看proc.c这个文件:
看完之后,发现很简单,它已经帮我们保存了当前进程的状态,我们只需要直接遍历并判断是否符合条件就行。同时注意加锁即可。

uint64
nproc(void)
{struct proc *p;uint64 num = 0;for (p = proc; p < &proc[NPROC]; p++){// 先加锁acquire(&p->lock);// 如果符合条件if (p->state != UNUSED){   num++;}// 计算完后释放锁即可release(&p->lock);}return num;
}

ok,大功告成。接下来去实现主要的函数。

哦,不要忘了先在defs.h添加这两个函数的声明:

// kalloc.c
...
uint64          free_mem(void);// proc.c
...
uint64          nproc(void);

好,关于sys_sysinfo,它说我们要复制一个struct sysinfo返回用户空间,并且告诉了我们位置在sysfile.c中有所提示,我们直接去看里面的sys_fstat( )以及这个函数里提到的在file.c中的filestat( )

image-20250319170407756

image-20250319170554860

然后我们就知道了,copyout( )将会发挥作用。

我们去vm.c里面看:

image-20250319170636418

分析可以知道:这个函数主要用于将数据从内核空间复制到用户空间,处理跨页边界的情况,并确保数据正确地复制到用户空间的目标地址。如果在复制过程中遇到任何错误(例如地址转换失败),函数将返回 -1。

也就是说,它相当于一个内核和用户之间的通道,用于拷贝收集到的信息。

好,看完之后仍旧一头雾水,嗯没关系,我们直接按需求去写。

首先看:我们要一个sysinfo结构体对吧,那我们直接写一个上去:(记得添加头文件sysinfo.h

struct sysinfo info;

然后要干啥,我们要调用我们写的函数来实现 收集系统的运行信息,包括空闲内存和活动进程数量。

那么我们就要从用户空间获取地址参数,然后根据地址参数来获取系统信息。

// 获取用户空间的地址参数if (argaddr(0, &addr) < 0)return -1; // 获取地址失败,返回错误// 获取系统信息info.freemem = freemem(); // 获取空闲内存的字节数info.nproc = nproc();     // 获取当前进程的数量

好,那么接下来我们信息都获取到了,就需要将它告诉用户空间啊!不然我们输出啥,你说对吧。这时候,copyout就派上用场了。

之前我们说过,这个函数是用来拷贝内核的系统信息到用户空间的指定位置,那么不就是可以让我们成功输出了嘛!

// 将系统信息拷贝到用户空间的指定地址if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)return -1; // 拷贝数据失败,返回错误

ok,整体如下:

uint64
sys_sysinfo(void)
{uint64 addr; // 获取用户空间的地址struct sysinfo info;struct proc *p = myproc();// 获取用户空间的地址参数if (argaddr(0, &addr) < 0)return -1; // 获取地址失败,返回错误// 获取系统信息info.freemem = freemem(); // 获取空闲内存的字节数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[])
{//标准公式if (argc != 1){fprintf(2, "Usage: %s need not param\n", argv[0]);exit(1);}
// 注意!这里开始调用struct sysinfo info;sysinfo(&info);// 打印信息printf("free space: %d\nused process: %d\n", info.freemem, info.nproc);exit(0);
}

ok,然后测试并且打分一套连招,发现完美通过:(别忘了写编写时间文件)
image-20250319172243109

写完这两个实验,我们会发现,系统调用(Syscall)在其中体现得淋漓尽致,都需要我们实现内核和用户之间的传输和交流,无论是进行信息的追踪还是拷贝,所以说,能够自己来编写并成功运行,对于我们学习操作系统这门课还是帮助极大的。

h"
#include “kernel/types.h”
#include “kernel/sysinfo.h”
#include “user/user.h”

int
main(int argc, char *argv[])
{
//标准公式
if (argc != 1)
{
fprintf(2, “Usage: %s need not param\n”, argv[0]);
exit(1);
}
// 注意!这里开始调用
struct sysinfo info;
sysinfo(&info);
// 打印信息
printf(“free space: %d\nused process: %d\n”, info.freemem, info.nproc);
exit(0);
}


ok,然后测试并且打分一套连招,发现完美通过:(别忘了写编写时间文件)
[外链图片转存中...(img-oLTIUPUU-1749812612445)]写完这两个实验,我们会发现,系统调用(Syscall)在其中体现得淋漓尽致,都需要我们实现内核和用户之间的传输和交流,无论是进行信息的追踪还是拷贝,所以说,能够自己来编写并成功运行,对于我们学习操作系统这门课还是帮助极大的。

相关文章:

  • Component name “index“ should always be multi-word的解决方式
  • SwiftUI隐藏返回按钮保留右滑手势方案
  • 系统架构设计师 1
  • Java 传输较大数据的相关问题解析和面试问答
  • 【prometheus+Grafana篇】基于Prometheus+Grafana实现postgreSQL数据库的监控与可视化
  • OmoFun动漫官网,动漫共和国最新入口|网页版
  • [Java恶补day24] 整理模板·考点三【二分查找】
  • 如何VMware虚拟机扩容磁盘,有很详细图文
  • leetcode HOT 100(128.连续最长序列)
  • 基于Netty的TCP Server端和Client端解决正向隔离网闸数据透传问题
  • [智能客服project] 架构 | 对话记忆 | 通信层
  • 事务传播行为详解
  • 基于RK3588,飞凌教育品牌推出嵌入式人工智能实验箱EDU-AIoT ELF 2
  • [Rviz2报错,已解决!]导入urdf模型错误:Could not load mesh resource 。。。
  • 微机原理与接口技术,期末习题(二)
  • 基于Three.js的交互式国风博物馆设计与实现
  • 【白雪讲堂】当前GEO是否能追溯数据源?
  • Ubuntu 与 Windows 实现文件夹共享
  • ubuntu 系统 多条命令通过 bash 脚本执行
  • [python]pycddlib使用案例
  • 做网站托管的好处/天津百度百科
  • 专题活动是在官方网站还是在其他网站做/近期10大新闻事件
  • 做的最少的网站/网站做优化好还是推广好
  • 网站建设和推广话术/seopc流量排行榜企业
  • 武汉网站建设公司有哪些/建站开发
  • 做网站选择什么服务器/免费的app推广平台