《UNIX网络编程 卷2》 笔记: 共享内存区介绍

共享内存区是所有IPC形式中最快的。一旦这样的内存区映射到共享它的进程的地址空间,这些进程间的数据传递就不再涉及到内核。但是这些进程间通常需要使用某种形式的同步(前几节介绍的互斥锁、条件变量、读写锁、记录锁和信号量)。

回想一下我们曾在管道这一节讲述了如下一个例子:

《UNIX网络编程 卷2》 笔记: 共享内存区介绍_第1张图片

其中从服务器到客户的数据流如下图所示:

《UNIX网络编程 卷2》 笔记: 共享内存区介绍_第2张图片

可以看到数据流共穿越内核四次,每次都是开销比较大的复制操作(从内核空间复制数据到用户空间或从用户空间复制数据到内核空间)。

当两个进程间使用共享内存区后,服务器到客户的数据流如下图所示:

《UNIX网络编程 卷2》 笔记: 共享内存区介绍_第3张图片

可以看到使用共享内存区后,数据流只穿越内核两次:一次从输入文件到共享内存区,一次从共享内存区到输出文件。

进一步讨论前,我们先看一个例子。在这个例子中,父子进程都对同一个全局变量count进行加1操作。代码如下:

#include "unpipc.h"

#define SEM_NAME	"/mysem"

/*全局变量*/
int count = 0;

int main(int argc, char **argv)
{
	int i, nloop;
	sem_t *mutex;

	if (argc != 2)
		err_quit("usage: incr1 <#loops>");
	nloop = atoi(argv[1]);

	mutex = Sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1);
	Sem_unlink(SEM_NAME);

	/*将标准输出设置为非缓冲模式*/
	setbuf(stdout, NULL);
	if (Fork() == 0) { /*子进程*/
		for (i = 0; i < nloop; i++) {
			Sem_wait(mutex);
			printf("child: %d\n", count++);
			Sem_post(mutex);
		}
		exit(0);
	}
	for (i = 0; i < nloop; i++) { /*父进程*/
		Sem_wait(mutex);
		printf("parent: %d\n", count++);
		Sem_post(mutex);
	}
	exit(0);
}
该程序其中一次执行结果如下

liu@ubuntu:~/work$ ./incr1 10
parent: 0
parent: 1
parent: 2
parent: 3
parent: 4
parent: 5
parent: 6
parent: 7
parent: 8
parent: 9
child: 0
child: 1
child: 2
child: 3
child: 4
child: 5
child: 6
child: 7
child: 8
child: 9

从结果我们可以看出全局变量count并不是父子进程间共享的。实际上由于Linux的写时复制机制的存在,子进程修改的是count变量一个副本。

mmap函数可以把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间(也即创建地址空间的一个内存映射关系)。munmap函数则删除进程地址空间的一个映射关系。它们的定义如下:

#include 

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

下图很清楚地说明了mmap函数的功能:

《UNIX网络编程 卷2》 笔记: 共享内存区介绍_第4张图片

参数offset和len分别表示参数fd指向的文件中要映射到进程地址空间的数据的偏移地址和长度。参数addr指定这块数据被映射到进程地址空间的起始地址。它的值通常为NULL,指示让内核选择起始地址。

参数prot指定内存映射区的访问权限。PROT_READ表示数据可读,PROT_WRITE表示数据可写,PROT_EXEC表示数据可执行,PROT_NONE表示数据不可访问。

参数flag的值通常为MAP_SHAREDMAP_PRIVATE。前者表示调用进程对内存映射区的修改对其他共享该映射区的进程都可见,而且确实改变了其底层支撑对象(文件对象或Posix共享内存区对象)。后者表示调用进程对内存映射区的修改只对该进程可见,而不改变其底层支撑对象

介绍完mmap函数后,我们可以对上面的例子进行修改,使得父子进程间共享一个全局变量。代码如下:

#include "unpipc.h"

#define SEM_NAME	"/mysem"

