Lab2:系统调用

Lab2:系统调用

参考博客:https://blog.miigon.net/posts/s081-lab2-system-calls/

切换分支:syscall

用户程序 - - (系统调用) - -→ 内核

在操作系统中,系统调用是用户程序和内核进行交互的桥梁。用户程序无法直接操作硬件,不能直接访问内核提供的资源。系统调用运行用户程序通过受控接口向内核请求服务,保持系统的安全性和稳定性。

一切为了实现用户态和内核态的良好隔离

操作系统通过CPU的特权级模式,将用户态与内核态隔离,保证了用户程序无法直接访问内核资源,避免了误操作和恶意行为。

用户 → 库函数(系统调用)→ 触发软件中断or陷阱,引发CPU切换到内核态。

内核:根据系统调用号确定要执行的服务,完成后,将结果或错误码存入用户程序寄存器。

返回用户态继续执行程序。

在上述描述中,可以发现一直在强调用户态和内核态。

在xv6系统项目中,本实验也是围绕着 userkernel 两个文件夹来操作。

用户态中,我需要声明跳板函数。内核态中,写对应的系统调用映射(system call),这里是用的c语言的一种实现方式 static uint64 (*syscallsp[])(void) = {} ,不过原理就是通过状态码建立起与调用函数的映射关系。状态码通过 #define SYS_trace 22 实现,不过在实习中,和书中都提到过尽量避免使用define,而是选择typedef SYS_trace 22; 之类的实现方法。

