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

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

早期的 UNIX 系統(tǒng)只支持對(duì)整個(gè)文件進(jìn)行加鎖,因此無(wú)法運(yùn)行數(shù)據(jù)庫(kù)之類(lèi)的程序,因?yàn)榇祟?lèi)程序需要實(shí)現(xiàn)記錄級(jí)的加鎖。在 System V Release 3 中,通過(guò) fcntl 提供了記錄級(jí)的加鎖,此后發(fā)展成為 POSIX 標(biāo)準(zhǔn)的一部分。本文將基于 2.6.23 版本的內(nèi)核來(lá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)制鎖,它都可以同時(shí)使用共享鎖和排他鎖(又稱(chēng)為讀鎖和寫(xiě)鎖)。多個(gè)共享鎖之間不會(huì)相互干擾,多個(gè)進(jìn)程在同一時(shí)刻可以對(duì)同一個(gè)文件加共享鎖。但是,如果一個(gè)進(jìn)程對(duì)該文件加了排他鎖,那么其他進(jìn)程則無(wú)權(quán)再對(duì)該文件加共享鎖或者排他鎖,直到該排他鎖被釋放。所以,對(duì)于同一個(gè)文件來(lái)說(shuō),它可以同時(shí)擁有很多讀者,但是在某一特定時(shí)刻,它只能擁有一個(gè)寫(xiě)者,它們之間的兼容關(guān)系如表 1 所示。


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

勸告鎖

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

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

強(qiáng)制鎖

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

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

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

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


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

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

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

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

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

使用 -o mand 選項(xiàng)來(lái)掛載文件系統(tǒng)。這樣在執(zhí)行 mount() 系統(tǒng)調(diào)用時(shí),會(huì)傳入 MS_MANDLOCK 標(biāo)記,從而將 super_block 結(jié)構(gòu)中的 s_flags 設(shè)置為 1,用來(lái)表示在這個(gè)文件系統(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í)行位。這種組合通常來(lái)說(shuō)是毫無(wú)意義的,系統(tǒng)用來(lái)表示該文件被加了強(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)用對(duì)該文件進(jìn)行加鎖或解鎖操作。

1.3. 共享模式鎖

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

租借鎖

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

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

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

		137 int lease_break_time = 45;
            

這個(gè)參數(shù)可以通過(guò)修改 /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)核中,所有類(lèi)型的文件鎖都是由數(shù)據(jù)結(jié)構(gòu) file_lock 來(lái)描述的,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 簡(jiǎn)單描述了 file_lock 結(jié)構(gòu)中的各個(gè)字段所表示的含義。


表 3. file_lock 數(shù)據(jù)結(jié)構(gòu)的字段
類(lèi)型 字段 字段描述
struct file_lock* fl_next 與索引節(jié)點(diǎn)相關(guān)的鎖列表中下一個(gè)元素
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)程的等待隊(duì)列
struct file * fl_file 指向文件對(duì)象
unsigned char fl_flags 鎖標(biāo)識(shí)
unsigned char fl_type 鎖類(lèi)型
loff_t fl_start 被鎖區(qū)域的開(kāi)始位移
loff_t fl_end 被鎖區(qū)域的結(jié)束位移
struct fasync_struct * fl_fasync 用于租借暫停通知
unsigned long fl_break_time 租借的剩余時(shí)間
struct file_lock_operations * fl_ops 指向文件鎖操作
struct lock_manager_operations * fl_mops 指向鎖管理操作
union fl_u 文件系統(tǒng)特定信息

一個(gè) file_lock 結(jié)構(gòu)就是一把“鎖”,結(jié)構(gòu)中的 fl_file 就指向目標(biāo)文件的 file 結(jié)構(gòu),而 fl_start 和 fl_end 則確定了該文件要加鎖的一個(gè)區(qū)域。當(dāng)進(jìn)程發(fā)出系統(tǒng)調(diào)用來(lái)請(qǐng)求對(duì)某個(gè)文件加排他鎖時(shí),如果這個(gè)文件上已經(jīng)加上了共享鎖,那么排他鎖請(qǐng)求不能被立即滿(mǎn)足,這個(gè)進(jìn)程必須先要被阻塞。這樣,這個(gè)進(jìn)程就被放進(jìn)了等待隊(duì)列,file_lock 結(jié)構(gòu)中的 fl_wait 字段就指向這個(gè)等待隊(duì)列。指向磁盤(pán)上相同文件的所有 file_lock 結(jié)構(gòu)會(huì)被鏈接成一個(gè)單鏈表 file_lock_list,索引節(jié)點(diǎn)結(jié)構(gòu)中的 i_flock 字段會(huì)指向該單鏈表結(jié)構(gòu)的首元素,fl_next 用于指向該鏈表中的下一個(gè)元素;當(dāng)前系統(tǒng)中所有被請(qǐng)求,但是未被允許的鎖被串成一個(gè)鏈表:blocked_list。fl_link 字段指向這兩個(gè)列表其中一個(gè)。對(duì)于被阻塞列表(blocked_list)上的每一個(gè)鎖結(jié)構(gòu)來(lái)說(shuō),fl_next 字段指向與該鎖產(chǎn)生沖突的當(dāng)前正在使用的鎖。所有在等待同一個(gè)鎖的那些鎖會(huì)被鏈接起來(lái),這就需要用到字段 fl_block,新來(lái)的等待者會(huì)被加入到等待列表的尾部。 此外,fl_type 表示鎖的性質(zhì),如讀、寫(xiě)。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 鎖是通過(guò)系統(tǒng)調(diào)用 fcntl() 創(chuàng)建的;而 FL_FLOCK 鎖是通過(guò)系統(tǒng)調(diào)用 flock()創(chuàng)建的(詳細(xì)內(nèi)容請(qǐng)參見(jiàn)后文中的介紹)。FL_FLOCK 鎖永遠(yuǎn)都和一個(gè)文件對(duì)象相關(guān)聯(lián),打開(kāi)這個(gè)文件的進(jìn)程擁有該 FL_FLOCK 鎖。當(dāng)一個(gè)鎖被請(qǐng)求或者允許的時(shí)候,內(nèi)核就會(huì)把這個(gè)進(jìn)程在同一個(gè)文件上的鎖都替換掉。FL_POSIX 鎖則一直與一個(gè)進(jìn)程以及一個(gè)索引節(jié)點(diǎn)相關(guān)聯(lián)。當(dāng)進(jìn)程死亡或者文件描述符被關(guān)閉的時(shí)候,這個(gè)鎖會(huì)被自動(dòng)釋放。

