Lab2: system calls
预备知识
执行一次系统调用的流程:
USER MODE
step1:系统调用声明
user/user.h
:系统调用函数(如int fork(void)
)
step2:ecall 进入内核态
user/usys.S
(该文件由user/usys.pl
生成,后续添加函数可以在这里添加):执行如下命令
.global fork
fork:
li a7, SYS_fork
ecall
ret
- 将系统调用的编号(在
kernel/syscall.h
中定义)写入a7
寄存器 - 从
ecall
进入中断处理函数
KERNEL MODE
step3:保存数据并跳转到中断判断函数
kernel/trampoline.S
:在uservec
中保存寄存器、切换到内核栈、切换栈指针SP等,最后跳转到内核指定的中断判断函数usertrap
(在kernel/trap.c
中)- 每一个进程都对应一个状态结构体
proc
(在kernel/proc.h
中定义这个结构体),将数据存储在这里
- 每一个进程都对应一个状态结构体
step4:中断判断函数
kernel/trap.c
:中断判断函数usertrap
用于处理来自用户态的中断、异常或系统调用,在此处判断是否是系统调用,如果是,则执行响应函数syscall
step5:执行对应的系统调用
kernel/syscall.c
:响应函数syscall
用于对号入座,根据给出的syscalls
表(将系统调用编号与执行函数相对应)获取系统调用类型(系统调用编号从a7
寄存器中读取),执行相应的函数(进入kernel/sysproc.c
中);系统调用的返回值传递给了a0
,后续会回传给用户态
step6:系统调用函数
kernel/sysproc.c
:保存着系统调用的执行函数,如果系统调用有参数,需要用argraw
(读取参数本质上是从内存中恢复,见kernel/syscall.c
)取出保存的寄存器数据,然后函数最终都将调用相对应的执行函数(在kernel/proc.c
中)
step7:系统调用核心功能
kernel/proc.c
:这里的函数用于执行核心功能,如创建进程等
添加系统调用需要考虑的地方
-
在
user/user.h
中添加系统调用的函数声明,并在user
中创建相应的系统调用(*.c
文件)(和lab1类似) -
在
user/usys.pl
中添加系统调用项 -
在
kernel/syscall.h
添加系统调用的编号 -
在
kernel/syscall.c
中的syscalls
表中添加映射关系,指出需要执行的函数 -
在
kernel/sysproc.c
和kernel/proc.c
中实现系统调用的执行函数
Part1:System call tracing
实现功能
-
添加一个系统调用的
trace
功能,在命令前输入trace <mask>
,能够打印出该命令使用的系统调用 -
mask = 1 << SYS_name
为一个整数,它能够指定跟踪哪个系统调用,SYS_name
是来自kernel/syscall.h
的一个系统调用号,mask
可以等于1 << SYS_name | 1 << SYS_other_name
-
如果在输入命令时使用
trace <mask>
,则在使用指定系统调用后应该返回一行包括进程id、系统调用名称和返回值
的打印信息 -
trace
要求能够跟踪进程以及进程派生出的子进程,且不影响其他进程
实验提示
-
在
user/user.h
、user/usys.pl
、kernel/syscall.h
添加系统调用以及编号 -
在
kernel/sysproc.c
添加一个sys_trace
函数实现新的系统调用,并在状态结构体proc
中插入一个新的变量(对该进程进行跟踪的mask
掩码),系统调用的执行函数参考kernel/sysproc.c
-
此外需要对
kernel/proc.c
的fork
函数进行修改,因为调用了trace
系统调用,会在当前进程状态proc
(在kernel/proc.h
中)中修改mask
掩码的设置,同时每次fork
时也需要对相关的子进程同步相关的设置 -
需要修改
kernel/syscall.c
下的syscall
使其打印追踪信息;为了打印系统调用名称,需要额外创建字符串数组
实验代码
-
在
user/user.h
中添加int trace(int);
的声明(trace接受一个整型的参数mask
) -
在
user/usys.pl
中添加entry("trace");
,这是进入内核态的入口 -
在
Makefile
中的UPROGS
添加_trace
-
进入内核态,在
kernel/syscall.h
添加系统调用的编号#define SYS_trace 22
-
在
kernel/syscall.c
中添加系统调用的映射extern uint64 sys_trace(void);
、[SYS_trace] sys_trace
-
在
kernel/proc.h
中的proc
中添加int mask
-
在
kernel/sysproc.c
中添加进入系统调用的函数
uint64
sys_trace(void){ // 参考sys_wait函数
uint64 p;
if(argaddr(0, &p) < 0) // 获取trace的参数(只有一个)
return -1;
return trace(p); // 系统调用的执行函数
}
- 在
kernel/proc.c
实现trace
的系统调用执行函数(仅仅是进行一个掩码的赋值)
int
trace(int mask){
struct proc *p = myproc();
p->mask = mask; // 将掩码赋值给结构体的mask
return 0;
}
- 在
kernel/defs.h
(这个里面包含了kernel
中常用函数的原型声明)中加入函数的声明int trace(int)
- 在
kernel/proc.c
中的fork
函数中加一行代码np->mask = p->mask;
,将父进程的 mask 赋值给子进程的 mask - 在
kernel/syscall.c
中对syscall
进行修改,打印追踪信息;并且为了打印系统调用的名称,创建一个字符串数组
char
*sys_name[] = {
"", "fork", "exit", "wait", "pipe", "read", "kill", "exec",
"fstat", "chdir", "dup", "getpid", "sbrk", "sleep", "uptime", "open",
"write", "mknod", "unlink", "link", "mkdir", "close", "trace"
};
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){ // 判断是否有mask输入
// 打印进程id、系统调用的名称和返回值
printf("%d: syscall %s -> %d", p->pid, sys_name[num], p->trapframe->a0); // 这里注意使用的prinrf是因为xv6的kernel中专门定义了一个printf的函数,在Linux内核中只能使用prinrk打印
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
p->mask >> num & 1
:p->mask 为 1<<SYS_read,num 为从 a7 寄存器中读出来的 SYS_read,p->mask >> num 能够将 mask 还原为 SYS_read,如果没有 mask,则不打印
实验感悟
- 在阅读源码的过程中,看到有些地方涉及到汇编语言,之后要把这块给学习一下,要不然很影响阅读体验
- 虽然按照系统调用的步骤能把整个流程走下来,但是每个函数以及它们之间的关系实际上还没有特别清楚,需要再进行梳理(将每个函数的作用都搞清楚)
- 在进行
proc
修改的时候,要考虑到fork
函数的子进程是否需要复制参数
Part2: Sysinfo
实现功能
- 添加一个系统调用
sysinfo
,通过系统调用将收集正在运行的程序的信息 - 在这个系统调用中将传入一个结构体指针
sysinfo
,结构体定义在kernel/sysinfo.h
中,其中freemem
应该设置为空闲内存的字节数,nproc
应该设置为进程状态不为UNUSED
的进程数
实验提示
- 在
user/user.h
中声明sysinfo()
的原型,你需要预先声明结构体sysinfo
的存在:struct sysinfo;
、int sysinfo(struct sysinfo *);
- sysinfo 需要将
struct sysinfo
复制回用户空间;参考sys_fstat() (kernel/sysfile.c)
和filestat() (kernel/file.c)
学习使用copyout()
- 要收集空闲内存量,可以在
kernel/kalloc.c
中添加一个函数 - 要收集进程数,请在
kernel/proc.c
中添加一个函数
实验代码
- 在
user/user.h
添加sysinfo
结构体和函数的声明struct sysinfo;
、int sysinfo(struct sysinfo *);
- 分别在
user/usys.pl
、Makefile
、kernel/syscall.h
、kernel/syscall.c
执行与上一个实验一样的步骤 - 在
kernel/kalloc.c
中实现空闲内存大小的查找
uint64
getfreemen(void){ // 获取空闲内存数量
struct run *rp;
uint64 result = 0;
acquire(&kmem.lock); // 考虑到并发问题,上锁
rp = kmem.freelist;
while(rp){
result += 1;
rp = rp->next;
}
release(&kmem.lock);
return result * PGSIZE; //一个内存页的大小为PGSIZE(4096)
}
- 在
kernel/proc.c
中实现进程数的统计
int
getproc(void){
struct proc *p;
int result = 0;
for(p = proc; p < &proc[NPROC]; p++){
acquire(&p->lock); //上锁
if(p->state != UNUSED){
result += 1;
}
release(&p->lock);
}
return result;
}
上述两个步骤都需要在
kernel/defs.h
中添加函数声明
- 在
kernel/sysproc.c
中实现执行函数sys_sysinfo
的功能,记得添加#include "sysinfo.h"
来使用sysinfo
结构体
uint64
sys_sysinfo(void){
struct sysinfo info;
struct proc *p;
uint64 addr;
if(argaddr(0, &addr) < 0) // 获取系统的指针参数
return -1;
p = myproc();
info.freemem = getfreemen();
info.nproc = getproc();
// 从内核copy到用户
if(copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0) //参考sys_fstat(在kernel/sysfile.c中)
return -1;
return 0;
}
实验感悟
- 这个实验实现的内存大小和进程数的统计函数需要了解了 kalloc.c 和 proc.c 的函数后才能写出来,我两个文件看的还不太完全,之后需要再看看
- 这个实验中也使用了指针,这一块使用还不太熟练,C 语言还需要精进