本文的目的是想幫助讀者理清 Linux 2.6中文件鎖的概念以及 Linux 2.6 都提供了何種數(shù)據(jù)結(jié)構(gòu)以及關(guān)鍵的系統(tǒng)調(diào)用來實(shí)現(xiàn)文件鎖,從而可以幫助讀者更好地使用文件鎖來解決多個進(jìn)程讀取同一個文件的互斥問題。本文主要描述了 Linux 中各類文件鎖的概念,使用場景,內(nèi)核中描述文件鎖的數(shù)據(jù)結(jié)構(gòu)以及與文件鎖密切相關(guān)的系統(tǒng)調(diào)用等內(nèi)容。

在多任務(wù)操作系統(tǒng)環(huán)境中,如果一個進(jìn)程嘗試對正在被其他進(jìn)程讀取的文件進(jìn)行寫操作,可能會導(dǎo)致正在進(jìn)行讀操作的進(jìn)程讀取到一些被破壞或者不完整的數(shù)據(jù);如果兩個進(jìn)程并發(fā)對同一個文件進(jìn)行寫操作,可能會導(dǎo)致該文件遭到破壞。因此,為了避免發(fā)生這種問題,必須要采用某種機(jī)制來解決多個進(jìn)程并發(fā)訪問同一個文件時所面臨的同步問題,由此而產(chǎn)生了文件加鎖方面的技術(shù)。

早期的 UNIX 系統(tǒng)只支持對整個文件進(jìn)行加鎖,因此無法運(yùn)行數(shù)據(jù)庫之類的程序,因為此類程序需要實(shí)現(xiàn)記錄級的加鎖。在 System V Release 3 中,通過 fcntl 提供了記錄級的加鎖,此后發(fā)展成為 POSIX 標(biāo)準(zhǔn)的一部分。本文將基于 2.6.23 版本的內(nèi)核來探討 Linux 中文件鎖的相關(guān)技術(shù)。

Linux 中的文件鎖

Linux 支持的文件鎖技術(shù)主要包括勸告鎖(advisory lock)和強(qiáng)制鎖(mandatory lock)這兩種。此外,Linux 中還引入了兩種強(qiáng)制鎖的變種形式:共享模式強(qiáng)制鎖(share-mode mandatory lock)和租借鎖(lease)。

在 Linux 中,不論進(jìn)程是在使用勸告鎖還是強(qiáng)制鎖,它都可以同時使用共享鎖和排他鎖(又稱為讀鎖和寫鎖)。多個共享鎖之間不會相互干擾,多個進(jìn)程在同一時刻可以對同一個文件加共享鎖。但是,如果一個進(jìn)程對該文件加了排他鎖,那么其他進(jìn)程則無權(quán)再對該文件加共享鎖或者排他鎖,直到該排他鎖被釋放。所以,對于同一個文件來說,它可以同時擁有很多讀者,但是在某一特定時刻,它只能擁有一個寫者,它們之間的兼容關(guān)系如表 1 所示。


表 1. 鎖間的兼容關(guān)系
是否滿足請求
當(dāng)前加上的鎖 共享鎖 排他鎖
共享鎖
排他鎖

勸告鎖

勸告鎖是一種協(xié)同工作的鎖。對于這一種鎖來說,內(nèi)核只提供加鎖以及檢測文件是否已經(jīng)加鎖的手段,但是內(nèi)核并不參與鎖的控制和協(xié)調(diào)。也就是說,如果有進(jìn)程不遵守“游戲規(guī)則”,不檢查目標(biāo)文件是否已經(jīng)由別的進(jìn)程加了鎖就往其中寫入數(shù)據(jù),那么內(nèi)核是不會加以阻攔的。因此,勸告鎖并不能阻止進(jìn)程對文件的訪問,而只能依靠各個進(jìn)程在訪問文件之前檢查該文件是否已經(jīng)被其他進(jìn)程加鎖來實(shí)現(xiàn)并發(fā)控制。進(jìn)程需要事先對鎖的狀態(tài)做一個約定,并根據(jù)鎖的當(dāng)前狀態(tài)和相互關(guān)系來確定其他進(jìn)程是否能對文件執(zhí)行指定的操作。從這點(diǎn)上來說,勸告鎖的工作方式與使用信號量保護(hù)臨界區(qū)的方式非常類似。

勸告鎖可以對文件的任意一個部分進(jìn)行加鎖,也可以對整個文件進(jìn)行加鎖,甚至可以對文件將來增大的部分也進(jìn)行加鎖。由于進(jìn)程可以選擇對文件的某個部分進(jìn)行加鎖,所以一個進(jìn)程可以獲得關(guān)于某個文件不同部分的多個鎖。

強(qiáng)制鎖

與勸告鎖不同,強(qiáng)制鎖是一種內(nèi)核強(qiáng)制采用的文件鎖,它是從 System V Release 3 開始引入的。每當(dāng)有系統(tǒng)調(diào)用 open()、read() 以及write() 發(fā)生的時候,內(nèi)核都要檢查并確保這些系統(tǒng)調(diào)用不會違反在所訪問文件上加的強(qiáng)制鎖約束。也就是說,如果有進(jìn)程不遵守游戲規(guī)則,硬要往加了鎖的文件中寫入內(nèi)容,內(nèi)核就會加以阻攔:

如果一個文件已經(jīng)被加上了讀鎖或者共享鎖,那么其他進(jìn)程再對這個文件進(jìn)行寫操作就會被內(nèi)核阻止;

如果一個文件已經(jīng)被加上了寫鎖或者排他鎖,那么其他進(jìn)程再對這個文件進(jìn)行讀取或者寫操作就會被內(nèi)核阻止。

如果其他進(jìn)程試圖訪問一個已經(jīng)加有強(qiáng)制鎖的文件,進(jìn)程行為取決于所執(zhí)行的操作模式和文件鎖的類型,歸納如表 2 所示:


表 2. 進(jìn)行對已加強(qiáng)制鎖的文件進(jìn)行操作時的行為
當(dāng)前鎖類型 阻塞讀 阻塞寫 非阻塞讀 非阻塞寫
讀鎖 正常讀取數(shù)據(jù) 阻塞 正常讀取數(shù)據(jù) EAGAIN
寫鎖 阻塞 阻塞 EAGAIN EAGAIN

需要注意的是,如果要訪問的文件的鎖類型與要執(zhí)行的操作存在沖突,那么采用阻塞讀/寫操作的進(jìn)程會被阻塞,而采用非阻塞讀/寫操作的進(jìn)程則不會阻塞,而是立即返回 EAGAIN。

另外,unlink() 系統(tǒng)調(diào)用并不會受到強(qiáng)制鎖的影響,原因在于一個文件可能存在多個硬鏈接,此時刪除文件時并不會修改文件本身的內(nèi)容,而是只會改變其父目錄中 dentry 的內(nèi)容。

然而,在有些應(yīng)用中并不適合使用強(qiáng)制鎖,所以索引節(jié)點(diǎn)結(jié)構(gòu)中的 i_flags 字段中定義了一個標(biāo)志位MS_MANDLOCK用于有選擇地允許或者不允許對一個文件使用強(qiáng)制鎖。在 super_block 結(jié)構(gòu)中,也可以將 s_flags 這個標(biāo)志為設(shè)置為1或者0,用以表示整個設(shè)備上的文件是否允許使用強(qiáng)制鎖。

要想對一個文件采用強(qiáng)制鎖,必須按照以下步驟執(zhí)行:

使用 -o mand 選項來掛載文件系統(tǒng)。這樣在執(zhí)行 mount() 系統(tǒng)調(diào)用時,會傳入 MS_MANDLOCK 標(biāo)記,從而將 super_block 結(jié)構(gòu)中的 s_flags 設(shè)置為 1,用來表示在這個文件系統(tǒng)上可以采用強(qiáng)制鎖。例如:

# mount -o mand /dev/sdb7 /mnt
            # mount | grep mnt
            /dev/sdb7 on /mnt type ext3 (rw,mand)
            

1.修改要加強(qiáng)制鎖的文件的權(quán)限:設(shè)置 SGID 位,并清除組可執(zhí)行位。這種組合通常來說是毫無意義的,系統(tǒng)用來表示該文件被加了強(qiáng)制鎖。例如:

# touch /mnt/testfile
            # ls -l /mnt/testfile
            -rw-r--r-- 1 root root 0 Jun 22 14:43 /mnt/testfile
            # chmod g+s /mnt/testfile
            # chmod g-x /mnt/testfile
            # ls -l /mnt/testfile
            -rw-r-Sr-- 1 root root 0 Jun 22 14:43 /mnt/testfile
            

2.使用 fcntl() 系統(tǒng)調(diào)用對該文件進(jìn)行加鎖或解鎖操作。

1.3. 共享模式鎖

Linux 中還引入了兩種特殊的文件鎖:共享模式強(qiáng)制鎖和租借鎖。這兩種文件鎖可以被看成是強(qiáng)制鎖的兩種變種形式。共享模式強(qiáng)制鎖可以用于某些私有網(wǎng)絡(luò)文件系統(tǒng),如果某個文件被加上了共享模式強(qiáng)制鎖,那么其他進(jìn)程打開該文件的時候不能與該文件的共享模式強(qiáng)制鎖所設(shè)置的訪問模式相沖突。但是由于可移植性不好,因此并不建議使用這種鎖。

