可重入函数 与 不可重入函数
目录
一、可重入函数概念(安全的、可靠的)
1、数据访问独立性
2、无副作用或副作用可控
3、安全用于中断和信号处理
二、不可重入函数概念(不安全的、不可靠的)
1、依赖共享资源
2、调用不可重入库函数
3、执行过程中被中断可能导致问题
三、场景描述与分析
四、重看 重入与不可重入函数 的概念
五、局部变量与参数的访问安全性
六、不可重入函数的判断条件
1、调用了 malloc 或 free
2、调用了标准 I/O 库函数
七、性能上的差异:可重入函数和不可重入函数
1、内存使用
2、执行效率
3、同步开销
4、代码复杂度和优化难度
一、可重入函数概念(安全的、可靠的)
可重入函数(Reentrant Function)是一种具备特殊性质的函数,它可以被多个任务或线程同时调用,也可以在函数执行过程中被中断,之后再次被调用,且不会导致数据混乱或程序错误,具体特点如下:
1、数据访问独立性
-
可重入函数只使用局部变量,即变量是在函数的栈帧上分配的。
-
每个函数调用都有自己独立的栈帧,因此不同调用之间的局部变量相互隔离。
-
例如,一个计算阶乘的可重入函数,每次调用时都会在各自的栈帧中存储计算过程中的中间结果和最终结果,不同调用之间不会相互干扰。
2、无副作用或副作用可控
-
函数不依赖于全局变量、静态变量等共享资源,或者对共享资源的访问进行了严格的同步控制。
-
比如,一个纯粹的数学计算函数,只根据输入参数进行计算并返回结果,不修改任何外部状态,这样的函数就是可重入的。
3、安全用于中断和信号处理
-
在中断处理程序或信号处理函数中可以安全调用可重入函数。
-
因为中断或信号可能随时发生,可重入函数能够保证在这种情况下不会破坏数据的一致性。
-
例如,在实时系统中,中断处理程序可能需要调用一些辅助函数来完成特定任务,使用可重入函数可以确保系统的稳定性。
二、不可重入函数概念(不安全的、不可靠的)
不可重入函数与可重入函数相反,它在多任务、多线程或中断处理环境下可能会引发问题,具体表现如下:
1、依赖共享资源
-
不可重入函数通常会访问全局变量、静态变量或使用共享的文件描述符等资源。
-
例如,一个函数使用全局变量来记录某个状态信息,当多个任务或线程同时调用该函数时,它们对全局变量的修改会相互覆盖,导致数据混乱。
2、调用不可重入库函数
-
如果函数调用了标准 I/O 库函数(如
printf、scanf等)或一些系统函数(如malloc、free等),而这些库函数本身是不可重入的,那么该函数也会成为不可重入函数。 -
以
malloc为例,它使用全局链表来管理堆内存,多个线程同时调用malloc可能会导致对链表的并发访问冲突,破坏内存管理的正确性。
3、执行过程中被中断可能导致问题
-
当不可重入函数在执行过程中被中断,并且中断处理程序又调用了同一个函数,就会破坏函数执行的上下文,导致不可预测的结果。
-
比如,一个函数正在修改一个全局的数据结构,在未完成修改时被中断,中断处理程序又调用该函数对同一个数据结构进行操作,就可能使数据结构处于不一致的状态。
总的来说:
可重入函数强调在多任务、多线程和中断环境下的安全性和可靠性,通过独立的数据访问和可控的副作用来保证函数调用的正确性;而不可重入函数由于对共享资源的依赖和调用不可重入库函数等原因,在并发环境下容易出现数据混乱和程序错误。在编写对并发性要求较高的程序时,应优先使用可重入函数,以确保程序的稳定性和正确性。
三、场景描述与分析
在主函数中,通过insert函数将结点node1插入链表;同时,某个信号处理函数也调用insert函数来插入结点node2。表面看来似乎没有问题。

接下来,我们以下面的链表为例进行分析:

在main函数调用insert函数将结点node1插入链表时,插入操作正进行到第一步。此时由于硬件中断导致进程切换到内核态,在返回用户态前系统检测到有待处理信号,于是转而执行sighandler函数。

