信号处理函数访问互斥量导致死锁问题的总结

1.背景

        

        下面要说明的是信号处理函数中调用了非异步信号安全函数导致的问题,如果读到这里就了解了标题中问题的原因,就可以不用再往下读了。
        在项目开发过程中,经常会使用多进程的方式。然而在对已结束子进程回收资源的处理上,可能会遇到本文所描述的问题。问题是,在捕获到子进程结束信号,对子进程进行资源回收时,想通过调用封装好的日志输出接口打印进程的一些信息,结果导致父进程异常了,某个线程或整个进程被永久阻塞了。

2.验证

 为了复现这个问题,我写了如下程序:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <thread>
#include <mutex>

#define TEST_LOCK_IN_SIG             1

std::mutex my_mutex;

void LockToHandle(const std::string name) {
#if TEST_LOCK_IN_SIG
    my_mutex.lock();
    printf("%s lock\n", name.c_str());
    // do something...
    sleep(1);
    my_mutex.unlock();
    printf("%s unlock\n", name.c_str());
#endif
}

void sighandle(int sig_val) {
    printf("sighandle: %d\n", sig_val);
    switch (sig_val)
    {
    case SIGCHLD: {
        pid_t pid = 0;
        do {
            int status = 0;
            pid = waitpid(-1, &status, WNOHANG);
            if (pid > 0) { 
                if (WIFEXITED(status)) {
                    printf("pid: %d exit, status: %d\n", pid, status);
                }
            }
        } while(pid > 0);
        printf("sighandle ready to lock\n");
        LockToHandle("sighandle");
        printf("to sleep\n");
        sleep(5);
        printf("sleep done\n"); 
        
    }break;
    
    default:
        break;
    }
}

void CreateThread() {
    std::thread thread = std::thread([](void) {
        while(true) {
            static int j = 0;
            printf("parent thread 2 process\n");
            sleep(1);
            // LockToHandle("thread 2");
            if (++j > 3) {
                printf("parent thread 2 done\n");
                return;
            }
        }
    });
    thread.detach();
}

int main(int argc, char *argv[]) {
    sigset_t oldmask;
    sigset_t mask;
    signal(SIGCHLD, sighandle);

    sigemptyset(&mask);
    sigaddset(&mask, SIGCHLD);

    pid_t pid = fork();
    if (pid > 0) {
        // parents
        printf("child pid: %d\n", pid);
        CreateThread();
        
        // pthread_sigmask(SIG_BLOCK,&mask,&oldmask);

        while (true) {
            static int i = 0;
            printf("parent thread 1 process\n");
            sleep(1);

            LockToHandle("thread 1");
            if (++i > 3) {
                printf("parent thread 1 process done\n");
                return 0;
            }
        }
    } else if (pid == 0) {
        // child process
        printf("child process done\n");
        sleep(1);
        return 1;
    } else {
        printf("fork error\n");
        return -1;
    }

    return 0;
}

运行日志1如下:

child pid: 23436
child process done
parent thread 1 process
parent thread 2 process
parent thread 2 process
thread 1 lock
sighandle: 17
pid: 23436 exit, status: 256
sighandle ready to lock
parent thread 2 process
parent thread 2 process
parent thread 2 done
死机不再输出日志

打开line 58,line 82,屏蔽line 89,重新编译后运行日志2如下:

child pid: 23551
child process done
parent thread 1 process
parent thread 2 process
thread 2 lock
parent thread 1 process
sighandle: 17
pid: 23551 exit, status: 256
sighandle ready to lock
parent thread 1 process
parent thread 1 process
parent thread 1 process done
运行结束

3.分析与结论
      

        日志1中,由主线程处理了信号,在日志1的06行主线程获取了锁,主线程因为捕获到子进程退出的信号而被打断,信号处理函数阻塞了主线程导致“parent thread 1 process”不再输出,信号处理函数又无法获取到锁导致代码41行不能输出,触发死锁。由日志“parent thread 2 process”可以看出线程2并没有受到影响,因为代码58行对锁的操作被屏蔽。最终整个进程无法结束。

        日志2中,由于代码82行被打开,主线程不再处理SIGCHLD信号。代码58行打开,89行屏蔽,可以看到捕获信号时,线程2被阻塞,信号处理函数仍然无法获取到锁,触发死锁由“parent thread 1 process”可以看出主线程(线程1)并没有受到影响。最终主线程运行结束使整个进程运行结束。

        由于日志输出接口大部分会被设计成多线程安全访问的接口(我们的项目设计也是如此),并且这种设计也是正确的。当进程捕获到子进程状态改变的信号时,会中断接收信号的线程,并跳转到信号处理函数中,在信号处理函数中,如果调用了同一个日志输出接口,就会与被中断的线程争夺资源,如果很不幸被中断的线程正好获得一把互斥锁还未及时释放就被中断,那么信号处理函数就会永久等待这把无法得到释放的锁,从而导致接受信号的线程出现异常。如果这个接口还被其他多个线程使用,那么这些线程都会出现永远获取不到锁的现象,导致整个进程功能异常。



4.解决方法
      

        尽量不要在信号处理函数中获取锁。如果一定要使用锁,需要确保捕获信号的线程(最好为主线程)一定不能与信号处理函数中访问的互斥量为同一个。
        建议可以在信号处理函数中只是做简单的记录,在其他线程中获取记录并输出日志。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值