1. 程式人生 > >fcntl與檔案鎖【轉】

fcntl與檔案鎖【轉】

第一節Unix支援的檔案鎖技術介紹

Unix系統允許多個程序同時對一個檔案進行讀寫,雖然每一個read或write呼叫本身是原子的,但核心在兩個讀寫操作之間並沒有加以同步,因此當一個程序多次呼叫read來讀檔案時,其它程序有可能在兩次read之間改變該檔案,造成檔案資料的隨機性衝突。為解決此類併發程序對共享檔案的訪問控制問題,Unix系統設計了檔案鎖技術。

1.1 讀鎖與寫鎖

Unix系統對檔案加鎖有兩種粒度:檔案鎖和記錄鎖,檔案鎖用來鎖定整個檔案,而記錄鎖可以鎖定檔案的部分割槽域甚至一個位元組,程序通過為檔案設定多個記錄鎖,可以實現檔案中不同區域的資料的讀寫同步,因此記錄鎖最為常見。記錄鎖根據訪問方式的不同,又分為讀鎖和寫鎖。讀鎖允許多個程序同時進行讀操作,也稱共享鎖。檔案加了讀鎖就不能再設定寫鎖,但仍允許其他程序在同一區域再設定讀鎖。寫鎖的主要目的是隔離檔案使所寫內容不被其他程序的讀寫干擾,以保證資料的完整性。寫鎖一旦加上,只有上鎖的人可以操作,其他程序無論讀還是寫只有等待寫鎖釋放後才能執行,故寫鎖又稱互斥鎖,寫鎖與任何鎖都必須互斥使用。

是否滿足請求

當前加上的鎖

共享鎖(讀鎖)

排他鎖(寫鎖)

共享鎖(讀鎖)

排他鎖(寫鎖)

表 1. 鎖間的相容關係

1.2 建議鎖和強制鎖

Unix檔案鎖根據實現機制的不同,又可分為建議鎖和強制鎖兩種型別。建議鎖由應用層實現,核心只為使用者提供程式介面,並不參與鎖的控制和協調,也不對讀寫操作做內部檢查和強制保護,其工作原理類似於訊號量機制,使用者首先定義並初始化特定的鎖,再根據程序間的的關係來呼叫相應的鎖操作,只有所有程序都嚴格遵循鎖的使用規則,才能有效防止同步錯誤,否則,只要有一個例外,整個鎖的功能就會被破壞。

強制鎖則由核心強制實施,每當有程序呼叫read或write時,核心都要檢查讀寫操作是否與已加的鎖衝突,如果衝突,阻塞方式下該程序將被阻塞直到鎖被釋放,非阻塞方式下系統將立即以錯誤返回。顯然,使用強制鎖來控制對已鎖檔案或檔案區域的訪問,是更安全可靠的同步形式,適用於網路連線、終端或串並行埠之類須獨佔使用的裝置檔案,因為對使用者都可讀的檔案加一把強制讀鎖,就能使其他人不能再寫該檔案,從而保證了裝置的獨佔使用。由於強制鎖執行在核心空間,處理機從使用者空間切換到核心空間,系統開銷大,影響效能,所以應用程式很少使用。建議鎖開銷小,可移植性好,符合POSIX標準的檔案鎖實現,在資料庫系統中應用廣泛,特別是當多個程序交叉讀寫檔案的不同部分時,建議鎖有更好的並行性和實時性。        

檔案存在鎖的型別

阻塞描述符,試圖

非阻塞描述符,試圖

read

write

read

write

讀鎖

允許

阻塞

允許

EAGAIN錯誤

寫鎖

阻塞

阻塞

EAGAIN錯誤

EAGAIN錯誤

表2強制性鎖對其他程序讀、寫的影響

系統

建議性鎖

強制性鎖

Linux2.4.22

支援

支援

Solaris 9

支援

支援

Mac OS X 10.3

支援

不支援

FreeBSD 5.2.1

支援

不支援

表3 不同系統的支援情況

第二節 檔案鎖的總體實現和設定

2.1 核心相關的資料結構

