问题描述
我正在使用证据不足(没有错误跟踪)的我公司的旧版软件调查客户问题。在对代码进行逆向工程时,我发现了一个我怀疑的C片段,可能是根本原因。
尽管我可以解决此问题,但仍然难以解释原因。
为了说明我的怀疑,我构建了以下C程序。该程序愚蠢地在两个文件夹之间来回移动文件,如果rename(2)
或stat(2)
失败,则退出1。
此程序在Linux(RHEL 7)上运行,对于我来说,文件系统为xfs
,对于我的客户,文件系统为ext4
。
我还要补充一点,发生问题时,文件系统上没有系统崩溃,断电或空间不足的问题。另外,据我所知,种族条件(以下注释)不是问题的原因。
到目前为止,当运行该程序时,我还没有看到任何故障。这并不意味着它不可能发生,不是吗?
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
void movefiles(char *src,char *dst) {
DIR* fd = opendir(src);
struct dirent* dp;
int res = 1;
while ((dp = readdir(fd))) {
if(strcmp(dp->d_name,".") && strcmp(dp->d_name,"..")) {
char fsrc[256]; /* may overflow,out of topic though */
char fdst[256];
fsrc[0] = '\0';
fdst[0] = '\0';
strcat(fsrc,src); strcat(fsrc,"/"); strcat(fsrc,dp->d_name);
strcat(fdst,dst); strcat(fdst,"/"); strcat(fdst,dp->d_name);
if(rename(fsrc,fdst) != 0) {
perror("rename Failed");
exit(1);
}
struct stat sb;
if(stat(fdst,&sb) != 0) { /* file name race condition */
perror("stat Failed"); /* can this happen ? */
exit(1);
}
}
}
closedir(fd);
}
int main() {
/* assume random content in either directories */
char *src = "tmp/dir1";
char *dst = "tmp/dir2";
while (1) {
movefiles(src,dst);
movefiles(dst,src);
}
return 0;
}
我知道文件系统是可能会失败/行为不同的复杂子系统,因此很难实现rename
的基本承诺。
我在代码中注释了一个具体的问题:rename
是否可以报告成功,而stat
是否可以报告失败?
某些文件系统是否有可能在异步进行操作的同时立即返回rename
,从而使stat
可能/很少报告故障?
搜索时,我看到有人谈论open
-write
-close
-rename
问题和fsync
推荐,但是{{1} }-rename
在我看来与众不同,找不到任何链接来验证/使我的怀疑无效。
谢谢你的灯光。
解决方法
如果另一个进程(包括人类使用的外壳程序)同时干扰文件或其目录,则代码可能会以多种方式失败。
如果我们忽略所有常见原因(来自另一个进程的干扰或整个文件系统正在卸载),则XFS中有一个细节可能会影响这一点-假设它是32位二进制数:XFS的inode编号文件可能超过2 32 -1 = 4,294,967,295,导致fs/stat.c:cp_old_stat()或fs/stat.c:cp_new_stat()失败,并出现EOVERFLOW。
要进行验证,请使用file
检查原始二进制文件。如果是64位,则不能是索引节点号。如果它是32位,则inode号可能是罪魁祸首。要解决此问题,请将二进制文件重新编译为64位。
不过,该代码确实值得怀疑。
问题在于在修改所述目录的内容时依赖于readdir()。由于更新的文件系统是如何工作的,因此不能保证readdir()仅看到新文件,而不看到已经移动的文件。
正确的方法是先获取完整的文件列表-您可以使用例如scandir()
,glob()
或nftw()
; scandir()
或glob()
(如果要将文件作为一组处理),nftw()
(如果要在回调函数中移动每个文件)。您会看到glob()
和nftw()
都应该正确处理“目录可能在遍历期间更改”,即使底层的readdir()不能正确处理。
(还有fts
系列遍历文件系统树,但在Linux上,使用64位文件偏移量的2.23之前的glibc实现(2016年2月发布)并不安全。)
考虑此实现:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#define _ATFILE_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <ftw.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
/* Because file_mover() does not descend into subdirectories,it needs a
very small number of descriptors; four should always suffice. */
#define FILE_MOVER_FDS 4
/* Per-thread state for file_mover(). Each thread sees separate variables! */
static __thread int file_mover_destfd = -1;
static __thread int file_mover_errno = 0;
static __thread long file_mover_count = 0;
static int file_mover(const char *srcpath,const struct stat *srcinfo,int typeflag,struct FTW *details)
{
/* Initial argument (srcpath)? */
if (details->level == 0) {
/* If it specifies a directory,move all its files. */
if (typeflag == FTW_D)
return FTW_CONTINUE;
/* Otherwise fail. */
file_mover_errno = ENODEV;
return FTW_STOP;
}
/* Skip directories. */
if (typeflag == FTW_D || typeflag == FTW_DNR || typeflag == FTW_DP)
return FTW_SKIP_SUBTREE;
/* Ignore all but ordinary files. */
if (typeflag != FTW_F)
return FTW_CONTINUE;
/* Obtain file name part,and its length. */
const char *name = srcpath + details->base;
const size_t namelen = strlen(name);
/* Zero-length files should never occur; detect them anyway. */
if (namelen < 1) {
file_mover_errno = ENOENT;
return FTW_STOP;
}
/* Detect if destination descriptor is invalid. */
if (file_mover_destfd == -1) {
file_mover_errno = EBADF;
return FTW_STOP;
}
/* Source path is either absolute,or relative to current working directory. */
if (renameat(AT_FDCWD,srcpath,file_mover_destfd,name) == -1) {
file_mover_errno = errno;
return FTW_STOP;
}
/* Verify the target file exists and matches the original file. */
{
struct stat destinfo;
if (fstatat(file_mover_destfd,name,&destinfo,0) == -1) {
file_mover_errno = errno;
return FTW_STOP;
}
/* Size and mode matches? */
if (destinfo.st_size != srcinfo->st_size || destinfo.st_mode != srcinfo->st_mode) {
file_mover_errno = EIO;
return FTW_STOP;
}
}
/* Add to the running count. */
file_mover_count++;
return FTW_CONTINUE;
}
/* Move files from directory srcdir to directory destdir.
Returns -1 if an error occurs with errno set to indicate the error,and the number of files moved otherwise.
*/
static long move_files(const char *srcdir,const char *destdir)
{
/* Paranoid sanity checks. */
if (!srcdir || !destdir) {
errno = EINVAL;
return -1;
}
/* Open the destination directory as a handle. */
do {
file_mover_destfd = open(destdir,O_PATH | O_CLOEXEC);
} while (file_mover_destfd == -1 && errno == EINTR);
if (file_mover_destfd == -1) {
return -1;
}
file_mover_errno = 0;
file_mover_count = 0;
if (nftw(srcdir,file_mover,FILE_MOVER_FDS,FTW_ACTIONRETVAL) != 0) {
/* Failed. Return reason in errno. */
close(file_mover_destfd);
file_mover_destfd = -1;
errno = file_mover_errno;
return -1;
}
if (close(file_mover_destfd) == -1) {
file_mover_destfd = -1;
/* errno set by close() */
return -1;
}
file_mover_destfd = -1;
/* Success. (Note: technically,the count could overflow on 32-bit arches.) */
return file_mover_count;
}
int main(int argc,char *argv[])
{
long n;
if (argc != 3) {
const char *cmd = (argc > 0 && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr,"\n");
fprintf(stderr,"Usage: %s [ -h | --help]\n",cmd);
fprintf(stderr," %s SOURCE-DIRECTORY DESTINATION-DIRECTORY\n","\n");
if (argc == 2 && (!strcmp(argv[1],"-h") || !strcmp(argv[1],"--help")))
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
n = move_files(argv[1],argv[2]);
if (n < 0) {
fprintf(stderr,"Failed: %s (%d)\n",strerror(errno),errno);
return EXIT_FAILURE;
} else
if (!n) {
fprintf(stderr,"No files to move.\n");
return EXIT_SUCCESS;
}
if (n == 1)
printf("1 file moved.\n");
else
printf("%ld files moved.\n",n);
return EXIT_SUCCESS;
}
要无限循环运行,请使用
int main(int argc,char *argv[])
{
long n,expected = 0;
if (argc != 3) {
const char *cmd = (argc > 0 && argv[0] && argv[0][0]) ? argv[0] : "(this)";
fprintf(stderr,"--help")))
return EXIT_SUCCESS;
else
return EXIT_FAILURE;
}
while(1) {
n = move_files(argv[1],argv[2]);
if (n < 0) {
fprintf(stderr,errno);
return EXIT_FAILURE;
} else
if (!n) {
fprintf(stderr,"No files to move.\n");
return EXIT_SUCCESS;
}
if (!expected) {
expected = n;
fprintf(stderr,"Moving %ld files around.\n",n);
} else
if (n != expected) {
fprintf(stderr,"Moved %ld of %ld files!\n",n,expected);
return EXIT_FAILURE;
}
n = move_files(argv[2],argv[1]);
if (n < 0) {
fprintf(stderr,errno);
return EXIT_FAILURE;
} else
if (n != expected) {
fprintf(stderr,"Moved only %ld of %ld files!\n",expected);
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
它将检测您是否在测试过程中删除其中一个文件。