租借鎖

采用強(qiáng)制鎖之后,如果一個進(jìn)程對某個文件擁有寫鎖,只要它不釋放這個鎖,就會導(dǎo)致訪問該文件的其他進(jìn)程全部被阻塞或不斷失敗重試;即使該進(jìn)程只擁有讀鎖,也會造成后續(xù)更新該文件的進(jìn)程的阻塞。為了解決這個問題,Linux 中采用了一種新型的租借鎖。

當(dāng)進(jìn)程嘗試打開一個被租借鎖保護(hù)的文件時,該進(jìn)程會被阻塞,同時,在一定時間內(nèi)擁有該文件租借鎖的進(jìn)程會收到一個信號。收到信號之后,擁有該文件租借鎖的進(jìn)程會首先更新文件,從而保證了文件內(nèi)容的一致性,接著,該進(jìn)程釋放這個租借鎖。如果擁有租借鎖的進(jìn)程在一定的時間間隔內(nèi)沒有完成工作,內(nèi)核就會自動刪除這個租借鎖或者將該鎖進(jìn)行降級,從而允許被阻塞的進(jìn)程繼續(xù)工作。

系統(tǒng)默認(rèn)的這段間隔時間是 45 秒鐘,定義如下:

		137 int lease_break_time = 45;
            

這個參數(shù)可以通過修改 /proc/sys/fs/lease-break-time 進(jìn)行調(diào)節(jié)(當(dāng)然,/proc/sys/fs/leases-enable 必須為 1 才行)。





Linux 內(nèi)核中關(guān)于文件鎖的實(shí)現(xiàn)

在 Linux 內(nèi)核中,所有類型的文件鎖都是由數(shù)據(jù)結(jié)構(gòu) file_lock 來描述的,file_lock 結(jié)構(gòu)是在 文件中定義的,內(nèi)容如下所示:


清單 1. file_lock 結(jié)構(gòu)
            811 struct file_lock {
            812         struct file_lock *fl_next;      /* singly linked list for this inode  */
            813         struct list_head fl_link;       /* doubly linked list of all locks */
            814         struct list_head fl_block;      /* circular list of blocked processes */
            815         fl_owner_t fl_owner;
            816         unsigned int fl_pid;
            817         wait_queue_head_t fl_wait;
            818         struct file *fl_file;
            819         unsigned char fl_flags;
            820         unsigned char fl_type;
            821         loff_t fl_start;
            822         loff_t fl_end;
            823
            824         struct fasync_struct *  fl_fasync; /* for lease break notifications */
            825         unsigned long fl_break_time;    /* for nonblocking lease breaks */
            826
            827         struct file_lock_operations *fl_ops;    /* Callbacks for filesystems */
            828      struct lock_manager_operations *fl_lmops;       /* Callbacks for lockmanagers */
            829         union {
            830                 struct nfs_lock_info    nfs_fl;
            831                 struct nfs4_lock_info   nfs4_fl;
            832             struct {
            833                  struct list_head link;  /* link in AFS vnode's pending_locks list */
            834                  int state;              /* state of grant or error if -ve */
            835                 } afs;
            836         } fl_u;
            837 };
            

表 3 簡單描述了 file_lock 結(jié)構(gòu)中的各個字段所表示的含義。


表 3. file_lock 數(shù)據(jù)結(jié)構(gòu)的字段
類型 字段 字段描述
struct file_lock* fl_next 與索引節(jié)點(diǎn)相關(guān)的鎖列表中下一個元素
struct list_head fl_link 指向活躍列表或者被阻塞列表
struct list_head fl_block 指向鎖等待列表
struct files_struct * fl_owner 鎖擁有者的 files_struct
unsigned int fl_pid 進(jìn)程擁有者的 pid
wait_queue_head_t fl_wait 被阻塞進(jìn)程的等待隊列
struct file * fl_file 指向文件對象
unsigned char fl_flags 鎖標(biāo)識
unsigned char fl_type 鎖類型
loff_t fl_start 被鎖區(qū)域的開始位移
loff_t fl_end 被鎖區(qū)域的結(jié)束位移
struct fasync_struct * fl_fasync 用于租借暫停通知
unsigned long fl_break_time 租借的剩余時間
struct file_lock_operations * fl_ops 指向文件鎖操作
struct lock_manager_operations * fl_mops 指向鎖管理操作
union fl_u 文件系統(tǒng)特定信息

