42

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

同样,printf()也可以说是不可重入的;为什么?

我知道重入的定义,但我想知道为什么它适用于这些功能。是什么阻止了他们保证可重入?

4

6 回答 6

63

malloc并且printf通常使用全局结构,并在内部使用基于锁的同步。这就是为什么它们不能重入。

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当只是从不同的线程调用时,这种情况不会发生。事实上,重入概念超越了线程安全,并且还要求函数正常工作,即使它的调用之一永远不会终止。这基本上就是为什么任何带锁的函数都不能重入的原因。

printf函数还对全局数据进行操作。任何输出流通常都使用一个附加到资源数据的全局缓冲区(用于终端或文件的缓冲区)。打印过程通常是将数据复制到缓冲区并随后刷新缓冲区的序列。这个缓冲区应该以同样的方式受到锁的保护malloc。因此,printf也是不可重入的。

于 2010-10-15T10:56:31.607 回答
13

让我们理解我们所说的re-entrant是什么意思。可以在先前的调用完成之前调用可重入函数。如果发生这种情况

  • 在信号处理程序(或更一般地,比 Unix 一些中断处理程序)中调用函数以获取在函数执行期间引发的信号
  • 递归调用函数

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

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

于 2010-10-15T10:46:35.313 回答
6

这里至少有三个概念,所有这些都在口语中混为一谈,这可能就是您感到困惑的原因。

  • 线程安全的
  • 临界区
  • 重入

首先采取最简单的方法:两者mallocprintf都是线程安全的。自 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它将成为任何使用它的程序中的关键部分。一次只允许一个执行线程进入临界区。

至少在 POSIX 兼容的系统中,这是通过printf调用 to 开始并以调用flockfile(stdout)结束来实现的funlockfile(stdout),这基本上就像使用与 stdout 关联的全局互斥锁一样。

但是,FILE程序中的每个不同的都可以有自己的互斥锁。这意味着一个线程可以同时调用fprintf(f1,...)另一个线程正在调用fprintf(f2,...). 这里没有竞争条件。(您的 libc 是否实际上并行运行这两个调用是QoI问题。我实际上不知道 glibc 做了什么。)

同样,malloc它不太可能成为任何现代系统中的关键部分,因为现代系统足够智能,可以为系统中的每个线程保留一个内存池,而不是让所有 N 个线程争夺一个内存池。(sbrk系统调用可能仍然是一个关键部分,但malloc很少花费时间在sbrk. 或mmap,或者这些天酷孩子正在使用的任何东西。)

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

单个线程也printf不能递归调用它们,因为它们是叶函数(它们不会调用自己,也不会调用任何可能进行递归调用的用户控制代码malloc而且,正如我们在上面看到的,自 2001 年以来,它们对 *multi-*threaded 重入调用是线程安全的(通过使用锁)。

因此,无论是谁告诉您printf并且malloc不可重入,都是错误的;他们的意思可能是它们都有可能成为程序中的关键部分——一次只能通过一个线程的瓶颈。


学究式注释:glibc 确实提供了一个扩展,通过它printf可以调用任意用户代码,包括重新调用自身。这在所有排列中都是完全安全的——至少就线程安全而言。(显然,它为绝对疯狂的格式字符串漏洞打开了大门。)有两种变体:(register_printf_function已记录且合理,但正式“弃用”)和register_printf_specifier(除了一个额外的未记录参数和完全缺乏外,几乎相同面向用户的文档)。我不会推荐它们中的任何一个,在这里提到它们只是作为一个有趣的旁白。

#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);
}
于 2014-11-11T20:10:18.757 回答
1

很可能是因为您无法开始写入输出,而对 printf 的另一个调用仍在打印它自己。内存分配和释放也是如此。

于 2010-10-15T10:16:05.523 回答
-2

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

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

EDIT2:另一个细节:默认情况下,它们可以通过使用互斥锁成为可重入的。但这种方法成本高昂,并且无法保证它们将始终用于 MT 环境。

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

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

于 2010-10-15T10:17:16.490 回答
-4

如果你尝试从两个单独的线程调用 malloc(除非你有一个线程安全的版本,C 标准不保证),就会发生不好的事情,因为两个线程只有一个堆。printf 也一样——行为未定义。这就是使它们在现实中不可重入的原因。

于 2010-10-15T10:20:11.853 回答