Search code examples
clinuxsynchronizationposix

Is using fcntl F_SETLKW on STDOUT_FILENO legal?


I have multiple processes spawned via fork from main program, which means they output to same stdout (which I want). However I need to somehow prevent the output from being interleaved. I already use fcntl locks to synchronize access to log file so I wanted to use it also for stdout.

However this blog post claims that fcntl locks are associated with an [i-node, pid] pair. Do stdout have an inode? I've decided to just try it

#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>

void do_log(const char *msg) {
    printf("%5ld: %s\n", getpid(), msg);
}

void try_lock(void) {
    do_log("Trying to lock stdout...");

    struct flock fl = {
        .l_type = F_WRLCK,
        .l_whence = SEEK_SET,
        .l_start = 0,
        .l_len = 0,
    };
    if (fcntl(STDOUT_FILENO, F_SETLKW, &fl) == -1) {
        perror("fcntl - lock");
        exit(1);
    }

    do_log("Stdout locked");
    sleep(2);
    do_log("Trying to unlock stdout...");

    fl.l_type = F_UNLCK;
    if (fcntl(STDOUT_FILENO, F_SETLKW, &fl) == -1) {
        perror("fcntl - unlock");
        exit(1);
    }

    do_log("Stdout unlocked");
}

int main(void) {
    if (fork()) {
        try_lock();
    } else {
        try_lock();
    }
    wait(NULL);
    return 0;
}

which seems to work

17156: Trying to lock stdout...
17155: Trying to lock stdout...
17155: Stdout locked
17155: Trying to unlock stdout...
17155: Stdout unlocked
17156: Stdout locked
17156: Trying to unlock stdout...
17156: Stdout unlocked

but in manpage for fcntl I've noticed If fildes refers to a typed memory object, the result of the fcntl() function is unspecified. and I have no idea what that means.

So I guess I have two questions:

  1. Is using fcntl lock on stdout like this guaranteed to work?
  2. If answer to 1. is yes, is the blog post wrong about [i-node, pid] or does stdout actually have an i-node?

Solution

  • An inode number (along with device number) is purely a unique identifier for a file. It has nothing to do, in modern usage of the term, with on-disk filesystem structures. All files (and file descriptors refer to instances of open files) have an inode number, and doing fcntl at least "makes sense" on them. However, per POSIX:

    Record locking shall be supported for regular files, and may be supported for other files.

    It's possible that there are some file types on which Linux doesn not support locks; I'm not sure.

    Note also that locking is purely advisory - it has no effect if the processes doing the writing don't attempt to take the lock before performing their access.

    It may make more sense, especially if you're using fork without exec, to do locking via shared memory. Before forking, mmap a MAP_ANON|MAP_SHARED region and setup a process-shared mutex in it. You can make this a robust mutex if any process might be able to die unexpectedly. This is guaranteed to work, and should be faster too since it's purely a userspace operation except on lock contention.