DEVIEW 2013 1일차 후기

나는 여태껏 개발자 관련 컨퍼런스에 별로 가보지 못했다. 하는 일이 이런 컨퍼런스에서 주제로 다룰 만큼 트랜디하거나 이 업계에서 주류에 속하는 분야가 아니었기 때문인지도 모르겠다. 이번에는 우연히 등록신청할 수 있어서(시작 10여분 만에 마감) 가 볼 수 있었다. 좀 더 이런 기회가 많았으면 좋겠다.

각설하고… 내가 들었던 세션들에 대해서 정리해볼까 한다. 간단한 개회사와 함께 세션 일곱개를 들을 수 있었고, 주제는 4개의 트랙을 통하여 웹, 모바일, 오픈소스, 기타 등으로 나뉘어져있었다. 나는 주로 모바일과 오픈소스 쪽을 들었다.

현재 발표된 세션의 슬라이드 자료와 동영상들이 어디 있는지 몰라서 그냥 기억나는대로 정리할 생각이다.

1 Session 1, Track 2 벡터기반 지하철 노선도 렌더링 엔진

네이버 직원이신 발표자께서 안드로이드용으로 서울 지하철 노선도를 어떻게 구현하고 있는지에 대한 내용이었다. 노선도 화면에 표시할 때 보통 크게 3가지 방식(하나의 비트맵, Titled 비트맵, 벡터)이 있는데 안드로이드 내에서의 파편화 뿐만 아니라, 아이폰, 일반 웹까지 다양한 크기의 화면에 대응하기 위해 벡터 방식을 선택했다고 했다.

벡터를 저장하는 여러 포맷이 있는데 그 중에 SVG를 선택했고, 이 포맷으로 디자이너가 파일을 만들면 자체적으로 가지고 있는 메타 데이터(역의 좌표와 이름 등)를 더하여 Canvas로 화면에 뿌린다고 했다.

문제는 모바일에서 그것이 WebView든 다른 브라우저 상이 든 Canvas 렌더링 속도가 너무 느려서 이것만을 사용할 수 없었다고 했다. 그래서 선택한 방법이 해당 화면 사이즈로 한번 Canvas로 그리고 그것을 비트맵 방식으로 캡쳐한 후 그것을 사용하는 것이라고 했다(이렇게 하려고 새로운 렌더링 클래스를 만들었다고 했는데 이름은 까먹었다).

내 입장에서는 안해본 쪽이라 예상보다 흥미는 없었지만, 최적화할 가능성이 더 있을 것 같아서 나에게 문제가 주어진다면 나름 흥미로울 수 있겠다는 생각을 했다.

2 Session 2, Track 2 Tizen – Universal Device Platform

삼성전자에서 나오선 발표자 두분께서 Tizen의 Web App Programming 부분에 대해 개략적으로 훑으시고, SDK를 통해 간단한 앱을 만드는 시연을 하셨다.

기억에 남는 부분은 Web App(JS 기반)에서 디바이스 제어를 지원해주는 정도에 대한 것이었다. 폰에서 사용하는 웬만한 기능들은 다 지원해주는 듯 했다. 물론 얼마나 충실히 구현되었는지는 API를 이용해서 직접 개발을 해봐야 알 수 있을 것이다.

내가 궁금했던 점은 보안에 대해 어느정도 신경쓰고 있는지에 관한 것인데, 잠깐 본다고 알 수 있는 부분은 아닌 것 같아 그냥 있었다.

듣고 난 후 든 생각은 Tizen이 개발자들을 확 끌리게 하는 그 무언가가 아직 없는 것 같다는 것이다. 네이티브 앱은 개발초기부터 지원했을테고, HTML5가 뜨니 웹기반 앱을 인터패이스를 급히 만든 것이 아닌가 하는 느낌을 지울 수 없었다.

3 Session 3, Track 3 당신의 인생에 오픈소스를 더하라 – OSCON 발표자 뒷담화

발표자 두분께서 오라일리가 매년 주최하는 오픈소스 컨퍼런스인 OSCON 에서 발표한 경험을 나누어주셨다.

동아시아 3국(한국, 중국, 일본)의 오픈소스 활동에 대한 전반적인 소개와 특징들을 설명했다고 한다. 한 분은 영어 회화가 되시고 한분은 주어진 스크립트를 읽는 수준이었다고 하는데, 열심히 발표준비를 하신 듯 보였다(발표자료).

비영어권 개발자가 영어권이 대부분인 오픈소스 커뮤니티에서 활동을 시작할 수 있는 방법이 참 궁금했는데, 발표자의 조언이 약간은 도움이 되었다. 영어가 안되다면, 되도록 원서를 읽고 IT관련 팟캐스트가 많이 있는데 그걸로 듣기 연습하여 영어 능력을 키우고, 관심이 가는 프로젝트를 골라 기능을 사용해보면서 코드도 같이 보고, 그러다가 작은 것이라도 고칠 부분이 나오면 수정해서 패치를 메일로 보내라는 것이었다. 그렇게 꾸준히 하다보면 커뮤니티에 들어갈 기회가 생긴다고…

역시 행동이 어려운 문제다. 다시 한번 헛짓 하지말고 목표를 세워서 하나씩 이루어 가야겠다는 굳은 결심을… 해본다. ㅜㅜ

4 Session 4, Track 3 나는 왜 개발자인데 자신이 없을까?

NHN의 교육기관인 NHN NEXT의 학장이신 발표자께서 제목 그대로 재미있게 강연해주셨다.

