詳解 fcntl 記錄上鎖。


fcntl函數原型:

#include <unsitd.h>
#include <fcntl.h>

int fcntl(int fd,int cmd, ...);

 

fcntl函數用於改變已打開文件性質,函數參數是可變參數:

  第一個參數 fd 表示打開文件的文件描述符,

  第二個參數是對應的功能指令,不同指令決定不同的后續參數。

    fcntl 基本用途包括以下五點:

    1.復制一個現有的描述符(cmd = F_DUPFD)。

    2.獲得/設置文件描述符標記(cmd = F_GETFD或F_SETFL)。

    3.獲得/設置文件狀態標志(cmd = F_GETOWN或F_SETOWN)。

    4.獲得/設置異步I/O所有權(cmd = F_GETOWN或F_SETOWN)。

    5.獲得/設置記錄鎖(cmd = F_GETLK、F_SETLK或F_SETLKW)。

  

本博客講述的是第五種功能,即獲得/設置記錄鎖:

 

I.第三個參數:

  凡是與記錄鎖有關時,必定會有fcntl.h 文件下的一個重要結構體:

struct flock{
  short l_type;  // F_RDLCK讀鎖,F_WRLCK寫鎖,F_UNLCK解鎖
  off_t l_start; // 鎖偏移的字節
  short l_whence;// 鎖開始的位置,SEEK_SET,SEEK_CURorSEEK_END
  off_t l_len;   // 鎖的范圍,為0表示從起始到文件末
  pid_t l_pid;   // 放入具有能阻塞當前進程的鎖(僅由F_GETLK返回)
};

  關於該結構體搭配函數,有以下幾點需要強調:

    1. l_start 是相對 l_whence 的偏移量,合起來表示鎖起始處,單位是字節。可以是文件尾端處或更后,但不可是文件初始之前。

    2. l_len 單位是字節,當是具體數字時是正整數,0時表示從起始位置到文件末尾。0時就表示之后在解鎖前加入的內容都在鎖區域。

    3. 通常對通篇文件上鎖的參數是 l_start = 0; l_whence = SEEK_SET ; l_len = 0;

  

 1 //給文件整體上鎖
 2 int lockfile(int fd)
 3 {
 4     struct flock fl;
 5     fl.l_type = F_WRLCK;
 6     fl.l_start = 0;
 7     fl.l_whence = SEEK_SET;
 8     fl.l_len = 0;
 9     return (fcntl(fd,F_SETLK,&fl));
10 }

 

    4.不同類型鎖之間的兼容性不同。

     我們可以很自然聯想到POSIX里的讀寫鎖。

  讀寫鎖的規則時:讀時可共享,寫時必獨占。讀鎖處於讀鎖住模式時遇到寫鎖加鎖請求時,系統會阻塞后續的讀模式上鎖請求。

  記錄鎖下不同鎖的規律:

          

   可以發現 記錄鎖讀寫鎖 還是有一定差異的,其實兩者本就是兩種東西,因為讀寫鎖是一個信號量,二記錄鎖中的讀鎖和寫鎖是兩個事物。

     當一個給定字節上已經有一把或多把讀鎖時,此時有一個其他一個進程想加個寫鎖,是不會被允許的。

      但是必須聲明:這個圖中的兼容性只適用於不同進程提出的鎖請求,但是並不適用於單個進程提出的多個請求。

            比如,一個進程已經對一段給定字節上了一把鎖,如果再在之后同樣區域上鎖,記住!新鎖將代替老鎖

    還需謹記:加讀鎖時,該描述符必須為讀打開;加寫鎖時,該描述符必須是寫打開。

