Operating system oganization

Before you start coding, read Chapter 2 of the xv6 book, and Sections 4.3 and 4.4 of Chapter 4, and related source files:

  • The user-space code for systems calls is in user/user.h and user/usys.pl.
  • The kernel-space code is kernel/syscall.h, kernel/syscall.c.
  • The process-related code is kernel/proc.h and kernel/proc.c.

这章主要是讲了操作系统为了兼顾并发性、隔离性、交互性做出的基本架构。

Kernel organization

宏内核与微内核

操作系统一个很重要的设计问题就是,哪部分的代码需要run在内核态,哪部分的需要run在用户态。

如果将操作系统所有系统调用统统都在内核态run,这种设计方式就叫宏内核monolithic kernel

如果仅将系统调用中必要的部分在内核态run,其他部分都在用户态run,并且采取Client/Server这样的异步通信方式,这种设计方式就叫微内核microkernel

image-20230107232802540

由于客户/服务器(Client/Server)模式,具有非常多的优点,故在单机微内核操作系统中几乎无一例外地都采用客户/服务器模式,将操作系统中最基本的部分放入内核中,而把操作系统的绝大部分功能都放在微内核外面的一组服务器(进程)中实现。

在微内核中,内核接口由一些用于启动应用程序、发送消息、访问设备硬件等的低级功能组成。这种组织允许内核相对简单,因为大多数操作系统驻留在用户级服务器中。

像大多数Unix操作系统一样,Xv6是作为一个宏内核实现的。因此,xv6内核接口对应于操作系统接口,内核实现了完整的操作系统。

Code: xv6 organization

XV6的源代码位于kernel子目录中,源代码按照模块化的概念划分为多个文件,图2.2列出了这些文件,模块间的接口都被定义在了def.hkernel/defs.h)。

文件 描述
bio.c 文件系统的磁盘块缓存
console.c 连接到用户的键盘和屏幕
entry.S 首次启动指令
exec.c exec()系统调用
file.c 文件描述符支持
fs.c 文件系统
kalloc.c 物理页面分配器
kernelvec.S 处理来自内核的陷入指令以及计时器中断
log.c 文件系统日志记录以及崩溃修复
main.c 在启动过程中控制其他模块初始化
pipe.c 管道
plic.c RISC-V中断控制器
printf.c 格式化输出到控制台
proc.c 进程和调度
sleeplock.c Locks that yield the CPU
spinlock.c Locks that don’t yield the CPU.
start.c 早期机器模式启动代码
string.c 字符串和字节数组库
swtch.c 线程切换
syscall.c Dispatch system calls to handling function.
sysfile.c 文件相关的系统调用
sysproc.c 进程相关的系统调用
trampoline.S 用于在用户和内核之间切换的汇编代码
trap.c 对陷入指令和中断进行处理并返回的C代码
uart.c 串口控制台设备驱动程序
virtio_disk.c 磁盘设备驱动程序
vm.c 管理页表和地址空间

图2.2:XV6内核源文件

Process overview

内核用来实现进程的机制包括用户态内核态标志、地址空间和进程的时间切片。

为了帮助加强隔离,进程抽象给程序提供了一种错觉,即它有自己的专用机器。进程为程序提供了一个看起来像是私有内存系统或地址空间的东西,其他进程不能读取或写入。进程还为程序提供了看起来像是自己的CPU来执行程序的指令。

Xv6使用页表(由硬件实现)为每个进程提供自己的地址空间。RISC-V页表将虚拟地址(RISC-V指令操纵的地址)转换(或“映射”)为物理地址(CPU芯片发送到主存储器的地址)。

每个进程也有自己的页表,页表中记录了以虚拟地址0开始的内存区域。

image-20230107233741922

xv6内核为每个进程维护许多状态片段,并将它们聚集到一个proc(*kernel/proc.h*:86)结构体中。一个进程最重要的内核状态片段是它的页表、内核栈区和运行状态。我们将使用符号p->xxx来引用proc结构体的元素;例如,p->pagetable是一个指向该进程页表的指针。

这应该相当于pcb表。

Code: starting xv6 and the first process

看完一遍说实话还乱乱的。。。。我整理整理跟linux的对比学习一下吧。

xv6

加载操作系统

