IOCP接收缓存导致的内存错乱
在用IOCP控件写了一个ERP服务器后,服务器会发生运行3天后,出现莫名的内存错误,用FastMM检测,是本没有内存错误的地方,而且内存错误出现的地方也不固定。这是一个不可重现的Bug,后续通过打日志把错误范围缩小后发现,每次出现内存错误之前都是由于有链接断开释放,因此就加了日志逐步定位到是TSocketHandle释放引起的,具体原因是:在IOCP中,每个Socket连接需要投递一个接收请求,并给出数据存放内存,原来是销毁TSocketHandle的同时,销毁投递接收请求的缓存,这样有可能对象销毁后,IOCP返回一个异步接收消息,会导致写入到已销毁的接收缓存,造成内存被重写,导致内存错误。
解决办法,是用锁和对象分离相同的机制,把接收缓存和对象分离,在释放对象的时候不释放接收缓存,等待超过30分钟后,重新使用这个锁和接受缓存,这样做即可以解决内存错乱问题,也起到了锁和接收缓存的池化处理。
具体代码处理:
投递请求缓存和对象分开,采用是锁和对象分离相同的机制。
{* 客户端对象和锁 *} TClientSocket = record Lock: TCriticalSection; SocketHandle: TSocketHandle; IocpRecv: TIocpRecord; //投递请求结构体 IdleDT: TDateTime; end; PClientSocket = ^TClientSocket;在释放TSocketHandle的时候,只释放对象,投递请求缓存不释放,和锁一起保留,加入到空闲列表中。
procedure TSocketHandles.Delete(const AIndex: Integer); var ClientSocket: PClientSocket; begin ClientSocket := FList[AIndex]; ClientSocket.Lock.Enter; try ClientSocket.SocketHandle.Free; ClientSocket.SocketHandle := nil; finally ClientSocket.Lock.Leave; end; FList.Delete(AIndex); ClientSocket.IdleDT := Now; FIdleList.Add(ClientSocket); end;在加入对象的时候,检测空闲列表是否有超过30分钟没使用的,如果有则重复利用。
function TSocketHandles.Add(ASocketHandle: TSocketHandle): Integer; var ClientSocket, IdleClientSocket: PClientSocket; i: Integer; begin ClientSocket := nil; for i := FIdleList.Count - 1 downto 0 do begin IdleClientSocket := FIdleList.Items[i]; if Abs(MinutesBetween(Now, IdleClientSocket.IdleDT)) > 30 then begin ClientSocket := IdleClientSocket; FIdleList.Delete(i); Break; end; end; if not Assigned(ClientSocket) then begin New(ClientSocket); ClientSocket.Lock := TCriticalSection.Create; ClientSocket.IocpRecv.WsaBuf.buf := GetMemory(MAX_IOCPBUFSIZE); ClientSocket.IocpRecv.WsaBuf.len := MAX_IOCPBUFSIZE; end; ClientSocket.SocketHandle := ASocketHandle; ClientSocket.IdleDT := Now; ASocketHandle.FLock := ClientSocket.Lock; ASocketHandle.FIocpRecv := @ClientSocket.IocpRecv; Result := FList.Add(ClientSocket); end;
CheckDisconnectedClient方法加锁及判断是否正在执行
原来检测释放断开连接的方法如下:
procedure TIocpServer.CheckDisconnectedClient; var i: Integer; begin FSocketHandles.Lock; try for i := FSocketHandles.Count - 1 downto 0 do begin if not FSocketHandles.Items[i].SocketHandle.Connected then begin FSocketHandles.Delete(i); end; end; finally FSocketHandles.UnLock; end; end;这个方法存在以下问题:1、对整个FSocketHandles加锁,FSocketHandles.Delete在释放的时候又加了一次锁,如果Delete加锁等待,则导致整个FSocketHandles被锁住,这时再加连接就会等待,造成IOCP无法接收连接,从而存在问题。
2、如果某个TScoketHandle执行很长时间,它的Connected属性为False,则FSocketHandles.Delete会锁住,造成和1相同的问题。
解决办法:
1、不对整个FSocketHandles加锁,我每次查找一个Connected为False的连接,避免一次加两个锁。
2、TSocketHandle增加一个属性标识是否正在执行中,在检测断开连接的时候如果正在执行中则跳过。
具体代码如下:
procedure TIocpServer.CheckDisconnectedClient; var iCount: Integer; ClientSocket: PClientSocket; function GetDisconnectSocket: PClientSocket; var i: Integer; begin Result := nil; FSocketHandles.Lock; try for i := FSocketHandles.Count - 1 downto 0 do begin if (not FSocketHandles.Items[i].SocketHandle.Connected) and (not FSocketHandles.Items[i].SocketHandle.Executing) then begin Result := FSocketHandles.Items[i]; Break; end; end; finally FSocketHandles.UnLock; end; end; begin ClientSocket := GetDisconnectSocket; iCount := 0; while (ClientSocket <> nil) and (iCount < 1024 * 1024) do begin ClientSocket.Lock.Enter; try if Assigned(ClientSocket.SocketHandle) then FreeSocketHandle(ClientSocket.SocketHandle); finally ClientSocket.Lock.Leave; end; ClientSocket := GetDisconnectSocket; Inc(iCount); end; end;主要是使用GetDisconnectSocket来返回一个已经断开的连接。TSocketHandle.Executing的赋值只需要在下面方法中赋值即可,因为他执行的进入口和返回口。procedure TSocketHandle.ProcessIOComplete(AIocpRecord: PIocpRecord; const ACount: Cardinal); begin FExecuting := True; try case AIocpRecord.IocpOperate of ioNone: Exit; ioRead: //收到数据 begin FActiveTime := Now; ReceiveData(AIocpRecord.WsaBuf.buf, ACount); if FConnected then PreRecv; //投递请求 end; ioWrite: //发送数据完成,需要释放AIocpRecord的指针 begin FActiveTime := Now; FSendOverlapped.Release(AIocpRecord); end; ioStream: begin FActiveTime := Now; FSendOverlapped.Release(AIocpRecord); WriteStream; //继续发送流 end; end; finally FExecuting := False; end; end;
解决这两个稳定性问题后,IOCP支持的ERP服务器已经能支持7*24小时运行。一般当服务器出现稳定性问题后,日志就开始发挥作用,但是太多无用的日志不利于定位到问题点,太少的日志又无法定位,一般写日志的原则是在调用顺序关键点上加日志、继承关键点上加日志、数据流输入输出关键点上加日志;这样出现问题后,可以快速把问题定位到一段代码上,能帮助缩短解决问题的周期。如果出现一个不可重现BUG,能控制在3天解决,出现一个内存错乱BUG,能控制在半个月解决,哪你的日志辅助调试就是有效的。
V1版下载地址:http://download.csdn.net/detail/sqldebug_fan/4510076,需要资源10分,有稳定性问题,可以作为研究稳定性用;
V2版下载地址:http://download.csdn.net/detail/sqldebug_fan/5560185,不需要资源分,解决了稳定性问题和提高性能;免责声明:此代码只是为了演示IOCP编程,仅用于学习和研究,切勿用于商业用途。水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]