一文搞定 | Linux 共享內(nèi)存原理
在Linux系統(tǒng)中,每個進程都有獨立的虛擬內(nèi)存空間,也就是說不同的進程訪問同一段虛擬內(nèi)存地址所得到的數(shù)據(jù)是不一樣的,這是因為不同進程相同的虛擬內(nèi)存地址會映射到不同的物理內(nèi)存地址上。
但有時候為了讓不同進程之間進行通信,需要讓不同進程共享相同的物理內(nèi)存,Linux通過 共享內(nèi)存 來實現(xiàn)這個功能。下面先來介紹一下Linux系統(tǒng)的共享內(nèi)存的使用。
共享內(nèi)存使用
1. 獲取共享內(nèi)存
要使用共享內(nèi)存,首先需要使用 shmget() 函數(shù)獲取共享內(nèi)存,shmget() 函數(shù)的原型如下:
int?shmget(key_t?key,?size_t?size,?int?shmflg);
參數(shù) key 一般由 ftok() 函數(shù)生成,用于標識系統(tǒng)的唯一IPC資源。
參數(shù) size 指定創(chuàng)建的共享內(nèi)存大小。
參數(shù) shmflg 指定 shmget() 函數(shù)的動作,比如傳入 IPC_CREAT 表示要創(chuàng)建新的共享內(nèi)存。
函數(shù)調(diào)用成功時返回一個新建或已經(jīng)存在的的共享內(nèi)存標識符,取決于shmflg的參數(shù)。失敗返回-1,并設置錯誤碼。
2. 關聯(lián)共享內(nèi)存
shmget() 函數(shù)返回的是一個標識符,而不是可用的內(nèi)存地址,所以還需要調(diào)用 shmat() 函數(shù)把共享內(nèi)存關聯(lián)到某個虛擬內(nèi)存地址上。shmat() 函數(shù)的原型如下:
void?*shmat(int?shmid,?const?void?*shmaddr,?int?shmflg);
參數(shù) shmid 是 shmget() 函數(shù)返回的標識符。
參數(shù) shmaddr 是要關聯(lián)的虛擬內(nèi)存地址,如果傳入0,表示由系統(tǒng)自動選擇合適的虛擬內(nèi)存地址。
參數(shù) shmflg 若指定了 SHM_RDONLY 位,則以只讀方式連接此段,否則以讀寫方式連接此段。
函數(shù)調(diào)用成功返回一個可用的指針(虛擬內(nèi)存地址),出錯返回-1。
3. 取消關聯(lián)共享內(nèi)存
當一個進程不需要共享內(nèi)存的時候,就需要取消共享內(nèi)存與虛擬內(nèi)存地址的關聯(lián)。取消關聯(lián)共享內(nèi)存通過 shmdt() 函數(shù)實現(xiàn),原型如下:
int?shmdt(const?void?*shmaddr);
參數(shù) shmaddr 是要取消關聯(lián)的虛擬內(nèi)存地址,也就是 shmat() 函數(shù)返回的值。
函數(shù)調(diào)用成功返回0,出錯返回-1。
共享內(nèi)存使用例子
下面通過一個例子來介紹一下共享內(nèi)存的使用方法。在這個例子中,有兩個進程,分別為 進程A 和 進程B,進程A 創(chuàng)建一塊共享內(nèi)存,然后寫入數(shù)據(jù),進程B 獲取這塊共享內(nèi)存并且讀取其內(nèi)容。
進程A
#include?
#include?
#include?
#include?
#include?
#define?SHM_PATH?"/tmp/shm"
#define?SHM_SIZE?128
int?main(int?argc,?char?*argv[])
{
????int?shmid;
????char?*addr;
????key_t?key?=?ftok(SHM_PATH,?0x6666);
????shmid?=?shmget(key,?SHM_SIZE,?IPC_CREAT|IPC_EXCL|0666);
????if?(shmid?0)?{
????????printf("failed?to?create?share?memory\n");
????????return?-1;
????}
????addr?=?shmat(shmid,?NULL,?0);
????if?(addr?<=?0)?{
????????printf("failed?to?map?share?memory\n");
????????return?-1;
????}
????sprintf(addr,?"%s",?"Hello?World\n");
????return?0;
}
進程B
#include?
#include?
#include?
#include?
#include?
#include?
#define?SHM_PATH?"/tmp/shm"
#define?SHM_SIZE?128
int?main(int?argc,?char?*argv[])
{
????int?shmid;
????char?*addr;
????key_t?key?=?ftok(SHM_PATH,?0x6666);
????char?buf[128];
????shmid?=?shmget(key,?SHM_SIZE,?IPC_CREAT);
????if?(shmid?0)?{
????????printf("failed?to?get?share?memory\n");
????????return?-1;
????}
????addr?=?shmat(shmid,?NULL,?0);
????if?(addr?<=?0)?{
????????printf("failed?to?map?share?memory\n");
????????return?-1;
????}
????strcpy(buf,?addr,?128);
????printf("%s",?buf);
????return?0;
}
測試時先運行進程A,然后再運行進程B,可以看到進程B會打印出 “Hello World”,說明共享內(nèi)存已經(jīng)創(chuàng)建成功并且讀取。
共享內(nèi)存實現(xiàn)原理
我們先通過一幅圖來了解一下共享內(nèi)存的大概原理,如下圖:

通過上圖可知,共享內(nèi)存是通過將不同進程的虛擬內(nèi)存地址映射到相同的物理內(nèi)存地址來實現(xiàn)的,下面將會介紹Linux的實現(xiàn)方式。
在Linux內(nèi)核中,每個共享內(nèi)存都由一個名為 struct shmid_kernel 的結構體來管理,而且Linux限制了系統(tǒng)最大能創(chuàng)建的共享內(nèi)存為128個。通過類型為 struct shmid_kernel 結構的數(shù)組來管理,如下:
struct?shmid_ds?{
?struct?ipc_perm??shm_perm;?/*?operation?perms?*/
?int???shm_segsz;?/*?size?of?segment?(bytes)?*/
?__kernel_time_t??shm_atime;?/*?last?attach?time?*/
?__kernel_time_t??shm_dtime;?/*?last?detach?time?*/
?__kernel_time_t??shm_ctime;?/*?last?change?time?*/
?__kernel_ipc_pid_t?shm_cpid;?/*?pid?of?creator?*/
?__kernel_ipc_pid_t?shm_lpid;?/*?pid?of?last?operator?*/
?unsigned?short??shm_nattch;?/*?no.?of?current?attaches?*/
?unsigned?short???shm_unused;?/*?compatibility?*/
?void????*shm_unused2;?/*?ditto?-?used?by?DIPC?*/
?void???*shm_unused3;?/*?unused?*/
};
struct?shmid_kernel
{?
?struct?shmid_ds??u;
?/*?the?following?are?private?*/
?unsigned?long??shm_npages;?/*?size?of?segment?(pages)?*/
?pte_t???*shm_pages;?/*?array?of?ptrs?to?frames?->?SHMMAX?*/?
?struct?vm_area_struct?*attaches;?/*?descriptors?for?attaches?*/
};
static?struct?shmid_kernel?*shm_segs[SHMMNI];?//?SHMMNI等于128
從注釋可以知道 struct shmid_kernel 結構體各個字段的作用,比如 shm_npages 字段表示共享內(nèi)存使用了多少個內(nèi)存頁。而 shm_pages 字段指向了共享內(nèi)存映射的虛擬內(nèi)存頁表項數(shù)組等。
另外 struct shmid_ds 結構體用于管理共享內(nèi)存的信息,而 shm_segs數(shù)組 用于管理系統(tǒng)中所有的共享內(nèi)存。
shmget() 函數(shù)實現(xiàn)
通過前面的例子可知,要使用共享內(nèi)存,首先需要調(diào)用 shmget() 函數(shù)來創(chuàng)建或者獲取一塊共享內(nèi)存。shmget() 函數(shù)的實現(xiàn)如下:
asmlinkage?long?sys_shmget?(key_t?key,?int?size,?int?shmflg)
{
?struct?shmid_kernel?*shp;
?int?err,?id?=?0;
?down(¤t->mm->mmap_sem);
?spin_lock(&shm_lock);
?if?(size?0?||?size?>?shmmax)?{
??err?=?-EINVAL;
?}?else?if?(key?==?IPC_PRIVATE)?{
??err?=?newseg(key,?shmflg,?size);
?}?else?if?((id?=?findkey?(key))?==?-1)?{
??if?(!(shmflg?&?IPC_CREAT))
???err?=?-ENOENT;
??else
???err?=?newseg(key,?shmflg,?size);
?}?else?if?((shmflg?&?IPC_CREAT)?&&?(shmflg?&?IPC_EXCL))?{
??err?=?-EEXIST;
?}?else?{
??shp?=?shm_segs[id];
??if?(shp->u.shm_perm.mode?&?SHM_DEST)
???err?=?-EIDRM;
??else?if?(size?>?shp->u.shm_segsz)
???err?=?-EINVAL;
??else?if?(ipcperms?(&shp->u.shm_perm,?shmflg))
???err?=?-EACCES;
??else
???err?=?(int)?shp->u.shm_perm.seq?*?SHMMNI?+?id;
?}
?spin_unlock(&shm_lock);
?up(¤t->mm->mmap_sem);
?return?err;
}
shmget() 函數(shù)的實現(xiàn)比較簡單,首先調(diào)用 findkey() 函數(shù)查找值為key的共享內(nèi)存是否已經(jīng)被創(chuàng)建,findkey() 函數(shù)返回共享內(nèi)存在 shm_segs數(shù)組 的索引。如果找到,那么直接返回共享內(nèi)存的標識符即可。否則就調(diào)用 newseg() 函數(shù)創(chuàng)建新的共享內(nèi)存。newseg() 函數(shù)的實現(xiàn)也比較簡單,就是創(chuàng)建一個新的 struct shmid_kernel 結構體,然后設置其各個字段的值,并且保存到 shm_segs數(shù)組 中。
shmat() 函數(shù)實現(xiàn)
shmat() 函數(shù)用于將共享內(nèi)存映射到本地虛擬內(nèi)存地址,由于 shmat() 函數(shù)的實現(xiàn)比較復雜,所以我們分段來分析這個函數(shù):
asmlinkage?long?sys_shmat?(int?shmid,?char?*shmaddr,?int?shmflg,?ulong?*raddr)
{
?struct?shmid_kernel?*shp;
?struct?vm_area_struct?*shmd;
?int?err?=?-EINVAL;
?unsigned?int?id;
?unsigned?long?addr;
?unsigned?long?len;
?down(¤t->mm->mmap_sem);
?spin_lock(&shm_lock);
?if?(shmid?0)
??goto?out;
?shp?=?shm_segs[id?=?(unsigned?int)?shmid?%?SHMMNI];
?if?(shp?==?IPC_UNUSED?||?shp?==?IPC_NOID)
??goto?out;
上面這段代碼主要通過 shmid 標識符來找到共享內(nèi)存描述符,上面說過系統(tǒng)中所有的共享內(nèi)存到保存在 shm_segs 數(shù)組中。
?if?(!(addr?=?(ulong)?shmaddr))?{
??if?(shmflg?&?SHM_REMAP)
???goto?out;
??err?=?-ENOMEM;
??addr?=?0;
?again:
??if?(!(addr?=?get_unmapped_area(addr,?shp->u.shm_segsz)))?//?獲取一個空閑的虛擬內(nèi)存空間
???goto?out;
??if(addr?&?(SHMLBA?-?1))?{
???addr?=?(addr?+?(SHMLBA?-?1))?&?~(SHMLBA?-?1);
???goto?again;
??}
?}?else?if?(addr?&?(SHMLBA-1))?{
??if?(shmflg?&?SHM_RND)
???addr?&=?~(SHMLBA-1);???????/*?round?down?*/
??else
???goto?out;
?}
上面的代碼主要找到一個可用的虛擬內(nèi)存地址,如果在調(diào)用 shmat() 函數(shù)時沒有指定了虛擬內(nèi)存地址,那么就通過 get_unmapped_area() 函數(shù)來獲取一個可用的虛擬內(nèi)存地址。
?spin_unlock(&shm_lock);
?err?=?-ENOMEM;
?shmd?=?kmem_cache_alloc(vm_area_cachep,?SLAB_KERNEL);
?spin_lock(&shm_lock);
?if?(!shmd)
??goto?out;
?if?((shp?!=?shm_segs[id])?||?(shp->u.shm_perm.seq?!=?(unsigned?int)?shmid?/?SHMMNI))?{
??kmem_cache_free(vm_area_cachep,?shmd);
??err?=?-EIDRM;
??goto?out;
?}
上面的代碼主要通過調(diào)用 kmem_cache_alloc() 函數(shù)創(chuàng)建一個 vm_area_struct 結構,在內(nèi)存管理一章知道,vm_area_struct 結構用于管理進程的虛擬內(nèi)存空間。
?shmd->vm_private_data?=?shm_segs?+?id;
?shmd->vm_start?=?addr;
?shmd->vm_end?=?addr?+?shp->shm_npages?*?PAGE_SIZE;
?shmd->vm_mm?=?current->mm;
?shmd->vm_page_prot?=?(shmflg?&?SHM_RDONLY)???PAGE_READONLY?:?PAGE_SHARED;
?shmd->vm_flags?=?VM_SHM?|?VM_MAYSHARE?|?VM_SHARED
????|?VM_MAYREAD?|?VM_MAYEXEC?|?VM_READ?|?VM_EXEC
????|?((shmflg?&?SHM_RDONLY)???0?:?VM_MAYWRITE?|?VM_WRITE);
?shmd->vm_file?=?NULL;
?shmd->vm_offset?=?0;
?shmd->vm_ops?=?&shm_vm_ops;
?shp->u.shm_nattch++;?????/*?prevent?destruction?*/
?spin_unlock(&shm_lock);
?err?=?shm_map(shmd);
?spin_lock(&shm_lock);
?if?(err)
??goto?failed_shm_map;
?insert_attach(shp,shmd);??/*?insert?shmd?into?shp->attaches?*/
?shp->u.shm_lpid?=?current->pid;
?shp->u.shm_atime?=?CURRENT_TIME;
?*raddr?=?addr;
?err?=?0;
out:
?spin_unlock(&shm_lock);
?up(¤t->mm->mmap_sem);
?return?err;
?...
}
上面的代碼主要是設置剛創(chuàng)建的 vm_area_struct 結構的各個字段,比較重要的是設置其 vm_ops 字段為 shm_vm_ops,shm_vm_ops 定義如下:
static?struct?vm_operations_struct?shm_vm_ops?=?{
?shm_open,??/*?open?-?callback?for?a?new?vm-area?open?*/
?shm_close,??/*?close?-?callback?for?when?the?vm-area?is?released?*/
?NULL,???/*?no?need?to?sync?pages?at?unmap?*/
?NULL,???/*?protect?*/
?NULL,???/*?sync?*/
?NULL,???/*?advise?*/
?shm_nopage,??/*?nopage?*/
?NULL,???/*?wppage?*/
?shm_swapout??/*?swapout?*/
};
shm_vm_ops 的 nopage 回調(diào)為 shm_nopage() 函數(shù),也就是說,當發(fā)生頁缺失異常時將會調(diào)用此函數(shù)來恢復內(nèi)存的映射。
從上面的代碼可看出,shmat() 函數(shù)只是申請了進程的虛擬內(nèi)存空間,而共享內(nèi)存的物理空間并沒有申請,那么在什么時候申請物理內(nèi)存呢?答案就是當進程發(fā)生缺頁異常的時候會調(diào)用 shm_nopage() 函數(shù)來恢復進程的虛擬內(nèi)存地址到物理內(nèi)存地址的映射。
shm_nopage() 函數(shù)實現(xiàn)
shm_nopage() 函數(shù)是當發(fā)生內(nèi)存缺頁異常時被調(diào)用的,代碼如下:
static?struct?page?*?shm_nopage(struct?vm_area_struct?*?shmd,?unsigned?long?address,?int?no_share)
{
?pte_t?pte;
?struct?shmid_kernel?*shp;
?unsigned?int?idx;
?struct?page?*?page;
?shp?=?*(struct?shmid_kernel?**)?shmd->vm_private_data;
?idx?=?(address?-?shmd->vm_start?+?shmd->vm_offset)?>>?PAGE_SHIFT;
?spin_lock(&shm_lock);
again:
?pte?=?shp->shm_pages[idx];?//?共享內(nèi)存的頁表項
?if?(!pte_present(pte))?{???//?如果內(nèi)存頁不存在
??if?(pte_none(pte))?{
???spin_unlock(&shm_lock);
???page?=?get_free_highpage(GFP_HIGHUSER);?//?申請一個新的物理內(nèi)存頁
???if?(!page)
????goto?oom;
???clear_highpage(page);
???spin_lock(&shm_lock);
???if?(pte_val(pte)?!=?pte_val(shp->shm_pages[idx]))
????goto?changed;
??}?else?{
???...
??}
??shm_rss++;
??pte?=?pte_mkdirty(mk_pte(page,?PAGE_SHARED));???//?創(chuàng)建頁表項
??shp->shm_pages[idx]?=?pte;??????????????????????//?保存共享內(nèi)存的頁表項
?}?else
??--current->maj_flt;??/*?was?incremented?in?do_no_page?*/
done:
?get_page(pte_page(pte));
?spin_unlock(&shm_lock);
?current->min_flt++;
?return?pte_page(pte);
?...
}
shm_nopage() 函數(shù)的主要功能是當發(fā)生內(nèi)存缺頁時,申請新的物理內(nèi)存頁,并映射到共享內(nèi)存中。由于使用共享內(nèi)存時會映射到相同的物理內(nèi)存頁上,從而不同進程可以共用此塊內(nèi)存。

