为啥说 malloc() 和 printf() 是不可重入的?
Posted
技术标签:
【中文标题】为啥说 malloc() 和 printf() 是不可重入的?【英文标题】:Why are malloc() and printf() said as non-reentrant?为什么说 malloc() 和 printf() 是不可重入的? 【发布时间】:2011-04-25 20:54:50 【问题描述】:在 UNIX 系统中,我们知道 malloc()
是一个不可重入函数(系统调用)。这是为什么?
同样,printf()
也被称为不可重入;为什么?
我知道重入的定义,但我想知道为什么它适用于这些功能。 是什么阻止了它们保证可重入?
【问题讨论】:
@ripunjay-tripathi : printf,如果它正在打印到公共资源,例如标准输出。 malloc 因为它依赖锁。请记住,可重入和线程安全之间存在差异。其中 malloc 是线程安全的。看看这个帖子。 ***.com/questions/855763/malloc-thread-safe. 您在哪里找到“规范实现”?如果开发人员说函数是可重入的。 glibc 开发人员没有说 malloc 或 printf 是可重入的:所以它们不是。 @pmg,关于“规范实现”的词是我添加的。这就是我的意思。很明显,可重入性是实现的属性,而不是接口的属性。但是,例如,POSIX 没有将malloc
和printf
列为可重入函数,这是有原因的。在这个问题中,OP 想知道原因是什么。
@pmg: 我想稍微有点问题的重点,POSIX 并没有说malloc
必须是可重入的,因此 glibc 开发人员可以这样做并且仍然声称是开发“一个 UNIX 系统”。
@Pavel:可重入性是接口的属性,也是实现的属性。必需的语义是接口的一部分。可重入接口是所有实现都必须可重入的接口之一。在考虑 POSIX 作者的动机时,没有“规范”的实现。重要的是标准作者希望允许的任何合理实施策略是否无法(有效)实施可重入malloc
。如果是这样,那么malloc
将被标记为不可重入,无论该实施策略是“规范”还是普通。
【参考方案1】:
这里至少有三个概念,所有这些概念都在口语中混为一谈,这可能就是您感到困惑的原因。
线程安全 临界区 重入先取最简单的一个:malloc
和 printf
都是 thread-safe。自 2011 年以来,它们在标准 C 中被保证是线程安全的,自 2001 年以来在 POSIX 中被保证是线程安全的,并且在很久以前就在实践中。这意味着保证以下程序不会崩溃或表现出不良行为:
#include <pthread.h>
#include <stdio.h>
void *printme(void *msg)
while (1)
printf("%s\r", (char*)msg);
int main()
pthread_t thr;
pthread_create(&thr, NULL, printme, "hello");
pthread_create(&thr, NULL, printme, "goodbye");
pthread_join(thr, NULL);
不是线程安全的函数的一个例子是strtok
。如果您同时从两个不同的线程调用strtok
,则结果是未定义的行为——因为strtok
在内部使用静态缓冲区来跟踪其状态。 glibc 添加了strtok_r
来解决这个问题,C11 添加了相同的东西(但可以选择使用不同的名称,因为不是在这里发明的)作为strtok_s
。
好的,但是printf
不也使用全局资源来构建它的输出吗?实际上,意味着同时从两个线程打印到标准输出?这将我们带到下一个主题。显然 printf
在任何使用它的程序中都将成为critical section。 一次只允许一个执行线程进入临界区。
至少在 POSIX 兼容的系统中,这是通过让 printf
以调用 flockfile(stdout)
开始并以调用 funlockfile(stdout)
结束来实现的,这基本上就像采用与 stdout 关联的全局互斥锁一样。
但是,程序中每个不同的FILE
都可以有自己的互斥体。这意味着一个线程可以调用fprintf(f1,...)
,同时第二个线程正在调用fprintf(f2,...)
。这里没有竞争条件。 (您的 libc 是否实际上并行运行这两个调用是 QoI 问题。我实际上不知道 glibc 是做什么的。)
同样,malloc
不太可能成为任何现代系统中的关键部分,因为现代系统是smart enough to keep one pool of memory for each thread in the system,而不是让所有 N 个线程争夺一个池。 (sbrk
系统调用可能仍然是一个关键部分,但malloc
在sbrk
上花费的时间很少。或者mmap
,或者现在很酷的孩子们正在使用的任何东西。)
好的,那么 re-entrancy 究竟是什么意思? 基本上,这意味着可以安全地递归调用该函数——当前调用在第二次调用运行时被“搁置”,并且那么第一次调用仍然能够“从中断的地方继续”。 (从技术上讲,这可能不是由于递归调用:第一次调用可能在线程 A 中,线程 B 在中间被线程 B 中断,从而进行第二次调用。但这种情况只是一个线程安全的特殊情况,所以我们可以在本段中忘记它。)
printf
和 malloc
都不能被单个线程递归调用,因为它们是叶函数(它们不会调用自己,也不会调用任何用户控制的代码可能会进行递归调用)。而且,正如我们在上面看到的,自 2001 年以来,它们对 *multi-*threaded 重入调用是线程安全的(通过使用锁)。
所以,谁告诉你printf
和malloc
是不可重入的,那就错了;他们的意思可能是它们都有可能成为您程序中的关键部分——一次只能通过一个线程的瓶颈。
迂腐提示:glibc 确实提供了一个扩展,通过它printf
可以调用任意用户代码,包括重新调用自身。这在所有排列中都是完全安全的——至少就线程安全而言。 (显然它为绝对insane 格式字符串漏洞打开了大门。)有两个变体:register_printf_function
(已记录在案且相当合理,但正式“弃用”)和register_printf_specifier
(其几乎相同,除了一个额外的未记录参数和一个total lack of user-facing documentation)。我不会推荐它们中的任何一个,在这里提及它们只是作为一个有趣的旁白。
#include <stdio.h>
#include <printf.h> // glibc extension
int widget(FILE *fp, const struct printf_info *info, const void *const *args)
static int count = 5;
int w = *((const int *) args[0]);
printf("boo!"); // direct recursive call
return fprintf(fp, --count ? "<%W>" : "<%d>", w); // indirect recursive call
int widget_arginfo(const struct printf_info *info, size_t n, int *argtypes)
argtypes[0] = PA_INT;
return 1;
int main()
register_printf_function('W', widget, widget_arginfo);
printf("|%W|\n", 42);
【讨论】:
这个答案几乎正确;但它缺少 POSIX 的一个关键概念,异步信号安全。由于异步信号,应用程序代码可以在调用printf
或malloc
的过程中执行。这两个函数都不需要是异步信号安全的,因此从异步信号的处理程序调用它们是不安全的。这就是 POSIX 系统程序员说 printf
和 malloc
是“不可重入”的意思。
“那么,谁告诉你 printf 和 malloc 是不可重入的,那是错误的” 那么信号处理程序呢?使用线程,原始线程最终将恢复 CPU 并继续运行,但使用信号处理程序,在信号处理程序返回之前什么都不会发生...
Jerry:当我给出这个答案时,我的印象是“可重入”和“异步信号安全”不是同义词,而 malloc/printf 是“可重入但不是异步信号” -安全的”。其实我还是有这种印象的;但@zwol 的评论表明,有很大一部分人认为它们是同义词,因此任何不是异步信号安全的函数都可能 i> 根据定义,是“可重入的”。听起来杰里也在那个阵营里。我想道德是:使用行话时,应该了解自己的受众及其假设。 :)
@Quuxplusone 我个人认为“可重入”是一个定义不明确的术语,应该避免使用它来支持“线程安全”、“递归调用安全” ”或“异步信号安全”,它们有更清晰的定义。所有异步信号安全的函数都是递归调用安全和线程安全的,所有递归调用安全的函数都是线程安全的,但反之则不然。递归调用安全只是回调到用户代码的库函数的一个相关类别(例如qsort
),这也是我刚刚编造的一个术语,我认为标准不会使用它。
@zwol:从词源上看,“reentrant”确实应该是“recursive-call-safe”的同义词,这就是我一直在使用它的方式。顺便说一句,int f(void (*g)()) static int x = 0; ++x; g();
是一个递归调用安全但不是线程安全的函数的示例,因为从两个不同的线程同时调用f([])
将导致x
上的数据竞争但调用@ 987654364@ 是完全安全的。 (对不起 C++ lambdas。这里的空间有限。;))【参考方案2】:
malloc
和printf
通常使用全局结构,并在内部使用基于锁的同步。这就是它们不能重入的原因。
malloc
函数可以是线程安全的,也可以是线程不安全的。两者都不可重入:
Malloc 在全局堆上运行,可能同时发生的两个不同的malloc
调用返回相同的内存块。 (第二次 malloc 调用应该在获取块的地址之前发生,但块没有被标记为不可用)。这违反了malloc
的后置条件,所以这个实现不会是可重入的。
为了防止这种影响,malloc
的线程安全实现将使用基于锁的同步。但是,如果从信号处理程序中调用 malloc,可能会出现以下情况:
malloc(); //initial call
lock(memory_lock); //acquire lock inside malloc implementation
signal_handler(); //interrupt and process signal
malloc(); //call malloc() inside signal handler
lock(memory_lock); //try to acquire lock in malloc implementation
// DEADLOCK! We wait for release of memory_lock, but
// it won't be released because the original malloc call is interrupted
当malloc
只是从不同的线程调用时不会发生这种情况。事实上,重入概念超越了线程安全,并且还要求函数能够正常工作即使其中一个调用永远不会终止。这就是为什么任何带锁的函数都不能重入的原因。
printf
函数也对全局数据进行操作。任何输出流通常都使用一个附加到资源数据的全局缓冲区(用于终端或文件的缓冲区)。打印过程通常是将数据复制到缓冲区并随后刷新缓冲区的序列。这个缓冲区应该像malloc
一样被锁保护。因此,printf
也是不可重入的。
【讨论】:
最后,谢谢。我正要写或多或少相同的东西,但是当我开始输入我的答案时,您的答案就出现了。 +1。 "任何带锁的函数都不能重入"。我在一个系统上工作,您可以在其中标记互斥锁以禁用信号,无论是在等待互斥锁时,还是在持有互斥锁的整个过程中。显然它很容易被误用,你必须保证函数会返回,但它被用来从可重入函数(通常在内核中)访问全局变量。我假设没有证据证明其他内核具有等效机制,而且标准不希望在可以避免的情况下要求这种恶作剧。 @Steve:例如,在 Linux 内核中,自旋锁通常会在获得锁时禁用中断。 “正常”睡眠互斥锁 OTOH 在启用中断的情况下运行。 从 C11 开始,malloc
和 printf
都必须是线程安全的(POSIX 从 2001 年开始就要求这样做)。但是,从异步信号处理程序中使用它们仍然不安全。
重入锁会让malloc重入吗?【参考方案3】:
让我们了解re-entrant 的含义。可以在先前的调用完成之前调用可重入函数。如果
在信号处理程序(或更一般地,比 Unix 的某些中断处理程序)中调用函数以处理函数执行期间引发的信号 递归调用函数malloc 不可重入,因为它管理着多个跟踪空闲内存块的全局数据结构。
printf 不可重入,因为它修改了一个全局变量,即 FILE* stout 的内容。
【讨论】:
【参考方案4】:这是因为两者都适用于全局资源:堆内存结构和控制台。
编辑:堆只不过是一种链表结构。每个malloc
或free
都会修改它,因此同时有多个线程对其进行写访问会破坏其一致性。
EDIT2:另一个细节:默认情况下,可以使用互斥锁使它们可重入。但这种方法成本高昂,并且无法保证它们将始终用于 MT 环境。
因此有两种解决方案:制作 2 个库函数,一个可重入,一个不可重入,或者将互斥锁部分留给用户。他们选择了第二个。
另外,也可能是因为这些函数的原始版本是不可重入的,所以为了兼容性而声明了。
【讨论】:
所以你声称malloc
不是线程安全的?有趣...(-1)而且您还声称包含互斥锁会使函数可重入...更有趣! (-2)
@PavelShved - malloc 是否是线程安全的是 arguable point。至少提供参考以帮助解决问题。 From your own post你不是说malloc有可能是线程un安全的吗?【参考方案5】:
如果您尝试从两个单独的线程调用 malloc(除非您有一个线程安全的版本,C 标准不保证),就会发生不好的事情,因为两个线程只有一个堆。 printf 也一样——行为未定义。这就是使它们在现实中不可重入的原因。
【讨论】:
好的,但是哪里会失败呢?如果我能得到一些例子会很好。 这是 C 规范中未定义的行为,没有可能失败的示例,因为在编写 C 规范时这不是问题(根本没有线程)。它可能在某些实现上起作用,而在其他实现上则根本不起作用,而两种实现都可能遵循规范。 @RIPUNJAY:对 malloc 的调用可能会返回相同的指针,因为两个 malloc 调用都确定该块可用(第一次调用将在确定块为空闲和标记之间中断它已分配)。 @UnixShadow:抱歉有问题。如果根本没有线程的概念,为什么 RE-ENTERANCY 与它相关联? @deadmg:线程安全和RE-ENTERANCY虽然是相关的,但它们是不同的。【参考方案6】:很可能是因为您无法开始写入输出,而对 printf 的另一个调用仍在打印它自己。内存分配和释放也是如此。
【讨论】:
这解释了什么是“可重入”,但不能解释为什么这些函数是不可重入的。 哦,太好了。我愚蠢地问这个关于printf的问题。谢谢。但是我们不能同时从两个不同的线程调用 malloc() 吗? “当另一个 printf 调用仍在打印它自己时,您无法开始写入输出。” 为什么?printf
的哪一点会导致这种情况?这并不明显。也许结果会是Hello, woSOMETHINGELSErld!
,但你问的所有内容都会被打印出来?
@RIPUNJAY TRIPATHI:这取决于实现,但如果您在编译中打开了线程支持,malloc 通常是线程安全的。
@Jeremy:线程安全和重入是一回事吗?我认为它们是两种不同的东西。以上是关于为啥说 malloc() 和 printf() 是不可重入的?的主要内容,如果未能解决你的问题,请参考以下文章