一.管道機(jī)制(pipe)
1.Linux的fork操作
在計(jì)算機(jī)領(lǐng)域中,尤其是Unix及類Unix系統(tǒng)操作系統(tǒng)中,fork是一種復(fù)制自身而創(chuàng)建自身進(jìn)程副本的操作。它通常是內(nèi)核實(shí)現(xiàn)的一種系統(tǒng)調(diào)用。Fork是類Unix操作系統(tǒng)上創(chuàng)建進(jìn)程的一種主要方法,甚至歷史上是唯一方法。
由fork創(chuàng)建的新進(jìn)程被稱為子進(jìn)程(child process)。該函數(shù)被調(diào)用一次,但返回兩次。兩次返回的區(qū)別是子進(jìn)程的返回值是0,而父進(jìn)程的返回值則是新進(jìn)程(子進(jìn)程)的進(jìn)程 id。將子進(jìn)程id返回給父進(jìn)程的理由是:一個(gè)進(jìn)程的子進(jìn)程可以多于一個(gè),沒有一個(gè)函數(shù)使一個(gè)進(jìn)程可以獲得其所有子進(jìn)程的進(jìn)程id。對子進(jìn)程來說,之所以fork返回0給它,是因?yàn)樗S時(shí)可以調(diào)用getpid()來獲取自己的pid;也可以調(diào)用getppid()來獲取父進(jìn)程的id。(進(jìn)程id 0總是由交換進(jìn)程使用,所以一個(gè)子進(jìn)程的進(jìn)程id不可能為0 )。
fork之后,操作系統(tǒng)會復(fù)制一個(gè)與父進(jìn)程完全相同的子進(jìn)程,雖說是父子關(guān)系,但是在操作系統(tǒng)看來,他們更像兄弟關(guān)系,這2個(gè)進(jìn)程共享代碼空間,但是數(shù)據(jù)空間是互相獨(dú)立的,子進(jìn)程數(shù)據(jù)空間中的內(nèi)容是父進(jìn)程的完整拷貝,指令指針也完全相同,子進(jìn)程擁有父進(jìn)程當(dāng)前運(yùn)行到的位置(兩進(jìn)程的程序計(jì)數(shù)器pc值相同,也就是說,子進(jìn)程是從fork返回處開始執(zhí)行的),但有一點(diǎn)不同——如果fork成功,子進(jìn)程中fork的返回值是0,父進(jìn)程中fork的返回值是子進(jìn)程的進(jìn)程號;如果fork不成功,父進(jìn)程會返回錯(cuò)誤。
可以這樣想象,2個(gè)進(jìn)程一直同時(shí)運(yùn)行,而且步調(diào)一致,在fork之后,他們分別作不同的工作,也就是分岔了。這也是fork為什么叫fork的原因。至于那一個(gè)最先運(yùn)行,可能與操作系統(tǒng)(調(diào)度算法)有關(guān),而且這個(gè)問題在實(shí)際應(yīng)用中并不重要。
為什么將管道機(jī)制之前要先講講fork機(jī)制呢?因?yàn)楣艿罊C(jī)制是建立在"具有親緣關(guān)系的進(jìn)程之間”,而如何找到這兩個(gè)進(jìn)程?筆者的理解是可以通過他們的id號來尋找——因?yàn)橥ㄟ^上面的額fork機(jī)制的講解,我們知道父子、兄弟進(jìn)程之間的id號是彼此可知的(理解不到位見諒)。
2.什么是管道機(jī)制?
管道是Linux/UNIX系統(tǒng)中比較原始的通信方式,他的實(shí)現(xiàn)以一種數(shù)據(jù)流的方式,在進(jìn)程之間流動。在Linux系統(tǒng)中,“一切皆文件”,因此"管道(pipe)"其相當(dāng)于文件系統(tǒng)上的一個(gè)文件,來緩存所要傳輸?shù)臄?shù)據(jù)。這個(gè)文件是一種“特殊的文件”,它和一般文件有些不同的:
- 管道是內(nèi)核管理的一個(gè)固定大小的緩沖區(qū)。在Linux 中,該緩沖區(qū)的大小為1 頁,即4KB,使得它的大小不像文件那樣不加檢驗(yàn)地增長。
- 當(dāng)管道中的數(shù)據(jù)被讀出時(shí),管道中就沒有數(shù)據(jù)了,文件沒有這個(gè)特性。注意從管道讀數(shù)據(jù)是一次性操作,數(shù)據(jù)一旦被讀,它就從管道中被拋棄,釋放空間以便寫更多的數(shù)據(jù)。
在寫管道時(shí)可能變滿,當(dāng)這種情況發(fā)生時(shí),隨后對管道的write()調(diào)用將默認(rèn)地被阻塞,等待某些數(shù)據(jù)被讀取,以便騰出足夠的空間供write()調(diào)用寫;當(dāng)所有當(dāng)前進(jìn)程數(shù)據(jù)已被讀取時(shí),管道變空,當(dāng)這種情況發(fā)生時(shí),一個(gè)隨后的read()調(diào)用將默認(rèn)地被阻塞,等待某些數(shù)據(jù)被寫入,這解決了read()調(diào)用返回文件結(jié)束的問題。
3.管道的實(shí)現(xiàn)機(jī)制
管道是由內(nèi)核管理的一個(gè)緩沖區(qū),相當(dāng)于我們放入內(nèi)存中的一個(gè)紙條。管道的一端連接一個(gè)進(jìn)程的輸出。這個(gè)進(jìn)程會向管道中放入信息。管道的另一端連接一個(gè)進(jìn)程的輸入,這個(gè)進(jìn)程取出被放入管道的信息。當(dāng)管道中沒有信息的話,從管道中讀取的進(jìn)程會等待,直到另一端的進(jìn)程放入信息。當(dāng)管道被放滿信息的時(shí)候,嘗試放入信息的進(jìn)程會等待,直到另一端的進(jìn)程取出信息。當(dāng)兩個(gè)進(jìn)程都終結(jié)的時(shí)候,管道也自動消失。
??在Linux 中,管道的實(shí)現(xiàn)并沒有使用專門的數(shù)據(jù)結(jié)構(gòu),而是借助了文件系統(tǒng)的file 結(jié)構(gòu)和VFS(Virtual File System,虛擬文件系統(tǒng))的索引節(jié)點(diǎn)inode(inode是管理一個(gè)具體的文件的模塊,是文件的唯一標(biāo)識,一個(gè)文件對應(yīng)一個(gè)inode)。通過將兩個(gè) file 結(jié)構(gòu)指向同一個(gè)臨時(shí)的 VFS 索引節(jié)點(diǎn),而這個(gè) VFS 索引節(jié)點(diǎn)又指向一個(gè)物理頁面而實(shí)現(xiàn)的。
圖中有兩個(gè) file 數(shù)據(jù)結(jié)構(gòu),它們定義文件操作例程地址是不同的,其中一個(gè)是向管道中寫入數(shù)據(jù)的例程地址,而另一個(gè)是從管道中讀出數(shù)據(jù)的例程地址。這樣,用戶程序的系統(tǒng)調(diào)用仍然是通常的文件操作,而內(nèi)核卻利用這種抽象機(jī)制實(shí)現(xiàn)了管道這一特殊操作。
4.管道機(jī)制的特點(diǎn)
- 管道是半雙工的,數(shù)據(jù)只能向一個(gè)方向流動;需要雙方通信時(shí),需要建立起兩個(gè)管道;
- 只能用于具有公共祖先(具有親緣關(guān)系的進(jìn)程)的進(jìn)程之間通信,即用于父子進(jìn)程或者兄弟進(jìn)程之間;
- 單獨(dú)構(gòu)成一種獨(dú)立的文件系統(tǒng):管道對于管道兩端的進(jìn)程而言,就是一個(gè)文件,但它不是普通的文件,它不屬于某種文件系統(tǒng),而是自立門戶,單獨(dú)構(gòu)成一種文件系統(tǒng),并且只存在于內(nèi)存中;
- 數(shù)據(jù)的讀出和寫入:一個(gè)進(jìn)程向管道中寫的內(nèi)容被管道另一端的進(jìn)程讀出。數(shù)據(jù)的流通方式是以一種“先進(jìn)先出”的隊(duì)列數(shù)據(jù)結(jié)構(gòu)進(jìn)行,即寫入的內(nèi)容每次都添加在管道緩沖區(qū)的末尾,并且每次都是從緩沖區(qū)的頭部讀出數(shù)據(jù)。
5.管道的創(chuàng)建即讀寫規(guī)則
Linux環(huán)境下使用pipe函數(shù)創(chuàng)建一個(gè)匿名半雙工管道,其函數(shù)原型如下:
#include <unistd.h>
int pipe(int fd[2])
參數(shù)int fd[2]為一個(gè)長度為2的文件描述數(shù)組,fd[0]是讀出端,fd[1]是寫入端,函數(shù)返回值為0表示成功,-1表示失敗。當(dāng)函數(shù)成功返回,則自動維護(hù)了一個(gè)從fd[1]到fd[0]的數(shù)據(jù)通道。
??需要注意的是,管道的兩端是固定了任務(wù)的。即一端只能用于讀,由描述字fd[0]表示,稱其為管道讀端;另一端則只能用于寫,由描述字fd[1]來表示,稱其為管道寫端。
二.命名管道機(jī)制(FIFO)
上面說的管道通信機(jī)制的缺點(diǎn)就是,它們的關(guān)系一定是父子進(jìn)程的關(guān)系,這就使得它的使用受到了一點(diǎn)的限制,但是我們可以使用命名管道來解決這個(gè)問題。
1.什么是命名管道?
命名管道也被稱為FIFO文件,它在文件系統(tǒng)中以文件名的形式存在,和之前所講的沒有名字的管道(匿名管道)類似,有名管道也是半雙工的通信方式,但是它允許無親緣關(guān)系進(jìn)程間的通信。
??由于Linux中所有的事物都可被視為文件,所以對命名管道的使用也就變得與文件操作非常的統(tǒng)一,也使它的使用非常方便,同時(shí)我們也可以像平常的文件名一樣在命令中使用。
2.命名管道的特點(diǎn)
- FIFO不同于管道之處在于它提供一個(gè)路徑名與之關(guān)聯(lián),以FIFO的文件形式存在于文件系統(tǒng)中。這樣,即使與FIFO的創(chuàng)建進(jìn)程不存在親緣關(guān)系的進(jìn)程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
經(jīng)該函數(shù)創(chuàng)建一個(gè)FIFO,F(xiàn)IFO在文件系統(tǒng)中表現(xiàn)為為一個(gè)文件(FIFO類型文件),其中文件的路徑由參數(shù)pathname指定(如果pathname路徑下的文件已經(jīng)存在則mkfifo返回-1),mode指定FIFO的讀寫權(quán)限(每個(gè)進(jìn)程在調(diào)用這個(gè)方法的時(shí)候要指明自己的額權(quán)限,是寫進(jìn)程還是讀進(jìn)程)。
說了這么多了,其實(shí)FIFO就是指明了一個(gè)文件路徑,創(chuàng)建了一個(gè)文件,然后通信的雙方進(jìn)程都往這個(gè)文件中讀寫東西,只不過一個(gè)讀一個(gè)寫而已——這樣只要雙方都知道了這個(gè)約定路徑就可以讀寫,那么就不必一定具有親緣關(guān)系了。當(dāng)刪除FIFO文件時(shí),管道連接也隨之消失。
有名管道的名字(包括路徑名和讀寫權(quán)限)存在于文件系統(tǒng)中,內(nèi)容存放在內(nèi)存中。這樣只要路徑常駐與文件系統(tǒng)(或者磁盤)中,那么在就可快速的多次通信。
三.消息隊(duì)列
Linux 中的消息可以被描述成在內(nèi)核地址空間的一個(gè)內(nèi)部鏈表,每一個(gè)消息隊(duì)列由一個(gè)IPC 的標(biāo)識號唯一地標(biāo)識。Linux 為系統(tǒng)中所有的消息隊(duì)列維護(hù)一個(gè) msgque 鏈表,該鏈表中的每個(gè)指針指向一個(gè) msgid_ds 結(jié)構(gòu),該結(jié)構(gòu)完整描述一個(gè)消息隊(duì)列。
??這種通信方式,個(gè)人覺得非常類似于Android的Looper消息機(jī)制中的MessageQuene,即數(shù)據(jù)隊(duì)列由一個(gè)鏈表構(gòu)成,鏈表上的每一個(gè)消息都包含了該消息的特定格式和優(yōu)先級的記錄。進(jìn)程間通過消息隊(duì)列通信,主要是:創(chuàng)建或打開消息隊(duì)列,添加消息,讀取消息和控制消息隊(duì)列。
因?yàn)橄㈥?duì)列獨(dú)立于進(jìn)程而存在,為了區(qū)別不同的消息隊(duì)列,需要以key值標(biāo)記消息隊(duì)列,這樣兩個(gè)不相關(guān)進(jìn)程可以通過事先約定的key值通過消息隊(duì)列進(jìn)行消息收發(fā)。例如進(jìn)程A向key消息隊(duì)列發(fā)送消息,進(jìn)程B從Key消息隊(duì)列讀取消息。
消息隊(duì)列的優(yōu)勢
- 消息隊(duì)列與管道通信相比,其優(yōu)勢是對每個(gè)消息指定特定的消息類型,接收的時(shí)候不需要按照先進(jìn)先出的次序讀取,而是可以根據(jù)自定義條件接收特定類型的消息。
- 與管道(匿名管道:只存在于內(nèi)存中的文件;命名管道:存在于實(shí)際的磁盤介質(zhì)或者文件系統(tǒng))不同的是消息隊(duì)列存放在內(nèi)核中,只有在內(nèi)核重啟(即,操作系統(tǒng)重啟)或者顯示地刪除一個(gè)消息隊(duì)列時(shí),該消息隊(duì)列才會被真正的刪除;而管道只要關(guān)閉其中的數(shù)據(jù)就會丟失(當(dāng)然命名管道存放在磁盤上的名稱不會因此丟失,除非顯示的刪除這個(gè)路徑名及其對應(yīng)的文件)。
- 另外與管道不同的是,消息隊(duì)列獨(dú)立于發(fā)送與接收進(jìn)程,在某個(gè)進(jìn)程往一個(gè)隊(duì)列寫入消息之前,并不需要另外某個(gè)進(jìn)程在該隊(duì)列上等待消息的到達(dá)。
關(guān)于消息隊(duì)列我們就說到這里,這種IPC機(jī)制在實(shí)際的應(yīng)用中并不多。
四.System V 共享內(nèi)存
1.共享內(nèi)存的概念
共享內(nèi)存可以說是最有用的進(jìn)程間通信方式,也是最快的IPC形式。兩個(gè)不同進(jìn)程A、B共享內(nèi)存的意思是,同一塊物理內(nèi)存被映射到進(jìn)程A、B各自的進(jìn)程地址空間。進(jìn)程A可以即時(shí)看到進(jìn)程B對共享內(nèi)存中數(shù)據(jù)的更新,反之亦然。由于多個(gè)進(jìn)程共享同一塊內(nèi)存區(qū)域,必然需要某種同步機(jī)制,互斥鎖和信號量都可以。
??要解釋清楚共享內(nèi)存這種IPC機(jī)制,我們不得不從Linux中的文件結(jié)構(gòu)講起:
2.System V 共享內(nèi)存實(shí)現(xiàn)原理
(1).Linux文件系統(tǒng)
操作系統(tǒng)的主要功能是為管理硬件資源和為應(yīng)用程序開發(fā)人員提供良好的環(huán)境,但是計(jì)算機(jī)系統(tǒng)的各種硬件資源是有限的,因此為了保證每一個(gè)進(jìn)程都能安全的執(zhí)行。處理器設(shè)有兩種模式:“用戶模式”與“內(nèi)核模式”。一些容易發(fā)生安全問題的操作都被限制在只有內(nèi)核模式下才可以執(zhí)行,例如I/O操作,修改基址寄存器內(nèi)容等。而連接用戶模式和內(nèi)核模式的接口稱之為系統(tǒng)調(diào)用。
我們知道,系統(tǒng)內(nèi)核為一個(gè)進(jìn)程分配內(nèi)存地址時(shí),通過分頁映射機(jī)制可以讓一個(gè)進(jìn)程的物理地址不連續(xù)只需保證進(jìn)程,但是進(jìn)程得到的虛擬地址是連續(xù)的。進(jìn)程的虛擬地址空間可分為兩部分,內(nèi)核空間和用戶空間。內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù),而進(jìn)程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)。不管是內(nèi)核空間還是用戶空間,它們都處于虛擬空間中,都是對物理地址的映射。
應(yīng)用程序中實(shí)現(xiàn)對文件的操作過程就是典型的系統(tǒng)調(diào)用過程。
虛擬文件系統(tǒng)
一個(gè)操作系統(tǒng)可以支持多種底層不同的文件系統(tǒng)(比如NTFS, FAT, ext3, ext4),為了給內(nèi)核和用戶進(jìn)程提供統(tǒng)一的文件系統(tǒng)視圖,Linux在用戶進(jìn)程和底層文件系統(tǒng)之間加入了一個(gè)抽象層,即虛擬文件系統(tǒng)(Virtual File System, VFS),進(jìn)程所有的文件操作都通過VFS,由VFS來適配各種底層不同的文件系統(tǒng),完成實(shí)際的文件操作。
通俗的說,VFS就是定義了一個(gè)通用文件系統(tǒng)的接口層和適配層,一方面為用戶進(jìn)程提供了一組統(tǒng)一的訪問文件,目錄和其他對象的統(tǒng)一方法,另一方面又要和不同的底層文件系統(tǒng)進(jìn)行適配。如圖所示:
虛擬文件系統(tǒng)主要模塊
1、超級塊(super_block),用于保存一個(gè)文件系統(tǒng)的所有元數(shù)據(jù),相當(dāng)于這個(gè)文件系統(tǒng)的信息庫,為其他的模塊提供信息。因此一個(gè)超級塊可代表一個(gè)文件系統(tǒng)。文件系統(tǒng)的任意元數(shù)據(jù)修改都要修改超級塊。超級塊對象是常駐內(nèi)存并被緩存的。
2、目錄項(xiàng)模塊,管理路徑的目錄項(xiàng)。比如一個(gè)路徑 /home/foo/hello.txt,那么目錄項(xiàng)有home, foo, hello.txt。目錄項(xiàng)的塊,存儲的是這個(gè)目錄下的所有的文件的inode號和文件名等信息。其內(nèi)部是樹形結(jié)構(gòu),操作系統(tǒng)檢索一個(gè)文件,都是從根目錄開始,按層次解析路徑中的所有目錄,直到定位到文件。
3、inode模塊,管理一個(gè)具體的文件,是文件的唯一標(biāo)識,一個(gè)文件對應(yīng)一個(gè)inode。通過inode可以方便的找到文件在磁盤扇區(qū)的位置。同時(shí)inode模塊可鏈接到address_space模塊,方便查找自身文件數(shù)據(jù)是否已經(jīng)緩存。
4、打開文件列表模塊,包含所有內(nèi)核已經(jīng)打開的文件。已經(jīng)打開的文件對象由open系統(tǒng)調(diào)用在內(nèi)核中創(chuàng)建,也叫文件句柄。打開文件列表模塊中包含一個(gè)列表,每個(gè)列表表項(xiàng)是一個(gè)結(jié)構(gòu)體struct file,結(jié)構(gòu)體中的信息用來表示打開的一個(gè)文件的各種狀態(tài)參數(shù)。
5、file_operations模塊。這個(gè)模塊中維護(hù)一個(gè)數(shù)據(jù)結(jié)構(gòu),是一系列函數(shù)指針的集合,其中包含所有可以使用的系統(tǒng)調(diào)用函數(shù),例如open、read、write、mmap等。每個(gè)打開文件(打開文件列表模塊的一個(gè)表項(xiàng))都可以連接到file_operations模塊,從而對任何已打開的文件,通過系統(tǒng)調(diào)用函數(shù),實(shí)現(xiàn)各種操作。
6、address_space模塊,它表示一個(gè)文件在頁緩存中已經(jīng)緩存了的物理頁。它是頁緩存和外部設(shè)備中文件系統(tǒng)的橋梁。如果將文件系統(tǒng)可以理解成數(shù)據(jù)源,那么address_space可以說關(guān)聯(lián)了內(nèi)存系統(tǒng)和文件系統(tǒng)。
I/O 緩沖區(qū)
如高速緩存(cache)產(chǎn)生的原理類似,在I/O過程中,讀取磁盤的速度相對內(nèi)存讀取速度要慢的多。因此為了能夠加快處理數(shù)據(jù)的速度,需要將讀取過的數(shù)據(jù)緩存在內(nèi)存里。而這些緩存在內(nèi)存里的數(shù)據(jù)就是高速緩沖區(qū)(buffer cache),下面簡稱為“buffer”。
??具體來說,buffer(緩沖區(qū))是一個(gè)用于存儲速度不同步的設(shè)備或優(yōu)先級不同的設(shè)備之間傳輸數(shù)據(jù)的區(qū)域。一方面,通過緩沖區(qū),可以使進(jìn)程之間的相互等待變少,從而使從速度慢的設(shè)備讀入數(shù)據(jù)時(shí),速度快的設(shè)備的操作進(jìn)程不發(fā)生間斷。另一方面,可以保護(hù)硬盤或減少網(wǎng)絡(luò)傳輸?shù)拇螖?shù)。
- Buffer和Cache
buffer和cache是兩個(gè)不同的概念:cache是高速緩存,用于CPU和內(nèi)存之間的緩沖;buffer是I/O緩存,用于內(nèi)存和硬盤的緩沖;簡單的說,cache是加速“讀”,而buffer是緩沖“寫”,前者解決讀的問題,保存從磁盤上讀出的數(shù)據(jù),后者是解決寫的問題,保存即將要寫入到磁盤上的數(shù)據(jù)。
- Buffer Cache和 Page Cache
buffer cache和page cache都是為了處理設(shè)備和內(nèi)存交互時(shí)高速訪問的問題。buffer cache可稱為塊緩沖器,page cache可稱為頁緩沖器。頁緩存page cache面向的是虛擬內(nèi)存(RAM和文件之間),塊I/O緩存Buffer cache是面向塊設(shè)備(磁盤等之間)。
??在linux不支持虛擬內(nèi)存機(jī)制之前,還沒有頁的概念,因此緩沖區(qū)以塊為單位對設(shè)備進(jìn)行。在linux采用虛擬內(nèi)存的機(jī)制來管理內(nèi)存后,頁是虛擬內(nèi)存管理的最小單位,開始采用頁緩沖的機(jī)制來緩沖內(nèi)存。
??buffer cache和page cache兩者最大的區(qū)別是緩存的粒度。buffer cache面向的是文件系統(tǒng)的塊。而內(nèi)核的內(nèi)存管理組件采用了比文件系統(tǒng)的塊更高級別的抽象:頁page,其處理的性能更高。因此和內(nèi)存管理交互的緩存組件,都使用頁緩存。
Page Cache
頁緩存是面向文件,面向內(nèi)存的。通俗來說,它位于內(nèi)存和文件之間緩沖區(qū),文件IO操作實(shí)際上只和page cache交互,不直接和內(nèi)存交互。page cache可以用在所有以文件為單元的場景下,比如網(wǎng)絡(luò)文件系統(tǒng)等等。page cache通過一系列的數(shù)據(jù)結(jié)構(gòu),比如inode, address_space, struct page,實(shí)現(xiàn)將一個(gè)文件映射到頁的級別:
struct page結(jié)構(gòu)標(biāo)志一個(gè)物理內(nèi)存頁,通過page + offset就可以將此頁幀定位到一個(gè)文件中的具體位置。同時(shí)struct page還有以下重要參數(shù):
- 標(biāo)志位flags來記錄該頁是否是臟頁,是否正在被寫回等等;
- mapping指向了地址空間address_space,表示這個(gè)頁是一個(gè)頁緩存中頁,和一個(gè)文件的地址空間對應(yīng);
- index記錄這個(gè)頁在文件中的頁偏移量;
文件讀寫基本流程
讀文件
- 1、進(jìn)程調(diào)用庫函數(shù)向內(nèi)核發(fā)起讀文件請求;
- 2、內(nèi)核通過檢查進(jìn)程的文件描述符定位到虛擬文件系統(tǒng)的已打開文件列表表項(xiàng);
- 3、調(diào)用該文件可用的系統(tǒng)調(diào)用函數(shù)read()
- 3、read()函數(shù)通過文件表項(xiàng)鏈接到目錄項(xiàng)模塊,根據(jù)傳入的文件路徑,在目錄項(xiàng)模塊中檢索,找到該文件的inode;
- 4、在inode中,通過文件內(nèi)容偏移量計(jì)算出要讀取的頁;
- 5、通過inode找到文件對應(yīng)的address_space;
- 6、在address_space中訪問該文件的頁緩存樹,查找對應(yīng)的頁緩存結(jié)點(diǎn):
①如果頁緩存命中,那么直接返回文件內(nèi)容;
②如果頁緩存缺失,那么產(chǎn)生一個(gè)頁缺失異常,創(chuàng)建一個(gè)頁緩存頁,同時(shí)通過inode找到文件該頁的磁盤地址,讀取相應(yīng)的頁填充該緩存頁;重新進(jìn)行第6步查找頁緩存; - 7、文件內(nèi)容讀取成功。
寫文件
前5步和讀文件一致,在address_space中查詢對應(yīng)頁的頁緩存是否存在:
- 6、如果頁緩存命中,直接把文件內(nèi)容修改更新在頁緩存的頁中。寫文件就結(jié)束了。這時(shí)候文件修改位于頁緩存,并沒有寫回到磁盤文件中去。
- 7、如果頁緩存缺失,那么產(chǎn)生一個(gè)頁缺失異常,創(chuàng)建一個(gè)頁緩存頁,同時(shí)通過inode找到文件該頁的磁盤地址,讀取相應(yīng)的頁填充該緩存頁。此時(shí)緩存頁命中,進(jìn)行第6步。
- 8、一個(gè)頁緩存中的頁如果被修改,那么會被標(biāo)記成臟頁。臟頁需要寫回到磁盤中的文件塊。有兩種方式可以把臟頁寫回磁盤:
①手動調(diào)用sync()或者fsync()系統(tǒng)調(diào)用把臟頁寫回
②pdflush進(jìn)程會定時(shí)把臟頁寫回到磁盤
原文鏈接:從內(nèi)核文件系統(tǒng)看文件讀寫過程,這里只截取一部分對本文分析有用的段落。
(2)mmap函數(shù)
mmap是一種內(nèi)存映射文件的方法,即將一個(gè)文件或者其它對象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對映關(guān)系。實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應(yīng)的文件磁盤上,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。
mmap內(nèi)存映射的實(shí)現(xiàn)過程,總的來說可以分為三個(gè)階段:
- ①進(jìn)程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
- ②調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系
- ③進(jìn)程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝
mmap和常規(guī)文件操作的區(qū)別
上面我們提到常規(guī)文件系統(tǒng)操作(調(diào)用read/fread等類函數(shù))中,函數(shù)的調(diào)用過程:
- 1.進(jìn)程發(fā)起讀文件請求。
- 2.內(nèi)核通過查找進(jìn)程文件符表,定位到內(nèi)核已打開文件集上的文件信息,從而找到此文件的inode。
- 3.inode在address_space上查找要請求的文件頁是否已經(jīng)緩存在頁緩存中。如果存在,則直接返回這片文件頁的內(nèi)容。
- 4.如果不存在,則通過inode定位到文件磁盤地址,將數(shù)據(jù)從磁盤復(fù)制到頁緩存。之后再次發(fā)起讀頁面過程,進(jìn)而將頁緩存中的數(shù)據(jù)發(fā)給用戶進(jìn)程。
總結(jié)來說,常規(guī)文件操作為了提高讀寫效率和保護(hù)磁盤,使用了頁緩存機(jī)制。這樣造成讀文件時(shí)需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內(nèi)核空間,不能被用戶進(jìn)程直接尋址,所以還需要將頁緩存中數(shù)據(jù)頁再次拷貝到內(nèi)存對應(yīng)的用戶空間中。這樣,通過兩次數(shù)據(jù)拷貝過程,才能完成進(jìn)程對文件內(nèi)容的獲取任務(wù)。寫操作也是一樣,待寫入的buffer在內(nèi)核空間不能直接訪問,必須要先拷貝至內(nèi)核空間對應(yīng)的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝。
而使用mmap操作文件中,創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址以及虛擬內(nèi)存區(qū)域映射這兩步,沒有文件拷貝操作。而之后訪問數(shù)據(jù)時(shí)發(fā)現(xiàn)內(nèi)存中并無數(shù)據(jù)而發(fā)起的缺頁異常過程,可以通過已經(jīng)建立好的映射關(guān)系,只使用一次數(shù)據(jù)拷貝,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中,供進(jìn)程使用。
總而言之,常規(guī)文件操作需要從磁盤到頁緩存再到用戶主存的兩次數(shù)據(jù)拷貝。而mmap操控文件,只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程。說白了,mmap的關(guān)鍵點(diǎn)是實(shí)現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互而省去了空間不同數(shù)據(jù)不通的繁瑣過程。因此mmap效率更高。
(3)System V 共享內(nèi)存建立過程
第一步:shmget函數(shù)創(chuàng)建共享內(nèi)存
shmget函數(shù)的原型為:
int shmget(key_t key, size_t size, int shmflg);
第一個(gè)參數(shù),程序需要提供一個(gè)參數(shù)key(非0整數(shù)),它有效地為共享內(nèi)存段命名,shmget函數(shù)成功時(shí)返回一個(gè)與key相關(guān)的共享內(nèi)存標(biāo)識符(非負(fù)整數(shù)),用于后續(xù)的共享內(nèi)存函數(shù)。調(diào)用失敗返回-1.
??第二個(gè)參數(shù),size以字節(jié)為單位指定需要共享的內(nèi)存容量
??第三個(gè)參數(shù),shmflg是權(quán)限標(biāo)志,他聲明創(chuàng)建這段共享內(nèi)存的進(jìn)程是可讀還是可寫的
進(jìn)程間需要共享的數(shù)據(jù)被放在一個(gè)叫做IPC共享內(nèi)存區(qū)域的地方,任何想要訪問該數(shù)據(jù)的進(jìn)程都必須在本進(jìn)程的地址空間新增一塊內(nèi)存區(qū)域,用來映射IPC共享內(nèi)存區(qū)域的物理內(nèi)存頁面。System V 共享內(nèi)存通過shmget獲得或創(chuàng)建一個(gè)IPC共享內(nèi)存區(qū)域,并返回相應(yīng)的標(biāo)識符(key)。
??上面的shmget()函數(shù)就是用來創(chuàng)建IPC共享區(qū)域的,內(nèi)核在保證shmget獲得或創(chuàng)建一個(gè)共享內(nèi)存區(qū),初始化該共享內(nèi)存區(qū)相應(yīng)的shmid_kernel結(jié)構(gòu)的同時(shí),還將在特殊文件系統(tǒng)shm中,創(chuàng)建并打開一個(gè)同名文件,并在內(nèi)存中建立起該文件的dentry及inode結(jié)構(gòu),新打開的文件不屬于任何一個(gè)進(jìn)程,任何進(jìn)程都可以訪問該共享內(nèi)存區(qū)。
注:每一個(gè)共享內(nèi)存區(qū)都有一個(gè)控制結(jié)構(gòu)struct shmid_kernel。shmid_kernel是共享內(nèi)存區(qū)域中非常重要的一個(gè)數(shù)據(jù)結(jié)構(gòu),它是存儲管理和文件系統(tǒng)結(jié)合起來的橋梁,定義如下:
struct shmid_kernel
{
struct kern_ipc_perm shm_perm; //與內(nèi)核交互,內(nèi)核通過他該結(jié)構(gòu)體來維護(hù)所有共享內(nèi)存區(qū)
struct file * shm_file; //存儲將被映射文件的地址
int id;
unsigned long shm_nattch;
unsigned long shm_segsz;
time_t shm_atim;
time_t shm_dtim;
time_t shm_ctim;
pid_t shm_cprid;
pid_t shm_lprid;
};
該結(jié)構(gòu)中最重要的一個(gè)域應(yīng)該是shm_file,它存儲了將被映射文件的地址。每個(gè)共享內(nèi)存區(qū)對象都對應(yīng)特殊文件系統(tǒng)shm中的一個(gè)文件,一般情況下,特殊文件系統(tǒng)shm中的文件是不能用read()、write()等方法訪問的,當(dāng)采取共享內(nèi)存的方式把其中的文件映射到進(jìn)程地址空間后(mmap函數(shù)映射),可直接采用訪問內(nèi)存的方式對其訪問。內(nèi)核通過數(shù)據(jù)結(jié)構(gòu)struct ipc_ids shm_ids維護(hù)系統(tǒng)中的所有共享內(nèi)存區(qū)域。
第二步:shmat函數(shù)啟動對該共享內(nèi)存的訪問
第一次創(chuàng)建完共享內(nèi)存時(shí),它還不能被任何進(jìn)程訪問,shmat函數(shù)的作用就是用來啟動對該共享內(nèi)存的訪問,并把共享內(nèi)存連接到當(dāng)前進(jìn)程的地址空間。它的原型如下:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
第一個(gè)參數(shù),shm_id是由shmget函數(shù)返回的共享內(nèi)存標(biāo)識。
第二個(gè)參數(shù),shm_addr指定共享內(nèi)存連接到當(dāng)前進(jìn)程中的地址位置,通常為空,表示讓系統(tǒng)來選擇共享內(nèi)存的地址。
第三個(gè)參數(shù),shm_flg是一組標(biāo)志位,通常為0。
調(diào)用shmat()函數(shù)之后,系統(tǒng)會發(fā)現(xiàn)“IPC共享內(nèi)存”在shm中對應(yīng)的文件為空,即發(fā)生缺頁,這個(gè)時(shí)候內(nèi)核會使用 mmap 把這個(gè)文件從磁盤上直接映射到你的進(jìn)程(虛擬)地址空間,這個(gè)時(shí)候你就能直接讀寫映射后的地址了。
??因?yàn)樵谡{(diào)用shmget()時(shí),我們已經(jīng)創(chuàng)建了文件系統(tǒng)shm中的一個(gè)同名文件與共享內(nèi)存區(qū)域相對應(yīng),因此,調(diào)用shmat()的過程相當(dāng)于映射文件系統(tǒng)shm中的同名文件過程。
第三步,進(jìn)程之間共享數(shù)據(jù)
做完前面的兩步,我們就可以利用搭好的橋梁來共享數(shù)據(jù)了,整個(gè)共享的過程中只拷貝一次數(shù)據(jù):從輸入文件到共享內(nèi)存區(qū),之后用戶進(jìn)程就可以直接讀取了。
??進(jìn)程之間在共享內(nèi)存時(shí),會一直保持共享區(qū)域,直到通信完畢為止,這樣,數(shù)據(jù)內(nèi)容一直保存在共享內(nèi)存中,并沒有寫回文件。共享內(nèi)存中的內(nèi)容往往是在解除映射時(shí)才寫回文件的。因此,采用共享內(nèi)存的通信方式效率是非常高的。
3.System V 共享內(nèi)存的特點(diǎn)
1.系統(tǒng)V共享內(nèi)存中的數(shù)據(jù),從來不寫入到實(shí)際磁盤文件中去;而通過mmap()映射普通文件實(shí)現(xiàn)的共享內(nèi)存通信可以指定何時(shí)將數(shù)據(jù)寫入磁盤文件中。注:前面講到,系統(tǒng)V共享內(nèi)存機(jī)制實(shí)際是通過映射特殊文件系統(tǒng)shm中的文件實(shí)現(xiàn)的,文件系統(tǒng)shm的安裝點(diǎn)在交換分區(qū)上,系統(tǒng)重新引導(dǎo)后,所有的內(nèi)容都丟失。
??2.系統(tǒng)V共享內(nèi)存是隨內(nèi)核持續(xù)的,即使所有訪問共享內(nèi)存的進(jìn)程都已經(jīng)正常終止,共享內(nèi)存區(qū)仍然存在(除非顯式刪除共享內(nèi)存),在內(nèi)核重新引導(dǎo)之前,對該共享內(nèi)存區(qū)域的任何改寫操作都將一直保留。
??3.System V 共享內(nèi)存這種IPC機(jī)制中,進(jìn)程真實(shí)所共享的內(nèi)存是特殊文件系統(tǒng)shm中的那個(gè)同名文件。
共享內(nèi)存允許兩個(gè)或多個(gè)進(jìn)程共享一給定的存儲區(qū),因?yàn)閿?shù)據(jù)不需要來回復(fù)制,所以是最快的一種進(jìn)程間通信機(jī)制。共享內(nèi)存可以通過mmap()映射普通文件(特殊情況下還可以采用匿名映射)機(jī)制實(shí)現(xiàn),也可以通過系統(tǒng)V共享內(nèi)存機(jī)制實(shí)現(xiàn)。應(yīng)用接口和原理很簡單,內(nèi)部機(jī)制復(fù)雜。為了實(shí)現(xiàn)更安全通信,往往還與信號量等同步機(jī)制共同使用,實(shí)現(xiàn)進(jìn)程同步。