如果文件不存在,如何创建文件?

9

我写了一个UNIX守护进程(目标是Debian,但这并不重要),我想提供一种创建“.pid”文件的方法(该文件包含守护进程的进程标识符)。

我搜索了一种只有在文件不存在时才打开文件的方法,但找不到。

基本上,我可以做以下事情:

if (fileexists())
{
  //fail...
}
else
{
  //create it with fopen() or similar
}

但是目前这段代码并没有以原子方式执行任务,这样做是危险的,因为另一个进程可能会在我的测试期间创建文件,并执行文件创建操作。

你们有什么办法解决这个问题吗?

谢谢。

附加题:如果只涉及std::streams的解决方案将获得额外加分。


1
可能是重复的问题:如何在创建文件之前测试其是否存在 - Brian Roach
也许使用 fopen 和 flock 可以实现你想要的功能? - Kevin
5个回答

9

man 2 open:

O_EXCL 确保此调用创建文件:如果在使用 O_CREAT 标志的情况下指定了此标志,并且 pathname 已经存在,则 open() 将失败。如果未指定 O_CREAT,则 O_EXCL 的行为是未定义的。

因此,您可以调用 fd = open(name, O_CREAT | O_EXCL, 0644); /* Open() 是原子性的。(有原因) */

更新:当然,您应该将 O_RDONLY、O_WRONLY 或 O_RDWR 标志之一 OR 到标志参数中。


5
我在这里学习了有关正确守护进程的知识(当时): 这是一篇很好的文章。我后来改善了锁定代码,以消除在允许使用指定区域的“咨询文件锁定” 的平台上的竞争条件。
以下是我参与的一个项目中的相关片段:
static int zfsfuse_do_locking(int in_child)
{
    /* Ignores errors since the directory might already exist */
    mkdir(LOCKDIR, 0700);

    if (!in_child)
    {
        ASSERT(lock_fd == -1);
        /*
         * before the fork, we create the file, truncating it, and locking the
         * first byte
         */
        lock_fd = creat(LOCKFILE, S_IRUSR | S_IWUSR);
        if(lock_fd == -1)
            return -1;

        /*
         * only if we /could/ lock all of the file,
         * we shall lock just the first byte; this way
         * we can let the daemon child process lock the
         * remainder of the file after forking
         */
        if (0==lockf(lock_fd, F_TEST, 0))
            return lockf(lock_fd, F_TLOCK, 1);
        else
            return -1;
    } else
    {
        ASSERT(lock_fd != -1);
        /*
         * after the fork, we instead try to lock only the region /after/ the
         * first byte; the file /must/ already exist. Only in this way can we
         * prevent races with locking before or after the daemonization
         */
        lock_fd = open(LOCKFILE, O_WRONLY);
        if(lock_fd == -1)
            return -1;

        ASSERT(-1 == lockf(lock_fd, F_TEST, 0)); /* assert that parent still has the lock on the first byte */
        if (-1 == lseek(lock_fd, 1, SEEK_SET))
        {
            perror("lseek");
            return -1;
        }

        return lockf(lock_fd, F_TLOCK, 0);
    }
}

