동적공유객체 (DSO) 지원 - Apache HTTP Server

Apache HTTP Server Version 2.2

Apache > HTTP Server > Documentation > Version 2.2

# 동적공유객체 (DSO) 지원

가능한 언어: en | ja | ko | tr

이 문서는 최신판 번역이 아닙니다. 최근에 변경된 내용은 영어 문서를 참고하세요.

아파치 웹서버는 관리자가 모듈들을 선택하여 서버에 포함할 기능을 결정할 수 있는 모듈화된 프로그램이다. 서버를 컴파할때 `httpd` 실행파일에 정적으로 모듈을 컴파일할 수 있다. 아니면 모듈을 `httpd` 실행파일과 분리하여 동적공유객체(Dynamic Shared Objects, DSO)로 컴파일할 수 있다. DSO 모듈은 서버를 컴파일할때 컴파일하거나, Apache Extension Tool (apxs)을 사용하여 나중에 컴파일하여 추가할 수 있다.

이 문서는 DSO 모듈 사용법과 배경 이론을 설명한다.

## 구현

관련된 모듈 | 관련된 지시어
--- | ---
`mod_so` | `LoadModule`

아파치 핵심에 정적으로 컴파일해야할 `mod_so.c`라는 모듈은 아파치 모듈을 읽어들이기위한 DSO를 지원한다. 이 모듈은 `core`를 제외하고 DSO가 될 수 없는 유일한 모듈이다. 실제로 다른 모든 아파치 모듈은 설치 문서에서 설명한 `configure`의 `--enable-module=shared` 옵션을 사용하여 DSO로 컴파일할 수 있다. 모듈을 `mod_foo.so`와 같이 DSO로 컴파일한후 `httpd.conf` 파일에 `mod_so`의 `LoadModule` 명령어를 사용하여 서버 시작시 혹은 재시작시 그 모듈을 읽어들일 수 있다.

아파치 모듈(특히 제삼자가 만든 모듈)로 사용할 DSO 파일을 쉽게 만들기위해 apxs (APache eXtenSion)라는 새로운 지원 프로그램이 있다. 이 프로그램은 아파치 소스 트리 밖에서 DSO로 사용할 모듈을 컴파일할때 사용한다. 개념은 쉽다. 아파치를 설치할때 `configure`와 `make install`이 아파치 C 헤더파일을 설치하고, DSO 파일을 컴파일하기위한 플래폼 특유의 컴파일러 옵션과 링커 옵션을 `apxs` 프로그램에 기록한다. 그래서 `apxs`를 사용하는 사용자는 아파치 배포본 소스 트리없이, 또 DSO 지원을 위한 플래폼 특유의 컴파일러 옵션와 링커 옵션에 신경을 쓰지않고 자신의 아파치 모듈 소스를 컴파일할 수 있다.

## 사용법 요약

Apache 2.2의 DSO 기능에 대한 짧고 간략한 요약이다:

1. 배포본에 있는 아파치 모듈을 컴파일하고 설치하는 경우. 예를 들어 `mod_foo.c`를 DSO `mod_foo.so`로:

   `$ ./configure --prefix=/path/to/install --enable-foo=shared
   $ make install`

2. 제삼자가 만든 아파치 모듈을 컴파일하고 설치하는 경우. 예를 들어 `mod_foo.c`를 DSO `mod_foo.so`로:

   `$ ./configure --add-module=module_type:/path/to/3rdparty/mod_foo.c --enable-foo=shared
   $ make install`

3. 공유 모듈을 나중에 사용하기위해 아파치를 구성하는 경우:

   `$ ./configure --enable-so
   $ make install`

4. 제삼자가 만든 아파치 모듈을 컴파일하고 설치하는 경우. apxs를 사용하여 아파치 소스 트리 밖에서 `mod_foo.c`를 DSO `mod_foo.so`로:

   `$ cd /path/to/3rdparty
   $ apxs -c mod_foo.c
   $ apxs -i -a -n foo mod_foo.la`

모든 경우 일단 공유 모듈이 컴파일되면, `httpd.conf`에 `LoadModule` 지시어를 사용하여 아파치가 그 모듈을 읽어들이게 만든다.

## 배경지식

