微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

'rename2old new'然后'stat2new'序列可能失败?

如何解决'rename2old new'然后'stat2new'序列可能失败?

我正在使用证据不足(没有错误跟踪)的我公司的旧版软件调查客户问题。在对代码进行逆向工程时,我发现了一个我怀疑的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;
}

它将检测您是否在测试过程中删除其中一个文件。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。