37-System V——共享内存函数详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82872197

1. shmat函数


   调用shmget函数后,系统会在内核空间中分配一块共享物理内存,进程想要访问该共享内存的话,必须调用shmat函数把进程空间的虚拟地址映射到该共享内存的物理地址。

  当然这个地址映射过程是由系统来完成的,也就是说,系统内核会在当前的进程空间选择合适的,且空闲未使用的虚拟地址映射到共享内存的物理页面。

#include <sys/types.h>
#inlude <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid , const void *shmaddr, int shmflg);

返回值说明:成功则返回映射成功的虚拟地址,失败则返回void *类型的-1,即(void *) -1

参数shmid:IPC对象的标识符(由shmget函数返回创建的IPC对象标识符)。

shmflg参数:是一个权限标志位,如果shmflg = 0表示默认权限可读写,如果指定shmflg = HM_RDONLY表示只读模式,其他为读写模式。

参数shmaddr:表示进程空间的虚拟地址,shmat函数会把shmaddr地址映射到共享内存,具体如何映射是由参数shmaddr和shmflg来决定。


2. 关于参数shmaddr


一般参数shmaddr一般有以下几种方式:

  1. shmaddr参数为NULL,那么系统将自动选择一个合适的虚拟地址挂接到共享内存的物理页面,这个过程有系统完成(通常shmaddr指定为NULL,推荐方式)。
  2. shmaddr不为NULL,但未指定了shmflg = SHM_RND,那么将shmaddr地址挂接到共享内存的物理地址,如果挂接成功返回shmaddr地址。如果指定了shmflg = SHM_RND,那么将shmaddr地址挂接到共享内存的(shmaddr - (shmaddr % SHMLBA))所表示的地址,SHM_RND表示取整的意思。如果挂接成功返回shmaddr地址。

  为什么推荐方式一?

  当shmat函数映射成功后,就会返回一个虚拟地址,然后程序就可以通过这个虚拟地址访问共享内存了。有时候我们并不知道进程的虚拟空间中哪些虚拟地址没有使用,因此shmaddr参数指定的虚拟地址有可能是已经使用的虚拟地址,为了防止这种情况,一般都会把shmaddr参数指定为NULL,让系统自动映射一个地址。

  还有一个原因在于,如果shmaddr指定为非NULL的话,这降低了程序的可移植性,可能会出现在一个unix系统上有效的地址,在另一个unix系统上却无效。


3. shmat函数挂接的陷阱


   关于shmat函数多次调用的问题。

   shmget函数第一次创建的共享内存还不能被进程访问,为了能够访问共享内存必须调用shmat函数把进程的虚拟地址挂接(attach)到共享内存。

void *ptr = shmat(shmid , NULL , 0);

   当一个进程调用shmat函数对同一块共享内存(物理内存页)进行多次挂接,且shmaddr参数为NULL的话,那么shmat函数每次调用返回的都是不同的虚拟地址,且对该共享内存的挂接次数(attach)加1,也就是进程的多个虚拟地址指向同一块物理内存,那么在删除共享内存时也应该调用shmdt函数取消挂接,直到attach为0 。

   因此考虑这么一种情况:一个进程的多个虚拟地址指向同一块物理内存,但是程序在后期没有多次调用shmdt函数取消对该共享内存的挂接(虚拟地址没有全部释放),由于进程并没有取消关联共享内存,那么会一直占用进程空间资源,这可能会导致进程空间虚拟地址使用完下次调用shmat函数或其他操作出错。

   为了避免一个进程对同一共享内存多次挂接这种情况发生,我们需要对shmat函数再进行封装,并判断需要申请共享内存的指针ptr 是否不为NULL,如果ptr = NULL则允许挂接共享内存,如果ptr != NULL则不允许挂接。

代码如下:

if(shmaddr != NULL){   
	printf("不是第一次挂接共享内存\n");
    	return;
}else{
    shmaddr = shmat(shmid , shmaddr , 0);
}

4. shmdt函数


   shmat函数的作用我们知道了,那么shmdt函数的作用正好相反,shmdt是用来取消进程空间的地址空间和共享内存的物理地址的挂接,不让进程访问共享内存了。

#include <sys/types.h>
#inlude <sys/ipc.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

返回值说明:成功返回0,失败返回-1并设置errno

