在C语言中打开和写入多个文件

4
输入是一个大约70GB的文件,每一行都包含客户信息。程序读取这个文件并为每个客户创建一个文件。有8000个客户,但我们必须为40000个客户提供服务。当前使用UNIX sort命令按客户排序文件,然后写入客户文件。这样程序只需要一个单一的文件处理器来创建文件。 我们不想使用sort命令,因为它需要大约1.5小时。然而,这意味着将需要打开8000个文件处理器。可能需要修改内核参数。 是否可以在不更改内核参数的情况下打开这么多文件。我尝试浏览了libevent网站,但不确定是否这是正确的解决方案。

你不能维护一个按客户端排序的文件索引,然后逐个处理客户端吗? - Alexey Frunze
2个回答

11

您不一定需要同时打开8000个文件句柄,也不需要对数据进行排序。除非您需要将每个客户端的行排序。

假设您可以通过行上的某个项目来识别客户端。比如说(举个例子),是每行的前8个字符,那么您的伪代码看起来像这样:

delete all files matching "*_out.dat"
for each line in file:
    key = left (line, 8)
    open file key + "_out.dat" for append
    write line to file
    close file

就是这样,简单明了。每次只打开一个文件,无需浪费时间进行排序。

现在还可以进行进一步的改进,其中之一是:

  1. 除非下一行具有不同的键,否则不要关闭上一行的文件。这将捕获连续出现一百个相同键的情况,并在这种情况下保持文件打开。

  2. 维护最近使用列表(例如16个不同的键)中的打开文件句柄缓存。同样,这将防止关闭,直到必须重用文件句柄,但它也将处理集群更有效的情况(例如客户1,2,3,7,1,2,3,2,2,3,7,4,...)。

但基本理论仍然相同:当您可以使用更少的文件时,请勿尝试一次打开8000(或40000)个文件。


或者,只需处理数据,将所有内容保存到数据库中,并使用查询来创建每个文件以进行一系列查询。是否比上述解决方案更快应该得到测试,实际上,此处提供的每个建议都应该得到测试。要衡量,不要猜测!


现在,既然我已经引用了优化口号,让我们进行一些时间测试,要记住这是特定于我的硬件,可能与您的不同。

从以下脚本开始,生成一个100万行的文件,其中每行的前八个字符是介于1000000010032767之间的随机数。我们将使用第5至8个字符来为我们提供客户编号,大约每个客户有一百行:

#!/bin/bash
line='the quick brown fox jumps over the lazy dog'
for p0 in 1 2 3 4 5 6 7 8 9 0 ; do
 for p1 in 1 2 3 4 5 6 7 8 9 0 ; do
  for p2 in 1 2 3 4 5 6 7 8 9 0 ; do
   for p3 in 1 2 3 4 5 6 7 8 9 0 ; do
    for p4 in 1 2 3 4 5 6 7 8 9 0 ; do
     for p5 in 1 2 3 4 5 6 7 8 9 0 ; do
      ((x = 10000000 + $RANDOM))
      echo "$x$line"
     done
    done
   done
  done
 done
done

生成的文件大小约为50M。我们可以通过将其两次复制并连接到另一个文件中来将其扩展到100M,这将为每个客户提供约200行。


现在,审查以下程序:

#include <stdio.h>
#include <string.h>
#include <errno.h>

#define FOUT_STR "1234_out.dat"

int main (void) {
    FILE *fIn, *fOut;
    char outFile[sizeof (FOUT_STR)];
    char buff[1000];

    if ((fIn = fopen ("data.dat", "r")) == NULL) {
        printf ("Error %d opening 'data.dat'\n", errno);
        return 1;
    }

    memcpy (outFile, FOUT_STR, sizeof (FOUT_STR));
    if ((fOut = fopen (outFile, "w")) == NULL) {
        printf ("Error %d opening '%s'\n", errno, outFile);
        fclose(fIn);
        return 1;
    }

    while (fgets (buff, sizeof (buff), fIn) != NULL) {
        fputs (buff, fOut);
    }

    fclose (fOut);
    fclose (fIn);
    return 0;
}

