文章目录
sigaction简介
当一个信号被发送给一个进程时, 内核会中断进程的正常控制流,转而执行与该信号相关的用户态处理函数进行处理
在执行该处理函数前,会将该信号所设置的信号屏蔽集加入到进程的信号屏蔽集中,在执行完该用户态处理函数后,又会将恢复原来的信号屏蔽集
任务描述
sigaction
结构体用于设置所需要处理的信号集及其对应的处理函数
sigset_t
使用32位表示MOS所需要处理的[1,32]信号掩码,对应位为1表示阻塞,为0表示未被阻塞
sigset_t
与sigaction
结构体定义如下:
typedef struct sigset_t {
uint32_t sig;
} sigset_t;
struct sigaction {
void (*sa_handler)(int); //信号的处理函数
sigset_t sa_mask;
};
数据结构、宏等设计
信号相关设置
此处挂起信号队列沿用此前MOS中设计的TAILQ结构并使用相关函数,实现参考 kern\env.c
中env_sched_list
相关部分
// include/signal.h
// 信号掩码控制宏 __how的取值
#define SIG_BLOCK (0) // 添加__set到当前掩码
#define SIG_UNBLOCK (1) // 从当前掩码中移除__set
#define SIG_SETMASK (2) // 设置当前掩码为__set
// SIGNUM编号
#define SIGINT (2)
#define SIGILL (4)
#define SIGKILL (9)
#define SIGSEGV (11)
#define SIGCHLD (17)
#define SIGSYS (31)
// 信号数量
#define SIG_MAX (32)
// 掩码结构体
typedef struct sigset_t {
uint32_t sig;
} sigset_t;
// 信号操作结构体
typedef void (*sa_handler)(int);
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
};
// 信号描述符
struct signal {
TAILQ_ENTRY(signal) sig_link;
int signum;
int time;
};
// 挂起的信号队列
TAILQ_HEAD(Sig_pend_list, signal);
Env结构体添加成员
// env.h
struct Env {
// lab4-challenge
u_int env_user_sighand_entry; // 用户态信号处理函数入口
u_int now_sig_time; // 记录当前正在处理的信号的到达时间,重入处理的时候用
u_int is_handling_SIGKILL; // 判定现在正在被处理的是否是SIGKILL信号,若是则为1,反之为0
u_int sig_exist[32]; // 每种信号在当前进程的存在性判断,保证每种信号的唯一性
sigset_t sig_blocked; // 信号掩码
struct Sig_pend_list sig_pend_list; // 挂起信号队列
struct sigaction sighand[32]; // 信号注册数组
};
错误返回值设置
// include/error.h
#define E_SIG 1 // 实际运用一般都是 return -E_SIG
头文件中添加相关函数声明
由于没有太多技术含量,此处省略,在实现具体函数后到相应头文件中添加即可
初始化与全局变量设置相关前置操作
// kern/env.c
// lab4-challenge
int sigsTime = 1; // 用于标记信号到达时间,越小则到达时间越早
struct signal signals[SIG_MAX] __attribute__((aligned(PAGE_SIZE))); // 进程信号数组
int env_alloc(struct Env **new, u_int parent_id) {
// lab4-challenge
// 最初都初始化为0
e->sig_blocked.sig = 0;
e->env_user_sighand_entry = 0;
e->sig_pend_cnt = 0;
e->now_sig_time = 0;
e->is_handling_SIGKILL = 0;
TAILQ_INIT(&e->sig_pend_list);
for(int i=0; i<32; i++){
e->sig_exist[i] = 0;
}
for(int i=0; i<32; i++){
e->sighand[i].sa_handler = NULL;
e->sighand[i].sa_mask.sig = 0;
}
}
新增相关系统调用
统一流程
添加系统调用 syscall_func(u_int envid, ......)
:
-
在
user/include/lib.h
中添加void syscall_func(u_int envid, ......);
-
在
user/lib/syscall_lib.c
中添加void syscall_func(u_int envid, ......) { ...... msyscall(SYS_func, envid, ......); }
-
在
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 sys_func(u_int envid, ......);
的具体实现
新增功能实现所需系统调用的具体实现
-
sys_
具体实现// kern/syscall_all.c // 信号注册 int sys_sigaction(int signum, const struct sigaction *newact, \struct sigaction *oldact) { struct Env *e; struct sigaction *sig = NULL; // 只需考虑signum小于或等于32的情况,超出该范围返回-1 if (signum < 1 || signum > SIG_MAX) { return -E_SIG; } e = curenv; sig = &e->sighand[signum - 1]; if (oldact) { *oldact = *sig; } if (newact) { *sig = *newact; } return 0; } // 设置进程的信号处理函数入口地址 int sys_set_sighand_entry(u_int envid, u_int func) { struct Env *env; try(envid2env(envid, &env, 1)); env->env_user_sighand_entry = func; return 0; } // 向进程发送信号 extern int sigsTime; extern struct signal signals[SIG_MAX] __attribute__((aligned(PAGE_SIZE))); int sys_sendsig(u_int envid, int sig) { struct Env *e; // 当envid对应进程不存在,或者sig不符合定义范围时,返回异常码-1 if ( sig < 1 || sig > SIG_MAX || envid2env(envid, &e, 0) != 0 ) { return -E_SIG; }; // 当进程中已经有同种信号,则不再接收 if ( e->sig_exist[sig-1] == 1 ){ return 0; } // 将信号添加进对应进程的信号处理队列 signals[sig-1].signum = sig; signals[sig-1].time = sigsTime; struct signal *s = (struct signal *)(&signals[sig-1]); e->sig_exist[sig-1] = 1; TAILQ_INSERT_HEAD(&e->sig_pend_list, (s), sig_link); sigsTime++; e->sig_pend_cnt++; return 0; } // 根据__how的值更改当前进程的信号屏蔽字 // __set是要应用的新掩码,__oset(如果非NULL)则保存旧的信号屏蔽字 // __how可以是SIG_BLOCK(添加__set到当前掩码)、SIG_UNBLOCK(从当前掩码中移除__set) // 或SIG_SETMASK(设置当前掩码为__set) int sys_sigprocmask(int __how, const sigset_t * __set, sigset_t * __oset){ struct Env *e; sigset_t *sigset; e = curenv; sigset = &e->sig_blocked; if (__oset!=NULL) { *__oset = *sigset; } if (__set!=NULL) { switch (__how) { case SIG_BLOCK: sigset->sig |= __set->sig; break; case SIG_UNBLOCK: sigset->sig &= ~__set->sig; break; case SIG_SETMASK: sigset->sig = __set->sig; break; default: return -E_SIG; } } return 0; } // 检查信号__signo是否是__set信号集的成员。如果是,返回1;如果不是,返回0。 int _sigismember(const sigset_t *__set, int __signo){ if (__set == NULL){ return 0; } __signo -= 1; return 1 & (__set->sig >> (__signo)); } // 获取当前被阻塞且未处理的信号集,并将其存储在__set中 int sys_sigpending(sigset_t *__set){ struct signal *s = NULL; int t; uint32_t newSig = __set->sig; TAILQ_FOREACH (s, &curenv->sig_pend_list, sig_link) { // 检查掩码 if ( _sigismember(&curenv->sig_blocked, s->signum) ) { t = s->signum; newSig |= (1 << (t-1)); } } __set->sig = newSig; return 0; } // 获取进程状态 int sys_check_env_status(u_int envid){ struct Env * e; try(envid2env(envid, &e, 0)); return e->env_status; }
-
部分除
msyscall
还有其他操作的syscall_
// user/lib/syscall_lib.c int syscall_sigaction(int signum, const struct sigaction *newact, \struct sigaction *oldact) { if (oldact) { memset(oldact, 0, sizeof(oldact)); } return msyscall(SYS_sigaction, signum, newact, oldact); } int syscall_sigprocmask(int __how, const sigset_t * __set, sigset_t * __oset) { if (__oset) { memset(__oset, 0, sizeof(__oset)); } return msyscall(SYS_sigprocmask, __how, __set, __oset); }
信号处理流程实现
信号处理的触发
在 ret_from_exception
中加入跳转到 do_signal
的代码,使得每次从用户态到内核态都调用一次信号检查函数
// kern/genex.S
FEXPORT(ret_from_exception)
// 加入跳转到do_signal的代码(每次从用户态到内核态都调用一次)
move a0, sp
addiu sp, sp, -8
jal do_signal
addiu sp, sp, 8
各类信号的发送触发
-
SIGINT
与SIGKILL
一般在程序中手动触发,不会直接在MOS代码中发送 -
SIGILL
发送契机 :在进行异常处理前,发现当前异常由非法指令引发,向自身发送
SIGILL
// kern/traps.c void do_reserved(struct Trapframe *tf) { if(((tf->cp0_cause >> 2) & 0x1f) == 10){ sys_sendsig(0, SIGILL); return; } }
-
SIGSEGV
发送契机 :当前地址过低不允许访问,则向自身发送
SIGSEGV
// kern/tlbex.c static void passive_alloc(u_int va, Pde *pgdir, u_int asid) { if (va < UTEMP) { // panic("address too low"); sys_sendsig(curenv->env_id, SIGSEGV); } }
-
SIGCHLD
发送契机 :子进程退出时,若父进程仍在运行,则子进程向父进程发送
SIGCHLD
// kern/env.c void env_destroy(struct Env *e) { // lab4-challenge if( e->env_parent_id != 0){ sys_sendsig( e->env_parent_id, SIGCHLD); } }
-
SIGSYS
发送契机 :当前系统调用号不存在时,需要先忽视(跳过)此条语句,再向自身发送
SIGSYS
// kern/syscall_all.c void do_syscall(struct Trapframe *tf) { int sysno = tf->regs[4]; if (sysno < 0 || sysno >= MAX_SYSNO) { tf->regs[2] = -E_NO_SYS; tf->cp0_epc += 4; sys_sendsig(curenv->env_id, SIGSYS); return; } }
信号的注册与发送
// 信号注册函数
int sigaction(int signum, const struct sigaction *newact, struct sigaction *oldact) {
// 确保设置好信号处理函数入口地址
if (env->env_user_sighand_entry != (u_int)sighand_entry) {
try(syscall_set_sighand_entry(0, (u_int)sighand_entry));
try(syscall_set_tlb_mod_entry(0, (u_int)entry_wrapper));
}
int r = syscall_sigaction(signum, newact, oldact);
return r;
}
// 信号发送函数
int kill(u_int envid, int sig) {
// 如果sig为SIGCHLD,SIGILL,SIGSYS,SIGSEGV信号,返回异常码-1
if (sig == SIGILL || sig == SIGSYS || sig == SIGCHLD || sig == SIGSEGV) {
// debugf("Only MOS can send SIGCHLD & SIGILL & SIGSYS & SIGSEGV signal.\n");
return -E_SIG;
}
// 确保设置好信号处理函数入口地址
if (env->env_user_sighand_entry != (u_int)sighand_entry) {
try(syscall_set_sighand_entry(0, (u_int)sighand_entry));
}
int r = syscall_sendsig(envid, sig);
return r;
}
信号检查
do_signal
实现对当前挂起信号队列的检查,选择合适的信号后调用sig_setuptf
跳转到信号处理函数入口并为其传递参数- 保存寄存器上下文函数
sig_setuptf
参照kern\tlbex.c
中do_tlb_mod
实现,具体参数设计见后续信号处理函数入口
// kern/env.c
// lab4-challenge
void do_signal(struct Trapframe *tf) {
// 在为进程设置sigpri之前也可能会发生一些异常产生异常重入的现象
// 而异常重入的时候不可能产生任何新的信号,没必要处理信号,直接return
if (((int)tf->cp0_epc)<0){
return;
}
// 如果没有待处理信号,直接返回
if (TAILQ_EMPTY(&curenv->sig_pend_list)){
return;
}
struct signal *s = NULL, *s_min = NULL;
int signo = 10000; // 初始化为10000,后续用于判断当前信号优先级是否高于之前选中的信号
int time = 0;
// 从进程的队列中取出未被阻塞的signal
// SIGKILL优先级最高
if (curenv->sig_exist[SIGKILL-1] == 1){
, TAILQ_FOREACH (s_min, &curenv->sig_pend_list, sig_link) {
if (s_min->signum == SIGKILL) {
signo = s_min->signum;
time = s_min->time;
curenv->is_handling_SIGKILL = 1;
break;
}
}
}else if(curenv->is_handling_SIGKILL == 0){
TAILQ_FOREACH (s, &curenv->sig_pend_list, sig_link) {
// 如果当前有信号正在处理且此信号比当前信号到达时间早,则break
if ( s->time <= curenv->now_sig_time ){
break;
}
// 选取优先级最高的
if ( s->signum < signo
&& ( (curenv->sig_blocked.sig & (1<<(s->signum-1))) == 0 )){
signo = s->signum;
time = s->time;
s_min = s;
}
}
}
// 若存在未被阻塞的signal, 则修改进程上下文, 转到用户态的信号处理函数
if (s_min) {
curenv->now_sig_time = time;
curenv->sig_exist[signo-1] = 0;
TAILQ_REMOVE(&curenv->sig_pend_list, s_min, sig_link);
// 保存寄存器上下文,给信号处理函数传递参数
sig_setuptf(tf, signo);
} else {
return;
}
}
// 保存寄存器上下文(仿写do_tlb_mod)
void sig_setuptf(struct Trapframe *tf, int signum) {
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;
// 将信号处理需要传递的相关参数保存至异常现场栈中
tf->regs[4] = tf->regs[29];
tf->regs[5] = signum;
tf->regs[6] = (unsigned int)(curenv->sighand[signum-1].sa_handler);
uint32_t newMask = 0;
newMask = curenv->sighand[signum-1].sa_mask.sig | curenv->sig_blocked.sig
\ | (1 << (signum - 1)) ;
tf->regs[7] = (uint32_t)newMask;
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_user_sighand_entry;
}
信号的实际处理
// 执行信号
void __attribute__((noreturn)) sighand_entry(struct Trapframe *tf, int signum,
\ void (*sa_handler)(int), uint32_t newMask) {
int r;
// 若设置了处理函数
if (sa_handler && signum != SIGKILL ) {
// 处理前修改进程掩码
sigset_t oldSigset={0}, newSigset={0};
newSigset.sig = newMask;
r= syscall_sigprocmask(2, (sigset_t *)&newSigset, (sigset_t *)&oldSigset);
sa_handler(signum);
// 恢复进程原掩码
syscall_sigprocmask(2, (sigset_t *)&oldSigset, NULL);
// 恢复上下文
r = syscall_set_sig_trapframe(0, tf);
user_panic("sig_entry syscall_set_sig_trapframe returned %d", r);
}
switch (signum) {
case SIGINT: case SIGILL: case SIGKILL: case SIGSEGV:
// 退出用exit
exit();
user_panic("sig_entry syscall_env_destroy returned");
default:
r = syscall_set_sig_trapframe(0, tf);
user_panic("sig_entry syscall_set_sig_trapframe returned %d", r);
}
}
信号处理后上下文的信号
参考同文件中的 sys_set_trapframe
实现
// kern/syscall_all.c
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->now_sig_time 为 0
env->now_sig_time = 0;
if (env == curenv) {
*((struct Trapframe *)KSTACKTOP - 1) = *tf;
// return `tf->regs[2]` instead of 0, because return value overrides regs[2] on
// current trapframe.
return tf->regs[2];
} else {
env->env_tf = *tf;
return 0;
}
}
信号集处理函数实现
注意如果传入参数不合法,返回值为 -E_SIG
// user/lib/syscall_lib.c
// 计算两个信号集__left和__right的并集,并将结果存储在__set中
int sigorset(sigset_t *__set, const sigset_t *__left, const sigset_t *__right){
if ( __left == NULL || __right == NULL ){
return -E_SIG;
}
__set->sig = __left->sig | __right->sig;
return 0;
}
// 根据__how的值更改当前进程的信号屏蔽字。__set是要应用的新掩码
// __oset(如果非NULL)则保存旧的信号屏蔽字
// __how可以是SIG_BLOCK(添加__set到当前掩码)、SIG_UNBLOCK(从当前掩码中移除__set)
// 或SIG_SETMASK(设置当前掩码为__set)
int sigprocmask(int __how, const sigset_t * __set, sigset_t * __oset){
syscall_sigprocmask(__how, __set, __oset);
return 0;
}
// 清空参数中的__set掩码,初始化信号集以排除所有信号。这意味着__set将不包含任何信号。(清0)
int sigemptyset(sigset_t *__set){
if ( __set == NULL){
return 0;
}
__set->sig = 0;
return 0;
}
// 将参数中的__set掩码填满,使其包含所有已定义的信号。这意味着__set将包括所有信号。(全为1)
int sigfillset(sigset_t *__set){
if ( __set == NULL){
return -E_SIG;
}
sigemptyset(__set);
__set->sig = ~(__set->sig);
return 0;
}
// 向__set信号集中添加一个信号__signo。如果操作成功,__set将包含该信号。(置位为1)
int sigaddset(sigset_t *__set, int __signo){
if ( __set == NULL){
return -E_SIG;
}
if ( __signo < 1 || __signo > SIG_MAX ) {
return -E_SIG;
};
__set->sig |= (1<<(__signo-1));
return 0;
}
// 从__set信号集中删除一个信号__signo。如果操作成功,__set将不再包含该信号。(置位为0)
int sigdelset(sigset_t *__set, int __signo){
if ( __set == NULL){
return 0;
}
if ( __signo < 1 || __signo > SIG_MAX ) {
return -E_SIG;
};
__set->sig &= ~(1<<(__signo-1));
return 0;
}
// 检查信号__signo是否是__set信号集的成员。如果是,返回1;如果不是,返回0
int sigismember(const sigset_t *__set, int __signo){
if ( __set == NULL){
return 0;
}
return 1 & (__set->sig >> (__signo-1));
}
// 检查信号集__set是否为空。如果为空,返回1;如果不为空,返回0
int sigisemptyset(const sigset_t *__set){
if ( __set == NULL){
return 1;
}
return __set->sig == 0;
}
// 计算两个信号集__left和__right的交集,并将结果存储在__set中。
int sigandset(sigset_t *__set, const sigset_t *__left, const sigset_t *__right){
if ( __left == NULL || __right == NULL ){
return -E_SIG;
}
__set->sig = __left->sig & __right->sig;
return 0;
}
// 获取当前被阻塞且未处理的信号集,并将其存储在__set中
int sigpending(sigset_t *__set){
if ( __set == NULL){
return -E_SIG;
}
syscall_sigpending( __set);
return 0;
}
其他修改(踩过的坑)
寄存器Trapframe保存与传递设置
当 env_pop_tf
函数被调用时,它会将 curenv->env_tf
(即当前进程的上下文)加载到处理器寄存器中,并且将 curenv->env_tf
的地址赋值给堆栈指针 sp
。进入 do_signal
函数时,堆栈指针 sp
指向 curenv->env_tf
的位置。
假设 do_signal
函数会在其执行过程中使用堆栈保存临时数据和函数调用信息,则这些数据会覆盖 curenv->env_tf
的内容,导致进程控制块中的数据被意外修改。
所以需要通过将 curenv->env_tf
复制到当前栈上一个临时变量 tmp_tf
,然后传入 env_pop_tf
函数,从而避免直接使用进程控制块中的地址。
将 kern/env.c
文件中 env_run
函数的末尾修改为:
- env_pop_tf(&curenv->env_tf, curenv->env_asid);
+ struct Trapframe tmp_tf = curenv->env_tf;
+ env_pop_tf(&tmp_tf, curenv->env_asid);
Q:为什么不能将
curenv->env_tf
复制到 (struct Trapframe *)KSTACKTOP - 1?因为
KSTACKTOP
是内核栈的顶部,直接在此位置存储Trapframe
结构体稍有不慎可能导致堆栈溢出,从而覆盖其他关键的内核数据,干扰内核栈的正常使用
fork时相关设置的继承
// kern/syscall_all.c
int sys_exofork(void) {
// lab4-challenge
// 全部和sigaction有关的都要复制
e->sig_blocked = curenv->sig_blocked;
e->env_user_sighand_entry = curenv->env_user_sighand_entry;
e->sig_pend_cnt = curenv->sig_pend_cnt;
e->now_sig_time = curenv->now_sig_time;
e->is_handling_SIGKILL = 0;
e->sig_pend_list = curenv->sig_pend_list;
for(int i=0; i<32; i++){
e->sig_exist[i] = curenv->sig_exist[i];
}
for(int i=0; i<32; i++){
e->sighand[i].sa_handler = curenv->sighand[i].sa_handler;
e->sighand[i].sa_mask.sig = curenv->sighand[i].sa_mask.sig;
}
}
同时,为了防止父进程没有注册信号处理函数,在fork时可以“再加一层保险”
// user/lib/fork.c
int fork(void) {
// lab4-challenge
if (env->env_user_sighand_entry != (u_int)sighand_entry) {
try(syscall_set_sighand_entry(0, (u_int)sighand_entry));
}
}
页错误处理函数入口的同步设置
在实际编码中,发现可能会出现页错误情况,报错未注册页错误处理函数。因此,在注册信号时也实现页错误处理函数入口的同步设置
由于 cow_entry
是静态函数,因此需要用一个函数将其包裹以便在其他文件中调用
// user/lib/fork.c
void entry_wrapper(struct Trapframe *tf) {
cow_entry(tf);
}
其他踩过的坑/关键设计
- 需要区分“打断”与“早已到”并妥善设计
- 问题:在一个信号处理完成之前,也可能会进行一些系统调用,这些系统调用在返回之前也会扫描信号队列,怎么防止系统转而去执行更早到达的信号(早已到)?但同时,我们也要允许当前处理完成之前,晚于当前信号到达的信号(打断)能够被先处理。
- 解决思路:
- 先在
env.c
里设置一个全局变量sigsTime
- 每次发送信号的时候让
s->sig_time=sigsTime
,然后sigsTime++
- 给
Env
结构体增加成员变量handlingSig_time
,进程每开始处理一个信号就让curenv->handlingSig_time=s->sig_time
- 在
dosignal
中根据比较s->sig_time
和curenv->handlingSig_time=s->sig_time
的大小来判断信号发出的先后
- 先在
- 不严谨的地方:发送信号的时间不严格等于信号开始被处理的时间,后续可以优化
- 处理前修改进程掩码,处理后恢复进程原掩码的实现需要小心谨慎,要想清楚到底要恢复成什么样