在Linux下使用C语言按块读写数据。

7

我有一个ASCII文件,每一行都包含一个不定长度的记录。例如:

Record-1:15 characters
Record-2:200 characters
Record-3:500 characters
...
...
Record-n: X characters

由于文件大小约为10GB,我希望能够分块读取记录。一旦读取完成,我需要将它们进行转换,并以二进制格式写入另一个文件中。

因此,在读取方面,我的第一反应是创建一个char数组,例如:

FILE *stream; 
char buffer[104857600]; //100 MB char array
fread(buffer, sizeof(buffer), 104857600, stream);
  1. 假设Linux会发出一个系统调用并获取整个100MB的数据,这是否正确?
  2. 由于记录是通过换行符分隔的,因此我在缓冲区中逐个字符搜索换行符并重建每个记录。

我的问题是,这是我应该如何分块读取数据并重建每个记录的方式,还是有更好的替代方法可以一次性读取ASCII文件中x个大小可变的行?

接下来,在写入过程中,我执行相同的操作。我有一个写字符缓冲区,将其传递给fwrite以一次性写入整组记录。

fwrite(buffer, sizeof(buffer), 104857600, stream);

更新:如果我使用 setbuf(stream, buffer) 将缓冲区(buffer)设置为100MB的字符缓冲区,fgets函数是否会从缓冲区中获取数据,还是会进行磁盘IO操作?

看一下fgets,如果你想的话,它会为你获取一行。 - Richard J. Ross III
我想避免逐行读取,而是希望一次性读取X个大小不同的行。此外,使用fgets(),我必须有一个缓冲区,最长的行可以适应其中。由于我的记录大小可以从几百字节到16MB不等,因此我会浪费内存。 - Jimm
3个回答

6
  1. Yes, fread will fetch the entire thing at once. (Assuming it's a regular file.) But it won't read 105 MB unless the file itself is 105 MB, and if you don't check the return value you have no way of knowing how much data was actually read, or if there was an error.

  2. Use fgets (see man fgets) instead of fread. This will search for the line breaks for you.

    char linebuf[1000];
    FILE *file = ...;
    while (fgets(linebuf, sizeof(linebuf), file) {
        // decode one line
    }
    
  3. There is a problem with your code.

    char buffer[104857600]; // too big
    

    If you try to allocate a large buffer (105 MB is certainly large) on the stack, then it will fail and your program will crash. If you need a buffer that big, you will have to allocate it on the heap with malloc or similar. I'd certainly keep stack usage for a single function in the tens of KB at most, although you could probably get away with a few MB on most stock Linux systems.

作为一种替代方案,您可以将整个文件mmap到内存中。在大多数情况下,这不会改善或降低性能,但更容易使用。
int r, fdes;
struct stat st;
void *ptr;
size_t sz;

fdes = open(filename, O_RDONLY);
if (fdes < 0) abort();
r = fstat(fdes, &st);
if (r) abort();
if (st.st_size > (size_t) -1) abort(); // too big to map
sz = st.st_size;
ptr = mmap(NULL, sz, PROT_READ, MAP_SHARED, fdes, 0);
if (ptr == MAP_FAILED) abort();
close(fdes); // file no longer needed

// now, ptr has the data, sz has the data length
// you can use ordinary string functions

使用 mmap 的优点在于您的程序不会耗尽内存。 在 64 位系统上,您可以将整个文件一次性放入您的地址空间中(即使是一个 10 GB 的文件),当您的程序访问内存时,系统会自动读取新的块。旧的块将被自动丢弃,并在您的程序再次需要它们时重新读取。这是处理大型文件的很好方法。

2

如果可以的话,你可能会发现使用mmap映射文件是最简单的方法。 mmap将(部分)文件映射到内存中,因此整个文件基本上可以作为字节数组访问。在您的情况下,您可能无法一次映射整个文件,它看起来可能像:

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/mman.h>


/* ... */

struct stat stat_buf;
long pagesz = sysconf(_SC_PAGESIZE);
int fd = fileno(stream);
off_t line_start = 0;
char *file_chunk = NULL;
char *input_line;
off_t cur_off = 0;
off_t map_offset = 0;
/* map 16M plus pagesize to ensure any record <= 16M will always fit in the mapped area */
size_t map_size = 16*1024*1024+pagesz;
if (map_offset + map_size > stat_buf.st_size) {
  map_size = stat_buf.st_size - map_offset;
}
fstat(fd, &stat_buf);
/* map the first chunk of the file */
file_chunk = mmap(NULL, map_size, PROT_READ, MAP_SHARED, fd, map_offset);
// until we reach the end of the file
while (cur_off < stat_buf.st_size) {
  /* check if we're about to read outside the current chunk */
  if (!(cur_off-map_offset < map_size)) {
    // destroy the previous mapping
    munmap(file_chunk, map_size);
    // round down to the page before line_start
    map_offset = (line_start/pagesz)*pagesz;
    // limit mapped region to size of file
    if (map_offset + map_size > stat_buf.st_size) {
      map_size = stat_buf.st_size - map_offset;
    }
    // map the next chunk
    file_chunk = mmap(NULL, map_size, PROT_READ, MAP_SHARED, fd, map_offset);
    // adjust the line start for the new mapping
    input_line = &file_chunk[line_start-map_offset];
  }
  if (file_chunk[cur_off-map_offset] == '\n') {
    // found a new line, process the current line
    process_line(input_line, cur_off-line_start);
    // set up for the next one
    line_start = cur_off+1;
    input_line = &file_chunk[line_start-map_offset];
  }
  cur_off++;
}

大部分的复杂性在于避免创建过大的映射。您可以使用以下方式来映射整个文件:
char *file_data = mmap(NULL, stat_buf.st_size, PROT_READ, MAP_SHARED, fd, 0);

0

我的观点是使用fgets(buff)来自动检测新行。

然后使用strlen(buff)来计算缓冲区大小,

if( (total+strlen(buff)) > 104857600 )

然后在新的块中编写...

但是该块的大小几乎不会超过104857600字节。

如有错误请指正。


在我的情况下,buff 是字符数组。但是 fgets 的文档说明它操作类型为 FILE 的流。 - Jimm
fgets有3个参数 char*intFILE*。所以对于你的情况,请将缓冲区放在char*上。 - Oki Sallata

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