一個 file_lock 結(jié)構(gòu)就是一把“鎖”,結(jié)構(gòu)中的 fl_file 就指向目標(biāo)文件的 file 結(jié)構(gòu),而 fl_start 和 fl_end 則確定了該文件要加鎖的一個區(qū)域。當(dāng)進(jìn)程發(fā)出系統(tǒng)調(diào)用來請求對某個文件加排他鎖時,如果這個文件上已經(jīng)加上了共享鎖,那么排他鎖請求不能被立即滿足,這個進(jìn)程必須先要被阻塞。這樣,這個進(jìn)程就被放進(jìn)了等待隊列,file_lock 結(jié)構(gòu)中的 fl_wait 字段就指向這個等待隊列。指向磁盤上相同文件的所有 file_lock 結(jié)構(gòu)會被鏈接成一個單鏈表 file_lock_list,索引節(jié)點(diǎn)結(jié)構(gòu)中的 i_flock 字段會指向該單鏈表結(jié)構(gòu)的首元素,fl_next 用于指向該鏈表中的下一個元素;當(dāng)前系統(tǒng)中所有被請求,但是未被允許的鎖被串成一個鏈表:blocked_list。fl_link 字段指向這兩個列表其中一個。對于被阻塞列表(blocked_list)上的每一個鎖結(jié)構(gòu)來說,fl_next 字段指向與該鎖產(chǎn)生沖突的當(dāng)前正在使用的鎖。所有在等待同一個鎖的那些鎖會被鏈接起來,這就需要用到字段 fl_block,新來的等待者會被加入到等待列表的尾部。 此外,fl_type 表示鎖的性質(zhì),如讀、寫。fl_flags 是一些標(biāo)志位,在 linux 2.6中,這些標(biāo)志位的定義如下所示:


清單 2. 標(biāo)志位的定義
            773 #define FL_POSIX        1
            774 #define FL_FLOCK        2
            775 #define FL_ACCESS       8       /* not trying to lock, just looking */
            776 #define FL_EXISTS       16      /* when unlocking, test for existence */
            777 #define FL_LEASE        32      /* lease held on this file */
            778 #define FL_CLOSE        64      /* unlock on close */
            779 #define FL_SLEEP        128     /* A blocking lock */
            

FL_POSIX 鎖是通過系統(tǒng)調(diào)用 fcntl() 創(chuàng)建的;而 FL_FLOCK 鎖是通過系統(tǒng)調(diào)用 flock()創(chuàng)建的(詳細(xì)內(nèi)容請參見后文中的介紹)。FL_FLOCK 鎖永遠(yuǎn)都和一個文件對象相關(guān)聯(lián),打開這個文件的進(jìn)程擁有該 FL_FLOCK 鎖。當(dāng)一個鎖被請求或者允許的時候,內(nèi)核就會把這個進(jìn)程在同一個文件上的鎖都替換掉。FL_POSIX 鎖則一直與一個進(jìn)程以及一個索引節(jié)點(diǎn)相關(guān)聯(lián)。當(dāng)進(jìn)程死亡或者文件描述符被關(guān)閉的時候,這個鎖會被自動釋放。

對于強(qiáng)制鎖來說,在 Linux 中,內(nèi)核提供了 inline 函數(shù) locks_verify_locked() 用于檢測目標(biāo)文件或者目標(biāo)文件所在的設(shè)備是否允許使用強(qiáng)制鎖,并且檢查該設(shè)備是否已經(jīng)加上了鎖,相關(guān)函數(shù)如下所示:


清單 3. 與強(qiáng)制鎖相關(guān)的函數(shù)
            166 #define __IS_FLG(inode,flg) ((inode)->i_sb->s_flags & (flg))
            173 #define IS_MANDLOCK(inode)      __IS_FLG(inode, MS_MANDLOCK)
            1047 /**
            1048  * locks_mandatory_locked - Check for an active lock
            1049  * @inode: the file to check
            1050  *
            1051  * Searches the inode's list of locks to find any POSIX locks which conflict.
            1052  * This function is called from locks_verify_locked() only.
            1053  */
            1054 int locks_mandatory_locked(struct inode *inode)
            1055 {
            1056         fl_owner_t owner = current->files;
            1057         struct file_lock *fl;
            1058
            1059         /*
            1060          * Search the lock list for this inode for any POSIX locks.
            1061          */
            1062         lock_kernel();
            1063         for (fl = inode->i_flock; fl != NULL; fl = fl->fl_next) {
            1064                 if (!IS_POSIX(fl))
            1065                         continue;
            1066                 if (fl->fl_owner != owner)
            1067                         break;
            1068         }
            1069         unlock_kernel();
            1070         return fl ? -EAGAIN : 0;
            1071 }
            1368 /*
            1369  * Candidates for mandatory locking have the setgid bit set
            1370  * but no group execute bit -  an otherwise meaningless combination.
            1371  */
            1372 #define MANDATORY_LOCK(inode) \
            1373         (IS_MANDLOCK(inode) && ((inode)->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID)
            1374
            1375 static inline int locks_verify_locked(struct inode *inode)
            1376 {
            1377         if (MANDATORY_LOCK(inode))
            1378                 return locks_mandatory_locked(inode);
            1379         return 0;
            1380 }
            

這里,函數(shù) locks_verify_locked() 利用宏 MANDATORY_LOCK 來檢測目標(biāo)文件是否允許加鎖,條件包括:該文件所在的設(shè)備的 super_block 結(jié)構(gòu)中的 s_flags 必須被置為 1,該文件的 SGID 被置為 1 而且同組可執(zhí)行位被清 0。如果允許,則調(diào)用函數(shù)locks_mandatory_locked(),該函數(shù)從索引節(jié)點(diǎn)的鎖列表中查找是否存在有與其相沖突的鎖,即是否已經(jīng)加上了鎖。





Linux 中關(guān)于文件鎖的系統(tǒng)調(diào)用

這里介紹在 Linux 中與文件鎖關(guān)系密切的兩個系統(tǒng)調(diào)用:flock() 和 fcntl()。勸告鎖既可以通過系統(tǒng)調(diào)用 flock() 來實(shí)現(xiàn),也可以通過系統(tǒng)調(diào)用 fcntl() 來實(shí)現(xiàn)。flock() 系統(tǒng)調(diào)用是從 BSD 中衍生出來的,在傳統(tǒng)的類 UNIX 操作系統(tǒng)中,系統(tǒng)調(diào)用flock() 只適用于勸告鎖。但是,Linux 2.6內(nèi)核利用系統(tǒng)調(diào)用 flock() 實(shí)現(xiàn)了我們前面提到的特殊的強(qiáng)制鎖:共享模式強(qiáng)制鎖。另外,flock() 只能實(shí)現(xiàn)對整個文件進(jìn)行加鎖,而不能實(shí)現(xiàn)記錄級的加鎖。系統(tǒng)調(diào)用fcntl() 符合 POSIX 標(biāo)準(zhǔn)的文件鎖實(shí)現(xiàn),它也是非常強(qiáng)大的文件鎖,fcntl() 可以實(shí)現(xiàn)對紀(jì)錄進(jìn)行加鎖。

flock()

flock() 的函數(shù)原型如下所示:

          int flock(int fd, int operation);
            

其中,參數(shù) fd 表示文件描述符;參數(shù) operation 指定要進(jìn)行的鎖操作,該參數(shù)的取值有如下幾種:LOCK_SH, LOCK_EX, LOCK_UN 和 LOCK_MANDphost2008-07-03T00:00:00

man page 里面沒有提到,其各自的意思如下所示:

  • LOCK_SH:表示要創(chuàng)建一個共享鎖,在任意時間內(nèi),一個文件的共享鎖可以被多個進(jìn)程擁有
  • LOCK_EX:表示創(chuàng)建一個排他鎖,在任意時間內(nèi),一個文件的排他鎖只能被一個進(jìn)程擁有
  • LOCK_UN:表示刪除該進(jìn)程創(chuàng)建的鎖
  • LOCK_MAND:它主要是用于共享模式強(qiáng)制鎖,它可以與 LOCK_READ 或者 LOCK_WRITE 聯(lián)合起來使用,從而表示是否允許并發(fā)的讀操作或者并發(fā)的寫操作(盡管在 flock() 的手冊頁中沒有介紹 LOCK_MAND,但是閱讀內(nèi)核源代碼就會發(fā)現(xiàn),這在內(nèi)核中已經(jīng)實(shí)現(xiàn)了)

通常情況下,如果加鎖請求不能被立即滿足,那么系統(tǒng)調(diào)用 flock() 會阻塞當(dāng)前進(jìn)程。比如,進(jìn)程想要請求一個排他鎖,但此時,已經(jīng)由其他進(jìn)程獲取了這個鎖,那么該進(jìn)程將會被阻塞。如果想要在沒有獲得這個排他鎖的情況下不阻塞該進(jìn)程,可以將 LOCK_NB 和 LOCK_SH 或者 LOCK_EX 聯(lián)合使用,那么系統(tǒng)就不會阻塞該進(jìn)程。flock() 所加的鎖會對整個文件起作用。

fcntl()

fcntl() 函數(shù)的功能很多,可以改變已打開的文件的性質(zhì),本文中只是介紹其與獲取/設(shè)置文件鎖有關(guān)的功能。fcntl() 的函數(shù)原型如下所示:

       int fcntl (int fd, int cmd, struct flock *lock);
            

其中,參數(shù) fd 表示文件描述符;參數(shù) cmd 指定要進(jìn)行的鎖操作,由于 fcntl() 函數(shù)功能比較多,這里先介紹與文件鎖相關(guān)的三個取值 F_GETLK、F_SETLK 以及 F_SETLKW。這三個值均與 flock 結(jié)構(gòu)有關(guān)。flock 結(jié)構(gòu)如下所示:


清單 4. flock 結(jié)構(gòu)
            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 結(jié)構(gòu)中,l_type 用來指明創(chuàng)建的是共享鎖還是排他鎖,其取值有三種:F_RDLCK(共享鎖)、F_WRLCK(排他鎖)和F_UNLCK(刪除之前建立的鎖);l_pid 指明了該鎖的擁有者;l_whence、l_start 和l_end 這些字段指明了進(jìn)程需要對文件的哪個區(qū)域進(jìn)行加鎖,這個區(qū)域是一個連續(xù)的字節(jié)集合。因此,進(jìn)程可以對同一個文件的不同部分加不同的鎖。l_whence 必須是 SEEK_SET、SEEK_CUR 或 SEEK_END 這幾個值中的一個,它們分別對應(yīng)著文件頭、當(dāng)前位置和文件尾。l_whence 定義了相對于 l_start 的偏移量,l_start 是從文件開始計算的。

可以執(zhí)行的操作包括:

  • F_GETLK:進(jìn)程可以通過它來獲取通過 fd 打開的那個文件的加鎖信息。執(zhí)行該操作時,lock 指向的結(jié)構(gòu)中就保存了希望對文件加的鎖(或者說要查詢的鎖)。如果確實(shí)存在這樣一把鎖,它阻止 lock 指向的 flock 結(jié)構(gòu)所給出的鎖描述符,則把現(xiàn)存的鎖的信息寫到 lock 指向的 flock 結(jié)構(gòu)中,并將該鎖擁有者的 PID 寫入 l_pid 字段中,然后返回;否則,就將 lock 指向的 flock 結(jié)構(gòu)中的 l_type 設(shè)置為 F_UNLCK,并保持 flock 結(jié)構(gòu)中其他信息不變返回,而不會對該文件真正加鎖。
  • F_SETLK:進(jìn)程用它來對文件的某個區(qū)域進(jìn)行加鎖(l_type的值為 F_RDLCK 或 F_WRLCK)或者刪除鎖(l_type 的值為F_UNLCK),如果有其他鎖阻止該鎖被建立,那么 fcntl() 就出錯返回
  • F_SETLKW:與 F_SETLK 類似,唯一不同的是,如果有其他鎖阻止該鎖被建立,則調(diào)用進(jìn)程進(jìn)入睡眠狀態(tài),等待該鎖釋放。一旦這個調(diào)用開始了等待,就只有在能夠進(jìn)行加鎖或者收到信號時才會返回

需要注意的是,F(xiàn)_GETLK 用于測試是否可以加鎖,在 F_GETLK 測試可以加鎖之后,F(xiàn)_SETLK 和 F_SETLKW 就會企圖建立一把鎖,但是這兩者之間并不是一個原子操作,也就是說,在 F_SETLK 或者 F_SETLKW 還沒有成功加鎖之前,另外一個進(jìn)程就有可能已經(jīng)插進(jìn)來加上了一把鎖。而且,F(xiàn)_SETLKW 有可能導(dǎo)致程序長時間睡眠。還有,程序?qū)δ硞€文件擁有的各種鎖會在相應(yīng)的文件描述符被關(guān)閉時自動清除,程序運(yùn)行結(jié)束后,其所加的各種鎖也會自動清除。

