关于sigaction实现的细节问题

唐凌
🔖︎ 20 订阅
👍︎ 21 点赞

对于sigaction挑战性任务的实现,感觉有很多同学被卡在第四或第六个点,感觉其中会涉及到方方面面的细节问题,所以希望发这样一个帖子和大家探讨一些问题。欢迎大家补充说明。
关于处理的时机、信号处理顺序、对信号集的处理规则等助教已经发帖说明,此处不再赘述。
目前在和其他同学讨论的过程中发现了一些实现的不同之处:

1.
对于信号处理的重入机制,掩码需要存储在栈中,那么对于这个栈的设置,既可以在env控制块中实现一个大小为32的栈,也可以存入到用户异常处理栈中。
当然,也有同学提出可以直接将旧掩码存入sig_entry的参数中,在sig_entry中维护这个栈。

2.
对于默认的终止进程处理,助教在之前的帖中说明统一在用户态用exit()实现。但是某些同学对于信号处理的用户入口函数env_entry的注册放在sigaction()函数中,那么如果在没有注册入口函数的时候,处理就会出现异常。那么是不是也可以考虑在内核态判断是否为终止进程的默认处理,如果是,那么直接在内核态销毁进程即可?

3.
关于epc+4的问题,是默认忽略处理+4,还是只在SIGSYS的时候+4?
目前一致的观点是只有SIGSYS +4。

4.
debug的困难:printk函数的使用,甚至debugf的使用都可能影响原来的调用栈,对于这些std的调用,具体的流程似乎并不清楚,但是经过实验发现其确实会影响正常执行的结果,表现为指针乱飞,内存泄漏等等。

5.
fork的结果,子进程要不要继承父进程的block和pending?
我的实现是继承block和所有sigaction,以及sig_entry,其他按初始化处理。(欢迎批评指正)。