void do_daemon(const char *pidfile)
{
    chdir("/");
    if (pidfile) {
        struct stat dummy;
        if (0 == stat(pidfile, &dummy)) {
            cmn_err(CE_WARN, "%s already exists; aborting.", pidfile);
            exit(1);
        }
    }

    /*
     * info gleaned from the web, notably
     * http://www.enderunix.org/docs/eng/daemon.php
     *
     * and
     *
     * http://sourceware.org/git/?p=glibc.git;a=blob;f=misc/daemon.c;h=7597ce9996d5fde1c4ba622e7881cf6e821a12b4;hb=HEAD
     */
    {
        int forkres, devnull;

        if(getppid()==1)
            return; /* already a daemon */

        forkres=fork();
        if (forkres<0)
        { /* fork error */
            cmn_err(CE_WARN, "Cannot fork (%s)", strerror(errno));
            exit(1);
        }
        if (forkres>0)
        {
            int i;
            /* parent */
            for (i=getdtablesize();i>=0;--i)
                if ((lock_fd!=i) && (ioctl_fd!=i))       /* except for the lockfile and the comm socket */
                    close(i);                            /* close all descriptors */

            /* allow for airtight lockfile semantics... */
            struct timeval tv;
            tv.tv_sec = 0;
            tv.tv_usec = 200000;  /* 0.2 seconds */
            select(0, NULL, NULL, NULL, &tv);

            VERIFY(0 == close(lock_fd));
            lock_fd == -1;
            exit(0);
        }

        /* child (daemon) continues */
        setsid();                         /* obtain a new process group */
        VERIFY(0 == chdir("/"));          /* change working directory */
        umask(027);                       /* set newly created file permissions */
        devnull=open("/dev/null",O_RDWR); /* handle standard I/O */
        ASSERT(-1 != devnull);
        dup2(devnull, 0); /* stdin  */
        dup2(devnull, 1); /* stdout */
        dup2(devnull, 2); /* stderr */
        if (devnull>2)
            close(devnull);

        /*
         * contrary to recommendation, do _not_ ignore SIGCHLD:
         * it will break exec-ing subprocesses, e.g. for kstat mount and
         * (presumably) nfs sharing!
         *
         * this will lead to really bad performance too
         */
        signal(SIGTSTP,SIG_IGN);     /* ignore tty signals */
        signal(SIGTTOU,SIG_IGN);
        signal(SIGTTIN,SIG_IGN);
    }

    if (0 != zfsfuse_do_locking(1))
    {
        cmn_err(CE_WARN, "Unexpected locking conflict (%s: %s)", strerror(errno), LOCKFILE);
        exit(1);
    }

    if (pidfile) {
        FILE *f = fopen(pidfile, "w");
        if (!f) {
            cmn_err(CE_WARN, "Error opening %s.", pidfile);
            exit(1);
        }
        if (fprintf(f, "%d\n", getpid()) < 0) {
            unlink(pidfile);
            exit(1);
        }
        if (fclose(f) != 0) {
            unlink(pidfile);
            exit(1);
        }
    }
}

请参考http://gitweb.zfs-fuse.net/?p=sehe;a=blob;f=src/zfs-fuse/util.c;h=7c9816cc895db4f65b94592eebf96d05cd2c369a;hb=refs/heads/maint。该链接相关于IT技术,提供了有用的信息。

非常好的阅读和有趣的文章。我希望我也能接受这个答案。为公正点赞。 - ereOn

1

0
一种解决这个问题的方法是打开文件进行追加。如果函数成功并且位置在0,则可以相当确定这是一个新文件。仍然可能是空文件,但这种情况可能不重要。
FILE* pFile = fopen(theFilePath, "a+");
if (pFile && gfetpos(pFile) == 0) { 
  // Either file didn't previously exist or it did and was empty

} else if (pFile) { 
  fclose(pFile);
}

2
文件为空的假设相当靠不住。虽然大多数情况下可能成立,但如果不知道具体情况... - Matthieu M.
但是有可能出现这样的情况,即一些线程同时打开此文件。它们中的每一个都将检查位置。有可能每个线程的位置都为零。然后我们就会遇到数据竞争问题。 - andigor

0

看起來並沒有一種嚴格使用流的方式來做到這一點。

相反,您可以使用open(如wildplasser所提到的);如果成功,再打開同一個文件作為流。當然,如果您只是將PID寫入文件中,則不清楚為什麼您不直接使用C風格的write()函數來寫入。

O_EXCL只會排除試圖使用O_EXCL打開相同文件的其他進程。這當然意味著您永遠無法完全保證,但如果文件名/位置在沒有其他人可能會打開的地方(除了那些您知道正在使用O_EXCL的人),那麼您應該是安全的。


实际上,O_EXCL确实会排除所有其他进程,而不仅仅是那些尝试使用O_EXCL打开同一文件的进程。你可能在想flock和相关函数? - zwol
我猜你说的有一定道理。无论其他进程如何,如果文件已经存在,O_EXCL将失败。但是,在open创建文件之后,另一个进程可以使用没有O_EXCL(甚至没有O_CREAT)的方式打开它并成功。您可以通过执行open(fname,O_WRONLY | O_CREAT | O_EXCL,0000)来解决这个问题 - 是的,零模式 - 在这一点上,您的进程可以写入该文件,但是除非您(或其他人)对其进行chmod,否则没有其他非root进程可以打开它。 - zwol
那基本上是我所考虑的情况 - 其他人打开并写入文件。这种情况可能不太可能发生,正如你所说 - 将权限设置为0就可以解决这个问题。 - DRVic
不需要再次打开,使用从open中获得的文件描述符调用fdopen函数来获取一个FILE* - Vladimir Panteleev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接