발표 대상이 소프트웨어를 전공한 대학생이거나 갓 졸업한 사회초년생들인 것 같다는 생각이 들었지만, 나에게도 도움되는 것이 많았다.

우선 개발자이면서 자신감을 가지지 못하는 이유는 지식만 쌓았지 그것을 기반으로 무언가 이루어 본 경험이 많지 않기 때문이라고 했다. 그러면서 예를 든 사진이 끝이 잘 보이지 않는 계단 사진이었는데, 자신이 없는 사람은 그 계단 아래에서 좌우로 왔다갔다만 한다는 것이다. 즉 자신감이 생기려면 위로 한 계단씩 걸어올라가야 한다는 것이다. 참 적절한 비유라고 생각했다. 올라가지 않고 밑에서 자꾸 왔다갔다 하는 모습, 내가 딱 이꼴이구나 싶었다.

그러면서 당장 취할 수 있는 행동은 대단한 것이 아니더라도 필요한 것을 만들어보는 것이라고 했다. 나도 이런 생각을 안한 것은 아닌데, 항상 끈기가 부족한 것이 실패의 원인인 듯 하다.

또 자신만의 뭔가를 만들어서, 다른 사람들이 쓸 수 있게 공개하고, 그것을 실제 여러사람이 쓰며, 그 사람들로부터 피드백을 받는 과정이 자신에게 얼마나 큰 기쁨과 성장을 주는지 모른다고 했다.

강의를 들으면서 만들다가 쉬고있는 앱이라도 얼른 다 만들어야겠다는 생각을 했다.

슬라이드 자료

5 Session 5, Track 2 WebView 뛰어 넘기 – 고성능 WebView 만들기

SK 플래닛의 개발자께서 안드로이드의 시원찮은 WebView 성능을 어떻게 보완하여 앱개발을 하는지에 대해 발표하셨다.

세션 7에서 들은 바로는 성능이 개선된 새로운 WebView가 조만간 나올꺼라는데, 그건 그거고 시중에서 쓰이는 대부분의 단말을 지원해야하는 앱개발 입장에서는 GB같이 오래된(?) 버전에서도 동일하게 보여줘야 할 필요가 있긴 있다.

관련 일을 한다면 재미있는 주제다.

하지만 나는 졸다가 하나도 못들었다. 아깝…

6 Session 6, Track 2 Firefox OS – Fulfilling the promises of HTML5

모질라 재단의 계신 외국분이 행오버로 온라인 발표를 하셨다. 영어라 잘은 못알아들었지만, 파폭 OS가 가진 장점들 몇가지는 알 수 있었다.

HTML5로만 앱을 만들기 때문에 쉽다고 하는데, 사실 이게 또 기존의 모바일 개발자 입장에서는 새로운 진입장벽이 될 수 있다고 본다.

암튼 나도 동향을 관심있게 지켜보고 있는 플랫폼인데 앞으로 좀 더 시간을 투자해봐야겠다 싶다.

7 Session 7, Track 4 Mobile Browser Internals & Trends

Naver의 웹엔진 관련 랩에서 두 분이 나오셔서 발표하셨다. 오늘 들은 것 중에 제일 유익한 강연이었던 것 같다.

현재 WebKit 관련하여 구글과 애플 사이에서 일어나고 있는 일들(애플은 WebKit2 개발하고 구글은 Blink라는 이름으로 새로 포킹함)과 그로 인한 Chromium의 개발 현황, 그리고 안드로이드의 WebView 컴포넌트 관련 내용(새로운 엔진인 Blink 기반으로 완전히 바뀔 것이라고 함)을 들을 수 있었다. 추가로 게코엔진 얘기도 조금하셨고…

재미있었던 부분은 Chromium의 멀티프로세스 아키텍처에서 컴포넌트들이 어떻게 나뉘어져 있고 왜 그렇게 나눴는지에 대한 이야기였다.

8 끝맺음

2일차 세션들은 신청에 실패해서 들을 수 없다(그냥 몰래 가서 들어도 뭐라하지는 않을 듯 한데). 암튼 오늘 들은 것만으로도 많은 도움이 되었다. 이런 기회가 자주 있어서 자극을 많이 받을 수 있었으면 좋겠다.

또 배터리 오래가는 노트북을 빨리 하나 마련해야겠다. 그래야 이거 org-mode로 메모를 하지. 기억력도 안좋은데 기억해내려니 머리가 아프다(어쩌면 치매예방에 도움이 될지도).

끝.

야크 털깍기

‘야크 털깍기(Yak Shaving)’란 이 글에서 보듯이 원래 목적한 일을 시작했다가, 거기에 필요로 하는 다른 무언가의 부재 때문에 우선 그것과 관련된 일을 먼저 하게 되고, 또 그것을 하려다보니 또 다른 무언가가 필요한 것 같아 하던 일을 놔두고 새로운 일을 시작하는 식으로, 재귀적으로 일의 밑바닥까지 파고 드는 어떤 패턴을 말한다. 그러니까 원래 나무를 베려고 했는데, 어찌하다보니 야크 털을 깍고 있더란 이야기.

이런 경향은 많은 사람들에게서 나타나는 것 같다. 물론 나도 마찬가지다. 아니 더 심하다. 원래 하려고 했던 일을 끝내 못하는 경우도 많고 원래의 목적을 까먹는 경우도 비일비재하다.

만약 시간이 무한정 주어지고 그 동안 다른 것에 집중력이나 흥미가 뺏기지 않는다는 보장이 선다면, ‘야크 털깍기’와 같은 문제는 오히려 자신의 열정을 쏟아부어 뭔가 의미있는 결과를 만들 수 있는 좋은 계기가 될 수 있을 것이다. 하지만 사람이 어떻게 그럴 수 있겠는가. 하루의 아침과 저녁 기분만 해도 하늘과 땅만큼 다를 수 있는데…

물론 원래 목적했던 일을 이루기 위해 ‘야크 털깍기’까지 하게 되는 과정은 흥미로운 부분이 있을 수도 있고, 새로운 지적인 호기심을 발동시키기도 한다. 도끼날을 갈아야 해서 돌을 구하다 보니 어떤 돌이 좋은 돌인지 알고 싶어질 수도 있고, 더 나가서 돌보다 더 좋은 재료를 찾아나설 수도 있을 것이다. 그렇지만 그럴 경우 원래 목적하던 바를 잃게 될 것은 자명하다.

이 글을 쓰면서도 몇번이나 다른 딴짓을 하고 있다. 주의력 겹핍일 수도 있겠고, 주제가 뚜렷하지 않아서 그런지도 모르겠다. 암튼 중요한 것은 ‘야크 떨깍기’가 목적한 일의 성취에 있어서 많은 장애가 된다는 것이다.

그래서 몇가지 원칙을 생각해 보았다.

  • 목적하는 일이 너무 깊은 depth로 내려가지 않도록 하자. 경우에 따라서는 순간적으로 드는 의문을 무시하자.
  • 순간적으로 드는 의문은 따로 ‘해야할 목록’을 만들어 관리하자.
  • 만약 목적하는 일을 성취함에 있어 깊은 depth로 내려갈 수 밖에 없는 상황이라면 내가 아직 그 일을 해결할 수 있는 수준이 아닐수도 있다는 것을 명심하자.
  • 일의 depth가 깊어지는 이유 중에 가장 큰 것은 더 잘 알아서 더 좋은 효율을 내고 싶어하는 욕구 때문이다. 하지만 도구로써 활용될 그 일이 다음에 몇번 더 활용될지 생각하자. 만약 다음에도 많이 사용될 가능성이 높다면 해당 depth의 일은 해야할 것이다. 하지만 몇번 사용하지 않을 것 같다면 조금 불편하고 불완전 할 수 있겠지만 그냥 아는(혹은 있는) 그대로 사용하자.
  • 완벽해지려고 하지말자.
  • 참고로 이 글에도 몇가지 방법을 제시하고 있다.

org2blog 이용하여 WordPress에 글쓰기

이 블로그에 글을 쓸 때, 항상 org-mode를 사용하긴 했다. 글을 쓰고 그 글을 html 형식으로 exporting 한 다음에 Copy/Paste로 포스팅했다. 이 방법도 그렇게 나쁘지는 않지만 좀 번거로웠다. Exporting된 html 결과물 전체를 붙여넣으면 형식이 깨져서 특정 영역만 복사해서 붙여넣어야 하고, 태그를 직접수정하기도 해야 했다.

좀 더 쉬운 방법이 없을까 찾아보다가. org2blog 라는 패키지를 찾았다!!! org-mode에서 작성한 글을 명령어 하나로 자신의 블로그에 포스팅가능하게 하는 패키지다.

설치에 관한 구체적인 내용은 사이트에 README 파일로 있으니 참고하자.

그리고 자신의 WordPress 사이트에 글을 올리려면 로그인을 해야한다. 이 부분에 대한 설정이 필요하다. org2blog/wp-blog-alist 변수에 값을 채워넣으면 된다.

(setq org2blog/wp-blog-alist
      '(("wordpress"
         :url "http://username.wordpress.com/xmlrpc.php"
         :username "username"
         :default-title "Hello World"
         :default-categories ("org2blog" "emacs")
         :tags-as-categories nil)
        ("my-blog"
         :url "http://username.server.com/xmlrpc.php"
          :username "admin")))

이제 새로운 글을 써보자. 시작하려면 다음 명령어를 써보자. M-x org2blog/wp-new-entry. 이 명령어를 실행해보면 미니버퍼에 자신의 블로그 로그인 과정이 진행된다. 이 과정이 끝나면 글을 쓸 수 있는 템플릿 버퍼가 뜬다.

글을 쓰다가 버퍼에 글이 어떻게 보이는지 확인하고 싶으면 deft로 포스트해보면 된다. 기본 명령어는 C-c d (M-x org2blog/wp-post-buffer) 이다. 글 수정이 다 되었다면 C-c p (C-u M-x org2blog/wp-post-buffer)로 글을 올려보자.

전반적으로 쓸만하다. 80행 줄넘기기를 해놔도 실제 포스티에서는 문단별로 글이 잘 이어지니 편하다. 다만 아직 필요한 몇몇 기능들을 잘 쓸 수 있는지 정확히 확인해보지 않았다. 이미지 첨부, 소스코드 syntax highlight, 표 꾸미기 등을 더 알아봐야 할 듯. 그리고 글에 소제목을 추가하려니 Invalid function: org-with-silent-modifications 라는 에러메시지가 나오며 포스팅이 안된다.

이 포스팅은 실험적으로 써본다. 이 포스팅의 org 파일은 여기에 예제로 올려봤다.

리눅스 커널의 연결리스트 정리

