2011년 11월 13일 일요일

디자인 패턴

Chromium 소스 분석을 시작한지 이제 한달이 넘어가고 있습니다...
아직 포스팅 할만큼은 분석이 안되서 못 올리고 있지만, 앞으로 조금씩 포스팅할 생각입니다.

한달을 돌이켜 생각해보면, 패턴을 알고있었더라면 조금은 더 쉽게 구조를 잡지않았을까 하는 아쉬움이 있습니다.
이제 시작이고 6개월이 더 걸릴지 1년이 더 걸릴지는 모르겠지만 언젠간 실시간으로 올라오는 개발자들의 commit log를 이해할 날이 오겠지요.

다른 대부분의 오픈소스 프로젝트들도 마찬가지겠지만, Chromium 도 역시 패턴으로 구현이 되어있는 것을 알 수 있습니다. 그중에서도 singleton, observer, delegate, proxy 등등은 거의 모든 소스 파일에서 볼수 있는 단어라고 생각해도 과언이 아니니까요.

Chromium 분석을 하기 전까지 디자인 패턴이란 말을 들어보기는 했으나, 공부를 해본적은 없었습니다. 그래서 소스코드에서 delegate, observer 등의 단어가 섞여있는 클래스 들을 보고도 전혀 감을 잡지 못했었죠. 클래스들이 뭐 이리 복잡하게 엮여있어!! 하는 불평만 했습니다.

만약 제가 패턴을 공부했었더라면, 아니 적어도 무엇인지 정도만 알고 있었더라도 분석을 하는데 많은 도움이 되었을 것입니다.

Chromium 은 방대합니다. 그만큼 수많은 라이브러리와 패턴들로 구현이 되어있구요.

Chromium 소스 분석은 공부하는데 많은 도움이 되는것 같습니다.

느낍니다. 저는 하수 중에 하수라는 것을...

많이 노력해야겠습니다

TCMalloc

Chromium의 소스 분석을 시작하면서 다음의 코드를 만나게 되었습니다.

//src/content/app/content_main.cc
tc_set_new_mode(1);

이 함수는 tcmalloc 에서 제공하는 API 입니다.

흐음... tcmalloc은 뭘까요?
tcmalloc 은 Thread-Caching Malloc의 약자로 구글성능도구(google-perftools) 패키지에 포함되어 있습니다.

이름에서 알수있는 것처럼 thread 별로 malloc 관리를 하는 라이브러리인데요,
doc page 를 읽어보면 각각의 쓰레드에 thread-local cache를 배정한다고 나와있습니다. (TCMalloc assigns each thread a thread-local cache.)

쓰레드에서 malloc 이나 new 를 통해 메모리 할당을 요청하면, thread-local cache에서 메모리할당이 수행되는 것인데요, 왜 이런 과정이 필요하게 된걸까요?
쓰레드를 많이 사용하는 프로그램에서는 기존 메모리 할당 방법(glibc malloc)으로는 어떤 문제가 있길래 쓰레드단위로 메모리 할당이 이루어지도록 했는지 궁금하네요.

