0.Briefly Speaking

这是6.S081的第二个实验,目的主要是自己实现一些系统调用,上一个实验更多的是使用已有的设施去实现一些功能各异的程序,这需要我们对系统调用的过程有更加深入的理解。当然整个过程要全部了解,可能还需要做完Lab4:Traps Lab,在那个实验中将会具体探究xv6是如何借助陷阱来实现系统调用的。本实验的实验指导书在这里。

因为系统调用的具体实现逻辑在内核内部,所以在实现一个系统调用时需要修改和增加一些东西,帮助内核能够正确索引到对应的系统调用,具体来说添加一个系统调用有以下文件需要修改:

1.首先,系统调用必须提供用户态下的调用接口,所以必须在user.h文件下添加用户态函数原型

2.在usys.pl中添加一个stub,我们在Makefile中检查一个这个文件,发现usys.pl会用来生成usys.S文件,这个文件专门用来生成汇编代码段来将对应的系统调用号读入a7寄存器:

$U/usys.S : $U/usys.plperl $U/usys.pl > $U/usys.S

3.接下来需要在kernel/syscall.h文件中加入一个新的系统调用号,内核需要借助这个调用号索引到对应的内核例程。还需要在kernel/syscall.c文件中的syscall中加入从系统调用号到函数的索引关系,如下所示:

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_trace]   sys_trace,    // 注意新加入的两个系统调用和对应关系
[SYS_sysinfo] sys_sysinfo,
};

4.最后在sysproc文件中实现对应的系统调用(sys_trace,sys_sysinfo等),完成具体的功能。
以上的4个步骤是添加一个系统调用所必需的步骤,在完成本实验时要注意检查。在下面的记录中,就不再一一将上面的步骤描述出来,而是聚焦于问题本身的实现思路。

1.System call tracing (moderate)

第一个实验是实现一个可以追踪任意系统调用的系统调用(看起来有点拗口…),当设置了追踪掩码mask之后,对应编号的系统调用在触发时都会被追踪。并打印出如下的信息:

进程ID号:syscall xxx -> 返回值

实验书中指明,xv6已经实现了一个文件trace.c,这个文件和我们在上一个实验中实现的用户态程序是一样的,trace.c会调用trace系统调用来跟踪后面具体执行的命令行,所以这里也需要将_trace加入UPROGS字段,否则找不到这个应用程序。下面来具体实现一下。

其实指导书中暗示的都差不多了,我们就一步步照指示做事就好:
1.首先在表示每个进程的结构体proc(kernel/proc.h)中加入一个表示用于进程追踪的掩码,见下面的结构体尾部:

struct proc {struct spinlock lock;// p->lock must be held when using these:enum procstate state;        // Process statevoid *chan;                  // If non-zero, sleeping on chanint killed;                  // If non-zero, have been killedint xstate;                  // Exit status to be returned to parent's waitint pid;                     // Process ID// wait_lock must be held when using this:struct proc *parent;         // Parent process// these are private to the process, so p->lock need not be held.uint64 kstack;               // Virtual address of kernel stackuint64 sz;                   // Size of process memory (bytes)pagetable_t pagetable;       // User page tablestruct trapframe *trapframe; // data page for trampoline.Sstruct context context;      // swtch() here to run processstruct file *ofile[NOFILE];  // Open filesstruct inode *cwd;           // Current directorychar name[16];               // Process name (debugging)int TraceMask;               // <在这里加入一个新的属性,用于表示要追踪的进程掩码>
};

2.然后就是在sysproc.c的文件中真正地实现sys_trace的逻辑,这里涉及到用户态向内核态传递参数,这个过程中需要使用一些函数(argint, argaddr,argstr等)从trapframe中将用户态下传递的参数读取到内核态。所以在内核态下的系统调用函数,都是没有参数的,因为它们的参数都是从用户态下读进来的。上述的这些函数的关系如下,本质上都调用了argraw来解析参数:

argraw的逻辑非常简单,就是从对应的trapframe中返回a0-a7寄存器,因为根据RISC-V的calling convention,头几个寄存器是用来存放函数参数的。

// 以64位无符号整数返回寄存器a0-a7中的值
// 当n大于5的时候,函数错误,陷入panic
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里,发生系统调用后又如何从用户态过渡到内核态的,这个过程后面做到对应实验时再分析源码。sys_trace的实现非常简单,直接将用户态下传进来的追踪掩码存入当前进程的TraceMask(之前刚加入的变量属性)即可。

uint64
sys_trace(void)
{int n;if(argint(0, &n) < 0)             // 将trapframe->a0读入return -1;myproc()->TraceMask = n;          // set the TraceMask in proc structreturn 0;
}

3.因为trace系统调用还要能够追踪当前进程的子进程,所以在进程调用fork时,子进程应该将TraceMask一并复制过去,加入的代码我已经在下面用中文标出了。

int
fork(void)
{int i, pid;struct proc *np;struct proc *p = myproc();// Allocate process.if((np = allocproc()) == 0){return -1;}// Copy user memory from parent to child.if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){freeproc(np);release(&np->lock);return -1;}np->sz = p->sz;// <在这加一行代码,将TraceMask复制到子进程中去>np->TraceMask = p->TraceMask;// copy saved user registers.*(np->trapframe) = *(p->trapframe);// Cause fork to return 0 in the child.np->trapframe->a0 = 0;// increment reference counts on open file descriptors.for(i = 0; i < NOFILE; i++)if(p->ofile[i])np->ofile[i] = filedup(p->ofile[i]);np->cwd = idup(p->cwd);safestrcpy(np->name, p->name, sizeof(p->name));pid = np->pid;release(&np->lock);acquire(&wait_lock);np->parent = p;release(&wait_lock);acquire(&np->lock);np->state = RUNNABLE;release(&np->lock);return pid;
}

4.最后实现具体的追踪功能,因为追踪时需要打印出具体的系统调用名称,所以在syscall.h中还需要加一个系统调用号到名称的映射表:

static char* SyscallName[] = {[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",
};

然后就是修改执行系统调用的具体函数syscall:

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]();// 如果当前系统调用需要被追踪,则打印出对应的调用信息// 用&运算判断对应的位是否同为1if(p->TraceMask & (1 << num)){                                                       printf("%d: syscall %s -> %d\n", p->pid, SyscallName[num], p->trapframe->a0);    }} else {printf("%d %s: unknown sys call %d\n",p->pid, p->name, num);p->trapframe->a0 = -1;}
}

到此为止,以上4步就已经很好地完成了这个任务,验证一下实现是否正确:

这就是Lab2第一个小任务的具体实现思路。

2.Sysinfo (moderate)

这个任务是实现一个系统调用统计一些系统信息,包括:空闲内存的数量和不处于UNUSED状态的进程数量。和trace一样的,这里提供了一个用户态下的应用程序user/sysinfotest.c用来检测我们编写的系统调用的正确性。可以阅读一下测试程序的逻辑,学习一下,这里不再展开了。

还是要强调一下,不要忘记实现一个系统调用的必经4步骤,这些在最上面已经列出了,这里不再展开赘述。sysinfo暴露在用户态下的编程接口原型如下,需要在user.h中加入这个接口:

int sysinfo(struct sysinfo *);

struct sysinfo是一个结构体,它定义在kernel/sysinfo.h中,定义如下:

// sysinfo结构体中定义了空闲内存和不处于UNUSED态进程的数量
struct sysinfo {uint64 freemem;   // amount of free memory (bytes)uint64 nproc;     // number of process
};

下面直接进入到系统调用的实现,首先给出sysinfo系统调用整体的实现思路:

uint64
sys_sysinfo(void)
{uint64 UserSysinfo;struct sysinfo KernelSysinfo;if(argaddr(0, &UserSysinfo) < 0)return -1;KernelSysinfo.freemem = kcountFreeMemory();    // 统计空闲内存的量KernelSysinfo.nproc = countProc();             // 统计状态不是UNUSED的进程数量/* 将sysinfo结构体拷贝回用户态下 */struct proc *p = myproc(); if(copyout(p->pagetable, UserSysinfo, (char*)&KernelSysinfo, sizeof(struct sysinfo)) < 0)return -1;return 0;
}

首先通过argaddr系统调用,将用户传进来的指向结构体的指针读入变量UserSysinfo,记录这个地址是因为后面还要使用copyout函数将此变量从内核态拷贝回用户态。其中分别调用了两个函数分别用来计算空闲内存的数量和进程数量,下面来具体看看这两个函数的实现:

1.kcountFreeMemory函数
在实现这个函数之前,需要先阅读其他内存管理函数的实现,大致弄清xv6中物理内存的管理机制。
首先来看内存管理需要的两个数据结构,run和kmem。稍微有些数据结构基本功的人就可以看出这就是一个很简单的链表,事实上,xv6就是将空闲内存块串成了一个链表来管理,如下所示:

// 一个链表,这个链表每个结点都指向了一个空闲内存块的开头地址
struct run {struct run *next;
};// 一把自旋锁,防止并发时的访问冲突
// 空闲内存块组成的列表
struct {struct spinlock lock;struct run *freelist;
} kmem;

接下来主要阅读一下kmalloc和kfree函数的实现:

// 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)
{// 声明一个run类型的指针rstruct run *r;// 在从空闲链表中获取空闲内存块时,首先加一把自旋锁acquire(&kmem.lock);r = kmem.freelist;// 如果r不为空,则空闲链表还没有到达结尾if(r)kmem.freelist = r->next;release(&kmem.lock);// 随意填一些数据将当前页面填满if(r)memset((char*)r, 5, PGSIZE); // fill with junk// 返回新申请的页面return (void*)r;
}

kmalloc的实现思路非常简单,就是从一个空闲链表中取一块空闲内存,稍作初始化之后返回。
接下来看看kfree函数,也非常简单,就是将空闲页面使用“头插法”插入回空闲列表:

// 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.)
// 此函数一次释放一整个页面(4096bytes)大小的内存
void
kfree(void *pa)
{struct run *r;// 如果pa不能被页面大小整除,或范围超出合法边界,则陷入panicif(((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);
}

看懂了基本的物理内存管理方式,下面就可以看看kcountFreeMemory的实现,基本思路就是遍历freelist链表直至结尾即可,直接贴出代码:

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

2.countProc函数
这个函数最重要的就是在kernel/proc.c文件中,学习如何遍历当前进程列表。事实上,在同一文件下的其他函数中,经常能看到类似于以下代码的实现:

for(p = proc; p < &proc[NPROC]; p++) {...
}

这里的proc[NPROC]就是xv6的进程组,以上代码就完成了所有进程的遍历。据此,我们可以写出如下代码来统计进程组中正在被使用的进程,如下所示:

uint64
countProc()
{struct proc* p;uint64 NumberOfProcess = 0;for(p = proc ; p < &proc[NPROC] ; p++){acquire(&p->lock);if(p->state != UNUSED)NumberOfProcess++;release(&p->lock);}return NumberOfProcess;
}

至此我们就已经完整实现了sysinfo的所有功能,在此测试一下:

表明上述实现是正确的。

3.小结

本实验的目的是在xv6中实现系统调用,这个过程中涉及到一些xv6中系统调用的基本机制,如系统调用号、如何从用户态向内核传参数、如何将结果从内核空间再传回用户空间(copyout)等。但要更加深入的理解系统调用的全过程,还需要在后面继续完成实验和研究内核源码。

6.S081——Lab2——system calls相关推荐

  1. Lab system calls

    6s081 Lab2: system calls 一开始看的时候看见这两个实验都是moderate,还以为挺简单.结果因为对xv6 book不熟悉,没有弄明白整个系统调用的过程,花了很多的时间去理解x ...

  2. 6.S081-Lab 2: System Calls

    更好的阅读体验 官方材料:https://pdos.csail.mit.edu/6.S081/2021/labs/syscall.html 参考材料: https://th0ar.gitbooks.i ...

  3. 12 File and Device I/O using System Calls

    1 System Calls, File Management and Device I/O 1.用户使用C standard library,调用malloc() calloc(),真实的空间申请是 ...

  4. 20 Alarms, sigaction(), and Reentrant System Calls

    1 Alarm Signals and SIGALRM 1.1 Setting an alarm unsigned int alarm(unsigned int seconds); 过n秒后向进程发送 ...

  5. iTron3学习笔记(一) System Calls of Memory Pool Management Functions

    iTron3学习笔记(一)  System Calls of Memory Pool Management Functions 1.创建固定内存池(Create Fixed Memory Pool) ...

  6. [mit6.1810] Lab system calls

    文章目录 前言 Using gdb 题目 分析 system call tracing 题目 分析与代码 Sysinfo 题目 分析与代码 前言 在这个lab中我们会实现一些系统调用,这些系统调用类似 ...

  7. System Calls [LKD 05]

    无论什么系统,都会向user space提供一些interface,用来和kernel系统交互,从而可以实现某些特定功能,比如访问硬件,获取系统资源等等.通过定义好的interface访问系统,有助于 ...

  8. Lab2: System Call

    1. Preview 1.1 xv6-book Chap2 xv6-book的第二章和lecture3的内容类似,主要介绍了操作系统的组织结构,从物理资源的抽象.用户态/内核态.系统调用.微内核/宏内 ...

  9. System calls

    Kernel programming is vital for as long as new hardware is being designed and produced or old-obsole ...

最新文章

  1. C语言实现通用链表初步(四)----双向链表
  2. 与Linus Torvalds“并列”,虚拟化天才程序员法布里斯贝拉
  3. python文件独特行数_python——文件和数据格式化练习题:文件独特行数
  4. Qt 模态和非模态窗口的创建与关闭
  5. Cuda Graph (cuda 优化)
  6. coreseek mysql_centos+php+coreseek+sphinx+mysql之一coreseek安装篇
  7. 2019年5月个人总结:大家都在跨界,原谅自己的懈怠
  8. Linux中bin文件的解压
  9. Revel敏捷后台开发框架
  10. 《西部世界》后续,研究人类学能让AI拥有灵魂?
  11. 自动擦窗机器人作文_清洁机器人作文(六篇)
  12. H3C服务器web怎么修改密码,h3c路由器怎么修改密码_h3c路由器找回密码
  13. 从员工到总经理的成长笔记:自慢(6)
  14. 2022-05-19 列式数据库-Clickhouse
  15. 【不就是java设计模式吗】设计模式七大原则,用代码对比方式,化抽象为具体,实打实的教会你
  16. GreeNC:植物lncRNA数据库
  17. 蒋哲远 java_半场:蒋哲远射造威胁,亚泰0-0建业
  18. 写给程序员的UI设计书 (转) (二)
  19. Linux:MySQL:重启服务细节
  20. 中国移动规范学习——4A技术要求(集中认证)

热门文章

  1. 微信小程序——image图片自适应
  2. c语言三种方法求n的k次方
  3. VC助手破解,含VC2010
  4. mysql5.1 数据类型
  5. 清华汪玉等电子设计自动化ML论文综述:180篇文献、ACM TODAES接收
  6. [并行计算]Matlab并行计算工具箱(Parallel Computing Toolbox)官方文档教程中文版(1)
  7. pdfh5证书过期解决办法
  8. mysql取出另外一张表的数据_mysql从一张表中取出数据插入到另一张表
  9. 10677 我们仍未知道那天所看见的花的名字
  10. LocalDateTime获取当日00:00、结束时间23.59与当月第一天00.00,月末最后一天23.59