Unix檔案鎖是在共享索引節點共享檔案的情況下設計的,因此與鎖機制相關的核心資料結構主要有虛擬索引節點(V-node)、系統開啟檔案表、程序開啟檔案表等資料表項。其中V-node中的索引節點(i-node)中含有一個指向鎖鏈表的首指標,該鎖鏈表主要由對檔案所加的全部的鎖通過指標鉤鏈而成。只要程序為檔案或區域加鎖成功,核心就建立鎖結構file lock,並根據使用者設定的引數初始化後將其插入鎖鏈表flock中。flock是鎖存在的唯一標誌,也是核心感知、管理及控制檔案鎖的主要依據,其結構和成員參見圖1中的struct flock。圖1描述了程序為開啟的檔案設定檔案鎖的核心相關表項及各資料結構之間的關聯,從中可以看出,鎖同時與程序和檔案相關,當程序終止或檔案退出時即使是意外退出,程序對檔案所加的鎖將全部釋放。

圖1 檔案鎖相關資料結構示意

2.2 鎖的使用

Unix提供了3個檔案鎖相關的系統呼叫:flock、lockf和fcntl。其中fcntl功能強大,使用靈活,移植性好,既支援建議式記錄鎖也支援強制式記錄鎖,函式原型定義為:int fcntl(int fd,int cmd,int arg),引數fd表示需要加鎖的檔案的描述符;引數cmd指定要進行的鎖操作,如設定、解除及測試鎖等;引數arg是指向鎖結構flock的指標。

無論哪種型別的檔案鎖,使用的流程是確定的:首先以匹配的方式開啟檔案,然後各程序呼叫上鎖操作同步對已鎖區域的讀寫,讀或寫完成時再呼叫解鎖操作,最後關閉檔案。鎖操作程式碼的編寫方法基本類似,首先形成適當的flock結構,然後呼叫fcntl完成實際的鎖操作。為避免每次分配flock並填充各成員的重複工作,可以預先定義專門的函式來完成頻繁呼叫的上鎖和解鎖操作。下列程式碼就是為一個檔案設定讀鎖的例項。

int set_read_lock(intfd,  int cmd,  int type,  off_t start,  int whence,  off_t len)

{

  struct flock lock;

  lock.l_type=F_RDLCK;

  lock.l_pid=getpid();

  lock.l_whence=SEEK_SET;

  lock.l_start=0;

  lock.l_len=0;

  return (fcntl(fd, F_SETLKW, &lock));

}

2.3 強制性鎖的設定

強制性鎖的設定需要兩個步驟:第一,對要加鎖的檔案需要設定相關許可權,開啟set-group-ID位,關閉group-execute位。第二,讓linux支援強制性鎖,需要執行命令,掛載檔案系統, mount /dev/sda1 /mnt –o mand。

# mount -o mand/dev/sdb7 /mnt

# mount | grep mnt

/dev/sdb7 on /mnt typeext3 (rw,mand)

# 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

附件中的測試程式(lock.c)可以測試是否支援強制性鎖。

2.4 設定強制性鎖注意問題

在設定強制性鎖注意的問題:幾乎所有的操作都需要root許可權。

1.      首先,su到root許可權下,輸入:mount 可以看到掛載的所有內容,如果所用磁碟存在其他掛載路徑,要讓mand成功,必須是:所用磁碟(sda1)被掛載的方式有mand存在。

比如存在: /dev/sda1 on /boot type ext3(rw)

           /dev/sda1 on /opt/327/XXX typeext3(rw, mand)

則不成功。

需要:  /dev/sda1 on /boot typeext3(rw, mand)

        /dev/sda1 on /opt/327/XXX type ext3(rw,mand)

2.mount /dev/sda1 /opt/327/XXX/ -o mand,把sda1磁碟掛載到/opt/327/XXX/目錄下,此時,把新檔案存在opt/327/XXX/下,實際是存在sda1磁碟中。

3. 此時/opt/327/XXX/的許可權變成了root,只用root許可權的使用者才能修改這個資料夾下的內容。

第三節    三種加鎖方法詳細介紹和linux內部實現

3.1 Posix lock加鎖的方法(fcntl):

3.1.1使用介紹:

fcntl() 函式的功能很多,可以改變已開啟的檔案的性質,本文中只是介紹其與獲取/設定檔案鎖有關的功能。fcntl() 的函式原型如下所示:

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

