Multiprocessing vs Multithreading

kudos·2021년 6월 5일

운영체제

목록 보기
1/1

1. 개요

Web Server는 concurrency를 제공해야 한다. 서버는 특정 클라이언트가 서버 자원을 소비하는 동안 다른 클라이언트가 starve 하지 않도록 클라이언트에게 시기 적절하고 공정한 방법으로 서비스를 제공해야 한다. Multiprocessing과 Multithreading은 이러한 concurrency를 달성하기 위한 전통적인 방식이다.

2. A Multiprocessing Server

초기 Web Server는 multiprocessing 방식을 사용했다. 각각의 process들이 서로 다른 connection을 다루는 방식이고, 현대 서버는 주로 multiprocessing과 multithreading을 합쳐서 사용한다.

이 서버는 parent process와 연결된 클라이언트 수만큼의 child process를 가진다. 클라이언트는 connection이 닫힐 때까지, 즉 세션이 끝날 때까지 active 상태이다.

parent process의 동작 과정은 다음과 같다.

  1. connection을 listen
  2. connection이 들어오면, 새로운 child process 생성
  3. 다시 1번으로 돌아감
pid_t pid = fork(); /* spawn child */
if (0 == pid) {     /* child */
   close(sock);     /* close inherited listening socket */
   /* handle request and terminate */
   ...
}               
else                /* parent */
  close(client);    /* close client, resume listening */

parent process가 fork()를 호출해서 성공하면, 음이 아닌 정수를 반환한다 : child process에게는 0, parent에게는 이 child를 식별할 수 있는 id. child는 parent의 open socket 명세를 상속하기 때문에 다음과 같은 if문이 동작한다.

위 코드의 if문을 설명하면 다음과 같다.

  • if 절 : child는 listening socket의 복사본을 닫는다. 클라이언트를 accept하는 것은 parent의 일이기 때문이다. child는 클라이언트의 요청을 다루고 exit()으로 끝낸다.
  • else 절 : child가 클라이언트를 상대하기 때문에 parent는 클라이언트 socket을 닫는다. 다시 connection listening을 재개한다.

process를 만들고 없애는 것은 비싼 작업이다. FastCGI와 같은 모듈이 이러한 비효율성을 pre-forking으로 줄여냈다. FastCGI는 처음에 재사용 가능한 client-handling process들의 pool을 생성한다.

그러나 비효율성은 여전히 존재한다. 어떤 process가 다른 process를 선점하면, switched-in process와 swithched-out process가 정상적으로 동작하도록 context switch가 발생한다. kernel은 선점 당한 process가 정상적으로 재시작할 수 있도록 process마다 다음과 같은 context 정보를 유지하고 있다.

  • page table : 가상 주소와 물리 주소 매핑
  • process table : 중요 정보 저장
  • file table : process가 열었던 파일 추적

시스템의 CPU cycle을 context switching 도중에는 Web Server와 같은 어플리케이션이 사용할 수 없다. Pre-forking은 process를 만들고 없애는 데 드는 비효율성을 완화시켜주지만 context switching 비용은 그대로이다.

multiprocessing의 장점

프로그래머 입장에서, 공유 메모리 동시 접근에 대한 동기화 작업을 해줄 필요가 없다. 다음 예시를 보자.

간단한 단어 게임을 위한 web 어플리케이션이 있다. 이 어플리케이션은 무작위 순서의 글자 배열을 보여주고 사용자는 이를 재조합해서 단어를 만드는 것이다.

single-player 게임인 경우에는 문자열을 계속 추적하면 된다. 다음 전역 변수가 있다고 생각해보자. 여기서 어플리케이션은 하나의 WordGame instance만을 갖는다. 동시에 사용하는 player는 또 다른 WordGame instance를 갖기 때문에 다른 사람의 게임을 방해할 수 없다.

typedef struct {
  /* variables to track game's state */
} WordGame;
WordGame game; /* single, global instance */

각각의 player는 담당하는 두 child process C1, C2가 있다고 가정하자. Linux에서 forked child는 parent의 주소를 상속하기 때문에 parent와 C1, C2는 같은 주소에서 시작한다. 만약 세 process가 쓰기 작업을 하지 않는다면 아무 문제가 생기지 않는다. 쓰기 작업을 할 때는 copy-on-write(COW) 정책에 의해, child가 쓰기 작업 시에 상속 받은 page에 대한 자신만의 복사본을 갖게 되고 child의 page table이 업데이트 된다. 결과적으로 parent와 forked child들이 서로 다른 주소 공간을 갖게 되고 각각의 클라이언트는 효과적으로 자신만의 쓰기 가능한 WordGame의 복사본을 갖게 된다.

Copy-on-Write(COW)

fork() system call를 사용하면 child process는 parent process를 복제한다. 즉, process를 복제하므로 부모와 지식 process에 해당하는 메모리 내의 page 또한 중복된다. 게다가 fork() system call을 사용하면 대부분 exec() system call을 호출하기 때문에 child process는 제거되고 중복된 메모리 또한 메모리에서 해제되는데, 이는 운영체제 관점에서 매우 낭비이다.

