<aside> 🧱
3. 두 모델 차이가 터진 곳이 네트워크 이야기였다면, 여기는 그 밖의 전부다.
서버는 소켓만 Windows에 묶여 있는 게 아니었다. 타이머·경로·코어 배치·종료 시그널·크래시 덤프·설정 파일·로그 시각까지, 눈에 안 띄는 곳이 더 많았다. 이 문서는 그것들을 어떻게 한곳으로 몰았는지, 그리고 몰면 안 되는 것이 하나 있었다는 이야기다.
</aside>
<aside> 📌
범위 — IOCP_Server/IOCP_Server/Platform/Platform.h(신설)와 전송 계층 경계. 락프리 자료구조 쪽 어댑터는 2. 락프리를 리눅스로에 따로 있다.
</aside>
소켓을 빼고도 이만큼이었다.
| 갈래 | Windows | 리눅스 |
|---|---|---|
| 고해상도 타이머 | timeBeginPeriod |
대응물 없음 — std::sleep_for가 이미 충분한 해상도 |
| 실행 파일 경로 | GetModuleFileNameA |
/proc/self/exe |
| 코어 배치 | SetThreadAffinityMask · GetProcessAffinityMask |
pthread_setaffinity_np · sched_getaffinity |
| 종료 시그널 | SetConsoleCtrlHandler |
sigaction(SIGINT·SIGTERM) |
| 스레드 CPU 시간 | GetThreadTimes (커널·유저 분리) |
clock_gettime(per-thread) — 커널분을 따로 못 준다 |
| 32비트 원자연산 | InterlockedIncrement 계열 |
__atomic_* |
| 설정 파일 | GetPrivateProfileString 계열 |
대응물 없음 — 직접 파서를 만들었다 |
| 로그 시각 | localtime_s |
localtime_r |
| 크래시 덤프 | 미니덤프(MiniDumpWriteDump) |
sigaction · backtrace — 4장에서 따로 |
이 아홉 갈래를 헤더 하나(Platform.h)에 몰았다. 호출부는 Platform::SetCurrentThreadAffinity(...)처럼 부르고, 어느 OS인지 모른 채로 돈다.
// 호출부는 이렇게만 보인다
Platform::SetHighResolutionTimer(true);
Platform::InstallShutdownHandler(cb, &completeFlag, 4500);
const int cores = Platform::GetAvailableCoreCount();
<aside> 🔑
가두는 목적은 "#ifdef를 없애는 것"이 아니다. #ifdef를 한 파일에만 두는 것이다.
#ifdef가 코드 곳곳에 흩어져 있으면, 새 OS를 붙일 때 무엇을 빠뜨렸는지 알 방법이 없다. 한 파일에 모여 있으면 그 파일만 훑으면 된다 — 실제로 리눅스 팔을 채울 때 이 헤더가 할 일 목록 역할을 했다.
</aside>
<aside> ⚠️
① 커널 CPU 시간이 사라진다.
Windows GetThreadTimes는 커널 시간과 유저 시간을 나눠 준다. 우리는 그걸로 mmo_thread_kernel_ratio(스레드가 커널에 머문 비율)를 재고 있었다.
리눅스의 per-thread 시계는 합계만 준다. 그래서 리눅스에서는 이 지표가 0으로 고정된다. 값을 지어내는 대신 "못 재는 것"으로 남겼다 — 나중에 이 지표를 볼 때 리눅스 숫자를 IOCP와 나란히 놓으면 안 된다.
</aside>
<aside> ⚠️
② 창을 닫을 때의 정리 시간.
Windows에서 콘솔 창을 닫거나 로그오프하면, SetConsoleCtrlHandler의 핸들러가 반환하는 순간 OS가 프로세스를 죽인다. DB 최종 저장이 잘릴 수 있다.
그래서 InstallShutdownHandler에 완료 플래그와 최대 대기 시간을 받는 인자를 더했다 — 핸들러가 "정리 끝났다"는 신호를 기다렸다가 반환한다. 리눅스 sigaction 경로에는 이런 제약이 없어 인자를 받아도 그냥 통과시킨다.
</aside>
소켓 헤더까지 Platform.h에 넣으려다 빌드가 통째로 깨졌다.
Windows 소켓 헤더에는 순서 규칙이 있다. <WinSock2.h>가 <Windows.h>보다 먼저 와야 한다. 안 그러면 <Windows.h>가 끌고 오는 옛 winsock.h와 충돌해 sockaddr 재정의 오류가 난다.
Platform.h에 <WinSock2.h>를 넣자, 이 헤더를 포함하지 않는 다른 번역 단위들이 여전히 <Windows.h>를 먼저 열면서 그 충돌이 났다. WIN32_LEAN_AND_MEAN으로도 못 막았다 — 그 매크로가 없는 파일들이 문제였기 때문이다.
<aside> 🕳️
결국 소켓 헤더 책임은 IOCPServer.h로 되돌렸다. Platform은 소켓 핸들 타입(Platform::NetSocket)과 동작(CloseSocket·WouldBlock)만 가리고, 헤더 포함 순서는 원래 있던 자리가 지킨다.
교훈은 이렇다 — 격리 계층은 "무엇을 가릴지"만이 아니라 "무엇을 가리면 안 되는지"도 정해야 한다. 헤더 포함 순서처럼 번역 단위 전체에 걸리는 제약은 한곳에 몰 수 있는 종류가 아니다.
</aside>