많은 쓰레드가 동작하는 상황에서 기존 malloc 사용시 다음 문제가 생기는 것 같습니다.
아래 글은 다음 블로그에서 참고하였습니다 (http://lethean.pe.kr/2009/04/29/tcmalloc-google-perftools/)

장기간 실행되면서 빈번하게 메모리를 할당 / 해제하는 것은 물론 수십 개의 쓰레드가 동작하는 프로그램에서는 어쩔 수 없이 메모리 단편화(Memory Fragmentation)가 발생합니다. 메모리 단편화가 많을 경우 어플리케이션 로직에 메모리 누수(memork leak)가 없어도 C 라이브러리 메모리 관리자가 메모리를 커널에 반환하지 않기 때문에 프로세스의 메모리 사용량은 계속 늘어납니다.(참고로 이러한 경우인지 여부는 주기적으로mallinfo() 정보를 확인하면 됩니다) 물론 이를 회피하기 위한 기법이나 아키텍쳐는 많이 있지만, 그리 쉽게 원하는 성능과 효율을 얻기는 힘들더군요. "


여러 쓰레드에서 메모리 할당/해제를 하게되면 glibc 내 malloc이 관리하는 메모리 풀의 단편화가 심해져서 커널에 메모리 반환이 잘 안되는 모양입니다. 그래서 쓰레드가 종료가 되더라도 메모리 단편화로 인해 glibc는 종료된 쓰레드가 사용했던 메모리 반환을 못하게되어 메모리 사용량이 줄지않고 계속 늘어나는 현상이 생길 수 있겠습니다.

tcmalloc 을 사용하면 malloc에 비해 빠르고 효율이 좋다고 합니다. 성능은 할당/해제 시간일 테고, 효율은 오래 프로그램을 돌리더라도 메모리 사용량이 잘 관리가 되는 것이겠네요.

사용해보지는 않았지만, 이제 앞으로는 tcmalloc을 이용하여 개발을 해야겠습니다. ^^

마지막으로, tcmalloc을 알게 해준 tc_set_new_mode() 함수에 대해 간단히 정리를 하고 넘어가야겠습니다.
이 함수의 주석은 다음과 같습니다.

" This function behaves similarly to MVSC's _set_new_mode.
   If flag is 0 (default), calls to malloc will behave normally.
   If flag is 1, calls to malloc will behave like calls to new,
   and the std_new_handler will be invoked on failure. "

아~ tc_set_new_mode(1)은 메모리 할당 실패 시, std_new_hander에 정의된 핸들러를 호출하라고 지시하는 함수네요.

근데,,, glibc, tcmalloc 모두 링크되는데 프로그램은 실행시 어떻게 tcmalloc의 malloc을 사용할까요???

2011년 11월 5일 토요일

LD_PRELOAD

API 후킹이라는 말을 많이 들어보셨을겁니다.

어떤 프로그램이 사용하는 API를 실행중에 가로채는 것을 말하는데요,
가로채서 그 API가 동작 못하게 할 수도 있고, 다른 동작을 수행하게 바꿀수도 있죠. 런타임에!

예를 들어, hostname 명령은 gethostname() 함수를 사용해서 현재 시스템의 호스트네임을 가져오고 있습니다.


제 노트북의 호스트네임이 출력이 되네요.

이제 API 후킹을 해볼까요?
런타임에 gethostname() 함수를 가로채서 다른 호스트네임을 출력하도록 바꿔보겠습니다.

gethostname() 함수는 libc에 정의되어 있습니다.


그렇다면 hostname 명령이 libc의 gethostname() 을 호출할 때, 새로 정의한 gethostname()이 호출되도록 해줘야 원하는 동작이 수행이 되겠네요.

hostname 명령은 libnsl.so 와 libc.so 에 의존하는 프로그램입니다.


gethostname()은 libc 에 정의되어 있습니다.
hostname  명령이 로더에 의해 로딩 될때 의존성을 가지는 libnsl, libc 라이브러리가 동적로딩이 될것인데,
그렇다면 어떻게 libc의 gethostname()이 아닌 다른 gethostname() 함수가 호출되게 할 수 있을까요?

우선 간단한 gethostname()을 작성해 봅시다.


이 소스를 이용해 gethostname.so 라이브러리를 생성합니다.

그리고 LD_PRELOAD 환경변수에 gethostname.so를 설정하고 hostname 명령을 실행 합니다.

와우~!! 어떻게 이게 가능한 걸까요?
strace 를 통해 hostname을 검사해 봅시다.


hostname 명령이 의존하는 라이브러리도 오픈하는게 보이네요.

open("/lib/i386-linux-gnu/libnsl.so.1", O_RDONLY) = 3
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3

앗 자세히 보니 preload 가 눈에 들어옵니다!!
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
그런데 파일이 존재하지 않는군요.

여기서 잠깐, LD_PRELOAD 와 /etc/ld.so.preload 는 무슨 차이가 있을까요.
loader의 source code를 보면 두가지에 대해 알 수 있습니다.
 
elf/rtld.c
로더는 LD_PRELOAD에 설정된 라이브러리를 먼저읽고, 그다음  /etc/ld.so.preload 파일에 기록된 라이브러리를 읽는 것을 알 수 있습니다.

다시 돌아와서, 다른 라이브러리가 open 되지 않는걸 보니,
hostname은 libc 내 정의된 gethostname() 을 이용하고 있는 것 같습니다.

그렇다면! LD_PRELOAD 환경변수를 설정한뒤 실행한 hostname은 어떻게 다를지 궁금하네요.

아하~ 다른 라이브러리보다 gethostname.so 라이브러리를 제일 먼저 읽네요.
그래서(?)  libc.so 가 아닌 gethostname.so 내 gethostname()이 호출될꺼라고 얼렁뚱땅 넘어가게되면 안되겠죠? ㅎㅎ

라이브러리 오픈한 뒤, read, fstat, mmap2 를 통해 라이브러리를 프로세스의 Address space에 매핑을 하는것 같습니다.
gethostname.so, libc.so 모두 로더에 의해 로딩이되는데, 어떻게 gethostname.so의 gethostname() 함수가 실행이 되는 걸까요?


어렵네요... 우선 LD_PRELOAD 에 대해 알아보았으니 다음 시간에 이 문제에 대해 정리를 해보겠습니다.

2011년 11월 4일 금요일

프로그램 국제화 도구 - Locale

소스코드의 시작 부분에서  setlocale() 을 종종 보게 됩니다.

예전엔 무심코 지나쳤었는데, 이젠 꼼꼼하고 디테일하게 하나씩 알아나가야 할 것 같네요.
(점점 위대하신 분의 영향을 받는것 같습니다 ㅎㅎ)

프로그램의 국제화 (Internationalization, i18n)은 프로그램이 어떤 환경(?)에서 수행할 지 선택할 수 있도록 하는 것인데요,

설명이 좀 애매합니다

예를들어 언어의 경우, 출력 시 어떤 언어를 출력할 것인가도 위에서 언급한 환경 중 하나의 종류가 되겠습니다.

이러한 종류를 Category라고 부르는데요, Category 는 다음과 같습니다.
















위 그림에서 ko_KR.UTF-8 은 로케일 값이구요, "locale -a"  명령을 통해 사용가능한 로케일의 이름을 알 수 있습니다.

각각의 Category에 로케일 값이 설정되어 있는데, LC_ALL에는 설정이 되어 있지 않네요.
LC_ALL 의 값을 설정하게 되면 LC_ALL의 값이 모든 다른 Category의 값으로 사용됩니다.
그리고 LC_ALL의 값을 없애면 다른 Category의 값들은 이전 값을 유지 합니다.

소스코드에서 setlocale(LC_ALL, "") 과 같은 코드를 사용하면, 시스템에 설정된 Category의 값을 그대로 이용하겠다는 의미가 되겠네요.

setlocale()을 호출하지 않으면 시스템에 설정된 Category의 값을 이용하지 않으므로  반드시 setlocale() 호출을 통해 프로그램에 locale 설정을 해야하겠습니다.

이 포스트는 아래의 링크를 참조하여 정리하였습니다.
자세한 설명이나 테스트 코드는 아래 링크를 참고하시면 됩니다.
로케일(Locale)에 관하여...

그리고 포스트에 틀린 내용이 있다면 코멘트 부탁드립니다!!