fcntl() 既可以用于勸告鎖,也可以用于強(qiáng)制鎖,在默認(rèn)情況下,它用于勸告鎖。如果它用于強(qiáng)制鎖,當(dāng)進(jìn)程對某個文件進(jìn)行了讀或?qū)戇@樣的系統(tǒng)調(diào)用時,系統(tǒng)則會檢查該文件的鎖的 O_NONBLOCK 標(biāo)識,該標(biāo)識是文件狀態(tài)標(biāo)識的一種,如果設(shè)置文件狀態(tài)標(biāo)識的時候設(shè)置了 O_NONBLOCK,則該進(jìn)程會出錯返回;否則,該進(jìn)程被阻塞。cmd 參數(shù)的值 F_SETFL 可以用于設(shè)置文件狀態(tài)標(biāo)識。

此外,系統(tǒng)調(diào)用 fcntl() 還可以用于租借鎖,此時采用的函數(shù)原型如下:

	       int fcntl(int fd, int cmd, long arg);
            

與租借鎖相關(guān)的 cmd 參數(shù)的取值有兩種:F_SETLEASE 和 F_GETLEASE。其含義如下所示:

  • F_SETLEASE:根據(jù)下面所描述的 arg 參數(shù)指定的值來建立或者刪除租約:
    • F_RDLCK:設(shè)置讀租約。當(dāng)文件被另一個進(jìn)程以寫的方式打開時,擁有該租約的當(dāng)前進(jìn)程會收到通知
    • F_WRLCK:設(shè)置寫租約。當(dāng)文件被另一個進(jìn)程以讀或者寫的方式打開時,擁有該租約的當(dāng)前進(jìn)程會收到通知
    • F_UNLCK:刪除以前建立的租約
  • F_GETLEASE:表明調(diào)用進(jìn)程擁有文件上哪種類型的鎖,這需要通過返回值來確定,返回值有三種:F_RDLCK、F_WRLCK和F_UNLCK,分別表明調(diào)用進(jìn)程對文件擁有讀租借、寫租借或者根本沒有租借

