Optimal lock file method
Windows has an option to open a file with exclusive access rights. Unix doesn't.
In order to ensure exclusive access to some file or device, it is common practice in Unix to use a lock file usually stored 开发者_StackOverflowin the /var/lock directory.
The C instruction open( "/var/lock/myLock.lock", O_RDWR | O_CREAT | O_EXCL, 0666 )
returns -1 if the lock file already exist, otherwise it creates it. The function is atomic and ensures there is not race condition.
When the resource is released, the lock file is deleted by the following instruction
remove( "/var/lock/myLock.lock" )
.
There are two problems with this method.
The program may terminate without removing the lock. For instance because it is killed, crashes or whatever. The lock file remains in place, and will prevent any access to the resource even though it is not used anymore.
The lock file is created with group and world write privilege but it is a common practice to configure accounts to use a permission mask that will clear the group and world write permission. Thus, if we had a reliable method to determine that the lock is orphan (not use), a user not owner of the file will not be allowed to remove it.
For the record, I use the lock file to ensure exclusive access to the device connected to the serial port (/dev/ttyUSBx in fact). Advisory method, requiring cooperation, is OK. But exclusive access should be ensured between different users.
Is there a better synchronization method than the lock file? How to determine if the process who created the lock file is still running? How to make it possible for another user to remove the lock file if not in use?
One solution I came up with was to use the file as Unix socket file. If the file exist, try to connect using the file. If it fails, we may assume the owner process of the file is dead. This requires to have a thread looping on socket accept()
in the owner process. Unfortunately, the system wouldn't be atomic anymore.
Take a look at the enlightening presentation File Locking Tricks and Traps:
This short talk presents several common pitfalls of file locking and a few useful tricks for using file locking more effectively.
Edit: To address your questions more precisely:
Is there a better synchronization method than the lock file?
As @Hasturkun already mentioned and as the presentation above told, the system call you need to use is flock(2)
. If the resource you'd like to share across many users is already file-based (in your case it is /dev/ttyUSBx
), then you can flock
the device file itself.
How to determine if the process who created the lock file is still running?
You don't have to determine this, as the flock
-ed lock will be automatically released upon closing the file descriptor associated with your file, even if the process was terminated.
How making it possible for another user to remove the lock file if not in use?
If you would lock the device file itself, then there will be no need to remove the file. Even if you would decide to lock an ordinary file in /var/lock
, with flock
you will not need to remove the file in order to release the lock.
You should probably be using flock()
, as in
fd = open(filename, O_RDWR | O_CREAT, 0666); // open or create lockfile
//check open success...
rc = flock(fd, LOCK_EX | LOCK_NB); // grab exclusive lock, fail if can't obtain.
if (rc)
{
// fail
}
The answer of Hasturkun is the one that has put me on track.
Here is the code I use
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <fcntl.h>
/*! Try to get lock. Return its file descriptor or -1 if failed.
*
* @param lockName Name of file used as lock (i.e. '/var/lock/myLock').
* @return File descriptor of lock file, or -1 if failed.
*/
int tryGetLock( char const *lockName )
{
mode_t m = umask( 0 );
int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
umask( m );
if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
{
close( fd );
fd = -1;
}
return fd;
}
/*! Release the lock obtained with tryGetLock( lockName ).
*
* @param fd File descriptor of lock returned by tryGetLock( lockName ).
* @param lockName Name of file used as lock (i.e. '/var/lock/myLock').
*/
void releaseLock( int fd, char const *lockName )
{
if( fd < 0 )
return;
remove( lockName );
close( fd );
}
Be careful with lock and release lock functions implemented like mentioned in one of the answers, i.e. like this:
int tryGetLock( char const *lockName )
{
mode_t m = umask( 0 );
int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
umask( m );
if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
{
close( fd );
fd = -1;
}
return fd;
}
and:
void releaseLock( int fd, char const *lockName )
{
if( fd < 0 )
return;
remove( lockName );
close( fd );
}
The problem is that releaseLock's remove call introduces a race situation bug. Let's consider there are three processes all trying to acquire the exclusive flock with nasty timing:
- Process #1 has opened the lock file, acquired the flock, and is about to call the unlock function but not yet done that.
- Process #2 has called open to open the file pointed be lockName, and has got a file descriptor of it, but not yet called flock. That is, the file pointed by lockName is opened two times now.
- Process #3 is not yet started.
With nasty timing, it is possible that process #1 calls first remove() and close() (the order doesn't matter), and then process #2 calls the flock using the file descriptor already opened, but which is not anymore the file named lockName but a file descriptor that is not linked to any directory entry.
Now, if the process #3 is started, the open() call of it creates the lockName file, and acquires the lock on it as that file is not locked. As an outcome, processes #2 and #3 both think they both own the lock on fileName -> a bug.
The issue in the implementation is that remove() (or more unlink()) only unlinks the name from the directory entry - the file descriptor referring to that file is still usable. One can create then another file having the same name, but still the already opened fd is referring to a different place.
This can be demonstrated adding delay to the lock function:
int tryGetLock( char const *lockName)
{
mode_t m = umask( 0 );
int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
umask( m );
printf("Opened the file. Press enter to continue...");
fgetc(stdin);
printf("Continuing by acquiring the lock.\n");
if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
{
close( fd );
fd = -1;
}
return fd;
}
static const char *lockfile = "/tmp/mylock.lock";
int main(int argc, char *argv[0])
{
int lock = tryGetLock(lockfile);
if (lock == -1) {
printf("Getting lock failed\n");
return 1;
}
printf("Acquired the lock. Press enter to release the lock...");
fgetc(stdin);
printf("Releasing...");
releaseLock(lock, lockfile);
printf("Done!\n");
return 0;
}
- Try starting process #1, and hit enter once to acquire the lock.
- Then start process #2 on another terminal,
- Press another enter on the terminal where process #1 is running to release the lock. 4. Continue with process #2 by pressing enter once so that it acquires the lock.
- Then open another terminal where to run process #3. In there, press enter once to acquire the lock.
The "impossible" happens: processes #2 and #3 think they both have the exclusive lock.
This might be rare to happen in practice at least with usual applications, but nevertheless the implementation is not correct.
Also, creating a file with mode 0666 might be a security risk.
I don't have "reputation to comment", and this is also quite old question, but people are still referring to this and doing something similar, so that's why to add this note as an answer.
To expand on Hasturhun's answer. Instead of using the presence or absence of the lock file as an indicator, you need to both create the lock file if it dosen't exists and then get an exclusive lock on the file.
The advantages of this approach is that unlike many other methods of syncing programs, the OS should tidy up for you if your program exits without unlocking.
So the program structure would be something like:
1: open the lock file creating it if it doesn't exist
2: ask for an exclusive lock an agreed byte range in the lock file
3: when the lock is granted then
4: <do my processing here>
5: release my lock
6: close the lock file
end
At step: you can either block waiting for the lock to be granted or return immediately. The bytes you lock don't actually have to exist in the file. If you can get hold of a copy of Advanced Unix Programming by Marc J. Rochkind, he develops a complete C library that uses this method to provide a way of syncing programs that gets tidied up by the OS, however the programs exit.
I was using the code posted by chmike, and noticed one small imperfection. I had a problem with the race during the opening lock file. Sometimes, several threads open the lock file simultaneously.
Therefore, I removed the "remove( lockName );" line from "releaseLock()" function. I do not understand why, but somehow this action helped the situation.
I have been using the following code to test the lock files. By its output one can see when several threads start using one lock simultaneously.
void testlock(void) {
# pragma omp parallel num_threads(160)
{
int fd = -1; char ln[] = "testlock.lock";
while (fd == -1) fd = tryGetLock(ln);
cout << omp_get_thread_num() << ": got the lock!";
cout << omp_get_thread_num() << ": removing the lock";
releaseLock(fd,ln);
}
}
精彩评论