本文的目的是想幫助讀者理清 Linux 2.6中文件鎖的概念以及 Linux 2.6 都提供了何種數據結構以及關鍵的系統調用來實現文件鎖,從而可以幫助讀者更好地使用文件鎖來解決多個進程讀取同一個文件的互斥問題。本文主要描述了 Linux 中各類文件鎖的概念,使用場景,內核中描述文件鎖的數據結構以及與文件鎖密切相關的系統調用等內容。
在多任務操作系統環境中,如果一個進程嘗試對正在被其他進程讀取的文件進行寫操作,可能會導致正在進行讀操作的進程讀取到一些被破壞或者不完整的數據;如果兩個進程并發對同一個文件進行寫操作,可能會導致該文件遭到破壞。因此,為了避免發生這種問題,必須要采用某種機制來解決多個進程并發訪問同一個文件時所面臨的同步問題,由此而產生了文件加鎖方面的技術。
早期的 UNIX 系統只支持對整個文件進行加鎖,因此無法運行數據庫之類的程序,因為此類程序需要實現記錄級的加鎖。在 System V Release 3 中,通過 fcntl 提供了記錄級的加鎖,此后發展成為 POSIX 標準的一部分。本文將基于 2.6.23 版本的內核來探討 Linux 中文件鎖的相關技術。
Linux 中的文件鎖
Linux 支持的文件鎖技術主要包括勸告鎖(advisory lock)和強制鎖(mandatory lock)這兩種。此外,Linux 中還引入了兩種強制鎖的變種形式:共享模式強制鎖(share-mode mandatory lock)和租借鎖(lease)。
在 Linux 中,不論進程是在使用勸告鎖還是強制鎖,它都可以同時使用共享鎖和排他鎖(又稱為讀鎖和寫鎖)。多個共享鎖之間不會相互干擾,多個進程在同一時刻可以對同一個文件加共享鎖。但是,如果一個進程對該文件加了排他鎖,那么其他進程則無權再對該文件加共享鎖或者排他鎖,直到該排他鎖被釋放。所以,對于同一個文件來說,它可以同時擁有很多讀者,但是在某一特定時刻,它只能擁有一個寫者,它們之間的兼容關系如表 1 所示。
表 1. 鎖間的兼容關系
| 是否滿足請求 |
當前加上的鎖 |
共享鎖 |
排他鎖 |
無 |
是 |
是 |
共享鎖 |
是 |
否 |
排他鎖 |
否 |
否 |
勸告鎖
勸告鎖是一種協同工作的鎖。對于這一種鎖來說,內核只提供加鎖以及檢測文件是否已經加鎖的手段,但是內核并不參與鎖的控制和協調。也就是說,如果有進程不遵守“游戲規則”,不檢查目標文件是否已經由別的進程加了鎖就往其中寫入數據,那么內核是不會加以阻攔的。因此,勸告鎖并不能阻止進程對文件的訪問,而只能依靠各個進程在訪問文件之前檢查該文件是否已經被其他進程加鎖來實現并發控制。進程需要事先對鎖的狀態做一個約定,并根據鎖的當前狀態和相互關系來確定其他進程是否能對文件執行指定的操作。從這點上來說,勸告鎖的工作方式與使用信號量保護臨界區的方式非常類似。
勸告鎖可以對文件的任意一個部分進行加鎖,也可以對整個文件進行加鎖,甚至可以對文件將來增大的部分也進行加鎖。由于進程可以選擇對文件的某個部分進行加鎖,所以一個進程可以獲得關于某個文件不同部分的多個鎖。
強制鎖
與勸告鎖不同,強制鎖是一種內核強制采用的文件鎖,它是從 System V Release 3 開始引入的。每當有系統調用 open()、read() 以及write() 發生的時候,內核都要檢查并確保這些系統調用不會違反在所訪問文件上加的強制鎖約束。也就是說,如果有進程不遵守游戲規則,硬要往加了鎖的文件中寫入內容,內核就會加以阻攔:
如果一個文件已經被加上了讀鎖或者共享鎖,那么其他進程再對這個文件進行寫操作就會被內核阻止;
如果一個文件已經被加上了寫鎖或者排他鎖,那么其他進程再對這個文件進行讀取或者寫操作就會被內核阻止。
如果其他進程試圖訪問一個已經加有強制鎖的文件,進程行為取決于所執行的操作模式和文件鎖的類型,歸納如表 2 所示:
表 2. 進行對已加強制鎖的文件進行操作時的行為
當前鎖類型 |
阻塞讀 |
阻塞寫 |
非阻塞讀 |
非阻塞寫 |
讀鎖 |
正常讀取數據 |
阻塞 |
正常讀取數據 |
EAGAIN |
寫鎖 |
阻塞 |
阻塞 |
EAGAIN |
EAGAIN |
需要注意的是,如果要訪問的文件的鎖類型與要執行的操作存在沖突,那么采用阻塞讀/寫操作的進程會被阻塞,而采用非阻塞讀/寫操作的進程則不會阻塞,而是立即返回 EAGAIN。
另外,unlink() 系統調用并不會受到強制鎖的影響,原因在于一個文件可能存在多個硬鏈接,此時刪除文件時并不會修改文件本身的內容,而是只會改變其父目錄中 dentry 的內容。
然而,在有些應用中并不適合使用強制鎖,所以索引節點結構中的 i_flags 字段中定義了一個標志位MS_MANDLOCK用于有選擇地允許或者不允許對一個文件使用強制鎖。在 super_block 結構中,也可以將 s_flags 這個標志為設置為1或者0,用以表示整個設備上的文件是否允許使用強制鎖。
要想對一個文件采用強制鎖,必須按照以下步驟執行:
使用 -o mand 選項來掛載文件系統。這樣在執行 mount() 系統調用時,會傳入 MS_MANDLOCK 標記,從而將 super_block 結構中的 s_flags 設置為 1,用來表示在這個文件系統上可以采用強制鎖。例如:
# mount -o mand /dev/sdb7 /mnt
# mount | grep mnt
/dev/sdb7 on /mnt type ext3 (rw,mand)
|
1.修改要加強制鎖的文件的權限:設置 SGID 位,并清除組可執行位。這種組合通常來說是毫無意義的,系統用來表示該文件被加了強制鎖。例如:
# 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() 系統調用對該文件進行加鎖或解鎖操作。
1.3. 共享模式鎖
Linux 中還引入了兩種特殊的文件鎖:共享模式強制鎖和租借鎖。這兩種文件鎖可以被看成是強制鎖的兩種變種形式。共享模式強制鎖可以用于某些私有網絡文件系統,如果某個文件被加上了共享模式強制鎖,那么其他進程打開該文件的時候不能與該文件的共享模式強制鎖所設置的訪問模式相沖突。但是由于可移植性不好,因此并不建議使用這種鎖。
租借鎖
采用強制鎖之后,如果一個進程對某個文件擁有寫鎖,只要它不釋放這個鎖,就會導致訪問該文件的其他進程全部被阻塞或不斷失敗重試;即使該進程只擁有讀鎖,也會造成后續更新該文件的進程的阻塞。為了解決這個問題,Linux 中采用了一種新型的租借鎖。
當進程嘗試打開一個被租借鎖保護的文件時,該進程會被阻塞,同時,在一定時間內擁有該文件租借鎖的進程會收到一個信號。收到信號之后,擁有該文件租借鎖的進程會首先更新文件,從而保證了文件內容的一致性,接著,該進程釋放這個租借鎖。如果擁有租借鎖的進程在一定的時間間隔內沒有完成工作,內核就會自動刪除這個租借鎖或者將該鎖進行降級,從而允許被阻塞的進程繼續工作。
系統默認的這段間隔時間是 45 秒鐘,定義如下:
137 int lease_break_time = 45;
|
這個參數可以通過修改 /proc/sys/fs/lease-break-time 進行調節(當然,/proc/sys/fs/leases-enable 必須為 1 才行)。
Linux 內核中關于文件鎖的實現
在 Linux 內核中,所有類型的文件鎖都是由數據結構 file_lock 來描述的,file_lock 結構是在 文件中定義的,內容如下所示:
清單 1. file_lock 結構
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 結構中的各個字段所表示的含義。
表 3. file_lock 數據結構的字段
類型 |
字段 |
字段描述 |
struct file_lock* |
fl_next |
與索引節點相關的鎖列表中下一個元素 |
struct list_head |
fl_link |
指向活躍列表或者被阻塞列表 |
struct list_head |
fl_block |
指向鎖等待列表 |
struct files_struct * |
fl_owner |
鎖擁有者的 files_struct |
unsigned int |
fl_pid |
進程擁有者的 pid |
wait_queue_head_t |
fl_wait |
被阻塞進程的等待隊列 |
struct file * |
fl_file |
指向文件對象 |
unsigned char |
fl_flags |
鎖標識 |
unsigned char |
fl_type |
鎖類型 |
loff_t |
fl_start |
被鎖區域的開始位移 |
loff_t |
fl_end |
被鎖區域的結束位移 |
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 |
文件系統特定信息 |
一個 file_lock 結構就是一把“鎖”,結構中的 fl_file 就指向目標文件的 file 結構,而 fl_start 和 fl_end 則確定了該文件要加鎖的一個區域。當進程發出系統調用來請求對某個文件加排他鎖時,如果這個文件上已經加上了共享鎖,那么排他鎖請求不能被立即滿足,這個進程必須先要被阻塞。這樣,這個進程就被放進了等待隊列,file_lock 結構中的 fl_wait 字段就指向這個等待隊列。指向磁盤上相同文件的所有 file_lock 結構會被鏈接成一個單鏈表 file_lock_list,索引節點結構中的 i_flock 字段會指向該單鏈表結構的首元素,fl_next 用于指向該鏈表中的下一個元素;當前系統中所有被請求,但是未被允許的鎖被串成一個鏈表:blocked_list。fl_link 字段指向這兩個列表其中一個。對于被阻塞列表(blocked_list)上的每一個鎖結構來說,fl_next 字段指向與該鎖產生沖突的當前正在使用的鎖。所有在等待同一個鎖的那些鎖會被鏈接起來,這就需要用到字段 fl_block,新來的等待者會被加入到等待列表的尾部。 此外,fl_type 表示鎖的性質,如讀、寫。fl_flags 是一些標志位,在 linux 2.6中,這些標志位的定義如下所示:
清單 2. 標志位的定義
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 鎖是通過系統調用 fcntl() 創建的;而 FL_FLOCK 鎖是通過系統調用 flock()創建的(詳細內容請參見后文中的介紹)。FL_FLOCK 鎖永遠都和一個文件對象相關聯,打開這個文件的進程擁有該 FL_FLOCK 鎖。當一個鎖被請求或者允許的時候,內核就會把這個進程在同一個文件上的鎖都替換掉。FL_POSIX 鎖則一直與一個進程以及一個索引節點相關聯。當進程死亡或者文件描述符被關閉的時候,這個鎖會被自動釋放。
對于強制鎖來說,在 Linux 中,內核提供了 inline 函數 locks_verify_locked() 用于檢測目標文件或者目標文件所在的設備是否允許使用強制鎖,并且檢查該設備是否已經加上了鎖,相關函數如下所示:
清單 3. 與強制鎖相關的函數
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 }
|
這里,函數 locks_verify_locked() 利用宏 MANDATORY_LOCK 來檢測目標文件是否允許加鎖,條件包括:該文件所在的設備的 super_block 結構中的 s_flags 必須被置為 1,該文件的 SGID 被置為 1 而且同組可執行位被清 0。如果允許,則調用函數locks_mandatory_locked(),該函數從索引節點的鎖列表中查找是否存在有與其相沖突的鎖,即是否已經加上了鎖。
Linux 中關于文件鎖的系統調用
這里介紹在 Linux 中與文件鎖關系密切的兩個系統調用:flock() 和 fcntl()。勸告鎖既可以通過系統調用 flock() 來實現,也可以通過系統調用 fcntl() 來實現。flock() 系統調用是從 BSD 中衍生出來的,在傳統的類 UNIX 操作系統中,系統調用flock() 只適用于勸告鎖。但是,Linux 2.6內核利用系統調用 flock() 實現了我們前面提到的特殊的強制鎖:共享模式強制鎖。另外,flock() 只能實現對整個文件進行加鎖,而不能實現記錄級的加鎖。系統調用fcntl() 符合 POSIX 標準的文件鎖實現,它也是非常強大的文件鎖,fcntl() 可以實現對紀錄進行加鎖。
flock()
flock() 的函數原型如下所示:
int flock(int fd, int operation);
|
其中,參數 fd 表示文件描述符;參數 operation 指定要進行的鎖操作,該參數的取值有如下幾種:LOCK_SH, LOCK_EX, LOCK_UN 和 LOCK_MANDphost2008-07-03T00:00:00
man page 里面沒有提到,其各自的意思如下所示:
- LOCK_SH:表示要創建一個共享鎖,在任意時間內,一個文件的共享鎖可以被多個進程擁有
- LOCK_EX:表示創建一個排他鎖,在任意時間內,一個文件的排他鎖只能被一個進程擁有
- LOCK_UN:表示刪除該進程創建的鎖
- LOCK_MAND:它主要是用于共享模式強制鎖,它可以與 LOCK_READ 或者 LOCK_WRITE 聯合起來使用,從而表示是否允許并發的讀操作或者并發的寫操作(盡管在 flock() 的手冊頁中沒有介紹 LOCK_MAND,但是閱讀內核源代碼就會發現,這在內核中已經實現了)
通常情況下,如果加鎖請求不能被立即滿足,那么系統調用 flock() 會阻塞當前進程。比如,進程想要請求一個排他鎖,但此時,已經由其他進程獲取了這個鎖,那么該進程將會被阻塞。如果想要在沒有獲得這個排他鎖的情況下不阻塞該進程,可以將 LOCK_NB 和 LOCK_SH 或者 LOCK_EX 聯合使用,那么系統就不會阻塞該進程。flock() 所加的鎖會對整個文件起作用。
fcntl()
fcntl() 函數的功能很多,可以改變已打開的文件的性質,本文中只是介紹其與獲取/設置文件鎖有關的功能。fcntl() 的函數原型如下所示:
int fcntl (int fd, int cmd, struct flock *lock);
|
其中,參數 fd 表示文件描述符;參數 cmd 指定要進行的鎖操作,由于 fcntl() 函數功能比較多,這里先介紹與文件鎖相關的三個取值 F_GETLK、F_SETLK 以及 F_SETLKW。這三個值均與 flock 結構有關。flock 結構如下所示:
清單 4. 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 結構中,l_type 用來指明創建的是共享鎖還是排他鎖,其取值有三種:F_RDLCK(共享鎖)、F_WRLCK(排他鎖)和F_UNLCK(刪除之前建立的鎖);l_pid 指明了該鎖的擁有者;l_whence、l_start 和l_end 這些字段指明了進程需要對文件的哪個區域進行加鎖,這個區域是一個連續的字節集合。因此,進程可以對同一個文件的不同部分加不同的鎖。l_whence 必須是 SEEK_SET、SEEK_CUR 或 SEEK_END 這幾個值中的一個,它們分別對應著文件頭、當前位置和文件尾。l_whence 定義了相對于 l_start 的偏移量,l_start 是從文件開始計算的。
可以執行的操作包括:
- F_GETLK:進程可以通過它來獲取通過 fd 打開的那個文件的加鎖信息。執行該操作時,lock 指向的結構中就保存了希望對文件加的鎖(或者說要查詢的鎖)。如果確實存在這樣一把鎖,它阻止 lock 指向的 flock 結構所給出的鎖描述符,則把現存的鎖的信息寫到 lock 指向的 flock 結構中,并將該鎖擁有者的 PID 寫入 l_pid 字段中,然后返回;否則,就將 lock 指向的 flock 結構中的 l_type 設置為 F_UNLCK,并保持 flock 結構中其他信息不變返回,而不會對該文件真正加鎖。
- F_SETLK:進程用它來對文件的某個區域進行加鎖(l_type的值為 F_RDLCK 或 F_WRLCK)或者刪除鎖(l_type 的值為F_UNLCK),如果有其他鎖阻止該鎖被建立,那么 fcntl() 就出錯返回
- F_SETLKW:與 F_SETLK 類似,唯一不同的是,如果有其他鎖阻止該鎖被建立,則調用進程進入睡眠狀態,等待該鎖釋放。一旦這個調用開始了等待,就只有在能夠進行加鎖或者收到信號時才會返回
需要注意的是,F_GETLK 用于測試是否可以加鎖,在 F_GETLK 測試可以加鎖之后,F_SETLK 和 F_SETLKW 就會企圖建立一把鎖,但是這兩者之間并不是一個原子操作,也就是說,在 F_SETLK 或者 F_SETLKW 還沒有成功加鎖之前,另外一個進程就有可能已經插進來加上了一把鎖。而且,F_SETLKW 有可能導致程序長時間睡眠。還有,程序對某個文件擁有的各種鎖會在相應的文件描述符被關閉時自動清除,程序運行結束后,其所加的各種鎖也會自動清除。
fcntl() 既可以用于勸告鎖,也可以用于強制鎖,在默認情況下,它用于勸告鎖。如果它用于強制鎖,當進程對某個文件進行了讀或寫這樣的系統調用時,系統則會檢查該文件的鎖的 O_NONBLOCK 標識,該標識是文件狀態標識的一種,如果設置文件狀態標識的時候設置了 O_NONBLOCK,則該進程會出錯返回;否則,該進程被阻塞。cmd 參數的值 F_SETFL 可以用于設置文件狀態標識。
此外,系統調用 fcntl() 還可以用于租借鎖,此時采用的函數原型如下:
int fcntl(int fd, int cmd, long arg);
|
與租借鎖相關的 cmd 參數的取值有兩種:F_SETLEASE 和 F_GETLEASE。其含義如下所示:
- F_SETLEASE:根據下面所描述的 arg 參數指定的值來建立或者刪除租約:
- F_RDLCK:設置讀租約。當文件被另一個進程以寫的方式打開時,擁有該租約的當前進程會收到通知
- F_WRLCK:設置寫租約。當文件被另一個進程以讀或者寫的方式打開時,擁有該租約的當前進程會收到通知
- F_UNLCK:刪除以前建立的租約
- F_GETLEASE:表明調用進程擁有文件上哪種類型的鎖,這需要通過返回值來確定,返回值有三種:F_RDLCK、F_WRLCK和F_UNLCK,分別表明調用進程對文件擁有讀租借、寫租借或者根本沒有租借
某個進程可能會對文件執行其他一些系統調用(比如 OPEN() 或者 TRUNCATE()),如果這些系統調用與該文件上由 F_SETLEASE 所設置的租借鎖相沖突,內核就會阻塞這個系統調用;同時,內核會給擁有這個租借鎖的進程發信號,告知此事。擁有此租借鎖的進程會對該信號進行反饋,它可能會刪除這個租借鎖,也可能會減短這個租借鎖的租約,從而可以使得該文件可以被其他進程所訪問。如果擁有租借鎖的進程不能在給定時間內完成上述操作,那么系統會強制幫它完成。通過 F_SETLEASE 命令將 arg 參數指定為 F_UNLCK 就可以刪除這個租借鎖。不管對該租借鎖減短租約或者干脆刪除的操作是進程自愿的還是內核強迫的,只要被阻塞的系統調用還沒有被發出該調用的進程解除阻塞,那么系統就會允許這個系統調用執行。即使被阻塞的系統調用因為某些原因被解除阻塞,但是上面對租借鎖減短租約或者刪除這個過程還是會執行的。
需要注意的是,租借鎖也只能對整個文件生效,而無法實現記錄級的加鎖。
文件鎖的使用樣例
為了使讀者更深入理解本文中介紹的內容,下面我們給出了一個例子來詳細介紹文件鎖的具體用法。這個例子可以用來檢測所使用的文件是否支持強制鎖,其源代碼如下所示:
清單 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
|
樣例代碼中所采用的技術在前文中大都已經介紹過了,其基本思想是在首先在父進程中對文件加上寫鎖;然后在子進程中將文件描述符設置為非阻塞模式(第 69 行),然后對文件加讀鎖,并嘗試讀取該文件中的內容。如果系統支持強制鎖,則子進程中的 read() 系統調用(代碼中的第 81 行)會立即返回 EAGAIN;否則,等父進程完成寫文件操作之后,子進程中的 read() 系統調用就會返回父進程剛剛寫入的前 5 個字節的數據。代碼中的幾個 sleep() 是為了協調父進程與子進程之間的同步而使用的。
該程序在測試系統的執行結果如下所示:
# 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 和同組可執行位的設置(第 44 行),因此 /mnt/testfile 可以支持強制鎖,而 /tmp/mnt/testfile 則不能。這也正是為什么前者的 read() 系統調用會失敗返回而后者則可以成功讀取到 hello 的原因。
總結
Linux 的文件鎖在以共享索引節點共享文件的情況下設計的,文件鎖的實現可以使得不同用戶同時讀寫同一文件的并發問題得以解決。本文描述了 Linux 中各類文件鎖的概念,使用場景,內核中描述文件鎖的數據結構以及與文件鎖密切相關的系統調用等內容,至于與文件鎖相關的索引節點數據結構,以及在對文件進行加鎖時遇到的死鎖問題等其他知識,這里沒有做詳盡介紹,感興趣的讀者可以自行參考內核源代碼。
本文的目的是想幫助讀者理清 Linux 2.6中文件鎖的概念以及 Linux 2.6 都提供了何種數據結構以及關鍵的系統調用來實現文件鎖,從而可以幫助讀者更好地使用文件鎖來解決多個進程讀取同一個文件的互斥問題。
參考資料
mandlock.c