UNP卷二 chapter12 共享内存区介绍

1、共享内存区是可用IPC形式中最快的

一旦某个某个内存区映射到相应进程的地直空间,则这些进程间数据的传递就不再涉及内核(即进程不再通过执行任何进入内核的系统调用来彼此传递数据)。但是往该共享内存区存放信息或从中取走信息的进程间通常需要某种形式的同步(如,互斥锁、条件变量、读写锁、记录锁、信号量)。

以下通过管道、FIFO或消息队列的IPC通道方式进行消息传递,产生的文件复制情况:

上图中将进行4次文件内容从内核到进程或进程到内核的复制,这将极大的消耗时间。

以下是通过两个或多个进程共享一个内存区,共享内存区这种IPC形式提供了绕过上述问题的办法。但必须在进程间协调或同步对该共享内存区的使用。其步骤及流程图如下:

i、服务器使用一个信号量取得访问某个共享内存区对象的权力;

ii、服务器将数据从输入文件读入到该共享内存区对象。read函数的第二个参数所指定的数据缓冲区地址指向这个共享内存区对象;

iii、服务器读入完毕时,使用一个信号量通知客户;

iv、客户将这些数据从该共享内存区对象写出到输出文件中。

上图中将进行2次文件内容从内核到进程或进程到内核的复制,这将极大的缩短交互时间。

2、mmap、munmap和msync函数

针对mmap函数:把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。使用些函数有三个目的:

i、使用普通文件以提供内存映射I/O;

ii、使用特殊文件以提供匿名内存映射(即MAP_ANON,fd=-1);

iii、使用shm_open以提供无亲缘关系进程间的Posix共享内存区。

#include<sys/mman.h>
void* mmap(void* addr, size_t len, int prot, int flags, int fd, off_t offset);
					//返回:若成功则为被映射区的起始地址,若出错则为MAP_FAILED

其中addr可以指定描述符fd应被映射到的进程内空间的起始地址。如果为一个空指针,则告诉内核自己云选择起始地址。该函数的返回值都是描述符fd所映射到内存区的起始地址。

len是映射到调用进程地址空间中的字节数,它从被映射文件开关起第offset个字节处开始算。offset通常设置为0。映射关系如下图所示:

内存映射区的保护由prot参数指定,参见下表: 

mmap的prot参数
prot 说明
PROT_READ 数据可读
PROT_WRITE 数据可写
PROT_EXEC 数据可执行
PROT_NONE 数据不可访问

flags使用参见下表:

Flags 说明
MAP_SHARED 变动是共享的
MAP_PRIVATE 变动是私自的
MAP_FIXED 准确地解释addr参数

需要值得注意的是,MAP_SHARED是进程间共享内存所需要的flag,对所有共享对象可见。而MAP_PRIVATE只对调用进程可见。

针对munmap函数:从某个进程的地址空间删除一个映射关系。

#include<sys/mman.h>
int munmap(void *addr, size_t len);
			//返回:若成功则为0,若出错则为-1

其中addr参数是由mmap返回的地址,len是映射区的大小。当使用munmap函数撤消映射关系后,再次访问这些地址将导致向调用进程产生一个SIGSEGV信号。

如果被映射区是使用MAP_PRIVATE标志映射的,那么调用进程对它所作的变动都会被丢弃掉。
 

针对msync函数:通过执行此函数更新硬盘文件,将硬盘上的文件内容与内存映射区中的内容同步。

#include<sys/mman.h>
int msync(void* addr, size_t len, int flags);
			//返回:若成功则为0,若出错则为-1

其中addr和len参数通常指代内存中的整个内存映射区,但也可以指定该内存区的一个子集。flags参数选择参见下表:

msync函数的flags参数
常值

说明

MS_ASYNC 执行异步写
MS_SYNC 执行同步写
MS_INVALIDATE 使高速缓存的数据失效

MS_ASYNC和MS_SYNC两个常值中必须仅指定一个。如果还指定了MS_INVALIDATE,那么与其最终副本不一致的文件数据的所有内存中副本都失效。后续的引用将从文件中取得数据。

使用mmap的特点:

提高IPC通道通信速度!简化代码!

不是所有描述符都可进行内存映射(终端或套接字的描述符不可以用来内存映射);

mmap被用来在无亲缘关系的进程间提供共享的内存区,此情形下,所映射文件的实际内容成了被共享内存区的初始内容,而且这些进程对该共享内存区所作的任何变都复制回所映射的文件(以提代随文件系统的持续性)。

3、在内存映射文件中给计数器持续加1(程序及其注释哈)

利用共享内存在父子进程间中给计数器持续加1,需要值得注意是信号量也可以看是处于共享内享区,即不会被子进程拷贝

#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 <pathname> <#loops>");
	nloop = atoi(argv[2]);

		/* 4open file, initialize to 0, map into memory */
	fd = Open(argv[1], O_RDWR | O_CREAT, FILE_MODE);//打开文件,利用open的是内存映射文件,而利用shm_open是共享内存区对象
	Write(fd, &zero, sizeof(int));//写入一个0字节
	ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);//提供一个父子进程共享的内存区
	Close(fd);

		/* 4create, initialize, and unlink semaphore */
	mutex = Sem_open(SEM_NAME, O_CREAT | O_EXCL, FILE_MODE, 1);//创建一个二值信号量
	Sem_unlink(Px_ipc_name(SEM_NAME));//从系统中删除信号量(注意存在引用计数限制)

	setbuf(stdout, NULL);	/* stdout is unbuffered *///输出设置为非缓冲模式,否则看不到父子进程交替输出
	if (Fork() == 0) {		/* child */
		for (i = 0; i < nloop; i++) {
			Sem_wait(mutex);
			printf("child: %d\n", (*ptr)++);
			Sem_post(mutex);
		}
		exit(0);
	}

		/* 4parent */
	for (i = 0; i < nloop; i++) {
		Sem_wait(mutex);
		printf("parent: %d\n", (*ptr)++);
		Sem_post(mutex);
	}
	exit(0);
}

