进程间通信(8) - 共享内存(posix)

1.前言

本篇文章的所有例子,基于RHEL6.5平台(linux kernal: 2.6.32-431.el6.i686)。

2.共享内存介绍

前面所讲述的Linux下面的各种进程间通信方式,例如:pipe(管道),FIFO(命名管道),message queue(消息队列),它们的共同点都是通过内核来进行通信(假设posix消息队列也是在内核中实现的,因为posix标准没有规定它的具体实现方式)。向pipe,fifo,message queue写入数据时,需要把数据从用户空间(用户进程)复制到内核,而从这些IPC读取数据时,又需要把数据从内核复制到用户空间。因此,所有的这些IPC方式,都需要在内核与用户进程之间进行2次数据复制,即进程间的通信必须通过内核来传递,如下图所示:
进程间通信(8) - 共享内存(posix)_第1张图片
                 通过内核进行进程间通信(IPC)

共享内存也是一种IPC,它是目前最快的IPC,它的使用方式是将同一个内存区映射到共享它的不同进程的地址空间中,这样这些进程间的通信就不再需要通过内核,只需对该共享的内存区域进程操作就可以了。和其他IPC不同的是,共享内存的使用需要用户自己进行同步操作。下图是共享内存区IPC的通信:
进程间通信(8) - 共享内存(posix)_第2张图片

3.映射函数mmap

每个进程都有自己的虚拟地址空间,我们知道除了堆中的虚拟内存我们可以由程序员灵活分配和释放,其他区域的虚拟内存都由系统控制,那么还有没有其他方法让程序员去灵活控制虚拟内存呢?linux下的mmap函数就由此而来。mmap函数可以为我们在进程的虚拟空间开辟一块新的虚拟内存,可以将一个文件映射到这块新的虚拟内存,所以操作新的虚拟内存就是操作这个文件。因此,mmap函数主要的功能就是将文件或设备映射到调用进程的地址空间中,当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用readwrite等系统调用。
UNIX网络编程第二卷进程间通信对mmap函数进行了说明。该函数主要用途有三个:
a)将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能。可以提供无亲缘进程间的通信;
b)将特殊文件进行匿名内存映射,可以为亲缘进程提供共享内存空间;
c)为无亲缘的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

#include   
void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);  
               //成功返回映射到进程地址空间的起始地址,失败返回MAP_FAILED
参数start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
参数len:映射到进程地址空间的字节数,它从被映射文件开头的第offset个字节处开始,offset通常被设置为0。
进程间通信(8) - 共享内存(posix)_第3张图片
参数prot:映射区域的保护方式。可以为以下几种方式的组合:

    ·  PROT_READ:数据可读;
    ·  PROT_WRITE:数据可写;
    ·  PROT_EXEC:数据可执行;
    ·  PROT_NONE:数据不可访问;

flags:设置内存映射区的类型标志,POSIX标志定义了以下三个标志:
    · MAP_SHARED:该标志表示,调用进程对被映射内存区的数据所做的修改对于共享该内存区的所有进程都可见,而且确实改变其底层的支撑对象(一个文件对象或是一个共享内存区对象)。
    ·  MAP_PRIVATE:调用进程对被映射内存区的数据所做的修改只对该进程可见,而不改变其底层支撑对象。
    ·  MAP_FIXED:该标志表示准确的解释start参数,一般不建议使用该标志,对于可移植的代码,应该把start参数置为NULL,且不指定MAP_FIXED标志。

    上面三个标志是在POSIX.1-2001标准中定义的,其中MAP_SHARED和MAP_PRIVATE必须选择一个。在Linux中也定义了一些非标准的标志,例如MAP_ANONYMOUS(MAP_ANON),MAP_LOCKED等,具体参考Linux手册。

fd:有效的文件描述符。如果设定了MAP_ANONYMOUS(MAP_ANON)标志,在Linux下面会忽略fd参数,而有的系统实现如BSD需要置fd为-1;

offset:相对文件的起始偏移。

从进程的地址空间中删除一个映射关系,需要用到下面的函数:

4.映射删除munmap

    从进程的地址空间中删除一个映射关系,需要用到下面的函数:
#include   
int munmap(void *start, size_t len);   //成功返回0,出错返回-1

start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。

len:映射区的大小。

5.映射同步

对于一个MAP_SHARED的内存映射区,内核的虚拟内存算法会保持内存映射文件和内存映射区的同步,也就是说,对于内存映射文件所对应内存映射区的修改,内核会在稍后的某个时刻更新该内存映射文件。如果我们希望硬盘上的文件内容和内存映射区中的内容实时一致,那么我们就可以调用msync开执行这种同步:

#include   
int msync(void *start, size_t len, int flags);  //成功返回0,出错返回-1

start:被映射到的进程地址空间的内存区的起始地址,即mmap返回的地址。

len:映射区的大小。

flags:同步标志,有以下三个标志:

    · MS_ASYNC:异步写,一旦写操作由内核排入队列,就立刻返回;

    · MS_SYNC:同步写,要等到写操作完成后才返回。

    . MS_INVALIDATE:使该文件的其他内存映射的副本全部失效。

6.内存映射区的大小

Linux下的内存是采用页式管理机制。通过mmap进行内存映射,内核生成的映射区的大小都是以页面大小PAGESIZE为单位,即为PAGESIZE的整数倍。如果mmap映射的长度不是页面大小的整数倍,那么多余空间也会被闲置浪费。

可以通过下面的方式来查看Linux的页面大小:
#include
#include

int main()
{
  int pSize=getpagesize();
  //或者int pSize=sysconf(_SC_PAGE_SIZE);
  std::cout<<"The page size is: "<
输出:
The page size is: 4096
从上述的运行结果,可以很明显的看出,当前系统的页大小为4k字节。
下面对映射文件的大小和映射长度的不同情况进行讨论。
1) 映射文件的大小等于映射长度

#include   
#include   
#include   

#include   
#include   
#include   

#define  PATH_NAME "/tmp/memmap"  

int main(int argc, char **argv)
{
        int fd;

        fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
        if (fd < 0)
        {
                std::cout << "open file " << PATH_NAME << " failed.";
                std::cout << strerror(errno) << std::endl;
                return -1;
        }

        if (ftruncate(fd, 5000) < 0) //修改文件大小为5000
        {
                std::cout << "change file size  failed.";
                std::cout << strerror(errno) << std::endl;

                close(fd);
                return -1;
        }

        char *memPtr;

        //指定映射长度为5000,与映射文件大小相等
        memPtr = (char *)mmap(NULL, 5000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);

        if (memPtr == MAP_FAILED)
        {
                std::cout << "mmap failed." << strerror(errno) << std::endl;
                return -1;
        }

        std::cout << "[0]:" << (int)memPtr[0] << std::endl;
        std::cout << "[4999]:" << (int)memPtr[4999] << std::endl;
        std::cout << "[5000]:" << (int)memPtr[5000] << std::endl;
        std::cout << "[8191]:" << (int)memPtr[8191] << std::endl;
        std::cout << "[8192]:" << (int)memPtr[8192] << std::endl;
        std::cout << "[4096*3-1]:" << (int)memPtr[4096*3-1] << std::endl;
        std::cout << "[4096*3]:" << (int)memPtr[4096*3] << std::endl;

        return 0;
}
输出:
[0]:0
[4999]:0
[5000]:0
[8191]:0
[8192]:60
[4096*3-1]:0
Segmentation fault (core dumped)

可以使用下图来分析上面的执行结果:

偏移0                         4999       
    |======文件=======|

下标0                         4999                               4096*2-1                4096*3-1
    |====内存映射区====|====第二页剩余部分====|====第三页====|====
    |--------------这段区间内访问不会出现问题------------|----------SIGSEGV


执行结果可以看到,能够完整的访问到前三页,在访问第四页的时候会产生SIGSEGV信号,发生Segmentation fault段越界访问错误。按照《UNIX 网络编程 卷2:进程间通信》中P257的讲解,内核会为该内存映射两个页面,访问前两个页面不会有问题,但访问第三个页面会产生SIGSEGV错误信号。这个差异具体应该是与底层实现有关。

2) 映射文件的大小小于映射长度

在上面代码的基础上,修改mmap内存映射函数中的第二个参数如果,即映射长度修改为4096*3,大于映射文件的大小(5000)。