對(duì)于強(qiáng)制鎖來(lái)說(shuō),在 Linux 中,內(nèi)核提供了 inline 函數(shù) locks_verify_locked() 用于檢測(cè)目標(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 來(lái)檢測(cè)目標(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)系密切的兩個(gè)系統(tǒng)調(diào)用:flock() 和 fcntl()。勸告鎖既可以通過(guò)系統(tǒng)調(diào)用 flock() 來(lái)實(shí)現(xiàn),也可以通過(guò)系統(tǒng)調(diào)用 fcntl() 來(lái)實(shí)現(xiàn)。flock() 系統(tǒng)調(diào)用是從 BSD 中衍生出來(lái)的,在傳統(tǒng)的類(lèi) 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)對(duì)整個(gè)文件進(jìn)行加鎖,而不能實(shí)現(xiàn)記錄級(jí)的加鎖。系統(tǒng)調(diào)用fcntl() 符合 POSIX 標(biāo)準(zhǔn)的文件鎖實(shí)現(xiàn),它也是非常強(qiáng)大的文件鎖,fcntl() 可以實(shí)現(xiàn)對(duì)紀(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 里面沒(méi)有提到,其各自的意思如下所示:

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

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

fcntl()

fcntl() 函數(shù)的功能很多,可以改變已打開(kāi)的文件的性質(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)的三個(gè)取值 F_GETLK、F_SETLK 以及 F_SETLKW。這三個(gè)值均與 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 用來(lái)指明創(chuàng)建的是共享鎖還是排他鎖,其取值有三種:F_RDLCK(共享鎖)、F_WRLCK(排他鎖)和F_UNLCK(刪除之前建立的鎖);l_pid 指明了該鎖的擁有者;l_whence、l_start 和l_end 這些字段指明了進(jìn)程需要對(duì)文件的哪個(gè)區(qū)域進(jìn)行加鎖,這個(gè)區(qū)域是一個(gè)連續(xù)的字節(jié)集合。因此,進(jìn)程可以對(duì)同一個(gè)文件的不同部分加不同的鎖。l_whence 必須是 SEEK_SET、SEEK_CUR 或 SEEK_END 這幾個(gè)值中的一個(gè),它們分別對(duì)應(yīng)著文件頭、當(dāng)前位置和文件尾。l_whence 定義了相對(duì)于 l_start 的偏移量,l_start 是從文件開(kāi)始計(jì)算的。

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

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

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

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

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

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

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

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

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

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






文件鎖的使用樣例

為了使讀者更深入理解本文中介紹的內(nèi)容,下面我們給出了一個(gè)例子來(lái)詳細(xì)介紹文件鎖的具體用法。這個(gè)例子可以用來(lái)檢測(cè)所使用的文件是否支持強(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)介紹過(guò)了,其基本思想是在首先在父進(jìn)程中對(duì)文件加上寫(xiě)鎖;然后在子進(jìn)程中將文件描述符設(shè)置為非阻塞模式(第 69 行),然后對(duì)文件加讀鎖,并嘗試讀取該文件中的內(nèi)容。如果系統(tǒng)支持強(qiáng)制鎖,則子進(jìn)程中的 read() 系統(tǒng)調(diào)用(代碼中的第 81 行)會(huì)立即返回 EAGAIN;否則,等父進(jìn)程完成寫(xiě)文件操作之后,子進(jìn)程中的 read() 系統(tǒng)調(diào)用就會(huì)返回父進(jìn)程剛剛寫(xiě)入的前 5 個(gè)字節(jié)的數(shù)據(jù)。代碼中的幾個(gè) sleep() 是為了協(xié)調(diào)父進(jìn)程與子進(jìn)程之間的同步而使用的。

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





總結(jié)

Linux 的文件鎖在以共享索引節(jié)點(diǎn)共享文件的情況下設(shè)計(jì)的,文件鎖的實(shí)現(xiàn)可以使得不同用戶(hù)同時(shí)讀寫(xiě)同一文件的并發(fā)問(wèn)題得以解決。本文描述了 Linux 中各類(lèi)文件鎖的概念,使用場(chǎng)景,內(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),以及在對(duì)文件進(jìn)行加鎖時(shí)遇到的死鎖問(wèn)題等其他知識(shí),這里沒(méi)有做詳盡介紹,感興趣的讀者可以自行參考內(nèi)核源代碼。

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






參考資料

mandlock.c