进程间通讯之-共享内存Shared Memory--linux内核剖析(十一)

共享内存


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

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

共享内存容许两个或更多进程访问同一块内存,就如同 malloc() 函数向不一样进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。svg

关于共享内存


当一个程序加载进内存后,它就被分红叫做页的块。函数

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

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

由于全部进程共享同一块内存,共享内存在各类进程间通讯方式中具备最高的效率。访问共享内存区域和访问进程独有的内存区域同样快,并不须要经过系统调用或者其它须要切入内核的过程来完成。同时它也避免了对数据的各类没必要要的复制。命令行

若是没有共享内存的概念,那一个进程不能存取另一个进程的内存部分,于是致使共享数据或者通讯失效。由于系统内核没有对访问共享内存进行同步,您必须提供本身的同步措施。线程

解决这些问题的经常使用方法是经过使用信号量进行同步。不过,咱们的程序中只有一个进程访问了共享内存,所以在集中展现了共享内存机制的同时,咱们避免了让代码被同步逻辑搞得混乱不堪。指针

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

例如,在数据被写入以前不容许进程从共享内存中读取信息、不容许两个进程同时向同一个共享内存地址写入数据等。

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

  • 获取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\n");
        return -1;
    }

    // 将这块共享内存区附加到本身的内存段
    p_setting = (st_setting *)shmat(shm_id, NULL, 0);

    strncpy(p_setting->agen, "gatieme", 10);
    printf("agen : %s\n", p_setting->agen);

    p_setting->file_no = 1;
    printf("file_no : %d\n",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\n");
    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\n") == 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\n");
    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\n") == 0) break;
        pause();                            // 阻塞,等待信号
    }
    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL);          // 删除共享内存

    return 0;
}

这里写图片描述