COW는 이러한 페이지의 불필요한 중복을 줄이는 방법이다. fork()를 통해 부모 프로세스가 소유하고 있던 메모리 영역은 모두 'copy-on-write'로 표시된다. 만약 child process가 parent process의 page들 중 하나를 수정한다면, 그 page를 복사하여 수정한 후, 수정된 page를 메모리에 따로 저장한다.

이렇게 하면 child process가 메모리에 로드된 page를 수정할 때에만 복사하기 때문에 기존에 fork()를 사용해 page 전체를 복사할 때보다 빠르다.

3. A Multithreading Server

multithreading 서버는 multiprocessing 서버와 전략은 비슷한데, 클라이언트를 처리하는 게 forked child 프로세스가 아니라 하나의 process 내부의 thread들이라는 점이 다르다. COW 덕분에 서로 다른 process들은 개별 주소 공간을 가졌지만, 같은 process 내의 thread들은 하나의 주소 공간을 공유한다.

클라이언트가 연결되면, 서버는 새로운 thread를 만들어서 그 클라이언트를 맡긴다. 서버는 thread가 클라이언트와 읽기/쓰기 작업을 할 수 있도록 client socket을 전달한다.

Multiprocessing 서버에서 봤던 WordGame은 여기서 어떻게 적용되는지 살펴보자. 이 서버는 클라이언트 하나당 하나의 WordGame instance를 갖도록 해야 한다.

WordGame games[BACKLOG]; /* BACKLOG == max clients */

그래서 이렇게 WordGame들의 배열을 만든 후, 클라이언트가 연결되면 사용가능한 instance를 찾아서 thread에게 전달한다. 각 thread는 각각의 instance를 갖게 되고 이를 통해 thread-safe하게 된다. 이러한 것은 thread pool을 통해 쉽게 구현될 수 있다.

4. 비교

5. 브라우저에서의 예시

chrome에서의 예시를 살펴보자.

multiprocess 구조가 Chrome에 주는 이점

Chrome은 렌더러 process를 여러 개 사용한다. 가장 간단한 예로 탭마다 렌더러 process를 하나 사용하는 경우를 생각해보자. 3개의 탭이 열려 있고 각 탭은 독립적인 렌더러 프로세스에 의해 실행된다. 이때 한 탭이 응답하지 않으면 그 탭만 닫고 실행 중인 다른 탭으로 이동할 수 있다. 만약 모든 탭이 하나의 프로세스에서 실행 중이었다면 탭이 하나만 응답하지 않아도 모든 탭이 응답하지 못하게 된다.

같은 원리로 브라우저 프로세스와 렌더러 프로세스를 분리했을 때의 장점도 생각할 수 있다. 렌더러 프로세스가 웹 페이지를 그리는 도중 오류가 발생해 실행이 중단되더라도 브라우저 전체가 종료되지 않고 오류 페이지를 보여 주는 정도로 문제를 처리할 수 있다.

브라우저의 작업을 여러 프로세스에 나눠서 처리하는 방법의 또 다른 장점은 보안과 격리(sandbox)이다. 운영체제를 통해 프로세스의 권한을 제한할 수 있어 브라우저는 특정 프로세스가 특정 기능을 사용할 수 없게 제한할 수 있다. 예를 들어 Chrome은 렌더러 프로세스처럼 임의의 사용자 입력을 처리하는 프로세스가 임의의 파일에 접근하지 못하게 제한한다.

유의해야 할 점

프로세스는 전용 메모리 공간을 사용하기 때문에 공통부분(예를 들어 Chrome의 JavaScript 엔진인 V8)을 복사해서 가지고 있는 경우가 많다. 동일한 프로세스의 스레드가 메모리를 공유할 수 있는 데 반해 서로 다른 프로세스는 메모리를 공유할 수 없어 메모리 사용량이 더 많아질 수밖에 없다. Chrome은 메모리를 절약하기 위해서 실행할 수 있는 프로세스의 개수를 제한한다. 정확한 한도는 기기의 메모리 용량과 CPU 성능에 따라 다르지만 프로세스의 개수가 한도에 다다르면 동일한 사이트를 열고 있는 여러 탭을 하나의 프로세스에서 처리한다.

또한, 성능이 좋은 하드웨어에서 Chrome이 실행 중일 때에는 각 서비스를 여러 프로세스로 분할해 안정성을 높이고, 리소스가 제한적인 장치에서 실행 중일 때에는 서비스를 하나의 프로세스에서 실행해서 메모리 사용량을 줄이는 것이 기본 아이디어이다. 메모리 절약을 위해 프로세스를 합치는 이런 방식은 Android와 같은 플랫폼에서는 이전부터 사용되었다.

참고

profile
kudos

0개의 댓글