1 머리말

리눅스 커널에서 제공하는 자료구조 중에 양방향/순환 연결리스트로 구현된 <linux/list.h> 를 정리하고자 한다. 이것은 커널 소스 전반에 사용됨은 물론이고, 제너릭하면서 포터블하고 에또… 유니크하며 엘레강스하다. ㅎㅎㅎ

 

2 일반적인 연결리스트의 구현

보통 제너릭한 연결리스트를 구현하는 방법은 아래와 같은 코드를 크게 벗어나지 않는다.

struct generic_list {
void *data;
struct generic_list *prev, *next;
};

구조체 멤버인 data 포인터 변수에 자신이 원하는 데이타를 assign 하는 것이다. 하지만 이런 방식으로 구현할 경우 의외로 번거로움이 많다. 리스트에 노드 하나를 추가한다고 가정해보라. 우선 struct generic_list 부터 동적으로 생성해야 할 것이고, data 포인터에 assign 할 어떤 데이타도 동적으로 생성해야 할 가능성이 높다. 노드 하나 생성에 두번의 malloc은 번거로울 뿐만 아니라 속도에도 나쁜 영향을 끼친다. 게다가 리스트에서 하나의 노드를 빼낼 때도 반대의 과정을 거쳐야 한다.

 

3 <linux/list.h> 의 구현

리눅스 커널에서 연결리스트를 구현하는 접근방식은 위와 같은 일반적인 방식이 아니라 생각을 전환해서 그 반대로 구현했다. 무슨 말인고 하니 리스트 노드를 데이타 안에 넣는다는 것 이다. 이해를 돕기위해 아래 그림을 보자.

https://i0.wp.com/www.makelinux.net/ldd3/images/0596005903/figs/ldr3_1101.gif

list_head 관련 자료구조

struct list_head 가 리스트 노드인데 이것을 사용자 데이타 안에 박아넣었다. 이것을 코드로 표현해보면,

/* in include/linux/types.h */
struct list_head {
struct list_head *next, *prev;
};

struct list_head 정의이고,

struct album_struct {
    char* artist;
    char* title;
    /* ... other fields ... */
    struct list_head list;
};

사용자 데이타로 음반 정보를 저장하는 구조체가 있다고 가정할 때 대략 위와 같이 정의할 수 있을 것이다. 마지막 list 멤버는 해당 구조체를 linked list의 node로 사용하기 위해 정의한 것이다. 즉 연결리스트로 만들고 싶은 어떤 구조체가 있다면, struct list_head 타입의 변수를 하나 삽입하면 된다는 뜻이다.

struct list_head 를 어떤 구조체에 넣을 때 다음을 참고하면 된다.

  1. struct list_head 의 삽입 순서는 상관없다. 아무 곳에나 넣어도 된다.
  2. struct list_head 의 변수명은 원하는대로 지어도 된다.
  3. 필요한 경우, 한 구조체에 여러개의 struct list_head 를 삽입해도 관계없다!

3.1 엔트리 접근방식

그래도 아직 이해가지 않는 것이 있다. 양방향으로 연결하는 포인터만 가진 구조체(struct list_head)로 어떻게 부모 구조체(struct album_struct)의 멤버에 접근할 수 있을까? 진정 쓸모 있으려면 자신의 데이타에 접근할 수 있어야 하지 않나?

커널 개발자들은 이 문제의 해결책을 영리하게도 C언어의 트릭에서 찾았다! 바로 구조체 안의 멤버들 간의 오프셋은 컴파일 시기에 고정된다는 점을 이용한 것이다. 아래 container_of() 매크로는 이 원리를 이용해 부모 구조체의 주소를 쉽게 찾아준다.

/* in include/kernel.h */
#define container_of(ptr, type, member) ({                          \
            const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
            (type *)( (char *)__mptr - offsetof(type,member) );})

/* in include/linux/stddef.h */
#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
#endif

위 코드가 잘 읽히지 않는다면 실제 값으로 대입하여 확인해보면 이해하기 쉬워진다. 예시의 코드를 적용해보자. 인자로 들어가는 세 값은 다음과 같다.

ptr
멤버의 실제 포인터. 여기서는 struct list_head 의 포인터
type
ptr 을 포함하고 있는 부모 구조체의 타입. 여기서는 struct album_struct
member
부모 구조체에 포함된 struct list_head 멤버명. 여기서는 list

이 값들을 대입을 해보면

container_of(ptr, struct album_struct, list) ==>
(struct album_struct *)((char *)ptr) - ((size_t) &((struct album_struct *)0)->list)

와 같이 된다. 즉 실제 메모리를 가리키고 있을 struct list_head 포인터 타입의 ptr 에서 부모 구조체의 주소까지의 오프셋을 구하여 빼면 실제 부모 구조체의 주소를 도출할 수 있다.

그래도 이해가지 않으면 offset 테스트 코드를 돌려보자.

[gist 6363192]

 

4 사용법

그러면 이제 어떻게 사용하는지 알아보자. 간단해서 몇번 써보면 금방 익힐 수 있다.

4.1 리스트 선언 및 초기화

4.1.1 리스트 노드 선언 및 초기화

리스트는 사용하기 전에 초기화가 되어야한다. 왜냐하면 리스트의 각 항목이 동적으로 만들어지기 때문인데, 연결리스트를 초기화하는 일반적인 길은 아래 루틴과 같다.

struct album_struct *cold_fact;
cold_fact = kmalloc(sizeof(*cold_fact), GFP_KERNEL);
cold_fact->artist = "Rodriguez";
cold_fact->title = "Cold Fact";
/* ... initialize others */
INIT_LIST_HEAD(&cold_fact->list);

만약 구조체를 컴파일 때 정적(static)으로 만들고자 하면다면, 직접적인 참조(direct reference)로 만들면 된다. 간단하게 아래와 같이 한다.

struct album_struct cold_fact = {
    .artist = "Rodriguez",
    .title = "Cold Fact",
    /* ... */
    .list = LIST_HEAD_INIT(cold_fact.list)
};

4.1.2 Head 포인터 선언하기

우리가 사용할 각 노드는 위와 같이 초기화하면 되지만, 그것을 어디다 연결시켜야 할까? 시작점이 있어야 한다. 바로 연결리스트의 head 포인터이다.

위의 그림 중 오른쪽 첫번째 (스스로를 가리키고 있는 “An empty list”) list head 가 바로 시작점이다. 코드로는 이렇다.

static LIST_HEAD(album_list);

위 코드에서 LIST_HEAD 매크로는 album_list 란 이름의 list_head 를 정의하고 초기화 시킨다.

4.2 연결리스트 수정하기

연결리스트에 노드를 추가/삭제 같은 일을 할 수 있도록 일련의 함수를 제공한다. 함수들은 struct list_head 만을 다루도록 되어있다. 여기에 속하는 함수들의 복잡도는 O(1) 이다.

4.2.1 추가

연결리스트에 노드를 추가하기 위해서는 다음 함수를 사용한다.

list_add(struct list_head *new, struct list_head *head)

이 함수는 new 노드를 주어진 리스트의 head 노드 다음에 추가한다. 왜냐하면 리스트는 원형이라 일반적으로 firstlast 노드라는 컨셉을 갖고 있지 않기 때문인데, head 로 어떤 항목이든 넘길 수 있다. 그러나, 만약 “last” 항목을 head 로 넘긴다면, 이 함수는 스택을 구현하는데 사용될 수 있다.

위 예시로 돌아가서, album_list 에 추가하길 원하는 새로운 struct album_struct 구조체가 있다고 치자. 이렇게 하면 된다.

list_add(&cold_fact->list, &album_list);

연결리스트의 마지막에 새로운 노드를 추가하려면 이 함수를 사용한다.

list_add_tail (struct list_head *new, struct list_head *head);

이 함수는 주어진 리스트에 new 노드를 head 노드 전에다 추가한다. list_add() 와 같이 리스트는 원형이기 때문에, head 에 어떤 항목이든 넘길 수 있다. 그러나, 이 함수는 “first” 항목을 넘긴다면, 큐를 만드는데 사용할 수 있다.

4.2.2 삭제

연결리스트에 노드를 추가한 후에, 리스트에서 노드를 지우는 것은 다음으로 가장 중요한 작업이다. 연결리스트로 부터 노드를 지우기 위해서는, list_del() 을 사용하라.

list_del(struct list_head *entry);

이 함수는 리스트로 부터 entry 항목을 삭제한다. 이 함수는 entry 를 가진 메모리나 이것을 포함하는 자료구조의 메모리를 해제시키지 않는다고, 단순히 리스트로 부터 이 항목을 삭제한다. 이 함수로 특정 노드를 리스트로 부터 빼낸 후 용도에 따라 사용하고는 동적으로 할당한 메모리가 있다면 해제시켜야 한다.

예를 들어 위의 album_list 에 추가한 album_struct 노드를 삭제하기 위해서는 다음과 같이 한다.

list_del(&cold_fact->list);

이 함수는 album_list head 포인터를 인자로 받지 않는데, 이 노드의 앞과 뒤에 있는 노드의 포인터를 수정한다. 구현은 직관적이다.

static inline void __list_del(struct list_head *prev, struct list_head *next)
{
next->prev = prev;
prev->next = next;
}

static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}

연결리스트로 부터 한 노드를 지우고 그것을 재초기화 까지 하려면 다음 함수를 쓰면 된다.

list_del_init(struct list_head *entry);

이 함수는 list_del() 과 같은 방식으로 동작하긴 하지만, 차이점은 리스트에서 빼낸 이 항목을 더 이상 사용하지 않는다는 이유를 가지고 주어진 list_head 를 재초기화 한다는 것이다. 하지만 자료구조 자체는 재사용할 수 있다.

4.2.3 그밖의 함수들

어떤 리스트에서 다른 리스트로 노드 하나를 옮기기 위해서는 다음 함수를 사용한다.

list_move(struct list_head *list, struct list_head *head);

이 함수는 list 항목을 이것을 포함하는 연결리스트로 부터 삭제하고 주어진 리스트의 head 다음에 이 항목을 추가한다.

어떤 리스트로 부터 노드 하나를 다른 리스트의 끝에 추가하기 위해서는 다음 함수를 사용한다.

list_move_tail(struct list_head *list, struct list_head *head);

이 함수는 list_move() 와 같은 방식으로 동작하지만, list 항목을 head 항목 앞에 삽입하는 것이 다르다.

리스트가 비어있는지 확인하려면 다음 함수를 사용한다.

list_empty(struct list_head *head);

이 함수는 리스트가 비어 있으면 nonezero 를 리턴하고, 그렇지 않을 경우에는 zero 를 리턴한다.

연결되지 않은 두개의 리스트를 함께 연결하려면 다음 함수를 사용한다.

