我正在看一些使用pthreads的遗留Linux代码 .
在一个线程中,通过fgets()读取文件 . FILE变量是在所有线程之间共享的全局变量 . (嘿,我没写这个...)
在另一个线程中,FILE一次又一次地关闭并用另一个文件名重新打开 .
发生这种情况几秒钟后,线程fgets()就像继续读取它从上一个文件中读取的最后一条记录一样:几乎就像有错误但fgets()没有返回NULL . 然后它自行排序并开始从新文件中读取 .
代码看起来有点像这样(为了简洁起见,我希望它仍然可以理解):
在一个线程中:
while(gRunState != S_EXIT){
nanosleep(&timer_delay,0);
flag = fgets(buff, sizeof(buff), gFile);
if (flag != NULL){
// do something with buff...
}
}
在另一个线程中:
fclose(gFile);
gFile = fopen(newFileName,"r");
没有锁定以确保fgets()不会与fclose()/ fopen()同时调用 .
有关失败模式的任何想法可能导致fgets()失败但不返回NULL?
4 回答
你也可以把一些条件 - 等待
(pthread_cond_wait)
而不是只是一些纳米睡眠,这将在预期时发出信号 . 当一个新文件被打开时 .描述的代码如何出错
stdio库缓冲数据,分配内存以存储缓冲的数据 . GNU C库动态分配文件结构(某些库,特别是在Solaris上,使用指向静态分配的文件结构的指针,但缓冲区仍然是动态分配的,除非你设置缓冲否则) .
如果您的线程使用指向全局文件指针的指针的副本(因为您将文件指针作为参数传递给函数),那么可以想象代码将继续访问已经分配的数据结构(甚至虽然它被关闭释放了,但是会从已经存在的缓冲区中读取数据 . 只有当您退出函数或读取超出缓冲区内容时才会出现问题 - 或者先前分配给文件结构的空间将重新分配以供新用途使用 .
概念证明代码
使用GCC 4.0.1在MacOS X 10.5.8(Leopard)上测试:
在自己的源代码上运行时,输出为:
因此,经验证明在某些系统上,我概述的情景可能会发生 .
如何修复代码
在其他答案中很好地讨论了对代码的修复 . 如果你避免我说明的问题(例如,通过避免全局文件指针),这是最简单的 . 假设这是不可能的,用适当的标志进行编译就足够了(在许多类Unix系统上,编译器标志'
-D_REENTRANT
'完成了这项工作),你将最终使用基本标准的线程安全版本I / O功能 . 如果做不到这一点,您可能需要围绕对文件指针的访问放置明确的线程安全管理策略;一个互斥体或类似的东西(并修改代码以确保线程在使用相应的文件指针之前使用互斥锁) .FILE *只是指向各种资源的指针 . 如果fclose没有将这些资源清零,那么这些值可能足以让fgets没有立即注意到它 .
也就是说,在你添加一些锁定之前,我会认为这段代码完全被破坏了 .
嗯,你真的需要用互斥锁来控制对FILE流的访问 . 你没有看到一些聪明的无锁方法实现,你正在寻找真正糟糕(和尘土飞扬)的代码 .
使用线程本地FILE流是明显且最优雅的修复,只需适当地使用锁以确保没有两个线程同时对同一文件的相同偏移量进行操作 . 或者,更简单地说,确保线程在等待文件锁清除时阻塞(或执行其他工作) . POSIX顾问锁最适合这个,或者你处理动态增长的互斥体树...或者每个线程初始化一个文件锁互斥锁并让每个线程检查另一个锁(yuck!)(因为文件可以重新命名) ) .
我认为你正在盯着一些主要的修复工具..不幸的是(从你所指出的),除了制造它们别无选择 . 在这种情况下,它实际上更容易调试以这种方式编写的线程程序比使用fork调试一些东西,认为自己很幸运:)