操作系统—系统调用(实验)

ops/2024/10/16 2:25:41/

文章目录

系统调用

1.实验目标

  阅读并了解xv6内核中关于系统调用的部分,基于实验手册的要求完成几个任务:完成wait系统调用的非阻塞选项、实现yield系统调用、阅读并理解系统调用的代码。

2.实验过程记录

(1).理解系统调用接口

操作内容:在VS Code中查看并理解user/usys.pl的代码

perl">#!/usr/bin/perl -w
# Generate usys.S, the stubs for syscalls.
print "# generated by usys.pl - do not edit\n";print "#include \"kernel/syscall.h\"\n";sub entry {my $name = shift;print ".global $name\n";print "${name}:\n";print " li a7, SYS_${name}\n";print " ecall\n";print " ret\n";
}

  usys.pl是基于perl语言的代码文件,它的主要作用是为了所有当前内核的系统调用生成对应的汇编代码,简单来理解就是,它会根据传入系统调用的名字生成如下的一段系统调用对应的汇编代码:

.global $name
${name}:li a7, SYS_${name}ecallret

  这段代码就很熟悉了,首先粘贴系统调用的name作为当前汇编代码生成文件的入口(通过.global指令指定入口),之后使用li(load immediate)将系统调用的调用号座位一个立即数加载到a7上,之后调用ecall进入CaoZuoXiTong.html" title=操作系统>操作系统内核,在执行结束之后再用ret返回。

(2).阅读argraw、argint、argaddr和argstr

操作内容:在VS Code中查看并理解argraw等函数的代码

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当中,它最多可以接受6个参数(这一点和Linux内核是一致的,先前我在完成第一次选做作业的自定义系统调用时,在Linux内核当中看到的基于va_args实现的系统调用接口也最多只能接受6个参数)。

  在有了argraw这个函数之后,后续就可以通过argint、argaddr和argstr三个函数来获取系统调用的参数,分别是整数、地址和字符串,这撒个函数的实现相当简单,因为地址和整数本质上都是获取整数,因此可以直接通过argraw获取对应的内容,但是argstr的实现相对复杂一点,因为C语言的字符串实际上字符数组,作为参数传入的时候会退化成指针,所以实现获取字符串的过程可以首先通过argaddr获取地址,之后再通过实现的一个fetchstr函数获取字符串的内容并且存储到buf当中:

// Fetch the nth 32-bit system call argument.
int argint(int n, int *ip) {*ip = argraw(n);return 0;
}// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
int argaddr(int n, uint64 *ip) {*ip = argraw(n);return 0;
}// Fetch the nth word-sized system call argument as a null-terminated string.
// Copies into buf, at most max.
// Returns string length if OK (including nul), -1 if error.
int argstr(int n, char *buf, int max) {uint64 addr;if (argaddr(n, &addr) < 0) return -1;return fetchstr(addr, buf, max);
}

  于是我在syscall.c当中又找到了fetchstr函数的实现细节:

// Fetch the nul-terminated string at addr from the current process.
// Returns length of string, not including nul, or -1 for error.
int fetchstr(uint64 addr, char *buf, int max) {struct proc *p = myproc();int err = copyinstr(p->pagetable, buf, addr, max);if (err < 0) return err;return strlen(buf);
}

  fetchstr的实现也并不复杂,不过因为处于内核态,它的实现不像我们会使用的类似strcpy之类的函数,首先获取到当前的进程控制块到p,然后从p的页表中拷贝出对应字节、对应地址的所有数据,在没有发生报错的时候则会正常返回字符串的长度。

(3).理解系统调用的解耦合实现方式

操作内容:在VS Code中查看并理解syscall.c的代码

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_rename] sys_rename,
};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.c当中将所有的系统调用存储在同一个指针数组当中,在具体调用某个系统调用的时候,syscall函数会通过trapframe当中的a7来访问刚刚存入的系统调用号,之后再直接通过下标访问就可以比较轻松地访问到对应的系统调用了,这里也可以看到,在根据系统调用号找到了对应的系统调用之后会执行这条指令:

    p->trapframe->a0 = syscalls[num]();

  也就是说,系统调用的执行结果会被存储在a0当中。

