MIT 6.s081操作系统实验 Lab2: system calls

news2024/11/26 22:21:38

文章目录

  • 1 System call tracing
    • 1.1 主要思路
    • 1.2 系统调用流程
  • 2 Sysinfo
    • 2.1 kernel/kalloc.c 此文件用于实现分配物理空间的函数
      • 2.1.1 结构体定义
      • 2.1.2 空闲链表初始化
      • 2.1.3 内存的分配和释放
      • 2.1.4 获取空闲内存字节数
    • 2.2 kernel/proc.c 此文件用于进程管理
    • 2.3 sys_Sysinfo
      • 2.3.1读取用户传递的参数
      • 2.3.2 数据复制到用户态
      • sys_sysinfo具体实现
  • 3 总结

实验链接

1 System call tracing

写一个系统调用追踪器,作用是对于指定的系统调用号,对进程号,系统调用名称,返回值进行打印。

1.1 主要思路

  1. 每个系统调用都需要经过syscall函数,此函数从进程的trapframe取得将要进行调用的系统调用号num,因此思路就是在进行系统调用后,若num是否在进程指定的调用号中,则将以上信息进行打印。
  2. 完成trace系统调用:在1.中需要知道进程要追踪的指定系统调用号是什么,因此trace系统调用就是来指定系统调用号的,因此对于每个进程,为进程结构体中新增属性mask指定系统调用号集合,并且还需要在fork出子进程时,让子进程继承此属性。
    //kernel/proc.h
    struct proc {
      int pid;                     // Process ID
      ....
      // mask that expresses which syscalls will be traced.
      int mask;
    };
    //kernel/proc.c
    int
    fork(void){
      .....
      np->mask = p->mask;
    }
    // kernel/sysproc.c
    uint64
    sys_trace(void){
      int mask;
      if(argint(0, &mask) < 0)
        return -1;
      myproc()->mask = mask;
      return 0;
    }
    

1.2 系统调用流程

根据Lab的介绍,系统调用的流程如下:

  • 系统调用函数需要被用户程序所感知,因此在/user/user.h中添加系统调用的方法int trace(int);,以供给用户调用
  • user/usys.pl脚本类似其他系统调用添加entry("trace");
    //user/usys.pl文件
    #!/usr/bin/perl -w
    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";
    }
    entry("fork");
    ...
    entry("trace");
    
  • Makefile会执行此脚本,生成user/usys.S
    //Makefile文件
    $U/usys.S : $U/usys.pl
    	perl $U/usys.pl > $U/usys.S
    
  • user/usys.S汇编代码中,用户调用trace,实际上会通过ecall跳转到真正的系统调用,具体的实现很简单,就是为1个a7寄存器赋值一个在头文件define好的系统调用号。
    # generated by usys.pl - do not edit
    #include "kernel/syscall.h"
    .....
    .global trace
    trace:
     li a7, SYS_trace
     ecall
     ret
    
  • 看到user/usys.S中,include了"kernel/syscall.h"的头文件,此头文件只有一些define值,因此以上汇编句,其实等价于li a7, 22,即a7 = 22,进行了一下寄存器赋值
    // System call numbers
    ...
    #define SYS_close  21
    #define SYS_trace  22
    
  • 22实际上是在kernel/syscall.c中的一个系统调用函数数组的下标,在系统调用时,进程取到寄存器a7的值作为系统调用函数数组的下标,至此终于真的调用到了这个函数,当然,最后我们还需要在kernel/sysproc.c中实现对应的函数。
    //kernel/syscall.c
    ........
    extern uint64 sys_trace(void);
    extern uint64 sys_sysinfo(void);
    static uint64 (*syscalls[])(void) = {
    .........
    [SYS_trace]   sys_trace,
    [SYS_sysinfo] sys_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](); //※
      } ......
      if((p->mask >> num) & 1)
        printf("%d: syscall %s -> %d\n", p->pid, sys_call_name[num], p->trapframe->a0);
    }
    

2 Sysinfo

新增一个系统调用Sysinfo,传入的参数为以下结构体的指针,内核会将结构体填满:freemem表示空闲内存的字节数,nproc表示状态为UNUSED的进程数。

