嵌入式Linux:线程中信号处理

目录

1、信号与多线程结合的复杂性

2、信号在多线程环境中的映射与处理

2.1、进程级信号

2.2、线程级信号

3、信号处理函数与多线程环境

4、信号掩码与线程独立性

5、异步信号安全函数


信号最早是为了单进程的环境而设计的,用于在进程中捕捉各种事件,比如硬件异常、终止请求等。每个信号都有对应的处理动作(默认或自定义),例如:

  • SIGTERM 用于请求进程终止;
  • SIGINT 是通过键盘中断(Ctrl+C)触发的信号;
  • SIGSEGV 则用于处理段错误(非法内存访问)。

这些信号的处理方式原本是进程级别的,也就是一个信号影响整个进程。而随着多线程模型的引入,进程内部可以有多个线程同时运行,信号处理的复杂性也大大增加。

1、信号与多线程结合的复杂性

多线程应用程序不仅需要继承原有的信号处理特性,还要保证线程之间的信号处理逻辑不会冲突。

在传统的单进程模型中,信号被设计为能够中断当前的执行流(如捕捉异常或处理终止请求),但在多线程环境下,多个线程并行运行,同一进程的信号可以由任意线程接收并处理。因此,这种多线程与信号处理的结合引发了以下问题:

  • 信号由哪个线程处理:当一个信号发给进程时,内核必须决定哪个线程来处理信号,这可能会影响应用程序的行为。
  • 信号处理与线程安全问题:信号处理函数可能在任意时刻被调用,打断当前线程的执行流,如果线程正在操作共享资源,可能引发竞争条件或不一致性。
  • 信号屏蔽(masking):信号掩码决定了线程是否能够接收到特定信号,而每个线程可以有独立的信号掩码设置,这样的设计带来了更多的灵活性,但也增加了复杂性。

2、信号在多线程环境中的映射与处理

信号的映射方式取决于其触发源以及信号的类型。我们可以将信号的映射机制分为进程层面和线程层面。

2.1、进程级信号

大多数信号是针对整个进程的。例如通过 kill() 发送的信号,或者来自操作系统的控制台中断信号。这类信号发送给进程,默认情况下,内核会从进程的所有线程中随机选择一个线程来处理信号。

cpp 复制代码
kill(getpid(), SIGINT); // 给当前进程发送 SIGINT 信号

当进程中的某个线程处理这个信号时,其他线程的执行不会受到影响。内核负责决定哪个线程接收到信号,通常是未屏蔽该信号的线程。

2.2、线程级信号

某些信号只能由特定线程处理。例如,当线程遇到异常情况时(如段错误 SIGSEGV,浮点异常 SIGFPE),信号只会发送给引发该错误的线程。

以下例子中,访问空指针将触发段错误,SIGSEGV 信号只会发送给导致错误的线程。

cpp 复制代码
void* faulty_thread(void* arg) {
    int* invalid_ptr = NULL;
    *invalid_ptr = 42;  // 这将触发 SIGSEGV
    return NULL;
}

在使用 kill()sigqueue() 发送信号时,信号是针对整个进程的,内核会选择进程中的某个线程来处理信号。而在多线程程序中,可以使用 pthread_kill() 向同一进程中的指定线程发送信号,具体如下:

cpp 复制代码
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);

参数说明:

  • thread:线程 ID,指定要接收信号的线程。
  • sig:信号编号,指定要发送的信号。

如果 sig 为 0,pthread_kill() 不会发送信号,但会执行错误检查。成功时返回 0,失败时返回错误编号。

除了 pthread_kill(),还可以使用 pthread_sigqueue() 发送信号。该函数与 sigqueue() 类似,但它是将信号发送给指定的线程,而不是整个进程:

cpp 复制代码
#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 会被调用。

cpp 复制代码
#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 信号。信号处理函数必须能在不同线程中正确处理信号事件。

cpp 复制代码
#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() 函数用于设置线程的信号掩码,控制哪些信号应该被阻止或接收。

cpp 复制代码
#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 类型的信号集,指定要阻塞或解除阻塞的信号集合。

  • howSIG_SETMASK 时,set 中的信号会替换当前屏蔽字;当 howSIG_BLOCKSIG_UNBLOCK 时,set 中的信号将被添加到或从屏蔽字中移除。

oldset: 如果不为 NULL,此参数将用来存储调用前的信号屏蔽字。这允许程序在修改信号屏蔽字后恢复原来的状态。

返回值:

  • 成功时,返回 0
  • 失败时,返回错误码,通常为 errno 中定义的错误。

以下示例中,线程会屏蔽 SIGINT 信号,即使按下 Ctrl+C,该线程也不会处理 SIGINT 信号。

cpp 复制代码
#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 来查阅相关文档,获取更多信息:

cpp 复制代码
man 7 signal

这些函数可以在信号处理函数中安全调用。反之,像 printf()malloc() 等函数并不安全,因为它们可能涉及内部的缓冲机制或全局状态,容易在信号处理中引发竞争条件。

通过理解信号在多线程环境中的复杂性和设计局限性,开发者可以更好地编写安全可靠的多线程程序。

  • 避免在多线程程序中使用全局信号处理函数:因为信号处理函数是全局共享的,它很容易引发线程之间的竞争。尽可能将信号处理逻辑与线程独立运行的机制分离。

  • 合理使用信号掩码:通过为不同线程设置独立的信号掩码,开发者可以避免不必要的信号干扰。尤其是在执行关键任务时,可以临时屏蔽所有不相关的信号。

  • 使用异步信号安全函数 :在编写信号处理函数时,尽量只调用那些已知的异步信号安全函数,如 write()_exit() 等,避免使用 malloc()free()printf() 这样的非异步信号安全函数。

  • 信号与线程同步:避免在信号处理函数中直接操作复杂的数据结构或进行同步操作(如加锁),因为信号处理函数可能随时中断当前线程,导致死锁或数据不一致。

相关推荐
MoRanzhi12034 小时前
SciPy傅里叶变换与信号处理教程:数学原理与Python实现
python·机器学习·数学建模·数据分析·信号处理·傅里叶分析·scipy
DuHz12 小时前
汽车角雷达波形设计与速度模糊解决方法研究——论文阅读
论文阅读·物联网·算法·汽车·信息与通信·信号处理
DuHz12 小时前
基于多普勒频率和距离变化率联合测量的增强型速度估计方法——论文阅读
论文阅读·目标检测·汽车·信息与通信·信号处理
egoist20234 天前
[linux仓库]信号快速认识[进程信号·壹]
linux·c语言·信号处理·信号·前后台进程
Zevalin爱灰灰5 天前
数字信号处理 第八章(多采样率数字信号处理)
信号处理
通信小呆呆7 天前
FPGA 上的 OFDM 同步:从 S&C 到残差 CFO 的工程化实现
fpga开发·信号处理·同步·ofdm
国际学术会议-杨老师9 天前
2025年计算机网络与信号处理国际会议(CNSP 2025)
计算机网络·信号处理
菜鸟‍9 天前
【实验报告】华东理工大学随机信号处理实验报告
信号处理
青草地溪水旁9 天前
从“快递签收规则”看 sigaction:信号处理的“总开关”
linux·信号处理