The Lock File Technique

The lock file technique is a coarse-grained locking technique, since it implies that the entire file is locked. The technique is simple, however:

  1. Attempt to create and open the lock file.

  2. If step 1 fails, sleep for a while and repeat step 1.

  3. If step 1 succeeds, then you have successfully locked the resource.

The success of this method depends on the fact that the creation and opening of the lock file must be atomic. In other words, they must either succeed completely or fail completely.

This is easily accomplished with the UNIX open(2) call, when the options O_CREAT|O_EXCL are used together:

fd = open("file.lck",O_WRONLY|O_CREAT|O_EXCL,mode);

The O_CREAT flag tells open(2) to create the file if it does not exist. However, the flag O_EXCL tells open(2) to return an error if the file already exists when the flag O_CREAT has also been supplied. This causes the open(2) call to succeed only if the file did not already exist and it was possible to create the file.

Listing 5.1 shows how locking can be performed using a lock file.

Code Listing 5.1. lockfile.c—Using a Lock File to Promote Safe Updates
1:   /* lockfile.c */
2:
3:   #include <stdio.h>
4:   #include <unistd.h>
5:   #include <string.h>
6:   #include <fcntl.h>
7:   #include <errno.h>
8:
9:   /*
10:   * Lock by creating a lock file :
11:   */
12:  static void
13:  Lock(void) {
14:      int fd = -1;                /* Lock file descriptor */
15:
16:      do  {
17:          fd = open("file.lck",O_WRONLY|O_CREAT|O_EXCL,0666);
18:          if ( fd == -1 ) {
19:              if ( errno == EEXIST ) {
20:                  sleep(1);       /* Nap for a bit.. */
21:              }  else {
22:                  fprintf(stderr,"%s: Creating lock file.lck
",
23:                      strerror(errno));
24:                  abort();        /* Failed */
25:              }
26:          }
27:      }  while ( fd == -1 );
28: 
29:      close(fd);                  /* No longer need file open */
30:  }
31:
32:  /*
33:   * Unlock by releasing the lock file :
34:   */
35:  static void
36:  Unlock(void) {
37:
38:      unlink("file.lck");         /* Release the lock file */
39:  }
40:
41:  int
42:  main(int argc,char **argv) {
43:      FILE *f = NULL;
44:      int i;
45:      int ch;
46:      int lck = 1;
47:
48:      /*
49:       * If command line argument 1 is nolock or NOLOCK,
50:       * this program runs without using the Lock() and
51:       * Unlock() functions :
52:       */
53:      if ( argc >= 2 && !strcasecmp(argv[1],"NOLOCK") )
54:          lck = 0;                        /* No locking */
55:
56:      printf("Process ID %ld started with %s
",
57:          (long)getpid(),
58:          lck ? "locking" : "no locking");
59:
60:      /*
61:       * Now create some rows of data in file.dat :
62:       */
63:      for ( i=0; i<1000; ++i ) {
64:          if ( lck )                      /* Using locks? */
65:              Lock();                     /* Yes, get lock */
66:
67:          /*
68:           * Here we just update file.dat with new records. If
69:           * no locking is used while multiple processes do this,
70:           * some records will usually be lost. However, when
71:           * locking is used, no records are lost.
72:           *
73:           * Here we just open the file if it exists, otherwise
74:           * the file is opened for write.
75:           */
76:          f = fopen("file.dat","r+");     /* Open existing file */
77:
78:          if ( !f && errno == ENOENT )
79:              f = fopen("file.dat","w");  /* Create file */
80:
81:          if ( !f ) {
82:              fprintf(stderr,"%s: opening file.dat for r/w
",
83:                  strerror(errno));
84:              if ( lck )
85:                  Unlock();               /* Unlock */
86:              return 1;                   /* Failed */
87:          }
88:
89:          /*
90:           * Seek to the end of the file, and add a record :
91:           */
92:          fseek(f,0,SEEK_END);            /* Seek to end of file */
93:  
94:          fprintf(f,"%05ld i=%06d ",(long)getpid(),i);
95:          for ( ch=''; ch<='z'; ++ch )
96:              fputc(ch,f);      /* A bunch of data to waste time */
97:          fputc('
',f);
98:
99:          fclose(f);
100:
101:         if ( lck )                      /* Using locks? */
102:             Unlock();                   /* Yes, unlock */
103:     }
104:
105:     /*
106:      * Announce our completion :
107:      */
108:     printf("Process ID %ld completed.
",(long)getpid());
109:     return 0;
110: }
					

The program in Listing 5.1 loops 1000 times to append records to the file file.dat. The function Lock() calls on open(2) with the O_CREAT|O_EXCL flags in order to exclusively open and create the file. If the create call fails, the function invokes sleep(3) for one second and then tries again.

Notice that Lock() closes the lock file after it successfully opens and creates it. The opening of the file is required only to prove that the file was created successfully by your current process and not some other. This is how the Lock() function determines that it has "acquired" the lock.

The procedure for unlocking the lock file is as simple as releasing the lock file (line 38 in function Unlock()). The unlink(2) function is discussed in Chapter 6, "Managing Files and Their Properties."

Compiling the program in Listing 5.1 is as follows:

$ make lockfile
cc -c -D_POSIX_C_SOURCE=199309L -Wall lockfile.c
cc lockfile.o -o lockfile
$

Next, make sure that the file file.dat does not exist:

$ rm file.dat
rm: file.dat: No such file or directory
$

This removal of file.dat is especially important if you run the test multiple times. If you prefer, you can do the following instead:

$ make cleanfiles
rm -f file.dat file.lck
$

The make cleanfiles command removes both the data file and the lock file if it should exist.

Next, using the compiled executable lockfile, run a test using three processes with no locking. This is done by providing the argument NOLOCK on the command line as follows:

$ ./lockfile NOLOCK & ./lockfile NOLOCK & ./lockfile NOLOCK &
$ Process ID 83554 started with no locking
Process ID 83556 started with no locking
Process ID 83555 started with no locking
Process ID 83556 completed.
Process ID 83555 completed.
Process ID 83554 completed.

[1] 83554 Exit 0              ./lockfile NOLOCK
[2] 83555 Exit 0              ./lockfile NOLOCK
[3] 83556 Exit 0              ./lockfile NOLOCK
$

It is very important that you start these processes as shown (the & character causes each of the commands to run in the background). If there is too much time delay between starting each of these processes, you will not see the expected problem. If this should still be a problem because of the speed of your system, change the number 1000 in line 63 of Listing 5.1 to something much larger.

In the session shown above, the three processes ran without using any locking and finished successfully. Now check the file file.dat, which was updated by all three:

$ wc -l file.dat
    2999 file.dat
$

The wc(1) command shown counted only 2999 lines, when there should have been 3000 (three times 1000 for each process). Remove file.dat and repeat the test. You may occasionally find that the count will change. You might get 2998, instead. This shows that you are not getting the full count.

Now repeat the test, but this time use the locking (which is the default for this program):

$ rm file.dat
$ ./lockfile & ./lockfile & ./lockfile &
$ Process ID 83606 started with locking
Process ID 83607 started with locking
Process ID 83608 started with locking
Process ID 83606 completed.
Process ID 83608 completed.
Process ID 83607 completed.

[1] 83606 Exit 0              ./lockfile
[2] 83607 Exit 0              ./lockfile
[3] 83608 Exit 0              ./lockfile
$ wc -l file.dat
    3000 file.dat
$

In this test, you can see that the final resulting line count in file.dat is 3000 lines, which is correct. The locking file file.lck prevented lost data by ensuring that only one process at a time was updating the file file.dat.

Limitations of the Lock File

One of the things that you probably noticed about running the program lockfile from Listing 5.1 was that when locks were enabled, the test took much longer to run. The reason for this has to do with the need for the Lock() function in line 20 to call upon sleep(3) when it was unsuccessful creating the lock file. While you could omit the sleep(3) function call, this would be unwelcome on a multiuser system.

Other functions could be used to reduce the sleep(3) time to less than one second, but the real problem lies in the fact that this is a polling method.

Another limitation of the lock file method is that it is reliable only on a local file system. If your lock file is created on an NFS file system, NFS cannot guarantee that your open(2) flags O_CREAT|O_EXCL will be respected (the operation may not be atomic). The operation must be atomic to be a reliable lock indicator.

Additionally, the lock file technique can only operate at a file level. Successful locking with a lock file implies that the process has access to update the entire data file. All other processes must wait, even if they want to update different parts of the same file.

Summarized, some lock file disadvantages are

  • There is high latency time between failed attempts when used with sleep(3).

  • It is unreliable when used on NFS file systems.

  • It is a coarse-grained lock (this implies that a process has locked the entire data file).

These are reasons why you should consider other file locking methods.

Using an Advisory Lock on the Entire File

An improvement over the file locking method was the creation of a UNIX kernel service that would allow a process to lock or unlock an entire file. Additionally, it was desirable to indicate when a file was being read or written. When a file is locked for reading, other processes can safely read the file concurrently. However, while the file remains read-locked, write-lock requests are blocked to ensure the safety of the data being read. Once all read locks are released, a write lock can be established on the file.

This kernel service provides the following benefits to the programmer:

  • Higher performance, since sleep(3) is not called

  • Finer lock granularity: read and write locks

The performance of the application is greatly improved because the kernel is able to resume process execution at the earliest opportunity, once the lock can be granted. This is in contrast to application calls to the sleep(3) function.

Granularity is finer because applications can acquire read locks or write locks. Read locks (also known as shared locks) allow multiple processes to read the same data regions concurrently. Write locks (also known as exclusive locks) are exclusive to any read locks and other write locks. This capability is in contrast to one file lock, allowing only one process to access the file at once.

Locking with flock(2)

The file locking service is provided by the flock(2) function on a BSD platform. This function provides the programmer with the following file locking capabilities:

Shared locks—for reading
Exclusive locks—for writing

Shared locks allow one or more concurrent reading processes to share access to the file. However, when an exclusive lock is obtained on the file, there can be no shared locks. Only one process is permitted to obtain an exclusive lock on the file. Consequently, exclusive locks are used when updates to the file are taking place.

The function synopsis for the flock(2) function is as follows:

#include <sys/file.h>

int flock(int fd, int operation);

#define LOCK_SH   0x01    /* shared file lock */
#define LOCK_EX   0x02    /* exclusive file lock */
#define LOCK_NB   0x04    /* don't block when locking */
#define LOCK_UN   0x08    /* unlock file */

The function flock(2) requires an open file descriptor fd. This open file descriptor must be open for read access to gain shared locks with LOCK_SH. The file descriptor must have write access in order to apply exclusive locks with LOCK_EX.

A shared lock is requested by using the operation LOCK_SH in the call. Other processes can request shared locks and succeed with existing shared locks. However, once a process establishes an exclusive lock (LOCK_EX), no shared lock will succeed.

When LOCK_NB is not used, a request that cannot be granted immediately causes the process to be put to sleep. When a shared lock is attempted when an exclusive lock is established, the calling process is put to sleep until the exclusive lock is released. Similarly, if a process has a shared lock and attempts to upgrade it to an exclusive lock, the calling process will sleep until the conflicting shared locks are released.

When LOCK_NB is used, the lock request immediately fails by returning -1, if the request cannot be granted. The value EWOULDBLOCK is returned in errno. This allows a process to attempt a lock without its execution being suspended if the request cannot be granted.

Note

Some platforms will provide a compatibility function. Sun's Solaris 8 flock(3UCB) documentation states that the "compatibility version of flock() has been implemented on top of fcntl(2) locking. It does not provide complete binary compatibility."


The flock(2) function has a few advantages over the lock file technique.

  • No additional lock file is involved.

  • sleep(3) is not called for retry attempts, providing improved performance.

  • Finer-grained locking allows locks to be shared or exclusive.

  • Allows locks to be held on NFS mounted file systems.

NFS can be configured to support a lock manager (rpc.lockd(8) under FreeBSD), to allow file locking on remote file systems. This overcomes the lock file limitation on remote file systems, where open and create are not atomic operations.

Note

According to simple tests performed under FreeBSD by the author, the flock(2) function does not appear to return the EINTR error after a signal handler return. However, the FreeBSD documentation states that "processes blocked awaiting a lock may be awakened by signals." For this reason, you might want to allow for the EINTR signal in your code.


Warning

Locks created by flock(2) are managed by file—not by file descriptors. Additional file descriptors obtained by dup(2) and dup2(2) manage the same locks.

The parent process that has fork(2) calls can lose locks on a file if its child process unlocks the file when it uses the open file descriptors obtained from the parent.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.80.45