현대적인 유닉스류에는 동적공유객체 (DSO)의 동적 링킹/로딩(dynamic linking/loading)이라고 하여, 특별한 형식의 실행코드 조각을 만들어 실행중인 실행프로그램의 주소공간에 읽어들이는 멋진 기능이 있다.

보통 두가지 방법으로 읽어들일 수 있다. 하나는 실행프로그램이 시작할때 `ld.so`라는 시스템 프로그램이 자동으로 읽어들이는 경우고, 다른 하나는 실행중인 프로그램이 `dlopen()/dlsym()` 시스템호출로 유닉스 로더(loader)의 시스템 인터페이스을 사용하여 직접 읽어들이는 경우다.

첫번째 경우 DSO를 보통 공유라이브러리(shared libraries) 혹은 DSO 라이브러리라고 부르며, 파일은 `libfoo.so`나 `libfoo.so.1.2` 같은 이름을 가진다. 이들은 시스템 디렉토리(보통 `/usr/lib`)에 있고, 컴파일시 링커 명령어에 `-lfoo`를 주어 실행파일과 연결한다. 이렇게 직접 써준 라이브러리는 실행파일에 참조되여서, 프로그램이 시작할때 링커 옵션 `-R`로 직접 지정한 경로, 환경변수 `LD_LIBRARY_PATH`로 지정한 경로 혹은 `/usr/lib`에서 유닉스 로더가 `libfoo.so`를 찾을 수 있다. 그러면 실행프로그램의 (아직 못찾은(unresolved)) 심볼(symbol)을 DSO에서 찾게된다.

DSO는 보통 실행프로그램의 심볼을 찾지않기 때문에 (DSO가 재사용가능한 일반적인 코드 라이브러리이므로) 찾기는 여기서 끝난다. 유닉스 로더가 심볼 찾기를 완전히 담당하므로 실행프로그램이 직접 DSO에서 심볼을 찾을 필요가 없다. (사실 `ld.so`를 부르는 코드는 정적이 아닌 모든 실행프로그램에 링크되는 실행시 시작코드의 일부다.) 공통된 라이브러리 코드를 동적으로 읽어들이는 장점은 명확하다. 라이브러리 코드가 모든 프로그램에 중복해서 저장되는 대신 `libc.so`와 같은 시스템 라이브러리에 한번만 저장되기 때문에 디스크 공간이 절약된다.

두번째 경우 DSO를 보통 공유객체(shared objects) 혹은 DSO 파일이라고 부르고, (규칙상 이름은 `foo.so`이지만) 파일의 확장자는 자유롭다. 이 파일들은 보통 프로그램 자체 디렉토리에 위치하고 실행프로그램에 자동으로 연결되지 않는다. 대신 실행프로그램은 실행시 `dlopen()`을 사용하여 DSO를 주소공간에 직접 읽어들여야 한다. 이때 실행프로그램은 DSO에서 심볼을 찾지 않는다. 대신 앞에서 본 유닉스 로더는 자동으로 실행파일과 실행파일이 이미 읽어들인 DSO 라이브러리(특히 항상 존재하는 `libc.so`의 모든 심볼)에서 DSO의 (아직 못찾은) 심볼을 찾는다. 그래서 DSO는 마치 처음부터 실행프로그램에 정적으로 링크된것과 같이 실행파일의 심볼을 알게된다.

DSO의 API를 이용하기위해서 마지막으로 실행프로그램은 `dlsym()`으로 DSO에서 특정 심볼을 찾아서, 앞으로 사용하기위해 디스패치(dispatch) 표 등에 저장한다. 다른 말로 실행프로그램은 사용할 모든 실볼을 직접 찾아야한다. 이런 구조의 장점은 프로그램의 일부를 프로그램이 필요할때까지 읽어들이지 않아도 (그래서 메모리를 낭비하지 않게) 된다는 점이다. 기본 프로그램의 기능을 확장하기위해 필요한 경우 이 부분을 동적으로 읽어들일 수 있다.