这为将所有条目写入单个文件提供了基准数字,并且运行时间不到一秒钟。


现在让我们来看一个每两百行打开一个新文件的例子 - 如果文件已按客户排序,则会看到这种行为:

#include <stdio.h>
#include <string.h>
#include <errno.h>

#define FOUT_STR "1234_out.dat"

int main (void) {
    FILE *fIn, *fOut;
    char outFile[sizeof (FOUT_STR)];
    char buff[1000];
    char custNum[5];
    int i = -1;

    if ((fIn = fopen ("data.dat", "r")) == NULL) {
        printf ("Error %d opening 'data.dat'\n", errno);
        return 1;
    }

    fOut = NULL;
    while (fgets (buff, sizeof (buff), fIn) != NULL) {
        i++;
        if ((i % 200) == 0) {
            if (fOut != NULL)
                fclose (fOut);
            sprintf (custNum, "%04d", i / 200);
            memcpy (outFile, FOUT_STR, sizeof (FOUT_STR));
            memcpy (outFile, custNum, 4);
            if ((fOut = fopen (outFile, "w")) == NULL) {
                printf ("Error %d opening '%s'\n", errno, outFile);
                break;
            }
        }
        fputs (buff, fOut);
    }
    if (fOut != NULL)
        fclose (fOut);

    fclose (fIn);
    return 0;
}

对于一个100M的文件,这需要大约2秒钟(0:00:02),测试200M和400M的文件表明它具有线性扩展性。也就是说,如果你有一个排序后的70G文件,大约需要1400秒或0:23:20。请注意,这是在你排序成本的基础上计算的,即1.5小时(1:30:00),总成本为1:53:20。


现在让我们实现一个简单的程序,每一行都将文件打开并追加:

#include <stdio.h>
#include <string.h>
#include <errno.h>

#define FOUT_STR "1234_out.dat"

int main (void) {
    FILE *fIn, *fOut;
    char outFile[sizeof (FOUT_STR)];
    char buff[1000];

    if ((fIn = fopen ("data.dat", "r")) == NULL) {
        printf ("Error %d opening 'data.dat'\n", errno);
        return 1;
    }

    while (fgets (buff, sizeof (buff), fIn) != NULL) {
        memcpy (outFile, FOUT_STR, sizeof (FOUT_STR));
        memcpy (outFile, &(buff[4]), 4);
        if ((fOut = fopen (outFile, "a")) == NULL) {
            printf ("Error %d opening '%s'\n", errno, outFile);
            break;
        }
        fputs (buff, fOut);
        fclose (fOut);
    }

    fclose (fIn);
    return 0;
}

当我们使用100M的文件运行它时,需要244秒(0:04:04)。再一次测试200M和400M的文件表明呈线性扩展。因此,对于70G的文件,这将是47:26:40,远不是您所谓的少于两小时的排序和处理选项的改进。


然而,让我们尝试另一种方法,使用以下程序,每次通过输入文件维护一百个文件句柄(重复一百次):

#include <stdio.h>
#include <string.h>
#include <errno.h>

#define FOUT_STR "1234_out.dat"

