BUAA-OS-Lab4_challenge sigaction实现
sigaction简介
当一个信号被发送给一个进程时, 内核会中断进程的正常控制流,转而执行与该信号相关的用户态处理函数进行处理
在执行该处理函数前,会将该信号所设置的信号屏蔽集加入到进程的信号屏蔽集中,在执行完该用户态处理函数后,又会将恢复原来的信号屏蔽集
任务描述
sigaction
结构体用于设置所需要处理的信号集及其对应的处理函数
sigset_t
使用32位表示MOS所需要处理的[1,32]信号掩码,对应位为1表示阻塞,为0表示未被阻塞
sigset_t
与sigaction
结构体定义如下:
1 | typedef struct sigset_t { |
数据结构、宏等设计
信号相关设置
此处挂起信号队列沿用此前MOS中设计的TAILQ结构并使用相关函数,实现参考 kern\env.c
中env_sched_list
相关部分
1 | // include/signal.h |
Env结构体添加成员
1 | // env.h |
错误返回值设置
1 | // include/error.h |
头文件中添加相关函数声明
由于没有太多技术含量,此处省略,在实现具体函数后到相应头文件中添加即可
初始化与全局变量设置相关前置操作
1 | // kern/env.c |
新增相关系统调用
统一流程
添加系统调用 syscall_func(u_int envid, ......)
:
在
user/include/lib.h
中添加void syscall_func(u_int envid, ......);
在
user/lib/syscall_lib.c
中添加1
2
3
4void 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_
具体实现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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114// 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_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 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
的代码,使得每次从用户态到内核态都调用一次信号检查函数
1 | // kern/genex.S |
各类信号的发送触发
SIGINT
与SIGKILL
一般在程序中手动触发,不会直接在MOS代码中发送SIGILL
发送契机 :在进行异常处理前,发现当前异常由非法指令引发,向自身发送
SIGILL
1
2
3
4
5
6
7// kern/traps.c
void do_reserved(struct Trapframe *tf) {
if(((tf->cp0_cause >> 2) & 0x1f) == 10){
sys_sendsig(0, SIGILL);
return;
}
}SIGSEGV
发送契机 :当前地址过低不允许访问,则向自身发送
SIGSEGV
1
2
3
4
5
6
7// 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
1
2
3
4
5
6
7// 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
1
2
3
4
5
6
7
8
9
10// 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;
}
}
信号的注册与发送
1 | // 信号注册函数 |
信号检查
do_signal
实现对当前挂起信号队列的检查,选择合适的信号后调用sig_setuptf
跳转到信号处理函数入口并为其传递参数- 保存寄存器上下文函数
sig_setuptf
参照kern\tlbex.c
中do_tlb_mod
实现,具体参数设计见后续信号处理函数入口
1 | // kern/env.c |
信号的实际处理
1 | // 执行信号 |
信号处理后上下文的信号
参考同文件中的 sys_set_trapframe
实现
1 | // kern/syscall_all.c |
信号集处理函数实现
注意如果传入参数不合法,返回值为 -E_SIG
1 | // user/lib/syscall_lib.c |
其他修改(踩过的坑)
寄存器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
函数的末尾修改为:
1 | - env_pop_tf(&curenv->env_tf, curenv->env_asid); |
Q:为什么不能将
curenv->env_tf
复制到 (struct Trapframe *)KSTACKTOP - 1?因为
KSTACKTOP
是内核栈的顶部,直接在此位置存储Trapframe
结构体稍有不慎可能导致堆栈溢出,从而覆盖其他关键的内核数据,干扰内核栈的正常使用
fork时相关设置的继承
1 | // kern/syscall_all.c |
同时,为了防止父进程没有注册信号处理函数,在fork时可以“再加一层保险”
1 | // user/lib/fork.c |
页错误处理函数入口的同步设置
在实际编码中,发现可能会出现页错误情况,报错未注册页错误处理函数。因此,在注册信号时也实现页错误处理函数入口的同步设置
由于 cow_entry
是静态函数,因此需要用一个函数将其包裹以便在其他文件中调用
1 | // user/lib/fork.c |
其他踩过的坑/关键设计
- 需要区分“打断”与“早已到”并妥善设计
- 问题:在一个信号处理完成之前,也可能会进行一些系统调用,这些系统调用在返回之前也会扫描信号队列,怎么防止系统转而去执行更早到达的信号(早已到)?但同时,我们也要允许当前处理完成之前,晚于当前信号到达的信号(打断)能够被先处理。
- 解决思路:
- 先在
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
的大小来判断信号发出的先后
- 先在
- 不严谨的地方:发送信号的时间不严格等于信号开始被处理的时间,后续可以优化
- 处理前修改进程掩码,处理后恢复进程原掩码的实现需要小心谨慎,要想清楚到底要恢复成什么样