2009년 9월 18일 금요일

SubclassWindow 주의사항

MFC CWnd 클래스의 SubclassWindow()를 사용함에 있어 주의해야 하는 제약 사항이 있다.

이미 CWnd 오브젝트에 맵핑되어 있는 hWnd를 파라미터로 하여 CWnd::SubclassWindow()를 호출하게 되면 충돌이 발생한다. 이런 경우는 서브클래싱을 통해 추가적인 기능을 제공하는 범용 클래스를 소스 레벨에서 통합시킬 때 발생할 수 있다.

정확히 어떤 경우인지 예를 들어 살펴보자.
class CExWnd : public CWnd
{
...
};

class CSomeTool : public CObject
{
CExWnd m_exWnd;
void ExtendWnd(CWnd* pWnd);
};

void CSomeTool::ExtendWnd(CWnd* pWnd)
{
m_exWnd.SubclassWindow(pWnd->GetSafeHwnd());
}

CSomeTool::ExtendWnd(CWnd* pWnd) 함수는 파라미터로 받은 pWnd의 윈도 핸들을 CExWnd로 서브클래싱을 시도하고 있다. 이때 pWnd가 permanent 오브젝트인 경우 CWnd::SubclassWindow() 함수에서 충돌이 발생한다. Permanent 오브젝트의 여부는 CWnd::FromHandlePermanent(HWND hWnd)를 통해 검사할 수 있다.

충돌의 원인은 MFC가 CWnd 오브젝트와 윈도 핸들을 맵핑시키는 메카니즘에서 기인한다.

MFC는 내부에 윈도 맵이라는 해쉬 테이블을 통해 윈도 핸들과 CWnd 오브젝트를 연관시켜 관리한다. 해쉬의 특성상 하나의 윈도 핸들에는 오직 하나의 CWnd 오브젝트만이 맵핑될 수 있다.

그런데 CWnd 오브젝트에 이미 맵핑되어 있는 윈도 핸들을 또 다른 CWnd 오브젝트인 CExWnd에 서브클래싱을 시도하게 되면 하나의 윈도 핸들에 두 개의 CWnd 오브젝트가 존재하는 상황이 발생하여 MFC의 내부 윈도 맵에 충돌이 발생하는 것이다.

단순히 이것만이 문제라면 SubclassWindow()를 호출하기 전에 CWnd::FromHandlePermanent()를 통해 얻은 permanent CWnd 오브젝트의 Detach() 함수를 먼저 호출하여 MFC 윈도 맵의 충돌을 피할 수 있다.

그러나 이것보다 더 심각한 문제가 있다.

MFC는 모든 CWnd 클래스에 대해 AfxWndProc()이라는 윈도 프로시저를 사용하고 있다. 즉, 서브클래싱 하고자 하는 permanent 윈도 오브젝트도 AfxWndProc()을 사용하고 있고 서브클래싱된 CExWnd 클래스도 AfxWndProc()을 사용하고 있다. CWnd::SubclassWindow()는 파라미터로 전달된 윈도의 윈도 프로시저를 상위 윈도 프로시저(oldWndProc)로 저장하고 자신의 윈도 프로시저(AfxWndProc)를 설정한다. AfxWndProc()에서는 CallWindowProc()을 호출하여 서브클래싱 되기 전의 상위 윈도 프로시저(oldWndProc)를 호출한다. 그런데 서브클래싱된 CExWnd는 상위 윈도 프로시저(oldWndProc)에 AfxWndProc을 담고 있으므로 AfxWndProc이 무한 반복 호출되는 상황이 발생한다.

이것은 MFC의 내부 구조상 어쩔 수 없는 제약사항이다.
해결책은 두 가지가 있을 수 있다.

첫째는 범용 클래스를 소스 레벨에서 통합하지 말고 별도의 DLL로 빌드하여 통합할 것.
DLL로 빌드하면 CExWnd는 DLL 컨텍스트에서 관리되는 MFC 윈도 맵과 AfxWndProc을 사용하게 된다. 파라미터로 넘겨지는 윈도 핸들이 permanent CWnd 오브젝트의 핸들이라 할지라도 Exe 컨텍스트에서 사용되는 CWnd 오브젝트라면 충돌이 일어나지 않는다.

둘째는 직접 WNDPROC 타입의 콜백 함수(fnExWndProc)를 만들어 SetWindowLongPtr(hWnd, GWLP_WNDPROC, fnExWndProc)로 직접 서브클래싱할 것.
당연하다. MFC를 사용하지 않는 서브클래싱이면 아무 문제도 없다.

2009년 9월 16일 수요일

128-bit MMX

