在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\n");return-1;}addr=shmat(shmid,NULL,0);if(addr<=0){printf("failedtomapsharememory\n");return-1;}sprintf(addr"%s","HelloWorld\n");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\n");return-1;}addr=shmat(shmid,NULL,0);if(addr<=0){printf("failedtomapsharememory\n");return-1;}strcpy(buf,addr,128);printf("%s",buf);return0;}测试时,先运行进程A,再运行进程B,可以看到进程B会打印出“HelloWorld”,说明共享内存创建成功,阅读共享内存的实现原理。我们先通过一张图来了解一下共享内存的大致原理,如下图所示:从上图可以看出,共享内存是通过将不同进程的虚拟内存地址映射到同一个物理内存地址来实现的。下面介绍Linux的实现方法。在Linux内核中,每一个共享内存都由一个名为structshmid_kernel的结构体来管理,Linux将系统最多可以创建的共享内存限制为128个。它由一个structshmid_kernel结构类型的数组管理,如下所示:/__kernel_ipc_pid_tshm_cpid;/*pidofcreator*/__kernel_ipc_pid_tshm_lpid;/*pidoflastoperator*/unsignedshortshm_nattch;/*no.ofcurrentattaches*/unsignedshortshm_unused;/*兼容性*/void*unshm_unused-usid2/*edittounused*/};structshmid_dnel{;structshmid_dnelthefollowingareprivate*/unsignedlongshm_npages;/*sizeofsegment(pages)*/pte_t*shm_pages;/*arrayofptrstoframes->SHMMAX*/structvm_area_struct*attaches;/*descriptorsforattaches*/};staticstructshmid_kernel*shm_segs[SHMMNI];//SHMMNI等于128从注释中我们可以知道structshmid_kernel结构体的各个字段的作用,比如shm_npages字段表示共享内存使用了多少内存页。shm_pages字段指向共享内存映射的虚拟内存页表项数组等。另外,structshmid_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(size<0||size>shmmax){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()函数的实现也比较简单,就是新建一个structshmid_kernel结构体,然后设置各个字段的值,保存在shm_segs数组中。shmat()函数实现了shmat()函数将共享内存映射到本地虚拟内存地址。由于shmat()函数的实现比较复杂,我们分段分析这个函数: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;再次: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);/*rounddown*/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_READONLYshmd>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);/*insertshmdintoshp->attaches*/shp->u.shm_lpid=current->pid;shp->u.shm_atime=CURRENT_TIME;*raddr=addr;err=0;out:spin_unlock(&shm_lock);向上(¤t->mm->mmap_sem);returnerr;...}上面的代码主要是设置新建的vm_area_struct结构体的字段,比较重要的是设置它的vm_ops字段为shm_vm_ops,shm_vm_ops定义如下:staticstructvm_operations_structshm_vm_ops={shm_open,/*open-callbackforanewvm-areaopen*/shm_close,/*close-callbackforwhenthevm-areaisreleased*/NULL,/*noneedtosyncpagesatunmap*/NULL,/*protect*/NULL,/*sync*/NULL,/*advise*/shm_nopage,/*nopage*/NULL,/*wppage*/shm_swapout/*swapout*/};shm_vm_ops的nopage回调是shm_nopage()函数,也就是说当发生缺页异常时,会调用该函数恢复内存映射。从上面的代码我们可以看出shmat()函数只申请了进程的虚拟内存空间,并没有申请共享内存的物理空间,那么它什么时候申请物理内存呢?答案是当进程发生pagefault时会调用shm_nopage()函数来恢复进程的虚拟内存地址到物理内存地址的映射。shm_nopage()函数实现内存页错误时调用shm_nopage()函数,代码如下:*page;shp=*(structshmid_kernel**)shmd->vm_private_data;idx=(address-shmd->vm_start+shmd->vm_offset)>>PAGE_SHIFT;spin_lock(&shm_lock);再次:pte=shp->shm_pages[idx];//共享内存页表项if(!pte_present(pte)){//如果内存页不存在if(pte_none(pte)){spin_unlock(&shm_lock);page=get_free_highpage(GFP_HIGHUSER);//申请一个新的物理内存页.}shm_rss++;pte=pte_mkdirty(mk_pte(page,PAGE_SHARED));//创建页表项shp->shm_pages[idx]=pte;//保存共享内存页表项}else--current->maj_flt;/*wasincrementedindo_no_page*/done:get_page(pte_page(pte));spin_unlock(&shm_lock);current->min_flt++;returnpte_page(pte);...}shm_nopage()函数的主要作用是在发生内存页错误时申请新的物理内存页,以及映射到共享内存。由于共享内存映射到同一个物理内存页,不同的进程可以共享这块内存。