可靠信号的处理有两个过程如下:

  1. 信号产生:当这些事件(硬件异常(如除以0)、软件条件(如alarm定时器超时)、终端产生的信号或调用kill函数)发生时,内核会为进程产生一个信号,同时在进程表中设置一个标志。
  2. 信号递送:内核使目标进程对该信号作出反应称为信号递送:或是改变目标进程的执行状态(默认动作),或是开始执行信号处理程序,或两者都是。

可靠信号

信号产生到信号递送的时间间隔内,信号的状态是未决的(pending)。

如果进程对信号设置阻塞递送,而且对该信号的动作是系统默认动作或捕捉该信号,那么内核会为该进程将此信号保持为未决状态,直到进程解除阻塞或忽略该信号。

进程在信号递送给其之前可以改变对信号的处理动作。sigpending函数可以判定哪些信号是阻塞并处于未决状态。

在对信号阻塞期间,该信号产生了多次,POSIX.1允许系统递送该信号一次或多次。如果递送多次,会对该信号进行排队。

事实上,只有POSIX.1的实时扩展信号才会递送多次并且排队,其他信号并不会排队且只递送一次。

每个进程都有一个信号屏蔽字,规定了当前要阻塞递送到该进程的信号集。进程可以调用sigpromask函数来检测和更新其当前信号屏蔽字。

POSIX.1定义了一个新数据类型sigset_t,来表示信号集。信号屏蔽字也使用信号集来表示。

产生信号的函数

kill函数

kill函数将信号发送进程或进程组:

#include <signal.h>
int kill(pid_t pid, int signo);

pid>0时,将信号发送给进程ID为pid的进程。

pid==0时,将信号发送给与调用进程属于统一进程组的所有进程(调用进程需要具有相应权限),系统进程除外。

pid<0时,将信号发送给进程组ID与pid绝对值相等的进程组中所有进程(调用进程需要具有相应权限),系统进程除外。

pid==-1时,将信号发送给调用进程有权限向其发送信号的所有进程,系统进程除外。

权限说明如下:

  1. 超级用户可将信号发送给任一进程。
  2. 非超级用户,发送进程的实际用户ID或有效用户ID与接收进程的实际用户ID或有效用户ID相等。
  3. 如果实现支持_POSIX_SAVED_IDS,则检查接收者的保存设置用户ID(而非有效用户ID)。保存设置用户ID通常是进程的有效用户ID,只有当提升权限或者降低权限时,才会将有效用户ID改为其他值。
  4. 如果发送的信号是SIGCONT,则可将其发送给同会话的任一进程。

POSIX.1将信号编号0定义为空信号,如果kill函数的signo参数为0, 则执行正常的错误检查但不发送信号。

如果信号不阻塞,那么kill返回之前信号就已经递送给目标进程。

raise函数

raise函数允许进程向自身发送信号。

等价于kill(getpid(), signo)

alarm函数

alarm函数可以设置一个定时器,在定时器超时时,会产生SIGALRM信号。如果忽略或不捕捉此信号,默认动作是终止调用进程。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回值:0或以前设置的闹钟时间的余留秒数

seconds指产生信号SIGALRM需要经过的秒数。SIGALRM信号由内核产生,到调度进程执行信号处理函数还需要一个时间间隔。

每个进程只能有一个闹钟时间。

如果调用alarm函数时,之前调用alarm设置的闹钟时间还未超时,则会将其余留值作为本次调用的返回值。 同时如果这次调用alarm函数的参数为0,则仅取消之前的闹钟时间,不会产生新的。

pause函数

pause函数会使调用进程挂起,直到捕捉到一个信号。

#include <unistd.h>
int pause(void);

只有执行了一个信号处理程序并从其返回时,pause函数才会返回。

这种情况下,pause函数返回-1,errno被设置为EINTR

abort函数

abort函数的作用是使进程异常终止。

#include <stdlib.h>
[[noreturn]] void abort(void);
// 无返回值

ISO C要求该函数即使捕捉到信号而且从响应信号处理程序返回,abort函数也不能返回到其调用者

