3.11 Atomic Operations
Appending to a File
Consider a single process that wants to append to the end of a file. Older versions of Unix didn't support the O_APPEND option to open, so the program was coded as
if (lseek(fd, 0L, 2) < 0) /* position to EOF */ err_sys("lseek error"); if (write(fd, buff, 100) !=
100) /* and write */ err_sys("write error");
This works fine for a single process, but problems arise if multiple processes use this technique to append to the same file. (This scenario can arise if multiple instances of the same program are appending messages to a log file, for example.)
Assume two independent processes, A and B, are appending to the same file. Each have opened the file but without the O_APPEND flag. This gives us the same picture as Figure 3.3. Each process has its own file table entry, but they share a single v-node table entry. Assume process A does the lseek and this sets the current offset for the file for process A to byte offset 1500 (the current end of file). Then the kernel switches processes and B continues running. It then does the lseek, which sets the current offset for the file for process B to byte offset 1500 also (the current end of file). Then B calls write, which increments B's current file offset for the file to 1600. Since the file's size has been extended, the kernel also updates the current file size in the v-node to 1600. Then the kernel switches processes and A resumes. When A calls write, the data is written starting at the current file offset for A, which is byte offset 1500. This overwrites the data that B wrote to the file.
The problem here is that our logical operation of "position to the end of file and write" requires two separate function calls (as we've shown it). The solution is to have the positioning to the current end of file and the write be an atomic operation with regard to other processes. Any operation that requires more than one function call cannot be atomic, as there is always the possibility that the kernel can temporarily suspend the process between the two function calls (as we assumed previously).
Unix provides an atomic way to do this operation if we set the O_APPEND flag when a file is opened. As we described in the previous section, this causes the kernel to position the file to its current end of file before each write. We no longer have to call lseek before each write.
Creating a File
We saw another example of an atomic operation when we described the O_CREAT and O_EXCL options for the open function. When both of these options are specified, the open will fail if the file already exists. We also said that the check for the existence of the file and the creation of the file was performed as an atomic operation. If we didn't have this atomic operation we might try
if ( (fd = open(pathname, O_WRONLY)) < 0) if (errno == ENOENT) { if ( (fd = creat(pathname, mode)) < 0) err_sys("creat error"); } else err_sys("open error");
The problem occurs if the file is created by another process between the open and the creat. If the file is created by another process between these two function calls, and if that other process writes something to the file, that data is erased when this creat is executed. By making the test for existence and the creation an atomic operation, this problem is avoided.
In general, the term atomic operation refers to an operation that is composed of multiple steps. If the operation is performed atomically, either all the steps are performed, or none is performed. It must not be possible for a subset of the steps to be performed. We'll return to the topic of atomic operations when we describe the link function in Section 4.15 and record locking in Section 12.3.