共享内存小结

共享内存是一种高效的IPC机制。其做法是将两个或多个进程的某部分虚拟内存对应到相同的物理内存上去。这样,不同的进程只要发起对该部分虚拟内存的操作,实际上操作了同一块物理内存,进而可实现进程之间的交流。所谓共享内存,共享的就是这些物理内存空间。

这是简图,实际对应的物理内存未必是连续的。 操作系统能这么做,依赖于虚拟内存机制,而虚拟内存实际源于CPU的支持,确切地说是MMU这个硬件在起作用。MMU接到CPU发出的虚拟地址,依据页表中的信息,将其翻译成对应的物理地址。最后将这个物理地址发给内存,内存按照此地址读写相应数据。

由此可见,只要对页表做一些改动就可将不同进程的虚拟地址对应到同一个物理地址上去。操作系统内核应该就是这么干的。

主流操作系统均实现了共享内存机制,并提供了相应的接口供用户态程序使用:

使用mmap函数启动 - 映射命名文件

mmap函数的基本功效是将一个对象(磁盘上的文件,IO设备)映射到调用进程的一块连续的虚拟地址空间中。映射成功后,就可以像访问内存一样去访问该对象了。这么一来就可以避免因使用read()、write()调用而产生的拷贝动作。

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

mmap函数“请求”内核以addr为起始地址,在进程地址空间中腾出一片内存区,将文件描述符fd对应的对象的指定范围(由offset和length指定)映射到该虚拟内存区域。

mmap只是请求内核要这么做,至于内核实际如何处理,还要看情况。操作系统管理内存是以页面为单位的,如果指定的addr不是页面尺寸的整数倍,内核会选择一个对齐的地址来完成映射(flags未设置MAP_FIXED位)。如果置位MAP_FIXED,内核发现addr无法对齐的时候,mmap将直接返回失败。

通常将addr置为NULL,且flags忽略MAP_FIXED位,让内核确定映射内存的起始地址。

使用mmap函数来启动共享内存机制,关键在于对flags的设置。flags用来描述被映射对象的类型。若要通过映射命名文件(假设文件名为tmp)来实现内存共享的话,仅将flags置为MAP_SHARED,即:将被映射的这个文件视为共享对象来处理。

示例代码:共享内存由P1发起,P1向队尾Push数据,P2从队首Pop数据。

p1.cc

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio>

#define MAX_QUEUE_LEN   (64)

struct Queue {
    int begin;
    int end;
    short buf[MAX_QUEUE_LEN];
} queue;

