一文搞定 | Linux 共享內(nèi)存原理
在下方公眾號(hào)后臺(tái)回復(fù):面試手冊(cè),可獲取杰哥匯總的 3 份面試 PDF 手冊(cè)。
在Linux系統(tǒng)中,每個(gè)進(jìn)程都有獨(dú)立的虛擬內(nèi)存空間,也就是說不同的進(jìn)程訪問同一段虛擬內(nèi)存地址所得到的數(shù)據(jù)是不一樣的,這是因?yàn)椴煌M(jìn)程相同的虛擬內(nèi)存地址會(huì)映射到不同的物理內(nèi)存地址上。
但有時(shí)候?yàn)榱俗尣煌M(jìn)程之間進(jìn)行通信,需要讓不同進(jìn)程共享相同的物理內(nèi)存,Linux通過 共享內(nèi)存 來實(shí)現(xiàn)這個(gè)功能。下面先來介紹一下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ù)生成,用于標(biāo)識(shí)系統(tǒng)的唯一IPC資源。
參數(shù) size 指定創(chuàng)建的共享內(nèi)存大小。
參數(shù) shmflg 指定 shmget() 函數(shù)的動(dòng)作,比如傳入 IPC_CREAT 表示要?jiǎng)?chuàng)建新的共享內(nèi)存。
函數(shù)調(diào)用成功時(shí)返回一個(gè)新建或已經(jīng)存在的的共享內(nèi)存標(biāo)識(shí)符,取決于shmflg的參數(shù)。失敗返回-1,并設(shè)置錯(cuò)誤碼。
2. 關(guān)聯(lián)共享內(nèi)存
shmget() 函數(shù)返回的是一個(gè)標(biāo)識(shí)符,而不是可用的內(nèi)存地址,所以還需要調(diào)用 shmat() 函數(shù)把共享內(nèi)存關(guān)聯(lián)到某個(gè)虛擬內(nèi)存地址上。shmat() 函數(shù)的原型如下:
void?*shmat(int?shmid,?const?void?*shmaddr,?int?shmflg);
參數(shù) shmid 是 shmget() 函數(shù)返回的標(biāo)識(shí)符。
參數(shù) shmaddr 是要關(guān)聯(lián)的虛擬內(nèi)存地址,如果傳入0,表示由系統(tǒng)自動(dòng)選擇合適的虛擬內(nèi)存地址。
參數(shù) shmflg 若指定了 SHM_RDONLY 位,則以只讀方式連接此段,否則以讀寫方式連接此段。
函數(shù)調(diào)用成功返回一個(gè)可用的指針(虛擬內(nèi)存地址),出錯(cuò)返回-1。
3. 取消關(guān)聯(lián)共享內(nèi)存
當(dāng)一個(gè)進(jìn)程不需要共享內(nèi)存的時(shí)候,就需要取消共享內(nèi)存與虛擬內(nèi)存地址的關(guān)聯(lián)。取消關(guān)聯(lián)共享內(nèi)存通過 shmdt() 函數(shù)實(shí)現(xiàn),原型如下:
int?shmdt(const?void?*shmaddr);
參數(shù) shmaddr 是要取消關(guān)聯(lián)的虛擬內(nèi)存地址,也就是 shmat() 函數(shù)返回的值。
函數(shù)調(diào)用成功返回0,出錯(cuò)返回-1。
共享內(nèi)存使用例子
下面通過一個(gè)例子來介紹一下共享內(nèi)存的使用方法。在這個(gè)例子中,有兩個(gè)進(jìn)程,分別為 進(jìn)程A 和 進(jìn)程B,進(jìn)程A 創(chuàng)建一塊共享內(nèi)存,然后寫入數(shù)據(jù),進(jìn)程B 獲取這塊共享內(nèi)存并且讀取其內(nèi)容。
進(jìn)程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;
}
進(jìn)程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;
}
測試時(shí)先運(yùn)行進(jìn)程A,然后再運(yùn)行進(jìn)程B,可以看到進(jìn)程B會(huì)打印出 “Hello World”,說明共享內(nèi)存已經(jīng)創(chuàng)建成功并且讀取。
共享內(nèi)存實(shí)現(xiàn)原理
我們先通過一幅圖來了解一下共享內(nèi)存的大概原理,如下圖:

