bug诞生记——信号(signal)处理导致死锁(代码片段)

breaksoftware breaksoftware     2022-12-09     656

关键词:

        这个bug源于项目中一个诡异的现象:代码层面没有明显的锁的问题,但是执行时发生了死锁一样的表现。我把业务逻辑简化为:父进程一直维持一个子进程。(转载请指明出于breaksoftware的csdn博客)

       首先我们定义一个结构体ProcessGuard,它持有子进程的ID以及保护它的的锁。这样我们在多线程中,可以安全的操作这个结构体。

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

struct ProcessGuard 
    pthread_mutex_t pids_mutex;
    pid_t pid;
;

        主进程的主线程启动一个线程,用于不停监视ProcessGuard的pid是否为0(即子进程不存在)。如果不存在就创建子进程,并把进程ID记录到pid中;

void chile_process() 
    while (1) 
        printf("This is the child process. My PID is %d.My thread_id is %lu.\\n", getpid(), pthread_self());
        sleep(1);
    


void create_process_routine() 
    printf("This is the child thread of parent process. My PID is %d.My thread_id is %lu.\\n", getpid(), pthread_self());
    while (1) 
        int child = 0;
        if (child == 0) 
            pthread_mutex_lock(&g_guard->pids_mutex);
        
        
        if (g_guard->pid != 0) 
            continue;    
        
        
        pid_t pid = fork();
        sleep(1);
        printf("Create child process %d.\\n", pid);

        if (pid < 0) 
            perror("fork failed");
        
        else if (pid == 0) 
            chile_process();
            child = 1;
            break;
        
        else 
            // parent process
            g_guard->pid = pid;
            printf("dispatch task to process. pid is %d.\\n", pid);
        

        if (child == 0) 
            pthread_mutex_unlock(&g_guard->pids_mutex);  
        
        else 
            break;
        
    

        我们在父进程的主线程中注册一个signal监听。如果子进程被杀掉,则将ProcessGuard中pid设置为0,这样父进程的监控线程将重新启动一个进程。

void sighandler(int signum) 
    printf("This is the parent process.Catch signal %d.My PID is %d.My thread_id is %lu.\\n", signum, getpid(), pthread_self());
    pthread_mutex_lock(&g_guard->pids_mutex);
    g_guard->pid = 0;
    pthread_mutex_unlock(&g_guard->pids_mutex); 

        最后看下父进程,它初始化一些结构后,注册了signal处理事件并启动了创建子进程的线程。

int main(void) 
    pthread_t creat_process_tid;

    g_guard = malloc(sizeof(struct ProcessGuard));
    pthread_mutex_t pids_mutex;
    if (pthread_mutex_init(&g_guard->pids_mutex, NULL) != 0) 
        perror("init pids_mutex error.");
        exit(1);
    
    g_guard->pid = 0;

    printf("This is the Main thread of parent process.PID is %d.My thread_id is %lu.\\n", getpid(), pthread_self());

    signal(SIGCHLD, sighandler);

    pthread_create(&creat_process_tid, NULL, (void*)create_process_routine, NULL);

    while(1)  
        printf("Get task from network.\\n");
        sleep(1);
    
    
    pthread_mutex_destroy(&g_guard->pids_mutex);

    return 0;

        上述代码,我们看到锁只在线程函数create_process_routine和signal处理函数sighandler中被使用了。它们两个在代码层面没有任何调用关系,所以不应该出现死锁!但是实际并非如此。

        我们运行程序,并且杀死子进程,会发现主进程并没有重新启动一个新的子进程。

$ ./test      
This is the Main thread of parent process.PID is 17641.My thread_id is 140014057678656.
Get task from network.
This is the child thread of parent process. My PID is 17641.My thread_id is 140014049122048.
Create child process 17643.
dispatch task to process. pid is 17643.
Create child process 0.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
Get task from network.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
Get task from network.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
Get task from network.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
Get task from network.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
Get task from network.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
Get task from network.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
This is the child process. My PID is 17643.My thread_id is 140014049122048.
Get task from network.
This is the parent process.Catch signal 17.My PID is 17641.My thread_id is 140014049122048.
Get task from network.
Get task from network.
Get task from network.
Get task from network.
Get task from network.

        这个和我们代码设计不符合,而且不太符合逻辑。于是我们使用gdb attach主进程。

Attaching to process 17641
[New LWP 17642]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f578fb7a9d0 in __GI___nanosleep (requested_time=requested_time@entry=0x7fffd2b41190, remaining=remaining@entry=0x7fffd2b41190) at ../sysdeps/unix/sysv/linux/nanosleep.c:28
28      ../sysdeps/unix/sysv/linux/nanosleep.c: No such file or directory.
(gdb) info threads
  Id   Target Id         Frame 
