Lab4-challenge实验报告

由于课程改革,本学期OS课程的lab4挑战性任务与往年完全不一样,题目要求如下:Lab4 Challenge 题干

附:

  • 本学期所有指导书链接2023 OS 指导书【全】

  • 小女子挑战性任务的PPT基本按照如下实验报告展开,但是由于答辩时间限制做了删减,所以仍然以实验报告为参考主体。

    【想要看看小女子源码PPT的友友们,可以在博客下面留言或者私信我,当然我的菜菜源码估计难以入眼,还望不要嫌弃~】

一、任务实现思路

1、概览

本任务我一共修改了12个文件,分别是:在include文件夹下的env.h、signal.h、syscall.h,kern文件夹下的env.c、genex.S、syscall_all.c、tlbex.c,在user文件夹下的include.mk,在user/include文件夹下的lib.h,在user/lib文件夹下的fork.c、signal.c、syscall_lib.c,整体实现思路仿照TLB_mod的异常处理思路完成,其中涉及到了用户态和内核态的切换及函数处理和跳转。

2、信号变量的定义

1)include/signal.h

在include文件夹中新建一个文件signal.h,其中定义了 struct sigset_tstruct sigactionstruct sigstack 结构体,其中 struct sigstack 是用来存储进程接收到的信号的链表节点:

1
2
3
4
struct sigstack{
int sig;
struct sigstack *next;
};

2)include/env.h

在结构体 struct Env 中加入有关信号处理的属性:

1
2
3
4
5
6
7
8
9
10
11
struct Env {
//......

//Lab4 challenge
u_int env_handlers[65];
sigset_t env_sa_mask;
struct sigstack *env_sig_stack;
u_int env_sig_entry;
u_int env_sig_flag;
u_int env_protect[256];
};

3、信号函数的定义与实现

在user/include/lib.h定义信号相关的用户处理函数,并在user/lib/signal.c中实现上述函数。

1)第一类:需要系统调用的信号函数

其中,函数 sigactionsigprocmaskkill 由于需要改变进程控制块中的信号属性,所以需要利用系统调用陷入内核态进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact) {
if (signum > 64 || signum < 1) {
return -1;
}
if (syscall_get_sig_act(0, signum, oldact) != 0) {
//旧的信号处理结构体则需要在 `oldact != NULL` 时保存该指针在对应的地址空间中
return -1;
}
if (env_set_sig_entry() != 0) { //【注意这里的代码后续会解释!】
return -1;
}
return syscall_set_sig_act(0, signum, act);
}

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset) {
return syscall_set_sig_set(0, how, set, oldset);
}

int kill(u_int envid, int sig) {
return syscall_kill(envid, sig);
//向进程控制号编号为 `envid` 的进程发送 `sig` 信号
}
附:信号函数相关的系统调用

上述signal.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
int sys_kill(u_int envid, int sig) {
if (sig > 64 || sig < 1) {
return -1;
}
struct Env *env;
if (envid2env(envid, &env, 0) < 0) {
return -1;
}

for (int i = 0; i < 4096; i++) {
if (SIGstack[i].next == NULL) {
SIGstack[i].sig = sig;
SIGstack[i].next = env->env_sig_stack;
env->env_sig_stack = &(SIGstack[i]);
break;
}
}
return 0;
}

int sys_set_sig_act(u_int envid, int signum, struct sigaction *act) {
struct Env *env;
if (envid2env(envid, &env, 0) < 0) {
return -1;
}

env->env_handlers[signum] = (u_int)act->sa_handler;
env->env_sa_mask = act->sa_mask;
return 0;
}

int sys_set_sig_set(u_int envid, int how, sigset_t *set, sigset_t *oldset) {
struct Env *env;
try(envid2env(envid, &env, 0));
if (oldset != NULL) {
//当oldset不为NULL时,还需将原有的信号掩码放在oldset指定的地址空间中
oldset->sig[0] = env->env_sa_mask.sig[0];
oldset->sig[1] = env->env_sa_mask.sig[1];
}
switch (how) {
case SIG_BLOCK: //将set参数中指定的信号添加到当前进程的信号掩码中
env->env_sa_mask.sig[0] |= set->sig[0];
env->env_sa_mask.sig[1] |= set->sig[1];
break;
case SIG_UNBLOCK: //将set参数中指定的信号从当前进程的信号掩码中删除
env->env_sa_mask.sig[0] &= (~set->sig[0]);
env->env_sa_mask.sig[1] &= (~set->sig[1]);
break;
case SIG_SETMASK: //将当前进程的信号掩码设置为set参数中指定的信号集
env->env_sa_mask.sig[0] = set->sig[0];
env->env_sa_mask.sig[1] = set->sig[1];
break;
default:
break;
}
return 0;
}

