神奇的共享記憶體

語言: CN / TW / HK

攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第2天,點選檢視活動詳情

前言

共享記憶體(shared memory)是最常見的ipc程序之間通訊的方式之一了,很多linux書籍上,都將共享記憶體評價為“最有用的ipc機制”,就連Binder機制盛行的android體系,同樣也離不開共享記憶體的應用!在所以ipc方式中,共享記憶體以“快”贏得了很多開發者的掌聲,我們下面深入看看!

共享記憶體相關函式

image.png 首先講到共享記憶體,那麼肯定離不開要介紹幾個函式

shmget

int shmget(key_t key, size_t size, int shmflg); shmget函式用來獲取一個記憶體區的ipc標識,這個標識在核心中,屬於一個身份識別符號號(ipc識別符號,正常情況下是不會重複的,但是識別符號也有限制的,比如linux2.4最大為32768,用完了就會重新計算),通過shmget呼叫,會返回給我們當前的ipc標識,如果這個共享記憶體區本來就不存在,就直接建立,否則就把當前標識直接返回給我們!說了一大堆,其實很簡單,就相當於給我們返回了一個代表該共享記憶體的標識罷了!

shmat

void *shmat(int shmid, const void *shmaddr, int shmflg); shmat把一個共享記憶體區域新增到程序上,我們之前在mmap這一章節有提到過線性區的概念,就是程序可用的一組地址(可以用,但是用的時候才真正分配),而shmat就把共享記憶體的這塊地址,通過(shmid shmget可以獲取到的)放到了程序中的可用地址範圍內,用範圍內的合適地址(shmaddr這裡指程序想要發生對映的可用地址)指向了共享記憶體實際的地址,可以見上圖!

shmdt

int shmdt(const void *shmaddr); 用於從當前程序把指定的共享記憶體shmaddr地址分離出去,這裡只是分離,只是從當前程序中不可見了,但是對於其他程序來說,還是依舊存在的,再拿上面的圖舉例子,如果程序1中呼叫了shmadt,那麼當前狀態就如下圖所示

image.png 同時這裡有個非常需要注意的點,就是就算共享記憶體沒有被其他任何程序使用,它所佔有的頁也是不能直接被刪除的,只能用“頁的換出”操作代替不用的頁(留個疑問,後文解析)

image.png 當然,為了避免使用者態過程中共享記憶體的過分建立,一般的限制大小為4096個

共享記憶體本質

看到這裡的朋友,包括我,一定會想問,共享記憶體最本質是個什麼東西呀?為什麼linux會建立處理這麼一個神奇的東西?在這裡我可以告訴大家,共享記憶體其實就是一個“檔案”!不光如此,我們所熟知的ipc方式,比如管道,訊息佇列,共享記憶體,其實就是對檔案的操作!我的天,我們嗤之以鼻的“檔案”,最不起眼不被用的ipc方式,只是換了個名稱,就讓大家高攀不起了!是的,共享記憶體的本質,其實就是shm特殊檔案系統的一個檔案罷了!因為shm檔案系統在linux系統中沒有安裝點,即沒有視覺化的檔案路徑,普通使用者無法“看到”或者“摸到”,就給我們產生了一個錯覺,以為是一個很高深的東西,其實並沒有啦!一個共享記憶體,其實就是一個檔案,只不過這個檔案我們看不到罷了,但是linux核心能看到,就這麼簡單!(以後面試官問到ipc有哪些,回答“檔案”即可哈哈哈,手動狗頭)

那麼接下來又有一個問題了,為什麼一個檔案能有這麼大的奇效,我們常說的共享記憶體只需要一次拷貝(假如程序a寫入到程序b可見算一次)呀,面試官還經常問我們呢!一個小小檔案怎麼做到的?沒錯,沒錯!就是mmap搞得鬼呀!屬於共享記憶體的這個檔案,在程序中其實就是使用了mmap操作,把程序的地址對映到了這個檔案,所以寫入一次就對其他同樣進行mmap的程序可見罷了!這個mmap,是通過shm_mmap函式實現的(細節可看官網,這裡就不貼出來了)最後我們再看一下共享記憶體的核心資料結構,shmid_kernel

``` struct shmid_kernel / private to the kernel / {
struct kern_ipc_perm shm_perm; //描述程序間通訊許可的結構 struct file * shm_file; //指向共享記憶體檔案的指標 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 pid_t shm_lprid;//最後使用程序的PID

....

}; ```

共享記憶體頁回收問題

我們剛剛留下了一個疑問點,就是共享記憶體的頁就算沒有程序引用,也不能被直接刪除,而是採用換出的方式!為什麼不能被刪除呢?因為在正常情況下,linux核心中對於頁刪除有比較嚴格的判斷,頁被刪除的前提需要頁被標記被髒,觸發磁碟寫回的操作,然後才會從刪除這個頁!但是共享記憶體的頁其實在磁碟上是沒有存在對映的索引節點的,因此寫回磁碟這個操作前提就不成立,所以正常的處理是這個頁會被保留,但是頁的內容會被其他有需要的頁的“夥伴”被複用,做到只是資料的刪除頁不刪除!這是需要注意的點!當然,在緊急記憶體不足的情況下,系統也會呼叫try_to_swap_out方法,回收一般頁,但是共享記憶體的頁會有定製的shmem_write_page,會進行頁的copy操作,防止了屬於共享記憶體的頁被“直接刪除”。

Android中的共享記憶體

Android中也有很多地方用到了共享記憶體,比如ContentProvider中資料的交換,比如CursorWindow的資料交換,裡面其實就是利用了共享記憶體。還有就是傳遞給SurfaceFlinger的渲染資料,也就是通過共享記憶體完成的。之所以使用共享記憶體,還是得益於共享記憶體的設計,效率較高且沒有像管道這種多拷貝的情況,不使用Binder是也是因為Binder依賴的Parcel資料傳輸,在大資料上並沒有很大的優勢!當然,相比於Binder,共享記憶體算是作為最底層api,並沒有提供同步機制!當然,Binder同時也用了mmap(binder_mmap),在這基礎上通過mutex_lock進行了同步機制,算是比共享記憶體有了更加契合Android的設計

image.png

總結

看完這裡,應該都會用共享記憶體進行我們所需的開發了,無論是Binder還是共享記憶體,只有在合適自己的場合使用,才能獲得最大收益!最後!

image.png