关于内存泄漏问题(共享内存、请求调页、缺页异常处理)

1. 问题描述

    最近发现了一个问题,当不同的进程申请了同一块共享内存时,会有内存泄漏现象,即当程序运行时,有些进程的内存会逐渐      增加,波动幅度较为明显。

2.问题排查

    通过调试排查,我们发现是共享内存引起的。我们用两个进程分别初始化同一个共享内存(5M),然后什么都不做,测试结果是第一个进程占用的内存是5M左右,第二个进程占用的内存是200K左右,结果如下图所示。

 

    通过查看代码发现,第一个初始化共享内存的进程会进行清零,也就是对所申请的内存全部进行一次写操作,而这导致上面结果的原因,如果我们把清零操作屏蔽了,会发现两个进程占用内存都是在200K左右。

void *CreateShareBuf(char *pName, unsigned int size)
{
	struct shmid_ds buf;
	char filename[64];
	void *pMemory = NULL;
	int fd, shmid;

	if(pName == NULL)
	{
		return;
	}
	
	sprintf(filename, "/tmp/.%s", pName);
	fd = open(filename, O_RDWR|O_CREAT|O_EXCL);
	if(fd > 0)
	{	
		close(fd);
	}

	shmid = shmget(ftok(filename, 'g'), size, IPC_CREAT);
	pMemory = shmat(shmid, NULL, 0);
	shmctl(shmid, IPC_STAT, &buf);

	if(buf.shm_nattch == 1)
	{
		memset(pMemory, 0x0, size);
	}

	
	return pMemory;
}

     然后我们又进行了一次实验,两个进程在初始化共享内存后对循环共享内存进行读操作,每次循环为100毫秒,测试结果如下图所示,可以明显的看到内存在逐渐的增加。

3.问题分析

为什么会出现这样的现象呢?这就要从几个方面说起了。

  共享内存

    所谓共享内存就是使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。这种IPC机制允许两个或多个进程通过把公共数据结构放入一个共享内存区来访问它们。如果进程要访问这种存放在共享内存的数据结构,就必须在自己的地址空间中增加一个新内存区,它将映射与这个共享内存相关的页框。这样的页框可以很容易地由内核通过请求调页进行处理。

    

  请求调页

    出于效率的原因,内存映射创建之后并没有离开把页框分配给它,而是尽可能相后推迟到不能推迟,也就是说,当进程试图对其中的一页进行寻址时,就产生一个缺页异常。

    请求调页技术背后的动机是:进程开始运行的时候并不访问其他地址空间中的全部地址;事实上,有一部分地址也许永远不被进程使用。此外,程序的局部性原理保证了再程序执行的每个阶段,真正引用的进程页只有一小部分,因此临时用不着的页所在的页框可以由其他进程来使用。因此,对于一开始就给进程分配所需要的全部页框,直到程序结束才释放这些页框来说,请求调页是首选的,因为它增加了系统中的空闲页框的平均数,从而更好地利用空闲内存。从令一个观点来看,在RAM总数保存不变的情况下,请求调页从总体上能使系统有更大的吞吐量。

    为这一切优点付出的代价是系统额外的开销:由请求调页所引发的每个缺页异常必须由内核处理,这就浪费CPU的时钟周期。

缺页异常处理

    Linux的缺页异常处理程序必须区分以下两种情况:有编程错误所引发的异常,以及由引用属于进程地址空间但还尚未分配物理页框的页所引起的异常。

    线性区描述符可以让缺页异常处理程序非常有效地完成它的工作。Do_page_fault()函数是80x86上的缺页中断服务程序,它把引起缺页的线性地址和当前进程的线性区相比较,从而能够根据和下图所示的方案选择适当的方法处理这个异常。

    当进程试图访问IPC共享内存区的一个单元,而其基本的页框还没分配时则发送缺页异常。相应的异常处理程序确定引起缺页的地址是在进程的地址空间内,且相应的页表项为空;因此,它就调用do_no_page()函数。这个函数又检测是否为这个内存区定义了nopage方法。然后调用这个方法,并把页表项设置成返回的地址。IPC共享内存所使用的内存区通常都定义了nopage方法,这是通过shmem_nopage()函数实现的。do_no_page()函数对引起缺页的地址在进程的页表中所对应的表项进行设置,以使该函数指向nopage方法所返回的页框。

4.解决方案

    综上所述,当我们初始化了共享内存后,如果我们没对该内存进行全部的读操作或者写操作,系统不会一开始就给进程分配所需要的全部页框,而会根据请求调页的机制来处理。如果想要达到我们的效果,不让内存波动幅度那么明显的话,最好在申请完内存后进行一次全部读或者写操作,代码如下。

void *CreateShareBuf(char *pName, unsigned int size)
{
	struct shmid_ds buf;
	char filename[64];
	void *pMemory = NULL;
	int fd, shmid;

	if(pName == NULL)
	{
		return;
	}
	
	sprintf(filename, "/tmp/.%s", pName);
	fd = open(filename, O_RDWR|O_CREAT|O_EXCL);
	if(fd > 0)
	{	
		close(fd);
	}

	shmid = shmget(ftok(filename, 'g'), size, IPC_CREAT);
	pMemory = shmat(shmid, NULL, 0);
	shmctl(shmid, IPC_STAT, &buf);
	
	if(buf.shm_nattch == 1)
	{
		memset(pMemory, 0x0, size);
	}
	else
	{
		#define MAX_READ_SIZE  (128)
		char readBuf[MAX_READ_SIZE] = {0};
		int readSize = MAX_READ_SIZE;
		int s = 0;
		for(s = 0; s < size; s += readSize)
		{
			if(s + MAX_READ_SIZE > size)
			{
				readSize = size - s;
			}
			else
			{
				readSize = MAX_READ_SIZE;
			}
			memcpy(readBuf, pMemory + s, readSize);
		}
	}
	
	return pMemory;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值