list_splice(struct list_head *list, struct list_head *head);

이 함수는 list 가 가리키는 리스트를 head 항목 다음에 삽입함으로써 두개의 리스트를 합친다.

4.3 연결리스트 순회하기

리스트에 노드를 삽입/삭제/이동/접합 등의 수정 기능은 list_head 구조체만으로 이루어진 연산이었다. 이제 본격적으로 자신의 데이타를 건드릴 차례다. 복잡도와 관련해서는 리스트 수정 루틴들과는 다르게 n개의 엔트리에 대해 O(n)의 복잡도를 가진다.

4.3.1 순방향 순회

위에서 알아본 container_of() 이 순회할 때 쓰인다. 그런데 list.h 에서는 다시 한번 매크로를 써서 문맥에 맞게 이름을 고쳐쓴다. 바로 list_entry() 인데 인자는 똑같다.

자 그럼 순방향으로 순회하는 루프를 만들어보자. 이것도 매크로가 있다.

struct list_head *p;
struct album_struct *a;

list_for_each(p, album_list) {
    /* a 는 리스트를 포함하고 있는 구조체를 가리킨다. */
    a = list_entry(p, struct album_struct, list);

    /* struct album_struct 타입인 각 엔트리 a를
     * 필요에 따라 쓰면 된다. */
}

위 코드가 기본이다. 그런데 더 간단하면서 실용성있는 매크로가 있다.

struct album_struct *a;

list_for_echo_entry(a, &album_list, list) {
/* 각 iteration 마다 a는 album_struct
* 타입의 엔트리를 가리킨다. */
}

4.3.2 역방향 순회

복잡도가 O(n)이기 때문에 경우에 따라서는 앞에서가 아니라 뒤에서 시작하여 순회할 필요도 있다. list_for_each_entry_reverse(pos, head, member) 를 사용하면 되는데, 확실한 이유가 없는 한 왠만하면 쓰지말라고 한다.

5 끝맺음

아… 이것도 정리하려니 오래 걸린다. 암튼 커널 코드를 보려고 한다면 가장 기본적인 자료구조이기 때문에 잘 알고 있어야 하고 잘 쓸 수도 있어야 한다.

코드를 약간 수정하는 정도로 리눅스가 아닌 다른 C 프로그램(VC6, ARM 컴파일러)에 사용해본 적이 있는데 약간의 수정만으로도 잘 쓸 수가 있었다. 이런 식의 적용을 많이들 하는 듯 하다.

발상의 전환이라고 해야할까… 이 코드를 처음 접했을 때는 그런 것을 느꼈었다. 정말 멋진 코드라고 생각한다.

6 참고문헌

  1. Love, Robert, Linux Kerenl Development 3rd, Addison-Wesley, 2010, p.88.
  2. Corbet, Jonathan and others, Linux Device Drivers 3rd, O’Reilly, 2005, p.295.
  3. Linux Kernel Linked List Explained, http://isis.poly.edu/kulesh/stuff/src/klist/

Android application에서 memory leak 관리하기

1 Intro

일전에 어떤 기업에서 기술면접을 본 적이 있다. 그 때 면접관이 물어봤던 물음 중에 하나가 “안드로이드 플랫폼에서 메모리 누수가 일어나는 경우와 그것을 어떻게 관리할지”에 대한 것이었다. 당시에는 솔직히 잘 몰라서 아주 원론적인 답만 했다. 그게 한동안 계속 기억에 남아서 날 찜찜하게 만들었다.

메모리 누수 문제는 어떤 언어나 플랫폼을 쓰던 꼭 집고 넘어가야 할 문제이다. 나의 경우엔 C와 같이 프로그래머가 직접 메모리를 관리하는 경우에만 익숙해져 있다보니 자동으로 메모리를 관리한다는 Java 같은 언어에서는 어떤 문제가 발생하는지 정확히 알지 못했다.

여담이지만 특정기능을 시스템 차원에서 지원한다는 것이 마냥 좋지만은 않은 것 같다. 뭔가가 ‘자동’으로 이루어진다는 말은 그걸 쓰는 사람이 필요에 따라 그 기능에 직접 개입할 수 없다는 뜻도 되기 때문이다. 설사 직접 개입할 수 있는 길이 있다 하더라도 보통 그런 접근법은 권장되지 않는다. 그 기능의 작동 플로우에 녹아들어 같이 휩쓸려가는 방식이 권장된다. 이런 관계로 자동으로 이루어지는 그 작동 방식을 알아야한다. 모르면 잘못 사용할 가능성이 높다는 것이다. Java의 Garbage Collection도 딱 이런 경우인데, C에서 직접 메모리를 관리하면서 주의해야할 부분과는 또 다른 방식으로 신경을 쓸 수 밖에 없는 것 같다.

암튼 모순적이게도 Java 세상에서도 프로그래머가 직접 메모리 관리에 대해 신경써야 하는 상황이 있으니 그에 대해 한번 정리해보고자 한다.

2 Java에서의 메모리 누수

원론적으로 말하자면 Java의 Garbage Collector(이하 GC)가 제대로 작동할 겨우 메모리 누수가 생길 수 없다. 왜냐하면 GC가 특정 조건을 trigger로 작동하면서 아무도 참조하지 않는 (힙의 특정영역을 점유하고 있을)객체는 회수하기 때문이다1. 반대로 말하자면 회수가 안된 객체는 누군가에 의해 분명 참조되고 있다는 뜻이다. 만약 룰이 깨진다면 그것은 해당 JVM 구현의 버그일 것이다.

