为什么malloc()和printf()表示不可重入?

时间:2010-10-15 10:12:25

标签: c unix operating-system reentrancy

在UNIX系统中,我们知道malloc()是一个不可重入的函数(系统调用)。这是为什么?

同样,printf()也被认为是不可重入的;为什么呢?

我知道re-entrancy的定义,但我想知道为什么它适用于这些函数。   什么阻止他们被保证可以重入?

6 个答案:

答案 0 :(得分:55)

mallocprintf通常使用全局结构,并在内部使用基于锁的同步。这就是他们不可重入的原因。

malloc函数可以是线程安全的,也可以是线程不安全的。两者都不是可重入的:

  1. Malloc在全局堆上运行,并且可能同时发生两次malloc的不同调用,返回相同的内存块。 (第二个malloc调用应该在获取块的地址之前发生,但是块没有标记为不可用)。这违反了malloc的后置条件,因此这种实施方式不会重复。

  2. 为了防止这种影响,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时,不会发生这种情况。实际上,重入概念超出了线程安全性,并且还要求函数正常工作即使其中一个调用永远不会终止。这基本上就是为什么带锁的任何函数都不可重入的原因。

  3. printf功能也适用于全球数据。任何输出流通常使用附加到资源数据的全局缓冲区发送到(终端缓冲区或文件)。打印过程通常是一系列复制数据以缓冲并随后刷新缓冲区。这个缓冲区应该以与malloc相同的方式受到锁的保护。因此,printf也是不可重入的。

答案 1 :(得分:12)

让我们理解re-entrant的含义。可以在上一次调用完成之前调用重入函数。如果

,可能会发生这种情况
  • 在函数执行期间引发的信号中,在信号处理程序(或者通常比Unix某些中断处理程序)中调用函数
  • 一个函数被递归调用

malloc不是可重入的,因为它正在管理几个跟踪空闲内存块的全局数据结构。

printf不可重入,因为它修改了一个全局变量,即FILE * stout的内容。

答案 2 :(得分:4)

这里至少有三个概念,所有这些概念都用口语混为一谈,这可能就是你混淆的原因。

  • 线程安全
  • 关键部分
  • 重入

首先采取最简单的方法: mallocprintf 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标准的系统中,这是通过让printfflockfile(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系统调用仍然可能是一个关键部分,但mallocsbrkmmapprintf或其他任何酷孩子使用的时间都很少。这些天。)

好的,那么 re-entrancy实际意味着什么?基本上,这意味着可以安全地递归调用函数 - 当第二次调用运行时,当前调用被“暂停” ,然后第一次调用仍然能够“从中断的地方继续”。 (从技术上讲,这个可能不是由于递归调用:第一次调用可能在线程A中,它在中间被线程B中断,这使得第二次调用。但这种情况只是一个线程安全的特例,我们可以在本段中忘记它。)

单个线程递归地调用mallocprintf都不可能 ,因为它们是叶子函数(它们不调用自身也不调用任何用户)受控制的代码,可能会进行递归调用)。而且,正如我们上面所看到的,自2001年以来,它们一直是针对*多*线程可重入调用的线程安全(通过使用锁)。

所以,谁告诉你mallocprintf不可重入的人是错的;他们想要说的可能是他们都有可能成为你程序中的关键部分 - 瓶颈只有一个线程可以一次通过。


迂腐: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)

这是因为它们都适用于全局资源:堆内存结构和控制台。

编辑:堆只是一种链式列表结构。每个mallocfree都会对其进行修改,因此同时拥有多个线程并对其进行写入访问会损害其一致性。

EDIT2:另一个细节:默认情况下,它们可以通过使用互斥锁进行重写。但是这种方法成本很高,并且没有保证它们将始终用于MT环境。

因此有两种解决方案:制作2个库函数,一个是可重入的,一个是非,或者将互斥部分留给用户。他们选择了第二个。

另外,可能是因为这些函数的原始版本是不可重入的,因此为了兼容性而声明了这些版本。

答案 5 :(得分:-4)

如果你尝试从两个独立的线程调用malloc(除非你有一个线程安全的版本,不能用C标准保证),就会发生不好的事情,因为两个线程只有一个堆。对于printf也是如此 - 行为未定义。这就是他们实际上不可重入的原因。