对于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中会重新选择新的信号,这种行为可能会导致处理顺序不一致的问题。
最后修改于:
最后回复于:
回复主题帖
想问问同学关于第九点是怎么处理的呢?我的实现在从do_signal跳转到sig_entry前有时候仍然会出现很多异常或者重入,导致出现问题,从而无法得到正确的处理顺序。
1 | void do_signal(struct Trapframe *tf) { |
最后修改于:2024-06-19 20:06:52
(如果不做这个判断的话,可能会导致debug的时候栈上内容被修改,加上进程调度的一定特性使得程序无法正常运行。但是根据亲身经历来看的话,这一点是不影响评测的。)
如果是还未返回用户态的sig_entry的话,那么异常大概率是对trapframe 进行 memcpy时出现缺页异常,保存的现场的cp0.epc是内核态的,按照你的处理来看,内核态异常在返回时是会跳过这一次do_signal处理的,理论上没有问题。
如果出现异常的话,有没有可能是你将栈上的某些数据放入了进程控制块,而根据第7点产生了对栈上数据的修改?
另外,对于你说的处理顺序不正确的问题,是你自己测试的结果还是评测点没有通过呢?因为大家在debug的时候会把输出放在用户态,这样的话考虑一种情况:有2.3.4(举个例子,不一定现实会产生这样的信号)信号待处理,那么,我们首先选择2号信号,然后进入用户态处理程序,假如未输出即再次进行内核态,此时再进入do_signal时,会选择3号信号,依次类推,可能你最终表面上看到的最先处理信号反而是4号。
最后修改于:2024-06-19 20:56:39
1 | u_int pid = syscall_getenvid(); |
我的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 | FEXPORT(ret_from_exception) |
最后修改于:2024-06-19 23:06:46
同学你好,请问你的默认终止进程处理是在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中完成处理。
希望我的观点能够起到帮助。
最后修改于:2024-06-20 01:28:15
补充一下我最后发现的问题:
1 | #include <lib.h> |
对于这个测试程序,可能会表现出来如下的输出
1 | [00001001] destroying 00001001 |
由于do_signal先处理SIGKILL,但是在跳转到sig_entry的时候发生了缺页异常,这个时候位于用户空间的缺页异常ret_from_exception的时候会重入do_signal,就会去处理SIGCHLD,表现出来的就是先进入SIGCHLD的处理程序,然后再执行SIGKILL的默认exit处理,这个好像是可以接受的,符合对于处理信号时机的要求。
1 | int kill(u_int envid, int sig) { |
我对于设置sig_entry采取的是在libos.c的libmain中设置入口,在fork时继承入口。然而我在kill函数中保留了之前发送信号时为目标进程设置sig_entry入口的系统调用,尽管此时目标进程一定已经有正确的入口。最后发现删去这个冗余的系统调用就通过了测评,可能是与测评的判断时机不太一致?
最后修改于:2024-06-20 18:18:09