嵌入式Linux:线程中信号处理
目录
1、信号与多线程结合的复杂性
2、信号在多线程环境中的映射与处理
2.1、进程级信号
2.2、线程级信号
3、信号处理函数与多线程环境
4、信号掩码与线程独立性
5、异步信号安全函数
信号最早是为了单进程的环境而设计的,用于在进程中捕捉各种事件,比如硬件异常、终止请求等。每个信号都有对应的处理动作(默认或自定义),例如:
SIGTERM
用于请求进程终止;SIGINT
是通过键盘中断(Ctrl+C)触发的信号;SIGSEGV
则用于处理段错误(非法内存访问)。
这些信号的处理方式原本是进程级别的,也就是一个信号影响整个进程。而随着多线程模型的引入,进程内部可以有多个线程同时运行,信号处理的复杂性也大大增加。
1、信号与多线程结合的复杂性
多线程应用程序不仅需要继承原有的信号处理特性,还要保证线程之间的信号处理逻辑不会冲突。
在传统的单进程模型中,信号被设计为能够中断当前的执行流(如捕捉异常或处理终止请求),但在多线程环境下,多个线程并行运行,同一进程的信号可以由任意线程接收并处理。因此,这种多线程与信号处理的结合引发了以下问题:
- 信号由哪个线程处理:当一个信号发给进程时,内核必须决定哪个线程来处理信号,这可能会影响应用程序的行为。
- 信号处理与线程安全问题:信号处理函数可能在任意时刻被调用,打断当前线程的执行流,如果线程正在操作共享资源,可能引发竞争条件或不一致性。
- 信号屏蔽(masking):信号掩码决定了线程是否能够接收到特定信号,而每个线程可以有独立的信号掩码设置,这样的设计带来了更多的灵活性,但也增加了复杂性。
2、信号在多线程环境中的映射与处理
信号的映射方式取决于其触发源以及信号的类型。我们可以将信号的映射机制分为进程层面和线程层面。
2.1、进程级信号
大多数信号是针对整个进程的。例如通过 kill()
发送的信号,或者来自操作系统的控制台中断信号。这类信号发送给进程,默认情况下,内核会从进程的所有线程中随机选择一个线程来处理信号。
kill(getpid(), SIGINT); // 给当前进程发送 SIGINT 信号
当进程中的某个线程处理这个信号时,其他线程的执行不会受到影响。内核负责决定哪个线程接收到信号,通常是未屏蔽该信号的线程。
2.2、线程级信号
某些信号只能由特定线程处理。例如,当线程遇到异常情况时(如段错误 SIGSEGV
,浮点异常 SIGFPE
),信号只会发送给引发该错误的线程。
以下例子中,访问空指针将触发段错误,SIGSEGV
信号只会发送给导致错误的线程。
void* faulty_thread(void* arg) {int* invalid_ptr = NULL;*invalid_ptr = 42; // 这将触发 SIGSEGVreturn NULL;
}
在使用 kill()
或 sigqueue()
发送信号时,信号是针对整个进程的,内核会选择进程中的某个线程来处理信号。而在多线程程序中,可以使用 pthread_kill()
向同一进程中的指定线程发送信号,具体如下:
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
参数说明:
thread
:线程 ID,指定要接收信号的线程。sig
:信号编号,指定要发送的信号。
如果 sig
为 0,pthread_kill()
不会发送信号,但会执行错误检查。成功时返回 0
,失败时返回错误编号。
除了 pthread_kill()
,还可以使用 pthread_sigqueue()
发送信号。该函数与 sigqueue()
类似,但它是将信号发送给指定的线程,而不是整个进程:
#include <signal.h>
#include <pthread.h>
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
参数说明:
thread
:线程 ID,指定接收信号的线程。sig
:要发送的信号。value
:伴随数据,类型为union sigval
,与sigqueue()
的value
参数类似。
3、信号处理函数与多线程环境
无论是单线程还是多线程,信号处理函数在进程中是全局的。也就是说,注册的信号处理函数可能会被进程中的任何一个线程调用。
以下示例当用户按下 Ctrl+C
发送 SIGINT
信号时,signal_handler
会被调用。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>void signal_handler(int sig) {printf("Caught signal %d\n", sig);
}int main() {signal(SIGINT, signal_handler); // 注册信号处理函数while (1) {printf("Running...\n");sleep(1);}return 0;
}
在多线程环境下,多个线程可能会同时触发信号。假设我们在每个线程中都执行某种操作,信号处理函数可能会在任意线程中执行。信号处理函数必须是线程安全的,避免数据竞争或死锁等问题。
以下示例按下 Ctrl+C
时,任意线程都有可能捕获 SIGINT
信号。信号处理函数必须能在不同线程中正确处理信号事件。
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>void signal_handler(int sig) {printf("Thread %ld caught signal %d\n", pthread_self(), sig);
}void* thread_function(void* arg) {while (1) {printf("Thread %ld is running...\n", pthread_self());sleep(1);}
}int main() {pthread_t thread1, thread2;signal(SIGINT, signal_handler); // 所有线程共享的信号处理函数pthread_create(&thread1, NULL, thread_function, NULL);pthread_create(&thread2, NULL, thread_function, NULL);pthread_join(thread1, NULL);pthread_join(thread2, NULL);return 0;
}
4、信号掩码与线程独立性
在多线程环境中,每个线程可以有自己独立的信号掩码。通过信号掩码,线程可以选择是否接收某些信号。这为线程的信号处理提供了极大的灵活性。
pthread_sigmask()
函数用于设置线程的信号掩码,控制哪些信号应该被阻止或接收。
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
参数说明:
how:
指定如何修改当前线程的信号屏蔽字。它的取值有以下几种:
SIG_BLOCK
:将set
中的信号添加到当前线程的信号屏蔽字中,阻塞这些信号。SIG_UNBLOCK
:将set
中的信号从当前线程的信号屏蔽字中移除,解除阻塞这些信号。SIG_SETMASK
:将当前线程的信号屏蔽字设置为set
中的信号集合,替换原有的阻塞信号。
set:
指向 sigset_t
类型的信号集,指定要阻塞或解除阻塞的信号集合。
- 当
how
为SIG_SETMASK
时,set
中的信号会替换当前屏蔽字;当how
为SIG_BLOCK
或SIG_UNBLOCK
时,set
中的信号将被添加到或从屏蔽字中移除。
oldset:
如果不为 NULL
,此参数将用来存储调用前的信号屏蔽字。这允许程序在修改信号屏蔽字后恢复原来的状态。
返回值:
- 成功时,返回
0
。 - 失败时,返回错误码,通常为
errno
中定义的错误。
以下示例中,线程会屏蔽 SIGINT
信号,即使按下 Ctrl+C
,该线程也不会处理 SIGINT
信号。
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>void* thread_function(void* arg) {sigset_t set;sigemptyset(&set);sigaddset(&set, SIGINT); // 屏蔽 SIGINT 信号pthread_sigmask(SIG_BLOCK, &set, NULL);while (1) {printf("Thread %ld is running...\n", pthread_self());sleep(1);}
}int main() {pthread_t thread;pthread_create(&thread, NULL, thread_function, NULL);pthread_join(thread, NULL);return 0;
}
5、异步信号安全函数
异步信号安全函数是指那些可以在信号处理程序中调用的函数。这些函数必须是可重入的,能够在信号处理期间中断正常执行流程而不会引发不一致行为。
Linux 提供了一组异步信号安全的系统调用,例如:
上表列出的这些函数被认为是异步信号安全函数。你可以通过执行命令 man 7 signal
来查阅相关文档,获取更多信息:
man 7 signal
这些函数可以在信号处理函数中安全调用。反之,像 printf()
、malloc()
等函数并不安全,因为它们可能涉及内部的缓冲机制或全局状态,容易在信号处理中引发竞争条件。
通过理解信号在多线程环境中的复杂性和设计局限性,开发者可以更好地编写安全可靠的多线程程序。
-
避免在多线程程序中使用全局信号处理函数:因为信号处理函数是全局共享的,它很容易引发线程之间的竞争。尽可能将信号处理逻辑与线程独立运行的机制分离。
-
合理使用信号掩码:通过为不同线程设置独立的信号掩码,开发者可以避免不必要的信号干扰。尤其是在执行关键任务时,可以临时屏蔽所有不相关的信号。
-
使用异步信号安全函数:在编写信号处理函数时,尽量只调用那些已知的异步信号安全函数,如
write()
、_exit()
等,避免使用malloc()
、free()
或printf()
这样的非异步信号安全函数。 -
信号与线程同步:避免在信号处理函数中直接操作复杂的数据结构或进行同步操作(如加锁),因为信号处理函数可能随时中断当前线程,导致死锁或数据不一致。