I’m quite sure that Intel would not like to see SSE2 named 128-bit MMX. In fact, MMX has a bad reputation: the Intel marketing hype pushed it as an universal solution to multimedia requirements, but at the same time the gaming industry switched from mostly 2D games to Virtual Reality-like 3D games that were not accelerated by MMX. Bad press coverage spread the news that MMX was meaningless as it did not improve the Quake frame-rate. That would be correct if the only applications worth running were 3D games, but the overly simplified vision of the world shared by most hardware sites missed several points: in fact, MMX instructions are constantly used to perform a wide array of tasks. PhotoShop users surely remember the performance boost given by MMX, but it should be made clear that each time you play a MP3, view a JPEG image in your browser or play a MPEG video a lot of MMX instructions are executed. Today all multimedia applications are built on MMX instructions, and they are the key to run computing-intensive tasks such as speech recognition on commonplace PCs.
Writing MMX code is still very hard, as you have to go back to assembler, but the performance benefits are rewarding. The support offered by current compilers is barebone. There are a few attempts to write C++ compilers that can automatically turn normal C code into vector MMX code, but they deal only with limited complexity loop vectorization and place too many constraints on the parallelizable code; in general, they appear notably less mature than vectorizing compilers available in the supercomputing domain.
So we cannot expect to have SSE2 enabled compilers anytime soon. This will not stop large companies that sell shrinkwrap software from exploiting SSE2 instructions as they can afford the required development time, but small-scale software firms are not likely to use SSE2 until the appearance of better development tools. In my opinion, the Pentium 4 scenario closely resembles the Pentium MMX one, where lack of software support made the additional investment for the Pentium MMX over plain old Pentium quite useless.
We have just analyzed the dark side of SSE2, i.e. difficult programming; now we can go on and delve into the technical details.
SSE2 extends MMX by using 128-bit registers instead of 64-bit ones, effectively doubling the level of parallelism. We may be tempted to replace MMX register names with SSE2 ones (e.g. turning MM0 into XMM0), recompile it and see it running at twice the speed. Unfortunately, it would not work, actually it would not even compile. These are the steps required to migrate MMX code to SSE2:
1) replace MMX register names with SSE2 ones, e.g. MM0 becomes XMM0;
2) replace MOVQ instructions with MOVAPD (if the memory address is 16-byte aligned) or MOVUPD (if the memory address is not aligned);
3) replace PSHUFW, which is a SSE extension to MMX, by a combination of the following instructions: PSHUFHW, PSHUFLW, PSHUFD;
4) replace PSLLQ and PSRLQ with PSLLDQ and PSRLDQ respectively;
5) update loop counters and numeric memory offsets, since we work on 128 bits at once instead of 64.
Looks easy, doesn’t it? Actually, it is not that simple. Replacing 64-bit shifts with 128-bit ones is trivial, but SSE2 expects memory references to be 16-byte aligned: while the MOVUPD instruction lets you load unaligned memory blocks at the expense of poor performance (so it should be not used unless strictly necessary), every instruction that uses a memory source operand, e.g. a PADDB MM0,[EAX], is a troublesome spot. Using unaligned memory references raises a General Protection fault, but avoiding GPF requires quite a lot of work. First of all, the memory allocators used in current compiler do not align data blocks on 16-bytes boundaries, so you will have to build a wrapper function around the malloc() function that allocates a slightly larger block than required and correctly aligns the resulting pointer (note: the Processor Pack for Visual C++ features an aligned_malloc() function that supports user-definable alignment of allocated blocks). Then you will have to find out all the lines in your source code where the code blocks that are processed with SSE2 instructions get allocated, and replace the standard allocation call with an invocation to your wrapper function: this is fairly easy if you have access to all the source code of your app, but impossible when third-party libraries allocate misaligned memory blocks; in this case, contact the software vendor and ask for an update.
If your MMX routine spills some variables onto the stack, we are in for more trouble, as we have to force the alignment of the stack, and it requires the modification of the entry and exit code of the routine.
The easiest way to fix a PSHUFW instruction is parting it in two, a PSHUFHW and a PSHUFLW, each operating respectively on the high and low 64-bit halves of the 128-bit register.
Here is the list of SSE2 instructions that extend MMX (adapted from Intel’s documentation):

2009년 9월 14일 월요일

load image from bitmap file

다음 코드에서는 LoadImage API 를 사용하여, DIBSection 같이 비트맵 로드 를, DIBSection 색 테이블에서 색상표를 만듭니다. 색 테이블이 있을 경우 하프톤 색상표가 사용됩니다:
BOOL LoadBitmapFromBMPFile( LPTSTR szFileName, HBITMAP *phBitmap,
HPALETTE *phPalette )
{

BITMAP bm;

*phBitmap = NULL;
*phPalette = NULL;

// Use LoadImage() to get the image loaded into a DIBSection
*phBitmap = (HBITMAP)LoadImage( NULL, szFileName, IMAGE_BITMAP, 0, 0,
LR_CREATEDIBSECTION | LR_DEFAULTSIZE | LR_LOADFROMFILE );
if( *phBitmap == NULL )
return FALSE;

// Get the color depth of the DIBSection
GetObject(*phBitmap, sizeof(BITMAP), &bm );
// If the DIBSection is 256 color or less, it has a color table
if( ( bm.bmBitsPixel * bm.bmPlanes ) <= 8 )
{
HDC hMemDC;
HBITMAP hOldBitmap;
RGBQUAD rgb[256];
LPLOGPALETTE pLogPal;
WORD i;

// Create a memory DC and select the DIBSection into it
hMemDC = CreateCompatibleDC( NULL );
hOldBitmap = (HBITMAP)SelectObject( hMemDC, *phBitmap );
// Get the DIBSection's color table
GetDIBColorTable( hMemDC, 0, 256, rgb );
// Create a palette from the color tabl
pLogPal = (LOGPALETTE *)malloc( sizeof(LOGPALETTE) + (256*sizeof(PALETTEENTRY)) );
pLogPal->palVersion = 0x300;
pLogPal->palNumEntries = 256;
for(i=0;i<256;i++)
{
pLogPal->palPalEntry[i].peRed = rgb[i].rgbRed;
pLogPal->palPalEntry[i].peGreen = rgb[i].rgbGreen;
pLogPal->palPalEntry[i].peBlue = rgb[i].rgbBlue;
pLogPal->palPalEntry[i].peFlags = 0;
}
*phPalette = CreatePalette( pLogPal );
// Clean up
free( pLogPal );
SelectObject( hMemDC, hOldBitmap );
DeleteDC( hMemDC );
}
else // It has no color table, so use a halftone palette
{
HDC hRefDC;

hRefDC = GetDC( NULL );
*phPalette = CreateHalftonePalette( hRefDC );
ReleaseDC( NULL, hRefDC );
}
return TRUE;

}


