它是唯一一個進程終止時內核自動清理的同步鎖。這是一種讀寫鎖的擴展類型,他可用於有親緣關系或無親緣關系的進程之間共享某個文件的讀或寫,被鎖住的文件通過文件描述符訪問,執行上鎖的操作時fcntl,這種類型的鎖通常在內核中維護,其屬主是由屬主的進程ID標識,這就說明了鎖可用於不同進程之間上鎖,而不是統一進程內的不同線程上鎖。
應用程序會指定文件中待上鎖或解鎖的部分字節范圍,這個字節范圍會跟同一文件內一個或多個邏輯記錄有關。
粒度指的是被鎖住的對象的大小,對於posix記錄上鎖來說,粒度就是單個字節,通常情況下粒度越小,允許同時訪問的用戶數也就越多,fcntl僅僅是個庫函數而不是系統調用,使用fcntl完成lockf的實現
#include<fcntl.h> int fcntl(int fd, int cmd ,.../*struct flock* arg*/); //返回:成功取決與cmd,失敗返回-1
返回值,與命令有關。
- 出錯,所有命令都返回-1。
- 成功則返回某個其他值。下列三個命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一個返回新的文件描述符,第二個返回相應標志,最后一個返回一個正的進程ID或負的進程組ID。
當fcntl用於管理文件記錄鎖的操作時,第三個參數指向一個struct flock *lock的結構體
struct flock { short l_type;/*鎖的類型F_RDLCK:為了獲得一把共享鎖文件必須以“讀”或“讀/寫”方式打開。F_WRLCK:為了獲得一把寫(獨占)文件也必須以“讀”或“讀/寫”方式打開。F_UNLCK:它用於把一個鎖定的區域解鎖*/ short l_whence; /*偏移量的起始位置:SEEK_SET,SEEK_CUR,SEEK_END*/ off_t l_start; /*加鎖的起始偏移*/ off_t l_len; /*上鎖字節即從偏移開始的連續字節數,o means until end-of-file即從起始偏移到文件偏移的最大值*/ pid_tl_pid; /*鎖的屬主進程ID,pid returned by F_GETLK */ };
跟lseek一樣,起始字節偏移是作為一個相對偏移(l_start成員)伴隨解釋(l_whence成員)指定的,l_whence成員指定的三個值
- SEEK_SET:l_start相對與文件的開頭解釋
- SEEK_CUR:l_start相對於文件的當前字節偏移(即當前讀寫指針位置)解釋
- SEEK_END:l_start相對於文件的末尾解釋
鎖住整個文件:
- 指定l_whence成員為SEEK_SET,l_start為0,l_len為0。(最常用,只需要調用一個函數而不是兩個)
- 使用lseek把讀寫指針定位到文件的頭,然后指定l_whence成員為SEEK_CUR,l_start為0,l_len為0
cmd把fcntl分為五種操作:
- 復制復制一個現有的描述符。
- F_DUPFD:與舊的文件描述符共同指向同一個文件。如果對象是文件(file)的話,返回一個新的描述符與arg共享相同的偏移量(offset),新的文件描述符與原來的文件描述符操作符一樣的某對象的引用,相同的訪問模式(讀,寫或讀/寫) ,相同的文件狀態標志(如:兩個文件描述符共享相同的狀態標志);原來的文件描述符與新的文件描述符結合在一起的close-on-exec標志被設置成交叉式訪問execve(2)的系統調用。fcnlt(oldfd, F_DUPFD, 0) ==dup2(oldfd, newfd)
- F_DUPFD:與舊的文件描述符共同指向同一個文件。如果對象是文件(file)的話,返回一個新的描述符與arg共享相同的偏移量(offset),新的文件描述符與原來的文件描述符操作符一樣的某對象的引用,相同的訪問模式(讀,寫或讀/寫) ,相同的文件狀態標志(如:兩個文件描述符共享相同的狀態標志);原來的文件描述符與新的文件描述符結合在一起的close-on-exec標志被設置成交叉式訪問execve(2)的系統調用。fcnlt(oldfd, F_DUPFD, 0) ==dup2(oldfd, newfd)
- 獲得/設置文件描述符標記
- F_GETFD:讀取文件描述符close-on-exec標志
- F_SETFD:將文件描述符close-on-exec標志設置為第三個參數arg的最后一位類似FD_CLOEXEC。如果返回值和FD_CLOEXEC進行與運算結果是0的話,文件保持交叉式訪問exec(),否則如果通過exec運行的話,文件將被關閉(arg被忽略)。fcntl(fd, F_SETFD, 0);//關閉fd的close-on-exec標志
- 獲得/設置文件狀態標記
- F_GETFL:獲取文件打開方式的標志;當執行F_SETLK時fcntl函數返回一個錯誤時,導致該錯誤的某個鎖的信息可能由F_GETLK命令返回,從而允許我們確定哪個進程鎖着了請求的文件區,及上鎖的方式,但是也可返回該文件區已經解鎖的信息,因為在F_SETLK和F_GETLK之間該文件可能解鎖。
- F_SETFL:設置文件打開方式為arg指定方式;O_DIRECT :最小化或去掉reading和writing的緩存影響.系統將企圖避免緩存你的讀或寫的數據.如果不能夠避免緩存,那么它將最小化已經被緩存了的數 據造成的影響.如果這個標志用的不夠好,將大大的降低性能;O_ASYNC:當I/O可用的時候,允許SIGIO信號發送到進程組,例如:當有數據可以讀的時候
- 獲得/設置異步I/O所有權
- F_GETOWN或F_SETOWN:設置獲取異步I/O所有權(SIGIO,SIGURG)0;s設置當前進程id時如果參數為負,表示該值的絕對值的一個進程組id;取得當前正在接收SIGIO或者SIGURG信號的進程id或進程組id,進程組id返回成負值(arg被忽略)
- 獲得/設置記錄鎖(此時必須使用第三個參數)
- F_SETLK:獲取(l_type為_RDLCK或F_WRLCK)或釋放(l_type為F_UNLCK)鎖。如果無法將鎖授予進程,該函數調用立刻返回EACCESS或EAGAIN而不阻塞
- F_SETLKW:與上一個類似,不過不同的是,當無法將所請求的鎖授予調用進程,調用線程將阻塞到該鎖能夠授予為止,該命令會等待相沖突的鎖被釋放(最后個字母W為wait意思)
- F_GETLK:檢查由arg指向的鎖以確定是否由某個已存在的鎖會妨礙將新鎖授予調用進程,如果沒有這樣的鎖存在,arg指向的flock結構的l_type成員就被設置為F_UNLCK,否則,關於這個已存在的鎖的信息將在由arg指向的flock結構中返回也就是該結構的內容由fcntl復寫,包括持有該鎖的進程ID。如果存在一個或多個鎖與希望設置的鎖相互沖突,則fcntl返回其中的一個鎖的flock結構。在F_GETLK命令后發送F_SETLK不是一個原子操作。但是此命令存在的原因是執行F_SETLK返回錯誤時導致該錯誤的信息可有F_GETLK返回。
注意:
- 多進程情況下:每個進程可以在該字節區域上設置不同的讀鎖。但給定的字節上只能設置一把寫鎖,並且寫鎖存在就不能再設其他任何鎖,且該寫鎖只能被一個進程單獨使用
- 一個進程可以對文件的某個特定的字節范圍多次發送F_SETLK or F_SETLKW,每次是否成功取決於其他進程當時是否鎖住該字節范圍及鎖的類型,與本進程先前是否鎖着該字節范圍無關,也就是說F_SETLK or F_SETLKW會覆蓋先前執行同一字節的兩個命令,文件能否讀寫相應的記錄與是否被其他進程鎖無關(勸告性上鎖)也就是說,一個進程可能訪問已經被另一個進程獨占的鎖住文件的記錄,但彼此協作的進程應該不去訪問
- 調用進程已有的針對同一字節的鎖不會妨礙它獲取新鎖,因為同一進程內,后執行的鎖的命令會覆蓋先前的命令。例,對一個進程的同一字節范圍先后執行F_SETLK(l_type=F_WRLCK)和F_GETLK(l_type=F_RDLCK),這兩個命令間無其他進程的干擾,且他們都執行成功,那么F_GETLK返回l_type成員是F_UNLCK
- 針對同一文件的任意字節,最多有一把類型的鎖(讀入或寫出鎖),而且給定的一個字節可以有多個多讀出鎖但只能有一個寫入鎖;當一個文件不是打開讀的,為其請求讀出鎖會失敗,寫入鎖同理
- 打開文件的進程來說,文件描述符關閉或進程終止,與文件關聯的鎖都被刪除。鎖不能由fork子進程繼承
- 記錄鎖不應與標准I/O一塊使用。因為該函數庫會執行內部緩沖。當某個文件需要上鎖時,為避免占個位問題,應該對它使用read、write。
空鎖
設文件描述符狀態
在修改文件描述符標志或文件狀態標志時先要取得現在的標志值,然后按照希望修改它,最后設置新標志值。不能只是執行F_SETFD或F_SETFL命令,這樣會關閉以前設置的標志位。
flags = fcntl(sockfd, F_GETFL, 0); //獲取文件的flags值。 fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); //設置成非阻塞模式; flags = fcntl(sockfd,F_GETFL,0); fcntl(sockfd,F_SETFL,flags&~O_NONBLOCK); //設置成阻塞模式;
勸告性上鎖
POSIX記錄上鎖也稱勸告性上鎖。共含義是內核維護着已由各個進程上鎖的所有文件的正確信息,它不能防止一個進程寫由另一個進程讀鎖定的某個文件,也不能防止一個進程讀已由另一個進程寫鎖定的文件。一個進程能夠無視勸告性鎖而寫一個讀鎖文件,或讀一個寫鎖文件,前提時該進程有讀或寫該文件的足夠權限。
勸告性上鎖對於協作進程足夠。如網絡編程中的協程,這些程序訪問如序列號文件的共享資源,而且都在系統管理員的控制下,只要包含該序列號的真正文件不是任何進程都可寫,那么在該文件被鎖住期間,不理會勸告性鎖的進程隨意進程無法訪問寫它。
強制性上鎖
另一種類型的記錄性上鎖。對於強制性上鎖,內核檢查每個read和write請求,以驗證其操作不會干擾由某個進程持有的某個鎖,對於通常的時阻塞描述符,與某個強制性鎖沖突的read和write將把調用進程投入睡眠,直到該鎖釋放為止,對於非阻塞描述符,與某個強制鎖沖突的read和write將導致他們返回一個EAGAIN。
為對某個特定的文件實行強制鎖應滿足:
- 組成員執行位必須關掉
- SGID位必須打開
打開某個文件的SUID位而不打開他的用戶執行位是沒有意義的,同樣打開SGID位而不打開組成員的執行位也是沒有意義的,因此這種方式的強制性上鎖不會影響任何現有的用戶軟件,強制性上鎖不需要新的系統調用。
在支持強制性上鎖的系統上,ls輸出l或L以指示相應的文件是否強制性上鎖。chmod接受l這個指示給某個文件以啟用強制性上鎖。
- linux 內核會阻塞其他進程的 IO 請求
- 可以通過刪除鎖文件繞過
與進程關聯
- 當一個進程終止時,所建立的所有鎖全部被釋放
- 關閉一個文件描述符,會釋放對該文件的所有鎖,包括對其他指向相同文件的文件描述符加的鎖
- fork 產生的子進程並不繼承父進程所設置的鎖
- 在執行
exec
后,新程序可以繼承原程序的鎖(如果對fd設置了close-on-exec,則exec前會關閉fd,相應文件的鎖也會被釋放)
與文件描述符關聯
- 當一個文件描述符及其所有副本(包括子進程繼承的和
dup
的)關閉時,才會釋放對其建立的鎖 fork
的子進程由於繼承了文件描述符,所以也繼承了其上的鎖- 子進程對繼承的文件描述符上的鎖進行修改/解鎖,會影響到父進程的鎖(對於
dup
出的副本同樣試用) - 在執行
exec
后,新程序可以繼承原程序的鎖
文件鎖作用
linux有幾種技巧可創建文件鎖。
- 如果以O_CREAT(文件不存在則創建)和O_EXCL(獨占打開)標志調用open,如果文件存在則返回錯誤。考慮到其它進程的存在,文件是否存在和創建文件應該是原子的。所以可以把這種技巧創建的文件作為鎖使用。
- 如果新鏈接的名字已經存在,那么link將失敗,為獲取一個鎖,首先創建一個唯一的臨時文件,其路徑名是含有調用進程的id(如果不同進程中的線程以及同一進程內的線程都需要上鎖,那么所含的是進程id和線程id的某種組合),然后以建立文件眾所周知的路徑名調用link函數創建這個臨時文件的一個連接,如果創建成功,該臨時路徑名可以unlink掉。當調用線程使用完該鎖時,只需unlink眾所周知的路徑名就可以解鎖;如果link失敗返回EEXIST錯誤,調用線程就需重新嘗試。這要求臨時文件路徑名和鎖文件眾所周知的路徑名在同一個文件系統中,因為硬鏈接不能跨越文件系統。
- 如果待打開文件已經存在,且打開時指定了O_TRUNC標志,而且調用進程不具備寫訪問權限,那么open將返回個錯誤。為獲取一個鎖,我們在指定O_CREATE|O_WRONLY|O_TRUNC標志並置mode參數為0(即打開的新文件不存在任何權限位)的前提下調用open,如果調用成功我們就持有該鎖,以后使用完之后只需unlink路徑名,如果open失敗調用EACCES錯誤,調用線程就必須重新嘗試,但是這種做法在線程具有超級用戶特權下不起作用。
以下是第一中創建辦法。
/************************************************************************* > File Name: test.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月15日 星期一 08時33分04秒 ************************************************************************/ #define LOCKFILE "/home/..." /* * 如果open成功,我們就持有相應的文件鎖,函數返回前close原文件,不需要文件 * 描述符因為文件的本身存在代表鎖,至於他是否打開無關緊要,如果存在再次嘗試 * open */ my_lock(int fd) { int tfd; /* * someone else has the lock,loop around and try again * */ //如果文件不存在就創建他和獨占打開它(O_EXCL),檢查該文件的存在和創建必須是原子的 while((tfd=open(LOCKFILE,O_RDWR|O_CREATE,O_EXCL,FILE_MODE))<0) { if(errno!=EEXIST) { cerr<<"open error for lock file: "<<strerror(errno)<<endl; exit(-1); } //opend the file,we have the lock close(tfd); } } void mu_unlock(int fd) { unlink(LOCKFILE); }
但存在問題:
- 如果進程持有的鎖沒有終止就釋放它,那么文件名並未刪除。解決辦法:檢查文件最近訪問時間,大於某個確定的數量就刪除;把進程持有的id寫進鎖文件,但是進程id在一段時間后會被重用。這種問題對於fcntl不成問題,因為它持有的鎖會在進程終止時完全釋放
- 如果另外的進程以打開文件,該進程只是無限的open,可以先sleep1s,然后再open,再次嘗試open。如果時fcntl指定FSETLKW命令,內核把進程投入睡眠,直到該鎖可用,然后喚醒它
- 調用open和unlink刪除一個額外的文件涉及系統訪問時間,通常比fcntl調用兩次(一次獲取鎖一次釋放鎖)花去的時間要長