案例 1
在对接入网A产品的网管软件测试中,发现了一个WINDSOWS资源损耗的的问题:当网管软件运行几天后,WINDOWS总会出现“资源不够”的告警提示。如果网管软件不关掉再重新启动的话,就会出现WINDOWS资源完全耗尽的现象,最终网管系统反应很慢,无法正常工作。
从现象上可以判断出,网管软件存在隐蔽的内存泄露或资源不释放的问题,并且这种资源耗尽是一个缓慢的过程。如何定位这个问题呢?
定位这种问题可以利用WINDOWS中的一个系统资源监视工具。打开Windows的“附件/系统工具/资源状况”,这是一个系统资源、用户资源、和GDI资源的实时监视工具。
工具有了,那么如何发现导致不断消耗资源的特定操作呢?
首先和开发人员共同探讨,列出几个最可能消耗资源的操作和一些操作组合,这样就缩小了监视范围,避免没有范围的碰运气,否则如大海捞针。
监视前,首先重新启动WINDOWS,最好不运行其他的程序,打开“系统状况”这个监视工具,然后运行网管软件,记下此时的资源状况数据。
然后针对一个可疑的操作,快速大量地重复进行。这种重复性的操作可以利用QArun测试工具执行,QArun可以记录操作者的一次操作步骤,然后按照设定的次数重复操作。操作后,观察此时的资源状况,并记下此时的数据,与操作前的数据比较,如果操作前后的数据数据没有变化或变化很小,可排除此项操作,否则就可断定此项操作会引起资源耗尽。
对其它可疑的操作和操作组合重复以上过程。
通过以上的步骤,终于找出引起资源耗尽的罪魁祸首。分析相应部分的代码,发现引起资源耗尽原因有:内存泄露,画笔和画刷资源用完后未释放等。
-
-
xilinxue 发表于 2008/11/6 12:58:01
某产品后台软件版本,是用C++写的,程序员在写代码时,经常在构造函数中申请一块内存,而不释放,在程序其他代码中也经常只管申请,不管释放。
例如:
void WarnSvr::SaveWarnData()
{
......for(int m="0";m<RecordsInBuffer[EVENT_ALARM];m++)
{
HISTORY_FILTER_INDEX* item=
new HISTORY_FILTER_INDEX;
item->Csn=Buffer[EVENT_ALARM][m].Csn;
item->Position=m
+(RecordsInHistoryFile-RecordsInBuffer[EVENT_ALARM]);//If a warn with a certain Csn is not in EventFilterIndex
//it is not necessary to be added to HistoryFilterIndex
int item_total=EventFilterIndex.GetItemsInContainer();
BOOL find_flag=false;
for(int k="0";k<item_total;k++)
if(EventFilterIndex[k]->Csn==item->Csn)
{
find_flag=true;
break;
}
if(find_flag)
{
HistoryFilterIndex.Add(item);
if(HistoryFilterIndex.IsFull())
ClearIndexEntry();
}
//建议在此处加上:
// else
// delete item;
}。
有的程序员认为,后台运行的环境有大量内存,几个字节的浪费不会造成死机等重大事故。然而,长时间累计起来,必然会造成资源紧张而出现故障。
实际上,这种思想是造成我们产品不稳定的原因之一。我们的主机在网上能运行几个月几年,大家对内存的分配释放较敏感,而我们的后台产品往往只能正常运行几天。这个地方不注意也是原因之一吧。
引用 xilinxue 2008/11/6 12:58:01 发表于2楼的内容
-
-
xilinxue 发表于 2008/11/6 12:58:22
在进行代码审查过程中,造成内存泄漏的代码比较多。下面举几种常见的内存泄漏错误,供测试人员在代码审查中参考:
1. 函数有多个出口时,没有在每个出口处对动态申请的内存进行释放。一般在异常处理时容易出现这种错误。下面的代码段就是这样的例子:
.....
pRecord = new char[pTable->GetRecordLength()];
assert(pRecord != NULL);if (pTable->GoTop(FALSE) != DBIERR_NONE)
return; // 如果从这里返回,pRecord将得不到释放
.....
pTable->Close();
delete[] pRecord;
}
2. 给指针赋值时,没有检查指针是否为空,如果指针不为空,那么指针原来指向的内存将丢失。请看如下代码段:
....
struct FileInfo * pdbffile = new struct FileInfo;
pdbffile->pfileinfo = new struct ffblk;
pdbffile->srcname = srcRootPath;
pdbffile->desname = desRootPath;
pdbffile->prev = NULL;
pfile = pdbffile;
//赋值之前没有检查一下pfile是否为空,如果不为空,会造成pfile指向的内存丢失。
dbf_start_needed = FALSE;
dbf_Finish = FALSE;
flag_begined = TRUE;
if(FALSE == Copy(TRUE))
{
dbf_start_needed = TRUE;
WarnMsgOut("Error occurs while copying files in directory <dbf>,trying again.");
}
}3. 连续二次内存动态分配,在第二次分配失败时,忘记释放第一次已经申请到的内存。
....
pMsgDB_DEV = (PDBDevMsg)GetBuff( sizeof( DBDevMsg ), __LINE__);
if( pMsgDB_DEV == NULL )
return;pMsgDBApp_To_Logic = (LPDBSelfMsg)GetBuff( sizeof(DBSelfMsg), __LINE__ );
if( pMsgDBApp_To_Logic == NULL )
return;//此处返回造成pMsgDB_DEV指向的内存丢失
....
4.代码中缺少应有的条件分支处理,导致程序未执行任何操作而退出时,也可能没有释放应释放的内存,这种情况一般是缺少应有的else分支,或switch语句的default分支没有应有的处理。
static void OncePowerCmdHandle( struct HT_Appmsg * msg )
{
... ...
pPower_test_answer =(struct _oncepower_test_answer *)GetBuff(sizeof(struct _oncepower_test_answer),__LINE__);
if( pPower_test_answer == NULL_PTR )
return;
... ...
if (TSS_State[testpsn].state == TEST_DEV_BUSY ||
TSS_State[testpsn].state == TEST_DEV_ERROR )
{...
}
else if (TSS_State[testpsn].state == TEST_DEV_IDLE )
{...
}
// 缺少 else 分支,可能造成 pPower_test_answer 得不到释放
}造成内存泄漏的情况很多,以上是几种典型的情况。
虽然内存泄露一般出现在异常情况下,毕竟给系统造成很大的隐患,使系统的健壮性降低。测试人员在作代码审查时,对上述几种情况要尤其注意。
引用 xilinxue 2008/11/6 12:58:22 发表于3楼的内容
-
-
xilinxue 发表于 2008/11/6 12:58:42
在进行SAR的PDU包发收的测试过程中要同时考虑几个边界值,即发送包大小范围[0-Nmax],SAR的PDU包接收的最大值Kmax,MBUF块的大小M.在实测中,将SAR的PDU包接收的最大值设为2000(Kmax=2000B), MBUF的块长设为512(M = 512B),则发送包大小的正确分支的取值为下限0,上限Nmax=2000,然后在0与2000之间随机取若干值,再考虑MBUF的块长,还可增加M倍数的若干选值及其附近值.以上是测试的一般思路,但由于很偶然的机会选择包长2000,及Kmax=2000B,才发现问题.原因如下:4楼 回复本楼
MBUF块长512,但块中实际存放数据的只有500(MBUF头上有2个长字,尾部有1个长字共12B只用于块控制),而发送的包长正好是500的整数倍4,由于是整数倍,所以SAR(BT8230)从FREE链上摘成5个MBUF(原因从略),而SAR驱动只知道有4个MBUF,这样到上层用户时,只释放4个MBUF,从而漏掉1个MBUF,经过很短一段时间后,内存即被耗尽.(此问题非常严重,因为在实际运用中,是500的整数倍的PDU包的概率较小,但一旦出现就会发生一次内存泄漏,这样经过若干天或若干月的运行后会使系统崩溃)
以前未发现此问题的原因是因为原来使用的缓冲块长为2048,减去12B的控制信息,实际存放数据的长度为2036.由于只考虑了2048这个值,忽略了2036,所以在选取上下限中的若干值时,选取包的长度是2036的倍数的概率就非常小,因而未发现该问题.
由于测试中一般很难将取值范围中的所有值覆盖全,所以在选取上下限中的若干取值时要格外仔细,考虑的方面尽可能全,因为很有可能其中某些值就是测试边界值.凡是涉及的数字尽量选取,象该例中正确分支的测试边界为0,2000,512及其整数倍,500 及其整数倍,12 及其整数倍等值,它们是必测的边界值,而非可测可不测的随机选取的所谓若干选值.
引用 xilinxue 2008/11/6 12:58:42 发表于4楼的内容
-
-
xilinxue 发表于 2008/11/6 12:59:07
ABIS.CPP中的函数rel_ABIS_CCB_conn( )中,在进行消息链表Msg_Queue[ces]的拆链操作时,对于相应的CCB只进行了一次拆链操作,即只拆除了一个节点,如果出现该CCB对应的消息节点不止一个的情况就会出现大量节点不能释放的问题。5楼 回复本楼
if( Msg_Queue[ces].msghead != NULL_PTR )//message buffer notempty
{
//get first message record
pMsgRecord = Msg_Queue[ces].msghead;
//release buffer-messages concerning with ccb_no
for( index = 0; index < MSGBUFFERNUM; index++ )
{
//这里要对pMsgRecord的值进行判断
if( (pMsgRecord != NULL_PTR) && pMsgRecord->CCB_no ==
ccb_no )
{
//free the message buffer
if( pMsgRecord == Msg_Queue[ces].msghead )//head
Msg_Queue[ces].msghead = pMsgRecord->pnext;
else if( pMsgRecord == Msg_Queue[ces].msgtail )//tail
{
Msg_Queue[ces].msgtail = pPrevMsgRecord;
Msg_Queue[ces].msgtail->pnext = NULL_PTR;
}
else//not head and tail
{
pPrevMsgRecord->pnext = pMsgRecord->pnext;
}
//put buffer back to buffer pool
if( Msg_Buffer.empty_num == 0 )
{
Msg_Buffer.linkhead = Msg_Buffer.linktail = pMsgRecord;
pMsgRecord->pnext = NULL_PTR;//这里将
pMsgRecord->pnext置为空
Msg_Buffer.empty_num++;
}
else
{
Msg_Buffer.linktail->pnext = pMsgRecord;
pMsgRecord->pnext = NULL_PTR;//这里将
pMsgRecord->pnext置为空
Msg_Buffer.linktail = pMsgRecord;
Msg_Buffer.empty_num++;
}
}
else if( pMsgRecord == NULL_PTR )
break;//end of if
//get next message record
pPrevMsgRecord = pMsgRecord;
pMsgRecord = pMsgRecord->pnext;//这时pMsgRecord为
NULL_PTR将跳出for循环语句
}//end of for
}//end of if
这里在拆除一个节点后导致pMsgRecord为NULL_PTR,再进行判断时将会跳出循环,这样将不能保证所有与同一个CCB有关的节点均被拆除,这时如果与同一个CCB对应的消息节点不止一个则这些消息节点均无法释放,造成可用的节点数不断减少,直接影响系统的建链过程,给系统的稳定带来隐患。
后与开发人员联系,根据这段算法编写小程序验证了该问题,并提出了相应的解决方案,消除了该隐患。
引用 xilinxue 2008/11/6 12:59:07 发表于5楼的内容
-
-
xilinxue 发表于 2008/11/6 13:00:21
1、建立一个呼叫,并保持通话。在AM控存监控操作界面中观察通话建立在哪一块FBI板上。
2、将有通话的FBI板拔出,观察通话情况,此时话音中断,但信令仍然保持。观察AM控存监控操作界面和E3M板2K网界面,发现AM侧因为检测到光纤已断,将通话在CTN、E3M板上占用的时隙置为空闲,即在AM控存监控操作界面和E3M板2K网界面观察不到时隙占用情况。
3、分别在30秒、1分钟、3分钟时将拔出的FBI板插回原槽位,发现每次插回FBI板后话音立即恢复。
4、观察BAM上的打印消息,发现打印的各模块占用CTN板大HW上DM时隙的空闲个数比较混乱。打印消息如下图所示:
其中:
1) 由于模块1、2、3、4各占用CTN板上两条大HW,每个DM时隙个数为256(即由两条大HW的两个DM组成,由于与OPT相联的大HW上有两个保留时隙,因此此DM上空闲时隙个数为:254。
2) 由于E3M板只与一条大HW相联,故每个DM上空闲的时隙个数为:128。
本现象对应2个问题:idle_count打印混乱,BM释放故障光路的时隙和对应的CCB、无线信道等资源。
1、idle_count打印混乱是由于函数restore_one_hw中的一些处理不当造成的,以前被当作B型机的历史遗留问题没有重视;2、B2模块有2条光路,如果断掉其中一条,模块状态不会改变,原B型机程序对此不作任何处理,但应该增加这个功能,以免光路故障导致资源吊死。
解决方法:
问题一: 将函数restore_one_hw中原代码作如下改动:
mod_dm[mod][i].tail.tsn = idle_dm_head + 125;
( idle_dm_head == 384 ) ?
mod_dm[mod][i].idle_count += TS_PER_DM - 1:
mod_dm[mod][i].idle_count += TS_PER_DM - 1;改为:
if ( idle_dm_head != 384 )
{
mod_dm[mod][i].tail.tsn = idle_dm_head + 127;
mod_dm[mod][i].idle_count += TS_PER_DM;
}
else
{
mod_dm[mod][i].tail.tsn = idle_dm_head + 126;
mod_dm[mod][i].idle_count += TS_PER_DM - 1;
}
问题二分析如下:
目前的模块状态是由IPATH调用DBMS模块的边检查实现的,只要存在一条可用的光路,即认为相邻模块为正常,对于具体的OPT板上的时隙状态的维护没有与呼叫控制的接口。具体的OPT板状态功能的检测是由IPATH完成的,在BM侧没有专门维护OPT和MC2板的模块,将转交OS组处理。总结:
在拔出FBC板后,通话话音被中断,AM/CM侧已将与被拔出的FBC 板相关的资源全部置为不可用,此时BM侧主机程序也应该与AM/CM侧一致,释放掉所占用的资源,并将原通话的信令连接断开。这可能是由于不同模块的开发人员缺少相互间了解而造成的,即AM/CM侧与BM侧开发人员交流不够。作为测试人员对类似两个或多个模块相关的部分应该充分进行测试,不要想当然,往往是看起来不可能出问题的地方也容易测出问题。
引用 xilinxue 2008/11/6 13:00:21 发表于6楼的内容
-
-
xilinxue 发表于 2008/11/6 13:00:41
在进行有关排队指示的系统测试中,先闭塞掉基站的所有业务信道TCH,进行呼叫,再直接挂机或超时释放,发现TC存在中继资源吊死的问题。
由于此问题重现,后经定位分析,发现是ccb超时后收到AIR发来的clear cmd,进入 rel_one_bm_res( )时,由于ccb所登记的CIC还放在pre_occupied_res,并没有放入occuped_res,而rel_one_bm_res()只对存入occuped_res的CIC进行判断,并向AIE发UNBOOKCIC,而没有对存入pre_occupied_res的CIC进行判断,并UNBOOK掉,导致TC的中继资源吊死。应在超时函数或释放函数中对pre_occupied_res的CIC进行处理。
在此过程中,CIC资源还存放在老CCB的pre_occupied_res中,在超时函数或释放函数中均未对pre_occupied_res中的CIC进行处理(即向AIE UNBOOK),导致TC中继资源吊死。
在超时函数RR_time_out()中timer_name为TN_WAIT_ASS_READY时,和释放函数rel_one_bam_res()中增加对CCB的pre_occupied_res中的CIC的判断和释放处理。
在使用资源同时,就要周密地考虑好资源的释放问题,只有这样,才能使我们的系统不断地稳定下来。
资源的释放对于我们的交换机来说是至关重要的,一点点的疏忽都可能最终使我们的交换机因为无资源使用而死掉,要知道,“千里长堤,毁于蚁穴”。
引用 xilinxue 2008/11/6 13:00:41 发表于7楼的内容