您的位置首页>企业动态>

深入剖析Linux共享内存原理

导读大家好,我是极客范的本期栏目编辑小友,现在为大家讲解深入剖析Linux共享内存原理问题。在Linux系统中,每个进程都有自己的虚拟内存空间,

大家好,我是极客范的本期栏目编辑小友,现在为大家讲解深入剖析Linux共享内存原理问题。

在Linux系统中,每个进程都有自己的虚拟内存空间,也就是说不同的进程在访问同一个虚拟内存地址时得到的数据是不同的,因为不同进程的同一个虚拟内存地址会映射到不同的物理内存地址。

但是有的时候,不同的进程之间为了进行通信,需要共享同一个物理内存,Linux就是通过共享内存来实现这个功能的。我们先来介绍一下共享内存在Linux系统中的使用。

共享内存使用

1.获取共享内存

要使用共享内存,首先需要使用shmget()函数来获取共享内存。shmget()函数的原型如下:

intshmget(key_tkey,size_tsize,intshmflg);

参数键一般由ftok()函数生成,用于标识系统唯一的IPC资源。

参数大小指定创建的共享内存的大小。

参数shmflg指定shmget()函数的动作,例如,传递IPC_CREAT表示要创建一个新的共享内存。

当函数调用成功时,它会返回一个新的或现有的共享内存标识符,该标识符取决于shmflg的参数。未能返回-1并设置错误代码。

2.关联共享内存

-align:left;background-color:rgb(255,255,255);">shmget()函数返回的是一个标识符,而不是可用的内存地址,所以还需要调用shmat()函数把共享内存关联到某个虚拟内存地址上。shmat()函数的原型如下:

void*shmat(intshmid,constvoid*shmaddr,intshmflg);参数shmid是shmget()函数返回的标识符。参数shmaddr是要关联的虚拟内存地址,如果传入0,表示由系统自动选择合适的虚拟内存地址。参数shmflg若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。

函数调用成功返回一个可用的指针(虚拟内存地址),出错返回-1。

3. 取消关联共享内存

当一个进程不需要共享内存的时候,就需要取消共享内存与虚拟内存地址的关联。取消关联共享内存通过shmdt()函数实现,原型如下:

intshmdt(constvoid*shmaddr);参数shmaddr是要取消关联的虚拟内存地址,也就是shmat()函数返回的值。

函数调用成功返回0,出错返回-1。

共享内存使用例子

下面通过一个例子来介绍一下共享内存的使用方法。在这个例子中,有两个进程,分别为进程A和进程B,进程A创建一块共享内存,然后写入数据,进程B获取这块共享内存并且读取其内容。

进程A#include#include#include#include#include#defineSHM_PATH"/tmp/shm"#defineSHM_SIZE128intmain(intargc,char*argv[]){intshmid;char*addr;key_tkey=ftok(SHM_PATH,0x6666);shmid=shmget(key,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);if(shmid< 0){printf("failedtocreatesharememory");return-1;}addr=shmat(shmid,NULL,0);if(addr<= 0){printf("failedtomapsharememory");return-1;}sprintf(addr,"%s","HelloWorld");return0;}进程B#include#include#include#include#include#include#defineSHM_PATH"/tmp/shm"#defineSHM_SIZE128intmain(intargc,char*argv[]){intshmid;char*addr;key_tkey=ftok(SHM_PATH,0x6666);charbuf[128];shmid=shmget(key,SHM_SIZE,IPC_CREAT);if(shmid< 0){printf("failedtogetsharememory");return-1;}addr=shmat(shmid,NULL,0);if(addr<= 0){printf("failedtomapsharememory");return-1;}strcpy(buf,addr,128);printf("%s",buf);return0;}

测试时先运行进程A,然后再运行进程B,可以看到进程B会打印出 “Hello World”,说明共享内存已经创建成功并且读取。

共享内存实现原理

我们先通过一幅图来了解一下共享内存的大概原理,如下图:

通过上图可知,共享内存是通过将不同进程的虚拟内存地址映射到相同的物理内存地址来实现的,下面将会介绍Linux的实现方式。

在Linux内核中,每个共享内存都由一个名为struct shmid_kernel的结构体来管理,而且Linux限制了系统最大能创建的共享内存为128个。通过类型为struct shmid_kernel结构的数组来管理,如下:

