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

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

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

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

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只是从不同的线程调用时,这种情况不会发生。 实际上,重入概念不仅仅是线程安全, 即使其中一个调用永远不会终止 ,也要求函数正常工作。 这基本上就是为什么带锁的函数不会重入的原因。

printf函数也在全局数据上运行。 任何输出stream通常都会使用一个连接到资源数据的全局缓冲区来发送(terminal缓冲区或文件缓冲区)。 打印过程通常是将数据复制到缓冲区并刷新缓冲区的顺序。 这个缓冲区应该像malloc一样被锁保护。 因此, printf也是不可重入的。

让我们明白我们的意思是重入 。 在之前的调用完成之前,可以调用重入函数。 这可能发生如果

  • 在函数执行过程中产生一个信号,在一个信号处理函数中调用一个函数(或者比Unix更为普通的中断处理程序)
  • 一个函数被recursion地调用

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

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

这里至less有三个概念,所有这些概念都是用口语混淆的,这可能就是为什么你感到困惑。

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

首先采取最简单的方法: 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增加了相同的东西(但是可选地和在不同的名字下,因为Not Invented Here)作为strtok_s

好吧,但是printf也不使用全局资源来构build它的输出呢? 事实上,从两个线程同时打印到stdout的意思是什么呢? 这使我们接下来的话题。 很明显, printf将是任何使用它的程序中的关键部分 一次只允许一个执行线程在临界区内。

至less在符合POSIX标准的系统中,这是通过让printfflockfile(stdout)调用开始,然后调用flockfile(stdout)funlockfile(stdout) ,基本上就像使用与stdout相关的全局互斥体一样。

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

同样, malloc不太可能成为任何现代系统中的关键部分,因为现代系统足够聪明,可以为系统中的每个线程保留一个内存池 ,而不是让所有N个线程在单个池中进行争夺。 ( sbrk系统调用仍然可能是一个关键部分,但是malloc花费很less的时间在sbrk ,或者mmap ,或者现在很酷的小孩所使用的。

好的, 重入是什么意思呢? 基本上,这意味着函数可以被安全地recursion地调用 – 当第二次调用运行时,当前的调用被“搁置”,然后第一次调用仍然能够“拾取离开的地方”。 (从技术上讲,这可能不是由于recursion调用引起的:第一个调用可能在线程A中,由线程B在中间被中断,从而进行第二次调用。但是这种情况只是线程安全的特例,所以我们可以在这段中忘记它。)

printfmalloc都不能被单线程recursion调用,因为它们是叶函数(它们不会自己调用,也不会调用任何用户可能进行recursion调用的用户控制的代码)。 而且,正如我们上面看到的那样,自2001年以来,它们已经针对*multithreading重入调用进行线程安全(通过使用锁)。

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


迂腐的笔记:glibc确实提供了一个扩展,通过该扩展可以调用printf来调用任意的用户代码,包括重新调用自己。 这是完全安全的所有排列 – 至less就线程安全而言。 (显然,它打开了绝对疯狂的格式string漏洞的大门。)有两个变种: register_printf_function (这是logging和合理的理智,但正式“弃用”)和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); } 

很可能是因为你不能开始写输出,而另一个printf调用仍然是自我打印。 内存分配和释放也是如此。

这是因为两者都与全局资源一起工作:堆内存结构和控制台。

编辑:堆只是一种链接列表结构。 每个mallocfree修改它,所以有多个线程在同一时间写入访问将损坏其一致性。

编辑2:另一个细节:通过使用互斥,默认情况下它们可以重入。 但是这种方法代价高昂,并没有保证他们会一直用在MT环境中。

所以有两个解决scheme:创build2个库函数,一个是可重入的,另一个不是,或者将互斥部分留给用户。 他们select了第二个。

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

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