系统加电,启动BIOS初始化硬件 -> BIOS从引导扇区将加载程序读入内存 -> 加载程序将操作系统镜像读入内存RAM。

这个过程由qemu模拟。

首先会通过mkfs造出操作系统镜像。然后由qemu将引导扇区,也即下面的filesys这图里的第0块:

image-20230121162324747

读入到主存中,然后开始执行引导扇区的程序,下同。

boot loader目的是把xv6加载进内存到0x8000 0000,然后跳转到xv6初始化程序。

The reason it places the kernel at 0x80000000 rather than 0x0 is because the address range 0x0:0x80000000 contains I/O devices.

操作系统初始化

entry.S配置栈空间

此时,目前的机器状态是,1.没有开启地址映射,也即虚拟地址=真实物理地址。2.运行在machine mode

xv6会在kernel/entry.S下的这里开始执行,目的是配置好栈,以开始C语言代码start.c的执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.global _entry
_entry:
# set up a stack for C.
# 这段主要是在计算栈顶指针sp
# stack0 is declared in start.c,
# with a 4096-byte stack per CPU.
# sp = stack0 + (hartid * 4096)
la sp, stack0
li a0, 1024*4
csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
# 已经有栈了,就可以开始执行C语言代码了
# jump to start()
call start

其中start0:

1
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
start.c

在start.c中,我们的任务是在machine mode下,获取machine mode才能访问到的硬件参数,做在machine mode 下才能做的时钟初始化【 it programs the clock chip to generate timer interrupts】,然后进行machine mode到内核态的切换,最后跳转到main.c进行操作系统的初始化和第一个进程的启动。

而其中,如果想从machine mode切换到内核态,就需要使用mret指令。但是mret指令除了会切换mode之外,还有一个“ret”的作用,并且是从machine mode ret到内核态。

This instruction( mret ) is most often used to return from a previous call from supervisor mode to machine mode.

所以,我们实际上可以把最后两步连起来,用mret一个指令就完成。也即,mret指令既完成了从machine mode到内核态的切换,又完成了从start.c到main.c的跳转。

这其实很容易,只需在栈中将调用者(此时应该是entry.S)的地址替换为main.c的地址,并且将调用者的mode改为内核态,这样就ok了。

it sets the previous privilege mode to supervisor in the register mstatus, it sets the return address to main by writing main’s address into the register mepc, disables virtual address translation in supervisor mode by writing 0 into the page-table register satp, and delegates all interrupts and exceptions to supervisor mode

后面两点不大明白。为什么为了mret,就还得让内核态跟machine mode一样关闭虚拟地址映射,还得把什么中断和异常委托给内核态??

【我猜测是因为现在页表还没初始化好所以当然得关闭虚拟地址映射();后者大概是开中断的意思?】

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// entry.S jumps here in machine mode on stack0.
void
start()
{
//修改调用者为内核态
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);

// set M Exception Program Counter to main, for mret.
// requires gcc -mcmodel=medany
w_mepc((uint64)main);

// disable paging for now.
w_satp(0);

// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);

// ask for clock interrupts.
timerinit();

// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);

// switch to supervisor mode and jump to main().
asm volatile("mret");
}
main.c

main.c的作用是做很多很多init。其中,它通过userinit();来创建第一个进程,这个第一个进程再由main调用scheduler()来被调度执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void
main()
{
if(cpuid() == 0){
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
//...很多很多init
userinit(); // first user process
__sync_synchronize();
started = 1;
} else {
while(started == 0)
;
__sync_synchronize();
/*
RISC-V的处理器对底层提供了一种特殊的抽象,Hardware Thread,简称为Hart。简单来说,Hart是真实物理CPU(bare metal)提供的一种模拟
*/
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}

//调用第一个scheduler,完成对scheduler线程的初始化,并且调度去执行第一个进程
scheduler();
}

注:关于里面的cpuid,我查了一下,指的是CPU的序列号,用来唯一标识cpu的。我想这个if架构的目的应该跟fork()==0差不多。也就是说,一开始的那个init仅有cpuid==0的CPU执行,其他的CPU就乖乖wait,只有CPU0执行初始化的程序。等到CPU0执行完所有init,才置标记位start=1,然后通过条件变量start控制抢占调度,轮流初始化自己。其中__sync_synchronize是GNU内置指令,起内存屏障作用。在竞赛中深刻地了解过了内存屏障,在这里再次跟老熟人再会感觉还是很有意思的。