struct sysinfo {
  uint64 freemem;   // amount of free memory (bytes)
  uint64 nproc;     // number of process
};

提示1:sysinfo需要将数据复制到用户态,需要使用copy_out函数,可参考sys_fstat() (kernel/sysfile.c) filestat() (kernel/file.c)函数。
提示2:在kernel/kalloc.c中新增统计空闲内存的函数,在kernel/proc.c中新增统计空闲进程的函数。

2.1 kernel/kalloc.c 此文件用于实现分配物理空间的函数

主要是定义了一个空闲链表用来管空闲内存,包括初始化,内存回收,内存分配,以下是具体函数功能介绍,非常非常简单。

2.1.1 结构体定义

首先是定义了1个run的极简链表,用来串起空闲内存,其次是kmem结构体,用来放1个锁,还有空闲内存链表的表头。

struct run {
  struct run *next;
};

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

2.1.2 空闲链表初始化

  • kinit 此函数用于空闲链表初始化
    先使用initlock初始化锁,然后使用freerange指定初始化的内存范围是从endPHYSTOP

    extern char end[]; // first address after kernel.
                       // defined by kernel.ld.
    extern char end[]; // first address after kernel.
                       // defined by kernel.ld.
    void
    kinit()
    {
      initlock(&kmem.lock, "kmem");
      freerange(end, (void*)PHYSTOP);
    }
    
  • freerange 此函数用于将连续长度的内存空间释放掉
    释放的方法是使用kfree逐页(一页PGSIZE为4096B)将首地址插入到空闲链表中

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

2.1.3 内存的分配和释放

  • kfree 此函数将1个物理页的首地址存放到空闲链表中
    实现上:
    • 先判断地址是否越界
    • 不越界则先将该页填满数据垃圾,避免数据泄露
    • 进行隐式链接:直接在该地址创建1个空闲链表节点,采用头插的方法将该节点作为新的空闲链表节点。这种链接方法为隐式链接,即直接在空闲的地址创建节点,这样的好处是空闲节点不用额外占据空间,可节约空间;坏处是隐式链接的稳定性较差,若某个链接断开或错漏,则整个链表失效。注意在链接前需要获得锁,在链接后释放锁,因为空闲链表是临界资源,同一时间只能由1个进程访问。
    // Free the page of physical memory pointed at by v,
    // which normally should have been returned by a
    // call to kalloc().  (The exception is when
    // initializing the allocator; see kinit above.)
    void
    kfree(void *pa)
    {
      struct run *r;
    
      if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
        panic("kfree");
    
      // Fill with junk to catch dangling refs.
      memset(pa, 1, PGSIZE);
    
      r = (struct run*)pa;
    
      acquire(&kmem.lock);
      r->next = kmem.freelist;
      kmem.freelist = r;
      release(&kmem.lock);
    }
    
  • kalloc 此函数将空闲链表中的一页分配出去
    // Allocate one 4096-byte page of physical memory.
    // Returns a pointer that the kernel can use.
    // Returns 0 if the memory cannot be allocated.
    void *
    kalloc(void)
    {
      struct run *r;
    
      acquire(&kmem.lock);
      r = kmem.freelist;
      if(r)
        kmem.freelist = r->next;
      release(&kmem.lock);
    
      if(r)
        memset((char*)r, 5, PGSIZE); // fill with junk
      return (void*)r;
    }
    

2.1.4 获取空闲内存字节数

  • 自实现get_freemem,用于返回当前内存空闲字节数
    根据以上分析,显然内存空闲字节数就是空闲链表的节点数量乘上每个物理页的大小,注意在访问空闲链表的时候需要加锁,访问结束后释放锁,因为可能出现以下场景:
    • 进程A调用sysinfo,并获取了kmem.freelist
    • 进程B调用malloc,并触发了kalloc,若未加锁,则kalloc将链表头部的节点kmem.freelist分配给进程B
    • 进程A尝试通过run链表的next指针尝试去获取下一个节点,但kmem.freelist已经给进程B在使用,此地址不再存放之前的next地址,而是进程B的私有数据,不应该再在此时访问,这样会导致数据泄露,破坏内存的安全性,并且next在此语义下仍是一个地址,可能会出现访问越界等情况。
    uint64 get_freemem(){
      uint64 freemem = 0;
      acquire(&kmem.lock);
      struct run *p= kmem.freelist;
      while(p){
        freemem += PGSIZE;
        p = p->next;
      }
      release(&kmem.lock);
      return freemem;
    }
    

