为什么malloc()和printf()说非伦敦?
-
30-09-2019 - |
题
在Unix系统中,我们知道 malloc()
是一个非诱因函数(系统调用)。这是为什么?
相似地, printf()
据说也是非伦特的;为什么?
我知道重新进入的定义,但我想知道为什么它适用于这些功能。是什么防止他们保证重新进入?
解决方案
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
也是非诱因。
其他提示
让我们了解我们的意思 重点. 。可以在先前的调用完成之前调用重点函数。如果这可能会发生
- 信号处理程序(或比Unix某些中断处理程序)在执行过程中升高的信号中调用函数
- 函数被递归称为
Malloc不是重新进入的,因为它正在管理几个跟踪自由存储器块的全局数据结构。
printf不是重新输入的,因为它修改了全局变量,即文件* stout的内容。
这里至少有三个概念,所有这些概念都用口语混为一谈,这可能就是为什么您感到困惑。
- 线程安全
- 关键部分
- 重点
首先采取最简单的一个: 两个都 malloc
和 printf
是 线程安全. 。自2011年以来,自2011年以来,自2001年以来在POSIX以及从那以后的实践中就可以保证它们在标准C中是安全性。这意味着以下程序可以保证不崩溃或表现出不良行为:
#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
也使用全球资源来构建其输出?实际上,甚至会有什么 意思是 从两个线程打印到stdout 同时? 这使我们进入了下一个主题。明显地 printf
会成为一个 关键部分 在任何使用它的程序中。 一次只允许执行一个线程。
至少在符合POSIX的系统中,这是通过拥有 printf
从打电话开始 flockfile(stdout)
并以打电话结尾 funlockfile(stdout)
, ,这基本上就像服用与Stdout相关的全局静音。
但是,每个都不同 FILE
在程序中,允许拥有自己的静音。这意味着一个线程可以调用 fprintf(f1,...)
同时第二个线程在呼叫的中间 fprintf(f2,...)
. 。这里没有种族条件。 (您的libc是否真的在并行运行这两个电话是 QOI 问题。我实际上不知道Glibc做什么。)
相似地, malloc
在任何现代系统中都不太可能成为关键部分,因为现代系统是 足够聪明,可以为系统中的每个线程保留一个内存池, ,而不是让所有n个线程在一个池上战斗。 (这 sbrk
系统呼叫仍然可能是关键部分,但是 malloc
花很少的时间在 sbrk
. 。或者 mmap
, ,或这些天很酷的孩子正在使用的任何东西。)
可以,然后呢 有什么 重新进入 实际上是什么意思? 基本上,这意味着该函数可以安全地递归地称为 - 当前调用是“搁置”的,而第二次调用则运行,然后第一个调用仍然可以“拾取其关闭的位置”。 (从技术上讲 可能 不是由于递归调用:第一个调用可能在线程A中,该线程A中间被线B中间打断,这是第二个调用。但是这种情况只是 线程安全, ,因此我们可以在本段中忘记它。)
两者都不 printf
也不 malloc
可以 是 由单个线程递归地称为叶子功能(它们不称呼自己,也不称为任何用户控制的代码,这些代码可能会进行递归调用)。而且,正如我们在上面看到的那样,自2001年以来,它们一直是针对 *多 *螺纹重新输入调用的线程安全的(使用锁)。
所以,谁告诉你 printf
和 malloc
非伦理是错误的;他们的意思是,他们俩都有可能成为 关键部分 在您的程序中 - 瓶颈一次只能通过一个线程。
pedantic注意: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);
}
很可能是因为您无法开始编写输出,而另一个对PrintF的调用仍在打印其自我。内存分配和交易的情况也是如此。
这是因为两者都可以使用全球资源:堆内存结构和控制台。
编辑:堆不过是一种链接列表结构。每个 malloc
或者 free
修改它,因此,在编写访问权限的同一时间内有几个线程会损害其一致性。
EDIT2:另一个细节:默认情况下可以通过使用Mutexes将它们重新进入。但是这种方法是昂贵的,而且没有garanty将始终在MT环境中使用。
因此,有两种解决方案:要制作2个库功能,一个重点和一个库,或将MUTEX零件留给用户。他们选择了第二个。
同样,这可能是因为这些功能的原始版本是非诱因的,因此已被宣布为兼容性。
如果您尝试从两个单独的线程调用malloc(除非您有一个线程安全版本,而不是由C标准保证),则发生坏事,因为两个线程只有一个堆。对于printf来说,行为是不确定的。这就是使他们实际上非属于的原因。