* 1    Thread 0x7f57902be740 (LWP 17641) "test" 0x00007f578fb7a9d0 in __GI___nanosleep (requested_time=requested_time@entry=0x7fffd2b41190, remaining=remaining@entry=0x7fffd2b41190)
    at ../sysdeps/unix/sysv/linux/nanosleep.c:28
  2    Thread 0x7f578fa95700 (LWP 17642) "test" __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
(gdb) t 2
[Switching to thread 2 (Thread 0x7f578fa95700 (LWP 17642))]
#0  __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
135     ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S: No such file or directory.
(gdb) bt
#0  __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1  0x00007f578fe91023 in __GI___pthread_mutex_lock (mutex=0x55c51383e260) at ../nptl/pthread_mutex_lock.c:78
#2  0x000055c512c29a9d in sighandler ()
#3  <signal handler called>
#4  __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:133
#5  0x00007f578fe91023 in __GI___pthread_mutex_lock (mutex=0x55c51383e260) at ../nptl/pthread_mutex_lock.c:78
#6  0x000055c512c29b42 in create_process_routine ()
#7  0x00007f578fe8e6db in start_thread (arg=0x7f578fa95700) at pthread_create.c:463
#8  0x00007f578fbb788f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

        我们查看线程2的调用栈,发现栈帧5和栈帧1锁住了相同的mutex(0x55c51383e260)。而我们线程代码中锁是加/解成对,那么第二个锁是哪儿来的呢?

        我们看到栈帧1的锁是源于栈帧2对应的函数sighandler,即下面代码

void sighandler(int signum) 
    printf("This is the parent process.Catch signal %d.My PID is %d.My thread_id is %lu.\\n", signum, getpid(), pthread_self());
    pthread_mutex_lock(&g_guard->pids_mutex);
    g_guard->pid = 0;
    pthread_mutex_unlock(&g_guard->pids_mutex); 

        于是,问题来了。我们在线程函数create_process_routine中从来没有调用sighandler,那这个调用是哪儿来的?

        在linux文档http://man7.org/linux/man-pages/man7/signal.7.html中,我们发现了有关signal的这段话

A process-directed signal may be delivered to any
one of the threads that does not currently have the signal blocked.
If more than one of the threads has the signal unblocked, then the
kernel chooses an arbitrary thread to which to deliver the signal.

        这句话是说process-directed signal会被投递到当前没有被标记不接受该signal的任意一个线程中。 具体是哪个,是由系统内核决定的。这就意味着我们的sighandler可能在主线程中执行,也可能在子线程中执行。于是发生了我们上面的死锁现象。

        那么如何解决?官方的方法是使用sigprocmask让一些存在潜在死锁关系的线程不接收这些信号。但是这个方案在复杂的系统中是存在缺陷的。因为我们的工程往往使用各种开源库或者第三方库,我们无法控制它们启动线程的问题。所以,我的建议是:在signal处理函数中,尽量使用无锁结构。通过中间数据的设计,将复杂的业务代码和signal处理函数隔离。

bug诞生记——临时变量栈变量导致的双杀(代码片段)

    这是《bug诞生记》的第一篇文章。本来想起个文艺点的名字,比如《Satan(撒旦)来了》,但是最后还是想让这系列的重心放在“bug的产生过程”和“缺失的知识点”上,于是就有了本系列这个稍微中性... 查看详情

signal之——异步回收机制

前言:回收子进程之前用了wait()和非阻塞模型,今天学了信号以后可以使回收机制更上一层楼,通过信号函数,父进程只需要做自己的事情,接收到信号后就触发信号函数。信号处理函数可能会出现的bug:  1.受到停止信号也会... 查看详情

bug诞生记——不定长参数隐藏的类型问题(代码片段)

    这个bug的诞生源于项目中使用了一个开源C库。由于对该C库API不熟悉,一个不起眼的错误调用,导致一系列诡异的问题。最终经过调试,我们发现发生了内存覆盖问题。为了直达问题根节,我将问题代码简化... 查看详情

bug诞生记——不定长参数隐藏的类型问题(代码片段)

    这个bug的诞生源于项目中使用了一个开源C库。由于对该C库API不熟悉,一个不起眼的错误调用,导致一系列诡异的问题。最终经过调试,我们发现发生了内存覆盖问题。为了直达问题根节,我将问题代码简化... 查看详情

bug诞生记——隐蔽的指针偏移计算导致的数据错乱(代码片段)

    C++语言为了兼容C语言,做了很多设计方面的考量。但是有些兼容设计产生了不清晰的认识。本文就将讨论一个因为认知不清晰而导致的bug。(转载请指明出于breaksoftware的csdn博客)classBasepublic:Base()=defau... 查看详情