POSIX对其的进一步要求是:

  1. abort函数并不理会进程对SIGABRT信号的忽略或阻塞(仅剩两种动作即默认或者自定义)。
  2. 当捕捉到SIGABRT信号并从其信号处理程序返回时(信号处理程序中没有终止进程),abort函数本身也要终止该进程(执行SIGABRT信号默认动作或者直接退出进程)。
  3. abort函数终止进程时,应该在终止前执行与fclose相同的操作(即刷洗I/O流)。

下面是依据POSIX标准实现的abort函数示例:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void abort1()
{
    struct sigaction action;

    // 如果进程对SIGABRT信号的动作是忽略,则重新设置为默认
    sigaction(SIGABRT, NULL, &action);
    if (action.sa_handler == SIG_IGN)
    {
        action.sa_handler = SIG_DFL;
        sigaction(SIGABRT, &action, NULL);
    }

    // 如果是默认则刷洗所有I/O流,否则由用户自定义的信号处理程序来决定
    if (action.sa_handler == SIG_DFL)
        // 为什么要在这儿刷洗I/O流呢,因为后面接收到SIGABRT信号会导致进程直接退出
        fflush(NULL);

    // 如果进程阻塞SIGABR信号,则解除阻塞
    sigset_t mask;
    sigfillset(&mask);
    sigdelset(&mask, SIGABRT);
    sigprocmask(SIG_SETMASK, &mask, NULL);

    // 向进程自己发送SIGABRT信号
    // 此时在kill返回前产生SIGABRT信号,处理方式是默认行为或者用户自定义动作, 默认方式会终止进程
    kill(getpid(), SIGABRT);

    // 如果执行到这里,说明SIGABRT是用户自定义信号处理程序,表明已经从信号处理程序返回
    // 同时也说明在其信号处理程序中没有退出进程,因此这里就要刷洗I/O流并退出进程(根据POSIX要求)

    // 刷洗所有流
    fflush(NULL);

    // 设置SIGABRT信号处理程序为默认,并发送SIGABRT信号,让进程退出,如果不退出则执行exit强行退出
    action.sa_handler = SIG_DFL;
    sigaction(SIGABRT, &action, NULL);
    sigprocmask(SIG_SETMASK, &mask, NULL);
    // 发送SIGABRT信号,调用系统默认行为
    kill(getpid(), SIGABRT);

    // 退出进程
    exit(1);
}

事实上,对于SIGABRT信号只有两种处理方式,分别是 SIG_DFL或者自定义处理函数abort函数则需要在SIG_DFL时刷洗流,或者在用户自定义程序不退出的时候进行善后(刷洗流以及退出进程)。

system函数

#include <stdlib.h>
int system(const char *command);

system函数的执行流程是:

  1. system函数的调用进程fork一个子进程,然后调用exec函数执行一个shell程序。
  2. shell进程再fork一个子进程,通过exec函数来执行传入system函数的命令字符串。
  3. system函数返回的是shell进程的终止状态,仅当shell进程异常终止时,system函数的返回值才会报告一个异常终止
  4. shell进程会以执行命令字符串进程的终止状态作为其返回值,但如果执行命令字符串带有符号&表示后台执行,则shell进程会立即返回,并且不会将执行命令字符串进程的终止状态作为其返回值

POSIX.1规定system函数忽略SIGINTSIGQUIT信号,阻塞SIGCHLD信号(这里指的是system函数的调用进程,而非创建的子进程)。

阻塞SIGCHLD信号的原因在于如果不阻塞SIGCHLD信号,那么当system创建的子进程结束时,system函数的调用进程就可能会认为自己的子进程结束了(如果该调用进程之前有过创建子进程的话),然后调用wait函数获取子进程的终止状态,导致system函数无法获得其子进程终止状态作为它的返回值

忽略SIGINTSIGQUIT信号的原因在于使用system函数执行命令时,如果不忽略这两个信号,system函数的调用进程就可能被这两个信号中断或这导致进程退出,造成命令的不完整执行