II. 各自意義:

  F_GETLK——

    判斷對應文件中的結構體中給定區間中是否有對應結構體中的鎖。

     比如我將結構體置為struct flock _flock;  >    _flock.l_type = F_WRLCK, _flock.l_start = 0, _flock.l_whence = SEEK_SET, _flock.l_len = 0;

     即表示我在請求獲取fd文件描述符對應的文件是否全區域有加寫鎖。

     如果寫鎖存在,就會阻止我fcntl函數進行,並且會替換我給的結構體指針中的結構體內容。返回當前鎖的信息,並且在l_pid 處返回鎖的持有者。

     如果寫鎖不存在,則將 _flock.l_type改為F_UNLCK ,其余不變。

  這里寫一個用例:

    這里編寫一個函數,用於測試一把鎖,可以很好體現F_GETLK的作用。

 1 //測試一把鎖
 2 pid_t lock_test(int fd,int type,off_t offset,int whence,off_t len)
 3 {
 4     struct flock lock;
 5     lock.l_type = type; /*F_RDLCK or F_WRLCK*/
 6     lock.l_start = offset;
 7     lock.l_whence = whence; /*SEEK_SET,SEEK_CUR or SEEK_END*/
 8     lock.l_len = len;
 9 
10     if(fcntl(fd,F_GETLK,&lock) < 0)
11         err_sys("fcntl error");
12 
13     if(lock.l_type == F_UNLCK) /*false*/
14         return(0);
15     return (lock.l_pid); /*true*/
16 }

 

 

  F_SETLK

    F_SETLK相對於F_GETLK就要常用很多。其目的顧名思義就是對文件指定區域加鎖/解鎖。

      記住這一切都要滿足記錄鎖的兼容性規則,當你回想起記錄鎖的兼容性規則時一定要記得,它適用於不同進程,而不適用於同一進程。

      當加鎖不成功時,即其上面本來就有規則不允許的條件的鎖時,就會出錯返回,errno會設置為EACCES或EAGAIN。

      EACCES:無存取權限。

      EAGAIN:非阻塞下調用阻塞操作。(非阻塞socket編程會遇到。)

 

  F_SETLKW  

      F_SETLKW即F_SETLK的阻塞等待版,W即wait。

      當fcntl 的請求不能滿足時,就會進入休眠,當請求創建的鎖可用時會被信號中斷休眠,進程就會被喚醒。

  這里再看一個實例:

    編寫一個函數來請求/釋放一個鎖,並生產對應的宏。

 1 #define read_lock(fd,offset,whence,len)\
 2         lock_reg((fd),F_SETLK,F_RDLCK,(offset),(whence),(len))
 3 #define readw_lock(fd,offset,whence,len)\
 4         lock_reg((fd),F_SETLKW,F_RDLCK,(offset),(whence),(len))
 5 #define write_lock(fd,offset,whence,len)\
 6         lock_reg((fd),F_SETLK,F_WRLCK,(offset),(whence),(len))
 7 #define writew_lock(fd,offset,whence,len)\
 8         lock_reg((fd),F_SETLKW,F_WRLCK,(offset),(whence),(len))
 9 #define un_lock(fd,offset,whence,len)\
10         lock_reg((fd),F_SETLK,F_UNLCK,(offset),(whence),(len))
11 
12 //請求和釋放一把鎖
13 int lock_reg(int fd,int cmd,int type,off_t offset,int whence,off_t len)
14 {
15     struct flock lock;
16     lock.l_type = type;
17     lock.l_start = offset;
18     lock.l_whence = whence;
19     lock.l_len = len;
20 
21     return (fcntl(fd,cmd,&lock));
22 }

   

  關於獲取/設置/釋放鎖有以下兩點需要注意:

     1.F_GETLK 和 F_SETLK(或F_SETLKW)並非一個原子操作,所以要視情況而寫代碼。

     2.當你在一塊區域中上鎖后,如果要釋放這塊區域中的中間一塊區域(不含邊界),那么系統會自動將一個鎖分成兩個鎖。

      如果再將剛剛釋放的區域重新加上本來的鎖,系統就會將三個區域合並。

    

 

 

