zoukankan      html  css  js  c++  java
  • 进程间通信之-共享内存Shared Memory--linux内核剖析(十一)

    共享内存


    共享内存是进程间通信中最简单的方式之中的一个。

    共享内存是系统出于多个进程之间通讯的考虑,而预留的的一块内存区。

    共享内存同意两个或很多其他进程訪问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。

    当一个进程改变了这块地址中的内容的时候,其他进程都会察觉到这个更改。

    关于共享内存


    当一个程序载入进内存后,它就被分成叫作页的块。

    通信将存在内存的两个页之间或者两个独立的进程之间。

    总之,当一个程序想和另外一个程序通信的时候。那内存将会为这两个程序生成一块公共的内存区域。这块被两个进程分享的内存区域叫做共享内存

    由于全部进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。

    訪问共享内存区域和訪问进程独有的内存区域一样快,并不须要通过系统调用或者其他须要切入内核的过程来完毕。同一时候它也避免了对数据的各种不必要的复制。

    假设没有共享内存的概念。那一个进程不能存取另外一个进程的内存部分。因而导致共享数据或者通信失效。由于系统内核没有对訪问共享内存进行同步,您必须提供自己的同步措施。

    解决这些问题的经常用法是通过使用信号量进行同步。

    只是,我们的程序中仅仅有一个进程訪问了共享内存。因此在集中展示了共享内存机制的同一时候。我们避免了让代码被同步逻辑搞得混乱不堪。

    为了简化共享数据的完整性和避免同一时候存取数据,内核提供了一种专门存取共享内存资源的机制。这称为相互排斥体或者mutex对象

    比如。在数据被写入之前不同意进程从共享内存中读取信息、不同意两个进程同一时候向同一个共享内存地址写入数据等。

    当一个进程想和另外一个进程通信的时候,它将按下面顺序运行:

    • 获取mutex对象,锁定共享区域。

    • 将要通信的数据写入共享区域。

    • 释放mutex对象。

    当一个进程从从这个区域读数据时候,它将反复相同的步骤,仅仅是将第二步变成读取。

    内存模型


    要使用一块共享内存

    • 进程必须首先分配

    • 随后须要訪问这个共享内存块的每一个进程都必须将这个共享内存绑定到自己的地址空间中

    • 当完毕通信之后,全部进程都将脱离共享内存,而且由一个进程释放该共享内存块

    /proc/sys/kernel/文件夹下,记录着共享内存的一些限制,如一个共享内存区的最大字节数shmmax。系统范围内最大共享内存区标识符数shmmni等,能够手工对其调整,但不推荐这样做。

    这里写图片描写叙述

    理解 Linux 系统内存模型能够有助于解释这个绑定的过程。

    linux系统内存模型


    在 Linux 系统中。每一个进程的虚拟内存是被分为很多页面的。这些内存页面中包括了实际的数据。

    每一个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。虽然每一个进程都有自己的内存地址,不同的进程能够同一时候将同一个内存页面映射到自己的地址空间中。从而达到共享内存的目的。

    分配一个新的共享内存块会创建新的内存页面。由于全部进程都希望共享对同一块内存的訪问。仅仅应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而仅仅是会返回一个标识该内存块的标识符。

    一个进程如需使用这个共享内存块,则首先须要将它绑定到自己的地址空间中。

    这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。

    当再也没有进程须要使用这个共享内存块的时候,必须有一个(且仅仅能是一个)进程负责释放这个被共享的内存页面。

    全部共享内存块的大小都必须是系统页面大小的整数倍。系统页面大小指的是系统中单个内存页面包括的字节数。在 Linux 系统中,内存页面大小是4KB。只是您仍然应该通过调用 getpagesize 获取这个值。

    共享内存的实现分为两个步骤:

    • 创建共享内存,使用shmget函数。

    • 映射共享内存。将这段创建的共享内存映射到详细的进程空间去,使用shmat函数。

    用于共享内存的函数


    共享内存的使用。主要有下面几个API:ftok()shmget()shmat()shmdt()及shmctl()。

    #include <sys/shm.h>
    void *shmat(int shm_id, const void *shm_addr, int shmflg);
    int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
    int shmdt(const void *shm_addr);
    int shmget(key_t key, size_t size, int shmflg);

    这里写图片描写叙述

    与信号量相相似,通常须要在包括shm.h文件之前包括sys/types.h与sys/ipc.h这两个头文件。

    用ftok()函数获得一个ID号


    应用说明,在IPC中,我们经经常使用用key_t的值来创建或者打开信号量,共享内存和消息队列。

    key_t ftok(const char *pathname, int proj_id);
    參数 描写叙述
    pathname 一定要在系统中存在而且进程能够訪问的
    proj_id 一个1-255之间的一个整数值,典型的值是一个ASCII值。

    当成功运行的时候,一个key_t值将会被返回。否则-1被返回。我们能够使用strerror(errno)来确定详细的错误信息。

    考虑到应用系统可能在不同的主机上应用,能够直接定义一个key,而不用ftok获得:

    #define IPCKEY 0x344378

    创建共享内存


    进程通过调用shmget(Shared Memory GET,获取共享内存)来分配一个共享内存块。

    int shmget(key_t key ,int size,int shmflg)
    參数 描写叙述
    key 一个用来标识共享内存块的键值
    size 指定了所申请的内存块的大小
    shmflg 操作共享内存的标识

    返回值:假设成功,返回共享内存表示符,假设失败,返回-1。

    • 该函数的第二个參数key是一个用来标识共享内存块的键值。

    彼此无关的进程能够通过指定同一个键以获取对同一个共享内存块的訪问。

    不幸的是,其他程序也可能挑选了相同的特定值作为自己分配共享内存的键值。从而产生冲突。

    用特殊常量IPC_PRIVATE作为键值能够保证系统建立一个全新的共享内存块。|

    key标识共享内存的键值:0/IPC_PRIVATE。当key的取值为IPC_PRIVATE,则函数shmget将创建一块新的共享内存;假设key的取值为0。而參数中又设置了IPC_PRIVATE这个标志,则相同会创建一块新的共享内存。

    • 该函数的第二个參数size指定了所申请的内存块的大小。

    由于这些内存块是以页面为单位进行分配的。实际分配的内存块大小将被扩大到页面大小的整数倍。

    • 第三个參数shmflg是一组标志。通过特定常量的按位或操作来shmget。这些特定常量包括:

    IPC_CREAT:这个标志表示应创建一个新的共享内存块。通过指定这个标志,我们能够创建一个具有指定键值的新共享内存块。

    IPC_EXCL:这个标志仅仅能与 IPC_CREAT 同一时候使用。当指定这个标志的时候。假设已有一个具有这个键值的共享内存块存在。则shmget会调用失败。

    也就是说,这个标志将使线程获得一个“独有”的共享内存块。假设没有指定这个标志而系统中存在一个具有相同键值的共享内存块。shmget会返回这个已经建立的共享内存块。而不是又一次创建一个。

    模式标志:这个值由9个位组成,分别表示属主、属组和其他用户对该内存块的訪问权限。

    当中表示运行权限的位将被忽略。

    指明訪问权限的一个简单办法是利用

    映射共享内存


    shmat()是用来同意本进程訪问一块共享内存的函数。将这个内存区映射到本进程的虚拟地址空间。

    int shmat(int shmid,char *shmaddr,int flag)
    參数 描写叙述
    shmid 那块共享内存的ID。是shmget函数返回的共享存储标识符
    shmaddr 是共享内存的起始地址,假设shmaddr为0,内核会把共享内存映像到调用进程的地址空间中选定位置。假设shmaddr不为0,内核会把共享内存映像到shmaddr指定的位置。所以一般把shmaddr设为0。
    shmflag 是本进程对该内存的操作模式。假设是SHM_RDONLY的话,就是仅仅读模式。

    其他的是读写模式

    成功时,这个函数返回共享内存的起始地址。失败时返回-1。

    要让一个进程获取对一块共享内存的訪问。这个进程必须先调用 shmat(SHared Memory Attach,绑定到共享内存)。

    将 shmget 返回的共享内存标识符 SHMID 传递给这个函数作为第一个參数。

    该函数的第二个參数是一个指针。指向您希望用于映射该共享内存块的进程内存地址;假设您指定NULL则Linux会自己主动选择一个合适的地址用于映射。

    第三个參数是一个标志位,包括了下面选项:

    SHM_RND表示第二个參数指定的地址应被向下靠拢到内存页面大小的整数倍。假设您不指定这个标志,您将不得不在调用shmat的时候手工将共享内存块的大小按页面大小对齐。
    SHM_RDONLY表示这个内存块将仅同意读取操作而禁止写入。 假设这个函数调用成功则会返回绑定的共享内存块相应的地址。通过 fork 函数创建的子进程同一时候继承这些共享内存块;

    假设须要,它们能够主动脱离这些共享内存块。 当一个进程不再使用一个共享内存块的时候

    共享内存解除映射


    当一个进程不再须要共享内存时,须要把它从进程地址空间中多里。

    int shmdt(char *shmaddr)
    參数 描写叙述
    shmaddr 那块共享内存的起始地址

    成功时返回0。失败时返回-1。

    应通过调用 shmdt(Shared Memory Detach。脱离共享内存块)函数与该共享内存块脱离。

    将由 shmat 函数返回的地址传递给这个函数。假设当释放这个内存块的进程是最后一个使用该内存块的进程,则这个内存块将被删除。

    对 exit 或不论什么exec族函数的调用都会自己主动使进程脱离共享内存块。

    控制释放


    shmctl控制对这块共享内存的使用

    函数原型

    int  shmctl( int shmid , int cmd , struct shmid_ds *buf );
    參数 描写叙述
    shmid 是共享内存的ID。
    cmd 控制命令
    buf 一个结构体指针。

    IPC_STAT的时候,取得的状态放在这个结构体中。假设要改变共享内存的状态,用这个结构体指定。

    当中cmd的取值例如以下

    cmd 描写叙述
    IPC_STAT 得到共享内存的状态
    IPC_SET 改变共享内存的状态
    IPC_RMID 删除共享内存

    返回值: 成功:0 失败:-1

    调用 shmctl(”Shared Memory Control”,控制共享内存)函数会返回一个共享内存块的相关信息。同一时候 shmctl 同意程序改动这些信息。

    该函数的第一个參数是一个共享内存块标识。
    要获取一个共享内存块的相关信息,则为该函数传递 IPC_STAT 作为第二个參数,同一时候传递一个指向一个 struct shmid_ds 对象的指针作为第三个參数。

    要删除一个共享内存块,则应将 IPC_RMID 作为第二个參数。而将 NULL 作为第三个參数。

    当最后一个绑定该共享内存块的进程与其脱离时,该共享内存块将被删除。

    您应当在结束使用每一个共享内存块的时候都使用 shmctl 进行释放,以防止超过系统所同意的共享内存块的总数限制。

    调用 exit 和 exec 会使进程脱离共享内存块,但不会删除这个内存块。

    要查看其他有关共享内存块的操作的描写叙述,请參考shmctl函数的手冊页。

    演示样例


    简单映射一块共享内存

    #include <stdio.h>
    #include <stdlib.h>
    
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <string.h>
    
    
    #define IPCKEY 0x366378
    
    
    
    typedef struct st_setting
    {
        char agen[10];
        unsigned char file_no;
    }st_setting;
    
    int main(int argc, char** argv)
    {
        int         shm_id;
        //key_t       key;
        st_setting  *p_setting;
    
        //  首先检查共享内存是否存在,存在则先删除
        shm_id = shmget(IPCKEY , 1028, 0640);
        if(shm_id != -1)
        {
            p_setting = (st_setting *)shmat(shm_id, NULL, 0);
    
            if (p_setting != (void *)-1)
            {
                shmdt(p_setting);
    
                shmctl(shm_id,IPC_RMID,0) ;
            }
        }
    
        //  创建共享内存
        shm_id = shmget(IPCKEY, 1028, 0640 | IPC_CREAT | IPC_EXCL);
        if(shm_id == -1)
        {
            printf("shmget error
    ");
            return -1;
        }
    
        //  将这块共享内存区附加到自己的内存段
        p_setting = (st_setting *)shmat(shm_id, NULL, 0);
    
        strncpy(p_setting->agen, "gatieme", 10);
        printf("agen : %s
    ", p_setting->agen);
    
        p_setting->file_no = 1;
        printf("file_no : %d
    ",p_setting->file_no);
    
        system("ipcs -m");//  此时可看到有进程关联到共享内存的信息,nattch为1
    
        //  将这块共享内存区从自己的内存段删除出去
        if(shmdt(p_setting) == -1)
           perror(" detach error ");
    
        system("ipcs -m");//  此时可看到有进程关联到共享内存的信息。nattch为0
    
        //  删除共享内存
        if (shmctl( shm_id , IPC_RMID , NULL ) == -1)
        {
            perror(" delete error ");
        }
    
        system("ipcs -m");//  此时可看到有进程关联到共享内存的信息,nattch为0
    
    
        return EXIT_SUCCESS;
    }
    
    

    这里写图片描写叙述

    ipcrm命令删除共享内存

    在使用共享内存。结束程序退出后。假设你没在程序中用shmctl()删除共享内存的话。一定要在命令行下用ipcrm命令删除这块共享内存。

    你要是无论的话,它就一直在那儿放着了。


    简单解释一下ipcs命令和ipcrm命令。

    取得ipc信息:

    usage : ipcs -asmq -tclup 
        ipcs [-s -m -q] -i id
        ipcs -h for help.
    m      输出有关共享内存(shared memory)的信息
    -q      输出有关信息队列(message queue)的信息
    -s      输出有关“遮断器”(semaphore)的信息

    删除ipc

    usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
              [-Q msgkey] [-M shmkey] [-S semkey] ... ]
    

    两端通信的程序


    读者程序


    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <signal.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    
    #define N 64
    
    typedef struct
    {
        pid_t pid;
        char buf[N];
    } SHM;
    
    void handler(int signo)
    {
        //printf("get signal
    ");
        return;
    }
    
    int main()
    {
        key_t key;
        int shmid;
        SHM *p;
        pid_t pid;
    
        if ((key = ftok(".", 'm')) < 0)
        {
            perror("fail to ftok");
            exit(-1);
        }
    
        signal(SIGUSR1, handler);//注冊一个信号处理函数
        if ((shmid = shmget(key, sizeof(SHM), 0666|IPC_CREAT|IPC_EXCL)) < 0)
        {
            if (EEXIST == errno)//存在则直接打开
            {
                shmid = shmget(key, sizeof(SHM), 0666);
                p = (SHM *)shmat(shmid, NULL, 0);
                pid = p->pid;
                p->pid = getpid();//把自己的pid写到共享内存
                kill(pid, SIGUSR1);
            }
            else//出错
            {
                perror("fail to shmget");
                exit(-1);
            }
        }
        else//成功
        {
            p = (SHM *)shmat(shmid, NULL, 0);
            p->pid = getpid();
            pause();
            pid = p->pid;//得到写端进程的pid
        }
    
        while ( 1 )
        {
            pause();//堵塞,等待信号
            if (strcmp(p->buf, "quit
    ") == 0) exit(0);//输入"quit结束"
            printf("read from shm : %s", p->buf);
            kill(pid, SIGUSR1);//向写进程发SIGUSR1信号
        }
    
        return 0;
    
    
    }
    

    写者程序


    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <signal.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    
    #define N 64
    
    typedef struct
    {
        pid_t pid;
        char buf[N];
    } SHM;
    
    void handler(int signo)
    {
        //printf("get signal
    ");
        return;
    }
    
    int main()
    {
        key_t key;
        int shmid;
        SHM *p;
        pid_t pid;
    
        if ((key = ftok(".", 'm')) < 0)
        {
            perror("fail to ftok");
            exit(-1);
        }
    
        signal(SIGUSR1, handler);               //  注冊一个信号处理函数
        if ((shmid = shmget(key, sizeof(SHM), 0666 | IPC_CREAT | IPC_EXCL)) < 0)
        {
            if (EEXIST == errno)                //  存在则直接打开
            {
                shmid = shmget(key, sizeof(SHM), 0666);
    
                p = (SHM *)shmat(shmid, NULL, 0);
    
                pid = p->pid;
                p->pid = getpid();
                kill(pid, SIGUSR1);
            }
            else//出错
            {
                perror("fail to shmget");
                exit(-1);
            }
        }
        else//成功
        {
    
            p = (SHM *)shmat(shmid, NULL, 0);
            p->pid = getpid();                  //  把自己的pid写到共享内存
            pause();
            pid = p->pid;                       //  得到读端进程的pid
    
        }
    
        while ( 1 )
        {
            printf("write to shm : ");
            fgets(p->buf, N, stdin);            //  接收输入
            kill(pid, SIGUSR1);                 //  向读进程发SIGUSR1信号
            if (strcmp(p->buf, "quit
    ") == 0) break;
            pause();                            //  堵塞,等待信号
        }
        shmdt(p);
        shmctl(shmid, IPC_RMID, NULL);          //  删除共享内存
    
        return 0;
    }
    

    这里写图片描写叙述

  • 相关阅读:
    服务器迁移总结
    使用OpenSSL生成证书
    mysql Event、存储过程、表命令
    负载均衡 > 常见问题
    SpringMVC记住密码功能
    spring mvc +cookie+拦截器功能 实现系统自动登陆
    android studio之argument for @notnull parameter 'name'
    jQuery ajax表单提交实现局部刷新
    Spring MVC 中采用注解方式 Action中跳转到另一个Action的写法
    ajax表单提交全路径
  • 原文地址:https://www.cnblogs.com/blfbuaa/p/7145946.html
Copyright © 2011-2022 走看看