其中,引數 fd 表示檔案描述符;引數 cmd 指定要進行的鎖操作,由於 fcntl() 函式功能比較多,這裡先介紹與檔案鎖相關的三個取值 F_GETLK、F_SETLK 以及 F_SETLKW。這三個值均與 flock 結構有關。flock 結構如下所示:

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 可以用於設定檔案狀態標識。

3.1.2       Posix lock 內部機制:

1)   呼叫fcntl(fd, F_SETLK, &lock), fcntl()方法呼叫核心的do_fcntl()方法(linux/fs/fcntl.c)

static long do_fcntl(unsigned int fd, unsigned intcmd, unsigned long arg, struct file * filp)

{       

switch (cmd) {

                    .....

               case F_GETLK: /*Posix Lock 操作*/                        err = fcntl_getlk(fd, (struct flock *) arg);                        break;                 case F_SETLK:                 case F_SETLKW:                        err = fcntl_setlk(fd, cmd, (struct flock *) arg);                        break;                 ... }

2)   do_fcntl()裡呼叫fcntl_setlk()(linux/fs/flock.c)

int fcntl_setlk(unsigned int fd, struct file *filp,unsigned int cmd, struct flock __user *l)

{

  structfile_lock *file_lock = locks_alloc_lock();

       struct flock flock;

        ......

        //把把客戶傳來的flock __user l拷貝為flock

if (copy_from_user(&flock, l, sizeof(flock)))

               goto out;

        //把客戶傳來的flock的鎖l變為file_lock

        error =flock_to_posix_lock(filp, file_lock, &flock);

        ......

        //阻塞的設定

  if (cmd ==F_SETLKW) {

         file_lock->fl_flags|= FL_SLEEP;

  }

  error = -EBADF;

  //設定不同型別的鎖資訊

  switch(flock.l_type) {

  case F_RDLCK:

         if(!(filp->f_mode & FMODE_READ))

                gotoout;

         break;

  case F_WRLCK:

         if(!(filp->f_mode & FMODE_WRITE))

                gotoout;

         break;

  case F_UNLCK:

         break;

  default:

         error =-EINVAL;

         goto out;

//鎖檔案filp

error = do_lock_file_wait(filp, cmd, file_lock);

spin_lock(¤t->files->file_lock);

f = fcheck(fd);

spin_unlock(¤t->files->file_lock);

......

}

3)    fcntl_setlk()呼叫do_lock_file_wait()

static int do_lock_file_wait(struct file *filp, unsignedint cmd,  struct file_lock *fl)

{

  ......

  error =security_file_lock(filp, fl->fl_type);

  ......

  //關鍵

  error =vfs_lock_file(filp, cmd, fl, NULL);

......

}

4)    do_lock_file_wait()呼叫vfs_lock_file()

int vfs_lock_file(struct file *filp, unsigned int cmd,struct file_lock *fl, struct file_lock *conf)

{

  if (filp->f_op&& filp->f_op->lock)

         returnfilp->f_op->lock(filp, cmd, fl);

  else

         //關鍵

         returnposix_lock_file(filp, fl, conf);

}

5)   vfs_lock_file()呼叫posix_lock_file()

int posix_lock_file(struct file *filp, struct file_lock*fl, struct file_lock *conflock)

{

  return__posix_lock_file(filp->f_path.dentry->d_inode, fl, conflock);

}

6)    posix_lock_file()呼叫__posix_lock_file()

__posix_lock_file()是真正鎖檔案的函式,它具有檢測衝突鎖的功能。具體程式碼看locks.c中的__posix_lock_file()。

大致流程是這樣:

遍歷inode->i_flock,找是POSIX鎖且有衝突的鎖(用posix_locks_conflict()函式),若存在或者有阻塞標誌或者死鎖了,做一定處理退出;若不存在這樣的鎖,就可以鎖檔案。再次遍歷inode->i_flock,找到屬於自己程序的鎖,如果檢測到了鎖並判斷鎖型別,處理記錄相交的部分,設定鎖屬性,最後按照一定方式插入鎖;如果未檢測到鎖,則插入鎖。

3.1.3兩種鎖檢測衝突:

· 建議鎖(Advisory lock)

系統預設的鎖,檢測兩鎖是否衝突的函式:

posix_locks_conflict(structfile_lock *caller_fl, struct file_lock *sys_fl)函式過程:

