buaa os lab4-challenge 信号系统的实现

发布时间 2023-06-13 18:20:25作者: clapp

buaa os lab4-challenge 信号系统的实现

信号是什么

  • 生活中我们会收到各种各样的信号,比如老师在群里布置了一个新的ddl,或者肚子发出咕咕的叫声提醒我们该吃饭了,接收到信号之后我们并不是马上处理,需要等到一些合适的时机并前横利弊,比如对于人来说肯定是吃饭重要,所以我们会忽略ddl的信号,先去食堂满足食欲。
  • 操作系统当中也有这样的一套信号机制。信号是操作系统发送给进程的一种消息,收到信号的进程会打断当前的正常执行(原子操作除外),如果进程注册了相应的处理函数就会跳转到那里去处理信号,否则执行系统默认的处理函数。除了进程通过系统调用向另一进程(或它自身)发出的信号,内核还可以将发生的中断通过信号通知给引发中断的进程。
  • 我们需要在MOS中实现的信号系统需要满足以下一些条件:
    1. 后来的信号先处理。
    2. 对于编号\(9,11,15\)的信号系统默认处理方式为终止进程,其余信号默认处理方式为忽略。
    3. 需要实现信号的重入。
    4. 子进程需要继承父进程的信号处理函数。
    5. 需要实现信号的阻塞,通过掩码来表示信号的阻塞与否。

信号的表示

  • 信号的表示采用如下方式:
    as
  • 每个进程也需要记录自己收到的信号,以下信息需要放入进程控制块中记录:
    as

信号的发送

  • 进程间通过kill函数发送信号,这个比较容易处理,通过新建syscall来实现即可。
    as

  • 遇到访问非法地址错误时需要通过MOS给相应进程发送信号。在添加信号处理函数之前MOS处理tlb异常是进入_do_tlb_refill函数进行panic,因此我们需要对这个函数修改,由于都是在内核态,可以直接调用sys_kill来发送。
    as
    但发送完毕之后不能简单返回,因为如果返回参数不当还会重新造成访存异常导致死循环。我们在tlb_asm.S中找到_do_tlb_refill的调用者,发现引起上述问题的原因在于,返回之后还需要利用返回值做如下操作
    as
    因此这里有个简单的解决办法就是新建函数do_adress_too_low直接跳转jr,这个办法的正确性还需要结合后文信号处理环节来看待。

信号的处理

  • 信号处理首先要解决一个问题就是用户信号处理函数是从内核态跳转过去的,并非通过jal jr的配合来实现正常的函数调用,如果直接跳转的话会造成PC跳飞。我们想到写时复制异常的处理也是在用户态完成,也面临同样的问题,我们可以借鉴cow_entry的方法,用一个统一的入口包裹真正的信号处理函数,从内核态返回用户态时先进入统一的入口函数,同时向用户态传递Trapframe以及真正处理函数的函数指针,处理完毕后调用set_trapframe设置进程继续执行需要的CPU现场,重新陷入内核态,从而解决上述问题。(这个方法由于使用了栈结构保存信息,还顺带解决了重入以及写时复制的问题,非常的好用。
    as

  • 信号的处理方式非常简单,从前往后扫描进程的信号队列,一旦遇到可以处理的信号立即停止扫描并去处理相应的信号。
    as

  • 然后需要思考的是信号处理的时机,由于存在进程自己给自己发送信号的情况,所以仅仅只在进程切换的时候处理信号是不行的,最好的位置就是在每次陷入内核态准备返回的前一步扫描信号队列进行处理。
    as

  • 使用这个方法就会出现一个问题,我们在一个信号处理完成之前也会进行一些系统调用,而这些系统调用在返回之前同样也会扫描信号队列,如何防止系统转而去执行更早到达的信号呢?无脑阻塞肯定是不行的,我们同时也要允许处理完成之前晚于当前信号到达的信号能被先处理,这里我们前面提到的sigpri参数就派上了用场,记录信号到达的时间戳,处理一个信号之前先将进程的sigpri设置成此信号的到达时间戳,后续再扫描信号的队列的时候放弃到达时间戳低于sigpri的信号即可。
    as

  • 另一个问题就是在为进程设置sigpri之前也可能会发生一些异常产生异常重入的现象,那么这个时候如何保证不被低时间戳的信号取代呢,其实异常重入的时候我们完全没必要处理信号,重入的时候不可能产生任何新的信号,因为这个时候时钟中断已经被屏蔽,也不可能产生系统调用,只能是内存访问时出现的异常(对MOS来说)。一次我们只需要在do_signal函数开始执行之前判断是否是异常重入的情况,如果是就直接return
    as

  • 笔者在实现的时候还想过一个问题,遍历信号队列的时候可能会有多个信号,如何在处理完一个信号的时候再次返回这里遍历呢。后来发现纯纯多余,找到一个信号之后直接break就行了,因此从用户态返回内核之前都会进行设置trapframe的系统调用,这个调用返回之前就又会进入do_signal遍历,所以能一直把所有能处理的信号都处理完毕。

  • 一些do_signal函数中参数传递的细节。
    as

  • 剩下一些其他要求的实现就非常非常简单了。

测试思路

  • 基础功能测试,向自身/别的进程发信号,空指针测试,fork测试。
  • 特殊信号的测试,\(9,11,15\)号信号。
  • 重入测试,在用户态的handler中触发空指针异常,在handler中触发fork,在handler中调用kill。
  • 信号阻塞测试,信号执行顺序(即是否严格满足后到达的信号先处理)。
  • 篇幅有限,这边给出几个比较简单的测试用例。

测试用例1:

#include <lib.h>

int global = 0;
void handler(int num) {
    debugf("Reach handler, now the signum is %d!\n", num);
    global = 1;
}

#define TEST_NUM 2
int main(int argc, char **argv) {
    sigset_t set;
    sigemptyset(&set);
    struct sigaction sig;
    sig.sa_handler = handler;
    sig.sa_mask = set;
    panic_on(sigaction(TEST_NUM, &sig, NULL));
    sigaddset(&set, TEST_NUM);
    panic_on(sigprocmask(0, &set, NULL));
    kill(0, TEST_NUM);
    int ans = 0;
    for (int i = 0; i < 10000000; i++) {
        ans += i;
    }
    panic_on(sigprocmask(1, &set, NULL));
    debugf("global = %d.\n", global);
    return 0;
}

运行结果:
as

测试用例2:

#include <lib.h>

int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *test = NULL;
void sgv_handler(int num) {
    debugf("Segment fault appear!\n");
    test = &a[0];
    debugf("test = %d.\n", *test);
    exit();
}

int main(int argc, char **argv) {
    sigset_t set;
    sigemptyset(&set);
    struct sigaction sig;
    sig.sa_handler = sgv_handler;
    sig.sa_mask = set;
    panic_on(sigaction(11, &sig, NULL));
    *test = 10;
    debugf("test = %d.\n", *test);
    return 0;
}

运行结果:

as

测试用例3:

#include <lib.h>

sigset_t set2;

int main(int argc, char **argv) {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, 1);
    sigaddset(&set, 2);
    panic_on(sigprocmask(0, &set, NULL));
    sigdelset(&set, 2);
    int ret = fork();
    if (ret != 0) {
        panic_on(sigprocmask(0, &set2, &set));
        debugf("Father: %d.\n", sigismember(&set, 2));
    } else {
        debugf("Child: %d.\n", sigismember(&set, 2));
    }
    return 0;
}