int main(int argc, char **argv)
{
	int fd, i, nloop, zero = 0;
	int *ptr; /*父子间共享内存区的起始地址*/
	sem_t *mutex;

	if (argc != 3)
		err_quit("usage: incr2  <#loops>");
	nloop = atoi(argv[2]);

	/*创建一个文件,写入32位数据0*/
	fd = Open(argv[1], O_RDWR | O_CREAT, FILE_MODE);
	Write(fd, &zero, sizeof(int));
	/*映射文件到进程地址空间*/
	ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	Close(fd);
	/*创建一个二值信号量*/
	mutex = Sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1);
	Sem_unlink(SEM_NAME);

	/*将标准输出设置为非缓冲模式*/
	setbuf(stdout, NULL);
	
	if (Fork() == 0) {
		for (i = 0; i < nloop; i++) { /*子进程*/
			Sem_wait(mutex);
			printf("child: %d\n", (*ptr)++);
			Sem_post(mutex);
		}
		exit(0);
	}
	
	for (i = 0; i < nloop; i++) { /*父进程*/
		Sem_wait(mutex);
		printf("parent: %d\n", (*ptr)++);
		Sem_post(mutex);
	}
	exit(0);
}
父进程调用fork之前先调用mmap函数并指定MAP_SHARED标志映射一个文件中4字节大小的数据(值为0)到它的地址空间,这块映射区的起始地址为ptr。然后调用fork创建了子进程。关键是使用MAP_SHARED标志创建的内存映射关系会存留到子进程中,所以这块4字节大小的映射区实际上是父子进程间的共享内存区。如下图所示:

《UNIX网络编程 卷2》 笔记: 共享内存区介绍_第5张图片

使用共享内存区后,父子进程就能对同一数据(变量)进行操作。运行该程序,执行结果如下:

liu@ubuntu:~/work$ ./incr2 /tmp/123 10
parent: 0
parent: 1
parent: 2
parent: 3
parent: 4
parent: 5
parent: 6
parent: 7
parent: 8
parent: 9
child: 10
child: 11
child: 12
child: 13
child: 14
child: 15
child: 16
child: 17
child: 18
child: 19

我们还可以将有名信号量改为基于内存的信号量,并将它存放到共享内存区中。代码如下:

#include "unpipc.h"

struct shared {
	sem_t mutex;
	int count;
} shared;

int main(int argc, char **argv)
{
	int fd, i, nloop;
	struct shared *ptr;

	if (argc != 3)
		err_quit("usage: incr3  <#loops>");
	nloop = atoi(argv[2]);

	fd = Open(argv[1], O_RDWR | O_CREAT, FILE_MODE);
	Write(fd, &shared, sizeof(struct shared));
	/*映射文件*/
	ptr = Mmap(NULL, sizeof(struct shared), PROT_READ | PROT_WRITE, 
				MAP_SHARED, fd, 0);
	Close(fd);
	
	Sem_init(&ptr->mutex, 1, 1);
	/*将标准输出设置为非缓冲模式*/
	setbuf(stdout, NULL);
	if (Fork() == 0) { /*子进程*/
		for (i = 0; i < nloop; i++) {
			Sem_wait(&ptr->mutex);
			printf("child: %d\n", ptr->count++);
			Sem_post(&ptr->mutex);
		}
		exit(0);
	}
	/*父进程*/
	for (i = 0; i < nloop; i++) {
		Sem_wait(&ptr->mutex);
		printf("parent: %d\n", ptr->count++);
		Sem_post(&ptr->mutex);
	}
	exit(0);
}

此时程序示意图如下图所示:

《UNIX网络编程 卷2》 笔记: 共享内存区介绍_第6张图片

上面给出的例子我们都需要先创建一个文件,然后调用mmap创建内存映射关系。更简单的一种方法是直接映射/dev/zero这个特殊的设备文件。读该设备返回的字节全是0,写往该设备的任何字节都被丢弃。我们只需把上例17行开始的几行代码改为如下:

fd = Open("/dev/zero", O_RDWR);
ptr = Mmap(NULL, sizeof(struct shared), PROT_READ | PROT_WRITE, 
			MAP_SHARED, fd, 0);
Close(fd);

你可能感兴趣的:(UNIX网络编程,卷2)