memPtr = (char *)mmap(NULL, 4096 * 3, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

则运行结果为:
[root@MiWiFi-R1CM csdnblog]# ./a.out 
[0]:0
[4999]:0
[5000]:0
[8191]:0
Bus error (core dumped)
再次修改访问代码如下,让程序访问4096*3以后的内存映射区域

        std::cout << "[0]:" << (int)memPtr[0] << std::endl;
        std::cout << "[4999]:" << (int)memPtr[4999] << std::endl;
        std::cout << "[5000]:" << (int)memPtr[5000] << std::endl;
        std::cout << "[8191]:" << (int)memPtr[8191] << std::endl;
        std::cout << "[4096*3]:" << (int)memPtr[4096*3] << std::endl;
        std::cout << "[4096*4-1]:" << (int)memPtr[4096*4-1] << std::endl;
        std::cout << "[4096*4]:" << (int)memPtr[4096*4] << std::endl;
输出结果:
[0]:0
[4999]:0
[5000]:0
[8191]:0
[4096*3]:60
[4096*4-1]:0
Segmentation fault (core dumped)
使用下图来分析上面的执行结果:

偏移0                             4999       
    |====文件=========|

下标0                            4999                                     4096*2-1               4096*3-1               4096*4-1
    |====内存映射区====|====第二页剩余部分====|====第三页====|====第四页====|==========
    |-------------这段区间内访问不会出现问题------------|-------SIGBUG------|---访问不出问题--|-----SIGSEGV
    |-------------------------------------mmap大小-------------------------------------|

该执行结果和之前的映射文件大小和映射长度相同的情况有比较大的区别,在访问内存映射区内部但超出底层支撑对象的大小的区域部分会产生SIGBUS错误信息,产生生BUS error错误,但访问第四页不会出问题,访问第四页以后的内存区就会产生 SIGSEGV错误信息。按照《UNIX 网络编程 卷2:进程间通信》中P258的讲解,访问第三个页面以后的内存会产生SIGSEGV错误信号。这个差异具体应该是底层实现有关。

7.使用mmap进行IPC

下面将介绍mmap本身提供的进程间通信的两种方式,分别用于无亲缘和亲缘进程间的通信。

1).通过匿名内存映射提供亲缘进程间的通信

我们可以通过在父进程fork之前指定MAP_SHARED调用mmap,通过映射一个文件来实现父子进程间的通信,POSIX保证了父进程的内存映射关系保留到子进程中,父子进程对内存映射区的修改双方都可以看到。
在Linux 2.4以后,mmap提供匿名内存映射机制,即将mmap的flags参数指定为:MAP_SHARED | MAP_ANON。这样就彻底避免了内存映射文件的创建和打开,简化了对文件的操作。匿名内存映射机制的目的就是为了提供一个穿越父子进程间的内存映射区,很方便的提供了亲缘进程间的通信。
测试代码如下:

#include   
#include   
#include   
#include 


#include   
#include   
#include   


int main(int argc, char **argv)
{
        char *memPtr = (char *)mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, 0, 0);
        if (memPtr == MAP_FAILED)
        {
                std::cout << "mmap failed." << strerror(errno) << std::endl;
                return -1;
        }

        *memPtr = 0;

        if (fork() == 0)
        {
                const char* tmp = "hello world";
                memcpy(memPtr, tmp, strlen(tmp));
                std::cout << "child set memory: " << memPtr << std::endl;


                exit(0);
        }

        sleep(1);
        std::cout << "parent get memory: " << memPtr << std::endl;

        return 0;
}
输出:
[root@MiWiFi-R1CM csdnblog]# ./a.out      
child set memory: hello world
parent get memory: hello world
2).通过内存映射文件提供无亲缘进程间的通信

通过在不同进程间对同一内存映射文件进行映射,来进行无亲缘进程间的通信。
测试代码如下:

//进程1
#include   
#include   
#include   

#include   
#include   
#include   

#define  PATH_NAME "/tmp/memmap"  

int main()
{
	int *memPtr;
	int fd;

	fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
	if (fd < 0)
	{
		std::cout << "open file " << PATH_NAME << " failed.";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	ftruncate(fd, sizeof(int));

	memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	if (memPtr == MAP_FAILED)
	{
		std::cout << "mmap execute failed." << strerror(errno) << std::endl;
		return -1;
	}

	*memPtr = 12345;
	std::cout << "process " << getpid() << " send data: " << *memPtr << std::endl;

	return 0;
}

//进程2  
#include   
#include   
#include   

#include   
#include   
#include   

#define  PATH_NAME "/tmp/memmap"  

int main()
{
	int *memPtr;
	int fd;

	fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
	if (fd < 0)
	{
		std::cout << "open file " << PATH_NAME << " failed.";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	if (memPtr == MAP_FAILED)
	{
		std::cout << "mmap execute failed." << strerror(errno) << std::endl;
		return -1;
	}

	std::cout << "process " << getpid() << " receive data:" << *memPtr << std::endl;

	return 0;
}
输出:
[root@MiWiFi-R1CM csdnblog]# ./test1 
process 8454 send data: 12345
[root@MiWiFi-R1CM csdnblog]# ./test2 
process 8463 receive data:12345

上面的代码都没进行同步操作,在实际的使用过程要考虑到进程间的同步,通常会用信号量来进行共享内存的同步。

8.基于mmap的POSIX共享内存实现

前面介绍了通过内存映射文件进行进程间的通信的方式,现在要介绍的是通过POSIX共享内存区对象进行进程间的通信。POSIX共享内存使用方法有以下两个步骤:
    a)通过shm_open创建或打开一个POSIX共享内存对象;
    b)然后调用mmap将它映射到当前进程的地址空间;