int sys_get_sig_act(u_int envid, int signum, struct sigaction *oldact) {
struct Env *env;
if (envid2env(envid, &env, 0) < 0) {
return -1;
}
if (oldact != NULL) {
oldact->sa_handler = (void *)env->env_handlers[signum];
oldact->sa_mask = env->env_sa_mask;
}
return 0;
}

2)第二类:简单的信号函数

此外,还有五个用来处理信号掩码的函数:

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
void sigemptyset(sigset_t *set) { // 清空信号集,将所有位都设置为 0
set->sig[0] = 0;
set->sig[1] = 0;
}

void sigfillset(sigset_t *set) { // 设置信号集,即将所有位都设置为 1
sigemptyset(set);
set->sig[0] = ~(set->sig[0]);
set->sig[1] = ~(set->sig[1]);
}

void sigaddset(sigset_t *set, int signum) { // 向信号集中添加一个信号,即将指定信号的位设置为 1
if (signum <= 32) {
set->sig[0] |= SIG(signum);
} else {
set->sig[1] |= SIG(signum);
}
}

void sigdelset(sigset_t *set, int signum) { // 从信号集中删除一个信号,即将指定信号的位设置为 0
if (signum <= 32) {
set->sig[0] &= ~SIG(signum);
} else {
set->sig[1] &= ~SIG(signum);
}
}

int sigismember(const sigset_t *set, int signum) { // 检查一个信号是否在信号集中,如果在则返回 1,否则返回 0
if (getSig(set, signum) == 0) {
return 0;
} else {
return 1;
}
}

4、用户态异常处理函数

1)异常处理函数的定义

在user/lib/fork.c文件中,实现异常处理函数 sig_entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void __attribute__((noreturn)) sig_entry(struct Trapframe *tf, 
void (*sa_handler)(int), int signum, int envid) {
if (sa_handler != 0) {
sa_handler(signum); //直接调用定义好的处理函数
int r = syscall_set_sig_trapframe(0, tf);
user_panic("sig_entry syscall_set_trapframe returned %d", r);
}
switch (signum) {
case SIGKILL: case SIGSEGV: case SIGTERM:
syscall_env_destroy(envid); //默认处理
user_panic("sig_entry syscall_env_destroy returned");
default:;
int r = syscall_set_sig_trapframe(0, tf);
user_panic("sig_entry syscall_set_trapframe returned %d", r);
}
}
附:信号处理恢复现场的系统调用

仿照 syscall_set_trapframe 函数实现,需要注意将进程的信号处理标志位 env_sig_flag 置零:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sys_set_sig_trapframe(u_int envid, struct Trapframe *tf) {
if (is_illegal_va_range((u_long)tf, sizeof *tf)) {
return -E_INVAL;
}
struct Env *env;
try(envid2env(envid, &env, 1));
env->env_sig_flag = 0;
if (env == curenv) {
*((struct Trapframe *)KSTACKTOP - 1) = *tf;
return tf->regs[2];
} else {
env->env_tf = *tf;
return 0;
}
}

2)异常处理函数的跳转

i、kern/genex.S

在kern/genex.S的汇编函数 ret_from_exception 中加入到跳转到 do_signal 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FEXPORT(ret_from_exception)
/*-----------------adding codes--------------*/
move a0, sp
addiu sp, sp, -8
jal do_signal
nop
addiu sp, sp, 8
/*----------------finish codes---------------*/
RESTORE_SOME
lw k0, TF_EPC(sp)
lw sp, TF_REG29(sp) /* Deallocate stack */
.set noreorder
jr k0
rfe
.set reorder
ii、kern/tlbex.c

do_signal 函数的功能是:判断是否接下来要跳转到 sig_entry 用户态处理函数,即将 tf->cp0_epc 改成 sig_entry 的入口地址。