下面是依据POSIX.1 标准实现的system函数

#include <asm-generic/errno-base.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

int system2(const char* cmdstring)
{
    pid_t pid;
    int status;

    if (cmdstring == NULL)
        return 1;

    // 忽略SIGINT与SIGQUIT,阻塞SIGCHLD
    struct sigaction ignore, saveintr, savequit;
    sigset_t chldmask, savemask;
    ignore.sa_handler = SIG_IGN;
    sigemptyset(&ignore.sa_mask);
    ignore.sa_flags = 0;
    if (sigaction(SIGINT, &ignore, &saveintr) != 0)
        return -1;
    if (sigaction(SIGQUIT, &ignore, &savequit) != 0)
        return -1;

    // 阻塞SIGCHLD信号,并保存之前的信号屏蔽字到savemask中,用于之后恢复
    sigemptyset(&chldmask);
    sigaddset(&chldmask, SIGCHLD);
    if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) != 0)
        return -1;

    // 创建子进程执行命令
    if ((pid = fork()) < 0)
        status = -1;
    else if (pid == 0)
    {
        // 子进程
        // 恢复SIGINT与SIGQUIT的默认动作,并且取消阻塞SIGCHLD
        sigaction(SIGINT, &saveintr, NULL);
        sigaction(SIGQUIT, &savequit, NULL);
        sigprocmask(SIG_SETMASK, &savemask, NULL);

        // exec函数执行成功之后不会返回
        // shell程序替换子进程的执行内容,至于shell进程如何执行命令字符串,那就由shell进程来决定了
        // 正常情况下,shell进程会将执行命令字符串进程的终止状态作为其终止状态
        // 但如果命令字符串是后台执行,那么shell进程就会直接返回。
        execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
        // 失败之后会执行下面
        _exit(127);
    }
    else
    {
        // 父进程不恢复SIGINT与SIGQUIT,是为了防止接收到这两个命令导致进程退出,造成子进程执行命令不完整。
        // waitpid函数获取子进程的终止状态,即shell进程的终止状态
        while (waitpid(pid, &status, 0) < 0)
        {
            if (errno != EINTR)
            {
                status = -1;  // 其他异常错误,如果被其他信号中断,则会返回继续等待
                break;
            }
        }
    }

    // 恢复SIGINT与SIGQUIT的默认动作和解除阻塞SIGCHLD
    if (sigaction(SIGINT, &saveintr, NULL) != 0)
        return -1;
    if (sigaction(SIGQUIT, &savequit, NULL) != 0)
        return -1;
    // 解除对SIGCHLD信号的阻塞,在waitpid获取其子进程状态之后,可以防止调用进程获取system函数的子进程的终止状态
    if (sigprocmask(SIG_SETMASK, &savemask, NULL) != 0)
        return -1;

    return status;
}

int main()
{
    system2("date");
}

执行结果如下:

$ ./system2
Mon Jan 22 00:04:09 CST 2024

sleep函数

#include <unistd.h>
unsigned int sleep(unsigned int seconds);
// 返回值: 如果请求休眠的时间达到,返回0;如果被信号处理程序中断,则返回剩余的时间

sleep函数是进程挂起直到满足下面两个条件:

  1. 经过了参数seconds指定的时钟时间。
  2. 调用进程捕捉到一个信号并从信号处理程序返回(被信号中断)。

可以使用alarm函数来实现sleep函数,但是存在问题是无法处理这两个函数之间的影响,以及SIGALRM信号产生的影响。

下面是使用alarm函数实现的sleep函数:

unsigned int sleep3(unsigned int seconds)
{
    struct sigaction newact, oldact;
    sigset_t newmask, oldmask, suspmask;
    unsigned int unslept;

    newact.sa_handler = sig_alrm1;
    sigemptyset(&newact.sa_mask);
    // 1. 设置SIGALRM的信号处理程序
    if (sigaction(SIGALRM, &newact, &oldact) != 0)
        return seconds;

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGALRM);
    // 2. 阻塞SIGALRM信号
    if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0)
        return seconds;

    // 3. 设置时钟,seconds秒后产生SIGALRM信号
    alarm(seconds);

    // 4. 从原有的信号屏蔽字中去除SIGALRM,解除SIGALRM阻塞,并并暂停进程等待接收信号
    suspmask = oldmask;
    sigdelset(&suspmask, SIGALRM);
    sigsuspend(&suspmask); // 原子操作,解除阻塞到进程休眠是原子操作,不会导致信号丢失

    // 5. 检测休眠剩余描述
    unslept = alarm(0);
    // 6. 恢复之前的信号处理程序
    sigaction(SIGALRM, &oldact, NULL);
    // 7. 恢复之前的信号屏蔽字
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    return unslept;
}

问题1:如果调用进程在调用该sleep函数之前,调用过alarm函数,并且恰好在步骤1和2之间产生了SIGALRM信号,这就会导致无法执行用户设定的信号处理函数。

问题2:如果在步骤2和3之间,调用进程之前调用alarm函数而产生的SIGALRM刚好被阻塞,那么步骤4中的sigsuspend函数就会立即返回,而不是等待步骤3中的alarm函数超时。

基于这些问题,现在sleep函数一般都由nanosleep函数来实现。

#include <time.h>
int nanosleep(const struct timespec *req, struct timespec * rem);

nanosleep函数直到参数req指定的时间过去或者被信号中断,才会返回。如果被信号处理程序中断则将剩余时间写入参数rem中,前提是rem!=NULL

nanosleep函数不会产生任何信号,因此没有上面sleep3函数的问题。

nanosleep函数采用的时钟是CLOCK_REALTIME,该时钟时间可能不会被修改,但是系统会进行实时性保证(比如休眠期间时间被NTP修改,系统会忽略这种修改直到休眠指定时长)。

函数clock_nanosleep函数可以指定采用哪种时钟(不同时钟的精度不同),同时也可以指定相对时间休眠或绝对时间休眠。

 #include <time.h>

int clock_nanosleep(clockid_t clockid, int flags,
                   const struct timespec *req,
                   struct timespec * rem);

参数flag取值为0表示相对时间,取值为TIMER_ABSTIME表示绝对时间。reqrem参数与nanosleep函数保持一致。

参数clockid的取值如下所示:

时钟类型说明用途
CLOCK_REALTIME表示系统的实时时间,即当前的日历时间,会受到系统时间调整的影响。用于获取当前的绝对时间
CLOCK_MONOTONIC表示一个单调递增的时钟,不受系统时间调整的影响。用于测量时间间隔和计时
CLOCK_PROCESS_CPUTIME_ID表示当前进程的CPU时间。测量进程使用的CPU时间
CLOCK_THREAD_CPUTIME_ID表示当前线程的CPU时间。测量线程使用的CPU时间

下面的例子说明了这些时钟的用法:

// 示例1,使用CLOCK_REALTIME获取系统当前时间
#include <stdio.h>
#include <time.h>

int main()
{
    struct timespec currentTime;

    // 获取当前绝对时间
    clock_gettime(CLOCK_REALTIME, &currentTime);

    // 将秒和纳秒打印出来
    printf("当前绝对时间:秒:%ld,纳秒:%ld\n", currentTime.tv_sec, currentTime.tv_nsec);

    return 0;
}
// $ ./get_current_time_by_CLOCK_REALTIME
// 当前绝对时间:秒:1705936900,纳秒:59752278

// 示例2展示了进程占用的CPU时间、线程占用的CPU时间、以及测量的时间间隔
#include <pthread.h>
#include <stdio.h>

// 线程函数
void* threadFunc(void* arg)
{
    struct timespec startTime, endTime;
    long elapsedTime;

    // 获取起始时间
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &startTime);

    // 执行一些操作
    for (int i = 0; i < 50000; i++)
    {
        for (int j = 0; j < 4000; j++)
        {
        }
    }

    // 获取结束时间
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &endTime);

    // 计算CPU时间(秒)
    elapsedTime = (endTime.tv_sec - startTime.tv_sec) * 1000000000 + (endTime.tv_nsec - startTime.tv_nsec);

    printf("线程使用CPU时间:%ld 纳秒\n", elapsedTime);

    return NULL;
}

