我正在看一些使用pthread的遗留Linux代码。
在一个线程中,通过fgets()读取一个文件。 FILEvariables是跨所有线程共享的全局variables。 (嘿,我没有写这个…)
在另一个线程中,每隔一段时间,FILE被closures,并以另一个文件名重新打开。
几秒钟后,线程fgets()就像继续读取从前一个文件中读取的最后一个logging一样:几乎就像有一个错误,但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");
没有locking来确保fgets()不会与fclose()/ fopen()同时被调用。
任何想法失败模式可能会导致fgets()失败,但不返回NULL?
stdio库缓冲数据,分配内存来存储缓冲的数据。 GNU C库动态地分配文件结构(一些库,特别是在Solaris上,使用指向静态分配的文件结构的指针,但是除非你设置了缓冲区,否则缓冲区仍然是动态分配的)。
如果你的线程和一个指向全局文件指针的指针的副本一起工作(因为你把文件指针作为参数传递给函数),那么可以想象,代码将继续访问初始分配的数据结构(偶数尽管它被封闭了),并从缓冲区中读取已经存在的数据。 只有当你退出函数,或者读取超出缓冲区的内容时,事情才会出错 – 或者之前分配给文件结构的空间被重新分配以用于新的用途。
FILE *global_fp; void somefunc(FILE *fp, ...) { ... while (fgets(buffer, sizeof(buffer), fp) != 0) ... } void another_function(...) { ... /* Pass global file pointer by value */ somefunc(global_fp, ...); ... }
使用GCC 4.0.1测试MacOS X 10.5.8(Leopard):
#include <stdio.h> #include <stdlib.h> FILE *global_fp; const char etc_passwd[] = "/etc/passwd"; static void error(const char *fmt, const char *str) { fprintf(stderr, fmt, str); exit(1); } static void abuse(FILE *fp, const char *filename) { char buffer1[1024]; char buffer2[1024]; if (fgets(buffer1, sizeof(buffer1), fp) == 0) error("Failed to read buffer1 from %s\n", filename); printf("buffer1: %s", buffer1); /* Dangerous!!! */ fclose(global_fp); if ((global_fp = fopen(etc_passwd, "r")) == 0) error("Failed to open file %s\n", etc_passwd); if (fgets(buffer2, sizeof(buffer2), fp) == 0) error("Failed to read buffer2 from %s\n", filename); printf("buffer2: %s", buffer2); } int main(int argc, char **argv) { if (argc != 2) error("Usage: %s file\n", argv[0]); if ((global_fp = fopen(argv[1], "r")) == 0) error("Failed to open file %s\n", argv[1]); abuse(global_fp, argv[1]); return(0); }
当运行在自己的源代码上时,输出是:
Osiris JL: ./xx xx.c buffer1: #include <stdio.h> buffer2: ## Osiris JL:
所以,经验证明,在某些系统上,我概述的情景可能会发生。
在其他答案中讨论了对代码的修复。 如果你避免了我说明的问题(例如,通过避免全局文件指针),这是最简单的。 假设这是不可能的,用适当的标志进行编译就足够了(在许多类Unix系统中,编译器标志' -D_REENTRANT
'完成这个工作),并且最终将使用基本标准的线程安全版本I / O功能。 否则,您可能需要在访问文件指针周围放置明确的线程安全管理策略; 互斥或类似的东西(并修改代码以确保线程在使用相应的文件指针之前使用互斥锁)。
文件*只是一个指向各种资源的指针。 如果fclose不会将这些资源归零,那么这些值可能足以让fgets不会立即注意到这一点。
这就是说,直到你添加一些锁,我会认为这个代码完全破碎。
嗯,你真的需要至少用一个互斥体来控制对FILE流的访问。 你没有看到一些聪明的无锁方法的实现,你正在看真正糟糕(和尘土飞扬)的代码。
使用线程本地文件流是显而易见的,最优雅的修复,只要适当地使用锁定,以确保没有两个线程同时在同一个文件的偏移量上操作。 或者,更简单地说,确保线程在等待文件锁定清除的同时阻塞(或做其他工作)。 POSIX咨询锁最适合这种情况,或者你处理动态增长的互斥树…或者每个线程初始化一个文件锁互斥,并让每个线程检查另一个锁(yuck!)(因为文件可以重新命名)。
我认为你正在盯着一些主要修复的桶。不幸的是(从你已经表明),没有选择,只能做出来。 在这种情况下,它实际上更容易调试用这种方式编写的线程程序,而不是使用fork来调试某些东西,认为自己是幸运的:)
你也可以把一些条件等待(pthread_cond_wait)
而不是一些nanosleep,这将得到信号时,例如当一个新的文件被打开。