Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Hi monks,

I'm working on some code that needs to do file locking over NFS using fcntl(). Flock won't work because of Red Hat deciding to remove flock() support from their NFS client in Red Hat Enterprise Linux 3 (see this recent thread). One thing I'm seeing different between fcntl() and flock() is the way they handle multiple locks on the same file. For instance:

Say I open a filehandle and get a F_RDLCK on it to prevent anyone else from writing to the file. I later call up a subroutine which also opens the file and puts a F_RDLCK on it, does something, then closes it's handle. With flock(), the original file handle still has a lock on the file. With fcntl(), closing the second filehandle clears both of the locks, so the file is now completely unlocked.

Is there a good way of making fcntl behave more like flock()? In other words, is there a way to do file locking over NFS with fcntl() in which when multiple handles are opened on the same file, closing one handle doesn't clear everyone else's locks?

Replies are listed 'Best First'.
Re: Fcntl() and multiple filehandles
by Thelonius (Priest) on Sep 20, 2004 at 20:06 UTC
    Unfortunately, this is the required semantics of fcntl. To quote the POSIX specification: "All locks associated with a file for a given process shall be removed when a file descriptor for that file is closed by that process or the process holding that file descriptor terminates."

    Everybody hates this, but it's necessary for backward compatibility. To quote the FreeBSD man page:

    This interface follows the completely stupid semantics of System V and IEEE Std 1003.1-1988 (``POSIX.1'') that require that all locks associated with a file for a given process are removed when any file descriptor for that file is closed by that process. This semantic means that applica- tions must be aware of any files that a subroutine library may access. For example if an application for updating the password file locks the password file database while making the update, and then calls getpwnam(3) to retrieve a record, the lock will be lost because getpwnam(3) opens, reads, and closes the password database. The database close will release all locks that the process has associated with the database, even if the library routine never requested a lock on the data- base. Another minor semantic problem with this interface is that locks are not inherited by a child process created using the fork(2) system call.

    Now, if your processes are the only ones locking this file (or files) and if they are all running on the same computer, you could use another file (that is not on NFS) for locking. I fear that your file is on NFS for a reason, though, which may mean that it is accessed from multiple computers.

      Unless you really have to have simultaneous access for multiple scripts or instances of your script, you should be able to make do by checking for lock file, and if it's there, sleeping for a second and checking again, rinse and repeat. If the lock file isn't gone after x number of iterations, and if the lock file is the same one that was there when you started iterating, you can assume that the script that locked it died or hung up, and ignore / delete the lock and create your own.

      WWWBoard comes with file lock routines - you can always look at these and use them as the basis for writing your own lock / unlock routines.

        Hmmm...but what's to lock the lock files? E.g. what happens if two processes try to create a lock file at the same time?
Re: Fcntl() and multiple filehandles
by sgifford (Prior) on Sep 20, 2004 at 21:13 UTC
    One solution is to implement your own flock in terms of fcntl, and keep track of the lock's state. For example, something like:
    our @lockstat; sub readlock { my($fh)=@_; my $fileno = fileno($fh); defined($fileno) or die "Bad filehandle"; if ($lockstat[$fileno] && $lockstat[$fileno]{count}) { $lockstat[$fileno]{count}++; } else { fcntl_lock($fh,F_RDLCK); $lockstat[$fileno]{count}=1; } } sub readunlock { my($fh)=@_; my $fileno = fileno($fh); defined($fileno) or die "Bad filehandle"; return unless $lockstat[$fileno] and $lockstat[$fileno]{count}; if (--$lockstat[$fileno]{count} == 0) { fcntl_lock($fh,F_UNLCK); } }

    You can expand this to cover write locks in a similar way (or write a general myflock that does both read, write, and unlocks). If you're using multiple threads, you'll need to use mutexes to protect the data structures from the different threads. You'll also have to think about how to deal with close.

    I seem to recall that Perl has a compile-time option to use its own flock implementation based on fcntl; you may want to look into that as well.

      Not a bad idea. I had thought of something similar. I guess I left out something in my original postb though: in this case, I'm actually working with a couple of different scripts that use the same files (one is a daemon and one is a CGI front end for interacting with it). There will probably be a couple more in the future. So in this case I need several differnt scripts to all adhere to the same locks.

      So does the whole file locking over NFS problem boil down to NFS being poorly designed (or at least being poorly designed for use by more than one client at a time) then? It seems odd to me that NFS has this big achilles heal given how long it's been around and how widely deployed it is.
        So in this case I need several differnt scripts to all adhere to the same locks.
        The behavior you complained about was that when the same process read-locked a file multiple times then unlocked it, all it was unlocked instead of decrementing a lock counter. Solving that problem doesn't require that the different scripts use the same lock discipline.
        So does the whole file locking over NFS problem boil down to NFS being poorly designed (or at least being poorly designed for use by more than one client at a time) then?
        I don't think it's fair to say it's a problem; you just have to use fcntl-locking, which is a POSIX standard. AFAIK, that's always been the case for portable programs, although flock over NFS may have worked on previous versions of RedHat. It's straightforward to implement your own lock-counting code if you want that behavior, and if you think it's useful clean it up and put it on CPAN.

        I don't see any documentation of the lock-counting behavior you describe in the flock(2) manpage. Are you sure you weren't relying on undefined behavior all along?