int main(int argc, char** argv)
{
    int fd;
    Queue* shm_ptr;
    if (argc != 2) {
        std::clog << "Usage: " 
            << argv[0] << " file" << std::endl;
        return -1;
    }
    if ((fd = open(argv[1], O_RDWR | O_CREAT, 0666)) < 0) {
        std::clog << "open failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    write(fd, &queue, sizeof(Queue));
    shm_ptr = (Queue*)mmap(NULL, sizeof(Queue), 
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    if (MAP_FAILED == shm_ptr) {
        std::clog << "mmap failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr->begin = 0;
    shm_ptr->end = 0;
    memset(shm_ptr->buf, 0, sizeof(shm_ptr->buf));
    // push data to tail.
    while (shm_ptr->end != MAX_QUEUE_LEN) {
        shm_ptr->buf[shm_ptr->end] = (short)shm_ptr->end + 1;
        std::cout<< "p1 push " 
            << shm_ptr->buf[shm_ptr->end++] 
            << std::endl;
        usleep(2 * 1000 * 1000);
    }
    munmap(shm_ptr, sizeof(Queue));
    return 0;
}

编译执行

g++ p1.cc -o p1  
./p1 tmp

p2.cc

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio>

#define MAX_QUEUE_LEN   (64)

struct Queue {
    int begin;
    int end;
    short buf[MAX_QUEUE_LEN];
} queue;

int main(int argc, char** argv)
{
    int fd;
    Queue* shm_ptr;
    if (argc != 2) {
        std::clog << "Usage: " 
            << argv[0] << " file" << std::endl;
        return -1;
    }
    if ((fd = open(argv[1], O_RDWR)) < 0) {
        std::clog << "open failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr = (Queue*)mmap(NULL, sizeof(Queue), 
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    if (MAP_FAILED == shm_ptr) {
        std::clog << "mmap failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    // pop data from head
    while (shm_ptr->begin != MAX_QUEUE_LEN) {
        if (shm_ptr->begin != shm_ptr->end) {
            std::cout<< "p2 pop " 
                << shm_ptr->buf[shm_ptr->begin++] 
                << std::endl;
        }
    }
    munmap(shm_ptr, sizeof(Queue));
    return 0;
}

编译执行

g++ p2.cc -o p2  
./p2 tmp

这个共享对象(tmp文件)其实是一个“道具”,mmap需要借由它来促使内核按照共享内存的方式来处理。并不是说P1和P2依靠对tmp文件的I/O操作来进行通信(一方写文件,另一方读文件),在mmap过后,读写操作就使用内存操作的逻辑了。在mmap成功后就将文件描述符close掉了,也说明只是临时用一下这个“道具”而已。

使用mmap函数启动 - 映射匿名文件

上面使用mmap映射了一个实实在在的文件tmp,这样做多少有点麻烦。而且,这个文件只是一个“道具”,似乎没有什么实际作用。

mmap的flags置为MAP_SHARED | MAP_ANON,fd置为-1,offset置0即可使用匿名文件映射。

基于匿名文件映射而发起的共享内存,更适合有亲缘关系的进程之间的通信,例如:父子进程。

示例代码:共享内存由父进程发起,父进程向队尾Push数据,子进程从队首Pop数据。

p.cc

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio>

#define MAX_QUEUE_LEN   (64)

struct Queue {
    int begin;
    int end;
    short buf[MAX_QUEUE_LEN];
} queue;

int main(int argc, char** argv)
{
    pid_t pid;
    Queue* shm_ptr;
    shm_ptr = (Queue*)mmap(NULL, sizeof(Queue), 
            PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
    if (MAP_FAILED == shm_ptr) {
        std::clog << "mmap failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr->begin = 0;
    shm_ptr->end = 0;
    memset(shm_ptr->buf, 0, sizeof(shm_ptr->buf));
    if ((pid = fork()) < 0) {
        std::clog << "fork failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    // child : pop data from head
    if (0 == pid) {
        while (shm_ptr->begin != MAX_QUEUE_LEN) {
            if (shm_ptr->begin != shm_ptr->end) {
                std::cout<< "child pop " 
                    << shm_ptr->buf[shm_ptr->begin++] 
                    << std::endl;
            }
        }
    }
    // parent: push data to tail
    while (shm_ptr->end != MAX_QUEUE_LEN) {
        shm_ptr->buf[shm_ptr->end] = (short)shm_ptr->end + 1;
        std::cout<< "parent push " 
            << shm_ptr->buf[shm_ptr->end++] 
            << std::endl;
    }
    munmap(shm_ptr, sizeof(Queue));
    return 0;
}

编译执行

g++ p.cc -o p  
./p

POSIX共享内存区

上面的两种情况实际是借助mmap的特性达到了共享内存的目的,mmap并非专为共享内存而生。

Posix自己定义了一种开启共享内存的方式,主要使用shm_open接口,同时也要使用mmap把由shm_open打开的“对象”映射到调用进程的地址空间中。

这里的“对象”不再是磁盘上的文件,而是“POSIX共享内存区对象”。这个共享内存区对象实际上是存放在/dev/shm目录下的文件(由shm_open的name参数指定),而/dev/shm目录挂载的是tmpfs分区。

tmpfs是一种基于虚拟内存子系统的页面来存储文件的文件系统。也就是说tmpfs使用的是内存空间,既然使用的是内存空间,就是非持久的,如果机器重启,则数据丢失。

tmpfs文件系统是POSIX共享内存机制得以实现的基础。

示例代码:共享内存由P1发起,P1向队尾Push数据,P2从队首Pop数据。

p1.cc

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio>
#include <sys/mman.h>
#include <sys/stat.h>     
#include <fcntl.h>

#define MAX_QUEUE_LEN   (64)

struct Queue {
    int begin;
    int end;
    short buf[MAX_QUEUE_LEN];
} queue;

int main(int argc, char** argv)
{
    int fd;
    Queue* shm_ptr;
    if ((fd = shm_open("/tmp", O_CREAT | O_RDWR, 0666)) < 0) {
        std::clog << "shm_open failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    if (ftruncate(fd, sizeof(Queue)) < 0) {
        std::clog << "ftruncate failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr = (Queue*)mmap(NULL, sizeof(Queue), 
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    if (MAP_FAILED == shm_ptr) {
        std::clog << "mmap failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr->begin = 0;
    shm_ptr->end = 0;
    memset(shm_ptr->buf, 0, sizeof(shm_ptr->buf));
    // push data to tail.
    while (shm_ptr->end != MAX_QUEUE_LEN) {
        shm_ptr->buf[shm_ptr->end] = (short)shm_ptr->end + 1;
        std::cout<< "p1 push " 
            << shm_ptr->buf[shm_ptr->end++] 
            << std::endl;
        usleep(2 * 1000 * 1000);
    }
    munmap(shm_ptr, sizeof(Queue));
    return 0;
}

编译执行

g++ p1.cc -o p1  
./p1

p2.cc

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio>

#define MAX_QUEUE_LEN   (64)

struct Queue {
    int begin;
    int end;
    short buf[MAX_QUEUE_LEN];
} queue;

int main(int argc, char** argv)
{
    int fd;
    Queue* shm_ptr;
    if ((fd = shm_open("/tmp", O_RDWR, 0)) < 0) {
        std::clog << "shm_open failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr = (Queue*)mmap(NULL, sizeof(Queue), 
            PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    if (MAP_FAILED == shm_ptr) {
        std::clog << "mmap failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    // pop data from head
    while (shm_ptr->begin != MAX_QUEUE_LEN) {
        if (shm_ptr->begin != shm_ptr->end) {
            std::cout<< "p2 pop " 
                << shm_ptr->buf[shm_ptr->begin++] 
                << std::endl;
        }
    }
    munmap(shm_ptr, sizeof(Queue));
    return 0;
}

编译执行

g++ p2.cc -o p2  
./p2

shm_open在创建共享内存对象的时候,不能指定大小,需要用ftruncate函数进行设置。

还有,可以使用shm_unlink函数删除指定共享内存区的名字(这里是/tmp),调用这个函数时,若其他进程还在操作共享内存对象,则这些进程的执行不会受到影响,直到对该对象的引用全部关闭为止。上面的示例代码中没使用shm_unlink函数。

System V共享内存

System V共享内存涉及到四个函数。

shmget:创建或者打开一个共享内存段。 shmat:将指定的共享内存段映射到进程的虚拟地址空间中。 shmdt:将指定的共享内存段从进程中脱离出去。 shmctl:管控指定的共享内存段。

与POSIX共享内存相比,System V共享内存可以在创建的时候指定大小。

示例代码:共享内存由P1发起,P1向队尾Push数据,P2从队首Pop数据。P1在启动的时候若发现该共享内存已经存在,则将其删除后再重新发起。

p1.cc

#include <iostream>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio>
#include <fcntl.h>      
#include <sys/ipc.h>
#include <sys/shm.h>

#define MAX_QUEUE_LEN   (64)

struct Queue {
    int begin;
    int end;
    short buf[MAX_QUEUE_LEN];
} queue;

int main(int argc, char** argv)
{
    int shmid;
    key_t shmkey = 0x123456;
    Queue* shm_ptr;
    if ((shmid = shmget(shmkey, sizeof(Queue), 
        IPC_CREAT | IPC_EXCL | 0666)) < 0) {
        if (EEXIST != errno) {
            std::clog << "shmget create failed, " 
                << strerror(errno) << std::endl;
            return -1;
        }
        // shm exist, try to touch it.
        if ((shmid = shmget(shmkey, 0, 0666)) < 0) {
            std::clog << "shmget touch failed, " 
                << strerror(errno) << std::endl;
            return -1;
        } else {
            // remove the exist shm.
            if (shmctl(shmid, IPC_RMID, NULL) < 0) {
                std::clog << "shmctl failed, " 
                    << strerror(errno) << std::endl; 
                return -1;
            }
            // recreate new shm.
            if ((shmid = shmget(shmkey, sizeof(Queue), 
                IPC_CREAT | IPC_EXCL | 0666)) < 0) {
                std::clog << "shmget recreate failed, " 
                    << strerror(errno) << std::endl;
                return -1;
            }
        }
    }
    shm_ptr = (Queue*)shmat(shmid, NULL, 0);
    if (((void*)-1) == shm_ptr) {
        std::clog << "shmat failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr->begin = 0;
    shm_ptr->end = 0;
    memset(shm_ptr->buf, 0, sizeof(shm_ptr->buf));
    // push data to tail.
    while (shm_ptr->end != MAX_QUEUE_LEN) {
        shm_ptr->buf[shm_ptr->end] = (short)shm_ptr->end + 1;
        std::cout<< "p1 push " 
            << shm_ptr->buf[shm_ptr->end++] 
            << std::endl;
        usleep(2 * 1000 * 1000);
    }
    return 0;
}

编译执行

g++ p1.cc -o p1  
./p1

p2.cc

#include <iostream>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <cstdlib>
#include <cstdio>
#include <fcntl.h>      
#include <sys/ipc.h>
#include <sys/shm.h>

#define MAX_QUEUE_LEN   (64)

struct Queue {
    int begin;
    int end;
    short buf[MAX_QUEUE_LEN];
} queue;

int main(int argc, char** argv)
{
    int shmid;
    key_t shmkey = 0x123456;
    Queue* shm_ptr;
    if ((shmid = shmget(shmkey, 0, 0)) < 0) {
        std::clog << "shmget failed, " 
            <<  strerror(errno) << std::endl;
        return -1;
    }
    shm_ptr = (Queue*)shmat(shmid, NULL, 0);
    if (((void*)-1) == shm_ptr) {
        std::clog << "shmat failed, " 
            << strerror(errno) << std::endl;
        return -1;
    }
    // pop data from head
    while (shm_ptr->begin != MAX_QUEUE_LEN) {
        if (shm_ptr->begin != shm_ptr->end) {
            std::cout<< "p2 pop " 
                << shm_ptr->buf[shm_ptr->begin++] 
                << std::endl;
        }
    }
    return 0;
}

编译执行

g++ p2.cc -o p2  
./p2

使用”ipcs -m”命令可以查看共享内存的相关信息: