/proc/locks
andrew@andrew-Thurley:/dev$ cat /proc/locks
1: POSIX ADVISORY WRITE 8968 08:01:11666907 1073741825 1073741825
2: POSIX ADVISORY READ 2433 08:01:11798469 128 128
...
35: FLOCK ADVISORY WRITE 1436 00:1a:7 0 EOF
51: FLOCK ADVISORY WRITE 1036 00:16:763 0 EOF
使用ps -p PID
查看進程的相關信息
andrew@andrew-Thurley:/dev$ ps -p 8968
PID TTY TIME CMD
8968 ? 00:00:02 chrome
從上面的輸出可以看出持有的鎖的程序是chrome
,即 google
瀏覽器
在/dev
我下搜索主設備號爲8
次設備號爲1
的設備,是/dev/sda1
andrew@andrew-Thurley:/dev$ ls -li /dev/sda1 |awk '$6=8'
351 brw-rw---- 1 root disk 8 1 12月 22 13:13 /dev/sda1
查看設備/dev/sda1
的掛載點,並在該部分文件系統中搜索i-node
節點爲11666907
的文件
andrew@andrew-Thurley:/dev$ mount |grep sda1
/dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)
從上面的輸出可以看出,設備/dev/sda1
是掛在在 /
上的
andrew@andrew-Thurley:/dev$ sudo find / -mount -inum 11666907
/home/andrew/.config/google-chrome/Default/History
find -mount
選項是防止find
進入到/
下的子目錄進行搜索
最後顯示被鎖住文件的內容--google
瀏覽器的歷史記錄
sudo vim /home/andrew/.config/google-chrome/Default/History
是一個google
瀏覽器的SQLite
數據庫文件
這樣就可以看出google
瀏覽器持有文件sudo vim /home/andrew/.config/google-chrome/Default/History
上的一把鎖,而這個文件的內容就是google
用於記錄歷史記錄的數據庫文件。
通過/proc/locks
還能夠獲取被阻塞的鎖請求的相關信息,如下圖所示
其中鎖號後面隨即跟着->
的行表示被阻塞的鎖的請求
僅運行一個程序的單個實例
一些程序-特別是很多的daemon
需要確保同一時刻只有一個程序實例在系統中運行。完成這項任務的一個常見的方法是讓daemon
在一個標準的目錄中創建一個文件並在該文件上放置一把寫鎖。daemon
在執行期間一直持有這個文件鎖並在即將終止前刪除這個文件。如果啓用了daemon
的另外一個實例,那麼它在獲取該文件的寫鎖時就會失敗,其結果是它會意識到daemon
的另一個實例肯定在運行,然後終止。
很多的網絡服務器採用了另一種常規的做法,即當服務器綁定的衆所周知的
socket
端口號已經被使用時就認爲該服務器實例已經處於運行在狀態了
/var/run
目錄通常是存放此類文件的位置,或者也可以在daemon
的配置文件中加上一行,來指定文件的位置。
通常daemon
會將其進程ID
寫入該文件,因此這個文件命名時通常將.pid
作爲擴展名。這對於那些需要找出daemon
的進程ID
的應用程序來講是比較有用的。它允許執行額外的健全檢查--效果可以像20.5節那樣使用kill(pid,0)來檢查進程ID
是否存在。
文件create_pid_file.c
展示了,用來創建和鎖住一個進程ID
鎖文件的代碼實現。可以使用下面的方式調用這個函數。
if(createPidFile("mydaemon", "/car/run/mydaemon.pid", 0) == -1)
errExit("createPidFile");
createPidFile
函數的精妙之處在於使用ftruncate()
函數來清除文件中之前存在的所有的字符串。之所以這樣做是因爲daemon
的上一個實例在刪除文件時可能因系統崩潰而失敗,在這種情況下,如果新的daemon
實例的進程ID
較小,那麼可能就無法完全覆蓋之前文件中的內容,從嚴格意義上來說清除所有的既有字符串不是必需的,但這樣做顯得更加簡潔並且排除產生混淆的可能。
#include <sys/stat.h>
#include <fcntl.h>
#include "region_locking.h" /* For lockRegion() */
#include "create_pid_file.h" /* Declares createPidFile() and
defines CPF_CLOEXEC */
#include "tlpi_hdr.h"
#define BUF_SIZE 100 /* Large enough to hold maximum PID as string */
#define CPF_CLOEXEC 1
static int
lockReg(int fd, int cmd, int type, int whence, int start, off_t len)
{
struct flock fl;
fl.l_type = type;
fl.l_whence = whence;
fl.l_start = start;
fl.l_len = len;
return fcntl(fd, cmd, &fl);
}
int /* Lock a file region using nonblocking F_SETLK */
lockRegion(int fd, int type, int whence, int start, int len)
{
return lockReg(fd, F_SETLK, type, whence, start, len);
}
int
createPidFile(const char *progName, const char *pidFile, int flags)
{
int fd;
char buf[BUF_SIZE];
fd = open(pidFile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("Could not open PID file %s", pidFile);
//< flags 設置爲1的時候使用fcntl設置FD_CLOEXEC
// 對於通過exec()來從起自己的進程比較有用,如果在exec()時文件描述符沒有被關閉,那麼重新啓動的服務器就會認爲服務器的另一個示例處於運行狀態。
if (flags & CPF_CLOEXEC) {
/* Set the close-on-exec file descriptor flag */
/* Instead of the following steps, we could (on Linux) have opened the
file with O_CLOEXEC flag. However, not all systems support open()
O_CLOEXEC (which was standardized only in SUSv4), so instead we use
fcntl() to set the close-on-exec flag after opening the file */
flags = fcntl(fd, F_GETFD); /* Fetch flags */
if (flags == -1)
errExit("Could not get flags for PID file %s", pidFile);
flags |= FD_CLOEXEC; /* Turn on FD_CLOEXEC */
if (fcntl(fd, F_SETFD, flags) == -1) /* Update flags */
errExit("Could not set flags for PID file %s", pidFile);
}
//設置一把寫鎖 SEEK_SET-開頭 0 到 len=0代表到EOF
//即對整個文件放置一把寫鎖,並且寫鎖的範圍隨着文件的增加而改變
if (lockRegion(fd, F_WRLCK, SEEK_SET, 0, 0) == -1) {
if (errno == EAGAIN || errno == EACCES)
fatal("PID file '%s' is locked; probably "
"'%s' is already running", pidFile, progName);
else
errExit("Unable to lock PID file '%s'", pidFile);
}
//將文件的大小截斷爲0
if (ftruncate(fd, 0) == -1)
errExit("Could not truncate PID file '%s'", pidFile);
//將PID寫如文件
snprintf(buf, BUF_SIZE, "%ld\n", (long) getpid());
if (write(fd, buf, strlen(buf)) != strlen(buf))
fatal("Writing to PID file '%s'", pidFile);
return fd;
}
老式加鎖技術
在較老的不支持文件加鎖的UNIX
實現上,可以是會用一些特別的加鎖技術。儘管這些技術都已經被fcntl()
記錄加鎖計數所取代,但這裏仍然需要介紹它們,因爲一些較早的應用程序中,仍然存在他們的身影。所有的這些技術在性質上都是勸告式的
open(file, O_CREAT | O_EXCL,...)
加上unlink(file)
SUSv3
第三版的單一規範裏,要求使用了O_CREAT
和O_EXCL
標記的open()
調用原子執行檢查文件的存在性,以及創建文件的兩個步驟。這就意味着如果兩個進程嘗試在創建一個文件時指定這些標記,那麼就保證其中只有一個進程能夠成功。(另外一個進程從open中收到EEXIST
錯誤。)這種調用與unlink()
系統調用組合起來就構成一種加鎖機制的基礎。獲取所可通過使用O_CREAT
和O_EXCL
標記打開文件後,立即跟着一個close()
來完成,釋放鎖可通過使用unlink()
來完成,儘管這項技術能夠正常的工作,但它存在一些侷限:
-
如果
open()
失敗了,即表示其他進程擁有了鎖,那麼就必須要在某種循環中重試open()
操作,這種循環既可以是持續不斷地,也可以是相鄰兩次嘗試時間上加上一定的時間延時。有了fcntl()
之後則可以使用F_SETTLKW
來阻塞直到鎖可用爲止。 -
使用
open()
和uunlink()
獲取和釋放鎖涉及到的文件系統的操作,這比記錄鎖要慢得多, -
如果一個進程意外的終止並且沒有刪除鎖文件,那麼鎖就不會被釋放。處理這個問題存在特別的技術,包括檢查文件的上次修改時間和讓鎖的持有者將進程
ID
寫入文件,這樣就能夠檢查進程是否存在,但這些計數中沒有一項技術是安全可靠的,與之相反的是,在一個進程終止時記錄鎖的釋放操作是原子的。 -
如果放置多把鎖,那麼就無法檢車出死鎖,如果發生了死鎖,那麼造成死鎖的進程就會永遠的保持阻塞。
-
第二版的
NFS
不支持O_EXCL
語義。Linux
2.4NFS
客戶端也沒有正確地實現O_EXCL
,即使是第三版的NFS
以及以後版本的也沒能完成這個任務。
link(file, lockfile)
加上unink()lockfile
link()
系統調用,在新鏈接已經存在時,會失敗的事實可用作一種加鎖機制,而解鎖功能則還是使用unlink()
來完成。常規的做法是讓需要獲取鎖的進程創建一個臨時的文件名,一般來將需要包含進程ID
。要獲取鎖則需要將這個臨時文件鏈接到某個約定的標準路徑上。如果link()
調用成功,那麼就獲取了鎖,如果調用失敗EEXIST
,那麼就是另一個進程持有了鎖,因此必須要在稍後的某個時刻重新嘗試獲取該鎖。這項技術與上面介紹的open(file, O_CREAT|O_EXCL, 0)
技術存在相同的侷限。
open(file, O_CREAT|O_TRUNC|O_WRONLY, 0)
當指定O_TRUNC
並且寫權限被拒絕時在一個既有文件上調用open()
會失敗的事實,可作爲一項幾所技術的基礎,要獲取一把鎖可以使用下面的代碼,來創建新文件。
fd=open(file, O_CREAT|O_TRUNC|O_WRONLY, (mode_t)0);
close(fd);
如果open()
調用成功,那麼就獲取了鎖,如果因EACCES
而失敗(即文件存在但是沒沒有人擁有權限),那麼其他進程持有了鎖,還需要在後面某個時刻嘗試重新獲取鎖。這項計數與前面介紹的技術存在相同的侷限,還需要注意的是不能具備超級用戶特權的程序使用這項技術,因爲open()
總是會成功,不管文件上設置的權限是什麼。
總結
文件加鎖使得進程能偶同步對一個文件的訪問,Linux
提供了兩種文件加鎖的系統調用
- BSD衍生出來的
flock()
system V
衍生出來的fcntl()
儘管這兩組系統調用在大多數UNIX
實現上都是可用的,但只有fcntl()
加鎖是在SUSv3
中進行了標準化。
flock()
系統調用對整個文件加鎖,可放置的鎖有兩種:一種是共享鎖,這種鎖與其他進程持有的共享鎖是兼容的;另一種互斥鎖,這種鎖能偶阻止其他進程放置這兩種鎖。
fcntl()
系統調用將一個文件的任意區域上放置一把鎖(“記錄鎖”),這個區域可以是單個字節也可以是整個文件。可放置的鎖有兩種:讀鎖和寫鎖,他們之間的兼容性語義與flock()
放置的共享鎖和互斥鎖之間的兼容語義類似。如果一個阻塞式(F_SETLKW
)鎖請求將會導致死鎖,那麼內核將讓其中一個受影響的進程的fcntl()
失敗(返回EDADLK
錯誤)。
除非系統中的flock()
是使用fcntl()
實現的,否則使用flock()
和fcntl()
放置的鎖之間是相互不可見的,通過flcok()
和fcntl()
放置的鎖在fork()
中的繼承語義和在文件描述符被關閉時的釋放語義是不同的。
Linux
特有的/proc/locks
文件給出了系統中所有進程當期持有的文件鎖。