(4).wait系统调用的非阻塞选项实现

操作内容:参考实验手册要求,修改wait系统调用的实现系统,增加一个非阻塞选项参数flags
  为了增加sys_wait的非阻塞版本,我们首先需要在sys_wait中增加获取整型变量flags作为非阻塞标志的流程:

uint64 sys_wait(void) {uint64 p;if (argaddr(0, &p) < 0) return -1;int flags;if (argint(1, &flags) < 0) return -1;return wait(p, flags);
}

  这个过程是比较简单的,参考argraw、argint的实现可以知道,argint函数第一个参数传入的实际上是系统调用参数的位置,因为flags显然是在第二个位置上的参数,因此仿照上面获取地址p的方式,获取flags参数,之后与p一起传入wait函数即可,那么接下来就可以修改wait系统调用的函数声明了

int             wait(uint64, int);

  接下来就需要直接修改wait系统调用的实现本身了:

int wait(uint64 addr, int flags) {if (flags != 1) sleep(p, &p->lock);  // DOC: wait-sleepelse {release(&p->lock);return -1;}}
}

  这里省略了wait函数的一部分,前面一部分的代码没有改动,具体的wait系统调用的实现已经再上一次的实验中分析过了,因此这里只需要改动最后一部分是否阻塞即可。
  当flags不为1的时候就阻塞等待,否则释放当前获取的自旋锁,然后返回-1,这样就可以完成非阻塞等待的全流程了,接下来尝试使用waittest完成对于wait系统调用的测试:
在这里插入图片描述
  可以看到,waittest在xv6内已经测试完毕,之后在内核仓库的目录下使用grade-lab-syscall进行测试:
在这里插入图片描述

(5).yield系统调用的实现

操作内容:参考实验手册要求,增加一个新的yield系统调用
  首先在kernel/syscall.h当中增加一个SYS_yield系统调用号,这里直接继承最后一个系统调用后的第一个系统调用号23:
在这里插入图片描述
  之后再kernel/syscall.c当中增加sys_yield的声明,并且在系统调用表syscalls当中增加刚刚添加的系统调用sys_yield:
在这里插入图片描述
  之后再sysproc.c当中添加sys_yield函数的定义:

uint64 sys_yield(void) {struct proc *p = myproc();uint64 pc = p->trapframe->epc;printf("start to yield, user pc %p\n", pc);yield();return 0;
}

  因为yield函数本身实现非常简单,所以不需要单独传参,为了满足实验手册的要求,在sys_yield当中获取了当前进程的PCB,从它的trapframe中获取epc即为进入trap时的用户pc,这里没有对PCB进行加锁,这是因为struct proc将trapframe划归为私有部分,访问时不需要持有锁。
然后需要再user.h和usys.pl当中添加代码
在这里插入图片描述
在这里插入图片描述
  最后,在Makefile的UPROGS增加_yieldtest的编译目标:
在这里插入图片描述
  之后编译运行,在xv6中执行yieldtest测试,发现效果是一致的:
在这里插入图片描述
  然后我尝试使用了grade-lab-syscall进行测试,发现有一个叫做time的点一直没有办法通过:
在这里插入图片描述
  于是我去MIT的课程网站上查看了一下实验要求,发现需要自行创建一个time.txt,然后写入做实验的时间:
在这里插入图片描述
  在创建了time.txt并且写入实验时间之后就100分通过了测试:
在这里插入图片描述

3.存在的问题及解决方案

问题:为什么在多CPU的情况下测试yield会出现乱码?

  首先我们来看看测试的时候会发生什么:
在这里插入图片描述
  可以发现,其实这一段输出信息混乱并不是完全没有规律,实际上应该是多个不同的需要打印出来的内容因为一些问题混杂在了一起,我猜测这个问题可能出在打印过程没有对缓冲区加锁,但是参考了printf.c当中对printf的实现,它实际上是加锁了的:

// Print to the console. only understands %d, %x, %p, %s.
void
#ifdef TEST
_printf(const char *filename, unsigned int line, char *fmt, ...)
#else
_printf(char *fmt, ...)
#endif
{…locking = pr.locking;if (locking) acquire(&pr.lock);#endifif (locking) release(&pr.lock);
}void printfinit(void) {initlock(&pr.lock, "pr");pr.locking = 1;
}

  所以问题应该不在给输出过程加锁这件事上,最终我猜测是多核调度的过程中,不同核心上运行的进程的缓冲区不同步,而缓冲区到IO设备上这个过程可能是无法保证线程安全的,因此导致了输出也不同步的问题。

实验小结


http://www.ppmy.cn/ops/12464.html

相关文章

Python 正则表达式

Python 正则表达式 目录 正则 flags:标志位 match函数 search函数 findall函数 finditer函数 元字符 匹配单个字符和数字 锚字符&#xff08;边界字符&#xff09; ^ 行首匹配 $ 行尾匹配 \A匹配字符串开始 \Z 匹配字符串结束 \b 匹配一个单词的边界 \B 匹配非单…

星途为什么对标奥迪

文/夏宁 在四月中旬举行的星途星纪元ET发布会上&#xff0c;星途致敬奥迪。会后&#xff0c;针对这一问题及有关产品热点&#xff0c;奇瑞集团星途品牌接受了媒体采访。接受采访的领导名单如下&#xff1a; 奇瑞汽车股份有限公司执行副总经理、奇瑞汽车工程技术研发总院 院长C…

【论文精读】DiffAttack:难以察觉和可转移的对抗性攻击的扩散模型

文章目录 一、文章概览&#xff08;一&#xff09;研究动机&#xff08;二&#xff09;扩散模型&#xff08;三&#xff09;文章工作 二、模型方法&#xff08;一&#xff09;问题表述&#xff08;二&#xff09;核心思想&#xff08;三&#xff09;具体框架1、DDIM反演技术2、…

unity 录制360全景渲染图

1.打开pakcageManager &#xff0c;选择packages为 unityRegisty&#xff0c;找到unityRecorder插件下载&#xff0c;点击右下角instant安装&#xff0c;如果插件列表为空&#xff0c;检查是否连接网络&#xff0c;重启Unity 2.打开录制面板 3.add recorder 选择ImageSequence …

上位机图像处理和嵌入式模块部署(树莓派4b与视觉slam十四讲)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 实际使用中&#xff0c;树莓派4b是非常好的一个基础平台。本身板子价格也不是很贵&#xff0c;建议大家多多使用。之前关于vslam&#xff0c;也就是…

基于Kepware的Hadoop大数据应用构建-提升数据价值利用效能

背景 Hadoop是一个由Apache基金会所开发的分布式系统基础架构&#xff0c;它允许用户在不需要深入了解分布式底层细节的情况下&#xff0c;开发分布式程序。Hadoop充分利用集群的威力进行高速运算和存储&#xff0c;特别适用于处理超大数据集。 Hadoop的生态系统非常丰富&…

git使用技巧记录

在Git中&#xff0c;如果您想要丢弃最近的提交并还原修改至提交前的状态&#xff0c;可以使用以下几种不同的方法&#xff0c;取决于您是否希望保留工作区的修改还是彻底还原到提交前的工作区和暂存区状态&#xff1a;1. 保留工作区的修改&#xff0c;仅撤销最近的提交&#xf…

jvm知识点总结(一)

JVM的跨平台 java程序一次编写到处运行。java文件编译生成字节码&#xff0c;jvm将字节码翻译成不同平台的机器码。 JVM的语言无关性 JVM只是识别字节码&#xff0c;和语言是解耦的&#xff0c;很多语言只要编译成字节码&#xff0c;符合规范&#xff0c;就能在JVM里运行&am…