structshmid_ds{structipc_permshm_perm;intshm_segsz;__kernel_time_tshm_atime;__kernel_time_tshm_dtime;__kernel_time_tshm_ctime;__kernel_ipc_pid_tshm_cpid;__kernel_ipc_pid_tshm_lpid;unsignedshortshm_nattch;unsignedshortshm_unused;void*shm_unused2;void*shm_unused3;};structshmid_kernel{structshmid_dsu;unsignedlongshm_npages;pte_t*shm_pages;structvm_area_struct*attaches;};staticstructshmid_kernel*shm_segs[SHMMNI];//SHMMNI等于128

从注释可以知道struct shmid_kernel结构体各个字段的作用,比如shm_npages字段表示共享内存使用了多少个内存页。而shm_pages字段指向了共享内存映射的虚拟内存页表项数组等。

另外struct shmid_ds结构体用于管理共享内存的信息,而shm_segs数组用于管理系统中所有的共享内存。

shmget() 函数实现

通过前面的例子可知,要使用共享内存,首先需要调用shmget()函数来创建或者获取一块共享内存。shmget()函数的实现如下:

asmlinkagelongsys_shmget(key_tkey,intsize,intshmflg){structshmid_kernel*shp;interr,id=0;down(¤t->mm->mmap_sem);spin_lock(&shm_lock);if(sizeshmmax){err=-EINVAL;}elseif(key==IPC_PRIVATE){err=newseg(key,shmflg,size);}elseif((id=findkey(key))==-1){if(!(shmflg&IPC_CREAT))err=-ENOENT;elseerr=newseg(key,shmflg,size);}elseif((shmflg&IPC_CREAT)&&(shmflg&IPC_EXCL)){err=-EEXIST;}else{shp=shm_segs[id];if(shp->u.shm_perm.mode&SHM_DEST)err=-EIDRM;elseif(size>shp->u.shm_segsz)err=-EINVAL;elseif(ipcperms(&shp->u.shm_perm,shmflg))err=-EACCES;elseerr=(int)shp->u.shm_perm.seq*SHMMNI+id;}spin_unlock(&shm_lock);up(¤t->mm->mmap_sem);returnerr;}

shmget()函数的实现比较简单,首先调用findkey()函数查找值为key的共享内存是否已经被创建,findkey()函数返回共享内存在shm_segs数组的索引。如果找到,那么直接返回共享内存的标识符即可。否则就调用newseg()函数创建新的共享内存。newseg()函数的实现也比较简单,就是创建一个新的struct shmid_kernel结构体,然后设置其各个字段的值,并且保存到shm_segs数组中。

shmat() 函数实现

shmat()函数用于将共享内存映射到本地虚拟内存地址,由于shmat()函数的实现比较复杂,所以我们分段来分析这个函数:

asmlinkagelongsys_shmat(intshmid,char*shmaddr,intshmflg,ulong*raddr){structshmid_kernel*shp;structvm_area_struct*shmd;interr=-EINVAL;unsignedintid;unsignedlongaddr;unsignedlonglen;down(¤t->mm->mmap_sem);spin_lock(&shm_lock);if(shmid< 0)gotoout;shp=shm_segs[id=(unsignedint)shmid%SHMMNI];if(shp==IPC_UNUSED||shp==IPC_NOID)gotoout;

上面这段代码主要通过shmid标识符来找到共享内存描述符,上面说过系统中所有的共享内存到保存在shm_segs数组中。

if(!(addr=(ulong)shmaddr)){if(shmflg&SHM_REMAP)gotoout;err=-ENOMEM;addr=0;again:if(!(addr=get_unmapped_area(addr,shp->u.shm_segsz)))//获取一个空闲的虚拟内存空间gotoout;if(addr&(SHMLBA-1)){addr=(addr+(SHMLBA-1))&~(SHMLBA-1);gotoagain;}}elseif(addr&(SHMLBA-1)){if(shmflg&SHM_RND)addr&=~(SHMLBA-1);elsegotoout;}

上面的代码主要找到一个可用的虚拟内存地址,如果在调用shmat()函数时没有指定了虚拟内存地址,那么就通过get_unmapped_area()函数来获取一个可用的虚拟内存地址。

spin_unlock(&shm_lock);err=-ENOMEM;shmd=kmem_cache_alloc(vm_area_cachep,SLAB_KERNEL);spin_lock(&shm_lock);if(!shmd)gotoout;if((shp!=shm_segs[id])||(shp->u.shm_perm.seq!=(unsignedint)shmid/SHMMNI)){kmem_cache_free(vm_area_cachep,shmd);err=-EIDRM;gotoout;}

上面的代码主要通过调用kmem_cache_alloc()函数创建一个vm_area_struct结构,在内存管理一章知道,vm_area_struct结构用于管理进程的虚拟内存空间。

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++;spin_unlock(&shm_lock);err=shm_map(shmd);spin_lock(&shm_lock);if(err)gotofailed_shm_map;insert_attach(shp,shmd);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);returnerr;...}

