[Linux Advanced I/O (6)] First understanding of file locks - flock() method (with code examples)

        Imagine what happens when two people edit the same file on disk at the same time? On Linux systems, the final state of the file usually depends on the last process that wrote the file. Multiple processes operate the same file at the same time, which can easily lead to confusion of the data in the file, because when multiple processes perform I/O operations on the file, it is easy to generate a race condition, causing the content in the file to be inconsistent with the expected!

        For some applications, a process sometimes needs to ensure that only itself can perform I/O operations on a certain file, and other processes are not allowed to perform I/O operations on the file during this time. In order to provide this function to the process, the Linux system provides a file lock mechanism.  

        I have learned about mutexes, spin locks, and read-write locks before. Like these locks, file locks are lock mechanisms provided by the kernel. The lock mechanism is implemented to protect access to shared resources; The application scenarios of spin locks, read-write locks and file locks are different. Mutex locks, spin locks, and read-write locks are mainly used in multi- threaded environments to protect access to shared resources and achieve thread synchronization.

        The file lock, as the name implies, is a lock mechanism applied to files. When multiple processes operate on the same file at the same time, how do we ensure the correctness of the file data? Linux usually adopts a method of locking the file to avoid multiple processes A race condition occurs when operating on the same file concurrently. For example, when a process performs I/O operations on a file, it first locks the file, locks it, and then performs read and write operations; as long as the process does not unlock the file, other processes will not be able to operate on it; thus It can be guaranteed that only it (the process) can read and write to the file while it is locked.

        Since a file can be operated by multiple processes at the same time, it means that the file must be a shared resource, so we can see that, in the final analysis, file lock is also a mechanism for protecting access to shared resources. By locking the file , to avoid race conditions when accessing shared resources.

        Classification of file locks

        File locks can be divided into two types: advisory locks and mandatory locks:

        ⚫ Advisory locks

        Advisory lock is essentially a protocol. Before the program accesses the file, it locks the file first, and then accesses the file after the lock is successful. This is a usage of advisory lock; but if your program doesn’t care , it is also possible to directly access the file without locking the file; if this is the case, then the advisory lock does not play any role. If the advisory lock is to work, then everyone must abide by the agreement. Lock the file before accessing it. This is like a traffic light, which stipulates that red lights cannot pass, only green lights can pass, but if you insist on passing at red lights, no one can stop you, then the consequences will lead to traffic accidents; so everyone must work together Obey the traffic rules, traffic lights can play a role.

        ⚫ Mandatory lock:

        Mandatory lock is easier to understand. It is a mandatory requirement. If a process has a mandatory lock on a file, other processes cannot access the file without obtaining the file lock. The essential reason is that the mandatory lock will let the kernel check every I/O operation (such as read(), write()) to verify whether the calling process is the owner of the file lock, and if not, the file will not be accessible. When a file is locked for writing, the kernel prevents other processes from reading and writing it. Taking mandatory locks has a great impact on performance, and file locks must be checked every time a read or write operation is performed.

        In the Linux system, you can call the three functions flock(), fcntl() and lockf() to lock the file. Next, we will introduce the usage of each function.

The flock() function locks

       Let’s first learn about the system call flock(), which can be used to lock or unlock files, but the flock() function can only generate advisory locks, and its function prototype is as follows:

#include <sys/file.h>

int flock(int fd, int operation);

        To use this function, the header file <sys/file.h> needs to be included.

        Function parameters and return values ​​have the following meanings:

        fd: The parameter fd is a file descriptor, specifying the file to be locked.

        operation: The parameter operation specifies the operation mode and can be set to one of the following values:

  •  LOCK_SH: Place a shared lock on the file referenced by fd. The so-called sharing means that multiple processes can own a shared lock on the same file, and the shared lock can be owned by multiple processes at the same time.
  • LOCK_EX: Place an exclusive lock (or mutex) on the file referenced by fd. The so-called mutual exclusion means that the mutex can only be owned by one process at the same time.
  • LOCK_UN: Unlock the file lock status, unlock and release the lock. In addition to the above three flags, there is one more flag:
  • LOCK_NB: Indicates that the lock is acquired in a non-blocking manner. By default, when the file lock cannot be obtained by calling flock(), it will block until other processes release the lock. If you do not want the program to be blocked, you can specify the LOCK_NB flag. If the lock cannot be obtained, it should return immediately (error return, and errno is set to EWOULDBLOCK), usually used with LOCK_SH or LOCK_EX, combined by the bitwise OR operator.

        Return value: success will return 0; failure will return -1, and errno will be set. For flock(), it should be noted that the same file does not have shared locks and mutex locks at the same time.

        Example of use

        The following code demonstrates the use of the flock() function to lock and unlock a file (advisory lock). The program first calls the open() function to open the file, and the file path is passed in by passing parameters; after the file is opened successfully, it calls the flock() function to lock the file (non-blocking mode, exclusive lock), and prints out "file If the locking fails, the message "Failed to lock the file" will be printed out. Then call the signal function to register a signal processing function for the SIGINT signal. When the process receives the SIGINT signal, it will execute the sigint_handler() function, unlock the file in the signal processing function, and then terminate the process.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h>

static int fd = -1; //文件描述符

