您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“Linux中如何共享存儲”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“Linux中如何共享存儲”這篇文章吧。
進程是運行著的程序,每個進程都有著它自己的地址空間,這些空間由進程被允許訪問的內存地址組成。進程有一個或多個執行線程,而線程是一系列執行指令的集合:單線程進程就只有一個線程,而多線程的進程則有多個線程。一個進程中的線程共享各種資源,特別是地址空間。另外,一個進程中的線程可以直接通過共享內存來進行通信,盡管某些現代語言(例如 Go)鼓勵一種更有序的方式,例如使用線程安全的通道。當然對于不同的進程,默認情況下,它們不能共享內存。
有多種方法啟動之后要進行通信的進程,下面所舉的例子中主要使用了下面的兩種方法:
一個終端被用來啟動一個進程,另外一個不同的終端被用來啟動另一個。
在一個進程(父進程)中調用系統函數 fork
,以此生發另一個進程(子進程)。
***個例子采用了上面使用終端的方法。這些代碼示例的 ZIP 壓縮包可以從我的網站下載到。
程序員對文件訪問應該都已經很熟識了,包括許多坑(不存在的文件、文件權限損壞等等),這些問題困擾著程序對文件的使用。盡管如此,共享文件可能是最為基礎的 IPC 機制了。考慮一下下面這樣一個相對簡單的例子,其中一個進程(生產者 producer
)創建和寫入一個文件,然后另一個進程(消費者 consumer
)從這個相同的文件中進行讀取:
writes +-----------+ readsproducer-------->| disk file |<-------consumer +-----------+
在使用這個 IPC 機制時最明顯的挑戰是競爭條件可能會發生:生產者和消費者可能恰好在同一時間訪問該文件,從而使得輸出結果不確定。為了避免競爭條件的發生,該文件在處于讀或寫狀態時必須以某種方式處于被鎖狀態,從而阻止在寫操作執行時和其他操作的沖突。在標準系統庫中與鎖相關的 API 可以被總結如下:
生產者應該在寫入文件時獲得一個文件的排斥鎖。一個排斥鎖最多被一個進程所擁有。這樣就可以排除掉競爭條件的發生,因為在鎖被釋放之前沒有其他的進程可以訪問這個文件。
消費者應該在從文件中讀取內容時得到至少一個共享鎖。多個讀取者可以同時保有一個共享鎖,但是沒有寫入者可以獲取到文件內容,甚至在當只有一個讀取者保有一個共享鎖時。
共享鎖可以提升效率。假如一個進程只是讀入一個文件的內容,而不去改變它的內容,就沒有什么原因阻止其他進程來做同樣的事。但如果需要寫入內容,則很顯然需要文件有排斥鎖。
標準的 I/O 庫中包含一個名為 fcntl
的實用函數,它可以被用來檢查或者操作一個文件上的排斥鎖和共享鎖。該函數通過一個文件描述符(一個在進程中的非負整數值)來標記一個文件(在不同的進程中不同的文件描述符可能標記同一個物理文件)。對于文件的鎖定, Linux 提供了名為 flock
的庫函數,它是 fcntl
的一個精簡包裝。***個例子中使用 fcntl
函數來暴露這些 API 細節。
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h> #define FileName "data.dat" void report_and_exit(const char* msg) { [perror][4](msg); [exit][5](-1); /* EXIT_FAILURE */} int main() { struct flock lock; lock.l_type = F_WRLCK; /* read/write (exclusive) lock */ lock.l_whence = SEEK_SET; /* base for seek offsets */ lock.l_start = 0; /* 1st byte in file */ lock.l_len = 0; /* 0 here means 'until EOF' */ lock.l_pid = getpid(); /* process id */ int fd; /* file descriptor to identify a file within a process */ if ((fd = open(FileName, O_RDONLY)) < 0) /* -1 signals an error */ report_and_exit("open to read failed..."); /* If the file is write-locked, we can't continue. */ fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */ if (lock.l_type != F_UNLCK) report_and_exit("file is still write locked..."); lock.l_type = F_RDLCK; /* prevents any writing during the reading */ if (fcntl(fd, F_SETLK, &lock) < 0) report_and_exit("can't get a read-only lock..."); /* Read the bytes (they happen to be ASCII codes) one at a time. */ int c; /* buffer for read bytes */ while (read(fd, &c, 1) > 0) /* 0 signals EOF */ write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */ /* Release the lock explicitly. */ lock.l_type = F_UNLCK; if (fcntl(fd, F_SETLK, &lock) < 0) report_and_exit("explicit unlocking failed..."); close(fd); return 0;}
上面生產者程序的主要步驟可以總結如下:
這個程序首先聲明了一個類型為 struct flock
的變量,它代表一個鎖,并對它的 5 個域做了初始化。***個初始化
使得這個鎖為排斥鎖(read-write)而不是一個共享鎖(read-only)。假如生產者獲得了這個鎖,則其他的進程將不能夠對文件做讀或者寫操作,直到生產者釋放了這個鎖,或者顯式地調用 fcntl
,又或者隱式地關閉這個文件。(當進程終止時,所有被它打開的文件都會被自動關閉,從而釋放了鎖)
lock.l_type = F_WRLCK; /* exclusive lock */
上面的程序接著初始化其他的域。主要的效果是整個文件都將被鎖上。但是,有關鎖的 API 允許特別指定的字節被上鎖。例如,假如文件包含多個文本記錄,則單個記錄(或者甚至一個記錄的一部分)可以被鎖,而其余部分不被鎖。
***次調用 fcntl
嘗試排斥性地將文件鎖住,并檢查調用是否成功。一般來說, fcntl
函數返回 -1
(因此小于 0)意味著失敗。第二個參數 F_SETLK
意味著 fcntl
的調用不是堵塞的;函數立即做返回,要么獲得鎖,要么顯示失敗了。假如替換地使用 F_SETLKW
(末尾的 W
代指等待),那么對 fcntl
的調用將是阻塞的,直到有可能獲得鎖的時候。在調用 fcntl
函數時,它的***個參數 fd
指的是文件描述符,第二個參數指定了將要采取的動作(在這個例子中,F_SETLK
指代設置鎖),第三個參數為鎖結構的地址(在本例中,指的是 &lock
)。
if (fcntl(fd, F_SETLK, &lock) < 0)
假如生產者獲得了鎖,這個程序將向文件寫入兩個文本記錄。
在向文件寫入內容后,生產者改變鎖結構中的 l_type
域為 unlock
值:
并調用 fcntl
來執行解鎖操作。***程序關閉了文件并退出。
lock.l_type = F_UNLCK;
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h> #define FileName "data.dat" void report_and_exit(const char* msg) { [perror][4](msg); [exit][5](-1); /* EXIT_FAILURE */} int main() { struct flock lock; lock.l_type = F_WRLCK; /* read/write (exclusive) lock */ lock.l_whence = SEEK_SET; /* base for seek offsets */ lock.l_start = 0; /* 1st byte in file */ lock.l_len = 0; /* 0 here means 'until EOF' */ lock.l_pid = getpid(); /* process id */ int fd; /* file descriptor to identify a file within a process */ if ((fd = open(FileName, O_RDONLY)) < 0) /* -1 signals an error */ report_and_exit("open to read failed..."); /* If the file is write-locked, we can't continue. */ fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */ if (lock.l_type != F_UNLCK) report_and_exit("file is still write locked..."); lock.l_type = F_RDLCK; /* prevents any writing during the reading */ if (fcntl(fd, F_SETLK, &lock) < 0) report_and_exit("can't get a read-only lock..."); /* Read the bytes (they happen to be ASCII codes) one at a time. */ int c; /* buffer for read bytes */ while (read(fd, &c, 1) > 0) /* 0 signals EOF */ write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */ /* Release the lock explicitly. */ lock.l_type = F_UNLCK; if (fcntl(fd, F_SETLK, &lock) < 0) report_and_exit("explicit unlocking failed..."); close(fd); return 0;}
相比于鎖的 API,消費者程序會相對復雜一點兒。特別的,消費者程序首先檢查文件是否被排斥性的被鎖,然后才嘗試去獲得一個共享鎖。相關的代碼為:
lock.l_type = F_WRLCK;...fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */if (lock.l_type != F_UNLCK) report_and_exit("file is still write locked...");
在 fcntl
調用中的 F_GETLK
操作指定檢查一個鎖,在本例中,上面代碼的聲明中給了一個 F_WRLCK
的排斥鎖。假如特指的鎖不存在,那么 fcntl
調用將會自動地改變鎖類型域為 F_UNLCK
以此來顯示當前的狀態。假如文件是排斥性地被鎖,那么消費者將會終止。(一個更健壯的程序版本或許應該讓消費者睡會兒,然后再嘗試幾次。)
假如當前文件沒有被鎖,那么消費者將嘗試獲取一個共享(read-only)鎖(F_RDLCK
)。為了縮短程序,fcntl
中的 F_GETLK
調用可以丟棄,因為假如其他進程已經保有一個讀寫鎖,F_RDLCK
的調用就可能會失敗。重新調用一個只讀鎖能夠阻止其他進程向文件進行寫的操作,但可以允許其他進程對文件進行讀取。簡而言之,共享鎖可以被多個進程所保有。在獲取了一個共享鎖后,消費者程序將立即從文件中讀取字節數據,然后在標準輸出中打印這些字節的內容,接著釋放鎖,關閉文件并終止。
下面的 %
為命令行提示符,下面展示的是從相同終端開啟這兩個程序的輸出:
% ./producerProcess 29255 has written to data file... % ./consumerNow is the winter of our discontentMade glorious summer by this sun of York
在本次的代碼示例中,通過 IPC 傳輸的數據是文本:它們來自莎士比亞的戲劇《理查三世》中的兩行臺詞。然而,共享文件的內容還可以是紛繁復雜的,任意的字節數據(例如一個電影)都可以,這使得文件共享變成了一個非常靈活的 IPC 機制。但它的缺點是文件獲取速度較慢,因為文件的獲取涉及到讀或者寫。同往常一樣,編程總是伴隨著折中。下面的例子將通過共享內存來做 IPC,而不是通過共享文件,在性能上相應的有極大的提升。
對于共享內存,Linux 系統提供了兩類不同的 API:傳統的 System V API 和更新一點的 POSIX API。在單個應用中,這些 API 不能混用。但是,POSIX 方式的一個壞處是它的特性仍在發展中,并且依賴于安裝的內核版本,這非常影響代碼的可移植性。例如,默認情況下,POSIX API 用內存映射文件來實現共享內存:對于一個共享的內存段,系統為相應的內容維護一個備份文件。在 POSIX 規范下共享內存可以被配置為不需要備份文件,但這可能會影響可移植性。我的例子中使用的是帶有備份文件的 POSIX API,這既結合了內存獲取的速度優勢,又獲得了文件存儲的持久性。
下面的共享內存例子中包含兩個程序,分別名為 memwriter
和 memreader
,并使用信號量來調整它們對共享內存的獲取。在任何時候當共享內存進入一個寫入者場景時,無論是多進程還是多線程,都有遇到基于內存的競爭條件的風險,所以,需要引入信號量來協調(同步)對共享內存的獲取。
memwriter
程序應當在它自己所處的終端首先啟動,然后 memreader
程序才可以在它自己所處的終端啟動(在接著的十幾秒內)。memreader
的輸出如下:
This is the way the world ends...
在每個源程序的最上方注釋部分都解釋了在編譯它們時需要添加的鏈接參數。
首先讓我們復習一下信號量是如何作為一個同步機制工作的。一般的信號量也被叫做一個計數信號量,因為帶有一個可以增加的值(通常初始化為 0)。考慮一家租用自行車的商店,在它的庫存中有 100 輛自行車,還有一個供職員用于租賃的程序。每當一輛自行車被租出去,信號量就增加 1;當一輛自行車被還回來,信號量就減 1。在信號量的值為 100 之前都還可以進行租賃業務,但如果等于 100 時,就必須停止業務,直到至少有一輛自行車被還回來,從而信號量減為 99。
二元信號量是一個特例,它只有兩個值:0 和 1。在這種情況下,信號量的表現為互斥量(一個互斥的構造)。下面的共享內存示例將把信號量用作互斥量。當信號量的值為 0 時,只有 memwriter
可以獲取共享內存,在寫操作完成后,這個進程將增加信號量的值,從而允許 memreader
來讀取共享內存。
/** Compilation: gcc -o memwriter memwriter.c -lrt -lpthread **/#include <stdio.h>#include <stdlib.h>#include <sys/mman.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <semaphore.h>#include <string.h>#include "shmem.h" void report_and_exit(const char* msg) { [perror][4](msg); [exit][5](-1);} int main() { int fd = shm_open(BackingFile, /* name from smem.h */ O_RDWR | O_CREAT, /* read/write, create if needed */ AccessPerms); /* access permissions (0644) */ if (fd < 0) report_and_exit("Can't open shared mem segment..."); ftruncate(fd, ByteSize); /* get the bytes */ caddr_t memptr = mmap(NULL, /* let system pick where to put segment */ ByteSize, /* how many bytes */ PROT_READ | PROT_WRITE, /* access protections */ MAP_SHARED, /* mapping visible to other processes */ fd, /* file descriptor */ 0); /* offset: start at 1st byte */ if ((caddr_t) -1 == memptr) report_and_exit("Can't get segment..."); [fprintf][7](stderr, "shared mem address: %p [0..%d]\n", memptr, ByteSize - 1); [fprintf][7](stderr, "backing file: /dev/shm%s\n", BackingFile ); /* semahore code to lock the shared mem */ sem_t* semptr = sem_open(SemaphoreName, /* name */ O_CREAT, /* create the semaphore */ AccessPerms, /* protection perms */ 0); /* initial value */ if (semptr == (void*) -1) report_and_exit("sem_open"); [strcpy][8](memptr, MemContents); /* copy some ASCII bytes to the segment */ /* increment the semaphore so that memreader can read */ if (sem_post(semptr) < 0) report_and_exit("sem_post"); sleep(12); /* give reader a chance */ /* clean up */ munmap(memptr, ByteSize); /* unmap the storage */ close(fd); sem_close(semptr); shm_unlink(BackingFile); /* unlink from the backing file */ return 0;}
下面是 memwriter
和 memreader
程序如何通過共享內存來通信的一個總結:
上面展示的 memwriter
程序調用 shm_open
函數來得到作為系統協調共享內存的備份文件的文件描述符。此時,并沒有內存被分配。接下來調用的是令人誤解的名為 ftruncate
的函數
它將分配 ByteSize
字節的內存,在該情況下,一般為大小適中的 512 字節。memwriter
和 memreader
程序都只從共享內存中獲取數據,而不是從備份文件。系統將負責共享內存和備份文件之間數據的同步。
ftruncate(fd, ByteSize); /* get the bytes */
接著 memwriter
調用 mmap
函數:
來獲得共享內存的指針。(memreader
也做一次類似的調用。) 指針類型 caddr_t
以 c
開頭,它代表 calloc
,而這是動態初始化分配的內存為 0 的一個系統函數。memwriter
通過庫函數 strcpy
(字符串復制)來獲取后續寫操作的 memptr
。
caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
ByteSize, /* how many bytes */
PROT_READ | PROT_WRITE, /* access protections */
MAP_SHARED, /* mapping visible to other processes */
fd, /* file descriptor */
0); /* offset: start at 1st byte */
到現在為止,memwriter
已經準備好進行寫操作了,但首先它要創建一個信號量來確保共享內存的排斥性。假如 memwriter
正在執行寫操作而同時 memreader
在執行讀操作,則有可能出現競爭條件。假如調用 sem_open
成功了:
那么,接著寫操作便可以執行。上面的 SemaphoreName
(任意一個唯一的非空名稱)用來在 memwriter
和 memreader
識別信號量。初始值 0 將會傳遞給信號量的創建者,在這個例子中指的是 memwriter
賦予它執行寫操作的權利。
sem_t* semptr = sem_open(SemaphoreName, /* name */
O_CREAT, /* create the semaphore */
AccessPerms, /* protection perms */
0); /* initial value */
在寫操作完成后,memwriter* 通過調用
sem_post` 函數將信號量的值增加到 1:
增加信號了將釋放互斥鎖,使得 memreader
可以執行它的讀操作。為了更好地測量,memwriter
也將從它自己的地址空間中取消映射,
這將使得 memwriter
不能進一步地訪問共享內存。
munmap(memptr, ByteSize); /* unmap the storage *
if (sem_post(semptr) < 0) ..
/** Compilation: gcc -o memreader memreader.c -lrt -lpthread **/#include <stdio.h>#include <stdlib.h>#include <sys/mman.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <semaphore.h>#include <string.h>#include "shmem.h" void report_and_exit(const char* msg) { [perror][4](msg); [exit][5](-1);} int main() { int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /* empty to begin */ if (fd < 0) report_and_exit("Can't get file descriptor..."); /* get a pointer to memory */ caddr_t memptr = mmap(NULL, /* let system pick where to put segment */ ByteSize, /* how many bytes */ PROT_READ | PROT_WRITE, /* access protections */ MAP_SHARED, /* mapping visible to other processes */ fd, /* file descriptor */ 0); /* offset: start at 1st byte */ if ((caddr_t) -1 == memptr) report_and_exit("Can't access segment..."); /* create a semaphore for mutual exclusion */ sem_t* semptr = sem_open(SemaphoreName, /* name */ O_CREAT, /* create the semaphore */ AccessPerms, /* protection perms */ 0); /* initial value */ if (semptr == (void*) -1) report_and_exit("sem_open"); /* use semaphore as a mutex (lock) by waiting for writer to increment it */ if (!sem_wait(semptr)) { /* wait until semaphore != 0 */ int i; for (i = 0; i < [strlen][6](MemContents); i++) write(STDOUT_FILENO, memptr + i, 1); /* one byte at a time */ sem_post(semptr); } /* cleanup */ munmap(memptr, ByteSize); close(fd); sem_close(semptr); unlink(BackingFile); return 0;}
memwriter
和 memreader
程序中,共享內存的主要著重點都在 shm_open
和 mmap
函數上:在成功時,***個調用返回一個備份文件的文件描述符,而第二個調用則使用這個文件描述符從共享內存段中獲取一個指針。它們對 shm_open
的調用都很相似,除了 memwriter
程序創建共享內存,而 `memreader 只獲取這個已經創建的內存:
int fd = shm_open(BackingFile, O_RDWR | O_CREAT, AccessPerms); /* memwriter */int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /* memreader */
有了文件描述符,接著對 mmap
的調用就是類似的了:
caddr_t memptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
mmap
的***個參數為 NULL
,這意味著讓系統自己決定在虛擬內存地址的哪個地方分配內存,當然也可以指定一個地址(但很有技巧性)。MAP_SHARED
標志著被分配的內存在進程中是共享的,***一個參數(在這個例子中為 0 ) 意味著共享內存的偏移量應該為***個字節。size
參數特別指定了將要分配的字節數目(在這個例子中是 512);另外的保護參數(AccessPerms
)暗示著共享內存是可讀可寫的。
當 memwriter
程序執行成功后,系統將創建并維護備份文件,在我的系統中,該文件為 /dev/shm/shMemEx
,其中的 shMemEx
是我為共享存儲命名的(在頭文件 shmem.h
中給定)。在當前版本的 memwriter
和 memreader
程序中,下面的語句
shm_unlink(BackingFile); /* removes backing file */
將會移除備份文件。假如沒有 unlink
這個語句,則備份文件在程序終止后仍然持久地保存著。
memreader
和 memwriter
一樣,在調用 sem_open
函數時,通過信號量的名字來獲取信號量。但 memreader
隨后將進入等待狀態,直到 memwriter
將初始值為 0 的信號量的值增加。
if (!sem_wait(semptr)) { /* wait until semaphore != 0 */
一旦等待結束,memreader
將從共享內存中讀取 ASCII 數據,然后做些清理工作并終止。
共享內存 API 包括顯式地同步共享內存段和備份文件。在這次的示例中,這些操作都被省略了,以免文章顯得雜亂,好讓我們專注于內存共享和信號量的代碼。
即便在信號量代碼被移除的情況下,memwriter
和 memreader
程序很大幾率也能夠正常執行而不會引入競爭條件:memwriter
創建了共享內存段,然后立即向它寫入;memreader
不能訪問共享內存,直到共享內存段被創建好。然而,當一個寫操作處于混合狀態時,***實踐需要共享內存被同步。信號量 API 足夠重要,值得在代碼示例中著重強調。
以上是“Linux中如何共享存儲”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。