运行结果:
as

测试用例4:

#include <lib.h> 
sigset_t set2;

void handler(int num){
    static int cnt=0;
    cnt++;
    debugf("cnt:%d HANDLER:%x %d\n",cnt,syscall_getenvid(),num);
    if(cnt==5) {
        debugf("CONGRATULATION:TEST PASSED!\n");
        kill(0,SIGKILL);
    }
}

int main(int argc, char **argv) {
    sigset_t set;
    set.sig[0]=set.sig[1]=0;
    sigaddset(&set, 10);
    struct sigaction act;
    act.sa_mask=set;
    act.sa_handler=handler;
    sigaction(10,&act,NULL);
    int ret = fork();
    if (ret != 0) {
        for (int i = 0; i < 5; i++)
        {
            kill(ret, 10);
        }
    } else {
        while(1);
    }
    return 0;
}

运行结果:

as

测试用例5:

#include <lib.h>
void handler1(int num){
    kill(0,2);
    debugf("handler1 arrive!\n");
}
void handler2(int num){
    kill(0,3);
    debugf("handler2 arrive!\n");
}
void handler3(int num){
    debugf("handler3 arrive!\n");
}
int main(){
   sigset_t a;
   a.sig[1]=0;
   a.sig[0]=0;
   struct sigaction act;
   act.sa_mask=a;
   act.sa_handler=handler1;
   sigaction(1,&act,NULL);
   act.sa_handler=handler2;
   a.sig[0]|=(1<<2);
   act.sa_mask=a;
   sigaction(2,&act,NULL);
   act.sa_mask.sig[0]=0;
   act.sa_handler=handler3;
   sigaction(3,&act,NULL);
   kill(0,1);
}

运行结果:

as

测试用例6:

#include <lib.h>
void handler1(int num){
    kill(0,3);
    sigset_t a;
    sigemptyset(&a);
    sigprocmask(2,&a,NULL);
    debugf("handler1 arrive!\n");
}
void handler2(int num){
    debugf("handler2 arrive!\n");
}
void handler3(int num){
    debugf("handler3 arrive!\n");
}
void handler4(int num){
    debugf("handler4 arrive!\n");
}
int main(){
   sigset_t a;
   sigemptyset(&a);
   struct sigaction act;
   act.sa_mask=a;
   act.sa_handler=handler1;
   sigaction(1,&act,NULL);
   act.sa_handler=handler2;
   sigaction(2,&act,NULL); 
   act.sa_handler=handler3;
   sigaction(3,&act,NULL);
   act.sa_handler=handler4;
   sigaction(4,&act,NULL);
   a.sig[0]^=2;
   sigprocmask(2,&a,NULL);
   kill(0,2);
   kill(0,1);
   sigemptyset(&a);
   sigprocmask(2,&a,NULL);
}

运行结果:

as

感想与展望

  • 本文已经几乎给出了信号系统实现的所有代码,可以看出代码实现非常简洁,有难度的地方在于对整个操作系统的理解,这不像之前的程序填空题,虽然只是单个lab的challenge,但实现起来需要对整个学期的操作系统知识的串联。虽然实现的过程比较痛苦,笔者花了整整10个小时才完成相关工作,但看到程序成功输出了正确的结果之后喜悦之情溢于言表,对于操作系统的认识也上了一个新的台阶。
  • 由于是在烤漆完成的挑战性任务,时间有限,笔者实现的信号系统还有许多优化的空间,比如为新信号分配内存空间的时候,笔者直接将整个页面送给信号,这样做是非常浪费空间的,可以专门实现一个管理存储信号空间的分配系统;还有信号的处理是利用用户异常栈实现的,而此栈的空间非常有限,MOS中只有一个页面的大小,因此如果每次仅仅无脑压栈可以处理的信号数量比较有限,这也是一个很大的优化空间。希望考期结束了笔者可以继续完善这些瑕疵。