Lab4-challenge实验报告
由于课程改革,本学期OS课程的lab4挑战性任务与往年完全不一样,题目要求如下:Lab4 Challenge 题干 。
附:
一、任务实现思路
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_t
、struct sigaction
、struct 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 { 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)第一类:需要系统调用的信号函数
其中,函数 sigaction
、sigprocmask
、kill
由于需要改变进程控制块中的信号属性,所以需要利用系统调用陷入内核态进行处理:
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 ) { 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); }
附:信号函数相关的系统调用
上述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->sig[0 ] = env->env_sa_mask.sig[0 ]; oldset->sig[1 ] = env->env_sa_mask.sig[1 ]; } switch (how) { case SIG_BLOCK: env->env_sa_mask.sig[0 ] |= set ->sig[0 ]; env->env_sa_mask.sig[1 ] |= set ->sig[1 ]; break ; case SIG_UNBLOCK: env->env_sa_mask.sig[0 ] &= (~set ->sig[0 ]); env->env_sa_mask.sig[1 ] &= (~set ->sig[1 ]); break ; case SIG_SETMASK: 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 ) { set ->sig[0 ] = 0 ; set ->sig[1 ] = 0 ; } void sigfillset (sigset_t *set ) { sigemptyset(set ); set ->sig[0 ] = ~(set ->sig[0 ]); set ->sig[1 ] = ~(set ->sig[1 ]); } void sigaddset (sigset_t *set , int signum) { if (signum <= 32 ) { set ->sig[0 ] |= SIG(signum); } else { set ->sig[1 ] |= SIG(signum); } } void sigdelset (sigset_t *set , int signum) { if (signum <= 32 ) { set ->sig[0 ] &= ~SIG(signum); } else { set ->sig[1 ] &= ~SIG(signum); } } int sigismember (const sigset_t *set , int signum) { 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); *(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
信号,通过观察我发现 U T E M P = 0 x 3 F E 000 UTEMP = 0x3FE000 U T E M P = 0 x 3 F E 0 0 0 ,于是我将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); } }
二、测试程序结果
1、基本信号测试
2、空指针测试
3、写时复制测试
三、问题解决方案
1、include/signal.h
1)sigset_t结构体的定义
通过观察题干给出的如下几个函数:
1 2 3 4 5 void sigemptyset (sigset_t *set ) ; void sigfillset (sigset_t *set ) ; void sigaddset (sigset_t *set , int signum) ; void sigdelset (sigset_t *set , int signum) ; int sigismember (const sigset_t *set , int signum) ;
我们可以发现函数使用了 sigset_t
来表示 struct sigset_t
,因此在signal.h文件中定义 struct sigset_t
结构体时需要使用 typedef
的语法:
1 2 3 4 typedef struct sigset_t { int sig[2 ]; } 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
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 ]; tf->regs[5 ] = (unsigned int )(curenv->env_sig_act.sa_handler); 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" ); } }
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, ......)
。
在user/include/lib.h中添加:
void user_func(u_int envid, ......);
void syscall_func(u_int envid, ......);
在user/lib/syscall_lib.c中添加:
1 2 3 void syscall_func (u_int envid, ......) { msyscall(SYS_func, envid, ......); }
在user/lib中实现 user_func
函数的文件中编写具体代码(其中会调用 syscall_func
函数)
在include/syscall.h中的 enum
的 MAX_SYSNO
前面 加上 SYS_func,
(注意有逗号)
在kern/syscall_all.c的 void *syscall_table[MAX_SYSNO]
的最后 加上 [SYS_func] = sys_func,
(注意有逗号)
在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