본문 바로가기

개발/개발 일기

2022.02.24 개발 일기 - WSASend, WSARecv 함수 호출 시 메모리 주소 정렬

작년 8월에 마지막으로 개발 일기를 작성한 이후로 벌써 6개월 정도가 지났다. 8월 중순부터 외주 개발과 팀으로 하는 프로젝트의 시작으로 바빠져서, 오프라인 렌더러의 개발은 진행하고 있지 않았다. 

 

물론, 꾸준히 논문이나 기타 자료를 통해서 오프라인 렌더러의 수학적인 기초는 계속해서 다지고는 있었지만, 실질적인 개발은 멈춘 상태이다. 다만, 수학적인 기초를 공부하면서, 블로그 포스팅용으로 자료들을 정리하고 있었고, 오프라인 렌더러를 개발하면서 사용했던 수학 라이브러리를 SIMD 버전으로 개발하고, 개인 프로젝트에서 사용할 Generic 라이브러리를 틈틈히 개발해왔다. 블로그 포스팅을 위한 자료들은 OneNote로 저장해서 블로그에 올리기 위해서는 한번 더 정리를 해야하는데, 다른 일들 때문에 계속해서 미루고 있었다.

 

최근에는 팀으로 진행하는 프로젝트에서 서버 개발을 진행해야할 수도 있어서, 서버 개발을 공부하면서 개인 라이브러리 개발 작업을 하고 있었다.

 

서버 개발은 기본적으로 Windows 환경에서 IOCP를 이용해서 개발을 하고 있었고, 클라이언트는 Unreal 엔진을 사용하기 때문에, 이 둘은 연동시켜서 사용하기 위한 몇가지 테스트를 진행하고 있었다. 물론 실제 프로젝트 진행을 할 때는 내가 개발한 Generic 라이브러리는 최대한 배제하고, EASTL을 사용할 예정이지만, 테스트 시에는 제작한 라이브러리의 검증을 겸하기 위해서 자체 제작 라이브러리를 최대한 활용하고 있었다.

 

테스트하고 있었던 기능은 언리얼 상(클라이언트)에서 네트워크 통신을 위한 네트워크 쓰레드를 생성하고, 네트워크 쓰레드에 서버 소켓이 사용할 메시지 큐를 생성한 후, 클라이언트에서 Send하는 메시지와 Recv하는 메시지를 분석해서, 메시지 전송시 등록한 함수(델리게이터, 함수 포인터)를 관련 메시지가 Recv 됐을 때 호출하는 기능이었다.

 

클라이언트에서는 큰 문제가 없었지만, 문제는 서버쪽에서 발생했다. 클라이언트로부터 GUID 요청을 받은 서버가 다시금 클라이언트한테 GUID를 전송해주는 코드였는데, 맨 처음 호출은 성공하고, 그 다음 호출은 실패하는 문제였다. 더 자세하게 말하면 홀수번째 호출은 성공하고, 짝수번째 호출은 실패하는 것이었다.

 

클라이언트 측에서의 Send는 문제없이 진행이 되는데, 서버쪽의 로그를 보니 서버가 짝수번째 요청에 대한 응답을 해주지 않고 있었다.

 

문제의 원인은 클라이언트가 보낸 메시지를 받은 서버가 메시지를 처리하고, 다시 WSASend() 함수를 통해서 패킷과 함께 GUID 데이터를 보내는데, WSASend() 함수가 제대로 동작하지 않고 있었던 것이다.

 

처음에는 WSASend() 함수는 제대로 호출되었는데, IOCP가 팬딩하고 있는 쓰레드 풀에서 이벤트를 제대로 처리를 해주지 못한다고 생각하고, 이 부분에 대해서 집중적으로 디버깅을 진행했다. 하지만 디버깅을 진행하면 진행할수록 문제는 발견하지 못했다. 

 

그렇게 계속해서 코드를 보다가 까먹고 할당을 해제하지 않은 코드 부분을 발견하고, 할당 해제 코드를 추가했다. 물론 이 부분은 내가 생각하기에, 버그와 관련이 없이 단순한 메모리 누수에 관한 부분이라고 생각했기 때문에 별 생각없이 코드를 추가하고 실행했는데, 갑자기 서버가 정상적으로 모든 호출에 대해서 응답을 해주기 시작했다.

 

