ult*_*use 37 c unix operating-system reentrancy
在UNIX系统中,我们知道malloc()
是一种不可重入的函数(系统调用).这是为什么?
同样,printf()
据说也是不可重入的; 为什么?
我知道re-entrancy的定义,但我想知道为什么它适用于这些函数.是什么阻止他们保证可以重入?
P S*_*ved 56
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
Run Code Online (Sandbox Code Playgroud)
malloc
简单地从不同的线程调用时不会发生这种情况.实际上,重入概念超出了线程安全性,并且即使其中一个调用永远不会终止,也要求函数正常工作.这基本上是为什么带锁的任何函数都不可重入的原因.
该printf
功能还对全球数据进行操作.任何输出流通常使用附加到资源数据的全局缓冲区发送到(终端缓冲区或文件).打印过程通常是一系列复制数据以缓冲并随后刷新缓冲区.这个缓冲区应该以相同的方式受到锁的保护malloc
.因此,printf
也是不可重入的.
这里至少有三个概念,所有这些都在口语中混为一谈,这可能是您感到困惑的原因。
先来一个最简单的:无论malloc
和printf
是线程安全的。自 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);
}
Run Code Online (Sandbox Code Playgroud)
非线程安全函数的一个示例是strtok
. 如果strtok
同时从两个不同的线程调用,结果是未定义的行为——因为strtok
内部使用静态缓冲区来跟踪其状态。glibc 添加strtok_r
来解决这个问题,C11 添加了与strtok_s
.
好的,但也不printf
使用全球资源来构建其输出?事实上,同时从两个线程打印到标准输出意味着什么?这将我们带到下一个主题。显然,在任何使用它的程序中printf
都将成为关键部分。一次只允许一个执行线程进入临界区。
至少在符合 POSIX 的系统中,这是通过以printf
对 的调用开始并以对 的调用flockfile(stdout)
结束来实现的funlockfile(stdout)
,这基本上就像获取与 stdout 关联的全局互斥锁。
但是,FILE
程序中的每个不同的都允许有自己的互斥锁。这意味着一个线程可以同时调用fprintf(f1,...)
第二个线程正在调用fprintf(f2,...)
. 这里没有竞争条件。(您的 libc 是否实际上并行运行这两个调用是QoI问题。我实际上不知道 glibc 是做什么的。)
同样,malloc
在任何现代系统中都不太可能成为关键部分,因为现代系统足够聪明,可以为系统中的每个线程保留一个内存池,而不是让所有 N 个线程争夺单个池。(sbrk
系统调用可能仍然是一个关键部分,但malloc
在sbrk
. 或 中花费的时间很少。或者mmap
,或者现在很酷的孩子们正在使用的任何东西。)
好了,什么重入实际上是什么意思?基本上,这意味着可以安全地递归调用该函数——当前调用被“搁置”,而第二次调用运行,然后第一次调用仍然能够“从停止的地方开始”。(从技术上讲,这可能不是由于递归调用:第一次调用可能在线程 A 中,它在中间被线程 B 中断,从而进行第二次调用。但这种情况只是线程安全的一个特例,所以我们可以在这一段中忘记它。)
既printf
不能malloc
也不可能被单个线程递归调用,因为它们是叶函数(它们不调用自己,也不调用任何可能进行递归调用的用户控制代码)。而且,正如我们在上面看到的,自 2001 年以来,它们对 *multi-*threaded re-entrant 调用一直是线程安全的(通过使用锁)。
所以,谁告诉你这个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);
}
Run Code Online (Sandbox Code Playgroud)