Linux中文件加鎖

/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_CREATO_EXCL標記的open()調用原子執行檢查文件的存在性,以及創建文件的兩個步驟。這就意味着如果兩個進程嘗試在創建一個文件時指定這些標記,那麼就保證其中只有一個進程能夠成功。(另外一個進程從open中收到EEXIST錯誤。)這種調用與unlink()系統調用組合起來就構成一種加鎖機制的基礎。獲取所可通過使用O_CREATO_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提供了兩種文件加鎖的系統調用

  1. BSD衍生出來的flock()
  2. system V衍生出來的fcntl()

儘管這兩組系統調用在大多數UNIX實現上都是可用的,但只有fcntl()加鎖是在SUSv3中進行了標準化。


flock()系統調用對整個文件加鎖,可放置的鎖有兩種:一種是共享鎖,這種鎖與其他進程持有的共享鎖是兼容的;另一種互斥鎖,這種鎖能偶阻止其他進程放置這兩種鎖。


fcntl()系統調用將一個文件的任意區域上放置一把鎖(“記錄鎖”),這個區域可以是單個字節也可以是整個文件。可放置的鎖有兩種:讀鎖和寫鎖,他們之間的兼容性語義與flock()放置的共享鎖和互斥鎖之間的兼容語義類似。如果一個阻塞式(F_SETLKW)鎖請求將會導致死鎖,那麼內核將讓其中一個受影響的進程的fcntl()失敗(返回EDADLK錯誤)。


除非系統中的flock()是使用fcntl()實現的,否則使用flock()fcntl()放置的鎖之間是相互不可見的,通過flcok()fcntl()放置的鎖在fork()中的繼承語義和在文件描述符被關閉時的釋放語義是不同的。

Linux特有的/proc/locks文件給出了系統中所有進程當期持有的文件鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章