int main()
{
    struct timespec startTime, endTime;
    long elapsedTime;

    // 获取起始时间
    clock_gettime(CLOCK_MONOTONIC, &startTime);

    // 执行一些操作

    {
        struct timespec startTime1, endTime1;
        long elaspedTime1;

        // 获取起始时间
        clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &startTime1);

        // 执行一些操作
        {
            pthread_t thread;

            // 创建线程
            pthread_create(&thread, NULL, threadFunc, NULL);

            // 等待线程结束
            pthread_join(thread, NULL);
        }

        // 获取结束时间
        clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &endTime1);

        // 计算CPU时间(秒)
        elaspedTime1 = (endTime1.tv_sec - startTime1.tv_sec) * 1000000000 + (endTime1.tv_nsec - startTime1.tv_nsec);

        printf("进程使用CPU时间:%ld 纳秒\n", elaspedTime1);
    }
    // 获取结束时间
    clock_gettime(CLOCK_MONOTONIC, &endTime);

    // 计算时间间隔(毫秒)
    elapsedTime = (endTime.tv_sec - startTime.tv_sec) * 1000000000 + (endTime.tv_nsec - startTime.tv_nsec);

    printf("时间间隔:%ld 纳秒\n", elapsedTime);

    return 0;
}
// 执行结果如下:
// $ ./get_thread_cpu_time
// 线程使用CPU时间:281239559 纳秒
// 进程使用CPU时间:281451290 纳秒
// 时间间隔:281492002 纳秒
// 可以看到这些时间之间存在微小的变化

示例

使用alarmpause,进程使自己休眠一段指定的时间

#include <signal.h>
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>

static void sig_alrm1(int signo)
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    printf("date:%ld\n", tv.tv_sec);
    printf("sig alrm1\n");
}
static void sig_alrm2(int signo)
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    printf("date:%ld\n", tv.tv_sec);
    printf("sig alrm2\n");
}

unsigned int sleep1(unsigned int seconds)
{
    // 1. 根据alarm返回检查之前是否有设置alarm
    unsigned int leftseconds = alarm(seconds);
    printf("leftseconds=%u\n", leftseconds);
    if (leftseconds > 0)
    {
        // 之前的alarm还有leftseconds 秒超时
        unsigned int diff = 0;
        if (leftseconds < seconds)
        {
            // 使用两段休眠,先使之前alarm超时, 在重新设置剩余时间
            alarm(leftseconds);
            pause();

            // 计算第二段需要休眠的时间并重新设置信号处理程序
            diff = seconds - leftseconds;
            printf("diff=%u\n", diff);
            if (signal(SIGALRM, sig_alrm2) == SIG_ERR)
                return (diff);
            alarm(diff);
            pause();
            return (alarm(0));
        }
        else
        {
            // 之前设置的alarm的剩余时间大于现在设置,采用两段休眠
            // 备份之前的信号处理函数,退出之前恢复
            void* prev = signal(SIGALRM, sig_alrm2);
            if (prev == SIG_ERR)
                return (seconds);
            alarm(seconds);
            pause();

            // 恢复之前信号处理配置以及剩余时间
            diff = leftseconds - seconds;
            printf("diff=%u\n", diff);
            if (signal(SIGALRM, prev) == SIG_ERR)
                return 0;  // 恢复之前的信号处理程序失败
            return (alarm(diff));
            // 这里不用pause,是用来恢复之前设置的alarm
        }
    }
    else
    {
        // alarm返回0,之前没有设置alarm
        void* prev = signal(SIGALRM, sig_alrm2);
        if (prev == SIG_ERR)
            return (seconds);
        alarm(seconds);
        pause();
        return (alarm(0));
    }
}

