Linux 文件鎖的原理、實現和應用
文件鎖簡介
在多數 unix 系統中,當多個進程 / 線程同時編輯一個文件時,該文件的最後狀態取決於最後一個寫該文件的進程。但對於有些應用程序,如數據庫,各個進程需要保證它正在單獨地寫一個文件,這時就要用到文件鎖。
文件鎖(也叫記錄鎖)的作用是,當一個進程讀寫文件的某部分時,其他進程就無法修改同一文件區域。更合適的術語可能是字節範圍鎖,應爲它鎖定的是一個文件中的一個區域(也可以是整個文件。)
文件鎖還分爲建議性鎖和強制性鎖,這裏主要介紹建議性鎖。
能夠實現文件鎖的函數有 flock、fcntl 和 lockf,主要是用前兩個。flock 和 fcntl 是系統調用,而 lockf 是庫函數,實際上是 fcntl 的封裝。flock 和 fcntl 加文件鎖,兩者是不衝突,對應內核類型分別爲 FLOCK 和 POSIX。
文件鎖的基本規則
-
文件鎖是進程級別的鎖,一個進程中的所有線程共享此進程的身份。
-
任意多個進程在一個給定的字節範圍上,每個進程都可以持有一個共享性的讀鎖,但只能有一個進程持有一個獨佔性的寫鎖。
-
如果在一個給定的字節範圍上,已經有一個或多個讀鎖,則不能在此範圍上再加寫鎖。如果在一個給定的字節範圍上已經有一個寫鎖,則不能在此範圍上再加任何讀鎖或寫鎖。
-
對於一個進程而言,如果進程對某個文件區域已經有了一個鎖,然後又試圖在相同區域再加一個鎖,在沒有衝突的前提下,則新鎖會替換舊鎖。
-
加讀鎖時,該描述符必須是讀打開,加寫鎖時,該描述符必須是寫打開。
規則如表 2-1 所示:
表 2-1 不同進程文件鎖加鎖規則
flock 介紹
函數原型
#include <sys/file.h>
int flock(int fd, int operation);
fd 是系統調用 open 返回的文件描述符
operation 的選項如下:
-
LOCK_SH :表示要創建一個共享鎖,在任意時間內,一個文件的共享鎖可以被多個進程擁有
-
LOCK_EX :表示要創建一個排他鎖,在任意時間內,一個文件的排他鎖,只能被一個進程擁有
-
LOCK_UN : 表示刪除該進程創建的鎖即解鎖
-
LOCK_NB : 非阻塞(與以上三種操作一起使用)
主要特性
-
只能加建議性鎖。
-
只能對整個文件加鎖,而不能對文件的某一區域加鎖。
-
使用 exec 後,文件鎖的狀態不變。
-
flock 鎖是可以遞歸,即通過 dup 或者 fork 產生的兩個 fd,都可以加鎖而不會產生死鎖。因爲其創建的鎖是和文件打開表項(struct file)相關聯的,而不是 fd。這就意味着複製文件 fd(通過 fork 或者 dup)後,這兩個 fd 都可以操作這把鎖(例如通過一個 fd 加鎖,通過另一個 fd 可以釋放鎖),也就是說子進程繼承父進程的鎖。但是加鎖過程中,關閉其中一個 fd,鎖是不會被釋放的(因爲 struct file 並沒有釋放),只有關閉所有複製出的 fd,鎖纔會被釋放。
-
使用 open 兩次打開同一個文件,得到的兩個 fd 是獨立的(因爲底層對應兩個 struct file 對象),通過其中一個 fd 加鎖,通過另一個 fd 無法解鎖,並且在前一個解鎖前也無法加有衝突的鎖。
-
flock 在 NFS 文件系統上使用時,服務端 NFSD 將文件鎖的類型由 FLOCK 改爲 POSIX。
-
不會進行死鎖檢查。
特性測試
open 測試
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1 = open(argv[1],O_RDWR);
int fd2 = open(argv[1],O_RDWR);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
ret = flock(fd1, LOCK_EX|LOCK_NB)
printf("get flock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = flock(fd2, LOCK_EX|LOCK_NB);
printf("get flock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系統:
nfs 導出:
dup 測試
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1 = open(argv[1],O_RDWR);
int fd2 = dup(fd1);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
ret = flock(fd1, LOCK_EX|LOCK_NB)
printf("get flock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = flock(fd2, LOCK_EX|LOCK_NB);
printf("get flock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系統:
nfs 導出:
fork 測試
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>
int main (int argc, char ** argv)
{
int ret;
int pid;
int fd = open(argv[1],O_RDWR);
if ((pid = fork()) == 0){
ret = flock(fd,LOCK_EX|LOCK_NB);
printf("child get lock, fd: %d, ret: %d",fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
sleep(10);
printf("child exit\n");
exit(0);
}
ret = flock(fd,LOCK_EX|LOCK_NB);
printf("parent get lock, fd: %d, ret: %d", fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
waitpid(pid);
printf("parent exit\n");
return 0;
}
本地文件系統:
nfs 導出:
死鎖檢查測試
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void flock_set(int fd, char *file_name, char *process_type)
{
int ret;
printf("process %s pid %d start set flock for %s by fd %d.\n",
process_type, getpid(), file_name, fd);
ret = flock(fd, LOCK_EX);
printf("process %s pid %d set flock for %s by fd end, ret %d",
process_type, getpid(), file_name, fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
}
int main (int argc, char *argv[])
{
int pid;
int fd1, fd2;
printf("====test FL_FLOCK dead lock ====\n", argv[1]);
if ((pid = fork()) == 0){
fd1 = open(argv[1], O_WRONLY|O_CREAT);
fd2 = open(argv[2], O_WRONLY|O_CREAT);
flock_set(fd2, argv[2], "child");
sleep(1);
flock_set(fd1, argv[1], "child");
sleep(2);
printf("process child exit\n");
exit(0);
}
fd1 = open(argv[1], O_WRONLY|O_CREAT);
fd2 = open(argv[2], O_WRONLY|O_CREAT);
flock_set(fd1, argv[1], "parent");
sleep(1);
flock_set(fd2, argv[2], "parent");
waitpid(pid);
printf("process parent exit\n");
return 0;
}
測試結果如下:
父子進程互相等待死鎖了,棧的信息如下
lockf 介紹
#include <unistd.h>
int lockf(int fd, int cmd, off_t len);
fd 爲通過 open 返回的打開文件描述符。
cmd 的取值如下:
-
F_LOCK :給文件加排他鎖,若文件已被加鎖,則會一直阻塞到鎖被釋放。
-
F_TLOCK :同 F_LOCK,但若文件已被加鎖,不會阻塞,並返回錯誤。
-
F_ULOCK :解鎖。
-
F_TEST :測試文件是否被加鎖,若文件沒被加鎖則返回 0,否則返回 - 1。
len 爲從文件當前位置的起始要鎖住的長度。
lockf 只支持排他鎖,不支持共享鎖。
fcntl 介紹
函數原型
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock);
fd 爲通過 open 返回的打開文件描述符。
cmd 的取值如下:
-
F_SETLK:申請鎖(讀鎖 F_RDLCK,寫鎖 F_WRLCK)或者釋放所(F_UNLCK),但是如果 kernel 無法將鎖授予本進程(被其他進程持有),立即返回 error,不會阻塞,並將衝突鎖的信息,保存存在 struct flock 中。
-
F_SETLKW:和 F_SETLK 幾乎一樣,唯一的區別是申請不到鎖,就會阻塞。
-
F_GETLK:這個操作是獲取鎖的相關信息,並會修改我們傳入的 lock。進程可以通過此操作,來獲取 fd 指向的那個文件的加鎖信息。執行該操作時,lock 中就保存了希望對文件的加鎖信息(或者是測試是否可以加鎖)。如果確實存和 lock 衝突的鎖,內核會把衝突的鎖的信息寫到 lock 中,並將該鎖擁有者的 PID 寫入 l_pid 字段中,然後返回;否則,就將 lock 中的 l_type 設置爲 F_UNLCK,並保持 lock 中其他信息不變返回,而不是對該文件真正加鎖。
需要注意的是,F_GETLK 用於測試是否可以加鎖,在 F_GETLK 測試可以加鎖之後,F_SETLK 和 F_SETLKW 就會企圖申請一個鎖,但是這兩者之間並不是一個原子操作,也就是說,在 F_SETLK 或者 F_SETLKW 還沒有成功加鎖之前,另外一個進程就有可能已經加上了一個鎖。而且 F_SETLKW 有可能導致程序長時間睡眠。還有,進程對某個文件擁有的各種類型的鎖,會在相應的文件描述符被關閉時自動清除,進程運行結束後,其所加的各種鎖也會自動清除。
flock 結構如下:
struct flock {
...
short l_type; /* Type of lock: F_RDLCK,F_WRLCK,F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
...
};
flock 結構說明:
-
鎖類型:共享讀鎖 F_RDLCK,獨佔寫鎖 F_WRLCK,解鎖 F_UNLCK。
-
加鎖或解鎖區域的起始字節偏移量,由 l_start 和 l_whence 決定。
-
l_start 是相對偏移量,l_whence 決定了 l_start 的起點,l_whence 可選用的值爲 SEEK_SET, SEEK_CUR, SEEK_END。
-
區域字節長度(l_len)
-
由 F_GETLK 獲取已存在的衝突鎖的進程 PID(l_pid)
-
鎖可以在文件尾處開始或者越過尾端開始,但是不能在文件起始位置之前開始
-
若 l_len=0, 表示鎖的範圍可以擴大到最大可能偏移量,這意味着,不論往文件中追加多少數據,它們都處於鎖的範圍內
-
設置 l_start 和 l_whence 指向文件的起始位置,並且指定 l_len=0,以實現對整個文件加鎖(一般 l_start=0, l_whence=SEEK_SET)
主要特性
-
加鎖可遞歸,如果一個進程對一個文件區間已經有一個鎖,後來又在同一區間再加一個鎖,在沒有衝突的前提下,則新鎖將替換老鎖。
-
加讀鎖(共享鎖)文件必須是讀打開,加寫鎖(排他鎖)文件必須是寫打開。
-
進程不能使用 F_GETLK 命令來測試它自己是否在文件的某一部分持有一個鎖。F_GETLK 命令定義說明,返回信息指示是否現存的鎖阻止調用進程設置它自己的鎖。因爲,F_SETLK 和 F_SETLKW 命令總是替換進程的現有鎖,所以調用進程絕不會阻塞再自己持有的鎖上,於是 F_GETLK 命令絕不會報告調用進程自己持有的鎖。
-
進程終止時,他所建立的所有文件鎖都會被釋放,同 flock。
-
任何時候關閉一個描述符時,則該進程通過這一描述符可以引用的文件上的任何一個鎖都被釋放(這些鎖都是該進程設置的),與 flock 不同。例如:
fd1 = open(pathname, …);
fcntl(fd1, F_SETLK, …);
fd2 = dup(fd1);
close(fd2);
// 在close(fd2)後,在fd1上加的鎖,會被釋放。
// 如果將dup換爲open,以打開同一文件的另一描述符,則效果也一樣。
fd1 = open(pathname, …);
fcntl(fd1, F_SETLK, …);
fd2 = open(pathname, …);
close(fd2);
-
由 fork 產生的子進程不繼承父進程所設置的鎖,與 flock 不同。
-
在執行 exec 後,新程序可以繼承原程序的鎖,這點和 flock 是相同的。(如果對 fd 設置了 close-on-exec,則 exec 前會關閉 fd,相應文件的鎖也會被釋放)。
-
支持強制性鎖:對一個特定文件打開其設置組的 ID 位 (S_ISGID),並關閉其組執行位 (S_IXGRP),則對該文件開啓了強制性鎖機制。再 Linux 中如果要使用強制性鎖,則要在文件系統 mount 時,使用_omand 打開該機制。
-
阻塞方式加鎖時,會進行死鎖檢查。死鎖鏈搜索深度爲 10 步,超過該深度的不再進行死鎖檢查。
特性測試
open 測試
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1, fd2;
struct flock lock;
fd1 = open(argv[1], O_RDWR);
fd2 = open(argv[1], O_RDWR);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = F_WRLCK;
ret = fcntl(fd1, F_SETLK, &lock);
printf("get POSIX lock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = fcntl(fd2, F_SETLK, &lock);
printf("get POSIX lock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系統:
nfs 導出:
dup 測試
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1, fd2;
struct flock lock;
fd1 = open(argv[1], O_RDWR);
fd2 = dup(fd1);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = F_WRLCK;
ret = fcntl(fd1, F_SETLK, &lock);
printf("get POSIX lock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = fcntl(fd2, F_SETLK, &lock);
printf("get POSIX lock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系統:
nfs 導出:
fork 測試
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main (int argc, char ** argv)
{
int ret;
int pid;
int fd;
struct flock lock;
fd = open(argv[1],O_RDWR);
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = F_WRLCK;
if ((pid = fork()) == 0){
ret = fcntl(fd, F_SETLK, &lock);
printf("child set lock, fd: %d, ret: %d",fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
sleep(2);
printf("child exit\n");
exit(0);
}
ret = fcntl(fd, F_SETLK, &lock);
printf("parent set lock, fd: %d, ret: %d", fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
waitpid(pid);
printf("parent exit\n");
return 0;
}
本地文件系統:
nfs 導出:
死鎖檢查測試
測試代碼如下:
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
void lock_set(int fd, struct flock *lock, char *process_type)
{
int ret;
int lock_end = lock->l_start + lock->l_len;
printf("process %s pid %d start set lock[%d %d], by fd %d.\n",
process_type, lock->l_pid, lock->l_start, lock_end, fd);
ret = fcntl(fd, F_SETLKW, lock);
printf("process %s pid %d set lock[%d %d] by fd %d end, ret %d",
process_type, lock->l_pid, lock->l_start, lock_end, fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
}
int main (int argc, char *argv[])
{
int pid;
int fd;
struct flock lock;
fd = open(argv[1],O_RDWR);
lock.l_whence = SEEK_SET;
lock.l_type = F_WRLCK;
printf("====test FL_POSIX dead lock for %s====\n", argv[1]);
if ((pid = fork()) == 0){
lock.l_pid = getpid();
lock.l_start = 20;
lock.l_len = 10;
lock_set(fd, &lock, "child");
sleep(1);
lock.l_start = 1;
lock.l_len = 10;
lock_set(fd, &lock, "child");
sleep(2);
printf("process child exit\n");
exit(0);
}
lock.l_pid = getpid();
lock.l_start = 1;
lock.l_len = 10;
lock_set(fd, &lock, "parent");
sleep(1);
lock.l_start = 20;
lock.l_len = 10;
lock_set(fd, &lock, "parent");
waitpid(pid);
printf("process parent exit\n");
return 0;
}
測試結果如下:
自己進程檢查到了死鎖,直接返回,不再阻塞,最後父子都退出了。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1nwZm1oSAbNHeyOL11C8XQ