Linux | 可重入函数 | volatile | SIGCHLD信号

本文探讨了可重入函数的概念及其与不可重入函数的区别,并通过实例演示了如何利用信号处理解决内存泄漏问题。此外,还介绍了volatile关键字的作用及volatile与const联合使用的场景,最后详细讲解了SIGCHLD信号的用途及其处理方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

可重入函数

当一个函数可以被两个执行流调用,我们称该函数具有重入特征

如果一个函数被重入后可能导致内存泄漏或其他问题,我们称该函数为不可重入函数,反之,一个函数被重入后不会导致任何问题,我们称该函数为可重入函数。

将一个节点new_node插入单链表分为两步,首先记录要插入位置的前后两个节点,prev和next,第一步是将new_node的next指针指向next节点,第二步时将prev的next指针指向new_node。
在这里插入图片描述
假设现在的main执行流正在执行insert函数,将new_node1插入单链表,在insert执行完第一步——将new_node1的next指针指向next节点后,由于进程收到一个信号,需要递达该信号,并且递达方式为自定义,包含insert函数,该insert插入一个节点new_node2,并且插入的位置的new_node1相同,信号的递达完成,new_node2被插入链表,进程将返回执行handler之前的代码,即执行insert的第二步,将prev节点的next指向new_node1,这样一来new_node1也被插入链表,由于两节点插入的位置一样,现在的实际情况是只有new_node1被插入链表,而new_node2在链表之外,这是一个明显的内存泄漏问题。
在这里插入图片描述
由于insert存在内存泄漏问题,所以insert是一个不可重入函数

但是可重入函数和不可重入函数函数没有优劣之分,是否能重入只是区别两个函数的方式,我们学习的大部分函数都是不可重入函数,STL库中的函数基本都是不可重入函数。由于函数重入导致的内存泄漏问题也只是在多个执行流执行进程时才会发生。

可重入与线程安全

线程安全:在多线程执行流执行同一段代码时,不会出现不同的结果,我们称这样的代码是线程安全的
重入:一个执行流执行一个函数时,其他执行流也能进入函数并执行,我们称这样的函数具有重入特征,如果函数在重入的情况下,不会出现任何问题,我们称这样的函数为可重入函数,反之就是不可重入函数

线程安全的代码中的函数不一定是没有问题的,函数可能是不可重入函数,由于线程对临界资源的访问实现了同步与互斥机制,使得函数变得安全,所以线程安全的代码中的函数一定是安全的,但不一定是可重入的

如果一个函数是可重入的,那么该函数也是线程安全的,但线程安全不一定是指函数可重入。

volatile

volatile属于C语言的易变关键字,其作用是告诉编译器该关键字修饰的变量可能发生变化,每次读取该变量的值时必须从内存中读取,以得到一个正确的值,防止编译器做自以为是的优化。

经过了信号的学习,可以通过一段有关信号的代码验证volatile的作用

#include <stdio.h>
#include <signal.h>

volatile int flag = 0;

// 自定义handler将flag的值设置为1
void handler(int signo)
{
    flag = 1;
    printf("flag->1\n");
}

int main()
{
    // 设置2号信号的handler方法
    signal(2, handler);
    // 如果flag的值为0,程序会卡在死循环中
    while (!flag);
    // flag的值不为0,程序会走到printf,打印语句
    printf("进程正常退出\n");    
    return 0;
}

这段demo根据flag的值决定程序是否会死循环,如果flag的值为0,程序死循环,flag的值不为0,程序会正常退出。flag作为一个全局变量,其初始值为0,如果不修改flag的值,程序会陷入死循环,这里可以通过捕捉2号信号,并自定义2号信号的handler方法,将flag的值设置为1,并打印"flag->1\n"这条语句。

所以一开始的程序会死循环,当按下Ctrl+C时,就意味着向前台进程发送2号信号,也就是将flag的值设置为1,程序退出死循环,打印"进程正常退出\n"这条语句。

