Linux进程间通信中的文件和文件锁
Linux進程間通信中的文件和文件鎖
前言
使用文件進行進程間通信應(yīng)該是最先學(xué)會的一種IPC方式。任何編程語言中,文件IO都是很重要的知識,所以使用文件進行進程間通信就成了很自然被學(xué)會的一種手段。考慮到系統(tǒng)對文件本身存在緩存機制,使用文件進行IPC的效率在某些多讀少寫的情況下并不低下。但是大家似乎經(jīng)常忘記IPC的機制可以包括“文件”這一選項。
我們首先引入文件進行IPC,試圖先使用文件進行通信引入一個競爭條件的概念,然后使用文件鎖解決這個問題,從而先從文件的角度來管中窺豹的看一下后續(xù)相關(guān)IPC機制的總體要解決的問題。閱讀本文可以幫你解決以下問題:
競爭條件(racing)
我們的第一個例子是多個進程寫文件的例子,雖然還沒做到通信,但是這比較方便的說明一個通信時經(jīng)常出現(xiàn)的情況:競爭條件。假設(shè)我們要并發(fā)100個進程,這些進程約定好一個文件,這個文件初始值內(nèi)容寫0,每一個進程都要打開這個文件讀出當(dāng)前的數(shù)字,加一之后將結(jié)果寫回去。在理想狀態(tài)下,這個文件最后寫的數(shù)字應(yīng)該是100,因為有100個進程打開、讀數(shù)、加1、寫回,自然是有多少個進程最后文件中的數(shù)字結(jié)果就應(yīng)該是多少。但是實際上并非如此,可以看一下這個例子:
[zorro@zorrozou-pc0 process]$ cat racing.c int do_child(const char *path) {/* 這個函數(shù)是每個子進程要做的事情每個子進程都會按照這個步驟進行操作:1. 打開FILEPATH路徑的文件2. 讀出文件中的當(dāng)前數(shù)字3. 將字符串轉(zhuǎn)成整數(shù)4. 整數(shù)自增加15. 將證書轉(zhuǎn)成字符串6. lseek調(diào)整文件當(dāng)前的偏移量到文件頭7. 將字符串寫會文件當(dāng)多個進程同時執(zhí)行這個過程的時候,就會出現(xiàn)racing:競爭條件,多個進程可能同時從文件獨到同一個數(shù)字,并且分別對同一個數(shù)字加1并寫回,導(dǎo)致多次寫回的結(jié)果并不是我們最終想要的累積結(jié)果。 */int fd;int ret, count;char buf[NUM];fd = open(path, O_RDWR);if (fd < 0) {perror("open()");exit(1);}/* */ret = read(fd, buf, NUM);if (ret < 0) {perror("read()");exit(1);}buf[ret] = '\0';count = atoi(buf);++count;sprintf(buf, "%d", count);lseek(fd, 0, SEEK_SET);ret = write(fd, buf, strlen(buf));/* */close(fd);exit(0); }int main() {pid_t pid;int count;for (count=0;count<COUNT;count++) {pid = fork();if (pid < 0) {perror("fork()");exit(1);}if (pid == 0) {do_child(FILEPATH);}}for (count=0;count<COUNT;count++) {wait(NULL);} }這個程序做后執(zhí)行的效果如下:
[zorro@zorrozou-pc0 process]$ make racing cc racing.c -o racing [zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count [zorro@zorrozou-pc0 process]$ ./racing [zorro@zorrozou-pc0 process]$ cat /tmp/count 71[zorro@zorrozou-pc0 process]$ [zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count [zorro@zorrozou-pc0 process]$ ./racing [zorro@zorrozou-pc0 process]$ cat /tmp/count 61[zorro@zorrozou-pc0 process]$ [zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count [zorro@zorrozou-pc0 process]$ ./racing [zorro@zorrozou-pc0 process]$ cat /tmp/count 64[zorro@zorrozou-pc0 process]$我們執(zhí)行了三次這個程序,每次結(jié)果都不太一樣,第一次是71,第二次是61,第三次是64,全都沒有得到預(yù)期結(jié)果,這就是競爭條件(racing)引入的問題。仔細分析這個進程我們可以發(fā)現(xiàn)這個競爭條件是如何發(fā)生的:
最開始文件內(nèi)容是0,假設(shè)此時同時打開了3個進程,那么他們分別讀文件的時候,這個過程是可能并發(fā)的,于是每個進程讀到的數(shù)組可能都是0,因為他們都在別的進程沒寫入1之前就開始讀了文件。于是三個進程都是給0加1,然后寫了個1回到文件。其他進程以此類推,每次100個進程的執(zhí)行順序可能不一樣,于是結(jié)果是每次得到的值都可能不太一樣,但是一定都少于產(chǎn)生的實際進程個數(shù)。于是我們把這種多個執(zhí)行過程(如進程或線程)中訪問同一個共享資源,而這些共享資源又有無法被多個執(zhí)行過程存取的的程序片段,叫做臨界區(qū)代碼。
那么該如何解決這個racing的問題呢?對于這個例子來說,可以用文件鎖的方式解決這個問題。就是說,對臨界區(qū)代碼進行加鎖,來解決競爭條件的問題。哪段是臨界區(qū)代碼?在這個例子中,兩端/ /之間的部分就是臨界區(qū)代碼。一個正確的例子是:
...ret = flock(fd, LOCK_EX);if (ret == -1) {perror("flock()");exit(1);}ret = read(fd, buf, NUM);if (ret < 0) {perror("read()");exit(1);}buf[ret] = '\0';count = atoi(buf);++count;sprintf(buf, "%d", count);lseek(fd, 0, SEEK_SET);ret = write(fd, buf, strlen(buf));ret = flock(fd, LOCK_UN);if (ret == -1) {perror("flock()");exit(1);} ...我們將臨界區(qū)部分代碼前后都使用了flock的互斥鎖,防止了臨界區(qū)的racing。這個例子雖然并沒有真正達到讓多個進程通過文件進行通信,解決某種協(xié)同工作問題的目的,但是足以表現(xiàn)出進程間通信機制的一些問題了。當(dāng)涉及到數(shù)據(jù)在多個進程間進行共享的時候,僅僅只實現(xiàn)數(shù)據(jù)通信或共享機制本身是不夠的,還需要實現(xiàn)相關(guān)的同步或異步機制來控制多個進程,達到保護臨界區(qū)或其他讓進程可以處理同步或異步事件的能力。我們可以認為文件鎖是可以實現(xiàn)這樣一種多進程的協(xié)調(diào)同步能力的機制,而除了文件鎖以外,還有其他機制可以達到相同或者不同的功能,我們會在下文中繼續(xù)詳細解釋。
再次,我們并不對flock這個方法本身進行功能性講解。這種功能性講解大家可以很輕易的在網(wǎng)上或者通過別的書籍得到相關(guān)內(nèi)容。本文更加偏重的是Linux環(huán)境提供了多少種文件鎖以及他們的區(qū)別是什么?
flock和lockf
從底層的實現(xiàn)來說,Linux的文件鎖主要有兩種:flock和lockf。需要額外對lockf說明的是,它只是fcntl系統(tǒng)調(diào)用的一個封裝。從使用角度講,lockf或fcntl實現(xiàn)了更細粒度文件鎖,即:記錄鎖。我們可以使用lockf或fcntl對文件的部分字節(jié)上鎖,而flock只能對整個文件加鎖。這兩種文件鎖是從歷史上不同的標準中起源的,flock來自BSD而lockf來自POSIX,所以lockf或fcntl實現(xiàn)的鎖在類型上又叫做POSIX鎖。
除了這個區(qū)別外,fcntl系統(tǒng)調(diào)用還可以支持強制鎖(Mandatory locking)。強制鎖的概念是傳統(tǒng)UNIX為了強制應(yīng)用程序遵守鎖規(guī)則而引入的一個概念,與之對應(yīng)的概念就是建議鎖(Advisory locking)。我們?nèi)粘J褂玫幕径际墙ㄗh鎖,它并不強制生效。這里的不強制生效的意思是,如果某一個進程對一個文件持有一把鎖之后,其他進程仍然可以直接對文件進行各種操作的,比如open、read、write。只有當(dāng)多個進程在操作文件前都去檢查和對相關(guān)鎖進行鎖操作的時候,文件鎖的規(guī)則才會生效。這就是一般建議鎖的行為。而強制性鎖試圖實現(xiàn)一套內(nèi)核級的鎖操作。當(dāng)有進程對某個文件上鎖之后,其他進程即使不在操作文件之前檢查鎖,也會在open、read或write等文件操作時發(fā)生錯誤。內(nèi)核將對有鎖的文件在任何情況下的鎖規(guī)則都生效,這就是強制鎖的行為。由此可以理解,如果內(nèi)核想要支持強制鎖,將需要在內(nèi)核實現(xiàn)open、read、write等系統(tǒng)調(diào)用內(nèi)部進行支持。
從應(yīng)用的角度來說,Linux內(nèi)核雖然號稱具備了強制鎖的能力,但其對強制性鎖的實現(xiàn)是不可靠的,建議大家還是不要在Linux下使用強制鎖。事實上,在我目前手頭正在使用的Linux環(huán)境上,一個系統(tǒng)在mount -o mand分區(qū)的時候報錯(archlinux kernel 4.5),而另一個系統(tǒng)雖然可以以強制鎖方式mount上分區(qū),但是功能實現(xiàn)卻不完整,主要表現(xiàn)在只有在加鎖后產(chǎn)生的子進程中open才會報錯,如果直接write是沒問題的,而且其他進程無論open還是read、write都沒問題(Centos 7 kernel 3.10)。鑒于此,我們就不在此介紹如何在Linux環(huán)境中打開所謂的強制鎖支持了。我們只需知道,在Linux環(huán)境下的應(yīng)用程序,flock和lockf在是鎖類型方面沒有本質(zhì)差別,他們都是建議鎖,而非強制鎖。
flock和lockf另外一個差別是它們實現(xiàn)鎖的方式不同。這在應(yīng)用的時候表現(xiàn)在flock的語義是針對文件的鎖,而lockf是針對文件描述符(fd)的鎖。我們用一個例子來觀察這個區(qū)別:
[zorro@zorrozou-pc0 locktest]$ cat flock.c int main() {int fd;pid_t pid;fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);if (fd < 0) {perror("open()");exit(1);}if (flock(fd, LOCK_EX) < 0) {perror("flock()");exit(1);}printf("%d: locked!\n", getpid());pid = fork();if (pid < 0) {perror("fork()");exit(1);}if (pid == 0) { /*fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);if (fd < 0) {perror("open()");exit(1);} */if (flock(fd, LOCK_EX) < 0) {perror("flock()");exit(1);}printf("%d: locked!\n", getpid());exit(0);}wait(NULL);unlink(PATH);exit(0); }上面代碼是一個flock的例子,其作用也很簡單:
這個程序直接編譯執(zhí)行的結(jié)果是:
[zorro@zorrozou-pc0 locktest]$ ./flock 23279: locked! 23280: locked!父子進程都加鎖成功了。這個結(jié)果似乎并不符合我們對文件加鎖的本意。按照我們對互斥鎖的理解,子進程對父進程已經(jīng)加鎖過的文件應(yīng)該加鎖失敗才對。我們可以稍微修改一下上面程序讓它達到預(yù)期效果,將子進程代碼段中的注釋取消掉重新編譯即可:
... /*fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);if (fd < 0) {perror("open()");exit(1);} */ ...將這段代碼上下的/ /刪除重新編譯。之后執(zhí)行的效果如下:
[zorro@zorrozou-pc0 locktest]$ make flock cc flock.c -o flock [zorro@zorrozou-pc0 locktest]$ ./flock 23437: locked!此時子進程flock的時候會阻塞,讓進程的執(zhí)行一直停在這。這才是我們使用文件鎖之后預(yù)期該有的效果。而相同的程序使用lockf卻不會這樣。這個原因在于flock和lockf的語義是不同的。使用lockf或fcntl的鎖,在實現(xiàn)上關(guān)聯(lián)到文件結(jié)構(gòu)體,這樣的實現(xiàn)導(dǎo)致鎖不會在fork之后被子進程繼承。而flock在實現(xiàn)上關(guān)聯(lián)到的是文件描述符,這就意味著如果我們在進程中復(fù)制了一個文件描述符,那么使用flock對這個描述符加的鎖也會在新復(fù)制出的描述符中繼續(xù)引用。在進程fork的時候,新產(chǎn)生的子進程的描述符也是從父進程繼承(復(fù)制)來的。在子進程剛開始執(zhí)行的時候,父子進程的描述符關(guān)系實際上跟在一個進程中使用dup復(fù)制文件描述符的狀態(tài)一樣(參見《UNIX環(huán)境高級編程》8.3節(jié)的文件共享部分)。這就可能造成上述例子的情況,通過fork產(chǎn)生的多個進程,因為子進程的文件描述符是復(fù)制的父進程的文件描述符,所以導(dǎo)致父子進程同時持有對同一個文件的互斥鎖,導(dǎo)致第一個例子中的子進程仍然可以加鎖成功。這個文件共享的現(xiàn)象在子進程使用open重新打開文件之后就不再存在了,所以重新對同一文件open之后,子進程再使用flock進行加鎖的時候會阻塞。另外要注意:除非文件描述符被標記了close-on-exec標記,flock鎖和lockf鎖都可以穿越exec,在當(dāng)前進程變成另一個執(zhí)行鏡像之后仍然保留。
上面的例子中只演示了fork所產(chǎn)生的文件共享對flock互斥鎖的影響,同樣原因也會導(dǎo)致dup或dup2所產(chǎn)生的文件描述符對flock在一個進程內(nèi)產(chǎn)生相同的影響。dup造成的鎖問題一般只有在多線程情況下才會產(chǎn)生影響,所以應(yīng)該避免在多線程場景下使用flock對文件加鎖,而lockf/fcntl則沒有這個問題。
總結(jié)
以上是生活随笔為你收集整理的Linux进程间通信中的文件和文件锁的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux中sort,uniq,cut,
- 下一篇: Linux 下的五种 IO 模型