羽翼飞扬

古人学问无遗力,少壮工夫老始成。纸上得来终觉浅,绝知此事要躬行。

导航

Linux IPC - 文件和文件锁

用文件共享数据情况下,会读到脏数据,代码如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <wait.h>

#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"

int do_child(const char *path)
{
    /* 这个函数是每个子进程要做的事情
    每个子进程都会按照这个步骤进行操作:
    1. 打开FILEPATH路径的文件
    2. 读出文件中的当前数字
    3. 将字符串转成整数
    4. 整数自增加1
    5. 将证书转成字符串
    6. lseek调整文件当前的偏移量到文件头
    7. 将字符串写会文件
    当多个进程同时执行这个过程的时候,就会出现racing:竞争条件,
    多个进程可能同时从文件独到同一个数字,并且分别对同一个数字加1并写回,
    导致多次写回的结果并不是我们最终想要的累积结果。 */
    int fd;
    int ret, count;
    char buf[NUM];
    fd = open(path, O_RDWR);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }
    /*    */
    ret = read(fd, buf, NUM);
    if (ret < 0) {
        perror("read()");
        exit(1);
    }
    buf[ret] = '\0';
    count = atoi(buf);
    ++count;
    sprintf(buf, "%d", count);
    lseek(fd, 0, SEEK_SET);
    ret = write(fd, buf, strlen(buf));
    /*    */
    close(fd);
    exit(0);
}

int main()
{
    pid_t pid;
    int count;

    for (count=0;count<COUNT;count++) {
        pid = fork();
        if (pid < 0) {
            perror("fork()");
            exit(1);
        }

        if (pid == 0) {
            do_child(FILEPATH);
        }
    }

    for (count=0;count<COUNT;count++) {
        wait(NULL);
    }
exit(0); }

执行效果如下:

$ gcc racing.c -o racing
$ echo 0 > /tmp/count
$ ./racing 
$ cat /tmp/count 
71 
$ echo 0 > /tmp/count
$ ./racing 
$ cat /tmp/count 
61
$ echo 0 > /tmp/count
$ ./racing 
$ cat /tmp/count 
64

可以看到,输出结果并不是我们预期的100

 

第一种文件锁flock

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

#define PATH "/tmp/lock"

int main()
{
    int fd;
    pid_t pid;

    fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC|O_CLOEXEC, 0644);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }

    // enable close-on-exec
    //int flags = fcntl(fd, F_GETFD);
    //flags |= FD_CLOEXEC;
    //fcntl(fd, F_SETFD, flags);

    if (flock(fd, LOCK_EX) < 0) {
        perror("flock()");
        exit(1);
    }
    printf("%d: locked!\n", getpid());

    pid = fork();
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
        //close(fd);
        //fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        //if (fd < 0) {
        //    perror("open()");
        //    exit(1);
        //}
        if (flock(fd, LOCK_EX) < 0) {
            perror("flock()");
            exit(1);
        }
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);
    exit(0);
}

上面代码作用如下:

  1. 打开/tmp/lock文件。
  2. 使用flock对其加互斥锁。
  3. 打印“PID:locked!”表示加锁成功。
  4. 打开一个子进程,在子进程中使用flock对同一个文件加互斥锁。
  5. 子进程打印“PID:locked!”表示加锁成功。如果没加锁成功子进程会推出,不显示相关内容。
  6. 父进程回收子进程并退出。

编译并运行它:

$ gcc flock.c -o flock
$ ./flock 
12069: locked!
12070: locked!

父子进程都加锁成功了,这不符合我们使用锁的本意,为什么呢?

原因就是子进程也继承了父进程的锁文件描述符,flock()对一个已加锁的文件描述符,总是返回成功,注意,dup、dup2此类系统API复制锁文件描述符,也会出现这种情况;

解决方法是在子进程运行后,重新打开锁文件:

close(fd);
fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd < 0) {
    perror("open()");
   exit(1);
}

 

第二种锁lockf

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

#define PATH "/tmp/lock"

int main()
{
    int fd;
    pid_t pid;

    fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }

    if (lockf(fd, F_LOCK, 0) < 0) {
        perror("lockf()");
        exit(1);
    }
    printf("%d: locked!\n", getpid());

    pid = fork();
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
        if (lockf(fd, F_LOCK, 0) < 0) {
            perror("lockf()");
            exit(1);
        }
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);    
    exit(0);
}

编译执行结果:

$ ./a.out
12146: locked!

在子进程不用open重新打开文件的情况下,进程执行被阻塞在子进程lockf加锁的操作上了。其实底层原理是通过调用fcntl实现的,可以用strace -f ./a.out 查看系统调用

 

Linux的flock命令

$ flock -x /tmp/lock-file -c "echo hello" 

上面命令的执行过程等同于:

  1. open("/tmp/lock-file", O_RDONLY|O_CREAT|O_NOCTTY, 0666)  = 3
  2. flock(3, LOCK_EX)
  3. fork()
  4. execve("/bin/bash", ["/bin/bash", "-c", "echo hello"])

假设echo命令要运行10秒,另外一个终端执行上面的命令,则会阻塞在flock加锁上面,等到上面的echo进程运行结束,另一个终端才会从flock系统API返回,继续执行;

 

文章小结:

  1. 文件共享数据会产生竞争,如果是不同进程间通信需要加更大粒度的文件锁;
  2. 文件锁有两种实现,一种是flock,另一种是lockf,lockf是对fcntl的封装;
  3. linux系统命令flock可以实现在并发调用下,当前时刻只运行一个进程,进程运行完毕后,再开启运行另外一个进程;

posted on 2022-01-22 19:06  羽翼飞扬  阅读(97)  评论(0)    收藏  举报