“有两种编写无错误程序的方法;只有第三种有效。” -艾伦·J·佩利斯
在关键计算机应用程序的生命周期中,日志记录是非常重要的活动,尤其是在故障症状不明显时。 日志记录提供了故障之前应用程序状态的最大详细信息,例如变量的值,函数的返回值等。 在一段时间内会生成单调增加的跟踪数据,并将其连续写入磁盘上的文本文件中。 有效的日志记录需要大量磁盘空间,并且在多线程环境中(其中有多个线程写入其跟踪信息)会增加很多倍。
常规文件记录的两个主要问题是:硬盘上的空间可用性以及在将数据写入文件时磁盘I / O缓慢。 连续写入磁盘会大大降低程序的性能,导致其运行缓慢。 通常,空间问题是通过使用日志轮换策略解决的,该日志策略将日志保存在多个文件中,这些文件在达到一定数量的预定义字节时会被截断并覆盖。
为了克服空间问题并最大程度地减少磁盘I / O,某些程序将其跟踪数据记录在内存中,仅在需要时将其转储。 这种循环的内存中缓冲区称为环形缓冲区 。 本文讨论了环形缓冲区的常见实现,并提出了一些在多线程程序中启用环形缓冲区机制的想法。
环形缓冲区
环形缓冲区是一种用于应用程序的日志记录技术,通过该技术,将要记录的相关数据保留在内存中,而不是每次将其写入磁盘上的文件中。 可以在需要时将内存中的此数据转储到磁盘,例如,当用户请求将文件中的内存数据转储,程序检测到错误或由于非法操作或收到信号而导致程序崩溃时。 环形缓冲区日志记录由固定大小的已分配内存缓冲区组成,供进程进行日志记录。 顾名思义,缓冲区以循环方式实现。 当缓冲区填充数据时,它不会在开始时再次写入,而不是为新数据分配更多的内存,从而覆盖了先前的内容。 有关示例,请参见图1 。
图1.写入环形缓冲区
图1显示了将两个日志条目写入环形缓冲区时的状态。 写入第一个日志条目(以蓝色显示)后,当进程尝试写入第二个日志条目(以红色显示)时,缓冲区中没有足够的空间。 该进程将写入所有可能的数据,直到缓冲区结束为止,其余的数据将被复制以开始覆盖先前的日志条目。
从环形缓冲区读取是通过保留读取指针来完成的。 该指针和写指针会相应移动,以确保读指针在读取时永远不会越过写指针。 为了提高效率,某些应用程序保留原始数据,而不是将格式化的数据放入缓冲区。 在这种情况下,需要一个解析器,该解析器从那些内存转储中生成有意义的日志消息。
环形缓冲区的优点
当您可以简单地写入文件时,为什么还要使用环形缓冲区? 由于您要覆盖环形缓冲区中的先前内容,因此会丢失数据。 与传统的文件日志记录机制相比,环形缓冲区具有以下优点。
- 很快 写入内存比对磁盘进行I / O快得多。 仅在需要时才执行刷新数据。
- 连续日志记录可能会占用系统上的空间,从而导致其他程序也用完空间并失败。 在这种情况下,要么必须手动删除日志,要么必须实施日志轮换策略。
- 启用日志记录后,无论是否需要,该过程都会继续填满硬盘上的空间。
- 有时,您只需要在程序崩溃之前获取数据,而不是进程的完整历史记录即可。
- 在多线程应用程序的情况下,用于调试的常用功能(如printf,write等)有时会更改程序的行为,从而使其难以调试。 使用这些功能可能导致应用程序无法发现某些否则会引起注意的错误。 这些函数是取消点,可能会导致在程序不期望的情况下在线程环境中传递挂起的信号。 下面的清单1和清单2中的假设示例(伪代码)使其更加清晰。
清单1.未启用调试的代码
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
/* I should not be cancelled in the below section */
var=5;
#ifdef DEBUG
write(fd,"Value of var = 5\n",17);
#endif
var=pow(var,2);
/* I can be cancelled now */
pthread_testcancel();
清单2.启用调试的代码
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
/* I should not be cancelled in the below section */
var=5;
#ifdef DEBUG
write(fd,"Value of var = 5\n",17); <======== Cancel delivered here!
#endif
var=pow(var,2);
/* I can be cancelled now */
pthread_testcancel();
在多线程程序中使用环形缓冲区
有时,当其他传统的日志记录方法失败时,环形缓冲区日志记录可以解决。 本节讨论使用环形缓冲区启用在多线程应用程序中的日志记录时要考虑的一些重要点。
同步一直是访问公共资源时多线程程序必不可少的部分,并且日志记录也不例外。 当每个线程尝试写入全局空间时,必须注意它们同步写入内存,否则消息将被破坏。 通常,每个线程在写入缓冲区之前会先获取一个锁,该缓冲区在完成时释放。 您可以下载使用锁写入内存的环形缓冲区的示例。
这种方法有一个缺点:如果您的应用程序有多个线程,并且每个线程都在详细级别上进行日志记录,则该过程的整体性能会受到影响,因为线程大部分时间都花在了获取和释放锁上。
通过使每个线程写入其自己的内存块,可以完全避免同步问题。 当用户发出转储数据请求时,每个线程都会获取一个锁并将其转储到中央位置。 由于仅在将数据刷新到磁盘时才获得锁定,因此性能不会受到很大影响。 在这种情况下,您可能需要一个附加程序来按时间顺序对线程ID或时间戳上的日志信息进行排序,以对其进行分析。 您也可以选择只写消息代码,而不是将完整格式的消息写到内存中,然后通过使用外部实用程序解析转储,将其转换为有意义的文本。
避免同步问题的另一种方法是分配大块全局内存并将其分成较小的插槽,每个线程将在其中使用一个插槽进行日志记录。 每个线程只能读写自己的插槽,而不能读写整个缓冲区。 当每个线程第一次尝试写入数据时,它都会尝试查找内存的空插槽并将其标记为忙。 当线程获取特定的插槽时,可以使用设置为1
的位图来跟踪插槽的使用情况,并在线程退出时将其重新设置为0
。 维护当前使用的插槽号的全局列表,以及使用该插槽号的线程的线程信息。
为了避免线程死亡而没有将插槽的位图重置为0
,您需要一个垃圾回收器线程,该线程将遍历全局列表并基于线程ID以固定间隔轮询该线程。 它释放插槽并修改全局列表。 有关示例,请参见下面的清单3 。
清单3.垃圾收集器线程的样本伪代码
void Check_and_free(List *ptr){
int slotno,ret_val;
LockList();
while(ptr){
if ( ((ret_val = pthread_kill(ptr->thread_id,0)) == ESRCH) ){
/* Thread has died */
slotno=ptr->slotno;
Free_slot(ptr->thread_id);
Mark_bitmap_free(slotno);
}
ptr=ptr->next;
}
UnlockList();
return ;
}
线程ID经常被重用,因此在某些情况下,线程可能会死而没有释放插槽,并在垃圾回收器释放它之前出现并分配了一个新的插槽。 对于新线程而言,检查全局列表并重用同一插槽(如果先前实例已使用该插槽)非常重要。 由于垃圾收集器线程和编写器线程都可以尝试同时修改全局列表,因此还必须使用某种锁定机制。
当用户发出转储环形缓冲区数据的信号时,处理该信号的线程将停止其他线程更改缓冲区的内容,并将已使用的插槽的内容转储到文件中。 清单4和清单5显示了将数据写入环形缓冲区并将其内容转储到文件的示例。 一旦接收到信号以转储数据, is_dumping
全局变量将用于阻止其他线程更改缓冲区的内容。
清单4.用于写入环形缓冲区的插槽“ i”的示例伪代码
void Write_to_buffer(char *msg){
read_atomically(&is_dumping);
if(!is_dumping)
memcpy(slot[i]->ptr,msg,strlen(msg));
return;
}
清单5.转储环形缓冲区数据的示例伪代码
void Dump_data(int fd){
change_atomically_to_true(&is_dumping);
for i in each_used_slot {
write_slot_data_to_file(fd,slot[i]);
}
change_atomically_to_false(&is_dumping);
return;
}
结论
环形缓冲区通过使用内存操作代替文件操作来提高日志记录的效率。 为缓冲区选择适当的大小可确保转储相关消息,这在对程序进行事后分析时会有所帮助。 环形缓冲区是连续记录程序的理想解决方案,调试时不需要完整的程序历史记录。 本文讨论了在多线程程序中实现环形缓冲区时的一些方法和注意事项。
翻译自: https://www.ibm.com/developerworks/aix/library/au-buffer/index.html