다음 코드에서는 LoadBitmapFromBMPFile 함수를 사용하여 방법을 보여 줍니다:
case WM_PAINT:
{
PAINTSTRUCT ps;
HBITMAP hBitmap, hOldBitmap;
HPALETTE hPalette, hOldPalette;
HDC hDC, hMemDC;
BITMAP bm;

hDC = BeginPaint( hWnd, &ps );

if( LoadBitmapFromBMPFile( szFileName, &hBitmap, &hPalette ) )
{
GetObject( hBitmap, sizeof(BITMAP), &bm );
hMemDC = CreateCompatibleDC( hDC );
hOldBitmap = (HBITMAP)SelectObject( hMemDC, hBitmap );
hOldPalette = SelectPalette( hDC, hPalette, FALSE );
RealizePalette( hDC );

BitBlt( hDC, 0, 0, bm.bmWidth, bm.bmHeight,
hMemDC, 0, 0, SRCCOPY );

SelectObject( hMemDC, hOldBitmap );
DeleteObject( hBitmap );
SelectPalette( hDC, hOldPalette, FALSE );
DeleteObject( hPalette );
}
EndPaint( hWnd, &ps );

}
break;

2009년 7월 30일 목요일

The three-way TCP handshake

1. The three-way TCP handshake

TCP는 장치들 사이에 논리적인 접속을 성립(establish)하기 위하여 three-way handshake를 사용한다.

Client > Server : TCP SYN
Server > Client : TCP SYN ACK
Client > Server : TCP ACK

여기서 SYN은 'synchronize sequence numbers', 그리고 ACK는'acknowledgment' 의 약자이다.
이러한 절차는 TCP 접속을 성공적으로 성립하기 위하여 반드시 필요하다.

TCP는 windowing을 사용한다. (윈도우는 현재 네트웍 혼잡과 수신 용량에 따라 조정될 수 있는 데이터 플로우의 크기)

구체적인 TCP three-way handshake 과정은 다음과 같다.

요청 단말(클라이언트)은 아래 요소를 포함하는 서버에게 TCP SYN 세그먼트를 송신
- 연결하고자하는 서버의 포트 번호
- 클라이언트의 Initial Sequence Number (ISN)
- 옵션인 Maximum Segment Size (MSS)


TCP SYN ACK
아래와 같은 SYN 정보를 가지고 있는 서버의 응답
- 서버의 ISN
- 클라이언트의 ISN + 1 에 대한 ACK
- 옵션인Maximum Segment Size (MSS)

서버의 SYN에 대한 클라이언트의 ACK (TCP ACK)
- 서버의 ISN + 1

TCP를 통한 TCP 통신의 분석 시, MSS는 접속 시간내에 다른 peer에게 알리며 MSS 옵션이 존재하지 않으면 디폴트로 536 bytes를 사용한다는 사실을 명심하라. (이것은 디폴트 IP MTU인 576 bytes에서 20-bytes의 IP 헤더와 20-bytes의 TCP 헤더를 뺀 값이다.)
옵션인 MSS는 일반적으로 클라이언트와 서버 사이의 경로상의 Maximum Transmission Unit (MTU)에서 프로토콜 오버헤드를 뺀 값이며 네트웍 계층(Network Layer)의 헤더를 포함하고 있다. 예를 들어, Ethernet에 대하여 요구되는 MSS는 1460 bytes 이며 IEEE 802.3에 대한 것은 1452 bytes 이다. (8-byte LLC SNAP 헤더에 대해 허용) 만약 어떤 네트웍 사이에 MTU에 대해 세그먼트 크기가 너무 크면 필요하지 않은 IP Fragmentation이 발생할 수 있음에 주의하라.


2. TCP Retransmissions

TCP는 수신자의 윈도우 크기에 따라서 다수의 TCP 패킷의 데이터를 전송하므로써 시작되며 그 후 수신자의 ACK로 인지 되는 버퍼 크기 (윈도우 크기)가 허용되는 비율만큼 계속 전송된다. 만약, TCP 세그먼트가 ACK를 보지 못하고 패킷이 유실되면 송신자는 반드시 어떤 순차적인 세그먼트를 다시 보내야 한다.
하나의 TCP 패킷 재전송에 대해 오직 하나의 패킷에 대한 재전송으로 생각하지는 말아야 한다.