이때부터 정신이 혼미해지기 시작했다. 분명히 해결은 됐는데, 왜 해결이 됐는지도 모르겠고, 할당 해제를 안해주는 것은 단순히 메모리 누수의 문제이지, WSASend() 함수와 큰 관계가 없다고 생각을 했기 때문이다. 물론 할당 해제를 해주는 리소스는 WSASend() 시 사용되는 WSABUF, WSAOVERLAPPED 구조체와 전송할 패킷을 저장하고 있는 구조체(struct IocpPacket)였기 때문에, 아무런 관계가 없다고 말하기는 애매하지만 단순히 할당 해제를 해주었다고 해서, 갑자기 WSASend() 함수가 정상적으로 작동하는 것이 이상했기 때문이다. 

 

최종적으로 문제의 원인은 메모리 할당에 있었다는 것을 알았다. 테스트 서버에서는 서버와 접속된 클라이언트를 Client 클래스를 통해서 관리하고 있었는데, 이 클래스 안에는 WSARecv()에서 사용하기 위한 IocpPacket 구조체가 멤버 변수로 있었다(IocpPacket 구조체는 WSABUF, WSAOVERLAPPED와 전송할 데이터 버퍼를 가지고 있다).

 

하지만 Client 클래스에 있는 IocpPacket은 Recv를 위한 구조체이고, Send를 위한 IocpPacket은 Send를 하기 전에 새롭게 할당하고, Packet을 전송 후에 할당을 해제하는 방식(위에서 새롭게 추가했다는 할당 해제 코드가 IocpPacket을 할당해제 하는 코드임)으로 코딩이 되어있었다.

 

위와 같은 방식을 사용한 이유는 Recv와 Send가 각각 다른 스레드에서 진행될 때 하나의 버퍼에 Recv와 Send가 동시에 접근하게 되면 데이터 오염이 일어날 것 같다고 판단했기 때문이다. 물론 서버를 제대로 해본 적이 없기 때문에, 이러한 현상이 일어나는지에 대해서는 테스트를 해본적이 없다. 

 

아무튼 WSASend() 함수 호출 전에 사용할 IocpPacket을 할당하고, 메시지 전송 이후에 사용했던 IocpPacket을 할당 해제하는 것을 new/delete 또는 malloc/free를 통해서 하게 되면, 성능 하락이 생길 수도 있고, 이전에 작성했던 Generic 라이브러리에 MemoryPool이 있었기 때문에, 아무 생각없이 이 MemoryPool을 사용해서 메모리 할당 및 해제를 진행했는데, 이 MemoryPool을 이용한 할당에서 문제가 생겼다.

 

자세하게 말하면 MemoryPool을 이용한 할당에는 문제가 없었지만, 반환된 Memory의 주소가 문제였다. IocpPakcet 구조체는 WSAOVERLAPPED, WSABUF와 데이터 버퍼를 합쳐서 총 262바이트의 사이즈를 가진다(물론 데이터를 전송할 때는 실제 패킷의 크기만큼만 전송한다). 또한 메모리 풀에서는 하나의 큰 배열에 데이터들이 연속적으로 배치되어있다. 예를 들면 첫번째 IocpPacket은 0 ~ 261의 범위를 차지하고 있고, 두번째 IocpPacket은 262 ~ 523의 범위를 차지하고 있는 셈이다. 

 

이때 첫번째 IocpPacket은 MemoryPool의 0번째 주소부터 할당이 되기 때문에, WSABUF가 메모리 정렬 조건을 만족하지만, 두번째 IocpPacket은  262번째 주소부터 시작하기 때문에 메모리 정렬 조건을 만족하지 않게 된다. 이러한 이유로 홀수번째 IocpPacket은 WSABUF가 메모리 정렬 조건을 만족하기 때문에, 문제없이 실행됐고 짝수번째 IocpPacket은 메모리 정렬 조건을 만족하지 않기 때문에, WSASend() 함수 호출에 실패를 했던 것이다. 

 

또한, 할당 해제 코드를 추가할 시에 문제없이 작동했던 이유는 할당이 해제될 때마다 MemoryPool의 0번째 주소부터 다시 할당하기 때문에 메모리 정렬 조건을 만족해서, 문제없이 작동했었던 것이다.

 

아마 같은 이유에서 WSARecv도 동일한 문제를 발생시키겠지만, Client 구조체에 저장된 IocpPacket이 자동적으로 메모리 정렬이 되기 때문에, 그런 문제를 발생시키지 않을 것이다. 이 문제는 직접 작성한 MemoryPool을 사용했기 때문에 생긴 문제였다. 

 

현재는 임시방편으로, MemoryPool을 초기화할때 264바이트씩 할당을 하도록 지정을 하고 있지만, 이 부분을 내부적으로 구현할지 아니면, 명시적으로 작성하게 해야할지 선택해야할 것 같다.