有任何想法吗?
很遗憾,你不能在不重写整个文件的情况下截断/覆盖文件开头的行。
我刚想到一个新方法,可能能解决你的问题...
你可以向文件中添加一个小标题,其结构如下。
编辑:垃圾,我刚才描述了循环缓冲区的变体!
标题字段
字节00-07(长整型)
- 写入文件的总行数(当前)。字节08-15(长整型)
- 指向文件“实际”第一行的指针。这最初将是标题结束后的下一个字节,但稍后会更改,当数据被覆盖时。字节16-23(长整型)
- 文件“结尾部分”的长度。同样,这最初为零,但稍后会更改,当数据被覆盖时。读取算法(伪代码)
读取整个文件。
读取指向文件“实际”第一行的标题字段 读取指定“结尾部分”长度的标题字段 读取文件末尾之前的每一行 定位到标题结束后的下一个字节 读取每行,直到完全读取“结尾部分”
写入算法(伪代码)
向文件中写入任意数量的新行。
读取包含文件总行数的标题字段 如果(行数)+(新行数)≤(最大行数),则 将新行附加到文件末尾 将线路计数的标题字段递增(按新行数) 否则 将尽可能多的行(最多为最大值)附加到文件末尾 从指向第一行的标题字段开始,读取仍需编写的行数 查找刚刚读取的行的总字节数 将指向第一行的标题字段设置为流中的下一个字节 继续将新行写入文件末尾,每次一个,直到剩余行的字节计数小于文件开头行的字节计数(可能立即满足此条件,在这种情况下,您不需要再写入任何内容) 将剩余的新行写入文件开头(从标题后的下一个字节开始) 将包含文件“结尾部分”长度的标题字段设置为刚刚写入标题后的字节数。
我承认,这不是一个非常简单的算法!然而,我仍然认为它在某种程度上相当优雅。当然,如果有任何不清楚的地方,请告诉我。希望现在它能够完全满足你的需求。
现在,如果你的行保证长度恒定(按字节计),那么你可以很容易地回到合适的位置并覆盖现有数据。然而,这似乎是一个相当不太可能的情况。如果你不介意强制要求你的行必须具有最大长度,并且额外填充每个写入此最大长度的行,那么这可能会让事情变得容易。但是,它也有其缺点,例如在某些情况下大幅增加文件大小(即大多数行远比最大长度短)。是否可以接受取决于具体情况...
最后,你可能想考虑利用现有的日志记录系统,这取决于你确切的目的。
由于文件是面向字节的,而您需要一种面向行的服务,因此您有两个选择:
在文件周围实现一个面向行的包装器
切换到某些面向行的设备。我脑海中首先想到的是:SQLite有一些不错的C++包装器可用。
使用循环缓冲区,并在每次添加时将缓冲区写入文件。
这里有一个小而简单的代码大小解决方案。它是一个简单的字符串循环缓冲区,每次添加字符串时,它会将整个字符串缓冲区写入文件(当然,您需要为单个添加操作写入所有字符串而产生显著的成本。因此,这仅适用于少量字符串)。
循环缓冲区的简单实现,并输出到文件:
// GLOBALS ( final implementation should not use globals )
#define MAX_CHARS_PER_LINE (1024)
#define MAX_ITEMS_IN_CIRCULARBUF (4) // must be power of two
char lineCircBuf[MAX_ITEMS_IN_CIRCULARBUF][MAX_CHARS_PER_LINE];
int lineCircBuf_add = 0;
int lineCircBuf_rmv = 0; // not being used right now
uint32_t lineCircBuf_mask = MAX_ITEMS_IN_CIRCULARBUF-1;
char FILENAME[] = "lineCircBuf.txt";
FILE * ofp = NULL;
int addLine(char * str) {
int i;
// Error checking
if( strlen(str) > MAX_CHARS_PER_LINE ) {
return -1; // failure
}
if( ofp != NULL) {
fclose(ofp);
}
// Copy string into circular buffer
strncpy( &(lineCircBuf[lineCircBuf_add][0]),
str,
MAX_CHARS_PER_LINE );
lineCircBuf_add = ( lineCircBuf_add + 1 ) & lineCircBuf_mask;
// Write to file
ofp = fopen(FILENAME,"w");
for( i = 0; i < MAX_ITEMS_IN_CIRCULARBUF-1; i++ ) {
fprintf( ofp, "%s\n", lineCircBuf[i] );
}
fprintf( ofp, "%s", lineCircBuf[i] ); // do not add a newline to the last line b/c we only want N lines in the file
return 0; // success
}
int removeLine(int index) {
// not implemented yet
}
void unitTest() {
int i;
// Dummy text to demonstrate adding string lines
char lines[5][MAX_CHARS_PER_LINE] = {
"Hello world.",
"Hello world AGAIN.",
"The world is interesting so far!",
"The world is not interesting anymore...",
"Goodbye world."
};
// Add lines to circular buffer
for( i = 0; i < sizeof(lines)/sizeof(lines[0]); i++ ) {
addLine(&(lines[i][0]));
}
}
int main() {
unitTest();
return 0;
}
所以在上面的例子中,我们有5行输入,但是我们的缓冲区只有4行长。因此,输出应该只有4行,并且第一行应该被最后一行"Goodbye world"覆盖。不出所料,输出的第一行确实包含了"Goodbye world":
Goodbye world.
Hello world AGAIN.
The world is interesting so far!
The world is not interesting anymore...
我曾经看到过这样的做法,即在某个地方保留文件的当前写入位置。当您需要添加一行时,您会寻找该位置,写入该行,并以原子方式更新该位置。如果溢出,则在写入该行之前将其寻找到零。我们今天为大小受限的循环日志文件执行此操作。按行限制执行此操作有点奇怪,但可能可以以类似的方式完成。我们的写入循环大致如下:
logFile.lockForWrite();
currentPosition = logFile.getWritePosition();
logFile.seek(currentPosition);
for each line in lineBuffer {
if ((currentPosition+line.length()) > logFile.getMaxSize()) {
currentPosition = 0;
logFile.seek(0);
}
logFile.write(line);
currentPosition += line.length();
}
logFile.setWritePosition(currentPosition);
logFile.unlock();
难点在于保持当前写入位置并找到一种协调读取文件(例如使用tail
实用程序)的方法,同时您的应用程序正在向其中写入。您的读取实用程序也必须跟踪写入位置,因此其读取循环变为:
lastPosition = logFile.getWritePosition();
while (!killed) {
logFile.wait();
logFile.lockForRead();
newPosition = logFile.getWritePosition();
logFile.seek(lastPosition);
newLine = logFile.readFrom(lastPosition, (newPosition-lastPosition));
lastPosition = newPosition;
logFile.unlock();
}
这不是任何特定语言 - 只是伪代码,但思路已经在那里。 当然,我留下了处理所有有趣的边缘情况给读者。
话虽如此... 我同意其他观点。 除非你有一个真正好的理由,不要这样做。 这听起来像一个好主意,但是:
grep
,tail
,perl
等)使日志处理变得困难。总的来说,最好使用一些现有的可配置日志文件管理的日志包。看一下Apache's log4cxx或Poco的 Poco::Logger
。
那会很棘手,因为文件I/O使用字节作为底层存储单元,而不是行。
我的意思是,你可以只是用fseek()返回到开头并覆盖早期的数据,但我有一种预感,这不是你想要的。
简单解决方案:
此解决方案旨在提供恒定的文件长度,而不是文件中恒定数量的行。行数将随时间而变化,取决于长度。这个解决方案使得快速查找特定行号变得更加困难,但您可以在文件顶部或底部放置一些指示数据,以使这更容易。
“聪明”的解决方案(上述解决方案的变体):
只需使用有时用于双端队列的相同技巧即可。从文件开头明确地绕到结尾,但要跟踪文件的开头/结尾位置。您可以编写一个取消包装实用程序,将此文件转换为标准文件,以便使用不支持它的程序读取它。这个解决方案非常容易实现,但我更喜欢上面的版本。
丑陋的解决方案:
添加行时,对每行添加适量的填充。
每当您想添加一行新内容时,请按照以下步骤进行:
请注意,除非您的行长度相当一致,否则这种方法的效果可能会很差。更简单的解决方案是保证所有行的长度都相同(但要提供某种方式来创建多行“行”,以防您超过了长度限制)。
如果文件需要是文本文件:
这会带来很多问题,因为每行的长度都不同。你的前两行都有80个字符,如何用100个字符的行覆盖它们?
如果新行应该替换第一行,那么这将导致文件插入,这是一种非常昂贵的操作(基本上需要读取和写入文件的剩余部分)。除了最小量的数据外,你真的不想这样做。
如果这是为了记录目的,请使用滚动日志文件 - 例如每天一个(如lassevek建议的)。我让它变得更简单:当文件大小超过限制时,旧文件被重命名为.bak(旧.bak被删除),然后重新开始。使用1MB的限制,这可以保留最后1 MB,同时永远不会占用超过2 MB。
你可以使用两个或更多文件采用类似的机制。基本上,将“翻转”移到文件而不是行。
如果文件可能是专有格式:
使用基本的数据库引擎(如建议的SQLite)或另一种结构化存储机制。
log4cxx
和RollingFileAppender
来将这些信息写入日志文件中。当日志文件达到一定大小时,RollingFileAppender
会自动进行滚动。我不认为这完全符合你的要求,但它相当简单,也许可以胜任。只需创建所需大小的文件映射(使用CreateFileMapping或mmap),将行写入缓冲区,达到最大数量时重新开始。