패킷 재전송이 발생하는 상황:

- 전송되는 TCP 세그먼트 혹은 되돌아온 ACK가 스위치나 라우터에의해서 유실(dropped) 되는 경우
- 패킷이 전송하는 동안 손상될 때 (패킷은 CRC 에러를 가지고 있음)
- 패킷의 TCP 데이터 부분이 스위치나 라우터에 의해서 손상될 때 (TCP 체크섬 에러 발생)
- 수신자가 패킷을 버퍼에 저장할 수 없을 때
- TCP 세그먼트가 fragment 되고 fragment가 손상되거나 유실(dropped) 될때
- ACK가 너무 늦게 돌아오고 송신자가 하나 혹은 그 이상의 세그먼트를 재전송 할 때

만약 ACK가 수신되지 않으면 세그먼트는 다시 전송하지만, 첫번째 대기 시간의 두배가 된다. TCP 스택은 접속의 round-trip 시간에 기반하여 초기 timeout 값을 동적으로 계산한다.
각각의 성공적인 재전송은 이전 시도의 두배로 연기된다. TCP 스택은 포기하기 전에 미리 정해진 시간 만큼 시도할 것이다. Windows 95/98/NT 상에서 디폴트 설정은 5번의 시도로 레지스트리상에 설정되어 있다.

Expert 시스템이 없는 Protocol Analyzer를 가지고도 다음과 같은 3가지 순서에 따라 TCP에 대한 재전송을 인지할 수 있다:

1. Analyzer상의 display 버퍼상에서 통신하고 있는 IP 스테이션의 두쌍에 대한 필터를 적용한다.
2. 성공적으로 전송된 패킷 사이의 대략 1초의 3/1에 해당하는 long delay 혹은 주요 delay들을 관찰한다.
3. 이전의 패킷에 대하여 전송하는 sequence number를 체크한다. 오리지널 전송은 몇몇 패킷 이전에 위치할 수 있음을 명심하라.

각각의 패킷 사이의 long delta 시간과 주요 delay에 지시되는 패킷 재전송을 확인하라.
이러한 패킷 재전송의 증거는 패킷내의 sequence number가 동일하다는 것으로 증명될 수 있다.
즉, TCP 패킷 재전송은 sequence number가 동일하다는 것을 의미한다.

몇 가지 경우, Analyzer의 버퍼 상에 왜 그런 재전송이 발생하는 지에 대한 실마리가 이미 있다.
몇 가지 가능성은 다음과 같다.

- 이전 세그먼트 혹은 ACK가 CRC 에러를 가지고 있다. (Ethernet상)
- 토큰링 상에서 스테이션은 대략 2초후에 소프트 에러를 보고한다.
- 이전 패킷에서 TCP 체크섬 에러는 가능한 브리지 혹은 라우터의 손상을 나타낸다.
- 이전 패킷의 TCP 체크섬 에러와 현재 패킷이 재전송되고 있는 패킷은 송신자의 문제이다.
(예를 들면, TCP 체크섬이 올바르게 계산되지 않을 수 있다.)
- 유실되거나 지연된 ACK가 있을 경우, Analyzer를 다른 세그먼트로 이동해서 문제를 정확하게 지적해야 한다.

만약 특정한 서버나 워크스테이션에서 많은 수의 TCP 재전송이 의심되면 netstat 명령어를 사용할 수 있다.
Netstat 명령의 일반적인 사용 (netstat –r)으로 라우팅 정보 - 예를들면 서버나 워크스테이션과 연관된 라우터등의 route table 등 – 를 얻을 수 있다. Netstat 명령으로 또한 “netstat –s” 를 사용하여 프로토콜 통계에 대한 정보를 얻을 수 있다. Windows 95/98/NT 상의 netstat 명령은 특정 프로토콜을 명시할 수 있다.
예를 들어, “netstat –s –p tcp” 명령으로 특정 NT 서버의 대한 다음과 같은 정보들을 얻을 수 있다.

C:\>netstat -s -p tcp

TCP Statistics

Active Opens = 204
Passive Opens = 734
Failed Connection Attempts = 2
Reset Connections = 263
Current Connections = 2
Segments Received = 44759
Segments Sent = 26058
Segments Retransmitted = 15

Active Connections

Proto Local Address Foreign Address State
TCP elca:1123 64.12.24.58:5190 ESTABLISHED
TCP elca:1549 203.231.222.194:pop3 TIME_WAIT
TCP elca:1553 203.231.222.194:pop3 TIME_WAIT
TCP elca:31510 test.iworld.co.kr:1635 ESTABLISHED

상기의 netstat 예제를 보면 26,058 TCP 세그먼트가 서버에 의해 전송되었고 15개의 재전송이 발생하였다. 이것은 1% 보다 작다. 만약, 퍼센트가 이보다 높아진다면 다음 순서는 Analyzer를 서버의 세그먼트에 Tapping하여 TCP 트래픽을 조사해야 한다.

Sniffer Expert System의 Symptom 설명

- 중요도: Minor