2.2 kernel/proc.c 此文件用于进程管理

因为这个文件的体量较为庞大,后续还有和进程管理更强相关的如Lab thread: Multithreading实现多线程,Lab lock: Parallelism/locking实现并行和进程锁,因此暂不做详细解析。
目前任务是统计空闲进程的数量。
采用的是最简单的遍历思路,在文件中定义了进程结构体数组struct proc proc[NPROC];直接遍历此进程数组,获取进程状态stateUNUSED的数量。参考已有代码中遍历进程数组的某个函数procdump:利用for(p = proc; p < &proc[NPROC]; p++)遍历进程数组,利用p->state判断进程状态。

//Get the num of processes that are not in UNUSED state
int 
not_UNUSED_process_num(void){
  int count = 0;
  for(int i = 0; i < NPROC; i ++)
    count += (proc[i].state != UNUSED);
  return count;
}

2.3 sys_Sysinfo

提示1:sysinfo需要将数据复制到用户态,因此需要使用copy_out函数,可参考sys_fstat() (kernel/sysfile.c) filestat() (kernel/file.c)函数。sys_fstat() (kernel/sysfile.c) 函数如下:

// kernel/sysfile.c
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);
}

主要完成的是2个工作:

2.3.1读取用户传递的参数

此系统调用有2个参数,参数1为struct file *类型,参数2为uint64类型,但参数不是直接传递的,而是需要通过argxx去读取trapframe寄存器,因为在用户调用参数前,会将参数按顺序存放到trapframea0a6寄存器。

例如static int argfd(int n, int *pfd, struct file **pf)用于读取用户传进来的fd,并从进程的打开文件列表ofile中,根据fd索引到其指向的文件结构体file

// Fetch the nth word-sized system call argument as a file descriptor
// and return both the descriptor and the corresponding struct file.
static int
argfd(int n, int *pfd, struct file **pf)
{
  int fd;
  struct file *f;

  if(argint(n, &fd) < 0)
    return -1;
  if(fd < 0 || fd >= NOFILE || (f=myproc()->ofile[fd]) == 0)
    return -1;
  if(pfd)
    *pfd = fd;
  if(pf)
    *pf = f;
  return 0;
}

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

内核获得用户态函数参数调用流程

2.3.2 数据复制到用户态

参考filestat() (kernel/file.c),此处使用copyout函数将数据从内核态复制到用户态。copyout函数原型如下,它从src开始,复制len字节到虚拟地址dstva中。
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)

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

sys_sysinfo具体实现

  1. 读取用户传递参数:用户传递进来的是1个sysinfo结构体的指针,因此先使用argaddr获取此指针
  2. 获取需要的系统信息
  3. 通过copyout将获取的信息传递到用户态。
uint64
sys_sysinfo(void)
{
  uint64 p;

  if(argaddr(0, &p) < 0)
    return -1;
  struct sysinfo sp;
  
  sp.nproc = not_UNUSED_process_num();
  sp.freemem = get_freemem();
  if(copyout(myproc()->pagetable, p, (char *)(&sp), sizeof(sp)) < 0)
      return -1;
  return 0;
}

3 总结

通过这个实验,我了解了:

  • 系统调用的流程,但还不了解具体是如何从用户态陷入内核态(TODO)
  • xv6使用隐式链表数据结构来进行空闲内存的分配和回收,同时加锁防止多个进程使用此临界资源
  • 进程管理是是1个进程结构体数组维护,可通过遍历获取每个进程的状态
  • 内核态要将数据返回给用户态,因为用户态有自己的进程空间,使用的虚拟地址无法直接在内核态解引用,所以在内核态需要实现copyout函数,利用用户态进程的页表来进行虚拟地址到物理地址的寻址,再直接将数据拷贝到用户态进程的虚拟地址映射的物理地址上。

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

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