和通过内存映射文件进行通信的使用上差别在于mmap描述符参数获取方式不一样:通过open或shm_open。如下图所示:

进程间通信(8) - 共享内存(posix)_第4张图片
<---------------------------------------------------------Posix内存去对象-------------------------------------------------------------------------------->

POSIX共享内存区对象的特殊操作函数就只有创建(打开)和删除两个函数,其他对共享内存区对象的操作都是通过已有的函数进行的。

#include 
int shm_open(const char *name, int oflag, mode_t mode);
                              //成功返回非负的描述符,失败返回-1
int shm_unlink(const char *name);
                              //成功返回0,失败返回-1

shm_open用于创建一个新的共享内存区对象或打开一个已经存在的共享内存区对象。

    --namePOSIX IPC的名字,前面关于POSIX进程间通信都已讲过关于POSIX IPC的规则,这里不再赘述。

    --oflag:操作标志,包含:O_RDONLYO_RDWRO_CREATO_EXCLO_TRUNC。其中O_RDONLYO_RDWR标志必须且仅能存在一项。

    --mode:用于设置创建的共享内存区对象的权限属性。和open以及其他POSIX IPCxxx_open函数不同的是,该参数必须一直存在,如果oflag参数中没有O_CREAT标志,该位可以置0

shm_unlink用于删除一个共享内存区对象,跟其他文件的unlink以及其他POSIX IPC的删除操作一样,对象的析构会等到该对象的所有引用全部关闭才会发生。

POSIX共享内存和POSIX消息队列,有名信号量一样都是具有随内核持续性的特点。

下面是通过POSIX共享内存进行通信的测试代码,代码中通过POSIX信号量来进行进程间的同步操作。

//进程1  
#include   
#include   
#include   

#include   
#include   
#include   
#include   

#define SHM_NAME "/memmap"  
#define SHM_NAME_SEM "/memmap_sem"   

char sharedMem[20];

int main()
{
	int fd;
	sem_t *sem;

	fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
	sem = sem_open(SHM_NAME_SEM, O_CREAT, 0666, 0);

	if (fd < 0 || sem == SEM_FAILED)
	{
		std::cout << "shm_open or sem_open failed...";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	ftruncate(fd, sizeof(sharedMem));

	char *memPtr;
	memPtr = (char *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	char msg[] = "hello world";

	memmove(memPtr, msg, sizeof(msg));
	std::cout << "process:" << getpid() << " send:" << memPtr << std::endl;

	sem_post(sem);
	sem_close(sem);

	return 0;
}

//进程2  
#include   
#include   
#include   

#include   
#include   
#include   
#include 
#include 

#define SHM_NAME "/memmap"  
#define SHM_NAME_SEM "/memmap_sem"   

int main()
{
	int fd;
	sem_t *sem;

	fd = shm_open(SHM_NAME, O_RDWR, 0);
	sem = sem_open(SHM_NAME_SEM, 0);

	if (fd < 0 || sem == SEM_FAILED)
	{
		std::cout << "shm_open or sem_open failed...";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	struct stat fileStat;
	fstat(fd, &fileStat);

	char *memPtr;
	memPtr = (char *)mmap(NULL, fileStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	sem_wait(sem);

	std::cout << "process:" << getpid() << " recv:" << memPtr << std::endl;

	sem_close(sem);

	return 0;
}
运行结果:
[root@MiWiFi-R1CM csdnblog]# g++ test2.cpp -o test2 -lrt
[root@MiWiFi-R1CM csdnblog]# g++ test1.cpp -o test1 -lrt
[root@MiWiFi-R1CM csdnblog]# ./test1 
process:12621 send:hello world
[root@MiWiFi-R1CM csdnblog]# ./test2 
process:12622 recv:hello world

在Linux 2.6.32中,对于POSIX信号量和共享内存的名字会在/dev/shm下建立对应的路径名,例如上面的测试代码,会生成如下的路径名:
[root@MiWiFi-R1CM csdnblog]# ll /dev/shm/
total 8
-rw-r--r--. 1 root root 20 Jun 21 20:39 memmap
-rw-r--r--. 1 root root 16 Jun 21 20:36 sem.memmap_sem

你可能感兴趣的:(Windows/Linux)