- 현상: 이 메시지는 특정 TCP 패킷의 순서 번호가 이전 것과 비교하여 같거나 작을 때, 즉 이전에 이미 전송된 TCP 세그먼트에 대한 ACK 신호를 받지 못하여 시간 초과 상태에서 해제되었던 패킷이 재전송되었을 경우에 발생한다.

- 원인:
. 네트웍 트래픽이 정체되었을 경우
. 수신하는 스테이션이 과부하 상태인 경우
. 라우터가 과부하 상태이거나 다른 이유로 전송지연이 되는 경우
. ACK 신호를 보냈지만 유실된 경우
. 원래의 패킷이나 ACK이 손상을 입은 경우
. ACK 신호가 늦은 경로로 전송되었을 경우
. IP 조각을 읽어버린 경우
. 네트워크의 완결성에 문제가 있는 경우
[이 게시물은 아쿠아님에 의해 2007-05-05 13:51:42 시스템자료실에서 이동 됨]

2009년 7월 21일 화요일

volatile에 대한 오해

“임베디드 프로그래밍 C 코드 최적화” 라는 책을 살짝 보는데 다음 내용에 오류가 있다. 아무리 이 책이 임베디드 환경을 기본으로 했다 하더라도 혼동하기 쉬운 내용이다. 53, 54, 57페이지의 내용이다. 책 내용 일부를 무단 전제하는 것이라 저작권에 위배되는 것 같지만 그래도 좋은 일(?) 하기에 양해 좀 부탁 드립니다.

1: // 하드웨어가 사용하는 메모리는 volatile로 선언2: #define TEMP (*(volatile unsigned int *) 0x001)3: void main(void) {4: int a[10], i;5: volatile int j; // 지연 루프에서 사용하는 변수를 volatile로 선언하여6: // 컴파일러가 최적화시 코드 제거를 막을 수 있도록 함.7: TEMP = 0x0;8: for(i = 0; i < 10; i++) {9: a[i] = TEMP;10: for(j = 0; j < 100000; j++);11: }12: for(i = 0; i < 10; i++)13: printf("a[%d] = %d \t", i, a[i]);14: }
volatile의 기능적 의미는 캐시사용안함(no-cache)이다. 보통 프로그램이 실행될 때 속도를 위해 필요한 데이터를 메모리에서 직접 읽어오지 않고 캐시(cache)로부터 읽어온다. 하지만, 하드웨어에 의해서 변경되는 값들은 캐시에 즉각적으로 반영되지 않으므로 데이터를 캐시로부터 읽어오지 말고 주 메모리에서 직접 읽어오도록 해야한다. 이러한 특성 때문에 하드웨어가 사용하는 메모리는 volatile로 선언해야 하드웨어에 의해 변경된 값들이 프로그램에 제대로 반영된다.

volatile은 임베디드 소프트웨어에서 자주 사용하는 타입 한정자이다. 하드웨어가 사용하는 메모리 선언 시에 주로 사용되고, 최적화 금지의 용도로도 쓰인다. 기본개념은 캐시사용금지(no-cache)로 변경된 값을 즉각 주 메모리에서 읽을 수 있게 하여 프로그램에서 하드웨어의 번화를 감지할 수 있도록 하는 것이다.

(볼드체는 내가 강조한 것임)

전형적으로 volatile에 대해 오해하고 있는 두 가지 사실이다. 일단 임베디드 환경 뿐만 아니라 보통 PC 같은 컴퓨터도 생각해보자.




C/C++가 정의하는 volatile 키워드는 이 값이 언제든지 변할 수 있기에 컴파일러는 최적화하지 말 것을 지시한다. 따라서 이 의미가 직접적으로 “캐시 사용 안 함”은 아니지만 간접적으로 이 말은 맞다. 그런데 여기서 말하는 캐시는 CPU 내의 L1/L2 캐시가 아니라, 레지스터를 가리킨다. 컴파일러는 보통 최적화를 할 때 최대한 변수가 레지스터에 머물도록 한다. 그러니까 변수를 레지스터에 ‘캐싱’하는 것이다 (추가: 레지스터에 캐시한다는 내용은 댓글을 봐주시면 고맙겠습니다. 보통 이런 경우, 레지스터에 캐시한다고 표현하지는 않지만, 반복적인 메모리 접근 대신 레지스터에 그 값을 두고 쓴다는 의미에서 캐시라는 표현을 썼습니다)

그런데 레지스터는 스레드마다 유효한 문맥(context)이므로 (다른 스레드에 유효한 레지스터 값을 쉽게 보는 방법은 없다) 다른 스레드가 이 변수 값을 수정한다면, 레지스터에 캐시된 값은 최신 값을 반영하지 못한다. 이런 문제를 막기 위해 volatile 키워드를 사용한다. 이 키워드는 컴파일러가 이런 레지스터에 캐시하는 것을 막으므로 항상 메모리에서 값을 읽도록 한다. 그런데 잠깐, 여기서 메모리는 정확하게 무엇을 가리키는가?

여기서 또 혼동하는 것이 volatile로 선언된 변수가 CPU 내의 캐시를 다 무시하고 바로 메모리(=DRAM 주메모리)에서 읽고 쓴다는 것이다. 캐시의 의미를 CPU 캐시로 오해했기 때문에 이런 이야기를 하게 된다.

