2010년 10월 26일 화요일

구글 크롬의 멀티프로세스 구조

구글 크롬의 멀티프로세스 구조

먼저, 이전 크롬에 관한글에 서 잘못 적은 부분이 있었다. 크롬이 구현한 떼었다가 붙일 수 있는 동적 탭 기능이 멀티스레드 구조에서는 힘들다고 했는데 아니다. 오히려 멀티스레드 구조에서 손 쉽게 구현이 된다. 그냥 탑 레벨 Window 하나 만들면 끝이다. 스레드를 별도로 만들 수도 있지만 그럴 필요도 없다. 내가 단단히 착각을 했다. MFC의 기본 프로젝트 옵션 중에서도 “Multiple top level documents”라는 옵션이 이런 구현을 보여주고 있다.

오히려 크롬이 도입한 멀티프레세스 구조에서 하나의 창 아래 여러 프로세스의 내용을 묶는 것이 더 어렵다. 간략하게 구글 크롬에서 사용한 멀티프로세스 구조에 대해 이야기해보자.

멀티프로세스 구조의 장점과 단점은 잘 알 것이고, 이런 구조에서 어떤 방식으로 다른 프로세스들이 가지고 있는 데이터들을 최종적으로 화면에 잘 보여주는지 생각해보자. 먼저, 생각할 수 있는 방법으로는 어차피 프로세스 하나가 탭 하나씩 맏고 있으니까 그리는 것도 각자 알아서 처리하면 될 것 같다. 그러나 다른 프로세스에 있는 윈도우를 탭 컨트롤에 넣어 보이게 하는 것은 그렇게 간단치 않다. 윈도우 프로그래머라면 익숙한 SetParent(HWND) 함수로 다른 프로세스의 윈도우 핸들을 넣어버리면 되기는 되지만 다른 자잘한 문제들을 많이 만들어 낸다.1

그래서 대안으로 GUI를 처리하는 프로세스 하나가 있고, 나머지 웹 데이터를 처리하는 프로세스들이 서로 IPC를 통해 데이터를 주고 받는 방식을 떠올릴 수 있다. 브라우저에서 마우스를 클릭하면 이 메세지를 IPC로 해당 프로세스로 보내면 되고, 반대로 프로세스 내부에서 어떤 이벤트가 발생하면 다시 IPC로 전달해 최종적으로 UI를 바꾸도록 하면 될 것이다. 복잡한 프로그램을 짤 때 GUI와 엔진을 분리하는 전형적인 방식일 것이다.

직접 소스나 관련 문서를 찾아보기 전에 한 번 Spy++와 Process Explorer로 조사 해보자. 각각 두 탭씩 두 개의 브라우저 창을 띄었다. Spy++로 확인한 윈도우 구조는 아래와 같았다.

보다시피 Chrome_HWNDViewcontainer_0이라는 가짜 윈도우가 각 브라우저 창 마다 하나씩 존재하고 실제 창을 보여주는 윈도우는 Chrome_VistaFrame이라는 녀석이다. 이 아래에 현재 활성화된 창이 보여지고 있다. 그리고 비활성화 된 탭은 보다시피 hidden 상태(옅은 아이콘)로 있다.

그리고 중요한 것은 이 모든 윈도우들이 탭을 관리하는 프로세스들과 다른 별도의 한 프로세스로, 또 하나의 스레드 위에서 돌아간다는 점이다.2 그렇다면 이제 웹 페이지를 들고 있는 프로세스와 모든 창과 그리기를 담당하는 프로세스 사이에 IPC를 해야하는데 어떤 방식으로 할까? 무식하게 동기 방식인 WM_COPYDATA3 으로 구현할리는 만무하다. 그럼 아무래도 얘들은 CreateNamedPipe 같은 Win32 Named Pipe 객체로 IPC를 할 것 같다. Process Explorer로 확인해보니 각 자식 프로세스마다 named pipe를 하나씩 열고 있음을 볼 수 있다.

생각보다 복잡하지 않고 그냥 우리가 생각할 수 있는 ‘당연한’ 방법으로 구현했다. 그러나 핵심은 이런 기본적인 설계가 아니라 구현 자체에 있다. 비록 설계는 “흠.. GUI 프로세스 하나와 IPC로 하면 되겠군!” 이라고 간단히 그려져도 막상 구현에 들어가면 생각하지 못한 문제가 속출하는 등 많은 어려움이 있을 것이다. 이런 것을 극복하고 이렇게 잘 만드는 것은 분명히 다른 차원의 이야기다.