相关文章

Openssl数据安全传输平台006:粘包的处理-代码框架及实现-TcpSocket.cpp

文章目录 0. 代码仓库1. TCP通信粘包问题2. 粘包、拆包表现形式2.1 正常情况2.2 两个包合并成一个包2.3 出现了拆包 3. 粘包的处理-参考仓库中的文件TcpSocket.cpp3.1 发送数据时候的处理3.2 接收数据时候的处理 0. 代码仓库 https://github.com/Chufeng-Jiang/OpenSSL_Secure_…

互联网Java工程师面试题·Java 面试篇·第四弹

目录 59、我们能自己写一个容器类&#xff0c;然后使用 for-each 循环码&#xff1f; 60、ArrayList 和 HashMap 的默认大小是多数&#xff1f; 61、有没有可能两个不相等的对象有有相同的 hashcode&#xff1f; 62、两个相同的对象会有不同的的 hash code 吗&#xff1f; …

有限小数题解(进制转换+某进制判断是否为无限小数)

给定一个 A 进制下的分数 a/b&#xff0c; 小蓝想把它化为 B 进制下的小数 c。 现在他想知道这个小数是不是一个有限小数。 Input 输入共一行&#xff0c;包含四个数 a, b, A, B&#xff0c;表示该分数和两种进制。 其中 A, B 使用十进制表示&#xff0c; a, b 中大于 9 的数…

2023最新UI酒桌喝酒游戏小程序源码 娱乐小程序源码 带流量主

2023最新UI酒桌喝酒游戏小程序源码 娱乐小程序源码 带流量主 修改增加了广告位&#xff0c;根据文档直接替换&#xff0c;原版本没有广告位 直接上传源码到开发者端即可 通过后改广告代码&#xff0c;然后关闭广告展示提交&#xff0c;通过后打开即可 无广告引流 流量主版…

【JavaEE】网络编程(网络编程基础、Socket套接字)

一、网络编程基础 1.1、什么是网络编程&#xff1f; 网络编程&#xff0c;指网络上的主机&#xff0c;通过不同的进程&#xff0c;以编程的方式实现网络通信&#xff08;或称为网络数据传输&#xff09; 注意&#xff1a;我们只要满足进程不同就行&#xff1b;所以即便是同一…

2023.10.19 关于设计模式 —— 单例模式

目录 引言 单例模式 饿汉模式 懒汉模式 懒汉模式线程安全问题 分析原因 引言 设计模式为编写代码的 约定 和 规范 阅读下面文章前建议点击下方链接明白 对象 和 类对象 对象和类对象 单例模式 单个实例&#xff08;对象&#xff09;在某些场景中有特定的类&#xff0c;…

VS采用nuget配置OpenGL

参考&#xff1a;如何配置Opengl编程环境_opengl配置_知心宝贝的博客-CSDN博客 这应该是最快的办法了&#xff0c;直接用nuget配置。 1.打开NuGet包管理器 2.搜索glew、glfw、glm、freeglut并点击安装即可 3.测试代码 能正常运行说明配置成功了 #include <GL/glew.h>…

Windows 安装 Java

1. 安装 JDK 从 Oracle 的官网下载的 JDK&#xff0c;例如 JDK 21 双击下载得到的 msi 文件&#xff0c;开始安装 JDK 选择要安装的文件路径&#xff08;我一般都默认&#xff09;&#xff1a; 等待安装&#xff1a; 安装完成&#xff1a; 2. 验证是否安装成功 2.1. 打开 cmd…

小插曲 -- 使用Visual Studio Code远程连接香橙派

在之前的学习中&#xff0c;代码的修改和保存都依赖于“vi”指令&#xff0c;而不得不承认vi指令的编辑界面非常原始&#xff0c;所以&#xff0c;如果可以将代码编辑放到更友好的环境里进行无疑是一件大快人心的事情。 本节介绍如何通过Visual Studio Code来进行远程连接: Vi…

Kubernetes 通过 Deployment 部署Jupyterlab