6.
(此条来自讨论区https://os.buaa.edu.cn/discussion/296存档
一个很关键的修改是sigset系列操作中对输入是否为空更严格的检查,这个小改动能决定是0/6还是6/6.
所以我推测test6是有更极端的参数,比如给set传入NULL这种,导致如果不做检查就会0/6。不知道是否有帮助。

7.
关于程控制块会变成预期之外的值的问题,详见https://os.buaa.edu.cn/discussion/289存档,主要为调用do_signal时,传入的参数指向了struct Env中的某处,导致结果被修改。

8.
关于评测时开启优化的结果与不开优化的结果不一致的问题,详见https://os.buaa.edu.cn/discussion/315(无需存档),主要原因为开启优化之后,对于SIGSEGV信号产生的条件的语句(例如包含取非法地址数据的lw指令),因为被优化而不再可以有效产生异常信号。

同时,对于同一个非法地址的多次访问只能产生一次信号的原因是,对于非法地址,我们修改了MOS对其的处理方法,即由原来的panic变为了发送信号,而后面还是会为该地址分配物理页框,而passive_alloc只会在tlb MISS时进入,因此后面不再缺页,不会再触发非法地址错误。(有错请指出)

9.
关于何时允许重入的问题,如果无脑在从内核态返回用户态的ret_from_exception中执行do_signal,考虑到在进入sig_entry之前,可能会触发新的异常,新异常返回时又会第二次进入do_signal,即重入了do_signal,但我们原先预期的重入是只有严格从内核返回用户态时执行的,对栈的操作仅仅发生在用户异常处理栈,所以无法预知允许在内核处理新异常时的重入结果是否正确。
因此,我们最好严格保证只有在内核态返回用户态时,才进入do_signal。(虽然就亲身经历而言,对评测无影响,但是debug的时候会出现很多问题,可能4中的猜想的原因出自于这)。因此,我们在判断是否进入do_signal时,需要加上判断导致异常的现场的PC是在用户态还是内核态,即与ULIM比较即可。

10.
关于何时注册sig_entry的问题,详见https://os.buaa.edu.cn/discussion/304存档

原先有一种做法是在sigaction注册信号的时候注册sig_entry。但是可能会有没有注册信号但是就发送信号的情况,这个时候进程控制块找不到对应处理函数的地址,就会报错。

有两种解决方法:
1.对默认处理在内核态即完成,这样注册的sig_entry函数只会在注册信号之后才有可能被调用。这种做法主要是参考了linux的宏内核的做法,可能与我们的微内核的思想不太相符,但是能够解决问题。
2.在libos.c中,对获取的第一个进程控制块进行sig_entry的注册,确保测试进程运行之前就被注册sig_entry。

11.
关于按优先级的处理顺序和实际表现的处理顺序不一致的问题,具体参见楼下评论区,因为我们的输出一般都会在用户态,如果按照优先级先挑选出一个信号,但是可能在处理的过程中未输出关键语句或者出现异常,就再次进入内核态,此时在do_signal中会重新选择新的信号,这种行为可能会导致处理顺序不一致的问题。

回复主题帖

lhp
👍︎ 1 点赞

想问问同学关于第九点是怎么处理的呢?我的实现在从do_signal跳转到sig_entry前有时候仍然会出现很多异常或者重入,导致出现问题,从而无法得到正确的处理顺序。

1
2
3
4
5
void do_signal(struct Trapframe *tf) {
if ((tf->cp0_epc) > ULIM) {
return;
}
//选择信号并设置参数跳转到用户处理函数

唐凌 回复 lhp
👍︎ 2 点赞

(如果不做这个判断的话,可能会导致debug的时候栈上内容被修改,加上进程调度的一定特性使得程序无法正常运行。但是根据亲身经历来看的话,这一点是不影响评测的。

如果是还未返回用户态的sig_entry的话,那么异常大概率是对trapframe 进行 memcpy时出现缺页异常,保存的现场的cp0.epc是内核态的,按照你的处理来看,内核态异常在返回时是会跳过这一次do_signal处理的,理论上没有问题。

如果出现异常的话,有没有可能是你将栈上的某些数据放入了进程控制块,而根据第7点产生了对栈上数据的修改?

另外,对于你说的处理顺序不正确的问题,是你自己测试的结果还是评测点没有通过呢?因为大家在debug的时候会把输出放在用户态,这样的话考虑一种情况:有2.3.4(举个例子,不一定现实会产生这样的信号)信号待处理,那么,我们首先选择2号信号,然后进入用户态处理程序,假如未输出即再次进行内核态,此时再进入do_signal时,会选择3号信号,依次类推,可能你最终表面上看到的最先处理信号反而是4号。


lhp 回复 唐凌
👍︎ 2 点赞
1
2
3
4
5
6
    u_int pid = syscall_getenvid();
if (fork() == 0) {
kill(pid, SIGKILL);
debugf("send to %x\n", pid);
exit();
}

我的bug表现是对于以上代码,切换到父进程时会先处理SIGKILL,但是执行到do_signal的最后一行tf->cp0_epc = curenv->env_sig_entry;之后,应该回到ret_from_exception,然后RESTORE_ALL并且eret,跳转到sig_entry。
困惑的是这个SIGKILL在eret这里似乎并不能成功跳转到sig_entry,而是会重新到do_signal选择下一个待处理的信号SIGCHLD正常然后进入他的处理程序。似乎第一个SIGKILL被忽略了,不知道有没有同学处理过类似的问题。

1
2
3
4
5
6
7
8
FEXPORT(ret_from_exception)
move a0, sp
addiu sp, sp, -24
jal do_signal
nop
addiu sp, sp, 24
RESTORE_ALL
eret

唐凌 回复 lhp
👍︎ 2 点赞

同学你好,请问你的默认终止进程处理是在do_signal还是在sig_entry中呢,根据你的描述我猜大概率是在用户态的sig_entry中,那么如果是在sig_entry中,发生任何异常或中断都会导致再次进入do_signal重新选择信号,其实是有一定不确定性的,无法确定你的程序是否存在问题。

如果你希望在此基础上debug,可以在do_signal的末尾输出tf->cp0_epc的值,并使用make objdump进行反汇编,在test程序的反汇编代码中查看sig_entry的地址和cp0_epc是否一致。

当然,如果问题解决不了的话,还可以尝试一下把默认处理放在内核态,即在do_signal中完成处理。

希望我的观点能够起到帮助。


lhp 回复 lhp
👍︎ 1 点赞

补充一下我最后发现的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <lib.h>

void sigchld_handler(int sig) {
debugf("capture SIGCHLD signal.\n");
debugf("shouldn't see this.\n");
}

int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
u_int pid = syscall_getenvid();
if (fork() == 0) {
kill(pid, SIGKILL);
exit();
}
while (1);
return 0;
}

对于这个测试程序,可能会表现出来如下的输出

1
2
3
4
5
6
7
8
[00001001] destroying 00001001
[00001001] free env 00001001
i am killed ...
capture SIGCHLD signal.
shouldn't see this.
[00000800] destroying 00000800
[00000800] free env 00000800
i am killed ...

由于do_signal先处理SIGKILL,但是在跳转到sig_entry的时候发生了缺页异常,这个时候位于用户空间的缺页异常ret_from_exception的时候会重入do_signal,就会去处理SIGCHLD,表现出来的就是先进入SIGCHLD的处理程序,然后再执行SIGKILL的默认exit处理,这个好像是可以接受的,符合对于处理信号时机的要求。

1
2
3
4
5
6
7
int kill(u_int envid, int sig) {
if (sig <= 0 || sig > 32) {
return -1;
}
try(env_set_sig_entry(envid));
return syscall_kill(envid, sig);
}

我对于设置sig_entry采取的是在libos.c的libmain中设置入口,在fork时继承入口。然而我在kill函数中保留了之前发送信号时为目标进程设置sig_entry入口的系统调用,尽管此时目标进程一定已经有正确的入口。最后发现删去这个冗余的系统调用就通过了测评,可能是与测评的判断时机不太一致?

0%