由于我们每一次从内核态恢复到用户态的时候,都要通过 ret_from_exception 汇编函数,即经过函数 do_signal ,因此我们需要额外判断是否此时需要转到 sig_entry 函数,我们需要在程序中具体如下实现 do_signal 函数:

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
void do_signal(struct Trapframe *tf) {
struct sigstack *sig_stack = curenv->env_sig_stack;
struct sigstack *front = NULL;
while (sig_stack != NULL && getSig((&(curenv->env_sa_mask)), sig_stack->sig) == 1) {
//找到第一个可以需要现在处理的信号
front = sig_stack;
sig_stack = sig_stack->next;
}
if (sig_stack == NULL) {
return;
}
if (curenv->env_sig_flag != 0 && sig_stack->sig != SIGSEGV) {
return;
}
curenv->env_sig_flag = 1;
if (front == NULL) {
curenv->env_sig_stack = sig_stack->next;
} else {
front->next = sig_stack->next;
}
sig_stack->next = NULL;

struct Trapframe tmp_tf = *tf;
if (tf->regs[29] < USTACKTOP || tf->regs[29] >= UXSTACKTOP) {
tf->regs[29] = UXSTACKTOP; // 将栈指针指向用户异常处理栈
}
tf->regs[29] -= sizeof(struct Trapframe); // 将当前的 Trapframe 压入异常处理栈
*(struct Trapframe *)tf->regs[29] = tmp_tf;

if (curenv->env_sig_entry) {
tf->regs[4] = tf->regs[29];
tf->regs[5] = (unsigned int)(curenv->env_handlers[sig_stack->sig]);
tf->regs[6] = sig_stack->sig;
tf->regs[7] = curenv->env_id;
tf->regs[29] -= sizeof(tf->regs[4]);
tf->regs[29] -= sizeof(tf->regs[5]);
tf->regs[29] -= sizeof(tf->regs[6]);
tf->regs[29] -= sizeof(tf->regs[7]);

tf->cp0_epc = curenv->env_sig_entry;
} else {
panic("sig but no user handler registered");
}
}

5、其他步骤

1)子进程继承父进程的信号处理函数

i、user/lib/fork.c

将父进程有关信号的用户态异常处理函数继承给子进程,具体操作就是将 sig_entry 函数的地址传给子进程 env_sig_entry 变量:

1
2
3
4
5
6
7
int fork(void) {
//......
if (env->env_sig_entry != (u_int)sig_entry) {
try(syscall_set_sig_entry(0, sig_entry));
}
//......
}
ii、kern/syscall_all.c

将父进程的信号处理函数继承给子进程,注意64个处理函数都要继承:

1
2
3
4
5
6
7
8
int sys_exofork(void) {
//......
for (u_int i = 1; i <= 64; i++)
{
e->env_handlers[i] = curenv->env_handlers[i];
}
//......
}

2)信号默认处理动作设置

对于 SIGSEGV 信号,通过观察我发现 UTEMP=0x3FE000UTEMP = 0x3FE000 ,于是我将kern/tlbex.c里面的 passive_alloc 函数中对 va < UTEMP 的处理修改成发送11号信号,如下:

1
2
3
4
5
6
7
8
9
static void passive_alloc(u_int va, Pde *pgdir, u_int asid) {
//......
/* 判断地址的合法性*/
if (va < UTEMP) {
sys_kill(0, SIGSEGV);
//panic("address too low");
}
//......
}

二、测试程序结果

1、基本信号测试

test1

2、空指针测试

test2

3、写时复制测试

test3

三、问题解决方案

1、include/signal.h

1)sigset_t结构体的定义

通过观察题干给出的如下几个函数:

1
2
3
4
5
void sigemptyset(sigset_t *set); // 清空信号集,将所有位都设置为 0
void sigfillset(sigset_t *set); // 设置信号集,即将所有位都设置为 1
void sigaddset(sigset_t *set, int signum); // 向信号集中添加一个信号,即将指定信号的位设置为 1
void sigdelset(sigset_t *set, int signum); // 从信号集中删除一个信号,即将指定信号的位设置为 0
int sigismember(const sigset_t *set, int signum); // 检查一个信号是否在信号集中,如果在则返回 1,否则返回 0

我们可以发现函数使用了 sigset_t 来表示 struct sigset_t ,因此在signal.h文件中定义 struct sigset_t 结构体时需要使用 typedef 的语法:

1
2
3
4
typedef struct sigset_t{
int sig[2]; //最多 32*2=64 种信号
//sig[0] 表示1~32 sig[1] 表示33~64
} sigset_t;

2)头文件的单次展开

我原本在 sigset_t 结构体定义问题中,是使用的 #define sigset_t struct sigset_t 来实现,但是出现了一个问题是:当第二次处理signal.h文件的时候,会导致实际 sigset_t 递归两次展开二出错。

所以,后来我观察了一下include文件夹中的其他.h文件,发现我们需要在每个.h文件中加入如下的开头和结尾:

1
2
3
4
#ifndef _SIGNAL_H_
#define _SIGNAL_H_
//......
#endif /* _SIGNAL_H_ */

2、Env中信号属性的处理

1)kern/env.c

env_alloc 函数中初始化相关的信号属性:

1
2
3
4
5
6
7
8
9
10
int env_alloc(struct Env **new, u_int parent_id) {
//......
for (u_int i = 1; i <= 64; i++) {
e->env_handlers[i] = 0;
}
e->env_sig_stack = NULL;
e->env_sig_entry = 0;
e->env_sig_flag = 0;
//......
}

2)kern/syscall_all.c

