삽질의 현장/- 네트워크 프로그래밍

#005_Window_Network_구조체 멤버 정렬

shovelman 2015. 10. 2. 19:18


안녕하세요 삽잡이입니다.


이번 시간에는 구조체 멤버 정렬에 관해 배워보도록 하겠습니다.



MSG라는 구조체의 크기는 몇일까요?

char 1바이트 int 4바이트 double 8바이트... 총 13바이트가 나오지 않을까요?



하지만, 16바이트가 출력이 됩니다. 제가 계산을 잘못한 것일까요?

아닙니다. 16이 나오는 것이 맞다는 것입니다. 그 이유에 대해서 알아보도록 하겠습니다.


자... 그런데 감히 잡히시지 않습니까?

저기 저 주석 처리된 메크로가 의심스럽니다.


아무튼...

이 문제는 구조체 바이트 정렬에 의해서 생겨난 일입니다.


우선 말씀드리고 싶은 말이 있습니다. 

'구조체를 함부로 날리지 않는다. 구조체를 함부로 받지 않는다.'

만약, 통신하는 다른 PC와 서로 구조체 정렬 방식이 다르다면,

연산이 잘못되는 문제가 발생할 수 도 있게 됩니다.


위의 코드는

'구조체의 데이터를 함부로 날리고 받지 않아야되'는 것을 말씀드리고자 소개했습니다.

구조체에도 바이트 정렬을 하기 때문이지요...


프로세스 하나가 있다고 해봅시다.

그 프로세스는 당연하게 주소 공간이 있겠지요...

그리고 그 주소 공간은 정확히는 물리 메모리에 실제 매핑 되어있는 가상의 주소 공간입니다.


그런데, 그 주소는 전체적으로 4GB의 용량을 사용할 수 있습니다.

지금도 마찬가지지만 예전에는 4바이트가 어마어마한 메모리 공간이였습니다.

실제 프로세스가 메모리를 4GB를 넘지 않는다고 했죠....

어떤 데이터를 읽거나 찾을 때 모든 주소를 다 뒤져야한다고 해봅시다.

시간이 어마어마하게 걸리겠지요...


그래서 CPU와 메모리간에 계속되는 통신 가운데

32bit OS에서는 32비트씩 즉, 4바이트씩 데이터를 가져옵니다.

한번에 데이터를 나를 수 있는 단위라는 것입니다.


주소 지정도 이와 같습니다.

주소 지정도 한번에 크게 3가지 버스를 통해 지정하게 되는데,

컨트롤 버스, 데이터 버스, 어드레스 버스가 도움을 주지요...

음... 다른길로 새는것인데...

버스는 CPU, 기억장치를 상호 연결해주는 중심 통로라고 할 수 있습니다.

주소 지정을 할 때 이 버스들이 사용되는 것입니다.


아무튼... 데이터를 가져오면 Read 컨트롤이 모든 주소에 활성화 되고

주소가 지정되면 자동적으로 주소가 넘어옵니다...

즉, 무조건 OS가 지원하는 바이트를 가져온다 이겁니다...


우리는 지금 구조체 정렬을 왜 해야하는지 알아가고 있습니다...

산으로 가고 있지 않습니다...


자... 어찌됬건,

32bit는 항상 4바이트씩 나르고 한번에 지정될 수 있는 데이터가 4바이트라고 했습니다.

지정할 수 있는 주소가 4바이트밖에 안된다는 것입니다.

그러니까 32bit OS에 LAM 16기가를 써도 소용이 없다는 것입니다.


이 이야기를 왜 할까요?

구조체를 만들었다고 해봅시다.


 


char형 메모리를 읽어온다면 1바이트만을 읽어올 수 없습니다.

4바이트씩 가져온다고 했엇지요...

따라서 1000 ~ 1004 주소까지 CPU에 가져와서 shift 연산으로 잘라 내 버려서 1바이트를 건지게 됩니다.


그런데 문제는 int형 메모리를 가져올 때입니다.

애매하게 1001 주소부터 시작되는데 4바이트씩 가져온다고 했으니

1000 ~ 1004, 1005 ~ 1008 주소 즉, 두번만에 가져와야합니다.

1000번지를 지정해서 한번 가져오고 1004번지를 한번 지정해서 또 가져오고...

무조건 4바이트씩 가져와야되니깐 말입니다.

그래서 각각 잘라서 데이터를 합쳐야됩니다.

즉, 두번 연산을 해야한다는 것입니다. 주소 사이에 걸쳐있으니깐 말입니다...

32bit는 4바이트씩만 주소를 가져올 수 있다는 사실을 잊지 마시길 바랍니다.

데이터를 가져오려면 무조건 4바이트씩!


이렇게 연산할바에 차라니 char형 메모리를 하나 사용하면 그 아래 3바이트를 비우고

그 다음번지부터 데이터를 채우자는 주장이 펼쳐졌습니다. (허허허)

그리고 int형 메모리를 1004 번지 부터 채우자 이거지요...



그렇게 되면 구조체를 보낼 때 1000번지와 1004번지를 각각 가져올 수 있습니다.

그러면 조립을 할 필요가 없다는 것이지요...

이와 같이 사용하기 위해 구조체에는 padding 바이트를 삽입하게 됩니다.

그래서 자기 자신 나름대로의 바이트 정렬을 수행하게 되는 것입니다.


double 형의 경우 8바이트인데 8바이트가 들어가면 

요즘은 64bit 운영제체가 많기 때문에...

8바이트 단위에 맞춰서 하기도.... 아니 근데 이건 프로세스가 하는 일이구요...

뭔말이냐면, 컴파일러가 우리의 컴퓨터 프로세스를 보고 최적하라는 것을 알아서 해주기 때문에

신경 쓸 일은 아닙니다.


최적화란, 읽고 쓰고 속도를 높이기 위한 작업으로, 

우리가 어떻게 프로그램을 해도 우리가 하고자 하는 행동을 변화 시키지 않고 

성능을 높이기 위한 작업을 말합니다.

그런데, 이 최적화 작업을 컴파일러가 수행하는데 

이 최적화 작업에서 구조체 바이트 정렬 작업이 일어난다 이겁니다.


그래서 #pragma pack이라는 메크로를 사용해서

최적화 작업을 해준 것이고,

이 메크로가 없으면 최적화를 하지 않아 패딩 사이즈까지 합쳐서 나오게 되는 것입니다.


즉, 위의 코드에서는 13 바이트가 날라가야되는데 컴파일러에게

구조체를 최적화 하지 말라고 말해주는 꼴이 되는 것입니다.

구조체에게 패딩작업을 하지 말라고 말입니다.


그런데 사실 이런 것은 잘 쓰지 않습니다.

이와 같은 작업들을 계속 진행하다 보면 모든 구조체가 바이트 정렬을 수행하지 않는

문제가 발생하기 때문입니다. 속도가 엄청 느려지겠지요...


따라서 해당 작업은 좋지 않은 작업입니다.

결론은, 저런 옵션이 있다는 것을 알아두자 이겁니다...


우리가 생각하는 바이트의 수와 구조체의 크기가 다를 수 있습니다.

그래서 구조체를 함부로 네트워크 상에서는 주고 받는게 아니라고 강조를 하는것입니다.


그래서 어떤 구조체가 있던 모두를 '바이트 배열'로 보내고

바이트를 순차적으로 날리게 하는 것입니다.


자바나 C#은 알아서 해주지만(Serialization) C++ 까지는 우리가 알아서 처리를 해야합니다...

아무튼....


이런 것이 있다는 사실을 알고 넘어갑시다~


이상 삽잡이였습니다!