在sighandler函数中,同样通过调用insert函数将结点node2插入链表。第一步插入操作完成后的状态如下:

在节点 node2 完成插入操作并从信号处理程序返回内核态后,链表结构呈现如下布局:

回到用户态后,程序从main函数调用的insert函数继续执行,继续完成结点node1的插入操作。

最终结果是,main函数和sighandler函数依次向链表插入两个节点,但只有node1成功加入链表,而node2节点则丢失,导致内存泄漏。具体执行顺序如下:

在上例中,insert函数被不同的控制流调用(main函数和sighandler函数使用不同的堆栈空间,它们之间没有调用关系,属于两个独立的控制流程)。这种情况可能导致第一次调用尚未返回时再次进入该函数,这种现象称为重入。
当insert函数访问全局链表时,重入可能导致数据错乱,这类函数称为不可重入函数。相反,如果一个函数仅访问自身局部变量或参数,则称为可重入(Reentrant)函数。
总的流程:
在一个程序执行过程中,main 函数调用 insert 函数向一个全局链表 head 中插入节点 node1。插入操作通常分为两步,例如可能是先分配节点内存并初始化部分字段,再将节点链接到链表中。然而,在刚完成第一步操作时,硬件中断发生,导致进程切换到内核态。在进程即将回到用户态之前,系统检查到有信号待处理,于是进程切换到信号处理函数 sighandler。而sighandler 函数同样调用了 insert 函数,向同一个链表 head 中插入节点 node2,并且这次插入操作的两步都顺利完成。之后,进程从 sighandler 函数返回内核态,再次回到用户态时,继续从 main 函数调用的 insert 函数中未完成的部分执行,即继续完成插入 node1 的第二步操作。

