getline()与fgets():控制内存分配

33
阅读文件行的方法有 getline()fgets() POSIX函数(忽略可怕的gets())。常识告诉我们,getline()fgets()更受欢迎,因为它根据需要分配行缓冲区。
我的问题是:这不危险吗?如果有人意外地或恶意地创建一个没有'\n'字节的100GB文件,那么我的getline()调用不会分配大量内存吗?

1
当然。我的担心是,使用getline()时系统可能会交换到停顿点。如果使用fgets(),这种情况就不会发生。我想知道是否有什么误解或者getline()的这种风险已经为人所知。 - edavid
@xing,我在手册中看到的唯一错误代码是 EINVAL - Christian Gibbons
4
“疯狂的内存量”- 100GB是疯狂的内存量吗?100KB?1PB?告诉未来20年的某个人……什么样的内存量是疯狂的,什么不是?“getline()”是1990年代的一个函数。它的存在是为了“让生活更轻松”,而不是“处理用户想要的所有疯狂情况”。编写带有最大限制的getline()实现并不难。 - KamilCuk
1
@KamilCuk 分别是是、否和是。20年后,答案将是否、否和是。构成疯狂内存量的确切阈值肯定会在以后的年份中增加,但基本问题(存在会导致内存不足或抖动的行)将存在于任何有限内存系统中。 - Ray
3
@KamilCuk: "一个不带长度前缀的包含100GB记录的文件这个概念,无论目标机器有多少内存,似乎都有点疯狂。" - supercat
显示剩余5条评论
6个回答

28
我的问题是:这不危险吗?如果有人意外或恶意地创建一个没有 '\n' 字节的 100GB 文件,那么我的 getline() 调用不会分配一大堆内存吗?
是的,你描述的情况是可能存在的风险。然而,
  • 如果程序需要一次性将整个行加载到内存中,则允许 getline() 尝试使用它并不比编写自己的代码使用 fgets() 更具有固有的风险; 和
  • 如果您的程序存在此类漏洞,则可以使用 setrlimit() 来限制它可保留的(虚拟)内存总量以减轻风险。这可以用于导致它失败,而不是成功分配足够的内存来干扰其余系统。
总的来说,最好的方法是在第一次编写代码时就不要将输入单位设置为完整的行(一次全部),但这种方法也有其自身的复杂性。

setrlimit可能有效,但存在陷阱;它无法在现代内核上限制常驻内存(RSS),而虚拟内存(VSZ)通常可以安全地超过物理RAM大小,而不会开始抖动,因此限制它可能会导致分配失败,而您不希望这样做。 - Ray
@Ray,关于setrlimit(),我特别建议限制进程允许的虚拟内存量,而不是它的RSS。选择什么限制的问题是次要的,没有涉及到。 - John Bollinger
fgets的问题确实有道理。我已经解决了这个异议。但即使限制虚拟内存,setrlimit问题仍然存在;RSS是我们实际上想要限制以避免抖动,而VSZ是其宽松的上限。例如,我正在运行一个使用5 MB实际内存但550 MB VSZ的gvim副本。使用setrlimit将虚拟内存限制为实际RAM大小将导致分配在需要之前失败。我同意setrlimit在这里可能有用,但该方法存在问题,OP需要在使用之前了解这些问题才能发挥其作用。 - Ray
很公平,@Ray,但我认为,如果程序需要一次性将整行加载到内存中,那么设置一个比所需的VSZ限制要小得多的限制以避免该操作抖动是合理的。毕竟,OP提出了他描述的输入是格式错误的。如果程序需要处理巨大的行,则要求它们完全保存在内存中是问题所在。 - John Bollinger

14

是的,这可能会很危险。不知道这在其他电脑上会怎么样,但是运行下面的代码使我的电脑冻结了,以至于需要进行硬重启:

/* DANGEROUS CODE */

#include <stdio.h>

int main(void)
{
    FILE *f;
    char *s;
    size_t n = 0;

    f = fopen("/dev/zero", "r");
    getline(&s, &n, f);

    return 0;
}

