블로그 이미지
대갈장군

calendar

1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29

Notice

'2016/02'에 해당되는 글 2

  1. 2016.02.08 Getting Physical With Memory
  2. 2016.02.06 Anatomy of a Program in Memory
2016. 2. 8. 23:04 프로그래밍/C++

출처: http://duartes.org/gustavo/blog/post/getting-physical-with-memory/

저자: Gustavo Duarte


이 글도 상당히 도움이 되는 글인듯 하여 번역하여 옮겨둔다.


원래 복잡한 시스템을 이해하려면 가장 기초가 되는 원초적인 것을 들여다 볼 필요가 있다. 그런 점에서 우리는 프로세서와 버스 간의 인터페이스에 대해서 알아보자... 저자는 EE 전공자가 아니므로 EE 친구들이 걱정하는 그런 부분은 가뿐히 무시하겠다고... CS! CS! /yay


Physical Memory Access

위의 그림은 Core 2 디자인이다. 코어 2의 경우 775개의 핀을 가지고 있다. 그중 절반은 파워만 제공하고 데이터를 운송하지 않는다. 그럼 대략 387.5 개 인가? 작동하는 핀들을 그룹으로 묶어주면 사실 코어2의 디자인은 상당히 단순하다. 위의 그림을 보면 중요한 핀들을 보여주고 있는데 Address pin, Data pin, Request pin 들이다. 이 동작들은 FSB (Front Side Bus)라 불리는 곳에서 이른바 Transaction이라는 이름으로 시행되는데 FSB Transaction은 5개의 페이즈를 가지고 있단다. Arbitration, Request, Snoop, Response, Data 요렇게 다섯가지. Agent라 함은 프로세서에 Northbridge까지 포함한다. 한국말로 요원들이라고 해야 겠군... 이 요원들이 다섯가지 페이즈를 호출하여 사용하는데 각각 다른 형태의 임무를 수행한다는 군...


우리가 관심있는 분야는 오직 Request Phase다. Request Agent (주로 한개의 프로세서) 에 의해 2개의 packet이 출력으로 나오는 페이즈가 바로 Request Phase. 

FSB Request Phase, Packet A

위 그림처럼 35-3 (33 비트)의 길이를 가지는 필드를 먼저 보게 되는데 이것은 물리 주소를 알려주는 필드이다. 그리고 바로 뒤에 REQ 핀이 오는데 이 녀석의 각 비트 값에 따라 어떠한 형태의 접근 및 처리를 원하는지 알려준다. 이 첫번째 패킷이 나간 후에 바로 두 번째 패킷이 나가는데 아래 그림과 같다.

FSB Request Phase, Packet B

여기서 재미 있는 부분은 바로 Attribute Signals 필드인데 (31:24) 여기를 보면 메모리 캐싱 동작의 5가지 종류에 대해서 정의하고 있다. FSB에 이 정보를 제공함으로써 Request Agent는 다른 프로세서들에게 이 Transaction이 그들의 캐시에 영향을 주는지 알려주며 메모리 컨트롤러인 Northbridge에게 어떻게 동작해야 하는지도 알려준다. 프로세서는 커널에 의해 작성되는 page table을 참조하여 주어진 메모리 영역의 타입을 결정한다.


일반적으로 커널은 모든 RAM 영역을 write-back으로 간주하는데 그것이 최상의 퍼포먼스를 내기 때문이다. Write-back 모드에서는 메모리 액세스의 단위가 cache line 이 되고 이것은 64 바이트이다 (코어2에서). 만약 하나의 프로그램에서 1 바이트를 메모리에서 읽으면 프로세서는 그 1바이트를 담고 있는 전체 cache line 을 L2 와 L1 캐시에 로드한다. 또한 프로그램이 메모리에 쓰는 경우, 프로세서는 캐시에 있는 라인만 수정하고 main memory는 업데이트 하지 않는다. 나중에, 처리해야 하는 시점이 오면 그제서야 cache line 전체를 한방에 메인 메모리로 써버린다. 고로 대부분의 request는 Length field가 11 (64 bytes) 값을 가지고있다. 다음 그림을 보면 캐시에 없는 데이터를 어떻게 읽어들이는지 보여준다.

Memory Read Sequence Diagram

위 그림을 보아하니 대충 이해가 간다. 프로그램이 특정 주소를 달라고 요청하게 되면 우선 L1 캐시에 있는지 확인해보고 없으면 L2로 가고 거기도 없으면 FSB로 요청해 그 주소로 직접적인 액세싱을 하게 된다는 거. 길이는 64바이트로 고정되어 있군. 


추가적으로 인텔 컴퓨터의 일부 메모리 영역은 장치로 연결된 경우가 있다. 즉, 메모리 영역이 실질적인 RAM이 아니라 하드 드라이브의 일부이거나 네트워크 상의 공간일 수 있다는 것인데 이런경우에는 커널이 이런 주소 영역을 uncacheable 로 도장을 쾅 찍어둔다. 당연히 이런 주소 공간을 캐시에 담아둔다는 것은 말이 안된다. 이런 경우에는 길이가 64바이트가 아닐수 있으므로 Length field가 64 바이트가 아닐수 있다는 점..


글의 결론은

1. 퍼포먼스가 중요한 프로그램의 경우에는 같은 캐시 라인 안에 필요한 데이터를 팩킹해서 처리하도록 노력하는 것이 성능향상에 도움이 된다. 캐시에서 바로 가져오는 경우 성능의 향상이 어마어마 하다.

2. 하나의 cache line 안에 포함되는 메모리 엑세싱Atomic 이 보장된다. 원소성이라고 하는데 이것이 좋은 이유는 중간에 다른 스레드가 개입하여 중단하는 일이 없기 때문이다. 즉, 멀티 스레드에도 안전성을 제공한다는 거.

3. FSB는 모든 에이전트에 의해 공유된다. 모든 에이전트는 모든 transaction을 듣고 있어야 하는데 이런 것이 FSB에 교통혼잡을 유발하고 성능을 떨어뜨리는 이유가 되었다. 바로 Core i7에서는 이러한 문제를 해결하기 위해 프로세서가 바로 메모리로 접근할수 있도록 변경했다. 그것이 성능 향상에 큰 도움.






posted by 대갈장군
2016. 2. 6. 05:55 프로그래밍/C++
출처: http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/

저자: Gustavo Duarte


요즘 메모리 관련 문제를 다루다 보니 점점 더 힙 속으로 빠져들고 있던 차에 좋은 글을 발견하고 내 개인 소장용으로 옮겨 볼까 한다.


일반적으로 멀티 테스킹 OS에서 각각의 프로세스는 그들 자신만의 메모리 샌드 박스에서 실행된다. 즉, 각각의 프로세스가 개별적인 주소 공간을 소유한다는 의미... 이 샌드 박스는 일반적으로 32비트 환경일 경우 Virtual Address Space라 부르고 약 4GB의 메모리 공간이 주어진다. (32비트니까~)


이 가상 메모리 주소 공간은 실질적인 (물질적인) 메모리와 이른바 Page Table이라는 놈을 통해 맵핑 되는데, 뭐, 쉽게 말해서 가상 주소는 말그대로 가상으로 주어진 공간이고 실제 메모리가 할당되려면 맵핑이 필요한데 이 주소록을 가지고 있는 녀석이 Page Table이라는 거지. 재미 있는 점은, 컴퓨터에 돌아가는 모든 프로세스는 이 룰에 따라야 한다는 말이다. 심지어 커널 자신 조차도... 쉽게 생각하면 커널은 예외일 것 같지만 그렇지 않다는 점을 글쓴이는 말하고 싶었던가 보다. 아무튼, 그래서 이 가상 주소 공간은 반드시 커널을 위해 일부를 쓴다는 점이다. (아래 그림처럼)

Kernel/User Memory Split

리눅스는 기본적으로 1GB만 커널이 사용하도록 기본 셋팅이 되지만 윈도우는 2GB나 커널을 위해 할당한다. 즉, 프로세스 자신은 남은 2GB만 사용가능하다는 거네... 이런 위도우 같은 경우를 봤나... 물론 윈도우도 3GB까지 확장하는 옵션이 있단다... 휴..


뭐 이쯤에서 눈치 챘겠지만 각 프로세스가 4GB씩이나 할당하면 나의 구린 컴퓨터는 고작 16GB 램인데 프로세스 4개 돌리면 램 바닥찍고 끝나는가 라는 의문이 스멀스멀 든다면 4 곱하기 4는 16이므로 정답..


물론, 각각의 프로세스가 4GB를 무조건 쓴다는 건 아니라는 점... 시작은 미미하게 해서 끝은 창대해 질수 있겠지만 일반적으로 시작할때는 최소 필요 메모리만 할당해서 최대 4GB까지 쓸수 있다는 점을 이론적으로 보여주는 것일 뿐, 실제로 프로세스가 무조건 4GB 할당하고 출발하는 건 아니라는 점.


다만, 프로그래머가 알아야 할 것은 실수로 커널 메모리 영역을 침범하면 운영체제가 "네 이놈! 여기가 어디라고 감히 들이대느냐!!" 하면서 프로그램을 종료시켜 버린다는 거... 또 한가지 중요한 점은, 커널 코드와 데이터는 항상 어디서나 접근 가능하고 인터럽트를 핸들링 하며 시스템 콜을 받아들일 준비가 되어 있는 반면, 사용자 주소 영역은 프로세스가 변경되면 그에 따라 변화한다는 점... (아래 그림)

Process Switch Effects on Virtual Memory

위 그림에서 사용자 주소 영역에 하늘색 표신 된 부분은 메모리가 할당되어 사용되어진 영역이고 흰색은 할당되지 않은 영역이다. 재미 있는 건, 저자가 Firefox는 메모리를 무지하게 잡아 먹는다면서 저렇게 Firefox의 메모리 할당을 크게 그렸다는 점... 크롬이 더 많이 먹는거 아닌가 몰라...

Flexible Process Address Space Layout In Linux

위 그림을 보면 대략적인 프로세스의 주소공간을 (4GB) 볼 수 있다. 일반적으로 모든게 제대로 잘 작동했을 경우 대부분의 프로그램은 위의 그림과 같은 구조의 메모리 공간을 가지고 시작하게 된다. 사실 이점에 대해서 저자는 보안 취약점이 될 수 있다고 지적하고 있다. 해커가 이런 메모리 구조를 미리 파악하고 대충 찍어서 맞추는 경우 보안상의 헛점이 드러날 수 있다는 점. 그래서 요즘은 메모리를 랜덤하게 유행한단다... 이 글이 써진 시점이 무려 2009년이니... 허허


위 그림에서 사용자 공간의 최상층에 위치하고 있는 것이 바로 Stack인데 이 녀석은 지역 변수를 저장하고 함수의 인자들을 저장하는 용도로 주로 사용된다. 함수를 호출하게 되면 스택영역에 Stack Frame이라는 놈을 넣게 되는데 이 함수가 리턴 될 때 이 Stack Frame도 파괴된다. 스택 자체가 LIFO 방식으로 단순하게 동작하므로 pushing과 popping이 매우 빠르고 효율적으로 수행 가능하다. 그리고 스택 영역을 자주 그리고 많이 사용하면 결국 CPU 캐시에 스택 메모리가 활성화 되어 있게 되므로 스피드를 더더욱 업업업~ 각각의 스레드는 자신만의 스택을 가진다.


만약 사용자가 스택 영역에 할당된 현재의 영역보다 더 큰 데이터를 집어 넣게 되면? 이렇게 되면 (리눅스의 경우) expand_stack()이라는 함수 (결국, acct_stack_growth() 함수 호출하는 녀석)를 호출해서 스택 영역의 확장이 가능한가 체크한다. 만약 스택 영억이 기본 최대 사이즈 (8MB)보다 작다면 스택 영역이 확장된다. 근데, 만약 이 8MB를 다 써버리면? 그때 바로 터지는 것이 Stack Overflow이고 프로그램은 Segmentation Fault를 받으면서 장렬히 전사한다. 또 한가지 흥미로운 점은 스택 영역은 한번 확장되면 스택이 줄어든다고 해서 확장된 영역을 줄이진 않는다. 이것이 마치 미국의 연방 정부의  예산과 같다고... 늘리기만 하고 줄이진 않는다는 이야기.. ㅋㅋ


자, 그 다음은 스택 아래에 위치한 Memory Mapping Segment라는 놈인데 이 놈은 파일의 내용을 직접적으로 메모리로 맵핑해준다. 이런 일을 해주는 대표적인 함수로는 mmap(), CreateFilemapping(), MapViewOfFile() 같은 함수들이다. 이러한 Memory Mapping은 고성능 파일 입출력이나 Dynamic library들을 로딩하기 위해서 사용되어 진다. 그리고 이른바 Anonymous Memory Mapping도 가능한데 이건 임의 메모리 맵핑이라 해야 하겠네.. 파일이나 라이브러리 같은걸 맵핑 하는게 아니라 malloc() 같은 함수가 제법 큰 메모리 공간을 할당하려고 할때 종종 이 구역의 메모리를 임의 메모리 맵핑이라는 형태로 사용하도록 해준다는 군. 메모리를 좀더 효율적이고 많이 사용하기 위해 그런듯...


그다음은 바로 힙! 엉덩이 아니지요.. Heap! 사실 프로그램 돌리다 보면 터지는 메모리 에러의 90% 이상이 힙에서 터진다고 봐도 과언이 아니다. 이 힙 영역은 프로그램이 런타임 (실행중) 중에 메모리를 임의의 크기로 할당할때 사용되는 영역이다. 이러한 동적 메모리 할당 때문에 힙 메모리 사용은 커널과 언어의 런타임이 서로 협상해서 만들어 진다. 대표적인 힙 할당 함수로 C에서는 malloc이 있고 C#이나 C++의 경우에는 new가 있다. 


만약 메모리 공간이 여유가 있다면 언어의 런타임에서 알아서 처리하고 끝낸다. 만약 현재의 할당된 힙으로 공간이 부족하다면 시스템 콜 (brk())을 통해 확장되는데 이것이 바로 커널의 개입이다. 사실 힙의 관리는 정말 복잡한데 머리 좋고 똑똑한 사람들이 최선을 다해 알아서 잘 관리하도록 만들어 놨다고... 뭐, 아무튼...


그리고 마지막으로 제일 아래 영역이 보이는데 BSS, data 그리고 program text다. BSS와 data store의 경우 static global variable들을 저장하는 용도로 사용된다. BSS는 할당되지 않은 (uninitialized) static variable들을 저장하고 data segment의 경우에는 initialized된 static variable들을 저장한다. 


뭐 몇가지 설명이 더 있기는 한데 그렇게 필요해 보이진 않아서 스킵~ 




posted by 대갈장군
prev 1 next