UNIX再学习 -- 记录锁
生活随笔
收集整理的這篇文章主要介紹了
UNIX再学习 -- 记录锁
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
APUE第 3 章,參看:UNIX再學習 -- 文件I/O? fcntl 函數它的記錄鎖功能我們當時沒講。接下來就詳細說明下。
一、讀寫沖突
1、如果兩個或兩個以上的進程同時向一個文件的某個特定的區域寫入數據,那么最后寫入文件的數據極有可能因為寫操作的交錯而產生混亂。2、如果一個進程寫而其他進程同時在讀一個文件的某個特定區域,那么讀出的數據極有可能因為讀寫操作的交錯而不完整。
多個進程同時讀一個文件的某個特定區域,不會有任何問題,它們只是各自把文件中的數據拷貝到各自的緩沖區中,并不會改變文件的內容,相互之間也就不會沖突。 由此可以得出結論,為了避免在讀寫同一個文件的同一個區域時發生沖突,進程之間應該遵循以下規則: 如果一個進程正在寫,那么其他進程既不能寫也不能讀。 如果一個進程正在讀,那么其他進程不能寫但是可以讀。
二、讀鎖和寫鎖
為了避免多個進程在讀寫同一個文件的同一區域時發生沖突,UNIX/Linux 系統引入了文件鎖機制,并把文件鎖分為讀鎖和寫鎖兩種,它們的區別在于: 讀鎖:共享鎖,對一個文件的特定區域可以加多把讀鎖。 寫鎖,排它鎖,對一個文件的特定區域只能加一把寫鎖。 基于鎖的操作模型是:讀/寫文件中的特定區域之前,先加上讀/寫鎖,鎖成功了再讀/寫。讀/寫完成以后再解鎖。三、加鎖和解鎖
讓我們重溫一下 fcntl 函數。 #include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ ); 返回:若成功則依賴于 cmd(見下),若出錯則為 -11、參數解析
對于記錄鎖,cmd 是 F_GETLK、F_SETLK 或 F_SETLKW。第三個參數是指向 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 結構說明如下: 所希望的鎖類型: F_RDLCK(共享讀鎖)、F_WRLCK(獨占性寫鎖)或 F_UNLCK(解鎖一個區域)要加鎖或解鎖的區域的起始地址,由 l_start 和 l_whence 兩者決定。l_stat 是相對位移量(字節),l_whence 則決定了相對位移量的起點。
區域的長度,由 l_len 表示。 進程的 ID (l_pid)持有的鎖能阻塞當前進程(僅由 F_GETLK 返回)。
關于加鎖和解鎖區域的說明還要注意下列各點:
指定區域起始偏移量的兩個元素與 lseek 函數中最后兩個參數類似。l_whence 可選用的值是 SEEK_SET、SEEK_CUR 或 SEEK_END。 該區域可以在當前文件尾端處開始或越過其尾端處開始,但是不能在文件起始位置之前開始或越過該起始位置。
如若 l_len 為 0,則表示鎖的區域從其起點(由 l_start 和 l_whence 決定)開始直至最大可能位置為止。也就是不管添寫到該文件中多少數據,它都處于鎖的范圍。
為了鎖整個文件,通常的方法是將 l_start 說明為 0, l_whence 說明為 SEEK_SET,l_len 說明為 0。
2、下面說明一下 fcntl 函數的 3 中命令
(1)F_GETLK
判斷由 flockptr 所描述的鎖是否會被另外一把鎖所排斥(阻塞)。如果存在一把鎖,它阻止創建由 flockptr 所描述的鎖,則該現有鎖的信息將重寫 flockptr 指向的信息。如果不存在這種情況,則除了將 l_type 設置為 F_UNLCK 之外,flockptr 所指向結構中的其他信息保持不變。(2)F_SETLK?
設置由 flockptr 所描述的鎖。如果我們試圖獲得一把讀鎖(l_type 為 F_RDLCK)或寫鎖(l_type 為 F_WRLCK),而兼容性規則阻止系統給我們這把鎖,那么 fcntl 會立即出錯返回,此時 errno 設置為 EACCES 或 EAGAIN。(3)F_SETLKW
這個命令是 F_SETLK 的阻塞版本(命令中的 W 表示等待(wait)) 。如果所請求的讀鎖或寫鎖因另一個進程當前已經對所請求區域的某部分進行了加鎖而不能被授予,那么調用進程會被置為休眠。如果請求創建的鎖已經可用,或者休眠由信號中斷,則該進程被喚醒。應當了解,用 F_GETLK 測試能否建立一把鎖,然后甩 F_SETLK 或 F_SETLKW 企圖建立那把鎖,這兩者不是一個原子操作。因此不能保證在這兩次 fcntl 調用之間不會有另一個進程插入并建立一把相同的鎖。如果不希望在等待鎖變為可用時產生阻塞,就必須處理由 F_SETLK 返回的可能的出錯。
3、示例說明
想看更多示例,可參看下面的擴展 擴展: 文件鎖 擴展:linux之記錄鎖詳解(1)從文件頭10字節開始的20字節以阻塞模式加讀鎖
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h>int main() {int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);if (fd == -1){perror ("open");exit (EXIT_FAILURE);}struct flock lock;lock.l_type = F_RDLCK; //定義鎖操作的類型為加讀鎖lock.l_whence = SEEK_SET; //定義鎖區偏移起點為文件頭lock.l_start = 10; //定義鎖區從文件頭開始計算的偏移 10 個字節lock.l_len = 20; //定義鎖區字節長度為 20 個字節,即只對文件中這 20 個字節進行區域加鎖。lock.l_pid = -1; //定義加鎖進程標示為自動設置if (fcntl (fd, F_SETLKW, &lock) == -1) //F_SETLKW 為阻塞模式,是指進程遇鎖,將被阻塞直到鎖被釋放。{perror ("fcntl");exit (EXIT_FAILURE);}if (close (fd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; }(2)從當前位置10字節開始到文件尾以非阻塞模式加寫鎖
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <errno.h>int main() {int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);if (fd == -1){perror ("open");exit (EXIT_FAILURE);}struct flock lock;lock.l_type = F_WRLCK; //定義鎖操作的類型為加寫鎖lock.l_whence = SEEK_CUR; //定義鎖區偏移起點為文件當前位置lock.l_start = 10; //定義鎖區從文件頭開始計算的偏移 10 個字節lock.l_len = 0; //定義鎖區字節長度到文件結尾,即僅文件開頭的 10 個字節不加鎖lock.l_pid = -1; //定義加鎖進程標識為自動設置if (fcntl (fd, F_SETLK, &lock) == -1) //F_SETLK 為非阻塞模式,是指進程遇鎖,立即以錯誤返回,并設錯誤碼為EAGAIN{if (errno != EAGAIN){perror ("fcntl");exit (EXIT_FAILURE);}printf ("暫時不能加鎖,稍后再試...\n");}if (close (fd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; }(3)對整個文件解鎖
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h>int main() {int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);if (fd == -1){perror ("open");exit (EXIT_FAILURE);}struct flock lock;lock.l_type = F_UNLCK; //定義鎖操作的類型為解鎖lock.l_whence = SEEK_SET; //定義鎖區偏移起點為文件頭lock.l_start = 0; //定義鎖區從文件頭開始計算lock.l_len = 0; //定義鎖區字節長度到文件結尾,即整個文件lock.l_pid = -1; //定義加鎖進程標識為自動設置if (fcntl (fd, F_SETLKW, &lock) == -1) //F_SETLKW 為阻塞模式,是指進程遇鎖,將被阻塞直到鎖被釋放。{perror ("fcntl");exit (EXIT_FAILURE);}if (close (fd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; }(4)測試鎖
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h>//打印不能加鎖的具體原因 void why_not (struct flock* lock) {printf ("%d進程", lock->l_pid);switch (lock->l_whence){case SEEK_SET:printf ("在距文件頭");break;case SEEK_CUR:printf ("在距當前位置");break;case SEEK_END:printf ("在距文件尾");break;}printf ("%ld字節處,為%ld字節加了", lock->l_start, lock->l_len);switch (lock->l_type){case F_RDLCK:printf ("讀鎖。\n");break;case F_WRLCK:printf ("寫鎖。\n");break; } }int main() {int fd = open ("data.txt", O_RDWR, 0666);if (fd == -1){perror ("open");exit (EXIT_FAILURE);}struct flock lock;lock.l_type = F_RDLCK;lock.l_whence = SEEK_SET;lock.l_start = 10;lock.l_len = 20;lock.l_pid = -1;//使用函數 fcntl 測試給定文件的特定區域是否可以加鎖if (fcntl (fd, F_GETLK, &lock) == -1){perror ("fcntl");exit (EXIT_FAILURE);}if (lock.l_type == F_UNLCK) //判斷能否加鎖,在不能加鎖的情況下,打印原因printf ("此鎖可加!\n");elsewhy_not (&lock);if (close (fd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; } 輸出結果: 此鎖可加!(5)示例解析
示例注釋講的很明白了,我現在主要想講下。下面這兩句話: 讀鎖:共享鎖,對一個文件的特定區域可以加多把讀鎖。寫鎖,排它鎖,對一個文件的特定區域只能加一把寫鎖。可用 示例一 ?和 示例二,添加延時,比如延時 20 秒,再另一個終端上再次執行加鎖,可看到結果。 讀鎖,可以再加讀鎖;而寫鎖,不可再加鎖了。測試一下參數鎖能否加上,如果能加上,則不會去加鎖而是將鎖的類型改成F_UNLCK 如果不能加上,則將文件中已經存在的鎖信息通過參數鎖帶出來并且將 l_pid 設置為真正給文件加鎖的進程號,所以可以使用 l_pid 判斷能否加上。
(6)擴展部分
為了避免每次分配 flock 結構,然后又填入各項信息,可寫一個函數來處理這些細節。 #include <fcntl.h> #include "apue.h"int lock_leg (int fd, int cmd, int type, off_t offset, int whence, off_t len) {struct flock lock;lock.l_type = type;lock.l_start = offset;lock.l_whence = whence;lock.l_len = len;return (fcntl (fd, cmnd, &lock)); }四、進階
1、死鎖
(1)死鎖產生
講線程互斥量的時候我們講過死鎖,當然這里的講的是文件鎖的死鎖。 如果兩個進程相互等待對方持有并且不釋放(鎖定)的資源時,則這兩個進程就處于死鎖狀態。如果一個進程已經控制了文件中的一個加鎖區域,然后它又試圖對另一個進程控制的區域加鎖,那么它就會休眠,在這種情況下,有發生死鎖的可能性。(2)示例說明
#include "apue.h" #include <fcntl.h>static void lockabyte(const char *name, int fd, off_t offset) {if (writew_lock(fd, offset, SEEK_SET, 1) < 0)err_sys("%s: writew_lock error", name);printf("%s: got the lock, byte %lld\n", name, (long long)offset); }int main(void) {int fd;pid_t pid;/** Create a file and write two bytes to it.*/if ((fd = creat("templock", FILE_MODE)) < 0)err_sys("creat error");if (write(fd, "ab", 2) != 2)err_sys("write error");TELL_WAIT();if ((pid = fork()) < 0) {err_sys("fork error");} else if (pid == 0) { /* child */lockabyte("child", fd, 0);TELL_PARENT(getppid());WAIT_PARENT();lockabyte("child", fd, 1);} else { /* parent */lockabyte("parent", fd, 1);TELL_CHILD(pid);WAIT_CHILD();lockabyte("parent", fd, 0);}exit(0); } 輸出結果: child:got the lock,byte 0 parent:got the lock,byte 1 child:writew_lock error:Deadlock situation detected/avoided parent:got the lock,byte 0(3)示例解析
上例中,子進程對第 0 字節加鎖,父進程對第 1 字節加鎖。然后,它們中的每一個又試圖對對方已經加鎖的字節加鎖。所以出現死鎖現象。2、鎖的隱含繼承和釋放
關于記錄鎖的自動繼承和釋放有三條規則:(1)鎖與進程、文件兩方面有關。
這有兩重含意:第一重很明顯,當一個進程終止時,它所建立的鎖全部釋放;第二重意思就不很明顯,任何時候關閉一個描述符時,則該進程通過這一描述符可以存訪的文件上的任何一把鎖都被釋放(這些鎖都是該進程設置的)。這就意味著如果執行下列四步:fd1=open (pathname, ...); read_lock (fd1, ...); fd2 = dup ( fd1 ) ; close ( fd2 ) ;則在 close(fd2)后,在 fd1 上設置的鎖被釋放。如果將 dup 代換為 open,其效果也一樣:
fd1=open (pathname, ...); read_lock (fd1, ...); fd2=open (pathname, ...); close ( fd2) ;
(2)由 fork 產生的子程序不繼承父進程所設置的鎖。
這意味著,若一個進程得到一把鎖,然后調用 fork,那么對于父進程獲得的鎖而言,子進程被視為另一個進程,對于從父進程處繼承過來的任一描述符,子進程要調用 fcntl 以獲得它自己的鎖。這與鎖的作用是相一致的。鎖的作用是阻止多個進程同時寫同一個文件(或同一文件區域)。如果子進程繼承父進程的鎖,則父、子進程就可以同時寫同一個文件。(3)在執行 exec 后,新程序可以繼承原執行程序的鎖。
但是注意,如果對一個文件描述符設置了執行時關閉標志,那么當作為 exec 的一部分關閉該文件描述符時,將釋放相關文件的所有鎖。總結
以上是生活随笔為你收集整理的UNIX再学习 -- 记录锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 提高数据库处理查询速度
- 下一篇: Netty入门(二)时间服务器及客户端