上面的代码主要是设置刚创建的vm_area_struct结构的各个字段,比较重要的是设置其vm_ops字段为shm_vm_ops,shm_vm_ops定义如下:

staticstructvm_operations_structshm_vm_ops={shm_open,shm_close,NULL,NULL,NULL,NULL,shm_nopage,NULL,shm_swapout};

shm_vm_ops的nopage回调为shm_nopage()函数,也就是说,当发生页缺失异常时将会调用此函数来恢复内存的映射。

从上面的代码可看出,shmat()函数只是申请了进程的虚拟内存空间,而共享内存的物理空间并没有申请,那么在什么时候申请物理内存呢?答案就是当进程发生缺页异常的时候会调用shm_nopage()函数来恢复进程的虚拟内存地址到物理内存地址的映射。

shm_nopage() 函数实现

shm_nopage() 函数是当发生内存缺页异常时被调用的,代码如下:

staticstructpage*shm_nopage(structvm_area_struct*shmd,unsignedlongaddress,intno_share){pte_tpte;structshmid_kernel*shp;unsignedintidx;structpage*page;shp=*(structshmid_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];//共享内存的页表项if(!pte_present(pte)){//如果内存页不存在if(pte_none(pte)){spin_unlock(&shm_lock);page=get_free_highpage(GFP_HIGHUSER);//申请一个新的物理内存页if(!page)gotooom;clear_highpage(page);spin_lock(&shm_lock);if(pte_val(pte)!=pte_val(shp->shm_pages[idx]))gotochanged;}else{...}shm_rss++;pte=pte_mkdirty(mk_pte(page,PAGE_SHARED));//创建页表项shp->shm_pages[idx]=pte;//保存共享内存的页表项}else--current->maj_flt;done:get_page(pte_page(pte));spin_unlock(&shm_lock);current->min_flt++;returnpte_page(pte);...}

shm_nopage() 函数的主要功能是当发生内存缺页时,申请新的物理内存页,并映射到共享内存中。由于使用共享内存时会映射到相同的物理内存页上,从而不同进程可以共用此块内存。

编辑:jq

在Linux系统中,每个进程都有独立的虚拟内存空间,也就是说不同的进程访问同一段虚拟内存地址所得到的数据是不一样的,这是因为不同进程相同的虚拟内存地址会映射到不同的物理内存地址上。

但有时候为了让不同进程之间进行通信,需要让不同进程共享相同的物理内存,Linux通过共享内存来实现这个功能。下面先来介绍一下Linux系统的共享内存的使用。

共享内存使用1. 获取共享内存

要使用共享内存,首先需要使用shmget()函数获取共享内存,shmget()函数的原型如下:

intshmget(key_tkey,size_tsize,intshmflg);参数key一般由ftok()函数生成,用于标识系统的唯一IPC资源。参数size指定创建的共享内存大小。参数shmflg指定shmget()函数的动作,比如传入IPC_CREAT表示要创建新的共享内存。

函数调用成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。失败返回-1,并设置错误码。

2. 关联共享内存

shmget()函数返回的是一个标识符,而不是可用的内存地址,所以还需要调用shmat()函数把共享内存关联到某个虚拟内存地址上。shmat()函数的原型如下:

void*shmat(intshmid,constvoid*shmaddr,intshmflg);参数shmid是shmget()函数返回的标识符。参数shmaddr是要关联的虚拟内存地址,如果传入0,表示由系统自动选择合适的虚拟内存地址。参数shmflg若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。

函数调用成功返回一个可用的指针(虚拟内存地址),出错返回-1。

3. 取消关联共享内存

当一个进程不需要共享内存的时候,就需要取消共享内存与虚拟内存地址的关联。取消关联共享内存通过shmdt()函数实现,原型如下:

intshmdt(constvoid*shmaddr);参数shmaddr是要取消关联的虚拟内存地址,也就是shmat()函数返回的值。

函数调用成功返回0,出错返回-1。

共享内存使用例子

下面通过一个例子来介绍一下共享内存的使用方法。在这个例子中,有两个进程,分别为进程A和进程B,进程A创建一块共享内存,然后写入数据,进程B获取这块共享内存并且读取其内容。

进程A#include#include#include#include#include#defineSHM_PATH"/tmp/shm"#defineSHM_SIZE128intmain(intargc,char*argv[]){intshmid;char*addr;key_tkey=ftok(SHM_PATH,0x6666);shmid=shmget(key,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);if(shmid< 0){printf("failedtocreatesharememory");return-1;}addr=shmat(shmid,NULL,0);if(addr<= 0){printf("failedtomapsharememory");return-1;}sprintf(addr,"%s","HelloWorld");return0;}进程B#include#include#include#include#include#include#defineSHM_PATH"/tmp/shm"#defineSHM_SIZE128intmain(intargc,char*argv[]){intshmid;char*addr;key_tkey=ftok(SHM_PATH,0x6666);charbuf[128];shmid=shmget(key,SHM_SIZE,IPC_CREAT);if(shmid< 0){printf("failedtogetsharememory");return-1;}addr=shmat(shmid,NULL,0);if(addr<= 0){printf("failedtomapsharememory");return-1;}strcpy(buf,addr,128);printf("%s",buf);return0;}

测试时先运行进程A,然后再运行进程B,可以看到进程B会打印出 “Hello World”,说明共享内存已经创建成功并且读取。

共享内存实现原理

我们先通过一幅图来了解一下共享内存的大概原理,如下图:

通过上图可知,共享内存是通过将不同进程的虚拟内存地址映射到相同的物理内存地址来实现的,下面将会介绍Linux的实现方式。

在Linux内核中,每个共享内存都由一个名为struct shmid_kernel的结构体来管理,而且Linux限制了系统最大能创建的共享内存为128个。通过类型为struct shmid_kernel结构的数组来管理,如下:

structshmid_ds{structipc_permshm_perm;intshm_segsz;__kernel_time_tshm_atime;__kernel_time_tshm_dtime;__kernel_time_tshm_ctime;__kernel_ipc_pid_tshm_cpid;__kernel_ipc_pid_tshm_lpid;unsignedshortshm_nattch;unsignedshortshm_unused;void*shm_unused2;void*shm_unused3;};structshmid_kernel{structshmid_dsu;unsignedlongshm_npages;pte_t*shm_pages;structvm_area_struct*attaches;};staticstructshmid_kernel*shm_segs[SHMMNI];//SHMMNI等于128

从注释可以知道struct shmid_kernel结构体各个字段的作用,比如shm_npages字段表示共享内存使用了多少个内存页。而shm_pages字段指向了共享内存映射的虚拟内存页表项数组等。

另外struct shmid_ds结构体用于管理共享内存的信息,而shm_segs数组用于管理系统中所有的共享内存。

shmget() 函数实现

通过前面的例子可知,要使用共享内存,首先需要调用shmget()函数来创建或者获取一块共享内存。shmget()函数的实现如下:

asmlinkagelongsys_shmget(key_tkey,intsize,intshmflg){structshmid_kernel*shp;interr,id=0;down(¤t->mm->mmap_sem);spin_lock(&shm_lock);if(sizeshmmax){err=-EINVAL;}elseif(key==IPC_PRIVATE){err=newseg(key,shmflg,size);}elseif((id=findkey(key))==-1){if(!(shmflg&IPC_CREAT))err=-ENOENT;elseerr=newseg(key,shmflg,size);}elseif((shmflg&IPC_CREAT)&&(shmflg&IPC_EXCL)){err=-EEXIST;}else{shp=shm_segs[id];if(shp->u.shm_perm.mode&SHM_DEST)err=-EIDRM;elseif(size>shp->u.shm_segsz)err=-EINVAL;elseif(ipcperms(&shp->u.shm_perm,shmflg))err=-EACCES;elseerr=(int)shp->u.shm_perm.seq*SHMMNI+id;}spin_unlock(&shm_lock);up(¤t->mm->mmap_sem);returnerr;}

shmget()函数的实现比较简单,首先调用findkey()函数查找值为key的共享内存是否已经被创建,findkey()函数返回共享内存在shm_segs数组的索引。如果找到,那么直接返回共享内存的标识符即可。否则就调用newseg()函数创建新的共享内存。newseg()函数的实现也比较简单,就是创建一个新的struct shmid_kernel结构体,然后设置其各个字段的值,并且保存到shm_segs数组中。

shmat() 函数实现

shmat()函数用于将共享内存映射到本地虚拟内存地址,由于shmat()函数的实现比较复杂,所以我们分段来分析这个函数:

asmlinkagelongsys_shmat(intshmid,char*shmaddr,intshmflg,ulong*raddr){structshmid_kernel*shp;structvm_area_struct*shmd;interr=-EINVAL;unsignedintid;unsignedlongaddr;unsignedlonglen;down(¤t->mm->mmap_sem);spin_lock(&shm_lock);if(shmid< 0)gotoout;shp=shm_segs[id=(unsignedint)shmid%SHMMNI];if(shp==IPC_UNUSED||shp==IPC_NOID)gotoout;

上面这段代码主要通过shmid标识符来找到共享内存描述符,上面说过系统中所有的共享内存到保存在shm_segs数组中。

if(!(addr=(ulong)shmaddr)){if(shmflg&SHM_REMAP)gotoout;err=-ENOMEM;addr=0;again:if(!(addr=get_unmapped_area(addr,shp->u.shm_segsz)))//获取一个空闲的虚拟内存空间gotoout;if(addr&(SHMLBA-1)){addr=(addr+(SHMLBA-1))&~(SHMLBA-1);gotoagain;}}elseif(addr&(SHMLBA-1)){if(shmflg&SHM_RND)addr&=~(SHMLBA-1);elsegotoout;}

上面的代码主要找到一个可用的虚拟内存地址,如果在调用shmat()函数时没有指定了虚拟内存地址,那么就通过get_unmapped_area()函数来获取一个可用的虚拟内存地址。

spin_unlock(&shm_lock);err=-ENOMEM;shmd=kmem_cache_alloc(vm_area_cachep,SLAB_KERNEL);spin_lock(&shm_lock);if(!shmd)gotoout;if((shp!=shm_segs[id])||(shp->u.shm_perm.seq!=(unsignedint)shmid/SHMMNI)){kmem_cache_free(vm_area_cachep,shmd);err=-EIDRM;gotoout;}

上面的代码主要通过调用kmem_cache_alloc()函数创建一个vm_area_struct结构,在内存管理一章知道,vm_area_struct结构用于管理进程的虚拟内存空间。

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++;spin_unlock(&shm_lock);err=shm_map(shmd);spin_lock(&shm_lock);if(err)gotofailed_shm_map;insert_attach(shp,shmd);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);returnerr;...}

上面的代码主要是设置刚创建的vm_area_struct结构的各个字段,比较重要的是设置其vm_ops字段为shm_vm_ops,shm_vm_ops定义如下:

staticstructvm_operations_structshm_vm_ops={shm_open,shm_close,NULL,NULL,NULL,NULL,shm_nopage,NULL,shm_swapout};

shm_vm_ops的nopage回调为shm_nopage()函数,也就是说,当发生页缺失异常时将会调用此函数来恢复内存的映射。

从上面的代码可看出,shmat()函数只是申请了进程的虚拟内存空间,而共享内存的物理空间并没有申请,那么在什么时候申请物理内存呢?答案就是当进程发生缺页异常的时候会调用shm_nopage()函数来恢复进程的虚拟内存地址到物理内存地址的映射。

shm_nopage() 函数实现

shm_nopage() 函数是当发生内存缺页异常时被调用的,代码如下:

staticstructpage*shm_nopage(structvm_area_struct*shmd,unsignedlongaddress,intno_share){pte_tpte;structshmid_kernel*shp;unsignedintidx;structpage*page;shp=*(structshmid_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];//共享内存的页表项if(!pte_present(pte)){//如果内存页不存在if(pte_none(pte)){spin_unlock(&shm_lock);page=get_free_highpage(GFP_HIGHUSER);//申请一个新的物理内存页if(!page)gotooom;clear_highpage(page);spin_lock(&shm_lock);if(pte_val(pte)!=pte_val(shp->shm_pages[idx]))gotochanged;}else{...}shm_rss++;pte=pte_mkdirty(mk_pte(page,PAGE_SHARED));//创建页表项shp->shm_pages[idx]=pte;//保存共享内存的页表项}else--current->maj_flt;done:get_page(pte_page(pte));spin_unlock(&shm_lock);current->min_flt++;returnpte_page(pte);...}

shm_nopage() 函数的主要功能是当发生内存缺页时,申请新的物理内存页,并映射到共享内存中。由于使用共享内存时会映射到相同的物理内存页上,从而不同进程可以共用此块内存。

编辑:jq .dfma { position: relative; width: 1000px; margin: 0 auto; } .dfma a::after { position: absolute; left: 0; bottom: 0; width: 30px; line-height: 1.4; text-align: center; background-color: rgba(0, 0, 0, .5); color: #fff; font-size: 12px; content:"广告"; } .dfma img { display: block; }
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。