일반적으로 이 말은 옳지 않다. 아무리 volatile을 붙인 변수라도 여전히 CPU 캐시를 활용한다. CPU의 캐시는 보통 사용자에게 투명(transparent)하므로 캐시가 있으나 없으나 밖에서는 구분하기 어렵다. 보통 특수한 명령어1)를 쓰지 않은 이상 데이터를 읽으면 CPU가 알아서 L1/L2 캐시에 있으면 거기서 값을 주고 없으면 DRAM까지 갔다 온 뒤 값을 돌려준다. 물론 세밀한 성능을 위해 캐시를 제어할 수 있는 명령도 제공된다.

그런데 예외가 있다. x86 CPU에는 특정 메모리 주소 영역을 캐시 가능하지 않도록 설정할 수 있다. MTTRs(Memory Type Range Registers)과 PAT(Page Attribute Table)라는 기능으로 캐시 가능한 부분과 그러하지 않은 부분을 제어할 수 있다. 보통 운영체제가 부팅 시 처음으로 설정한다고 한다.

프로그램이 쓰는 일반적인 메모리 영역은 당연히 캐시 가능할 것이다. 그래서 멀티 코어 환경에서도 CPU가 잘 알아서 캐시 코히런스를 맞춰주기 때문에 프로그램은 큰 걱정하지 않고 데이터를 읽어도 그 값의 정확함을 기대할 수 있다. (물론 여기에도 또 예외가 있다. 메모리 컨시스턴시, consistency 문제를 고려 하지 않으면 부정확한 값을 읽을 수 있다. 이 이야기는 복잡함으로 생략.)

그런데 이 책에서는 LED를 깜빡이게 하는 하드웨어를 제어하는 C코드를 예로 들면서 volatile을 설명하고 있다. 보통 이런 하드웨어에 접근할 때는 memory mapped I/O을 이용한다. 그냥 메모리에 값을 쓰는 것으로 바로 하드웨어에 접근한다. 이런 하드웨어에 매핑된 주소는 보통 캐시 되지 않도록 한다. 따라서 volatile로 선언된 주소가 캐시 불가능한 메모리 영역이라면 바로 주 메모리 혹은 해당 하드웨어 장치로 왔다 갔다 한다. 그렇다면 이 책의 설명은 어느 정도 옳다. 그러나 이런 내용이 나와있지 않아 초보자에게 혼동을 심어줄 수 있다.

실제 이 책에서는, 맨 위의 코드처럼, 하드웨어 주소를 가리키는 변수 하나와 단순히 시간 지연을 위한 for 루프의 변수 j, 이 두 개에 volatile을 붙인 뒤 설명하고 있다. 전자는 volatile로 바로 메모리에서 읽는 것이 말이 되지만 j의 경우에는 volatile이 있더라도 (일반적으로) CPU 캐시를 바로 지나치게 할 수는 없다.



요약하면:

C/C++ volatile 키워드는 컴파일러 최적화를 막아 레지스터에 그 값이 캐시되지 않도록 하여 바로 메모리의 값을 읽도록 하는데, 일반적인 경우, 여전히 CPU 내의 캐시는 최대한 활용된다.
volatile 키워드는 컴파일러, 프로그래밍 언어마다 그 뜻이 다르므로 잘 알아보고 쓰자. 특히 멀티스레디드 환경에서 함부로 쓰는 것은 주의해야 한다.
혹시 제가 잘못 알고 있다면 꼭 좀 알려주세요.

CPU는 x86 같은 범용 프로세서도 있지만 임베디드 환경에 쓰이는 특수한 녀석도 많다. 그래서 volatile 키워드 하나를 설명하는데도 많은 경우의 수를 고려해야 한다. 또, CPU도 시간이 지남에 따라 여러 기능이 추가 되기 때문에 단정적으로 말하기가 매우 힘들다. 그래서 “volatile이 있더라도 (일반적으로) CPU 캐시를..” 라는 문장처럼 “(일반적으로)”라는 작은 구멍을 만들었다. 나도 언제든지 틀릴 수 있기 때문이다.




1) x86에서 메모리(다시 한번, 여기서 말하는 메모리는 L1/L2/L3 및 주 메모리를 통칭하는 메모리 계층을 뜻) 읽기(load)와 쓰기(store) 명령은 CPU가 알아서 처리하므로 캐시를 지나치게 할 수는 없다. 그러나 SSE2 확장부터는 쓰기(store)에 대해서 캐시 오염(cache pollution)을 최소화 하는 명령을 지원한다. 그러니까 데이터를 쓸 때 캐시를 건들이지 않고 바로 메모리에 쓰는 것이다. 이런 메모리 명령을 non-temporal이라 부른다. Non-temporal 쓰기에는 MOVNTI, MOVNTDQ, MOVNTPD가 있다. 이게 왜 멀티미디어 확장 같은 SSE 명령에 있냐면, 이런 non-temporal 쓰기는 그래픽 비디오 메모리에 효과적으로 대역폭을 높일 수 있다.

반면, non-temporal 읽기는 SSE4.1에 와서 하나 추가 되었다. 바로 streaming read를 목적으로 하는 MOVNTDQA라는 명령어다. 이 명령은 캐시 가능하지 않은 메모리 영역에 대해 (또 weakly ordering을 지원하는 영역, 이 부분은 설명하고 싶지만 너무 글이 길어져 생략) 높은 대역폭으로, 캐시를 건들이지 않고, 데이터를 바로 레지스터로 가져오게 한다. 차차세대 인텔 CPU에는 이런 non-temporal 읽기가 더욱 강화된다고 한다.