shmdt函数的参数就是shmat挂接成功后返回的虚拟地址。


shmdt函数注意事项:
   1 . shmdt函数只是将当前进程和共享内存分离,并不会从系统中删除其IPC对象id以及其相关数据结构,只是将该内存对于当前进程来说不再可用,并不代表其他进程不可以对共享内存的访问。

   2. 一般来说进程终止的话,那么该进程对共享的内存的挂接也会分离,但是此时共享内存并不会立即从系统中消失,因为此时可能还有别的进程在连接着这块共享内存。

   3. 一旦使用shmdt函数将进程的虚拟地址和共享内存物理地址分离后,共享内存会对该进程标记为不可访问,该进程不能再使用shmat函数建立映射,虽然可以建立连接,但是该进程在访问共享内存时会发生segment fault错误。


5. shmctl函数

   shmctl函数是用来操作共享内存的函数,比如:获取共享内存信息,设置共享内存信息,删除共享内存等操作,都可以用shmctl来完成。

#include <sys/types.h> 
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数shmid: shmid是共享内存IPC内核对象的标识符shmid

参数cmd: 表示要执行的命令操作,总共有5种,这里只说三种IPC_STAT,IPC_SET,IPC_RMID三种命令

  1. IPC_STAT: IPC_STAT: 获取共享内存属性,并把获取到的共享内存的shmid_ds结构体信息拷贝到buf中。

  2. IPC_SET: 设置共享内存属性,把buf指向的shmid_ds结构体中的uid,gid,mode等信息复制到共享内存的shmid_ds结构体中。

  3. IPC_RMID: 删除这片共享内存

参数buf: 共享内存管理结构体(struct shmid_ds)


关于IPC_RMID选项:
   在调用shmctl函数指定IPC_RMID删除共享内存时,如果该共享内存的挂接次数(attach)为0,那么将会执行删除操作,如果当前还有进程在使用该共享内存(attach不为0),那么将会在所有进程调用shmdt与共享内存分离后再执行删除操作。


   shmctl函数的参数buf的数据类型是一个struct shmid_ds结构体,该结构体具体定义信息参考:57-System V 共享内存-shmctl


6. 父子进程读写共享内存示例


父进程往共享内存中写数据,子进程从共享内存中读取数据。

#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>

int main(void)
{
        key_t key = 0x112233;
        int shmid = 0;
        int ret = 0;
        void *shmaddr = NULL;

        //创建共享内存页大小为4K
        shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0644);
        if(shmid < 0){
                perror("shmget error:");
        }

        //映射虚拟地址
        shmaddr = shmat(shmid , NULL , 0);
        //出错
        if(shmaddr == (void *)-1){
                perror("shmat error:");
        }
        printf("shmaddr = %p\n" , (char *)shmaddr);

        //清空内存
        memset(shmaddr , 0 , 4096);
        char str[20] = "hello world";

        //往共享内存写入hello world
        strncpy(shmaddr , str , strlen(str));

        pid_t pid;
        pid = fork();

        //子进程读取数据
        if(pid == 0){
                //从共享内存中读取数据并打印
                memset(str , 0 , sizeof(str));
                strncpy(str , shmaddr , sizeof(str));
                printf("str = %s\n" , str);
                //取消挂接
                ret = shmdt(shmaddr);
                if(ret < 0){
                        perror("shmdt error:");
                }
        }

        //父进程等待
        sleep(5);
        //删除共享内存
        ret = shmctl(shmid , IPC_RMID , NULL);
        if(ret < 0){
                perror("shmctl error:");
        }
        //回收子进程
        if(pid > 0){
                wait(NULL);
        }
        return 0;
}

程序执行结果:
在这里插入图片描述

   在调用fork后,子进程会继承父进程的shmid,挂接的虚拟地址等信息,因此子进程可以直接通过shmid访问共享内存并打印hello world,然后子进程取消对共享内存的挂接后,nattch减为1,当父进程调用shmtcl删除共享内存IPC对象退出时,nattch就会减为0,并把IPC对象从系统内核中删除掉。


7. 总结

   本篇着重介绍了在使用共享内存操作函数应该注意的事项以及可能会出现的错误和处理方法,掌握这些注意事项和错误处理,在使用共享内存函数时能避免一些坑了。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82872197
今日推荐