通過上圖可知,共享內(nèi)存是通過將不同進(jìn)程的虛擬內(nèi)存地址映射到相同的物理內(nèi)存地址來實(shí)現(xiàn)的,下面將會(huì)介紹Linux的實(shí)現(xiàn)方式。
在Linux內(nèi)核中,每個(gè)共享內(nèi)存都由一個(gè)名為 struct shmid_kernel 的結(jié)構(gòu)體來管理,而且Linux限制了系統(tǒng)最大能創(chuàng)建的共享內(nèi)存為128個(gè)。通過類型為 struct shmid_kernel 結(jié)構(gòu)的數(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 結(jié)構(gòu)體各個(gè)字段的作用,比如 shm_npages 字段表示共享內(nèi)存使用了多少個(gè)內(nèi)存頁。而 shm_pages 字段指向了共享內(nèi)存映射的虛擬內(nèi)存頁表項(xiàng)數(shù)組等。
另外 struct shmid_ds 結(jié)構(gòu)體用于管理共享內(nèi)存的信息,而 shm_segs數(shù)組 用于管理系統(tǒng)中所有的共享內(nèi)存。
shmget() 函數(shù)實(shí)現(xiàn)
通過前面的例子可知,要使用共享內(nèi)存,首先需要調(diào)用 shmget() 函數(shù)來創(chuàng)建或者獲取一塊共享內(nèi)存。shmget() 函數(shù)的實(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ù)的實(shí)現(xiàn)比較簡單,首先調(diào)用 findkey() 函數(shù)查找值為key的共享內(nèi)存是否已經(jīng)被創(chuàng)建,findkey() 函數(shù)返回共享內(nèi)存在 shm_segs數(shù)組 的索引。如果找到,那么直接返回共享內(nèi)存的標(biāo)識(shí)符即可。否則就調(diào)用 newseg() 函數(shù)創(chuàng)建新的共享內(nèi)存。newseg() 函數(shù)的實(shí)現(xiàn)也比較簡單,就是創(chuàng)建一個(gè)新的 struct shmid_kernel 結(jié)構(gòu)體,然后設(shè)置其各個(gè)字段的值,并且保存到 shm_segs數(shù)組 中。
shmat() 函數(shù)實(shí)現(xiàn)
shmat() 函數(shù)用于將共享內(nèi)存映射到本地虛擬內(nèi)存地址,由于 shmat() 函數(shù)的實(shí)現(xiàn)比較復(fù)雜,所以我們分段來分析這個(gè)函數(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 標(biāo)識(shí)符來找到共享內(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)))?//?獲取一個(gè)空閑的虛擬內(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;
?}
上面的代碼主要找到一個(gè)可用的虛擬內(nèi)存地址,如果在調(diào)用 shmat() 函數(shù)時(shí)沒有指定了虛擬內(nèi)存地址,那么就通過 get_unmapped_area() 函數(shù)來獲取一個(gè)可用的虛擬內(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)建一個(gè) vm_area_struct 結(jié)構(gòu),在內(nèi)存管理一章知道,vm_area_struct 結(jié)構(gòu)用于管理進(jìn)程的虛擬內(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;
?...
}
上面的代碼主要是設(shè)置剛創(chuàng)建的 vm_area_struct 結(jié)構(gòu)的各個(gè)字段,比較重要的是設(shè)置其 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ù),也就是說,當(dāng)發(fā)生頁缺失異常時(shí)將會(huì)調(diào)用此函數(shù)來恢復(fù)內(nèi)存的映射。
從上面的代碼可看出,shmat() 函數(shù)只是申請(qǐng)了進(jìn)程的虛擬內(nèi)存空間,而共享內(nèi)存的物理空間并沒有申請(qǐng),那么在什么時(shí)候申請(qǐng)物理內(nèi)存呢?答案就是當(dāng)進(jìn)程發(fā)生缺頁異常的時(shí)候會(huì)調(diào)用 shm_nopage() 函數(shù)來恢復(fù)進(jìn)程的虛擬內(nèi)存地址到物理內(nèi)存地址的映射。
shm_nopage() 函數(shù)實(shí)現(xiàn)
shm_nopage() 函數(shù)是當(dāng)發(fā)生內(nèi)存缺頁異常時(shí)被調(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)存的頁表項(xiàng)
?if?(!pte_present(pte))?{???//?如果內(nèi)存頁不存在
??if?(pte_none(pte))?{
???spin_unlock(&shm_lock);
???page?=?get_free_highpage(GFP_HIGHUSER);?//?申請(qǐng)一個(gè)新的物理內(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)建頁表項(xiàng)
??shp->shm_pages[idx]?=?pte;??????????????????????//?保存共享內(nèi)存的頁表項(xiàng)
?}?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ù)的主要功能是當(dāng)發(fā)生內(nèi)存缺頁時(shí),申請(qǐng)新的物理內(nèi)存頁,并映射到共享內(nèi)存中。由于使用共享內(nèi)存時(shí)會(huì)映射到相同的物理內(nèi)存頁上,從而不同進(jìn)程可以共用此塊內(nèi)存。
推薦閱讀
Cache 工作原理,Cache 一致性,你想知道的都在這里
全局負(fù)載均衡、CDN內(nèi)容分發(fā)的原理與實(shí)踐