그럼 정답을 찾아보자. 구글 크롬은 Chromium이라는 오픈 소스로 관리되고 생각보다 많은 문서들이 있다. 멀티 프로세스 구조에 대해서도 꽤 자세히 잘 설명되어있다.

대략 예측한대로 ‘Browser’ 프로세스는 그림 그리기나 여러 마우스 이벤트를 관할하고, ‘Renderer’ 프로세스는 보다시피 웹 엔진 WebKit을 표현한다. 그리고 Named pipe 기반의 비동기 IPC를 통해 서로 데이터를 주고 받는다. 참고로 이 문서에 소개된 대표적인 IPC 경우를 예로 들어보자:

  1. Browser에서 Renderer로 가는 경우: 마우스 클릭과 같은 메세지. 이런 마우스 메세지는 Win32 윈도우 객체가 일단 받는다. 그런 뒤 플랫폼 독립적인 형태로 바꿔 Renderer로 보낸다.
  2. Renderer에서 Browser로 가는 경우: 중 마우스 커서 모양을 바꾸라는 신호. 이 신호는 분명 내부 웹 엔진인 WebKit이 만들어야 할 것이다. 이걸 브라우저가 IPC로 받아 GUI가 그에 맞게 커서를 보여준다.

물론 자세히 소스를 까보면 복잡하겠지만 역시 큰 그림은 이해가 쉽다.

생각보다 Chromium의 문서들이 참 설명이 잘 되어있어 이해하기 쉽다. 자고로 소스 코드를 읽고 이해하는 것 보다 새로 짜는 것이 더 편하다는 말이 있을 정도로 소스 읽기는 암호 해독 수준이다. 오픈소스를 지향하면 그 만큼 이 부분을 해결해야하는데 많은 기술 문서 뿐만 아니라 디버깅 방법까지 친절히 알려주고 있다. 크롬은 멀티 프로세스라서 디버깅이 그리 간단치 않기 때문이다. 이런 친절함은 곧 공짜로 전세계 (순전한) 프로그래머들의 노동력을 착취하겠다는 소리..

결론: 이 Browser 프로세스를 죽이면 (혹 여기서 버그가 나면) 그냥 모든 탭도 다 같이 죽어버린다 ㅎㅎ

추가: 크롬은 한 탭에 뜬 페이지라 하더라도 플러그인을 다른 프로세스로 돌리기 때문에, 그 플러그인이 죽었을 때 페이지가 죽지 않도록 하고 있습니다. 이 부분은 아주 좋은 아이디어였습니다. 그리고 반드시 모든 탭이 각각 하나의 프로세스로 대응되는 것은 아니었습니다. 탭을 40~50개 띄어보면 제한된 프로세스 풀에서 할당되어있음을 볼 수 있습니다.

1. 대표적인 SSH 클라이언트인 Putty는 멀티탭 구현이 없다. 그래서 여러 멀티탭 구현이 존재하지만 제대로 작동하지 않는다. 모두 다른 프로스세에서 돌아가는 창을 무리하게 한 윈도우로 넣으려고 하기 때문이다.

2. Spy++로 해당 윈도우가 어느 프로세스/스레드 위에 만들어졌는지는 간단히 알 수 있다. Windows 운영체제에서 Window 객체가 어느 스레드 위에 만들어졌는가는 꽤 중요하다. 보통은 primary thread에서만 만들어지기 때문에 별 신경을 안쓰지만 멀티스레드로 갈 경우에는 이것을 중요하게 따져야 한다. 메세지 큐가 per thread로 존재하기 때문에 스레드 넘어 있는 윈도우를 조작할 때 자칫하다 데드락이 발생할 수도 있다.

3. WM_COPYDATA는 서로 다른 프로세스에게 데이터를 전달할 수 있는 가장 간단한 동기 IPC 방법이고 SendMessage로만 가능하다. 생각보다 WM_COPYDATA도 동기적인 IPC에서는 꽤나 편하고 정확하게 작동한다.