共享內(nèi)存的使用和原理
還是先看共享內(nèi)存的使用方法鲫趁,我主要介紹兩個函數(shù):
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg); //申請共享內(nèi)存
void *shmat(int shmid, const void *shmaddr, int shmflg); //把共享內(nèi)存映射到進(jìn)程的地址空間
通過shmget()函數(shù)申請共享內(nèi)存,它的入?yún)⑷缦?/p>
- key:用來唯一確定這片內(nèi)存的標(biāo)識,。
- size:就是我們申請內(nèi)存的大小
- shmflg:讀寫權(quán)限
- 返回值:這個操作會返回一個id, 我們一般稱為 shmid
通過shmat()函數(shù)將我們申請到的共享內(nèi)存映射到自己的用戶空間堡僻,映射成功會返回地址钉疫,有了這個地址,我們就可以隨意的讀寫數(shù)據(jù)了牲阁,我們繼續(xù)看一下這個函數(shù)的入?yún)?/p>
- shmid :我們申請內(nèi)存時, 返回的shmid
- shmaddr:共享內(nèi)存在進(jìn)程的內(nèi)存地址,傳NULL讓內(nèi)核自己決定一個合適的地址位置.
- shmflg:讀寫權(quán)限
- 返回值:映射后進(jìn)程內(nèi)的地址指針, 代表內(nèi)存的頭部地址
共享內(nèi)存的原理是在內(nèi)存中單獨開辟的一段內(nèi)存空間备燃,這段內(nèi)存空間其實就是一個tempfs(臨時虛擬文件)凌唬,tempfs是VFS的一種文件系統(tǒng)客税,掛載在/dev/shm上,前面提到的管道pipefs也是VFS的一種文件系統(tǒng)更耻。
由于共享的內(nèi)存空間對使用和接收進(jìn)程來講秧均,完全無感知,就像是在自己的內(nèi)存上讀寫數(shù)據(jù)一樣疙描,所以也是效率最高的一種IPC方式讶隐。
上面提到的IPC的方式都是在內(nèi)核空間中開辟內(nèi)存來存儲數(shù)據(jù),寫數(shù)據(jù)時效五,需要將數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間炉峰,讀數(shù)據(jù)時,需要從內(nèi)核空間拷貝到自己的用戶空間戒劫,
共享內(nèi)存就只需要一次拷貝,而且共享內(nèi)存不是在內(nèi)核開辟空間巫橄,所以可以傳輸?shù)臄?shù)據(jù)量大茵典。
但是共享內(nèi)存最大的缺點就是沒有并發(fā)的控制,我們一般通過信號量配合共享內(nèi)存使用彩倚,進(jìn)行同步和并發(fā)的控制扶平。
Android中共享內(nèi)存的使用場景
共享內(nèi)存在Android系統(tǒng)中主要的使用場景是用來傳輸大數(shù)據(jù),并且Android并沒有直接使用Linux原生的共享內(nèi)存方式盯质,而是設(shè)計了Ashmem匿名共享內(nèi)存概而。
之前說到有名管道和匿名管道的區(qū)別在于有名管道可以在vfs目錄樹中查看到這個管道的文件囱修,但是匿名管道不行破镰,所以匿名共享內(nèi)存同樣也是無法在vfs目錄中查看到的,Android之所以要設(shè)計匿名共享內(nèi)存鲜漩,我覺得主要是為了安全性的考慮吧孕似。
我們來看看共享內(nèi)存的一個使用場景,在Android中喉祭,如果我們想要將當(dāng)前的界面顯示出來泛烙,需要將當(dāng)前界面的圖元數(shù)據(jù)傳遞Surfaceflinger去做圖層混合,圖層混合之后的數(shù)據(jù)會直接送入幀緩存藐唠,送入幀緩存后,顯卡就會直接取出幀緩存里的圖元數(shù)據(jù)顯示了宇立。
那么我們?nèi)绾螌?yīng)用的Activity的圖元數(shù)據(jù)傳遞給SurfaceFlinger呢泄伪?想要將圖像數(shù)據(jù)這樣比較大的數(shù)據(jù)跨進(jìn)程傳輸,靠binder是不行的蟋滴,所以這兒便用到匿名共享內(nèi)存津函。
從谷歌官方提供的架構(gòu)圖可以看到,圖元數(shù)據(jù)是通過BufferQueue傳遞到SurfaceFlinger去的涩馆,當(dāng)我們想要繪制圖像的時候允坚,需要從BufferQueue中申請一個Buffer,Buffer會調(diào)用Gralloc模塊來分配共享內(nèi)存當(dāng)作圖元緩沖區(qū)存放我們的圖元數(shù)據(jù)涯雅。
//文件-->hardware/libhardware/modules/gralloc/gralloc.cpp
static int gralloc_alloc_buffer(alloc_device_t* dev,
size_t size, int usage, buffer_handle_t* pHandle)
{
int err = 0;
int fd = -1;
size = roundUpToPageSize(size);
// 創(chuàng)建共享內(nèi)存展运,并且設(shè)定名字跟size
fd = ashmem_create_region("gralloc-buffer", size);
if (err == 0) {
private_handle_t* hnd = new private_handle_t(fd, size, 0);
gralloc_module_t* module = reinterpret_cast<gralloc_module_t*>(
dev->common.module);
// 執(zhí)行mmap拗胜,將內(nèi)存映射到自己的進(jìn)程
err = mapBuffer(module, hnd);
if (err == 0) {
*pHandle = hnd;
}
}
?
return err;
}
?
int mapBuffer(gralloc_module_t const* module,
private_handle_t* hnd)
{
void* vaddr;
return gralloc_map(module, hnd, &vaddr);
}
?
static int gralloc_map(gralloc_module_t const* module,
buffer_handle_t handle,
void** vaddr)
{
private_handle_t* hnd = (private_handle_t*)handle;
if (!(hnd->flags & private_handle_t::PRIV_FLAGS_FRAMEBUFFER)) {
size_t size = hnd->size;
//映射創(chuàng)建的匿名共享內(nèi)存
void* mappedAddress = mmap(0, size,
PROT_READ|PROT_WRITE, MAP_SHARED, hnd->fd, 0);
if (mappedAddress == MAP_FAILED) {
return -errno;
}
hnd->base = intptr_t(mappedAddress) + hnd->offset;
}
*vaddr = (void*)hnd->base;
return 0;
}
可以看到Android的匿名共享內(nèi)存是通過ashmem_create_region() 函數(shù)來申請共享內(nèi)存的埂软,它會在/dev/ashmem下創(chuàng)建一個虛擬文件,Linux原生共享內(nèi)存是通過shmget()函數(shù)勘畔,并會在/dev/shm下創(chuàng)建虛擬文件咖杂。
匿名共享內(nèi)存是通過mmap()函數(shù)將申請到的內(nèi)存映射到自己的進(jìn)程空間,而Linux是通過*shmat()函數(shù)懦尝。
雖然函數(shù)不一樣,但是Android的匿名共享內(nèi)存和Linux的共享內(nèi)存在本質(zhì)上是大同小異的陵霉。
6.1 什么是共享內(nèi)存?
- 共享內(nèi)存是系統(tǒng)處于多個進(jìn)程之間通訊的考慮,而預(yù)留的一塊內(nèi)存區(qū)乍桂。
- 共享內(nèi)存允許兩個或更多的進(jìn)程訪問同一塊內(nèi)存效床,就如同malloc()函數(shù)向不同進(jìn)程返回了指向同一個物理內(nèi)存區(qū)域的指針剩檀。
- 當(dāng)一個進(jìn)程改變了這塊地址中的內(nèi)容的時候,其他進(jìn)程都會覺察到這個更改沪猴。
6.2 關(guān)于共享內(nèi)存
- 當(dāng)一個程序加載進(jìn)內(nèi)存后运嗜,它就被分成叫做頁的塊。
- 通信將存在內(nèi)存的兩個頁之間或者兩個獨立的進(jìn)程之間砸民。
- 當(dāng)一個程序想和另外一個程序通信的時候翩活,那內(nèi)存將會為這兩個程序生成一塊公共的內(nèi)存區(qū)域便贵。這塊被兩個進(jìn)程分享的內(nèi)存區(qū)域叫做共享內(nèi)存。
- 由于所有進(jìn)程共享同一塊內(nèi)存利耍,共享內(nèi)存在各種進(jìn)程間通信方式中具有最高的效率盔粹。
- 訪問共享內(nèi)存區(qū)域和訪問進(jìn)程獨有的內(nèi)存區(qū)域一樣快舷嗡,并不需要通過系統(tǒng)調(diào)用或者其他需要切入內(nèi)核的過程來完成。同時它也也避免了對數(shù)據(jù)的跟中不必要的復(fù)制进萄。
- 如果沒有共享內(nèi)存的概念锐峭,那一個進(jìn)程不能存取另外一個進(jìn)程的內(nèi)存部分可婶,因而導(dǎo)致共享數(shù)據(jù)或者通信失效。因為系統(tǒng)內(nèi)核沒有對訪問共享內(nèi)存進(jìn)行同步椎扬,開發(fā)者必須提供自己的同步措施具温。
- 解決了這些問題的常用方法是是通過信號量進(jìn)行同步。不過通常我們程序只有一個進(jìn)程訪問了共享內(nèi)存钻趋,因此在集中展示了共享內(nèi)存機(jī)制的同時剂习,我們避免了讓代碼被同步邏輯搞的混亂不堪。
- 為了簡化共享數(shù)據(jù)的完整性和避免同時存取數(shù)據(jù)失仁,內(nèi)核提供了一種專門存取共享內(nèi)存資源的機(jī)制们何。這稱為互斥體或者M(jìn)utex對象。
6.3 Mutex對象
- 例如拂封,在數(shù)據(jù)被寫入前不允許進(jìn)程從共享內(nèi)存中讀取信息鹦蠕、不允許兩個進(jìn)程同時向一個共享內(nèi)存地址寫入數(shù)據(jù)等。
- 當(dāng)一個基礎(chǔ)想和兩一個進(jìn)程通信的時候萧恕,它將按以下順序運行:
- 1肠阱、獲取Mutex對象屹徘,鎖定共享區(qū)域
- 2、將要通信的數(shù)據(jù)寫入共享區(qū)域
- 3吆视、釋放Mutex對象
- 當(dāng)一個進(jìn)程從這個區(qū)域讀取數(shù)據(jù)的時候,它將重復(fù)同樣的步驟啦吧,只是將第二步變成讀取授滓。
6.4 內(nèi)存模型
要使用一塊共享內(nèi)存
- 進(jìn)程必須首先分配它
- 隨后需要訪問這個共享內(nèi)存塊的每一個進(jìn)程都必須將這個共享內(nèi)存綁定到自己的地址空間中琳水。
- 當(dāng)完成通信之后,所有進(jìn)程都脫離共享內(nèi)存般堆,并且由一個進(jìn)程釋放該共享內(nèi)存塊在孝。
- 在/proc/sys/kernel/目錄下,記錄著共享內(nèi)存的一些限制淮摔,如一個共享內(nèi)存區(qū)的最大字節(jié)數(shù)shmmax私沮,系統(tǒng)范圍內(nèi)最大的共享內(nèi)存區(qū)標(biāo)志符數(shù)shmmni等。
6.5 Linux系統(tǒng)內(nèi)存模型
- 在Linux系統(tǒng)中和橙,每個進(jìn)程的虛擬內(nèi)存是被分為許多頁面的仔燕。這些內(nèi)存頁面中包含了實際的數(shù)據(jù)。每個進(jìn)程都會維護(hù)一個從內(nèi)存地址到虛擬內(nèi)存頁面之間的映射關(guān)系魔招。盡管每個進(jìn)程都有自己的內(nèi)存地址,不同的進(jìn)程可以同時將同一個頁面頁面映射到自己的地址空間办斑,從而達(dá)到共享內(nèi)存的目的外恕。
- 分配一個新的共享內(nèi)存塊會創(chuàng)建新的內(nèi)存頁面。因為所有進(jìn)程都希望共享對同一塊內(nèi)存的訪問乡翅,只應(yīng)由一個進(jìn)程創(chuàng)建一塊新的共享內(nèi)存鳞疲。再次分配一塊已經(jīng)存在的內(nèi)存塊不會創(chuàng)建新的頁面,而只是會返回一個標(biāo)示該內(nèi)存塊的標(biāo)識符蠕蚜。
- 一個進(jìn)程如需使用這個共享內(nèi)存塊尚洽,則首先需要將它綁定到自己的地址空間中。
- 這樣會創(chuàng)建一個從進(jìn)程本身虛擬地址到共享頁面的映射關(guān)系波势。當(dāng)對共享內(nèi)存的使用結(jié)束之后翎朱,這個映射關(guān)系將被刪除橄维。
- 當(dāng)再也沒有進(jìn)程需要使用這個共享內(nèi)存塊的時候尺铣,必須有一個(有且只有一個)進(jìn)程負(fù)責(zé)釋放這個被共享的內(nèi)存頁面。
- 所有共享內(nèi)存塊的大小必須是系統(tǒng)頁面大小的整數(shù)倍争舞。系統(tǒng)頁面大小指的是系統(tǒng)中單個內(nèi)存頁面包含的字節(jié)數(shù)凛忿。在Linux系統(tǒng)中,內(nèi)存頁面大小是4KB竞川,不過您仍然應(yīng)高通過調(diào)用getPageSize獲取這個值店溢。