Mutex & Signal死锁
项目遇到一次死锁的问题记录。
前置知识
Mutex互斥锁
#include <pthread.h>
static int data = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void foo(void) {
pthread_mutex_lock(&mtx);
data ++;
pthread_mutex_unlock(&mtx);
}
上述是Mutex互斥锁的基本用法,初始化一个全局的互斥锁,用来保护了一个全局变量data
;在数据data
前后使用锁对其进行保护。看起来很稳,不会有什么问题,那可能是它没有遇到不省心的队友--Signal
。
Signal信号
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <pthread.h>
void handle_sigint(int arg) {
printf("handle_sigint\n");
}
int main(int argc, char* argv[]) {
// ctl + C
signal(SIGINT, handle_sigint);
while (true) {
}
return 0;
}
代码说明:设置一个信号处理函数,当接收到SIGINT
信号时,执行handle_sigint
函数。SIGINT
信号是ctrl + c
产生的,所以按下ctrl + c
,会执行handle_sigint
函数。
代码小改一下:打印相应函数的线程信息
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <pthread.h>
void handle_sigint(int arg) {
printf("handle_sigint, thread ID: %ld\n", pthread_self());
pthread_exit(NULL);
}
int main(int argc, char* argv[]) {
printf("main, thread ID: %ld\n", pthread_self());
// ctl + C
signal(SIGINT, handle_sigint);
while (true) {
}
return 0;
}
编译&运行:
$ ./a.out
main, thread ID: 139685512120128
^Chandle_sigint, thread ID: 139685512120128
有意思的问题来了,handle_sigint
函数的执行线程居然和main
函数线程是同一个线程。你可能会有疑问,main
函数里面是个while (true)
的死循环,程序主线程应该一直执行这个循环,主线程执行流程为什么会跑到hanldle_sigint
函数那里去呢?这不符合程序循序执行的逻辑啊。
回答这个问题,站在程序本身执行的角度是无法理解的,应该站在操作系统的视角去看。现代主流的操作系统大多是time-share
时间分片的操作系统,把时间分成片,固定时长被动的进行调度切换;调度的时候:先执行下内核,然后进程调度去执行下A程序;过了一个固定时长,又会切换到其它的程序;中间可能还有其它打断,执行流又会回到内核。Signal就是其中一种,
现在来分析while (true)
代码块,在操作系统调度程序的过程中,程序是被切成多个指令片段去执行的,而不是被某一个程序的 while (true)
代码块一直霸占着CPU的执行权。从内核态和用户态的角度看,用户态的代码只是属于内核态代码的“回调程序”。
Signal的处理函数执行流程如下:
![](/assets/image-2023062895738841%20PM.yWLMEMh9.webp)
Mutex和Singal组合
代码简单:主线程和Singal处理函数使用同一把互斥锁。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdbool.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void main_thread_func(void) {
pthread_mutex_lock(&mutex);
printf("Main thread: Got ths lock\n");
usleep(100 * 1000); // do something
pthread_mutex_unlock(&mutex);
printf("Main thread: Release ths lock\n");
}
void sig_handler(int num) {
printf("sig handler\n");
pthread_mutex_lock(&mutex);
printf("Signal handle func: Got ths lock\n");
usleep(100 * 1000); // do something
pthread_mutex_unlock(&mutex);
printf("Signal handle func: Release ths lock\n");
}
int main(int argc, char* argv[]) {
// handle ctrl-c
signal(SIGINT, sig_handler);
while (true) {
main_thread_func();
}
return 0;
}
运行程序,按下Ctrl-C
输出:
$ cc 03_demo.c && ./a.out
Main thread: Got ths lock
Main thread: Release ths lock
Main thread: Got ths lock
Main thread: Release ths lock
Main thread: Got ths lock
^Csig handler
要退出可以按下Ctrl-\
发送SIGQUIT信号来退出程序。实际项目中出现这种问题可能没有这么直接明显,可能是调用其它的库,其它的库里面用了锁。
解决方案
Mutex的类型recursive
Mutex互斥锁也有多种类型:
- PTHREAD_MUTEX_NORMAL
- PTHREAD_MUTEX_ERRORCHECK
- PTHREAD_MUTEX_RECURSIVE
这里主要讲第三个,recursive
是递归的意思,表示互斥锁可以递归嵌套获取/释放,使用方式:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
// #include <unistd.h>
// 方式1:初始化全局的Mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main(int argc, char* argv[]) {
// 方式2:初始化局部的Mutex
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // set type
pthread_mutex_init(&mutex, &attr);
return 0;
}
修改下代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdbool.h>
pthread_mutex_t mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
void main_thread_func(void) {
pthread_mutex_lock(&mutex);
printf("Main thread: Got ths lock\n");
usleep(100 * 1000); // do something
pthread_mutex_unlock(&mutex);
printf("Main thread: Release ths lock\n");
}
void sig_handler(int num) {
printf("sig handler\n");
pthread_mutex_lock(&mutex);
printf("Signal handle func: Got ths lock\n");
usleep(100 * 1000); // do something
pthread_mutex_unlock(&mutex);
printf("Signal handle func: Release ths lock\n");
}
int main(int argc, char* argv[]) {
// handle ctrl-c
signal(SIGINT, sig_handler);
while (true) {
main_thread_func();
}
return 0;
}
这里在怎么按Ctrl-C
都不会出现死锁了。好像问题解决了,但也只是死锁的问题解决了;信号打断其它处理函数、中断其它函数的问题并没有解决,这里就可能会有 data race
的问题。
类似中断处理的方式
Linux 内核对于繁重的中断处理,在处理时会分为两部分:上半部和下半部。上半部只用来通知,简单快速、主打一个轻量化,类似做一个标记或者通知一样的处理;真正核心处理流程放到了下半部里面。也提供了相应的原子类型sig_atomic_t
去设置状态。有最后代码做了类似这样的处理,Signal处理函数只是用来做标记或通知,不写相应的业务逻辑。这里的核心是:信号处理函数尽量少去和其它函数竞争资源。