最终的结果是,main 函数和 sighandler 先后向链表中插入两个节点,但最后只有一个节点真正插入到链表中。这是因为 insert 函数在执行过程中被中断,且在中断处理期间又被再次调用,导致对全局链表 head 的访问出现了混乱。
四、重看 重入与不可重入函数 的概念
-
重入:像上述例子中,
insert函数被不同的控制流程(main函数调用和sighandler函数调用)调用,有可能在第一次调用还没返回时就再次进入该函数,这种情况称为重入。 -
不可重入函数:如果一个函数访问全局变量或共享资源(如上述例子中的全局链表
head),有可能因为重入而造成数据错乱,像这样的函数称为不可重入函数。不可重入函数在多任务或中断处理环境下可能会导致不可预测的行为。 -
可重入函数:反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。可重入函数可以在不同的控制流程中安全地被调用,而不会相互干扰。
五、局部变量与参数的访问安全性
-
两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数不会造成错乱,原因在于局部变量和参数是存储在函数的栈帧中的。
-
每个函数调用都会在栈上创建一个独立的栈帧,用于存储该函数调用的局部变量、参数和返回地址等信息。(回顾函数调用栈帧那部分)
-
因此,不同控制流程对同一个函数的调用,其局部变量和参数实际上是相互隔离的,各自拥有独立的存储空间,不会相互影响。
六、不可重入函数的判断条件
如果一个函数符合以下条件之一,则是不可重入的:
1、调用了 malloc 或 free
-
因为
malloc和free函数也是使用全局链表来管理堆内存的。 -
在多任务环境下,如果多个控制流程同时调用
malloc或free,可能会对这个全局链表进行并发访问,导致数据不一致和内存管理混乱。 -
例如,一个控制流程正在
malloc分配内存并修改全局链表时被中断,另一个控制流程又调用malloc,就可能会破坏链表的结构,导致内存分配错误。
2、调用了标准 I/O 库函数
-
标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。
-
例如,
printf函数可能会使用全局的缓冲区来存储输出数据。如果多个控制流程同时调用printf,这些全局缓冲区可能会被同时修改,导致输出内容混乱,甚至引发程序崩溃。
七、性能上的差异:可重入函数和不可重入函数
可重入函数和不可重入函数在性能上存在多方面的差异,以下从内存使用、执行效率、同步开销等维度进行详细分析:
1、内存使用
可重入函数
-
主要依赖局部变量,这些变量存储在栈上。栈内存的分配和释放速度较快,因为它只需要移动栈指针。
-
例如,在一个递归的可重入函数中,每次递归调用都会在栈上分配新的局部变量空间,函数返回时栈指针回退,内存自动释放,这种动态的内存管理方式高效且灵活。
-
由于不依赖或少依赖全局变量,减少了内存中全局数据结构的维护成本,避免了因多个调用对全局变量修改而导致的复杂内存状态管理。
不可重入函数
-
可能会大量使用全局变量和静态变量,这些变量通常存储在数据段或 BSS 段。数据段和 BSS 段的内存管理相对固定,在程序启动时就已经确定大小,不如栈内存的动态分配灵活。
-
全局变量的存在可能导致内存占用增加,因为它们在整个程序生命周期中都存在,即使某些情况下并不需要。
-
而且,对全局变量的频繁访问和修改可能引发数据一致性问题,需要额外的机制来保证数据的正确性,这也间接影响了内存使用的效率。
2、执行效率
可重入函数
-
执行路径相对清晰和直接,因为它不依赖于外部共享资源的状态。
-
函数只根据输入参数进行计算和操作,减少了因等待共享资源或处理资源冲突而产生的延迟。
-
例如,一个简单的可重入数学计算函数,能够快速根据输入参数得出结果,执行速度较快。
-
在多线程或多任务环境中,可重入函数可以同时被多个线程或任务调用,提高了系统的并行处理能力,从而提升了整体的执行效率。
不可重入函数
-
由于可能访问共享资源,在并发环境下,当多个线程或任务同时调用该函数时,可能会出现资源竞争的情况。
-
为了解决资源竞争,需要使用锁、信号量等同步机制,这些机制会增加函数的执行开销。
-
例如,当一个不可重入函数正在访问一个全局数据结构时,其他试图访问该结构的调用必须等待,直到锁被释放,这会导致调用线程阻塞,降低了执行效率。
-
不可重入函数可能会因为对共享资源的复杂操作而引入更多的条件判断和逻辑处理,增加了函数的执行时间和复杂度。
3、同步开销
可重入函数
-
一般不需要复杂的同步机制,因为其局部变量的特性使得不同调用之间相互独立。
-
这减少了同步操作带来的性能损耗,例如获取和释放锁的时间开销,以及因锁竞争导致的线程阻塞和上下文切换成本。
-
在无并发冲突的情况下,可重入函数可以更流畅地执行,提高了系统的响应速度和吞吐量。
不可重入函数
-
为了保证在并发调用时的数据一致性,必须使用同步机制。
-
获取和释放锁需要一定的时间,而且在高并发场景下,锁竞争会变得激烈,导致大量的线程阻塞和上下文切换。
-
上下文切换需要保存和恢复线程的上下文信息,这会消耗较多的 CPU 资源,严重影响系统的性能。
-
同步机制的使用还可能使代码变得复杂,增加了开发和调试的难度,同时也可能引入潜在的死锁等问题,进一步影响系统的稳定性和性能。
4、代码复杂度和优化难度
可重入函数
-
代码结构相对简单,因为不涉及对共享资源的复杂操作和同步处理。这使得编译器更容易对其进行优化,例如进行指令重排、寄存器分配等优化操作,从而提高函数的执行效率。
-
简单的代码结构也有助于开发人员理解和维护,减少了出现错误的概率,间接提升了系统的整体性能。
不可重入函数
-
由于需要考虑共享资源的访问和同步问题,代码通常会变得更加复杂。
-
复杂的代码结构使得编译器在进行优化时面临更多的限制,可能无法充分发挥优化效果。
-
开发人员在编写和调试不可重入函数时需要更加谨慎,以确保数据的一致性和正确性,这也增加了开发的难度和时间成本,可能对性能产生负面影响。
总体而言,可重入函数在内存使用、执行效率、同步开销和代码优化等方面通常具有更好的性能表现,更适合在多线程、多任务和高并发的环境中使用。而不可重入函数由于自身的特性,在并发场景下可能会面临更多的性能挑战。