先判斷兩把鎖是否屬於同一程序,自己不會和自己衝突。

如果它們兩是不同程序的鎖,判斷是否有鎖定區域重合。

· 強制性鎖(Mandatory lock)

檢測強制性鎖衝突的函式,滿足條件則加鎖,locks_mandatory_area()函式過程:

函式裡呼叫了posix_locks_conflict()用於判斷衝突鎖,呼叫__posix_lock_file()函式用於加鎖。

3.1.3 說明一個問題:如果檔案被加了強制性鎖,為什麼系統呼叫read(),write()就會受到限制?

看系統呼叫的read的實現方法:

1)   核心函式 sys_read() 是 read 系統呼叫在該層的入口點(read_write.c):

SYSCALL_DEFINE3(read, unsigned int, fd,char __user *, buf, size_t, count)

2)   Sys_read()裡呼叫vfs_read()(read_write.c)

3)    vfs_read()裡呼叫rw_verify_area(READ,file, pos, count);

4)    rw_verify_area()呼叫了mandatory_lock()和locks_mandatory_area()(mandatory_lock在fs.h中,locks_mandatory_area在locks.c中)

5)    mandatory_lock()呼叫了IS_MANDLOCK()和__mandatory_lock(),其中,IS_MANDLOCK()判斷檔案系統是否有MS_MANDLOCK,__mandatory_lock()判斷檔案是否setgid和去掉組執行位

6)    locks_mandatory_area()檢查是否有鎖衝突,若有衝突,讀操作出錯。

結果:所以系統呼叫read就不能在有強制性鎖的情況下讀檔案。Write和open是類似的道理。

3.2   flock的加鎖方法(flock):

3.2.1 使用介紹:

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() 所加的鎖會對整個檔案起作用。

說明:共享模式強制鎖可以用於某些私有網路檔案系統,如果某個檔案被加上了共享模式強制鎖,那麼其他程序開啟該檔案的時候不能與該檔案的共享模式強制鎖所設定的訪問模式相沖突。但是由於可移植性不好,因此並不建議使用這種鎖。

3.2.2       flock的內部機制:

         flock和posix lock都共享一組機制,系統呼叫fcntl() 符合 POSIX 標準的檔案鎖實現,功能比flock強大,可以支援記錄鎖。flock() 系統呼叫是從 BSD 中衍生出來的,只能支援整個檔案的鎖。

flock_lock_file()與posix lock機制類似,詳細看程式碼(locks.c)

3.3   lease鎖的方法:

3.3.1使用說明:

系統呼叫 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 就可以刪除這個租借鎖。不管對該租借鎖減短租約或者乾脆刪除的操作是程序自願的還是核心強迫的,只要被阻塞的系統呼叫還沒有被髮出該呼叫的程序解除阻塞,那麼系統就會允許這個系統呼叫執行。即使被阻塞的系統呼叫因為某些原因被解除阻塞,但是上面對租借鎖減短租約或者刪除這個過程還是會執行的。

需要注意的是,租借鎖也只能對整個檔案生效,而無法實現記錄級的加鎖。

採用強制鎖之後,如果一個程序對某個檔案擁有寫鎖,只要它不釋放這個鎖,就會導致訪問該檔案的其他程序全部被阻塞或不斷失敗重試;即使該程序只擁有讀鎖,也會造成後續更新該檔案的程序的阻塞。為了解決這個問題,Linux 中採用了一種新型的租借鎖。

當程序嘗試開啟一個被租借鎖保護的檔案時,該程序會被阻塞,同時,在一定時間內擁有該檔案租借鎖的程序會收到一個訊號。收到訊號之後,擁有該檔案租借鎖的程序會首先更新檔案,從而保證了檔案內容的一致性,接著,該程序釋放這個租借鎖。如果擁有租借鎖的程序在一定的時間間隔內沒有完成工作,核心就會自動刪除這個租借鎖或者將該鎖進行降級,從而允許被阻塞的程序繼續工作。

系統預設的這段間隔時間是 45 秒鐘,定義如下:

137 int lease_break_time = 45;

這個引數可以通過修改 /proc/sys/fs/lease-break-time 進行調節(當然,/proc/sys/fs/leases-enable 必須為 1 才行)。