多進程並發訪問同一文件時,fcntl() 文件鎖是核心協調手段。但鎖分建議性與強制性兩種語意,差異巨大:前者靠程序員自覺,後者由內核強制攔截。本文深度對比兩者機制、fcntl/lockf 適用範圍、建議性鎖「關文件即釋放」陷阱、強制性鎖啟用條件(掛載選項 + 文件權限位)。
| 維度 | 建議性鎖 (Advisory) | 強制性鎖 (Mandatory) |
|---|---|---|
| 執行主體 | 應用程式自覺檢查 | 內核 VFS 層強制攔截 |
| 生效條件 | 進程主動呼叫 fcntl(F_SETLK) 並檢查返回值 | 文件系統掛載 mand + 文件 setgid + !group_exec |
| 攔截範圍 | 僅攔截同樣用 fcntl 協作的進程 | 所有 read/write/open 系統調用 |
| 預設行為 | Linux 預設啟用 | 預設關閉,需顯式啟用 |
| 釋放時機 | 文件描述符關閉即自動釋放(即使進程未解鎖) | 進程顯式解鎖或進程終結 |
| 典型用途 | 數據庫、協作式應用、大多數場景 | 強制隔離、非協作環境、安全合規 |
int fd = open("data.txt", O_RDWR);
fcntl(fd, F_SETLKW, &write_lock); // 加寫鎖
// ... 業務邏輯 ...
close(fd); // 【陷阱】鎖自動釋放!其他進程可立即搶入
// 甚至:
fcntl(fd, F_SETLKW, &write_lock);
close(fd); // 鎖已釋放
open("data.txt", O_RDWR); // 重新打開,新 fd 無鎖保護
根因:建議性鎖綁定於 struct file(打開文件實例),而非 inode。close() 釋放 file 時內核自動清理該 file 關聯的所有建議性鎖。
對策:
1. 加鎖後持有 fd 直到業務完全結束。
2. 需跨函數/模組共享鎖時,傳遞 fd 或封裝 RAII 守衛。
3. 或改用強制性鎖(綁定 inode)。
struct flock {
short l_type; // F_RDLCK, F_WRLCK, F_UNLCK
short l_whence; // SEEK_SET, SEEK_CUR, SEEK_END
off_t l_start; // 起始偏移
off_t l_len; // 長度(0=到文件結尾)
pid_t l_pid; // 僅 F_GETLK 返回時填寫持有者 PID
};
fcntl(fd, F_SETLK, &flk); // 非阻塞加鎖,失敗返回 -1/EAGAIN
fcntl(fd, F_SETLKW, &flk); // 阻塞加鎖
fcntl(fd, F_GETLK, &flk); // 檢測衝突鎖
lockf() 封裝 fcntl,僅支援建議性鎖、整文件/從當前位置到尾部,語法更簡潔。
mount -o remount,mand /dev/sda1 /data
# 或 /etc/fstab
/dev/sda1 /data ext4 defaults,mand 0 0
注意:APUE 書中寫成 _omand 是誤植,正確是 mand。
chmod g+s, g-x filename
# 即:set-group-ID 位 = 1,group execute 位 = 0
# 八進制:chmod 2644 filename (假設原 0644)
# ls -l 顯示:-rw-r-Sr-- (大寫 S 表示 setgid 但無組執行權)
原理:內核 VFS 在 __mnt_want_write() 路徑檢測到 (inode->i_mode & S_ISGID) && !(inode->i_mode & S_IXGRP) 且掛載點 MNT_MANDLOCK 標誌置位時,啟用強制性鎖檢查。
# 終端 1
$ cat > test_mand.c
#include
#include
int main() {
int fd = open("data.txt", O_RDWR|O_CREAT, 0644);
struct flock fl = {F_WRLCK, SEEK_SET, 0, 0, 0};
fcntl(fd, F_SETLKW, &fl); // 加寫鎖
write(fd, "locked", 6);
sleep(30); // 持有 30 秒
return 0;
}
$ gcc test_mand.c -o test_mand
$ chmod g+s,g-x data.txt
$ ./test_mand &
# 終端 2(鎖生效期間)
$ cat data.txt # 將被阻塞 30 秒,直到終端 1 釋放鎖
$ echo "hack" > data.txt # 同樣阻塞
若未啟用強制性鎖,終端 2 的 cat/echo 會無視建議性鎖直接讀寫成功。
建議性鎖靠自覺、綁定 file、close() 即釋放,適合協作環境;
強制性鎖靠內核、綁定 inode、需 mount -o mand + chmod g+s,g-x,攔截所有 I/O,適合強隔離場景。
選型原則:能用建議性鎖解決就用建議性(簡單、標準、無額外依賴);必須防範非協作進程(如第三方二進制、容器逃逸、安全審計)時才啟用強制性鎖。