int main (void) {
    FILE *fIn, *fOut[100];
    char outFile[sizeof (FOUT_STR)];
    char buff[1000];
    int seg, cust;
    char segNum[3], custNum[3];

    for (seg = 0; seg < 100; seg++) {
        sprintf (segNum, "%02d", seg);

        if ((fIn = fopen ("data.dat", "r")) == NULL) {
            printf ("Error %d opening 'data.dat'\n", errno);
            return 1;
        }

        for (cust = 0; cust < 100; cust++) {
            sprintf (custNum, "%02d", cust);

            memcpy (outFile, FOUT_STR, sizeof (FOUT_STR));
            memcpy (outFile+0, segNum, 2);
            memcpy (outFile+2, custNum, 2);
            if ((fOut[cust] = fopen (outFile, "w")) == NULL) {
                printf ("Error %d opening '%s'\n", errno, outFile);
                return 1;
            }
        }

        while (fgets (buff, sizeof (buff), fIn) != NULL) {
            if (memcmp (&(buff[4]), segNum, 2) == 0) {
                cust = (buff[6] - '0') * 10 + buff[7] - '0';
                fputs (buff, fOut[cust]);
            }
        }

        for (cust = 0; cust < 100; cust++) {
            fclose (fOut[cust]);
        }

        fclose (fIn);
    }

    return 0;
}

这是一个略微变化的方法,实际上会处理输入文件100次,每次只处理针对100个单独输出文件的行。

当在100M文件上运行时,大约需要28秒(0:00:28)。再次注意,对于200M和400M文件似乎呈线性扩展,因此70G的文件应该需要5:26:40。

仍然远远达不到两小时的目标。


那么,当我们一次打开一千个输出文件时会发生什么:

#include <stdio.h>
#include <string.h>
#include <errno.h>

#define FOUT_STR "1234_out.dat"

int main (void) {
    FILE *fIn, *fOut[1000];
    char outFile[sizeof (FOUT_STR)];
    char buff[1000];
    int seg, cust;
    char segNum[2], custNum[4];

    for (seg = 0; seg < 10; seg++) {
        sprintf (segNum, "%01d", seg);

        if ((fIn = fopen ("data.dat", "r")) == NULL) {
            printf ("Error %d opening 'data.dat'\n", errno);
            return 1;
        }

        for (cust = 0; cust < 1000; cust++) {
            sprintf (custNum, "%03d", cust);

            memcpy (outFile, FOUT_STR, sizeof (FOUT_STR));
            memcpy (outFile+0, segNum, 1);
            memcpy (outFile+1, custNum, 3);
            if ((fOut[cust] = fopen (outFile, "w")) == NULL) {
                printf ("Error %d opening '%s'\n", errno, outFile);
                return 1;
            }
        }

        while (fgets (buff, sizeof (buff), fIn) != NULL) {
            if (memcmp (&(buff[4]), segNum, 1) == 0) {
                cust = (buff[5] - '0') * 100 + (buff[6] - '0') * 10 + buff[7] - '0';
                fputs (buff, fOut[cust]);
            }
        }

        for (cust = 0; cust < 1000; cust++) {
            fclose (fOut[cust]);
        }

        fclose (fIn);
    }

    return 0;
}

处理100兆文件需要大约12秒钟,这将为我们提供2小时20分钟的时间,接近排序但还不够。


不幸的是,当我们迈出下一个逻辑步骤,试图一次性打开全部10000个文件时,我们发现:

Error 24 opening '1020_out.dat'

这意味着我们终于到达了极限(标准输入、标准输出、标准错误和其他大约1019个文件句柄),这表明1024个句柄大约是我们被允许的全部。

所以也许排序和处理方法确实是最好的方法。


感谢您的努力。您为我们提供了许多解决方案,以及它们是如何相互关联的。 不幸的是,目前使用的方法似乎是最好的。 由于许可成本和高层不允许使用免费数据库,我们将无法实施数据库解决方案。 再次感谢。 - Bharat Elwe
@BharatElwe,那些一概排斥可能解决方案的人都是白痴(不包括你,我指的是你们那些所谓的“前辈”)。但也许你得像我这样成为一个老练的老山羊,才能公开称他们为白痴而逃脱惩罚 :-) 我至少会尝试一下数据库解决方案。如果你发现它可以在27分钟内完成最快的非数据库解决方案需要两个小时才能完成的工作,你就向前辈们展示出来,让他们做决定。但至少你已经提供了建议,如果他们选择无视你的建议,那是他们的问题,而不是你的。 - paxdiablo

0

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