int main()
{
    if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
    {
        perror("signal");
        return -1;
    }
    struct timeval tv;
    gettimeofday(&tv, NULL);
    printf("date:%ld\n", tv.tv_sec);
    // 第一次设置alarm
    alarm(20);

    sleep(5);

    printf("before sleep\n");
    // 里面会是第二次设置alarm,看看退出时会不会恢复
    sleep1(10);
    printf("after sleep\n");

    gettimeofday(&tv, NULL);
    printf("date:%ld\n", tv.tv_sec);
    while (1)
    {
        sleep(1);
    }
}

执行结果如下:

$ ./sleep1
date:1705091044
before sleep
leftseconds=15
date:1705091059
sig alrm2
diff=5
after sleep
date:1705091059
date:1705091064
sig alrm1
^C

sleep1函数中会检查第一次调用alarm的返回值:

  1. 如果其值大于0并且小于本次调用alarm的值,则恢复之前的闹钟并等其超时,然后设置差值作为本次alarm参数值;
  2. 如果其值大于本次设置值,则会使用本次设置值先调用alarm,再在sleep1函数之前使用差值来重置之前的闹钟(包括信号处理程序)。

存在问题:alarmpause函数之间存在竞争条件,如果alarm函数在调用pause之前超时,并调用了信号处理程序,则pause会使进程永久阻塞。

使用setjmplongjmp避免竞争条件

#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
static jmp_buf env_alrm;
static void sig_alrm1(int signo)
{
    printf("sig alrm1\n");
    longjmp(env_alrm, 1);  // 设置setjmp的返回值为1
}
unsigned int sleep2(unsigned int seconds)
{
    if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
        return (seconds);

    if (setjmp(env_alrm) == 0)
    {
        alarm(seconds);
        pause();
    }
    return (alarm(0));
}

static void sig_int(int signo)
{
    int i, j;
    volatile int k;
    printf("\nsig_int starting\n");
    for (i = 0; i < 300000; i++)
    {
        for (j = 0; j < 300000; j++)
            k = i + j;
    }
    printf("sig_int finished\n");
}

int main()
{
    unsigned int unslept;
    if (signal(SIGINT, sig_int) == SIG_ERR)
        perror("signal");
    unslept = sleep2(5);
    printf("sleep2 returned: %u\n", unslept);
    exit(0);
}

执行结果如下:

$ ./sleep2
^C
sig_int starting # sig_int信号处理程序中断
sig alrm1 # longjmp执行完之后不会恢复之前中断的sig_int信号处理程序
sleep2 returned: 0

在这个例子中,使用setjmplongjmp函数解决上个例子中的竞争问题。

但是如果SIGALRM中断了其他信号处理函数,longjmp执行之后,就会提前终止该信号处理程序,因为longjmp函数会清理栈空间。

这两个例子说明在涉及信号处理时,需要考虑周到、不影响其他代码段才行

使用alarm对可能阻塞的操作设置上限值

下面的例子是从标准输入读一行(低速系统调用,可能阻塞),然后写入到标准输出中。

#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
static jmp_buf env_alrm;
static void sig_alrm1(int signo)
{
    printf("sig alrm1\n");
    longjmp(env_alrm, 1);  // 设置setjmp的返回值为1
}
unsigned int sleep2(unsigned int seconds)
{
    if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
        return (seconds);

    if (setjmp(env_alrm) == 0)
    {
        alarm(seconds);
        pause();
    }
    return (alarm(0));
}

int main()
{
    int n;
    char line[1024];
    if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
    {
        perror("signal");
        exit(00);
    }

    if (setjmp(env_alrm) != 0)
    {
        printf("read timeout");
        exit(-1);
    }
    alarm(10);
    if ((n = read(STDIN_FILENO, line, sizeof(line))) < 0)
        perror("read");
    alarm(0);

    write(STDOUT_FILENO, line, n);
    exit(0);
}

执行结果如下:

$ ./read_timeout_longjmp
12
12
$ ./read_timeout_longjmp
sig alrm1
read timeout

该实例与上个示例存在相同的问题,与其他信号处理程序交互会导致其他信号处理程序不正常退出