实际上,一段时间后输出是 $ Killed。没有操作系统冻结。这不好玩:(。我猜Ubuntu现在有一些防御措施来防止进程消耗过多的内存:(。 - milanHrabos

3

一些编码准则(例如MISRA C)可能阻止您使用动态内存分配(例如getline())。这么做有其原因,例如避免内存泄漏。

如果您知道所有可接受行的最大大小,则可以使用fgets()而不是getline()来避免内存分配,从而消除一个潜在的内存泄漏点。


2
getline 函数在内部使用 mallocrealloc,如果它们失败,则返回 -1,因此结果与尝试调用 malloc(100000000000) 没有区别。也就是说,errno 被设置为 ENOMEM,并且 getline 返回 -1。
因此,无论您使用 getline 还是尝试使用 fgets 和手动内存分配来确保读取完整行,都会遇到相同的问题。

7
除非你使用 fgets() 和手动内存分配,否则你可以在进程的整个内存耗尽之前选择退出。 - TripeHound
6
实际上,这种行为与简单地调用malloc(100000000000)非常不同,因为getline()首先分配一个小的空间,然后逐步重新分配直到它找到换行符、文件结尾或内存限制。这个过程会对系统造成巨大的压力,可能导致系统变得无法使用,而malloc(100000000000)可能只会立即返回NULL,而没有其他后果。 - chqrlie

1

实际上,它取决于您如何处理过长的行。

使用具有相当大缓冲区的fgets通常可以工作,并且您可以检测到它已经“失败”——缓冲区末尾没有换行符。可能可以避免始终执行strlen()以确认缓冲区是否溢出,但这是另一个问题。

也许您的策略是简单地跳过无法处理的行,或者剩余部分只是您要忽略的注释,在这种情况下,很容易将fgets放入循环中以舍弃其余部分的行而不会产生分配惩罚。

如果您确实想读取整个行,则getline可能是更好的策略。恶意用户需要大量磁盘空间才能导致您描述的不良行为,或者将类似于/ dev / random之类的内容作为输入文件名传递。

同样,如果getline无法重新分配内存,则会以一种您可以从中恢复的方式失败,但是如果您正在重用缓冲区进行多行读取,则在错误后尝试读取更多内容之前,可能需要释放已有的缓冲区,因为它仍然被分配并且可能已经增长到最大。


我现在决定设置一行长度限制为10,000,并使用fgets()。在我的用例中,限制行长度是完全可以的。我提出了原始问题,因为在glibc文档中关于fgets()存在一个缺陷警告,调用者无法确定输入数据是否包含NUL字节,建议改用getline()。然而,对于getline(),它可能通过不受控制的内存分配使系统崩溃,却没有任何警告。 - edavid
哦,如果你的字符串包含空字符,那么很多C字符串API就会失效。但是如果确实包含空字符,那么它就不是一个文本文件,所以你根本不应该使用基于行的处理方式。 - Gem Taylor
就注入空字节的恶意潜力而言,您的代码将看到一行被缩短了,也许(取决于您如何检测它),然后将下一行视为行继续。我无法立即看出这比任何其他稍微有点可疑的文本文件输入更糟糕。 - Gem Taylor
1
没错。然而,glibc文档关于fgets的说法是:“不要用它来读取用户编辑的文件,因为如果用户插入了空字符,你应该正确处理它或打印一个清晰的错误消息。我们建议使用getline代替fgets。” - 我很惊讶他们非常关心这个麻烦,但显然并不关心getline的无限内存分配的危险。 - edavid
嗯...他们可能建议一种适当处理它的方法吗?我遇到了 strlenftell 差异不匹配的问题? ftell 是一个不错的速度调用,但这意味着你必须调用 strlen。实际上,在一般情况下使用 ftell 并不是一个坏策略,但你需要处理 Windows/Unix 文本文件。 - Gem Taylor
显示剩余2条评论

0

getline()函数为你重新分配缓冲区,以减轻程序中的内存管理负担。

但实际上,这可能导致大量内存被分配。如果这是一个问题,那么你应该采取额外措施使用不会隐式分配内存的函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接