/* 信号处理函数 */
static void sigint_handler(int sig){
    if (SIGINT != sig)
    return;
    /* 解锁 */
    flock(fd, LOCK_UN);
    close(fd);
    printf("进程 1: 文件已解锁!\n");
}    

int main(int argc, char *argv[])    {
    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    /* 打开文件 */
    fd = open(argv[1], O_WRONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }
    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
        perror("进程 1: 文件加锁失败");
        exit(-1);
    }
    printf("进程 1: 文件加锁成功!\n");

    /* 为 SIGINT 信号注册处理函数 */
    signal(SIGINT, sigint_handler);
    for ( ; ; )
        sleep(1);
}

        After the lock is successfully added, the program enters the for infinite loop and holds the lock all the time; at this time we can execute another program, as shown below, the program will also open the file first, and the file path is passed in by passing parameters, also in The program will also call the flock() function to lock the file (exclusive lock, non-blocking mode). Regardless of whether the lock is successful or not, the following I/O operations will be performed, and the data will be written to the file, read and printed. .

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[100] = "Hello World!";
    int fd;
    int len;
    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    
    /* 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
        perror("进程 2: 文件加锁失败");
    else
        printf("进程 2: 文件加锁成功!\n");

    /* 写文件 */
    len = strlen(buf);
    if (0 > write(fd, buf, len)) {
        perror("write error");
        exit(-1);
    }
    printf("进程 2: 写入到文件的字符串<%s>\n", buf);

    /* 将文件读写位置移动到文件头 */
    if (0 > lseek(fd, 0x0, SEEK_SET)) {
        perror("lseek error");
        exit(-1);
    }
    /* 读文件 */
    memset(buf, 0x0, sizeof(buf)); //清理 buf
    if (0 > read(fd, buf, len)) {
        perror("read error");
        exit(-1);
    }
    printf("进程 2: 从文件读取的字符串<%s>\n", buf);

    /* 解锁、退出 */
    flock(fd, LOCK_UN);
    close(fd);
    exit(0);
}

        Take the above codes as application 1 and application 2 respectively, and compile them into different executable files testApp1 and testApp2, as follows:

         Before testing, create a test file infile, just use the touch command to create it, first execute the testApp1 application, use the infile file as an input file, and place it in the background to run:

         testApp1 will run in the background, and its pid can be viewed as 20710 by the ps command. Then execute the testApp2 application and pass in the same file infile, as shown below:

         From the printed information, it can be seen that the testApp2 process fails to lock the infile file, because the lock is already held by the testApp1 process, so the testApp2 lock will naturally fail; but it can be found that although the lock fails, the read and write operations of testApp2 to the file are If there is no problem, it is successful. This is the feature of the advisory lock (non-mandatory) ; the correct way to use it is not to perform I/O operations on the file after the lock fails, and follow this protocol.

        Then we send a SIGIO signal to the testApp1 process to let it unlock the file infile, and then execute testApp2 again, as shown below:

         Use the kill command to send a signal numbered 2 to the testApp1 process, which is the SIGIO signal. After receiving the signal, testApp1 unlocks the infile file and then exits; then executes the testApp2 program again. From the printed information, it can be seen that the infile file can be successfully processed this time. The file is locked, and there is no problem with reading and writing.

        A few rules about flock()

  • Locking a file multiple times by the same process will not cause deadlock. When the process calls flock() to lock the file successfully, call flock() again to lock the file (the same file descriptor), so that no deadlock will be caused, and the newly added lock will replace the old lock. For example, call flock() to add a shared lock to the file, call flock() again to add an exclusive lock to the file, and finally the file lock will be replaced by a shared lock with an exclusive lock.
  • When the file is closed, it will be automatically unlocked. The process calls flock() to lock the file. If the file is closed before it is unlocked, the file lock will be automatically unlocked, that is, the file lock will be automatically released after the corresponding file descriptor is closed. Similarly, when a process terminates, all locks it has established will be released.
  • A process cannot unlock a file lock held by another process.
  • Child processes created by fork() do not inherit locks created by the parent process. This means that if a process locks a file successfully, and then the process calls fork() to create a child process, then the child process is regarded as another process for the lock created by the parent process, although the child process from the parent process inherits its file descriptor, but not the file lock. This constraint makes sense, because the function of the lock is to prevent multiple processes from writing the same file at the same time. If the child process inherits the lock of the parent process through fork(), the parent process and the child process can write the same file at the same time.

        In addition, when a file descriptor is copied (such as using dup(), dup2() or fcntl() F_DUPFD operation), these copied file descriptors and source file descriptors will refer to the same file lock , unlocking with any of these file descriptors works as follows:

flock(fd, LOCK_EX); //加锁
new_fd = dup(fd);
flock(new_fd, LOCK_UN); //解锁

        This code first sets an exclusive lock on fd, then uses dup() to copy fd to get a new file descriptor new_fd, and finally unlocks it through new_fd, which can unlock successfully. However, if you do not explicitly call an unlock operation, the lock will not be released until all file descriptors are closed. For example, in the above example, if flock(new_fd, LOCK_UN) is not called to unlock, the lock will be released automatically only when both fd and new_fd are closed.

        The content of flock() will stop here for the time being, and we will learn to use fcntl() to lock files later.

Guess you like

Origin blog.csdn.net/cj_lsk/article/details/130873466