III.鎖無法繼承,鎖的釋放。

  鎖的繼承:

   讓我們再回顧一下,子進程生成的時候發生了什么?(fork()為例)

      1.父進程給子進程分配pid和PCB。

      2.復制父進程的環境。

      3.給子進程分配地址空間和資源。(vfork() 不執行3,4兩步。不建議用,函數不成熟)

      4.復制父進程的地址信息。

     那么子進程會獲得什么?

      1.父進程的持有者和允許使用者以及其屬性。

      2.父進程的環境。

      3.父進程分配給其的進程上下文。(比如堆棧信息)。

      4.自己獨立的一份內存。

      5.nice值和進程調度級別,其中進程調度級別在 多級反饋隊列中尤為體現。(這是windows和linux下的進程調度置換算法)。

      6.根目錄和工作目錄。

      7.打開的文件描述符。(父子進程間溝通關鍵)。

       等....

     那么父進程不會給子進程什么?(三點)

      1.父進程的父進程ID

      2.父進程的阻塞信號和計時器。

      3.父進程的文件鎖。

 

     OK,那么接下來我們就可以得到結論了:

      子進程是無法繼承父進程所設置的鎖的。

      做個很簡單的例子,當我父進程給一個文件上寫鎖后,執行fork(),如果子進程也能繼承文件鎖,那其也可以在文件中肆意寫,這與記錄鎖的兼容性規則相悖。

 

   鎖的釋放:

     我們都知道,進程通信中IPC對象的持續性分為三種:隨進程持續性,隨內核持續性,隨文件持續性。

     記錄鎖是隨進程持續性的IPC對象,鎖與進程和文件兩方面有關。

      即一:當你的進程結束了,其對文件上的所有鎖也就都被釋放了。

       二:當你的文件描述符被關閉了,其上面的鎖也就都會被銷毀。

         這個有點不好理解,並不是文件被關閉,才會銷毀鎖,即當文件描述符的引用計數在變少時,其上的鎖就會被銷毀。(前提是這些鎖是被執行關閉文件描述符操作的進程上的)

 

   看下列代碼:  

 1 fd1 = open("file1",...);
 2 read_lock(fd1,...);//上讀鎖
 3 fd2 = dup(fd1);
 4 close(fd2);//鎖會被釋放
 5 
 6 
 7 //////////////////////////
 8 
 9 fd1 = open("file1",...);
10 read_lock(fd1,...);//上讀鎖
11 fd2 = open("file2",...);
12 close(fd2);//鎖會被釋放

 

 

 

  

IV.需要獨立出來講的文件尾端加鎖

    看下列代碼:

1 writew_lock(fd,0,SEEK_END,0);//從內容末到文件末阻塞上讀鎖
2 write(fd,buf,1);
3 un_lock(fd,0,SEEK_END,0);//從內容末到文件末解鎖
4 write(fd,buf,1);

 

  看起來並沒有什么問題,但其實上述中的兩個內容末並不是同一位置,我們在上鎖后還進行了寫入操作導致內容后移,所以當我們上文件末鎖操作后還有解鎖需求時,要記得相對偏移量。

      

 

X.關於強制性鎖和建議性鎖。

   建議性鎖:

     它的規則有點類似讀寫鎖,它可以限制訪問數據庫的函數,但對於對數據庫擁有寫權限的任何其它進程對數據庫文件提出的寫請求無法駁回。

 

  強制性鎖:

     強制性鎖又叫強迫方式鎖。

     當一個進程視圖讀、寫一個強制性鎖起作用的文件,且讀、寫部分又被別的進程加了讀或寫鎖。之后的結果要綜合三種情況:

       1.操作類型(讀or寫)2.其他進程設置鎖的類型(讀鎖or寫鎖)3.文件描述符類型(阻塞or非阻塞)

           

 

  參考文獻:《UNIX環境高級編程》

       https://blog.csdn.net/qingkongyeyue/article/details/53914885

       https://www.cnblogs.com/fengkang1008/p/4725514.html (寫的挺好)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM