使用文件進行進程間通訊應該是最早學會的一種IPC方式。任何編程語言中,文件IO都是很重要的知識,因此使用文件進行進程間通訊就成了很天然被學會的一種手段。考慮到系統對文件自己存在緩存機制,使用文件進行IPC的效率在某些多讀少寫的狀況下並不低下。可是你們彷佛常常忘記IPC的機制能夠包括「文件」這一選項。linux
咱們首先引入文件進行IPC,試圖先使用文件進行通訊引入一個競爭條件的概念,而後使用文件鎖解決這個問題,從而先從文件的角度來管中窺豹的看一下後續相關IPC機制的整體要解決的問題。閱讀本文能夠幫你解決如下問題:sql
咱們的第一個例子是多個進程寫文件的例子,雖然還沒作到通訊,可是這比較方便的說明一個通訊時常常出現的狀況:競爭條件。假設咱們要併發100個進程,這些進程約定好一個文件,這個文件初始值內容寫0,每個進程都要打開這個文件讀出當前的數字,加一以後將結果寫回去。在理想狀態下,這個文件最後寫的數字應該是100,由於有100個進程打開、讀數、加一、寫回,天然是有多少個進程最後文件中的數字結果就應該是多少。可是實際上並不是如此,能夠看一下這個例子:shell
[zorro@zorrozou-pc0 process]$ cat racing.c
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <sys/file.h> #include <wait.h> #define COUNT 100 #define NUM 64 #define FILEPATH "/tmp/count" int do_child(const char *path) { /* 這個函數是每一個子進程要作的事情 每一個子進程都會按照這個步驟進行操做: 1. 打開FILEPATH路徑的文件 2. 讀出文件中的當前數字 3. 將字符串轉成整數 4. 整數自增長1 5. 將證書轉成字符串 6. lseek調整文件當前的偏移量到文件頭 7. 將字符串寫會文件 當多個進程同時執行這個過程的時候,就會出現racing:競爭條件, 多個進程可能同時從文件獨到同一個數字,而且分別對同一個數字加1並寫回, 致使屢次寫回的結果並非咱們最終想要的累積結果。 */ 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); } }
這個程序作後執行的效果以下:編程
[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]$
咱們執行了三次這個程序,每次結果都不太同樣,第一次是71,第二次是61,第三次是64,全都沒有獲得預期結果,這就是競爭條件(racing)引入的問題。仔細分析這個進程咱們能夠發現這個競爭條件是如何發生的:數組
最開始文件內容是0,假設此時同時打開了3個進程,那麼他們分別讀文件的時候,這個過程是可能併發的,因而每一個進程讀到的數組可能都是0,由於他們都在別的進程沒寫入1以前就開始讀了文件。因而三個進程都是給0加1,而後寫了個1回到文件。其餘進程以此類推,每次100個進程的執行順序可能不同,因而結果是每次獲得的值均可能不太同樣,可是必定都少於產生的實際進程個數。因而咱們把這種多個執行過程(如進程或線程)中訪問同一個共享資源,而這些共享資源又有沒法被多個執行過程存取的的程序片斷,叫作臨界區代碼。緩存
那麼該如何解決這個racing的問題呢?對於這個例子來講,能夠用文件鎖的方式解決這個問題。就是說,對臨界區代碼進行加鎖,來解決競爭條件的問題。哪段是臨界區代碼?在這個例子中,兩端/ /之間的部分就是臨界區代碼。一個正確的例子是:ruby
... 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); } ...
咱們將臨界區部分代碼先後都使用了flock的互斥鎖,防止了臨界區的racing。這個例子雖然並無真正達到讓多個進程經過文件進行通訊,解決某種協同工做問題的目的,可是足以表現出進程間通訊機制的一些問題了。當涉及到數據在多個進程間進行共享的時候,僅僅只實現數據通訊或共享機制自己是不夠的,還須要實現相關的同步或異步機制來控制多個進程,達到保護臨界區或其餘讓進程能夠處理同步或異步事件的能力。咱們能夠認爲文件鎖是能夠實現這樣一種多進程的協調同步能力的機制,而除了文件鎖之外,還有其餘機制能夠達到相同或者不一樣的功能,咱們會在下文中繼續詳細解釋。bash
再次,咱們並不對flock這個方法自己進行功能性講解。這種功能性講解你們能夠很輕易的在網上或者經過別的書籍獲得相關內容。本文更加偏重的是Linux環境提供了多少種文件鎖以及他們的區別是什麼?微信
從底層的實現來講,Linux的文件鎖主要有兩種:flock和lockf。須要額外對lockf說明的是,它只是fcntl系統調用的一個封裝。從使用角度講,lockf或fcntl實現了更細粒度文件鎖,即:記錄鎖。咱們可使用lockf或fcntl對文件的部分字節上鎖,而flock只能對整個文件加鎖。這兩種文件鎖是從歷史上不一樣的標準中起源的,flock來自BSD而lockf來自POSIX,因此lockf或fcntl實現的鎖在類型上又叫作POSIX鎖。數據結構
除了這個區別外,fcntl系統調用還能夠支持強制鎖(Mandatory locking)。強制鎖的概念是傳統UNIX爲了強制應用程序遵照鎖規則而引入的一個概念,與之對應的概念就是建議鎖(Advisory locking)。咱們平常使用的基本都是建議鎖,它並不強制生效。這裏的不強制生效的意思是,若是某一個進程對一個文件持有一把鎖以後,其餘進程仍然能夠直接對文件進行各類操做的,好比open、read、write。只有當多個進程在操做文件前都去檢查和對相關鎖進行鎖操做的時候,文件鎖的規則纔會生效。這就是通常建議鎖的行爲。而強制性鎖試圖實現一套內核級的鎖操做。當有進程對某個文件上鎖以後,其餘進程即便不在操做文件以前檢查鎖,也會在open、read或write等文件操做時發生錯誤。內核將對有鎖的文件在任何狀況下的鎖規則都生效,這就是強制鎖的行爲。由此能夠理解,若是內核想要支持強制鎖,將須要在內核實現open、read、write等系統調用內部進行支持。
從應用的角度來講,Linux內核雖然號稱具有了強制鎖的能力,但其對強制性鎖的實現是不可靠的,建議你們仍是不要在Linux下使用強制鎖。事實上,在我目前手頭正在使用的Linux環境上,一個系統在mount -o mand分區的時候報錯(archlinux kernel 4.5),而另外一個系統雖然能夠以強制鎖方式mount上分區,可是功能實現卻不完整,主要表如今只有在加鎖後產生的子進程中open纔會報錯,若是直接write是沒問題的,並且其餘進程不管open仍是read、write都沒問題(Centos 7 kernel 3.10)。鑑於此,咱們就不在此介紹如何在Linux環境中打開所謂的強制鎖支持了。咱們只需知道,在Linux環境下的應用程序,flock和lockf在是鎖類型方面沒有本質差異,他們都是建議鎖,而非強制鎖。
flock和lockf另一個差異是它們實現鎖的方式不一樣。這在應用的時候表如今flock的語義是針對文件的鎖,而lockf是針對文件描述符(fd)的鎖。咱們用一個例子來觀察這個區別:
[zorro@zorrozou-pc0 locktest]$ cat flock.c #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/file.h> #include <wait.h> #define PATH "/tmp/lock" 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的例子,其做用也很簡單:
這個程序直接編譯執行的結果是:
[zorro@zorrozou-pc0 locktest]$ ./flock 23279: locked! 23280: locked!
父子進程都加鎖成功了。這個結果彷佛並不符合咱們對文件加鎖的本意。按照咱們對互斥鎖的理解,子進程對父進程已經加鎖過的文件應該加鎖失敗纔對。咱們能夠稍微修改一下上面程序讓它達到預期效果,將子進程代碼段中的註釋取消掉從新編譯便可:
... /* fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644); if (fd < 0) { perror("open()"); exit(1); } */ ...
將這段代碼上下的/ /刪除從新編譯。以後執行的效果以下:
[zorro@zorrozou-pc0 locktest]$ make flock cc flock.c -o flock [zorro@zorrozou-pc0 locktest]$ ./flock 23437: locked!
此時子進程flock的時候會阻塞,讓進程的執行一直停在這。這纔是咱們使用文件鎖以後預期該有的效果。而相同的程序使用lockf卻不會這樣。這個緣由在於flock和lockf的語義是不一樣的。使用lockf或fcntl的鎖,在實現上關聯到文件結構體,這樣的實現致使鎖不會在fork以後被子進程繼承。而flock在實現上關聯到的是文件描述符,這就意味着若是咱們在進程中複製了一個文件描述符,那麼使用flock對這個描述符加的鎖也會在新複製出的描述符中繼續引用。在進程fork的時候,新產生的子進程的描述符也是從父進程繼承(複製)來的。在子進程剛開始執行的時候,父子進程的描述符關係實際上跟在一個進程中使用dup複製文件描述符的狀態同樣(參見《UNIX環境高級編程》8.3節的文件共享部分)。這就可能形成上述例子的狀況,經過fork產生的多個進程,由於子進程的文件描述符是複製的父進程的文件描述符,因此致使父子進程同時持有對同一個文件的互斥鎖,致使第一個例子中的子進程仍然能夠加鎖成功。這個文件共享的現象在子進程使用open從新打開文件以後就再也不存在了,因此從新對同一文件open以後,子進程再使用flock進行加鎖的時候會阻塞。另外要注意:除非文件描述符被標記了close-on-exec標記,flock鎖和lockf鎖均可以穿越exec,在當前進程變成另外一個執行鏡像以後仍然保留。
上面的例子中只演示了fork所產生的文件共享對flock互斥鎖的影響,一樣緣由也會致使dup或dup2所產生的文件描述符對flock在一個進程內產生相同的影響。dup形成的鎖問題通常只有在多線程狀況下才會產生影響,因此應該避免在多線程場景下使用flock對文件加鎖,而lockf/fcntl則沒有這個問題。
爲了對比flock的行爲,咱們在此列出使用lockf的相同例子,來演示一下它們的不一樣:
[zorro@zorrozou-pc0 locktest]$ cat lockf.c
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/file.h> #include <wait.h> #define PATH "/tmp/lock" 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 (lockf(fd, F_LOCK, 0) < 0) { perror("lockf()"); 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 (lockf(fd, F_LOCK, 0) < 0) { perror("lockf()"); exit(1); } printf("%d: locked!\n", getpid()); exit(0); } wait(NULL); unlink(PATH); exit(0); }
編譯執行的結果是:
[zorro@zorrozou-pc0 locktest]$ ./lockf 27262: locked!
在子進程不用open從新打開文件的狀況下,進程執行仍然被阻塞在子進程lockf加鎖的操做上。關於fcntl對文件實現記錄鎖的詳細內容,你們能夠參考《UNIX環境高級編程》中關於記錄鎖的14.3章節。
C語言的標準IO庫中還提供了一套文件鎖,它們的原型以下:
#include <stdio.h> void flockfile(FILE *filehandle); int ftrylockfile(FILE *filehandle); void funlockfile(FILE *filehandle);
從實現角度來講,stdio庫中實現的文件鎖與flock或lockf有本質區別。做爲一種標準庫,其實現的鎖必然要考慮跨平臺的特性,因此其結構都是在用戶態的FILE結構體中實現的,而非內核中的數據結構來實現。這直接致使的結果就是,標準IO的鎖在多進程環境中使用是有問題的。進程在fork的時候會複製一整套父進程的地址空間,這將致使子進程中的FILE結構與父進程徹底一致。就是說,父進程若是加鎖了,子進程也將持有這把鎖,父進程沒加鎖,子進程因爲地址空間跟父進程是獨立的,因此也沒法經過FILE結構體檢查別的進程的用戶態空間是否家了標準IO庫提供的文件鎖。這種限制致使這套文件鎖只能處理一個進程中的多個線程之間共享的FILE 的進行文件操做。就是說,多個線程必須同時操做一個用fopen打開的FILE 變量,若是內部本身使用fopen從新打開文件,那麼返回的FILE *地址不一樣,也起不到線程的互斥做用。
咱們分別將兩種使用線程的狀態的例子分別列出來,第一種是線程之間共享同一個FILE *的狀況,這種狀況互斥是沒問題的:
[zorro@zorro-pc locktest]$ cat racing_pthread_sharefp.c
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <sys/file.h> #include <wait.h> #include <pthread.h> #define COUNT 100 #define NUM 64 #define FILEPATH "/tmp/count" static FILE *filep; void *do_child(void *p) { int fd; int ret, count; char buf[NUM]; flockfile(filep); if (fseek(filep, 0L, SEEK_SET) == -1) { perror("fseek()"); } ret = fread(buf, NUM, 1, filep); count = atoi(buf); ++count; sprintf(buf, "%d", count); if (fseek(filep, 0L, SEEK_SET) == -1) { perror("fseek()"); } ret = fwrite(buf, strlen(buf), 1, filep); funlockfile(filep); return NULL; } int main() { pthread_t tid[COUNT]; int count; filep = fopen(FILEPATH, "r+"); if (filep == NULL) { perror("fopen()"); exit(1); } for (count=0;count<COUNT;count++) { if (pthread_create(tid+count, NULL, do_child, NULL) != 0) { perror("pthread_create()"); exit(1); } } for (count=0;count<COUNT;count++) { if (pthread_join(tid[count], NULL) != 0) { perror("pthread_join()"); exit(1); } } fclose(filep); exit(0); }
另外一種狀況是每一個線程都fopen從新打開一個描述符,此時線程是不能互斥的:
[zorro@zorro-pc locktest]$ cat racing_pthread_threadfp.c
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <sys/file.h> #include <wait.h> #include <pthread.h> #define COUNT 100 #define NUM 64 #define FILEPATH "/tmp/count" void *do_child(void *p) { int fd; int ret, count; char buf[NUM]; FILE *filep; filep = fopen(FILEPATH, "r+"); if (filep == NULL) { perror("fopen()"); exit(1); } flockfile(filep); if (fseek(filep, 0L, SEEK_SET) == -1) { perror("fseek()"); } ret = fread(buf, NUM, 1, filep); count = atoi(buf); ++count; sprintf(buf, "%d", count); if (fseek(filep, 0L, SEEK_SET) == -1) { perror("fseek()"); } ret = fwrite(buf, strlen(buf), 1, filep); funlockfile(filep); fclose(filep); return NULL; } int main() { pthread_t tid[COUNT]; int count; for (count=0;count<COUNT;count++) { if (pthread_create(tid+count, NULL, do_child, NULL) != 0) { perror("pthread_create()"); exit(1); } } for (count=0;count<COUNT;count++) { if (pthread_join(tid[count], NULL) != 0) { perror("pthread_join()"); exit(1); } } exit(0); }
以上程序你們能夠自行編譯執行看看效果。
系統爲咱們提供了flock命令,能夠方便咱們在命令行和shell腳本中使用文件鎖。須要注意的是,flock命令是使用flock系統調用實現的,因此在使用這個命令的時候請注意進程關係對文件鎖的影響。flock命令的使用方法和在腳本編程中的使用能夠參見個人另外一篇文章《shell編程之經常使用技巧》中的bash併發編程和flock這部份內容,在此不在贅述。
咱們還可使用lslocks命令來查看當前系統中的文件鎖使用狀況。一個常見的現實以下:
[root@zorrozou-pc0 ~]# lslocks
COMMAND PID TYPE SIZE MODE M START END PATH firefox 16280 POSIX 0B WRITE 0 0 0 /home/zorro/.mozilla/firefox/bk2bfsto.default/.parentlock dmeventd 344 POSIX 4B WRITE 0 0 0 /run/dmeventd.pid gnome-shell 472 FLOCK 0B WRITE 0 0 0 /run/user/120/wayland-0.lock flock 27452 FLOCK 0B WRITE 0 0 0 /tmp/lock lvmetad 248 POSIX 4B WRITE 0 0 0 /run/lvmetad.pid
這其中,TYPE主要表示鎖類型,就是上文咱們描述的flock和lockf。lockf和fcntl實現的鎖事POSIX類型。M表示是否事強制鎖,0表示不是。若是是記錄鎖的話,START和END表示鎖住文件的記錄位置,0表示目前鎖住的是整個文件。MODE主要用來表示鎖的權限,實際上這也說明了鎖的共享屬性。在系統底層,互斥鎖表示爲WRITE,而共享鎖表示爲READ,若是這段出現*則表示有其餘進程正在等待這個鎖。其他參數能夠參考man lslocks。
本文經過文件盒文件鎖的例子,引出了競爭條件這樣在進程間通訊中須要解決的問題。並深刻探討了系統編程中經常使用的文件鎖的實現和應用特色。但願你們對進程間通訊和文件鎖的使用有更深刻的理解。
Linux的進程間通訊-文件和文件鎖 做者:zorro 微博ID:orroz 微信公衆號:Linux系統技術