proc.c中的userinit()

userinit的作用就是新创建一个进程信息proc,然后开始给第一个程序(initcode)填信息填入proc。这个进程创建完后,在main中的scheduler被调度执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void
userinit(void)
{
struct proc *p;

p = allocproc();
initproc = p;

// 申请一页,将initcode的指令和数据放进去
// allocate one user page and copy initcode's instructions
// and data into it.
uvmfirst(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;

//为内核态到用户态的转变做准备
// prepare for the very first "return" from kernel to user.
/*
Trap Frame是指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构
*/
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer

// 修改进程名
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");

//这个也许是为了能被优先调度
p->state = RUNNABLE;

release(&p->lock);
}
initcode.S

以上程序都位于kernel/下。这个位于user/下。

它调用exec系统调用进入了内核态。当exec完成后,它就跳转到了用户态user/init.c中。【这里估计又用了修改返回地址的trick】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.globl start
start:
la a0, init
la a1, argv
li a7, SYS_exec
ecall
# char init[] = "/init\0";
init:
.string "/init\0"

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

在init.c中,创建了console设备文件,打开了012文件描述符,并且fork了一个子进程,开始执行shell。这样一来,操作系统就完成了全部的启动。

感想

我的疑点有三个:

  1. 见start.c

  2. 是怎么完成从内核态到用户态的切换的?是执行了return就会自动切换吗?userinit中设置了initcode的信息为用户态的,然后就直接能进入用户态,这里感觉有点模糊。

    其实用户态和内核态本质上好像差别不大,似乎也就只有两方面,一个是页表(虚拟地址),另一个就是权限问题了。前者很好说,在main.c中完成了页表初始化,开启了虚拟地址:

    1
    2
    kvminit();       // create kernel page table
    kvminithart(); // turn on paging

    后者的话,从用户态切到内核态使用ecall指令,从machine mode到内核态需要修改mstatus寄存器并且使用mret指令:

    1
    2
    3
    4
    5
    6
    7
    8
    // set M Previous Privilege mode to Supervisor, for mret.
    unsigned long x = r_mstatus();
    x &= ~MSTATUS_MPP_MASK;
    x |= MSTATUS_MPP_S;
    w_mstatus(x);
    ...
    // switch to supervisor mode and jump to main().
    asm volatile("mret");

    因而从内核态切换到用户态应该也是需要类似这段对mstatus寄存器的修改的,并且其对应修改的是sstatus寄存器。

    但是,我只在普通的用户态-trap入内核态-用户态这个过程的usertrapret中看到对sstatus寄存器的写入,并没有在init的时候对这个玩意进行写入。

    所以,最后,我初步猜测,是会在scheduler()中的上下文切换中修改sstatus寄存器的内容为user mode,从而实现由内核态向用户态进程(initcode)的切换。不过这也仅仅是【猜想】,因为我并没有在switch的汇编代码中看到对sstatus的修改。真是令人麻木。。。

步骤十分直接且有理由:

加载操作系统——为了能执行C语言需要一个栈,所以得执行造一个的代码,然后再进入C语言zone——做点machine mode才能做的事,然后从machine mode切换到内核态——做点内核态才能做的事,从内核态切换到用户态

linux0.11

bootsect -> setup -> head.s ->main.c

加载操作系统

系统加电,启动BIOS初始化硬件 -> BIOS从引导扇区将加载程序读入内存 -> 加载程序将操作系统镜像读入内存RAM。

其中,第二三步做进一步的细化。

读入bootsect.s

加载程序的512个字节被读入到内存从0x7c00开始的一段内存中,并且BIOS设置CS=07c0,ip=0,开始执行加载程序的每一条指令。

bootsect.s

加载程序的代码为bootsect.s。在bootsect.s中,首先将自身从7c00处移动到了9000处【留下空间放操作系统】,然后分别依次读取磁盘的setupsystem模块,最后bootsect将控制权转交给setup。

setup.s

setup首先获取操作系统运行的必要硬件参数

image-20230108011824655

再然后,将system代码移到0地址。然后,我们就需要进入system代码块。

image-20230108012316631

最后一句jmpi指令本来应该是要跳到system代码段首0地址处的的,可此处却跳到了80处,这显然不合理。但它写的肯定是没错的。之所以会有这样的矛盾,是因为setup在此之前,还做了一件事情:改变寻址方式。jmpi上面的那条mov指令便做了这点。

我们之前的寻址方式一直是cs<<4+ip。但是这东西只能是16位的内存,无法满足寻址需求。故而setup要从16位切换到32位。32位模式也叫保护模式。

至于怎么切的呢?要注意到一点,改变寻址方式也即改变cs和ip的地址计算方法,也即换一条硬件电路实现。计算机给我们提供了一个简单的方式操纵保护模式的转变,即修改cr0寄存器的内容。

在保护模式下,寻址方式发生了改变。此时cs不再代表基址,而是表示地址在gdb表global description table中的偏移下标。真正的基址放在表项中。cs被称为selector,从表中取得基址,再和ip加在一起得到地址。

gdt表的内容由setup初始化

image-20230108013156116

这样一来,就正确跳到了system模块。

操作系统初始化

head.s

跳到system第一个文件,也就是head.s去执行。

head.s也是在保护模式下进行的,是在保护模式下的初始化。

head.s建立了真正的gdt表,然后就要跳转到main.c执行初始化和Shell的启动。此处有汇编语言和C语言的转化,也就是push参数然后push main的地址。

main.c

对各种东西的初始化。

image-20230108013849664

最后完成从内核态到用户态的切换。

感想

linux0.11的启动的具体思路是:

加载操作系统,获取硬件参数,进入保护模式,跳转到操作系统第一行代码——操作系统初始化,切换到用户态

linux0.11相比于xv6更加复杂,上课的时候隐藏了很多实现细节但依旧理解很费劲(。

这两个步骤思路其实都是差不多的,区别在于linux0.11好像没有machine mode这个概念。感觉也不能锐评什么,因为看完了感觉两个都很有道理,两个都一样很难懂(。

【注:为什么没有machine mode呢?是因为这个mode的划分是RISC-V架构做的,而linux0.11是基于X86架构。】

不过linux0.11这里进入保护模式后改变寻址方式是因为机器问题(好像是),xv6难道也是因为硬件问题吗?因为一开始的时候操作系统还未进行内存分页页表初始化,所以用不了地址映射?有待学习。

关于保护模式,可以看看这篇文章,今天太晚了先睡了:

Linux从头学08:Linux 是如何保护内核代码的?【从实模式到保护模式】

Real world

现实中,大多数操作系统都会兼顾宏内核与微内核。

大多数操作系统都支持与xv6类似的process进程概念,也有很多系统还支持线程概念。

Lab system calls

To start the lab, switch to the syscall branch:

1
2
3
$ git fetch
$ git checkout syscall
$ make clean

trace

In this assignment you will add a system call tracing feature that may help you when debugging later labs.

You’ll create a new trace system call that will control tracing. It should take one argument, an integer “mask”, whose bits specify which system calls to trace.

For example, to trace the fork system call, a program calls trace(1 << SYS_fork). You have to modify the xv6 kernel to print out a line when each system call is about to return. The line should contain the process id, the name of the system call and the return value; you don’t need to print the system call arguments.

The trace system call should enable tracing for the process that calls it and any children that it subsequently forks, but should not affect other processes.

感想

一开始为了把trace做得封装性良好一些尽量不改别的代码,想了好久好久,最后就只能想出,在syscall.c获取系统调用返回值处加个条件打印,在trace中维护一个map,映射进程pid和进程当前的mask,并且给外界提供查询当前进程是否对某个系统调用有mask作为syscall条件打印的接口。

这个最后还是失败了,失败的点在于不知道要创建多大的数组来作为map映射所有进程,因为pid分配估计是递增的,是会超过最大进程数的,所以pid会是多少是不确定的。还有一点就是fork之后子进程不能自动继承父进程的mask,还得手动调用一下trace,这更加不封装了(。

总之先放上我原来的代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// in kernel/syscall.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "syscall.h"
#include "defs.h"

//...

void strcpy(char* buf,const char* tmp){
int i=0;
while((*tmp)!='\0'){
buf[i++] = *tmp;
tmp++;
}
buf[i] = '\0';

}

void getname(int callid,char* buf){
switch(callid){
case SYS_fork: strcpy(buf,"fork"); break;
case SYS_exit: strcpy(buf,"exit"); break;
case SYS_wait: strcpy(buf,"wait"); break;
case SYS_pipe: strcpy(buf,"pipe"); break;
case SYS_read: strcpy(buf,"read"); break;
case SYS_kill: strcpy(buf,"kill"); break;
case SYS_exec: strcpy(buf,"exec"); break;
case SYS_fstat: strcpy(buf,"fstat"); break;
case SYS_chdir: strcpy(buf,"chdir"); break;
case SYS_dup: strcpy(buf,"dup"); break;
case SYS_getpid: strcpy(buf,"getpid"); break;
case SYS_sbrk: strcpy(buf,"sbrk"); break;
case SYS_sleep: strcpy(buf,"sleep"); break;
case SYS_uptime: strcpy(buf,"uptime"); break;
case SYS_open: strcpy(buf,"open"); break;
case SYS_write: strcpy(buf,"write"); break;
case SYS_mknod: strcpy(buf,"mknod"); break;
case SYS_unlink: strcpy(buf,"unlink"); break;
case SYS_link: strcpy(buf,"link"); break;
case SYS_mkdir: strcpy(buf,"mkdir"); break;
case SYS_close: strcpy(buf,"close"); break;
case SYS_trace: strcpy(buf,"trace"); break;
default: return;
}
}

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]();
char buf[32];
getname(num,buf);
// 在此处添加条件打印
if(istraced(num))
printf("syscall %s -> %d\n",buf,p->trapframe->a0);
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// in kernel/trace.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "defs.h"
#include "elf.h"

int m_mask[NPROC];

int
trace(int mask){
struct proc *p = myproc();
m_mask[p->pid] = mask;
return 1;
}

//提供给外界查询的接口
int
istraced(int callid){
struct proc *p = myproc();
//printf("from trace ,pid = %d\n",p->pid);
if(((m_mask[p->pid] >> callid) & 1) == 1){
return 1;
} else{
return 0;
}
}

下面是按照hints修改后的正确代码。

代码步骤

实际上,标答跟我的思路差不多,只不过它没有像我一样创建数组作为map,而是在proc结构体里添加了一个属性,这本质上也是利用了map。

在各种文件添加签名
user/user.h
user/usys.pl
syscall.h

添加系统调用号

syscall.c

添加系统调用号和sys_trace映射

修改Makefile
  1. 在第一个OBJS添加trace.o
  2. 在UPROGS添加user中的trace
代码
修改proc.h
1
2
3
4
5
// Per-process state
struct proc {
// ...
int mask; //记录trace的mask
};
编写trace.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "defs.h"
#include "elf.h"

int
trace(int mask){
struct proc *p = myproc();
p->mask = mask;
return 1;
}

int
istraced(int callid){
struct proc *p = myproc();
if(((p->mask >> callid) & 1) == 1){
return 1;
} else{
return 0;
}
}
修改syscall.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// in kernel/syscall.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "syscall.h"
#include "defs.h"

//...

void strcpy(char* buf,const char* tmp){
int i=0;
while((*tmp)!='\0'){
buf[i++] = *tmp;
tmp++;
}
buf[i] = '\0';

}

void getname(int callid,char* buf){
switch(callid){
case SYS_fork: strcpy(buf,"fork"); break;
case SYS_exit: strcpy(buf,"exit"); break;
case SYS_wait: strcpy(buf,"wait"); break;
case SYS_pipe: strcpy(buf,"pipe"); break;
case SYS_read: strcpy(buf,"read"); break;
case SYS_kill: strcpy(buf,"kill"); break;
case SYS_exec: strcpy(buf,"exec"); break;
case SYS_fstat: strcpy(buf,"fstat"); break;
case SYS_chdir: strcpy(buf,"chdir"); break;
case SYS_dup: strcpy(buf,"dup"); break;
case SYS_getpid: strcpy(buf,"getpid"); break;
case SYS_sbrk: strcpy(buf,"sbrk"); break;
case SYS_sleep: strcpy(buf,"sleep"); break;
case SYS_uptime: strcpy(buf,"uptime"); break;
case SYS_open: strcpy(buf,"open"); break;
case SYS_write: strcpy(buf,"write"); break;
case SYS_mknod: strcpy(buf,"mknod"); break;
case SYS_unlink: strcpy(buf,"unlink"); break;
case SYS_link: strcpy(buf,"link"); break;
case SYS_mkdir: strcpy(buf,"mkdir"); break;
case SYS_close: strcpy(buf,"close"); break;
case SYS_trace: strcpy(buf,"trace"); break;
default: return;
}
}

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]();
char buf[32];
getname(num,buf);
// 在此处添加条件打印
if(istraced(num))
printf("syscall %s -> %d\n",buf,p->trapframe->a0);
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
在sysproc.c中添加系统调用
1
2
3
4
5
6
7
8
9
uint64
sys_trace(void)
{
int mask;
if(argint(0,&mask)<0)
return -1;
trace(mask);
return 0;
}
修改fork

继承父进程的mask

1
np->mask = p->mask;
在defs.h中添加需要public的函数签名
1
2
3
// trace.c
int trace(int);
int istraced(int);

sysinfotest

In this assignment you will add a system call, sysinfo, that collects information about the running system.

The system call takes one argument: a pointer to a struct sysinfo (see kernel/sysinfo.h).

The kernel should fill out the fields of this struct: the freemem field should be set to the number of bytes of free memory, and the nproc field should be set to the number of processes whose state is not UNUSED.

We provide a test program sysinfotest; you pass this assignment if it prints “sysinfotest: OK”.

1
2
3
4
5
// kernel/sysinfo.h
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};

感想

代码

系统调用要做的事情同上。

有一个我在hit实验没想到,在这里依然没有想到的点是,参数的指针来自用户空间,所以不能直接对其指向的空间进行写入,需要借助copyout函数。

还有一件事,就是不知道该怎么统计free mem的数量,后来在hints提示下才知道要去kalloc.c中找。【之前只找过了vm.c】这里其实是很后悔提前看了提示的。我应该先去看一下上面关于kernel各个文件用途的笔记,再去继续自己找的,不能太过依赖提示。

还有一点做的不好的地方是,标答是选择了将两个计数函数放在各自的文件中,我是选择直接将成员变量在头文件中extern 公开出来,比如说在proc.h中这么写:

1
extern struct proc proc[NPROC];

hints采取了比我封装性更好的操作,这也是非常顺理成章的,我没有想到这样真是有点惭愧(。

总而言之,这个还是挺简单的,就是我很后悔我心浮气躁看了提示,要不然收获会更多。

sysinfo.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "defs.h"
#include "elf.h"
#include "sysinfo.h"

int
sysinfo(struct sysinfo* info){
struct sysinfo res;
res.nproc = countproc();
res.freemem = countfree();
struct proc *p = myproc();
if(copyout(p->pagetable, (uint64)info,(char *)(&res), sizeof(res)) != 0)
return -1;
return 1;
}
sysproc.c中
1
2
3
4
5
6
7
uint64
sys_sysinfo(void){
uint64 addr;
if(argaddr(0, &addr) < 0)
return -1;
return sysinfo((struct sysinfo*)addr);
}
kalloc.c中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 采用的是链表结构,run代表一页
struct run {
struct run *next;
};

struct {
struct spinlock lock;
// 指向第一个空闲页
struct run *freelist;
} kmem;

int
countfree(){
int npage = 0;
struct run* r = kmem.freelist;
while(r){
r = r->next;
npage++;
}
return npage*PGSIZE;
}
proc.c中
1
2
3
4
5
6
7
8
9
10
11
struct proc proc[NPROC];
int
countproc(){
int nproc = 0;
for(int i=0;i<NPROC;i++){
if(proc[i].state != UNUSED){
nproc++;
}
}
return nproc;
}

附加题

trace plus

Print the system call arguments for traced system calls.

这个实现起来要说简单也简单,麻烦也麻烦。这里就先摆了【实际上尝试了半小时发现太烦了看别人写的也不大满意就放弃了】

sysinfo plus

Compute the load average and export it through sysinfo

说实话没太看懂,不就加个 running process/ncpu就行了吗?