We are continuing our journey through Unix IPC by looking at the concept of file locking. If you want to know how multiple processes can access the same file without causing chaos, let's dive right in!
File locking allows us to coordinate read and write access to a file. There are two main types of locking mechanisms: mandatory and advisory. Mandatory locks completely prevent read()
and write()
calls to a file, while advisory locks operate on a more cooperative principle. With advisory locks processes can still access a file while it's locked, but they have the ability to check for the presence of a lock beforehand. For our discussion, we'll be focusing on advisory locks.
There are 2 types of advisory locks:
- Read Lock - multiple processes can read a file at the same time
- Write Lock - exclusive access to a file, no other read or write locks are possible
With theory covered, let's dig into some system calls!
flock and fcntl()
Similar to signals, we will use a combination of a struct and a function to lock a file. The struct is called flock
and looks like this:
struct flock {
short int l_type;
short int l_whence;
__off64_t l_start;
__off64_t l_len;
__pid_t l_pid;
};
l_type
is the type of lock to be applied:F_RDLCK
- read lockF_WRLCK
- write lockF_UNLCK
- unlock
l_whence
is a reference point forl_start
with 3 possible values:SEEK_SET
- the beginning of the fileSEEK_CUR
- the current file positionSEEK_END
- the end of the file
l_start
is the starting offset for the lock, relative tol_whence
parameterl_len
specifies the length to be locked in bytes. 0 means lock to the end of the filel_pid
is filled by the kernel when retrieving info about lock on the file
The function fcntl()
(short for "file control") can do a multitude of things related to file descriptors. In case of advisory locking, it has the following parameters:
int fcntl(
int fd, // A file descriptor to operate on
int cmd, // A command or operation to perform
&fl); // A reference to the `flock` struct
To get the fd
, we need to open()
the file, and the mode in which we open it must be compatible with the type of lock we're trying to put.
F_RDLCK
requiresO_RDONLY
orO_RDWR
F_WRLCK
requiresO_WRONLY
orO_RDWR
Parameter cmd
has these values for advisory locking:
F_GETLK
to check if the lock defined in&fl
can be placed. If there is a conflicting lock, its details will be in&fl
after returning. If not, the&fl.l_type
will be set toF_UNLCK
to indicate that the lock can be placed.F_SETLK
to set (or clear) the lock. Returns error if there is a conflicting lock but does not block.F_SETLKW
to block the execution until the lock can be set.
As before, here's a demo program that shows locking and unlocking:
#include "common.h"
#define FILE_NAME "locks.c"
void locks()
{
struct flock fl = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0,
.l_pid = 0,
};
int fd;
fd = open(FILE_NAME, O_RDWR);
printf("PARENT: Trying to get a write lock to the file\n");
fcntl(fd, F_SETLKW, &fl);
printf("PARENT: Got the lock\n");
pid_t pid = fork();
if (pid == 0)
{
fl.l_type = F_RDLCK;
printf("CHILD: Checking who has the lock:\n");
fcntl(fd, F_GETLK, &fl);
printf("CHILD: Lock owner PID: %d, Parent PID: %d\n", fl.l_pid, getppid());
printf("CHILD: Blocking until I get a read lock to the file\n");
fcntl(fd, F_SETLKW, &fl);
printf("CHILD: Got the lock, can exit now\n");
fflush(stdout);
exit(0);
}
else
{
printf("PARENT: Press <RETURN> to release the write lock\n");
getchar();
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &fl);
printf("PARENT: Unlocked the file\n");
fflush(stdout);
wait(NULL);
close(fd);
}
}
This was a dense one, with a BUNCH_OF constants AND_A lot of details, but that's what it takes to lock and unlock a file. To get back to the real inter-process communication, next time we'll look at message queues.