- Overview
-
Table of Contents
- Special Member Functions: Constructors, Destructors, and the Assignment Operator
- Operator Overloading
- Memory Management
- Templates
- Namespaces
- Time and Date Library
- Streams
- Object-Oriented Programming and Design Principles
- The Standard Template Library (STL) and Generic Programming
- Exception Handling
- Runtime Type Information (RTTI)
- Signal Processing
- Creating Persistent Objects
- Bit Fields
- New Cast Operators
- Environment Variables
- Variadic Functions
- Pointers to Functions
- Function Objects
- Pointers to Members
- Lock Files
- Design Patterns
- Dynamic Linking
- Tips and Techniques
- Five Things You Need to Know About C++11 Unions
- A Tour of C99
- A Tour of C1X
- C++0X: The New Face of Standard C++
- C++0x Concurrency
- The Reflecting Circle
- We Have Mail
- The Soapbox
- Numeric Types and Arithmetic
- Careers
- Locales and Internationalization
Lock Files
Last updated Jan 1, 2003.
A locking mechanism ensures that shared resources, such as configuration files and the system registry, aren't accessed by multiple processes simultaneously. Most operating systems provide platform-specific libraries for resource locking. In this article, I will show you a simple and portable technique for implementing locks, without resorting to intricate platform-dependent services.
Designing a Locking Policy
Let me use an example to demonstrate the usefulness of the locking technique we're about to use.
Consider a terminal server, into which multiple users can login simultaneously. The server maintains a data file, called password.dat, in which users' passwords are kept. The system administrator occasionally accesses this file to add new users, remove expired accounts, modify privileges, and so on. The users may also access this file to change their passwords.
To ensure that only a single user at a time modifies the data file, we need to create a lock file. The lock file is essentially a disk file that has the same name as the data file and a .lck extension. When the lock file password.lck exists, its associated data file is considered locked and other processes are not allowed to edit it. If the lock file doesn't exist, a process that wishes to modify the data file first creates it. Only then can it access the data file. Once that process has finished updating the data file, it deletes the lock file, thereby signaling that the lock has been released.
In other words, locking a resource consists of creating a file with an .lck extension in the same directory as the data file.
(d)Implementation
The lock file creation must be atomic. The pre-standard <fstream.h> library used to have an ios::noshare flag that guaranteed atomic file creation. Sadly, it was removed from the <fstream> library, which superseded <fstream.h>. As a result, we are forced to use the traditional Unix file I/O interface declared in <fcntl.h> (under Unix and Linux) or <io.h> (Windows) to ensure an atomic operation.
Before a process can write to the data file, it should obtain a lock like this:
#include <fcntl.h> // for open()
#include <cerrno> // for errno
#include <cstdio> // for perror()
int fd;
fd=open("password.lck", O_WRONLY | O_CREAT | O_EXCL)
If the open() call succeeds, it returns a descriptor, which is a small positive integer that identifies the file. Otherwise, it returns -1 and assigns a matching error code to the global variable errno. The O_CREAT flag indicates that if the file doesn't exist, open() should create it. The O_EXCL flag ensures that the call is atomic; if the file already exists, open() will fail and set errno to EEXIST. This way you guarantee that only a single process at a time can hold the lock.
You check the return code of open() as follows:
int getlock() // returns the lock's descriptor on success
{
if (fd<0 && errno==EEXIST)
{
// the file already exist; another process is
// holding the lock
cout<<"the file is currently locked; try again later";
return -1;
}
else if (fd < 0)
{
// perror() appends a verbal description of the current
// errno value after the user-supplied string
perror("locking failed for the following reason");
return -1;
}
// if we got here, we own the lock
return fd;
}
Once a process owns the lock, it can write to the data file safely. When it has finished updating the file, it should delete the lock as follows:
remove("password.lck");
At this moment, the data file is considered unlocked and another process may access it.
Lock File Applications
Thus far, I've focused on one kind of resources, namely data files. However, locking techniques can be applied to other system resources too. For example, a fax application and a Web browser can synchronize their access to the system's modem by using such a lock file.
Remember, though, that this locking protocol is based on voluntary cooperation of all the process involved. If a process fails to check whether a resource is locked before trying to access it, multiple processes might still face a race condition, i.e., an access clash. Another problem could occur if a process that holds the lock terminates before releasing it (e.g., due to an unhandled <exception> or a kill <signal> sent from another process). In this case, the data file will remain locked. To avoid this, you can set an expiry date after which the lock file can be deleted safely.
Summary
The main advantages of the simple locking protocol presented here are simplicity and platform independence. More sophisticated locking mechanisms such as mutexes and spinlocks are safer and offer more functionality fine-grained access policies. For example, they allow you to grant access to a resource to four processes or less. The decision which locking mechanism to use should be based on the level of security needed and the availability of specialized synchronization libraries.
Note that the rudimentary file lock mechanism demonstrated here can be further enhanced. For example, you may write the lock owner's pid to the lock file thereby allowing other processes and user to know which process is currently holding the lock.


