今天遇到的问题是,我需要在一个二进制文件中写入一个数字数组,并指定一个起始位置。我知道它应该从哪里开始,但我不希望覆盖在此之后的任何值,只想将数组插入到文件的起始位置。例如:
12345
让我们将456推到位置2:
12456345
我知道可能需要自己实现,但我想知道你对如何尽可能高效地实现它的意见。
今天遇到的问题是,我需要在一个二进制文件中写入一个数字数组,并指定一个起始位置。我知道它应该从哪里开始,但我不希望覆盖在此之后的任何值,只想将数组插入到文件的起始位置。例如:
12345
让我们将456推到位置2:
12456345
我知道可能需要自己实现,但我想知道你对如何尽可能高效地实现它的意见。
这是一个名为extend_file_and_insert()
的函数,大体上可以完成工作。
#include <sys/stat.h>
#include <unistd.h>
enum { BUFFERSIZE = 64 * 1024 };
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
/*
off_t is signed
ssize_t is signed
size_t is unsigned
off_t for lseek() offset and return
size_t for read()/write() length
ssize_t for read()/write() return
off_t for st_size
*/
static int extend_file_and_insert(int fd, off_t offset, char const *insert, size_t inslen)
{
char buffer[BUFFERSIZE];
struct stat sb;
int rc = -1;
if (fstat(fd, &sb) == 0)
{
if (sb.st_size > offset)
{
/* Move data after offset up by inslen bytes */
size_t bytes_to_move = sb.st_size - offset;
off_t read_end_offset = sb.st_size;
while (bytes_to_move != 0)
{
ssize_t bytes_this_time = MIN(BUFFERSIZE, bytes_to_move);
ssize_t rd_off = read_end_offset - bytes_this_time;
ssize_t wr_off = rd_off + inslen;
lseek(fd, rd_off, SEEK_SET);
if (read(fd, buffer, bytes_this_time) != bytes_this_time)
return -1;
lseek(fd, wr_off, SEEK_SET);
if (write(fd, buffer, bytes_this_time) != bytes_this_time)
return -1;
bytes_to_move -= bytes_this_time;
read_end_offset -= bytes_this_time; /* Added 2013-07-19 */
}
}
lseek(fd, offset, SEEK_SET);
write(fd, insert, inslen);
rc = 0;
}
return rc;
}
(请注意于2013-07-19添加的额外行;这是一个错误,只有当缓冲区大小小于要复制到文件上的数据时才会显示出来。感谢malat指出了这个错误。现在使用BUFFERSIZE = 4
进行测试。)
这是一些小规模的测试代码:
#include <fcntl.h>
#include <string.h>
static const char base_data[] = "12345";
typedef struct Data
{
off_t posn;
const char *data;
} Data;
static const Data insert[] =
{
{ 2, "456" },
{ 4, "XxxxxxX" },
{ 12, "ZzzzzzzzzzzzzzzzzzzzzzzzX" },
{ 22, "YyyyyyyyyyyyyyyY" },
};
enum { NUM_INSERT = sizeof(insert) / sizeof(insert[0]) };
int main(void)
{
int fd = open("test.dat", O_RDWR | O_TRUNC | O_CREAT, 0644);
if (fd > 0)
{
ssize_t base_len = sizeof(base_data) - 1;
if (write(fd, base_data, base_len) == base_len)
{
for (int i = 0; i < NUM_INSERT; i++)
{
off_t length = strlen(insert[i].data);
if (extend_file_and_insert(fd, insert[i].posn, insert[i].data, length) != 0)
break;
lseek(fd, 0, SEEK_SET);
char buffer[BUFFERSIZE];
ssize_t nbytes;
while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
write(1, buffer, nbytes);
write(1, "\n", 1);
}
}
close(fd);
}
return(0);
}
它产生以下输出:
12456345
1245XxxxxxX6345
1245XxxxxxX6ZzzzzzzzzzzzzzzzzzzzzzzzZ345
1245XxxxxxX6ZzzzzzzzzzYyyyyyyyyyyyyyyYzzzzzzzzzzzzzzZ345
应该在一些更大的文件上进行测试(比BUFFERSIZE大的文件),但是测试时使用的BUFFERSIZE应该远小于64 KiB,我使用了32个字节,结果看起来还不错。我只是用肉眼检查了结果,但这些模式都是为了方便查看它们是否正确而设计的。代码没有检查任何lseek()
调用,这是一个较小的风险。
read_end_offset
的值需要减去 bytes_this_time
。我已经编写了修复程序,并使用 BUFFERSIZE=4
进行了测试(这在未修复的代码中显示了错误)。感谢你指出这个错误。 - Jonathan Leffler首先使用ftruncate()
来扩大文件到最终大小。然后将旧结尾的所有内容复制到新结尾,一直回溯到插入点。然后用要插入的数据覆盖中间内容。我认为这是效率最高的方法,因为文件系统通常不会真正提供在文件中间进行“插入”。
我同意其他人的观点,但是让我稍微以不同的方式陈述解决方案:
获取一个临时文件名(有特定于操作系统的调用)
将原始文件复制到临时文件中(现在有两个相同文件的副本)
打开原始文件进行“追加”。
将其“截断”到您要插入的位置
编写新数据
打开您的临时文件进行“读取”
“寻找”插入点(再次调用是特定于操作系统的)
在临时文件中读取到文件末尾;插入到您仍然打开的原始文件中进行“追加”。
关闭两个文件
删除临时文件
这听起来很复杂吗?绝对是的!实现高效的中间文件插入比追加更加复杂。