由于局部变量会在函数结束之后释放,所以在处理 e->env_sig_stack 的压栈记录数据时,我模仿了env_alloc 函数向 envs[] 申请进程控制块的机制,定义了一个静态数组 SIGstack[4096] 用来申请栈空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct sigstack SIGstack[4096];

int sys_kill(u_int envid, int sig) {
//......
for (int i = 0; i < 4096; i++) {
if (SIGstack[i].next == NULL) {
SIGstack[i].sig = sig;
SIGstack[i].next = env->env_sig_stack;
env->env_sig_stack = &(SIGstack[i]);
break;
}
}
//......
}

3、用户态异常处理函数

1)给进程设置异常处理函数入口地址的预处理

仿照TLB_mod异常处理函数入口地址是在 fork 函数中,使用系统调用为父子进程设置 cow_entry 异常处理函数入口地址。因此,我选择在 sigaction 注册函数中,使用用户态函数 env_set_sig_entry 函数为进程设置异常处理函数入口地址:

1
2
3
4
5
int env_set_sig_entry(void) {
try(syscall_set_sig_entry(0, sig_entry));
try(syscall_set_tlb_mod_entry(0, cow_entry));
return 0;
}

【注意】:特别值得注意的是,我们这里为进程设置 sig_entry 函数入口地址的同时,也需要为进程设置 cow_entry 函数入口地址,防止在后续写时复制的时候出现 panic("TLB Mod but no user handler registered"); 异常。

2)从do_signal函数向sig_entry函数传参

如下 sig_entry 用户异常处理函数定义了四个参数:

1
2
typedef void __attribute__((noreturn)) (*sig_entry_t)(struct Trapframe *tf, 
void (*sa_handler)(int), int signum, int envid);

do_signal 中用 tf 进行传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void do_signal(struct trapframe **tf) {
//......
if (curenv->env_sig_entry) {
tf->regs[4] = tf->regs[29]; //参数1
tf->regs[5] = (unsigned int)(curenv->env_sig_act.sa_handler); //参数2
tf->regs[6] = sig_stack->sig; //参数3
tf->regs[7] = curenv->env_id; //参数4
tf->regs[29] -= sizeof(tf->regs[4]);
tf->regs[29] -= sizeof(tf->regs[5]);
tf->regs[29] -= sizeof(tf->regs[6]);
tf->regs[29] -= sizeof(tf->regs[7]);

tf->cp0_epc = curenv->env_sig_entry;
} else {
panic("sig but no user handler registered");
}
}

3)预防爆栈处理

do_signal 中判断是否需要转到用户态异常处理函数的时候,需要提前判断一下是否当前已经有进程在处理 sig_entry ,避免爆掉异常处理栈。

注意:如果是 SIGSEGV = 11 的信号则需要“允许进行重入优先处理”。

1
2
3
4
5
6
7
8
void do_signal(struct Trapframe *tf) {
//......
if (curenv->env_sig_flag != 0 && sig_stack->sig != SIGSEGV) {
return;
}
curenv->env_sig_flag = 1;
//......
}

4、内存泄露envs数组溢出处理

struct Env 结构体的最后加上一个长数组保护:

1
2
3
4
struct Env {
//......
u_int env_protect[256];
};

5、添加系统调用的操作方法

本任务中涉及到了许多自定义的系统调用,对此我整理了相关添加系统调用的操作方法:

前提:假设用户进程调用用户态函数 user_func(u_int envid, ......) 过程中,需要使用到系统调用 syscall_func(u_int envid, ......)

  1. 在user/include/lib.h中添加:
    void user_func(u_int envid, ......);
    void syscall_func(u_int envid, ......);

  2. 在user/lib/syscall_lib.c中添加:

    1
    2
    3
    void syscall_func(u_int envid, ......) { 
    msyscall(SYS_func, envid, ......);
    }
  3. 在user/lib中实现 user_func 函数的文件中编写具体代码(其中会调用 syscall_func 函数)

  4. 在include/syscall.h中的 enumMAX_SYSNO 前面加上 SYS_func, (注意有逗号)

  5. 在kern/syscall_all.c的 void *syscall_table[MAX_SYSNO]最后加上 [SYS_func] = sys_func, (注意有逗号)

  6. 在kern/syscall_all.c的 void *syscall_table[MAX_SYSNO]前面具体实现函数 void syscall_func(u_int envid, ......);

6、编译链接.o文件

在最初使用 make run 来运行样例数据的时候,我始终出现编译错误,在后续询问同学之后,才知道需要在user/include.mk中加入链接的signal.o文件:

1
2
3
4
5
6
7
8
USERLIB          := entry.o \
syscall_wrap.o \
debugf.o \
libos.o \
fork.o \
syscall_lib.o \
ipc.o \
signal.o