그렇다면 Java에서 메모리 누수란 무엇일까? 위의 전제에서 힌트가 있다. 즉 누군가에 의해 참조되고 있는 그 객체가 진정 쓸모있냐 없냐에 달린 것이다. 쓸모없는 객체를 계속 누군가가 참조를 하고 있다면 그게 바로 메모리 누수이다.

Stackoverflow에 있는 재미있는 쓰레드에 보면 Java에서 쓸모없는 객체를 미친듯이 생성시켜 Out Of Memory Exception을 일으키는 여러 경우에 대한 예시가 나온다.

3 Android에서의 메모리 누수

안드로이드에서는 일반적인 JVM과는 달리 프로세스 하나마다 dalvik(JVM)이 하나씩 실행되기 때문에, JVM 하나에 다중 프로세스 때문에 생기는 문제에 대해 신경쓸 필요는 없어서 그나마 좀 간편하다고 하겠다2. 하지만 플랫폼의 구조적인 결함이나 버그로 인한 메모리 누수가 몇가지 알려져 있다. 알아보도록 하자.

3.1 Context 관련

이 문제는 안드로이드 공식 블로그 글에 원인과 해결책이 잘 나와있다. 간단히 말하면 일정한 생명주기를 가지는 Activity 내에서 View 를 생성할 때 Context 즉 해당 Activity 자체를 인자로 넘기는데, 화면 방향전환과 같이 Activity 재생성이 일어나는 경우 관련된 모든 객체들의 참조를 끊지 않으면 Activity 객체가 사용하던 메모리가 회수되지 않아 누수가 발생한다는 것이다.

이 문제는 플랫폼 설계상 어쩔 수 없이 생길 수 있는 것인 듯 하다. 플랫폼 구조를 뜯어고치지 않는 이상 응용프로그래머가 주의하여 프로그래밍 할 수 밖에 없는 상황이라 하겠다.

그럼 해결 방법은 무엇일까? 몇가지가 있다.

  • Activity Context 대신 Application Context를 사용할 것을 고려하라. Application Context 객체는 Application 단위의 생명주기를 가진다.
  • Activity Context를 Activity에서 쓸 경우, Activity 생명주기와 싱크를 맞추어라.
  • 만일 Activity의 sub-class를 정의하여 사용한다면, sub-class에서 상위 Activity를 참조하는 경우를 되도록 만들지 마라. 대신 sub-class를 static으로 정의하고 해당 클래스가 Activity와 WeakReference 를 갖도록 하라.

3.2 Bitmap 관련3

안드로이드 버전 2.3.3(API Level 10) 이전의 경우 Bitmap 의 픽셀 데이타를 native heap에 저장했다고 한다. 이로 인해서 Bitmap 객체가 GC를 통해 메모리 회수되지 않고 자꾸 쌓여서 OOM Exception을 일으키는 경우이다. 어떻게 보면 말도 안되는 일인데, 이를 해결하기 위해서 Bitmaprecycle() 4이란 함수를 호출하라고 친철히 설명하고 있다. 최근 버전들은 이 문제가 해결되어(native heap을 쓰지않고 Dalvik VM heap으로 바꾸었다함) 참조변수를 null 로 바꾸기만 해도 충분하다고 한다.

3.3 기타

그 외에도 WebView5 나 MapView6 등에서 누수가 일어나는데 딱히 해결책이 없는 모양이다. 그나마 누수의 크기가 크지 않아서 조심하면서 쓰는 수 밖에 없단다.(뭐 이래~!)

4 마무리

Memory Leak이란 결국 해당 프로그램이 메모리(heap) 부족으로 원하는 시간만큼 실행되지 못할 때 문제가 된다. 잠재적인 위험을 가지고 있지만, 즉 누수가 조금씩 일어나지만 메모리 부족이 생기기 이전에 실행이 끝나고 프로세스가 정상적으로 죽는 경우라면 사실 문제될 이유가 없다. 하지만 그렇게 제한적인 프로그램이 활용도가 높을리는 없을꺼다. 따라서 프로그래머는 메모리 누수를 신경써야 한다. 아주 많이.

5 References

Footnotes:

1 더 정확히 말하면 레퍼런스의 Root Set에 해당객체를 직접적 혹은 간접적으로 참조하는 변수가 없다면 그 객체는 garbage collecting 시에 회수된다.

2 시스템의 아래로 내려가면 결국은 Linux 프로세스 위에 dalvik이 하나씩 떠 있는 상황이라 특정 프로세스에서 메모리릭이 발생하여 그 프로세스가 죽는다고 해도 다른 프로세스에는 영향이 없다.

3 Issue 8488: ‘bitmap size exceeds VM budget’ if Activity is restarted {includes test demo!}

4 이 함수는 Bitmap 이 객체화 될때 native heap에 할당한 픽셀 데이타를 free 시킨다.

5 http://stackoverflow.com/questions/3130654/memory-leak-in-webview

6 Issue 2181: Memory leak in system when using MapView

systemd 관련

1 출발점

며칠 전에 내 젠투 portage를 업데이트 했더니 Gnome 3.8이 올라와 있었다(ACCEPT_KEYWORDS~amd64). 얼씨구나 드디어 기다리던게 왔구나 하고 시스템을 업그레이드 하려니 emerge 가 systemd와 consolekit를 같이 설치할 수 없다는 에러메시지를 뿌린다. 둘 중 하나를 선택해야 한단다.

