9

I wrote a UNIX daemon (targeting Debian, but it shouldn't matter) and I wanted to provide some way of creating a ".pid" file, (a file which contains the process identifier of the daemon).

I searched for a way of opening a file only if it doesn't exist, but couldn't find one.

Basically, I could do something like:

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

But as it stands, this code does not perform the task in a atomic fashion, and doing so would be dangerous, because another process might create the file during my test, and the file creation.

Do you guys have any idea on how to do that?

Thank you.

P.S: Bonus point for a solution which only involves std::streams.

Zoran Jankov
  • 236
  • 2
  • 5
  • 18
ereOn
  • 53,676
  • 39
  • 161
  • 238
  • 1
    possible duplicate of [How to test if a file exists before creating it](http://stackoverflow.com/questions/7863145/how-to-test-if-a-file-exists-before-creating-it) – Brian Roach Mar 26 '12 at 14:29
  • perhaps fopen and flock together can achieve what you want? – Kevin Mar 26 '12 at 14:33

5 Answers5

9

man 2 open:

O_EXCL Ensure that this call creates the file: if this flag is specified in conjunction with O_CREAT, and pathname already exists, then open() will fail. The behavior of O_EXCL is undefined if O_CREAT is not specified.

so, you could call fd = open(name, O_CREAT | O_EXCL, 0644); /* Open() is atomic. (for a reason) */

UPDATE: and you should of course OR one of the O_RDONLY, O_WRONLY, or O_RDWR flags into the flags argument.

wildplasser
  • 43,142
  • 8
  • 66
  • 109
5

I learned about proper daemonizing here (back in the day):

It is a good read. I have since improved the locking code to eliminate race conditions on platforms that allow advisory file locking with specific regions specified.

Here is a relevant snippet from a project that I was involved in:

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);
        }
    }
}

See also http://gitweb.zfs-fuse.net/?p=sehe;a=blob;f=src/zfs-fuse/util.c;h=7c9816cc895db4f65b94592eebf96d05cd2c369a;hb=refs/heads/maint

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Very good read and interesting article. I wish I could accept this answer as well. Upvoting for fairness. – ereOn Mar 27 '12 at 07:03
1

The only way I can think of is to use system level locks. See this: C++ how to check if file is in use - multi-threaded multi-process system

Community
  • 1
  • 1
Sid
  • 7,511
  • 2
  • 28
  • 41
0

One way to approach this problem is to open the file for appending. If the function succeeds and the position is at 0 then you can be fairly certain this is a new file. Could still be an empty file but that scenario may not be important.

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);
}
JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 2
    The assumption that the file will be empty is rather fallacious. It may hold most of the times but without knowing the specifities... – Matthieu M. Mar 26 '12 at 15:22
  • But there can be the situation when some threads open this file simultaneously. Each of them will check for the position. There is a chance that position will be zero for each of the threads. And then we will have data race. – andigor Mar 13 '15 at 17:30
0

It would appear that there's no way to do it strictly using streams.

You can, instead, use open (as mentioned above by wildplasser) and if that succeeds, proceed to open the same file as a stream. Of course, if all you're writing to the file is a PID, it is unclear why you wouldn't just write it using C-style write().

O_EXCL only excludes other processes that are attempting to open the same file using O_EXCL. This, of course, means that you never have a perfect guarantee, but if the file name/location is somewhere nobody else is likely to be opening (other than folks you know are using O_EXCL) you should be OK.

DRVic
  • 2,481
  • 1
  • 15
  • 22
  • Actually `O_EXCL` *does* exclude all other processes, not just those attempting to open the same file with `O_EXCL`. You are perhaps thinking of `flock` and friends? – zwol Mar 26 '12 at 15:42
  • I guess there's a sense in which you are correct. `O_EXCL` will fail if the file already exists, regardless of what other processes do or did. But *after `open` creates the file*, another process could come along and open it without `O_EXCL` (or even `O_CREAT`) and succeed. You could get around that by doing `open(fname, O_WRONLY|O_CREAT|O_EXCL, 0000)` -- yes, zero mode -- at which point *your process* can write the file, but no other non-root process can open it until you (or someone else) `chmod`s it. – zwol Mar 26 '12 at 20:05
  • That was basically the scenario I was thinking about - someone else opening and writing the file. Kind of unlikely, and as you say - setting the perms to 0 takes care of that. – DRVic Mar 26 '12 at 23:27
  • No need to open it a second time - use `fdopen` with the file descriptor you got from `open` to get a `FILE*`. – Vladimir Panteleev Oct 08 '18 at 21:40