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); }
上面代码作用如下:
- 打开/tmp/lock文件。
- 使用flock对其加互斥锁。
- 打印“PID:locked!”表示加锁成功。
- 打开一个子进程,在子进程中使用flock对同一个文件加互斥锁。
- 子进程打印“PID:locked!”表示加锁成功。如果没加锁成功子进程会推出,不显示相关内容。
- 父进程回收子进程并退出。
编译并运行它:
$ 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"
上面命令的执行过程等同于:
- open("/tmp/lock-file", O_RDONLY|O_CREAT|O_NOCTTY, 0666) = 3
- flock(3, LOCK_EX)
- fork()
- execve("/bin/bash", ["/bin/bash", "-c", "echo hello"])
假设echo命令要运行10秒,另外一个终端执行上面的命令,则会阻塞在flock加锁上面,等到上面的echo进程运行结束,另一个终端才会从flock系统API返回,继续执行;
文章小结:
- 文件共享数据会产生竞争,如果是不同进程间通信需要加更大粒度的文件锁;
- 文件锁有两种实现,一种是flock,另一种是lockf,lockf是对fcntl的封装;
- linux系统命令flock可以实现在并发调用下,当前时刻只运行一个进程,进程运行完毕后,再开启运行另外一个进程;
浙公网安备 33010602011771号