2009년 7월 17일 금요일

[mfc] WM_CLOSE, WM_DESTROY, WM_QUIT의 차이점

WM_CLOSE

윈도우가 닫히기 전에 이 메시지가 전달되며 메인 윈도우인 경우는 응용 프로그램이 종료된다는 신호이다.
이 메시지를 처리하지 않고 DefWindowProc으로 보내면 DestroyWindow 함수를 호출하여 윈도우를 파괴하도록 한다.
이 메시지가 전달되었을 때는 아직 윈도우가 파괴된 것이 아니므로 윈도우가 파괴되는 것을 중간에 차단할 수 있다.
미저장 파일이 있거나 프로그램을 종료할 상황이 되지 않을 때 사용자에게 메시지 박스를 통해 종료 사실을 확인시킬
수 있으며 이 메시지를 가로채서 단순히 return하면 DestroyWindow가 호출되지 않도록 할 수 있다.
예를 들어 프로그램을 사용자가 닫을 때 "저장할까요?"라는 확인이 필요한 경우 등에 사용할 수 있다.

WM_DESTROY

윈도우가 파괴될 때 이 메시지가 전달된다.
사용자가 Alt+F4 또는 닫기 버튼을 누를 경우 WM_CLOSE 메시지가 전달되며 이 메시지를 별도로 처리하지 않으면
DefWindowProc은 DestroyWindow 함수를 호출하여 윈도우를 파괴한다. 또는 프로그램 코드 내부에서 명시적으로
DestroyWindow 함수를 호출할 때도 윈도우가 파괴되는데 이 함수 호출 결과로 WM_DESTROY 메시지가 전달된다.
이 메시지를 받은 윈도우는 윈도우의 종료를 위한 처리를 해야 하는데 예를 들어 열어 놓은 파일을 닫고 할당한
메모리를 해제하는 등의 정리 작업을 한다. WM_CREATE에서의 초기화 처리의 반대 동작이 이 메시지에 작성되는
것이 일반적이며 그외 레지스트리에 미보관 정보를 저장하는 등의 작업을 할 수 있다. 만약 파괴되는 윈도우가
클립보드 체인에 속해 있으면 자신을 클립보드 체인에서 제거해야 한다.
DestroyWindow 함수는 파괴할 윈도우를 화면에서 숨긴 후 이 메시지를 보내므로 이 메시지를 받은 시점에서는
윈도우 자체가 파괴되지 않은 상태이다. 또한 DestroyWindow 함수는 자식 윈도우에게도 이 메시지를 차례대로
보내주는데 부모 윈도우가 먼저 이 메시지를 받고 자식 윈도우에게로 이 메시지가 보내진다. 따라서 부모 윈도우가
이 메시지를 처리하는 동안은 모든 자식 윈도우가 아직 파괴되기 전이므로 자식 윈도우를 프로그래밍할 수 있다.
파괴되는 윈도우가 메인 윈도우일 경우 PostQuitMessage 함수를 반드시 호출하여 프로세스의 메시지 루프를
종료하도록 해야 한다. 만약 이 처리를 생략하면 윈도우만 파괴되고 메시지 루프는 계속 실행중인 상태가 되므로
프로세스가 종료되지 않는다.

WM_QUIT

응용 프로그램을 종료하라는 신호이다. PostQuitMessage 함수 호출에 의해 발생하며 GetMessage 함수가 0을
리턴하도록 함으로써 메시지 루프를 종료시키는 역할을 한다. GetMessage 함수는 WM_QUIT 이외의 모든 메시지에
대해 0이 아닌 값을 리턴하므로 계속 루프를 돌지만 WM_QUIT에 대해서만 0을 리턴한다.
그래서 메시지 루프는 통상 다음과 같이 작성된다.

while(GetMessage(&Message, 0, 0, 0))
{
TranslateMessage(&Message);
DispatchMessage(&Message);
}
return (int)Message.wParam;


GetMessage 함수가 0이 아닌 값을 리턴하는 동안 무한히 이 루프를 도는데 단 WM_QUIT가 전달될 때는
while문이 종료되며 따라서 WinMain이 종료된다. 메인 윈도우의 WM_DESTROY에서는 반드시 PostQuitMessage
함수를 호출하여 메시지 루프가 종료될 수 있도록 해 주어야 한다. 그렇지 않으면 메인 윈도우는 파괴되었으나
프로세스는 계속 실행중인 상태가 된다.

PeekMessage 함수는 WM_QUIT 메시지와 상관없이 메시지 큐에 메시지가 있는지만 리턴하므로 메시지 루프를
구성할 때 따로 WM_QUIT 메시지를 점검해야 한다.
for(;;)
{
if(PeekMessage(&Message, NULL, 0, 0, PM_REMOVE))
{
if (Message.message == WM_QUIT)
{
break;
}

TranslateMessage(&Message);
DispatchMessage(&Message);
}
else
{
// 백그라운드 작업
}
}