信号量锁导致死锁[重复]

】信号量锁导致死锁[重复]【英文标题】:Semaphorelockresultinginadeadlock[duplicate]【发布时间】:2021-05-0911:06:12【问题描述】:publicclassSampleDataprivatestaticreadonlySemaphorepool=newSemaphore(0,1);publicstringData=>getFromFile();privatestaticst 查看详情

信号---早期signal函数和现代signal函数的一些对比

...nal函数的一些缺点:由于signal函数调用成功时会返回原来信号处理程序的指针,所以如果我想要利用signal函数来获取原先信号处理程序的指针的话,必须要先去改变其信号处理方式。如下图所示在早期的signal函数的实现中,使用... 查看详情

signal的函数名:signal

参考技术A表头文件#include<signal.h>功能:设置某一信号的对应动作函数原型:void(*signal(intsignum,void(*handler)(int)))(int);或者:typedefvoid(*sig_t)(int);sig_tsignal(intsignum,sig_thandler);参数说明:第一个参数signum指明了所要处理的信号类型... 查看详情

bug诞生记——const_cast引发只读数据区域写违例(代码片段)

    对于C++这种强类型的语言,明确的类型既带来了执行的高效,又让错误的发生提前到编译期。所以像const这类体现设计者意图的关键字,可以隐性的透露给我们它描述的对象的使用边界。它是我们的朋友&#x... 查看详情

使用 tensorflow.contrib.signal 重构信号会导致放大或调制(帧、重叠和添加、stft 等)

】使用tensorflow.contrib.signal重构信号会导致放大或调制(帧、重叠和添加、stft等)【英文标题】:reconstructingsignalwithtensorflow.contrib.signalcausesamplificationormodulation(frames,overlap_and_add,stftetc)【发布时间】:2018-07-0816:16:16【问题描述】:... 查看详情

linux系列signal函数详解(代码片段)

Date:2023.1.18文章目录1、介绍2、如何安装多个处理函数3、信号列表转载自:http://imhuchao.com/2300.htmlsignal作用是为信号注册一个处理器。这里的“信号”是软中断信号,这种信号来源主要有三种:程序错误:比如除0&#... 查看详情

信号(signal)

信号本质  信号是软件中断,是在软件层次上对中断的一种模拟信号产生(来源)1.硬件来源:比如我们按下了键盘或者其它硬件故障;2.软件来源:最常用发送信号的系统函数是kill,raise,alarm和pause;信号递送  当导致产生... 查看详情

Qt - QLocalSocket 信号槽不起作用导致析构函数死锁

】Qt-QLocalSocket信号槽不起作用导致析构函数死锁【英文标题】:Qt-QLocalSocketSignal-Slotnotworkingresultingindeadlocksindestructor【发布时间】:2016-07-0510:10:04【问题描述】:我正在使用QLocalSocket和QLocalServer在Windows7上使用VS2010和Qt5.5.1进行进... 查看详情

linux进程通信|信号(代码片段)

一、什么是信号?信号就像是一个突然的电话铃声,它会打断正在进行的程序并引起其注意。在Linux系统中,信号是一种软件中断,它通常是异步发生的,可以用来通知进程某个事件已经发生。。每个信号都有一个唯一的编号,... 查看详情

signal.alarm() 处理程序导致 pyserial 出现问题

】signal.alarm()处理程序导致pyserial出现问题【英文标题】:signal.alarm()handlercausingproblemwithpyserial【发布时间】:2010-10-2705:44:56【问题描述】:所以我有一个运动传感器连接到通过USB与我的python应用程序通信的avrmicro。我使用pyserial... 查看详情

libevent源码分析之信号处理

新看看官方demo的libevent如何使用信号intcalled=0;staticvoidsignal_cb(intfd,shortevent,void*arg){ structevent*signal=arg; printf("%s:gotsignal%d ",__func__,EVENT_SIGNAL(signal)); if(called>=2) event_del(signal 查看详情

关于信号处理函数的参数问题!signal???

...指针为参数,同时也返回的是这个类型的函数指针,即该信号之前的处理方式. 参考技术B这是一个函数指针吧输入的参数是int 查看详情

c信号处理

1.使用signal(intsignal,function)向内核注册信号处理函数2.使用raise()向本进程发送信号,通过kill()向其他进程发送信号#include<stdio.h>#include<signal.h>#include<unistd.h>#include<stdlib.h>voidsignal_handle(intsignal);intmain(intargc,char**argv)s... 查看详情