首页 > 其他分享 >[MIT 6.S081] Lab: system calls

[MIT 6.S081] Lab: system calls

时间:2024-01-22 11:24:10浏览次数:34  
标签:sys calls trace proc void mask SYS S081 MIT

Lab: system calls

前言

这次实验是实现内核中的两个 syscalltracesysinfo

回顾一下第三节课,用户态的进程想要执行某个系统调用,例如 exex(init, argv) ,首先会将 initargv 的地址分别存放在 a0a1 寄存器中,然后 a7 存放所要调用的系统调用,最后执行 ecall。之后要结束系统调用时,将系统调用的返回值传递给 a7 ,最后再一次执行 ecall。这样,一个系统调用就做完了。

ecall 指令用于 CPU 状态的转换。

System call tracing

这里所要实现的是 sys_trace ,也就是跟踪命令所调用的系统调用。它的指令格式如下:

trace trace_mask command

其中,trace_mask 是一个用来控制追踪系统调用类型的掩码。

首先,我们需要在 MakefileUPROGS 添加 $U/_trace

UPROGS=\
	...
	$U/_trace\

观察 user/trace.c ,可以发现其中的 trace 函数缺失,需要在 user/user.h 中补全一个函数声明。

// system calls
...
int trace(int);

按照 Lab 提供的信息,我们还需要在 user/usys.pl 中为刚刚添加的系统调用 trace 添加一个 entry

sub entry {
    my $name = shift;
    print ".global $name\n";
    print "${name}:\n";
    print " li a7, SYS_${name}\n";
    print " ecall\n";
    print " ret\n";
}
...
entry("trace");

观察上面的汇编代码,我们可以发现,这个 perl 文件的作用就是生成一段汇编,其中 name 将会替换为 entry 中的系统调用名称。

.global trace
global:
  li a7, SYS_trace
  ecall
  ret

还记得某处的 initcode 数组吗?它的实际代码如下:

# exec(init, argv)
.global start
start:
  la a0, init
  la a1, argv
  li a7, SYS_exec
  ecall
  
# for(;;) exit();
exit:
  li a7, SYS_exit
  ecall
  jal exit
  
# char init[] = "/init\0";
inti:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

我们可以看到,a7 寄存器将会承载 syscall 的编号。那么我们的 SYS_trace 代号在哪里呢?观察 kernal/syscall.h ,可以发现所有的 syscall 编号借由宏定义在这里。我们只需要在最后加一个就可以了。

...
#define SYS_close  21
#define SYS_trace  22

此时我们需要看一下,通用的 syscall 到底是干了什么。观察 kernal/syscall.c ,找到 void syscall() 函数定义,将会发现它利用之前的编号,在一个函数指针数组中直接查找对应的系统调用。trace 也是一个系统调用,因此我们为这个表补全一个 sys_trace

// in kernal/syscall.c
...
extern uint64 sys_uptime(void);
extern uint64 sys_trace(void);

static uint64 (*syscalls[])(void) = {
...
  [SYS_trace] sys_trace,  
};

到此,我们就开始实现 sys_trace 了。

通过 Lab 内容,我们观察 kernal/sysproc.c ,可以看到这里包含了很多的 sys_name 函数定义,还记得我们之前的 sys_trace 吗?我们只是指明了它在某个地方实现,但是目前为之依然还是找不到它。因此,我们要在这个文件下添加一个新的系统调用函数 sys_trace ,同时它的签名符合刚才的函数指针表。

uint64
sys_trace(void)
{
  // 获取 `trace` 的参数 trace_mask
  int trace_mask;
  if (argint(0, &trace_mask) < 0)
    return -1;
  // ?
  return 0;
}

现在获取到了 trace_mask ,但是我们要如何知道被执行的系统调用名称呢,难道是为每个系统调用添加吗?

回顾刚刚的一个通用系统调用 void syscall(void) ,可以知道的是,它将知道所有的系统调用具体是什么,那么我们直接修改它就可以了。问题是,要如何让它知道我们需要打印信息呢?

对于每一个执行的进程,应该有一个东西在描述它,那么这个东西最适合的就是描述进程的结构体 struct proc 了。我们为它添加一个 trace_mask 成员,通过它来传递 trace 的信息了。

// in kernal/proc.h
struct proc {
  struct spinlock lock;
  
  ...
  char name[16];               // Process name (debugging)

  uint64 trace_mask;
};

回到 sys_trace 函数,我们添加了这个成员后,就可以通过它来传递参数了。那么该如何传递呢?= 吗?假如命令是 trace a trace b com ,那么最后一个命令的关系应该为 a | b ,因此这里需要实现的是 |

uint64
sys_trace(void)
{
  // 获取 `trace` 的参数 trace_mask
  int trace_mask;
  if (argint(0, &trace_mask) < 0)
    return -1;
  // 使用 myproc() 获取当前进程的描述块
  myproc()->trace_mask |= trace_mask;
  return 0;
}

到此为止,我们依然还未实现输出跟踪信息。回到 void syscall(void) 函数中,我们将利用 trace_mask 实现。为了方便,这里需要在多添加一个表,用于记录每个系统调用的名称。此外,对于 trace_mask ,如果要输出某一个系统调用,其关系应该为 (trace_mask & (1 << SYS_which)) == 1

static const char* syscalls_name[] = {
[SYS_fork]    "fork",
[SYS_exit]    "exit",
[SYS_wait]    "wait",
[SYS_pipe]    "pipe",
[SYS_read]    "read",
[SYS_kill]    "kill",
[SYS_exec]    "exec",
[SYS_fstat]   "fstat",
[SYS_chdir]   "chdir",
[SYS_dup]     "dup",
[SYS_getpid]  "getpid",
[SYS_sbrk]    "sbrk",
[SYS_sleep]   "sleep",
[SYS_uptime]  "uptime",
[SYS_open]    "open",
[SYS_write]   "write",
[SYS_mknod]   "mknod",
[SYS_unlink]  "unlink",
[SYS_link]    "link",
[SYS_mkdir]   "mkdir",
[SYS_close]   "close",
[SYS_trace]   "trace",
[SYS_sysinfo] "sysinfo",
};

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]();  // return value

    // match trace
    if ((1 << num) & p->trace_mask) {
      // pid: syscall syscall_name ret_value
      printf("%d: syscall %s -> %d\n", p->pid, syscalls_name[num], p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

有一个问题,初始化呢?进程 fork 后又怎么办呢?

观察 kernal/proc.c 中,谁负责新建一个进程呢?allocproc。谁负责 fork 呢? fork

allocproc 中,我们只需要记得将新建后的进程块为其 trace_mask 初始化为 \(0\) 即可。

static struct proc*
allocproc(void)
{
  ...
  
  p->trace_mask = 0;

  return p;
}

fork 中,我们应该是新的进程块与其父进程块的 trace_mask 一致。

int
fork(void)
{
  int i, pid;
  struct proc *np;
  struct proc *p = myproc();

  // Allocate process.
  if((np = allocproc()) == 0){
    return -1;
  }

  // copy trace mask
  np->trace_mask = p->trace_mask;

  ...
  return pid;
}

至此,sys_trace 实现完毕了。

Sysinfo

sysinfo 的步骤和 trace 的基本步骤差不多,不太需要修改哪些结构体之类的,但是它需要添加点函数,一个计算剩余的内存块大小,一个计算分配的进程数量。

计算内存块的大小,需要观察内存的组织形式。在 kernal/kalloc.c 可以看到,内存的组织形式是个链表,直接遍历就行了。要注意的是注意上一下锁。

// in kernel/kalloc.c
// Get free memory byte
uint64
free_mem_byte(void) {
  uint64 free_byte = 0;

  acquire(&kmem.lock);
  struct run *p = kmem.freelist;
  while (p) {
    free_byte += PGSIZE;
    p = p->next;
  }
  release(&kmem.lock);

  return free_byte;
}

计算分配出去的进程数,就更直接了,直接遍历进程数组,获取对应的信息即可。

// in kernel/proc.c
// Get process count where state is not unused
uint64
free_proc_count(void) {
  uint64 cnt = 0;

  for (struct proc* p = proc; p < &proc[NPROC]; p ++) {
    cnt += p->state != UNUSED;
  }

  return cnt;
}

最后就是 sys_sysinfo 的实现,我们只需在函数内将上述两个信息获取到后,将这个信息拷贝到用户态中指定的位置,这个方式可以参考其他位置的实现,重点是看 copyout 是如何使用的。

// in kernel/sysproc.c
extern uint64 free_mem_byte();
extern uint64 free_proc_count();

uint64
sys_sysinfo(void) {
  uint64 dst;
  if (argaddr(0, &dst) < 0)
    return -1;

  struct sysinfo info = {
    .freemem = free_mem_byte(),
    .nproc = free_proc_count()
  };

  if (copyout(myproc()->pagetable, dst, (char*)&info, sizeof(info)) < 0)
    return -1;

  return 0;
}

Grade

标签:sys,calls,trace,proc,void,mask,SYS,S081,MIT
From: https://www.cnblogs.com/FlandreScarlet/p/17979656

相关文章

  • [MIT 6.S081] Lab: Xv6 and Unix utilities
    Lab:Xv6andUnixutilitiesGradesleepsleep格式如下sleep5这边需要使用kernal/stat.h中的sleep系统调用,并将参数转化为传入。#include"kernel/types.h"#include"kernel/stat.h"#include"user/user.h"intmain(intargc,char*argv[]){if(......
  • linux修改max user processes limits
    突破ulimit限制修改普通用户单个用户可同时运行的最大进程数(默认为4096)[root@xxxdevops]#cat/etc/security/limits.d/20-nproc.conf#Defaultlimitfornumberofuser'sprocessestoprevent#accidentalforkbombs.#Seerhbz#432903forreasoning.*......
  • 封装的子组件,才fromitem中,子组件的变化,如何被form监听到。
    子组件,应该有个onChange事件。问题:在嵌入自定义组件时,需要将子组件的onChange方法暴露出去。原因:1.form会监听的onChange方法和value变量(form是的一个属性)2.其它基于封装的组件如:,自定义的方法名必须叫onChange,变量名必须叫value,否则自定义组件的状态变化不能被form表单的onCha......
  • mitmproxy
    mitmproxy是一个代理工具(软件安装或Python模块安装),实现代理请求(拦截请求或修改请求)这里介绍python的模块使用这里强烈推荐这个安装第三方库的软件:链接:https://pan.baidu.com/s/1L56TY68VNrw54go8eTxjkg?pwd=pg22提取码:pg22pipinstallmitmproxy基本就没有报错了。启动启动mit......
  • vue+tsc+noEmit导致打包报TS类型错误问题及解决方法
    当我们新建vue3项目,package.json文件会自动给我添加一些配置选项,这写选项基本没有问题,但是在实际操作过程中,当项目越来越复杂就会出现问题。本文列举一个目前我遇到的一个问题:打包后报了一堆TS类型错误,怎么消除这些错误?项目环境:Vue3+Vite+TS当项目进行打包时候,突然发现终端......
  • Git - 合并本地未提交的 commit
    每次工作养成了保存commit的习惯,提交远程仓库时想合并这些commit保证历史信息干净整洁。通过rebase合并本地未提交的commit并修改消息。gitrebase-i[hash]如上图所示,我要将这两个3分钟前的commit合并在一起,那么就要执行gitrebase-i[第commit的hash值](第三个h......
  • 文件上传超出了tomcat的限制大小:org.apache.tomcat.util.http.fileupload.impl.FileSi
    报错的原因springBoot项目自带的tomcat对上传的文件大小有默认的限制,SpringBoot官方文档中展示:每个文件的配置最大为1Mb,单次请求的文件的总数不能大于10Mb。解决方法SpringBoot2.0版本在【application】配置文件中加入如下代码:#maxFileSize单个数据大小spring.servlet......
  • 资源限制LimitRange
    目录一.资源限制LimitRange概述1.什么是资源限制2.资源限制LimitRange和请求的约束二.资源限制LimitRange实战案例1.计算资源最大,最小限制1.1设置容器的最大值和最小值限制资源1.2小于requests样例测试1.3大于limits样例测试1.4不设置resources字段1.5设置合法的resources字......
  • MIT 6.S081入门 lab0 操作系统环境及其配置
    MIT6.S081入门lab0操作系统环境及其配置闲话由于不是正经计算机专业出身,但是又想做Linux内核/驱动开发,因此赶在暑假实习开始前把操作系统的课程补习一下。之前自学的linux的驱动系统入门的笔记在这个寒假也会整理并发布(包括U-boot移植和驱动/应用开发入门)。实验环境Ubuntu-......
  • docker 设置 ulimit
    一、通过dockerrun–ulimit参数设置这个容器的ulimit值dockerrun--ulimitnofile=1024:1024--rmdebiansh-c"ulimit-n"二、通过配置daemon.json配置默认值配置nofile{"default-ulimits":{"nofile":{......