조사한 메시지가 WM_QUIT이면 메시지 루프를 탈출하는 별도의 코드가 필요하다.
WM_QUIT는 윈도우에게 전달되는 메시지가 아니므로 윈도우 프로시저는 이 메시지를 받을 수 없다.
윈도우 프로시저까지 전달되기 전에 메시지 루프에서 이 메시지를 차단하여 루프를 탈출하게 된다.

WM_NCDESTROY
비작업 영역이 파괴될 때 보내진다.
윈도우와 그 차일드들이 먼저 파괴된 후에 비작업 영역이 파괴되므로 이 메시지는 윈도우가 가장 마지막으로 받는 메시지이다.
WM_DESTROY보다 뒤에 발생되며 이 메시지를 받았을 때는 모든 차일드가 이미 파괴된 후이다.
반면 WM_DESTROY 메시지는 차일드가 아직 파괴되기 전이다.
종료 처리가 필요할 경우는 일반적으로 WM_DESTROY 메시지에 코드를 작성하므로 이 메시지는 실용적인 가치가 거의 없는
셈이며 처리하는 경우가 극히 드물다.

2009년 7월 10일 금요일

[socket] IOCP에서 합리적인 강제종료 방법

IOCP의 기본 규칙은, 하나의 세션에 대해 IO요청중인 상태에서는 이게 완료통보로 오기까지는

이 세션에 대해 다른 요청작업을 하면 안된다는 것이다



그렇다면 PostQueuedCompletionStatus(hcp, cbTransferred, ssid, p); 는 방법으로 적절하지 못하다



하나의 세션은 IO요청중이거나, 완료통보 된 상태에서 작업하여 다시 IO요청으로 가기전의 상태만 존재하기 때문이다

즉, 완료통보 상태가 되려면 반드시 client 측에서 뭔가 신호 하나를 던져줘야만 한다는 것이다



P.. 방법을 사용할 경우, IO요청중에 이루어진다면, 맨위에 기술한 기본 규칙에 어긋나므로 부적절

다음으로 작업중에 요청이 된다면, 하나의 세션에 대해 2개의 쓰레드에서 작업을 해버리므로 부적절(P.. 는 직접적 큐잉방법이므로 호출 즉시, G.. 로 뜨게됨)



그렇다면 방법은 두가지다... (효율적인 방법을 꼽는 다면)



1. 쓰레드에 안전한 ConnList 를 사용하는 방법

해당 struct에 bool disconn을 추가하여, 매 GQCS 호출시, 체크를 하여 true 일때는 transferred byte 를 0으로 바꿔서

자연스럽게 클라이언트의 종료 루틴으로 빠지게 하는 방법

장점: P... 보다는 루틴의 안정성이 뛰어남

단점: 클라이언트가 반드시 길이 有의 패킷을 보내야한다는점, 마지막에 완료 루틴으로 온 패킷이 drop(씹히는 상태)된다는점



2. closesocket을 사용하는 방법



하지만 이때 또 주의사항이 있다

1. c.. => IO요청 시작(중) 일때 : WSARecv 에러리턴즉시 처리가능

2. IO요청 시작(중) 일때 => c.. : 정상적인 완료통보로 뜸

3. c.. => 세션 작업중일때 : 다시 IO요청으로 가므로 : 1과 동일

4. c.. => 세션 종료 작업 중일때 : closesocket이 중복허용하지 않는다면 에러가 발생할 수도??? 아니라면 문제없음(IO요청이 더이상 안일어나므로)

5. c.. => 세션 종료 작업 이후에 요청될때 : 뭐, 없는 개체이므로 JUST OK (하지만 없는 개체에 대해 위의 함수를 허용하지 않는다면, 에러, 아니라면 문제 없음)

=> 확인 결과: 문제 없음

장점:

- 이미 워커쓰레드에서 close 루틴이 진행중이라 하더라도 delete ptr 및 push, ConnList erase 과정은 여기서만 담당하므로 상관없음

- 동일 socket에 대해 두번씩이나 closesocket를 호출한다하더라도 에러는 안걸림

- 워커쓰레드 함수상에서는 항상 IO요청이 이루어지도록 하고있으므로 아래 두가지 항목의 장점이 있음

- 세션에 대해 작업중일때는 WSARecv 호출 즉시 종료처리를 해주면 됨

- IO요청 중일때에는 자연스럽게 완료통보상으로 클라이언트의 FIN 핸드쉐이크의 결과 신호가 리턴됨

단점: 없음

주의사항: 이 방법을 사용하므로써, 어떤 에러가 일어나게 될지는 아직 검증되지 않은 상태임

- 이상이 없을 것이다: closesocket(sock) 에서 어짜피 sock은 전역변수가 아니니 상관은 없음

(IOCP 에서는 이미 어셉트 쓰레드와 워커 쓰레드가 나뉘어지므로, 하나의 유저의 accept와 동시에 다른 유저의 closesocket이 처리될 수 있음, 그렇다면, 딱히 이 부분은 동기화에 너무 과하게 신경쓰지 않아도 되는 부분일듯)
[출처] IOCP에서 합리적인 강제종료 방법|작성자 Harpy

팔로어

프로필

평범한 모습 평범함을 즐기는 평범하게 사는 사람입니다.