运行这段demo,运行结果和预期相同
在这里插入图片描述
gcc编译器有一些高优化级别的选项,-O2表示以较高的优化级别编译这段demo,带上这个选项后,再运行程序,通过截图可以看出不论发送多少次2号信号,程序都不能退出死循环,也就是说程序认为flag的值依然为0。
在这里插入图片描述
导致这样结果的原因是:while信号的判断条件是!flag,也就是说判断条件只需要进行单纯的值判断,不需要进行计算,而while循环之前也没有对flag的值进行修改(signal只是设置了信号的自定义handler方法,虽然方法修改了flag的值,但编译器只能做语法检测,不能做逻辑判断,因此编译器认为在while之前,flag的值没有被修改。而循环需要不断的读取flag的值,每次从内存中读取flag的速度比每次从寄存器中读取的速度慢很多,而flag的值又没有修改,因此编译器将flag的值优化到寄存器中,这样每次的读取就能从寄存器中读取,有效的提高了程序运行的速度。

所以在寄存器中,flag的值始终为0,即使2号信号的自定义handler方法将内存中的flag值修改为1,由于cpu没有重新向内存中读取数据,所以寄存器的flag值不会因为内存的flag值改变。因此不论进程收到多少次2号信号,flag的值都为0,程序一直卡在死循环中。

所以编译的优化使得进程屏蔽了内存上的flag值,进程只可见寄存器上的flag值。而volatile的作用就是使得内存可见,强制程序每次读取flag的值都要从内存中读取。

在对flag添加了volatile修饰后,重新编译程序并带上-O2选项,可以看到,由于volatile对flag的修饰,在编译器的高优化下,2号信号的handler方法依然可以修改fla的值使程序退出死循环。
在这里插入图片描述

volatile和const同时修饰变量

volatile的作用是告诉编译器其修饰的变量很可能发生变化,需要编译器从内存中读取变量的值,而const的作用是告知编译器其修饰的变量不能被修改,如果有代码对const修饰的变量进行修改,编译将报错。

因此,const对于编译器来说,只需要在编译器期间检查const修改的变量是否被修改即可,const属于一种编译时就能完成语法检查的关键字。而volatile则是在程序运行起来后,才能实现其作用的关键字,编译器在编译期间无法对volatile修饰的变量做什么语法检查。

而volatile和const一起修饰一个变量,保证了该变量在后续代码中不会被修改,而在运行期间,其可能被修改,而编译器总是需要从内存中读取它的值,保证了程序不会过度优化出错。

SIGCHLD信号

当父进程fork创建子进程后,子进程退出时,会向父进程发送SIGCHLD信号,以表示子进程的退出。对于SIGCHLD信号,进程的递达方式为忽略,所以子进程的退出总是默默的,但我们可以捕捉父进程的SIGCHLD信号,并设置其自定义handler方法,以观察子进程的退出
在这里插入图片描述

void handler(int signo)
{
    cout << "父进程接收到子进程退出的信号:" << signo << "父进程pid:" << getpid() << endl;
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        // child
        while (1)
        {
            cout << "我是子进程,pid:" << getpid() << endl;
            sleep(1);
        }
        exit(1);
    }
    // parent
    while (1)
    {
        cout << "我是父进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
19号信号:暂停一个进程,18号信号:唤醒一个进程
在这里插入图片描述
可以看到,除了子进程的退出,子进程的暂停与继续都会向父进程发送SIGCHLD信号

之前回收子进程资源时,总是父进程以阻塞或者非阻塞的方式调用waitpid,提前进行子进程的退出等待,经过了信号的学习,我们就可以通过捕捉子进程退出时向父进程发送的SIGCHLD信号,并自定义handler方法使父进程调用waitpid回收子进程资源。

// mypro.cc
void handler(int signo)
{
	// 第一个参数为-1表示等待任意的子进程
    pid_t id = waitpid(-1, nullptr, 0);
    if (id > 0) // 等待成功
    {
        cout << "父进程成功地回收了子进程资源," << "父进程pid:" << getpid() << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        // child
        int cnt = 5;
        while (cnt)
        {
            cout << "我是子进程,pid:" << getpid() << "cnt:" << cnt << endl;
            cnt--;
            sleep(1);
        }
        exit(1);
    }
    // parent
    while (1)
    {
        cout << "我是父进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

修改代码,在父进程对SIGCHLD的handler方法中添加对子进程的回收。运行以上demo
在这里插入图片描述
当进程在递达一个信号时,会将该信号加入信号屏蔽字,也就是阻塞该信号,如果父进程有很多的子进程,这些子进程同时退出,也就是说很多子进程同时向父进程发送SIGCHLD信号,进程在递达信号期间最多只会收到一次信号。这就造成了有些子进程虽然退出了,但是SIGCHLD信号没有被父进程接收,导致其资源无人回收,因此子进程陷入僵尸状态,无法释放资源,造成了内存泄漏。


using namespace std;

void handler(int signo)
{
    pid_t id = waitpid(-1, nullptr, 0);
    if (id > 0) // 等待成功
    {
        cout << "父进程成功地回收了子进程资源," << "父进程pid:" << getpid() << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    // 创建10个子进程,它们的退出时间可能会重叠
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // child
            int cnt = 5;
            while (cnt)
            {
                cout << "我是子进程,pid:" << getpid() << " cnt:" << cnt << endl;
                cnt--;
                sleep(1);
            }
            exit(1);
        }
    }

    // parent
    while (1)
    {
        cout << "我是父进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
由于子进程在同一时间退出,可能造成有的子进程SIGCHLD信号没有被父进程接收到的情况,所以SIGCHLD的handler方法不能只回收一次子进程资源,应该多次调用waitpid回收子进程,并清理处于僵尸状态的子进程,当还有子进程在运行却没有退出时,父进程不再调用waitpid,至此才算完成了一次handler方法,信号递达完成,父进程可以去执行自己的代码了。

void handler(int signo)
{
    while (1)
    {
        pid_t id = waitpid(-1, nullptr, WNOHANG);
        if (id > 0) // 等待成功
        {
            cout << "父进程成功地回收了子进程资源,"
                 << "父进程pid:" << getpid() << endl;
        }
        else if (id == 0) // 等待失败,单还有子进程在运行
        {
            cout << "当前没有子进程需要回收资源,但还有子进程在运行" << endl;
            break;
        }
        else // 调用失败,没有子进程了
        {
            cout << "所有子进程回收完毕" << endl;
            break;
        }
    }
}

int main()
{
    signal(SIGCHLD, handler);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // child
            int cnt = 5;
            while (cnt)
            {
                cout << "我是子进程,pid:" << getpid() << " cnt:" << cnt << endl;
                cnt--;
                sleep(1);
            }
            exit(1);
        }
    }

    // parent
    while (1)
    {
        cout << "我是父进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

在这里插入图片描述

如果程序没有对SIGCHLD进行自定义handler,系统对SIGCHLD的默认处理方式是忽略,当子进程退出时,会陷入僵尸状态等待资源被释放,下图以上demo运行结果,以及运行期间有关的进程情况
在这里插入图片描述
在这里插入图片描述
父进程调用signal设置SIGCHLD信号的递达方法为系统提供的SIG_IGN后,子进程退出时将不再向父进程发送SIGCHLD信号,而是直接退出并释放资源,不会陷入僵尸状态。

系统对SIGCHLD信号的默认递达方法SIG_DFL,在该递达方法下,进程依然接收SIGCHLD信号,只是不对其做处理,当SIGCHLD信号的递达方法被设置为SIG_IGN时,系统会对SIGCHLD进行特殊处理,使该进程的子进程退出时不再发送SIGCHLD信号,并且退出时会自动释放资源,不会陷入僵尸态。
在这里插入图片描述

在这里插入图片描述
运行结果,子进程没有陷入僵尸态

<think>嗯,用户的问题是关于在STM32中断处理函数中调用非可重入函数可能导致的问题及后果,以及解决方案。我需要先理解什么是可重入函数和非可重入函数,然后分析在中断处理中使用非可重入函数可能带来的影响。 首先,可重入函数是指可以在多个执行上下文中被同时调用而不会产生副作用的函数。这类函数通常不使用静态变量、全局变量,也不依赖于外部状态。相反,非可重入函数可能使用了这些共享资源,因此在并发环境下可能导致数据竞争或不一致。 在STM32的中断处理中,中断服务函数(ISR)可能随时打断主程序的执行。如果ISR调用了非可重入函数,而这个函数在主程序的其他部分也被调用,就可能出现重入问题。例如,如果主程序和ISR同时修改同一个全局变量,可能会导致数据损坏或程序逻辑错误。 接下来,我需要考虑具体的后果。比如,数据竞争可能导致变量值不可预测,进而引发程序崩溃或功能异常。此外,使用标准库中的函数如printf,这些函数通常是不可重入的,如果在中断中使用,可能因为中断嵌套或主程序同时调用而导致死锁或缓冲区溢出。 解决方案方面,首先应该避免在ISR中使用非可重入函数。如果必须使用,可以考虑使用临界区保护,即在访问共享资源时禁用中断,但这可能影响系统的实时性。另外,可以替换为可重入版本的函数,或者使用信号量、队列等RTOS机制来安全传递数据,让主循环处理实际的操作。 还需要参考用户提供的引用内容。根据引用[1],HAL库的中断处理回调函数HAL_GPIO_EXTI_Callback是在中断上下文中执行的,所以在这里调用非可重入函数会有问题。引用[3]提到回调函数用于处理串口接收数据,如果回调函数中有非可重入操作,同样需要处理。 用户可能是在开发中遇到了此类问题,或者想提前预防。他们可能对如何检测非可重入函数、具体如何实现临界区保护或使用RTOS机制感兴趣。需要给出具体的例子,比如使用__disable_irq和__enable_irq来保护临界区,或者使用队列传递数据。 还要注意,解决方案需要根据具体的应用场景来选择。例如,在实时性要求高的系统中,禁用中断可能不可行,这时候使用RTOS的机制会更合适。此外,代码示例可以帮助用户更好地理解如何实施这些解决方案。 总结,我需要分点列出后果和解决方案,确保每个部分清晰,并引用提供的参考资料来支持回答。</think>### STM32中断处理函数调用非可重入函数的影响及解决方案 #### 一、非可重入函数的特点 非可重入函数通常具备以下特征: 1. 使用静态变量或全局变量 2. 依赖外部状态(如硬件寄存器) 3. 未对共享资源进行保护(如标准库的`printf`) #### 二、中断中调用非可重入函数的影响 1. **数据竞争** 若主程序和中断同时修改同一全局变量,可能导致数据损坏。例如: ```c int counter = 0; // 全局变量 void non_reentrant_func() { counter += 1; // 中断和主程序同时执行此处会引发竞争 } ``` 2. **死锁风险** 若函数内部使用未保护的系统资源(如动态内存分配),可能因中断嵌套导致资源锁死。 3. **堆栈溢出** 高频中断调用大型非可重入函数时,可能因堆栈未及时释放而溢出。 4. **时序不可控** 非可重入函数执行时间不确定,可能破坏中断响应实时性[^1]。 #### 三、解决方案 1. **禁止在中断中使用非可重入函数** - 优先选择原子操作或仅访问局部变量的函数 - 使用HAL库提供的线程安全API(如`HAL_UART_Transmit_IT`) 2. **临界区保护** 在访问共享资源时临时关闭中断: ```c __disable_irq(); // 关闭所有中断 critical_code(); // 操作共享资源 __enable_irq(); // 恢复中断 ``` *注意:关闭中断时间需控制在微秒级* 3. **使用可重入函数替代** - 将全局变量改为通过参数传递 - 使用C标准库的可重入版本(如`snprintf`替代`sprintf`) 4. **RTOS同步机制** 在FreeRTOS等系统中,通过队列/信号量传递数据: ```c // 中断中发送数据 BaseType_t xHigherPriorityTaskWoken; xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken); // 主任务中处理 xQueueReceive(xQueue, &data, portMAX_DELAY); process_data(data); ``` 5. **双缓冲技术** 适用于高频数据采集场景: ```c volatile uint8_t buffer[2][256]; volatile int active_buffer = 0; // 中断中填充非活跃缓冲区 void ADC_IRQHandler() { buffer[1-active_buffer][idx++] = ADC_VALUE; } // 主程序处理完成时切换缓冲区 void swap_buffer() { __disable_irq(); active_buffer = 1 - active_buffer; __enable_irq(); } ``` #### 四、实践建议 1. 通过`CubeMX`生成的中断服务函数模板已包含必要保护,避免直接修改[^2] 2. 使用`__weak`特性重写回调函数时,确保函数体简洁高效[^3] 3. 调试时开启`-fstack-usage`编译选项监测堆栈使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值