如何从用户态切换到内核态?本项目使用的是Perl脚本,通过 entry("trace"); 的方式注册,大概是会被宏展开成一段汇编代码,生成 `usys.S 汇编文件,定义了每个 system call 的用户态跳板函数。

1
2
3
4
trace:		# 定义用户态跳板函数
li a7, SYS_trace # 将系统调用 id 存入 a7 寄存器
ecall # ecall,调用 system call ,跳到内核态的统一系统调用处理函数 syscall() (syscall.c)
ret # 返回用户态

实际上就是执行了 ecall 进入内核。

ecall(RISC-V 平台的系统调用指令),触发软件中断 trap,引导CPU进入内核态(S模式)。

ecall 触发异常,进入 kernel/trap.c 中的 usertrap() 处理。

usertrap() 发现是系统调用异常,调用 syscall() 处理。

syscall() 解析 a7 中的系统调用编号,并调用对应的 sys_exit() 等系统调用处理函数。

对于切换啰嗦了几句,再提一个更基础的知识点,就到下一个环节。

上下文切换:多个进程 or 线程切换控制权。寄存器状态、程序计数器PC、栈指针、页表;进程控制块PCB。

实验:System call tracing

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), where SYS_fork is a syscall number from kernel/syscall.h. You have to modify the xv6 kernel to print out a line when each system call is about to return, if the system call’s number is set in the mask. 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.

在本作业中,您将添加一个系统调用跟踪功能,该功能可能会在调试后续实验时为您提供帮助。您将创建一个新的跟踪系统调用,该调用将控制跟踪。它应该接受一个参数,一个整数 “mask”,其位指定要跟踪的系统调用。例如,为了跟踪 fork 系统调用,程序调用 trace(1 « SYS_fork),其中 SYS_fork 是 kernel/syscall.h 中的系统调用编号。如果在掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。该行应包含进程 ID、系统调用的名称和返回值;您无需打印系统调用参数。trace 系统调用应启用对调用它的进程及其随后分叉的任何子进程的跟踪,但不应影响其他进程。

实现配置部分省略~照搬参考博客

系统调用全过程

1
2
3
4
5
user/user.h:		用户态程序调用跳板函数 trace()
user/usys.S: 跳板函数 trace() 使用 CPU 提供的 ecall 指令,调用到内核态
kernel/syscall.c 到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理。
kernel/syscall.c syscall() 根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用。
kernel/sysproc.c 到达 sys_trace() 函数,执行具体内核操作
  • 内核态与用户进程的页表不同,寄存器也不互通,所以参数无法直接通过c语言参数的形式传过来,使用argaddr、argint、argstr等系列函数,从进程的trapframe中读取用户进程寄存器中的参数。
  • 内核态与用户进程的页表不同,指针也不能直接互通访问(不能直接对拿到的指针解引用),使用copyin、copyout方法结合进程的页表,顺利找到用户态指针(逻辑地址)对应的物理内存地址。

页表:记录虚拟页到物理页框的映射关系。

实验逻辑部分

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// kernel/proc.h
struct proc {
...
uint64 syscall_trace;
// Mask for syscall tracing (新添加的用于标识追踪哪些 system call 的 mask)
};

// kernel/proc.c
static struct proc*
allocproc(void)
{
p->syscall_trace = 0;
// (newly added) 为 syscall_trace 设置一个 0 的默认值
}

// kernel/sysproc.c
uint64
sys_trace(void)
{
int mask;

if(argint(0, &mask) < 0) // 通过读取进程的 trapframe,获得 mask 参数
return -1;

myproc()->syscall_trace = mask; // 设置调用进程的 syscall_trace mask
return 0;
}

// kernel/proc.c
int
fork(void)
{
np->syscall_trace = p->syscall_trace; //子进程继承父进程的 syscall_trace
}

// kernel/syscall.c
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]();
// 通过系统调用编号,获取系统调用处理函数的指针,调用并将返回值存到用户进程的 a0 寄存器中
// 如果当前进程设置了对该编号系统调用的 trace,则打出 pid、系统调用名称和返回值。
if((p->syscall_trace >> num) & 1) {
printf("%d: syscall %s -> %d\n",p->pid, syscall_names[num], p->trapframe->a0);
// syscall_names[num]: 从 syscall 编号到 syscall 名的映射表
}
}
else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

// kernel/syscall.c
const char *syscall_names[] = {
[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",
};

实验:Sysinfo

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”.
在此作业中,您将添加一个系统调用 sysinfo,用于收集有关正在运行的系统的信息。系统调用采用一个参数:指向 struct sysinfo 的指针(请参阅 kernel/sysinfo.h)。内核应该填写这个结构体的字段:freemem 字段应该被设置为空闲内存的字节数,nproc 字段应该被设置为状态不是 UNUSED 的进程数。我们提供了一个测试程序 sysinfotest;如果它打印 “sysinfotest: OK”,则通过此赋值。

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
// 获取空闲内存、获取运行的进程数
// kernel/defs.h
uint64 count_free_mem(void); // here
uint64 count_process(void); // here

// kernel/kalloc.c
uint64
count_free_mem(void) // added for counting free memory in bytes (lab2)
{
acquire(&kmem.lock); // 必须先锁内存管理结构,防止竞态条件出现

// 统计空闲页数,乘上页大小 PGSIZE 就是空闲的内存字节数
uint64 mem_bytes = 0;
struct run *r = kmem.freelist;
while(r){
mem_bytes += PGSIZE;
r = r->next;
}

release(&kmem.lock);

return mem_bytes;
}

// kernel/proc.c
uint64
count_process(void) { // added function for counting used process slots (lab2)
uint64 cnt = 0;
for(struct proc *p = proc; p < &proc[NPROC]; p++) {
// acquire(&p->lock);
// 不需要锁进程 proc 结构,因为我们只需要读取进程列表,不需要写
if(p->state != UNUSED) { // 不是 UNUSED 的进程位,就是已经分配的
cnt++;
}
}
return cnt;
}

// kernel/proc.c
uint64
sys_sysinfo(void)
{
// 从用户态读入一个指针,作为存放 sysinfo 结构的缓冲区
uint64 addr;
if(argaddr(0, &addr) < 0)
return -1;

struct sysinfo sinfo;
sinfo.freemem = count_free_mem(); // kalloc.c
sinfo.nproc = count_process(); // proc.c

// 使用 copyout,结合当前进程的页表,获得进程传进来的指针(逻辑地址)对应的物理地址
// 然后将 &sinfo 中的数据复制到该指针所指位置,供用户进程使用。
if(copyout(myproc()->pagetable, addr, (char *)&sinfo, sizeof(sinfo)) < 0)
return -1;
return 0;
}

// user.h
char* sbrk(int);
int sleep(int);
int uptime(void);
int trace(int);
struct sysinfo; // 这里要声明一下 sysinfo 结构,供用户态使用。
int sysinfo(struct sysinfo *);

草稿图

1

2

3

4


Lab2:系统调用
https://kevin-aron.github.io/categories/mit6.s081/Lab2-系统调用/
作者
Iuk
发布于
2025年3月17日
许可协议