某個進(jìn)程可能會對文件執(zhí)行其他一些系統(tǒng)調(diào)用(比如 OPEN() 或者 TRUNCATE()),如果這些系統(tǒng)調(diào)用與該文件上由 F_SETLEASE 所設(shè)置的租借鎖相沖突,內(nèi)核就會阻塞這個系統(tǒng)調(diào)用;同時,內(nèi)核會給擁有這個租借鎖的進(jìn)程發(fā)信號,告知此事。擁有此租借鎖的進(jìn)程會對該信號進(jìn)行反饋,它可能會刪除這個租借鎖,也可能會減短這個租借鎖的租約,從而可以使得該文件可以被其他進(jìn)程所訪問。如果擁有租借鎖的進(jìn)程不能在給定時間內(nèi)完成上述操作,那么系統(tǒng)會強(qiáng)制幫它完成。通過 F_SETLEASE 命令將 arg 參數(shù)指定為 F_UNLCK 就可以刪除這個租借鎖。不管對該租借鎖減短租約或者干脆刪除的操作是進(jìn)程自愿的還是內(nèi)核強(qiáng)迫的,只要被阻塞的系統(tǒng)調(diào)用還沒有被發(fā)出該調(diào)用的進(jìn)程解除阻塞,那么系統(tǒng)就會允許這個系統(tǒng)調(diào)用執(zhí)行。即使被阻塞的系統(tǒng)調(diào)用因為某些原因被解除阻塞,但是上面對租借鎖減短租約或者刪除這個過程還是會執(zhí)行的。

需要注意的是,租借鎖也只能對整個文件生效,而無法實(shí)現(xiàn)記錄級的加鎖。






文件鎖的使用樣例

為了使讀者更深入理解本文中介紹的內(nèi)容,下面我們給出了一個例子來詳細(xì)介紹文件鎖的具體用法。這個例子可以用來檢測所使用的文件是否支持強(qiáng)制鎖,其源代碼如下所示:


清單 5. 鎖的使用方法具體示例
            # cat -n mandlock.c
            1  #include <errno.h>
            2  #include <stdio.h>
            3  #include <fcntl.h>
            4  #include <sys/wait.h>
            5  #include <sys/stat.h>
            6
            7  int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
            8  {
            9          struct flock    lock;
            10
            11          lock.l_type = type;     /* F_RDLCK, F_WRLCK, F_UNLCK */
            12          lock.l_start = offset;  /* byte offset, relative to l_whence */
            13          lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
            14          lock.l_len = len;       /* #bytes (0 means to EOF) */
            15
            16          return( fcntl(fd, cmd, &lock) );
            17  }
            18
            19  #define read_lock(fd, offset, whence, len) \
            20                          lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)
            21  #define write_lock(fd, offset, whence, len) \
            22                          lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)
            23
            24  #define err_sys(x) { perror(x); exit(1); }
            25
            26  int main(int argc, char *argv[])
            27  {
            28      int             fd, val;
            29      pid_t           pid;
            30      char            buf[5];
            31      struct stat     statbuf;
            32      if (argc != 2) {
            33          fprintf(stderr, "usage: %s filename\n", argv[0]);
            34          exit(1);
            35      }
            36      if ((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC )) < 0)
            37          err_sys("open error");
            38      if (write(fd, "hello world", 11) != 11)
            39          err_sys("write error");
            40
            41      /* turn on set-group-ID and turn off group-execute */
            42      if (fstat(fd, &statbuf) < 0)
            43          err_sys("fstat error");
            44      if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
            45          err_sys("fchmod error");
            46
            47      sleep(2);
            48
            49      if ((pid = fork()) < 0) {
            50          err_sys("fork error");
            51      } else if (pid > 0) {   /* parent */
            52          /* write lock entire file */
            53          if (write_lock(fd, 0, SEEK_SET, 0) < 0)
            54              err_sys("write_lock error");
            55
            56          sleep(20);      /* wait for child to set lock and read data */
            57
            58          if (waitpid(pid, NULL, 0) < 0)
            59              err_sys("waitpid error");
            60
            61      } else {            /* child */
            62          sleep(10);      /* wait for parent to set lock */
            63
            64          if ( (val = fcntl(fd, F_GETFL, 0)) < 0)
            65                  err_sys("fcntl F_GETFL error");
            66
            67          val |= O_NONBLOCK;           /* turn on O_NONBLOCK flag */
            68
            69          if (fcntl(fd, F_SETFL, val) < 0)
            70                  err_sys("fcntl F_SETFL error");
            71
            72         /* first let's see what error we get if region is locked */
            73         if (read_lock(fd, 0, SEEK_SET, 0) != -1)    /* no wait */
            74             err_sys("child: read_lock succeeded");
            75
            76 printf("read_lock of already-locked region returns %d: %s\n", errno, strerror(errno));
            77
            78         /* now try to read the mandatory locked file */
            79         if (lseek(fd, 0, SEEK_SET) == -1)
            80             err_sys("lseek error");
            81         if (read(fd, buf, 5) < 0)
            82             printf("read failed (mandatory locking works)\n");
            83         else
            84             printf("read OK (no mandatory locking), buf = %5.5s\n", buf);
            85      }
            86      exit(0);
            87  }
            88
            