概要 在Kubernetes上部署jupyterlab服务&#xff0c;链接Kubernetes集群内的MySQL&#xff0c;实现简单的数据开发功能。 前置条件 镜像准备&#xff1a;自定义Docker镜像--Jupyterlab-CSDN博客 MySQL-Statefulset准备&#xff1a;StatefulSet 简单实践 Kubernetes-CSDN博客…

Nvidia显卡基础概念介绍

一、PCIe与SXM 1.1 Nvidia GPU PCIe PCIe(peripheral component interconnect express)是一种高速串行计算机扩展总线标准&#xff0c;是英特尔公司在2001年提出来的&#xff0c;它的出现主要是为了取代AGP接口&#xff0c;优点就是兼容性比较好&#xff0c;数据传输速率高、…

rust学习——字符串、字符串字面量、切片(slice)、字符串 slice

文章目录 字符串、字符串字面量、切片&#xff08;slice&#xff09;、字符串 slice01、字符串02、字符串字面量03、切片 &#xff08;slice&#xff09;04、字符串 slice 字符串 slice注意要点String 与 &str 的转换字符串深度剖析字符串 slice 作为函数参数例子001例子00…

SL8541 android系统环境+编译

1.Ubuntu系统的安装 最好使用ubuntu18.0.4 2.工具环境包的安装 // 安装Android8.1源码编译环境 sudo apt-get install openjdk-8-jdk --------------ok sudo apt-get install libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-dev g-multilib --------------ok sudo…

Git 保姆级使用教程

目录 一、Git介绍 二、Git 与 SVN 区别 三、Git 安装配置 1.Linux 平台上安装 Debian/Ubuntu Centos/RedHat 源码安装 2.Windows 平台上安装 3.Mac 平台上安装 四、安装完成后配置 五、创建版本仓库 六、Git常用命令 1.创建仓库命令 2.提交与修改 3.提交日志 4.…

高可用双机GPFS集群的的自动化部署脚本

1.环境说明&#xff1a; 系统主机名IP地址内存添加共享磁盘大小Centos7.9gpfs1192.168.10.1012G20GCentos7.9gpfs2192.168.10.1022G20G 2.配置共享硬盘&#xff1a; 前提&#xff1a;两台虚拟机没有拍摄快照 在mds001主机中&#xff1a; 添加五块5G的硬盘 SCSI > 创建新虚…

【Javascript】数组的基本操作

目录 声明 字面量形式 构造函数声明 访问数组中的元素 数组的长度 增删改查 增 通过索引添加数据 在数组后面添加数据 在数组前添加数据 删 删除数组中最后一个元素 删除数组中第一个元素 改 查 数组是⼀种列表对象&#xff0c;它的原型中提供了遍历和修改元素的…

力扣每日一题59:螺旋矩阵||

题目描述&#xff1a; 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,2,3],[8,9,4],[7,6,5]]示例 2&#xff1a; 输入&am…

专业安卓实时投屏软件:极限投屏(QtScrcpy作者开发)使用说明

基本介绍 极限投屏是一款批量投屏管理安卓设备的软件&#xff0c;是QtScrcpy作者基于QtScrcpyCore开发&#xff0c;主要功能有&#xff1a; 设备投屏&控制&#xff1a;单个控制、批量控制分组管理wifi投屏adb shell快捷指令文件传输、apk安装 更多功能还在持续更新。 极…

【C语言小游戏--猜数字】

文章目录 前言1.游戏描述2.代码实现2.1打印菜单2.2构建基础框架2.3玩游戏2.3.1生成随机数2.3.1.1rand()2.3.1.2srand()2.3.1.3time() 2.3.2game() 2.4自己设定猜的次数 3.完整代码 前言 猜数字小游戏是我们大多数人学习C语言时都会了解到的一个有趣的C语言小游戏&#xff0c;下…

Milk-V Duo移植rt-thread smart

前言 &#xff08;1&#xff09;PLCT实验室实习生长期招聘&#xff1a;招聘信息链接 &#xff08;2&#xff09;首先&#xff0c;我们拿到Milk-V Duo板子之后&#xff0c;我个人建议先移植大核Linux。因为那个资料相对多一点&#xff0c;也简单很多&#xff0c;现象也容易观察到…