4、4.4BSD匿名内存映射及SVR4 /dev/zero内存映射

i、4.4BSD提供匿名内存映射,其彻底避免了文件的创建和打开。其办法是把mmap的flags参数指定成MAP_SHARED | MAP_ANON,把fd参数指定为-1。offset参数则被忽略。这样的内存区初始化为0.

相应的程序见下:

#include	"unpipc.h"

#define	SEM_NAME	"mysem"

/* include diff */
int
main(int argc, char **argv)
{
	int		i, nloop;
	int		*ptr;
	sem_t	*mutex;

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

		/* 4map into memory */
	ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
			   MAP_SHARED | MAP_ANON, -1, 0);
/* end diff */

		/* 4create, initialize, and unlink semaphore */
	mutex = Sem_open(Px_ipc_name(SEM_NAME), O_CREAT | O_EXCL, FILE_MODE, 1);
	Sem_unlink(Px_ipc_name(SEM_NAME));

	setbuf(stdout, NULL);	/* stdout is unbuffered */
	if (Fork() == 0) {		/* child */
		for (i = 0; i < nloop; i++) {
			Sem_wait(mutex);
			printf("child: %d\n", (*ptr)++);
			Sem_post(mutex);
		}
		exit(0);
	}

		/* 4parent */
	for (i = 0; i < nloop; i++) {
		Sem_wait(mutex);
		printf("parent: %d\n", (*ptr)++);
		Sem_post(mutex);
	}
	exit(0);
}

ii、SVR4提供/dev/zero设备文件,open它之后可在mmap调用中使用得到的描述符。从该设备读时返回的字节全为0,写往该设备的任何字节则被丢弃。

相应程序见下:

#include	"unpipc.h"

#define	SEM_NAME	"mysem"

/* include diff */
int
main(int argc, char **argv)
{
	int		fd, i, nloop;
	int		*ptr;
	sem_t	*mutex;

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

		/* 4open /dev/zero, map into memory */
	fd = Open("/dev/zero", O_RDWR);//往fd写字节被丢弃,从fd读出的字节则全为0,保证了内存映射区初始化为0
	ptr = Mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	Close(fd);
/* end diff */

		/* 4create, initialize, and unlink semaphore */
	mutex = Sem_open(Px_ipc_name(SEM_NAME), O_CREAT | O_EXCL, FILE_MODE, 1);
	Sem_unlink(Px_ipc_name(SEM_NAME));

	setbuf(stdout, NULL);	/* stdout is unbuffered */
	if (Fork() == 0) {		/* child */
		for (i = 0; i < nloop; i++) {
			Sem_wait(mutex);
			printf("child: %d\n", (*ptr)++);
			Sem_post(mutex);
		}
		exit(0);
	}

		/* 4parent */
	for (i = 0; i < nloop; i++) {
		Sem_wait(mutex);
		printf("parent: %d\n", (*ptr)++);
		Sem_post(mutex);
	}
	exit(0);
}

5、访问内存映射的对象

值得注意的是,文件大小和内存映射区大小可以不同

访问其大小可能不同于文件大小的内存映射区的程序:

#include	"unpipc.h"

int
main(int argc, char **argv)
{
	int		fd, i;
	char	*ptr;
	size_t	filesize, mmapsize, pagesize;

	if (argc != 4)
		err_quit("usage: test1 <pathname> <filesize> <mmapsize>");
	filesize = atoi(argv[2]);
	mmapsize = atoi(argv[3]);

		/* 4open file: create or truncate; set file size */
	fd = Open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE);//O_TRUNC是将打开文件截短为0
	Lseek(fd, filesize-1, SEEK_SET);//将文件大小设置为filesize-1,并写入一个字节,此时文件大小为filesize
	Write(fd, "", 1);               //可以用后续的ftruncate函数来更改文件大小

	ptr = Mmap(NULL, mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	Close(fd);

	pagesize = Sysconf(_SC_PAGESIZE);//获取系统页面实现大小
	printf("PAGESIZE = %ld\n",(long) pagesize);
	
	for(i=0;i<max(filesize,mmapsize);i+=pagesize) {
		printf("ptr[%d] = %d\n", i, ptr[i]);
		ptr[i] = 1;
		printf("ptr[%d] = %d\n", i + pagesize - 1, ptr[i + pagesize - 1]);
		ptr[i + pagesize - 1] = 1;
	}
	printf("ptr[%d] = %d\n", i, ptr[i]);
	
	exit(0);
}

当共享内存长度大于文件大小时,访问内存映射区大于文件大小的部分会出现两种错误信号:

i、SIGSEGV信号:意味着已在内存映射以远访问。

ii、SIGBUS信号:意味着是在内存映射区内访问,但是已超出了底层支撑对象的大小。

以上知识点来均来自steven先生所著UNP卷二(version2),刚开始学习网络编程,如有不正确之处请大家多多指正。

猜你喜欢

转载自blog.csdn.net/TT_love9527/article/details/81330119