在UNIX系统中,我们知道malloc()
是一个不可重入的函数(系统调用)。这是为什么?
同样,printf()
也被认为是不可重入的;为什么呢?
我知道re-entrancy的定义,但我想知道为什么它适用于这些函数。 什么阻止他们被保证可以重入?
答案 0 :(得分:55)
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 :(得分:12)
让我们理解re-entrant的含义。可以在上一次调用完成之前调用重入函数。如果
,可能会发生这种情况malloc不是可重入的,因为它正在管理几个跟踪空闲内存块的全局数据结构。
printf不可重入,因为它修改了一个全局变量,即FILE * stout的内容。
答案 2 :(得分:4)
这里至少有三个概念,所有这些概念都用口语混为一谈,这可能就是你混淆的原因。
首先采取最简单的方法: 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添加了同样的东西(但是可选地,并且以不同的名称,因为Not Invented Here)为strtok_s
。
好的,但printf
是否也使用全球资源来构建其输出?事实上,它甚至意味着什么同时从两个线程打印到stdout?这将我们带到下一个主题。显然 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
或printf
或其他任何酷孩子使用的时间都很少。这些天。)
好的,那么 re-entrancy实际意味着什么?基本上,这意味着可以安全地递归调用函数 - 当第二次调用运行时,当前调用被“暂停” ,然后第一次调用仍然能够“从中断的地方继续”。 (从技术上讲,这个可能不是由于递归调用:第一次调用可能在线程A中,它在中间被线程B中断,这使得第二次调用。但这种情况只是一个线程安全的特例,我们可以在本段中忘记它。)
单个线程递归地调用malloc
和printf
都不可能 ,因为它们是叶子函数(它们不调用自身也不调用任何用户)受控制的代码,可能会进行递归调用)。而且,正如我们上面所看到的,自2001年以来,它们一直是针对*多*线程可重入调用的线程安全(通过使用锁)。
所以,谁告诉你malloc
和printf
不可重入的人是错的;他们想要说的可能是他们都有可能成为你程序中的关键部分 - 瓶颈只有一个线程可以一次通过。
迂腐:glibc确实提供了一个扩展,通过该扩展可以register_printf_function
调用任意用户代码,包括重新调用自身。这在所有排列中都是完全安全的 - 至少就线程安全而言。 (显然它打开了通向疯狂的格式字符串漏洞的大门。)有两种变体:register_printf_specifier
(记录合理,理性“弃用”)和{{1 (除了一个额外的未记录参数和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);
}
答案 3 :(得分:1)
很有可能因为你无法开始写输出而另一次对printf的调用仍在打印它自己。内存分配和释放也是如此。
答案 4 :(得分:-2)
这是因为它们都适用于全局资源:堆内存结构和控制台。
编辑:堆只是一种链式列表结构。每个malloc
或free
都会对其进行修改,因此同时拥有多个线程并对其进行写入访问会损害其一致性。
EDIT2:另一个细节:默认情况下,它们可以通过使用互斥锁进行重写。但是这种方法成本很高,并且没有保证它们将始终用于MT环境。
因此有两种解决方案:制作2个库函数,一个是可重入的,一个是非,或者将互斥部分留给用户。他们选择了第二个。
另外,可能是因为这些函数的原始版本是不可重入的,因此为了兼容性而声明了这些版本。
答案 5 :(得分:-4)
如果你尝试从两个独立的线程调用malloc(除非你有一个线程安全的版本,不能用C标准保证),就会发生不好的事情,因为两个线程只有一个堆。对于printf也是如此 - 行为未定义。这就是他们实际上不可重入的原因。