宁夏建设厅网站官网,电商网,南宁网站建设工具,重庆市工程建设信息网官方网站【6S.081】Lab2 System Calls
注意#xff0c;在开始本章实验前#xff0c;你应该将代码切换到syscall分支 每次做完试验后#xff0c;下一次实验都应该切换到对应的分支 $ git fetch
$ git checkout your lab
$ make clean实验1 System call tracing#xff08;难度…【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系统调用。32是1SYS_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我们将看到 这就是操作对应的编号前面是系统调用操作名称。
好那么既然我们需要实现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\如下 这里说明一下
存根指的是系统调用的占位符函数。这些函数在用户空间中定义并通过特定的指令将控制权转移到内核以执行实际的系统调用。存根函数的主要作用是提供一个接口使用户程序可以方便地调用内核提供的系统服务。
没错而这里的entry也可以很明显看出它的作用。
当我们编译后查看usys.S文件我们就可以看到存根已经将系统调用号放入了寄存器的指令中。 对于这里的寄存器指令对接的是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[ ] 这个数组主要直接对接相应的系统调用。注意这里数组中元素的顺序需要与上个数组一样否则会出现打印的操作名称和相应具体操作实现不一样的情况。
声明。 好到这里我们对于系统调用的大致过程已经设置完成了 需要注意我们要始终知道我们学习的这门课是操作系统那么上述对于系统调用的过程实际上是非常重要的这也就和平常我们的写代码不一样的地方。 接下来就要具体实现trace这个操作。
首先我们来看下user中已经有了trace.c并且发现这个函数传入的是一个数字一个整型int那么我们直接对其进行声明在user.h中。
int trace(int);然后对于这个操作我们需要有一个整数参数也就是mask掩码用来指定要跟踪的系统调用。具体提示如下 我们查看proc.h上面说要将参数保存到proc这个结构体的一个新变量这个新变量指的就是mask我们添加进去。 好然后我们直接在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 数组中对应的系统调用函数指针是否有效。如果条件满足则调用对应的系统调用函数并将返回值存储在 trapframe 的 a0 寄存器中。如果当前进程的 mask 中对应系统调用号的位被设置则打印系统调用的信息包括进程 ID、系统调用名称和返回值。 总体来说这段代码实现了一个基本的系统调用处理机制通过读取系统调用号并调用相应的系统调用函数来处理用户程序的请求。 然后我们还要添加一个字符指针数组中的元素trace。——syscall_names[ ] 这个代码片段定义了一个静态字符指针数组 syscall_names用于存储系统调用的名称。每个数组元素都是一个字符串对应一个系统调用的名称。数组的索引与系统调用的编号相对应。主要用于打印时告诉我们对应的系统调用名称。
ok该实验的主要部分已经全部完成接下来就是要 再在Makefile添加该操作的目录即可。
大功告成测试一下
make qemu出现以下 输入对应案例trace 32 grep hello README都可以出现以下 退出xv6进行单元测试make grade一下 发现完美。
对于该实验在做完之后可以收获以下的知识
加深对系统调用机制的理解 通过添加新的系统调用你会亲自体验用户态与内核态之间的转换了解系统调用的参数传递和返回过程从而把理论知识转化为实际操作。
熟悉内核内部结构和进程管理 修改内核源码如 kernel/syscall.c、kernel/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***中添加一个函数
实验思路
根据提示前期工作已经很明了
我们先添加Makefile的UPROGS 然后在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的进程数量
看一下就知道了。 确实。那么接下来我们就需要在内核代码中实现一个名为sys_sysinfo的函数它应该有以下功能
接收一个指针指向上面提到的 struct sysinfo统计系统中空闲的内存并把数值填到 freemem 字段中遍历进程表统计那些状态不是 UNUSED 的进程数量并把结果填到 nproc 字段中
这些功能基于题目中的需求 那么第一个功能很好实现在后续该函数编写的时候直接调用即可。
我们先来分别实现后面两个功能。
功能的分别实现
首先来看第二个统计系统中空闲的内存并把数值填到 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( )。 然后我们就知道了copyout( )将会发挥作用。
我们去vm.c里面看 分析可以知道这个函数主要用于将数据从内核空间复制到用户空间处理跨页边界的情况并确保数据正确地复制到用户空间的目标地址。如果在复制过程中遇到任何错误例如地址转换失败函数将返回 -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.hint
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然后测试并且打分一套连招发现完美通过别忘了写编写时间文件
写完这两个实验我们会发现系统调用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在其中体现得淋漓尽致都需要我们实现内核和用户之间的传输和交流无论是进行信息的追踪还是拷贝所以说能够自己来编写并成功运行对于我们学习操作系统这门课还是帮助极大的。