樣例代碼中所采用的技術(shù)在前文中大都已經(jīng)介紹過了,其基本思想是在首先在父進(jìn)程中對文件加上寫鎖;然后在子進(jìn)程中將文件描述符設(shè)置為非阻塞模式(第 69 行),然后對文件加讀鎖,并嘗試讀取該文件中的內(nèi)容。如果系統(tǒng)支持強(qiáng)制鎖,則子進(jìn)程中的 read() 系統(tǒng)調(diào)用(代碼中的第 81 行)會立即返回 EAGAIN;否則,等父進(jìn)程完成寫文件操作之后,子進(jìn)程中的 read() 系統(tǒng)調(diào)用就會返回父進(jìn)程剛剛寫入的前 5 個字節(jié)的數(shù)據(jù)。代碼中的幾個 sleep() 是為了協(xié)調(diào)父進(jìn)程與子進(jìn)程之間的同步而使用的。

該程序在測試系統(tǒng)的執(zhí)行結(jié)果如下所示:

# mount | grep mnt
            /dev/sdb7 on /mnt type ext3 (rw,mand)
            /dev/sdb6 on /tmp/mnt type ext3 (rw)
            # ./mandlock /mnt/testfile
            read_lock of already-locked region returns 11: Resource temporarily unavailable
            read failed (mandatory locking works)
            # ./mandlock /tmp/mnt/testfile
            read_lock of already-locked region returns 11: Resource temporarily unavailable
            read OK (no mandatory locking), buf = hello
            

我們可以看到,/dev/sdb7 使用 –o mand 選項掛載到了 /mnt 目錄中,而 /dev/sdb6 則么有使用這個選項掛載到了 /tmp/mnt 目錄中。由于在程序中我們完成了對測試文件 SGID 和同組可執(zhí)行位的設(shè)置(第 44 行),因此 /mnt/testfile 可以支持強(qiáng)制鎖,而 /tmp/mnt/testfile 則不能。這也正是為什么前者的 read() 系統(tǒng)調(diào)用會失敗返回而后者則可以成功讀取到 hello 的原因。





總結(jié)

Linux 的文件鎖在以共享索引節(jié)點(diǎn)共享文件的情況下設(shè)計的,文件鎖的實(shí)現(xiàn)可以使得不同用戶同時讀寫同一文件的并發(fā)問題得以解決。本文描述了 Linux 中各類文件鎖的概念,使用場景,內(nèi)核中描述文件鎖的數(shù)據(jù)結(jié)構(gòu)以及與文件鎖密切相關(guān)的系統(tǒng)調(diào)用等內(nèi)容,至于與文件鎖相關(guān)的索引節(jié)點(diǎn)數(shù)據(jù)結(jié)構(gòu),以及在對文件進(jìn)行加鎖時遇到的死鎖問題等其他知識,這里沒有做詳盡介紹,感興趣的讀者可以自行參考內(nèi)核源代碼。

本文的目的是想幫助讀者理清 Linux 2.6中文件鎖的概念以及 Linux 2.6 都提供了何種數(shù)據(jù)結(jié)構(gòu)以及關(guān)鍵的系統(tǒng)調(diào)用來實(shí)現(xiàn)文件鎖,從而可以幫助讀者更好地使用文件鎖來解決多個進(jìn)程讀取同一個文件的互斥問題。






參考資料

mandlock.c