웹서핑을 약간 해보니 Gnome 3가 systemd를 쓰게 되었다는 얘기가 얼핏 보이고, consolekit는 더이상 관리되지 않기 때문에 안쓰는게 좋다는 얘기가 보였다. 더구나 현재 내 젠투에서 systemd을 설치하지 않으면 Gnome 3.8로 업그레이드 하는 것을 더 이상 진행할 수 없기도 하거니와 이미 3.6 패키지들을 막 지운 상태여서, 이렇게 된 이상 청와대 아니 systemd로 가야 했다.

문제는 이 systemd 가 기존의 init 시스템인 OpenRC 를 대체한다는 점이다. 즉 시스템의 부팅을 관장하는 프로그램이 바뀌는 것이 때문에 무턱대로 설치할 수도 없는 노릇이었다. 그래서 우선 systemd 로 왜 바꾸는지, 그리고 이게 도데체 무엇인지 좀 알아봤다.

2 이건 또 뭐야?

systemd는 오랫동안 사용되어 온 shell script 기반의 init 시스템을 대체하기 위해 만들어졌다. 가장 큰 특징은 부팅시에 띄워야 하는 서비스 수를 최대한 줄이고, 서비스 간의 의존성을 없애서 서비스들을 병렬로 실행하게 함으로써 부팅속도를 획기적으로 개선했다는 점이다. 가령 bluetoothd 와 같은 daemon은 블루투스 동글이 접속되지 않은 이상 꼭 떠야할 필요는 없다. 즉 그것이 필요할 때까지 최대한 기다려서 띄우는 방식이라 꼭 부팅 때 실행할 필요가 없게 만든 것이다. 또 서비스 간의 의존성이란, 예를 들어 D-Bus가 실행되려면 로그시스템인 syslog가 먼저 준비되어 있어야 하는 경우처럼 순차적으로 꼭 실행해야하는 것을 의미한다. 하지만 systemd는 이 부분을 각 서비스가 의존하고 있는 선행 서비스에서 꼭 필요한 부분이 그 서비스 전체가 아니라 소켓이라는 사실을 이용하여 해당 소켓을 먼저 생성해 두고, 경우에 따라 들어오는 시그널은 소켓 버퍼에 쌓아두었다가 해당 서비스가 준비 완료되면 그것을 처리하게 해주는 방식으로 의존성을 낮추었다고 한다.

암튼 이런 기발한 방법으로 시스템의 부팅속도를 높였다. 이런 장점 외에도 대부분 c로 구현되어 있어서, 실행중 사용하는 다른 유틸리티들 때문에 수시로 포크가 일어나는 shell script 기반의 실행속도 보다 훨씬 빠르다고 한다. 이런 저런 이유로 최근 배포판들의 대세가 된 듯한데…

이거 반대하는 쪽도 꽤 있나보다. systemd 메인개발자 중 한명이 PulseAudio 개발자인데 이걸 싫어하는 사람들이 systemd도 싫어하는 듯 하다(나는 PulseAudio의 내력을 잘 몰라서 뭐가 문제인지 모르겠다). 그런데 그들의 주장 또한 일리가 있는 듯 한다. 우선 systemd가 이식성이 낮아서 리눅스에만 적용할 수 있다는 점이다. Debian의 경우 커널을 리눅스 뿐만 아니라 FreeBSD나 Hurd 버전도 있기 때문에 systemd을 default로 적용하기 어렵다고 한다. 또 다른 이유로는 소켓 preallocation 관련하여 각 데몬들에 패치를 적용해야한다는 점이다.

3 그럼 설치는?

우선 내 젠투에는 consolekit와 충돌을 없애야 했다. 그래서 사용한 방법은 강제로 consolekitUSE flag를 제거하고 systemd 를 추가하는 것이었다.

# in the /etc/portage/profile/use.force
-consolekit

와 같이 하고,

# in the /etc/portage/make.conf
USE="${USE} -consolekit systemd"

한 다음,

emerge --ask --changed-use --deep world

를 실행해 준다.

이 와중에 emerge --depcleanrevdep-rebuild 를 번갈아 가면 몇번 더 실행했다.

암튼 시스템이 정상적으로 설치가 되었다면 systemd도 설치가 되었을 것이다. systemd가 설치된 디렉토리는 대충 다음과 같다.

/usr/lib/systemd 혹은 /usr/lib64/systemd
service나 target 같은 기본 설정파일들이 들어있다. 젠투에서만 /usr 아래로 가 있고, 기본적으로는 /lib/systemd 이다.
/bin/systemd
init으로 실행되는 파일이다. 젠투에서는 /usr/lib/systemd/systemd 의 심볼릭 링크이다.
/etc/systemd
만약 여기에 특정 서비스에 대한 설정파일이 있다면 /usr/lib/systemd 에 있는 같은 설정파일은 무시된다. 일반적으로는 systemctl 으로 서비스를 추가할 경우 /usr/lib/systemd 에 있는 것이 링크된다.

그 다음, systemd가 부팅에 관여하게 하려면 커널 파라미터로 init=/usr/lib/systemd/systemd 를 추가해주면 된다. grub 설정 파일에 위 내용을 추가하고 재부팅해보면 OpenRC가 아닌 systemd가 부팅을 관장하는 것을 볼 수 있을 것이다. 구체적인 내용은 여기저기 를 참조.