이런 DSO 구조가 자연스럽게 보이지만, 최소한 어려운 점이 한가지있다. 프로그램을 확장하기위해 DSO를 사용할때 DSO가 실행프로그램의 심볼을 찾는 일이다. 왜? DSO가 실행프로그램의 심볼을 "역으로 찾는 것"은 (라이브러리는 자신을 사용하는 프로그램에 대해 모른다는) 라이브러리 설계에 반하며, 모든 플래폼에서 지원되지않고 표준화되지도 않았기 때문이다. 실제로 실행파일의 전역심볼(global symbol)은 보통 익스포트(export)되지 않기때문에 DSO가 사용할 수 없다. DSO를 사용하여 실행중 프로그램을 확장하려면 링커에게 모든 전역심볼을 익스포트하도록 강제하는 것이 주된 해결책이다.

공유라이브러리는 DSO 방식의 설계원칙대로 전형적이기때문에 운영체제가 제공하는 거의 모든 종류의 라이브러리가 사용한다. 반대로 많은 프로그램은 프로그램을 확장하기위해 공유객체를 사용하지 않는다.

1998년 실행중 실제로 기능을 확장하기위해 DSO 구조를 사용한 소프트웨어 패키지는 (XS 구조와 DynaLoader 모듈을 사용한) Perl 5, Netscape Server 등으로 드물었다. 아파치는 이미 기능을 확장하기위해 모듈 개념을 사용했고 외부 모듈을 아파치 핵심기능에 연결하기위해 내부적으로 디스패치목록을 이용한 접근방법을 사용했기때문에 1.3 버전부터 이 대열에 합류했다. 그래서 아파치는 실행중 모듈을 읽어들이는데 DSO를 사용하도록 운명지워졌다.

## 장단점

앞에서 말한 DSO를 사용하면 다음과 같은 장점이 있다:

- 실제 서버 프로세스가 컴파일시 `configure` 옵션대신 `httpd.conf`의 `LoadModule`을 사용하여 실행중에 결합되므로 서버 패키지 실행이 더 유연하다. 예를 들어 한번의 아파치 설치만으로 다른 서버(표준 버전과 SSL 버전, 최소화 버전과 기능추가 버전 [mod_perl, PHP3] 등)를 실행할 수 있다.
- 서버는 설치후에도 제삼자가 만든 모듈을 사용하여 쉽게 확장할 수 있다. 최소한 기업의 패키지 제작자는 아파치 핵심 패키지와 별도로 PHP3, mod_perl, mod_fastcgi 등을 추가 패키지로 만들 수 있어서 큰 이득이다.
- DSO와 `apxs`를 가지고 아파치 소스 트리 밖에서 작업하고 `apxs -i`와 `apachectl restart` 명령어만으로 현재 개발한 모듈의 새 버전을 실행중인 아파치 서버에 반영할 수 있어서 더 쉽게 아파치 모듈을 개발할 수 있다.

DSO는 다음과 같은 단점이 있다:

- 프로그램의 주소공간에 코드를 동적으로 읽어들이는 기능을 지원하지않는 운영체제가 있기 때문에 모든 플래폼에서 DSO를 사용할 수 없다.
- 유닉스 로더가 심볼을 찾아야하기 때문에 서버 시작이 약 20% 정도 늦어진다.
- 서버는 위치독립코드(position independent code, PIC) 때문에 절대주소지정(absolute addressing)보다 느린 상대주소지정(relative addressing)의 복잡한 어셈블러 기법이 필요하므로 어떤 플래폼에서 실행시 약 5% 정도 늦다.
- DSO 모듈을 다른 DSO기반 라이브러리(`ld -lfoo`)에 링크할 수 없는 플래폼이 있기때문에 (예를 들어 ELF기반 플래폼은 지원하지만 a.out기반 플래폼은 보통 이 기능을 지원하지 않는다) 모든 종류의 모듈에 DSO를 사용할 수 없다. 다른 말로 DSO 파일로 컴파일하는 모듈은 아파치 핵심과 아파치 핵심이 사용하는 C 라이브러리(`libc`)와 다른 동적/정적 라이브러리, 위치독립코드를 담고 있는 정적 라이브러리 아카이브(`libfoo.a`)의 심볼만을 사용할 수 있다. 다른 코드를 사용하려면 아파치 핵심이 그것을 참조하던지, `dlopen()`으로 직접 코드를 읽어들여야 한다.

가능한 언어: en | ja | ko | tr