namalzi

← 글 목록

Apache httpd 소스 파헤치기 3: 전체 지도 — 코어, APR, MPM, 모듈과 세 개의 큰 구조체

Apache httpd · 게시

Apache httpd 소스를 처음 여는 사람을 위한 지도입니다. 운영체제·APR·코어·MPM·모듈로 나뉘는 층, 코어조차 모듈이라는 것, 실행 파일에 묶이는 모듈과 LoadModule로 싣는 모듈의 차이, server_rec·conn_rec·request_rec 세 구조체와 풀 계층, 요청 하나가 지나가는 길을 실제 소스와 직접 돌린 예제로 확인합니다.

이 글에서 다루는 것

"Apache httpd 소스 파헤치기"의 세 번째 글입니다. 1편에서 소스를 받아 빌드하고 폴더 지도를 펴 보았고, 2편에서는 httpd 같은 큰 C 프로그램을 읽을 때 필요한 구조체·함수 포인터·매크로를 익혔습니다. 이번 글은 코드를 한 줄씩 읽기 전에 펴 놓는 전체 지도입니다.

httpd 저장소를 열면 server/, modules/, include/, os/ 같은 폴더에 C 파일이 수백 개 있습니다. 어디가 중심인지, 무엇이 무엇을 부르는지 처음에는 알 수 없습니다. 이 글은 다음 질문에 답합니다.

항목 내용
대상 Apache HTTP Server (httpd)
버전 2.5.1-dev (trunk)
커밋 e4af30fe6d (2026-10-24)
APR APR 1.7.2 (커밋 e905526629)
주로 읽는 파일 include/httpd.h, include/http_config.h, server/core.c, server/config.c, modules/http/http_core.c, modules/core/mod_so.c

글에 나오는 줄 번호는 모두 이 커밋 기준입니다. 같은 커밋의 소스를 받고 예제용 도커 이미지를 만드는 방법은 1편에 있습니다.

지도를 그리는 글이라 이 글은 큰 줄기만 따라갑니다. 각 개념을 자세히 읽는 글은 절마다 "N편에서 봅니다"로 이어 두었습니다.

먼저 알아 둘 말

모듈(module)은 httpd에 기능 하나를 더하는 코드 묶음입니다. mod_mime(확장자로 Content-Type 정하기), mod_dir(DirectoryIndex), mod_proxy(리버스 프록시)처럼 이름이 mod_로 시작합니다. 소스에서 모듈은 module이라는 C 구조체 하나로 표현됩니다.

DSO(Dynamic Shared Object, 동적 공유 객체)는 실행 중에 불러올 수 있는 라이브러리 파일입니다. Linux에서는 확장자가 .so입니다. httpd.conf의 LoadModule 한 줄은 이런 .so 파일 하나를 서버 프로세스 안으로 불러옵니다.

APR(Apache Portable Runtime)은 운영체제마다 다른 기능(메모리, 파일, 소켓, 스레드, 공유 라이브러리 불러오기 등)을 같은 함수 이름으로 쓸 수 있게 감싼 C 라이브러리입니다. httpd와 따로 개발되고 따로 설치됩니다. 함수 이름이 apr_로 시작합니다. 버킷·훅 같은 기능은 짝인 APR-util에 있습니다.

MPM(Multi-Processing Module)은 프로세스와 스레드를 몇 개 만들고, 들어오는 연결을 누구에게 맡길지 정하는 모듈입니다. event, worker, prefork가 대표적입니다. 이름에 "모듈"이 들어 있듯 이것도 모듈입니다.

풀(pool)은 APR의 메모리 관리 방식입니다. 메모리를 하나씩 free()하지 않고, 풀에서 꺼내 쓴 뒤 풀을 통째로 지웁니다. 요청 하나에 풀 하나, 연결 하나에 풀 하나처럼 수명을 묶어 씁니다. 자세한 것은 4편에서 봅니다.

훅(hook)은 코어가 "이 시점에 끼어들 함수를 받습니다"라고 열어 둔 자리입니다. 모듈은 훅에 자기 함수를 걸어 두고, 코어는 그 시점이 되면 걸린 함수들을 차례로 부릅니다. 훅을 거는 방법은 6편에서 자세히 봅니다. C의 함수 포인터와 구조체가 낯설다면 2편을 먼저 보면 좋습니다.

1. 한 장으로 보는 다섯 층

httpd를 층으로 나누면 이렇게 됩니다.

httpd의 다섯 층. 맨 위에 모듈과 MPM이 나란히 있고, 둘 다 코어의 훅에 함수를 건다. 코어 아래에 APR·APR-util이, 그 아래에 운영체제가 있다. 모듈과 MPM도 APR을 직접 쓴다. 화살표는 "위가 아래를 가져다 쓴다"는 뜻입니다. 테두리가 굵은 코어가 가운데에서 훅을 정의하고 부릅니다.

그 밖에 include/에는 모든 층이 함께 쓰는 헤더(httpd.h, http_config.h 등)가, support/에는 ab, htpasswd 같은 도구가 있습니다. 폴더별 지도는 1편에 더 자세히 있습니다.

APR이 하는 일: OS 차이를 한 함수 뒤에 숨긴다

APR이 왜 따로 있는지는 코드 한 조각이 잘 보여 줍니다. LoadModule이 .so 파일을 불러올 때 결국 부르는 함수가 APR의 apr_dso_load()입니다.

APR_DECLARE(apr_status_t) apr_dso_load(apr_dso_handle_t **res_handle, 
                                       const char *path, apr_pool_t *pool)
{
#if defined(DSO_USE_SHL)
    shl_t os_handle = shl_load(path, BIND_IMMEDIATE, 0L);

#elif defined(DSO_USE_DYLD)
    /* ... */
#elif defined(DSO_USE_DLFCN)
    /* ... */
    os_handle = dlopen(path, flags);
#endif    
#endif /* DSO_USE_x */

dso/unix/dso.c 80–141행 (APR 1.7.2)

#if defined(...)·#elif·#endif는 컴파일하기 전에 조건에 맞는 줄만 남기는 C 전처리기 지시문입니다. 같은 함수 안에 HP-UX용(shl_load), 옛 macOS용(NSLinkModule), 그리고 Linux를 포함한 대부분의 유닉스용(dlopen) 코드가 함께 있고, 빌드하는 OS에 맞는 한 갈래만 남습니다. Windows용은 아예 다른 파일(dso/win32/dso.c, LoadLibraryExW)에 있습니다. httpd 쪽은 OS가 무엇이든 apr_dso_load() 한 줄만 부르면 됩니다.

APR이 httpd 실행 파일 안에 들어 있지 않고 따로 있는 라이브러리라는 것도 눈으로 확인할 수 있습니다. 예제 컨테이너에서 ldd로 httpd 실행 파일이 기대는 공유 라이브러리를 보았습니다.

### 0. httpd 실행 파일이 기대는 공유 라이브러리 (APR 줄만)
	libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0
	libaprutil-1.so.0 => /lib/x86_64-linux-gnu/libaprutil-1.so.0
	libapr-1.so.0 => /lib/x86_64-linux-gnu/libapr-1.so.0

이 글의 예제는 모두 기준 커밋(e4af30fe6d)을 그대로 빌드한 httpd 2.5.1-dev로 도커 컨테이너(Ubuntu 24.04)에서 실행했습니다. 실행 스크립트는 ex/u03-architecture/run.sh 하나이고, 출력의 ### 줄은 스크립트가 찍은 제목입니다.

libapr-1과 libaprutil-1은 Ubuntu 패키지로 설치된 APR입니다. 정규식 라이브러리 PCRE2도 따로 있습니다.

헷갈리기 쉬운 곳: 화살표가 한쪽으로만 가지 않는다

층 그림은 "위가 아래를 쓴다"로 그렸지만, 코어와 MPM·모듈 사이는 양쪽으로 부릅니다. 모듈은 코어가 만든 함수(ap_로 시작)를 부르고, 코어는 훅을 통해 모듈의 함수를 부릅니다. 코어는 모듈의 이름을 모르고 "이 훅에 걸린 함수들"만 압니다. 그래서 모듈을 빼거나 더해도 코어를 다시 컴파일할 필요가 없습니다. MPM도 같습니다. main()은 마지막에 ap_run_mpm()이라는 훅을 부를 뿐이고, 어느 MPM이 그 훅에 걸려 있는지는 설정 파일이 정합니다(3절).

2. 코어도 모듈이다

httpd 소스를 읽다 보면 놀라는 것이 하나 있습니다. "코어"라고 부르는 부분조차 다른 모듈과 똑같은 module 구조체 하나로 등록된다는 것입니다.

모듈 하나 = module 구조체 하나

모든 모듈은 include/http_config.h에 정의된 이 구조체를 하나씩 가집니다.

struct module_struct {
    /* ... */
    int version;
    /* ... */
    int module_index;

    /* ... */
    const char *name;
    /* ... */
    unsigned long magic;

    /* ... */
    void *(*create_dir_config) (apr_pool_t *p, char *dir);
    /* ... */
    void *(*create_server_config) (apr_pool_t *p, server_rec *s);
    /* ... */
    const command_rec *cmds;

    /* ... */
    void (*register_hooks) (apr_pool_t *p);
    /* ... */
};

include/http_config.h 348–424행

이 글에 필요한 것만 남겼습니다.

필드 뜻 자세히
module_index 모듈마다 붙는 번호. 설정 배열에서 내 칸을 찾을 때 쓴다(4절) 6편
name 모듈을 만든 C 파일 이름(__FILE__) 이 절
magic "이것은 httpd 모듈 구조체가 맞다"는 표시값 3절
create_dir_config, create_server_config 등 이 모듈의 설정 구조체를 만들고 합치는 함수 7편
cmds 이 모듈이 처리하는 디렉티브 목록 7편
register_hooks 이 모듈이 어느 훅에 함수를 걸지 등록하는 함수 6편

void *(*create_dir_config) (...)처럼 이름 앞에 (* 가 붙은 필드는 함수 포인터, 곧 "함수의 주소를 담는 칸"입니다. 모듈마다 다른 함수를 이 칸에 넣어 두면 코어는 칸을 통해 그 함수를 부릅니다. 자바의 인터페이스 메서드를 구현해 넣는 것과 비슷한 일을 C에서는 이렇게 합니다(2편).

코어의 module: core_module

server/core.c 맨 끝에 코어의 모듈 구조체가 있습니다.

AP_DECLARE_MODULE(core) = {
    MPM20_MODULE_STUFF,
    AP_PLATFORM_REWRITE_ARGS_HOOK, /* hook to run before apache parses args */
    create_core_dir_config,       /* create per-directory config structure */
    merge_core_dir_configs,       /* merge per-directory config structures */
    create_core_server_config,    /* create per-server config structure */
    merge_core_server_configs,    /* merge per-server config structures */
    core_cmds,                    /* command apr_table_t */
    register_hooks                /* register hooks */
};

server/core.c 6216–6225행

AP_DECLARE_MODULE(core)는 module core_module이라는 변수를 선언하는 매크로입니다(http_config.h 467–469행). MPM20_MODULE_STUFF는 앞쪽 필드(버전, module_index = -1, name = __FILE__, magic 등)를 한꺼번에 채우는 매크로입니다(http_config.h 499–505행). 그 뒤로 디렉터리 설정·서버 설정을 만드는 함수, 디렉티브 표 core_cmds(DocumentRoot, <Directory>, <VirtualHost> 같은 기본 디렉티브), 그리고 register_hooks가 옵니다.

코어의 register_hooks를 열어 보면 코어도 다른 모듈처럼 훅에 함수를 겁니다. 몇 줄만 봅니다.

static void register_hooks(apr_pool_t *p)
{
    /* ... */
    ap_hook_create_connection(core_create_conn, NULL, NULL,
                              APR_HOOK_REALLY_LAST);
    /* ... */
    ap_hook_translate_name(ap_core_translate,NULL,NULL,APR_HOOK_REALLY_LAST);
    /* ... */
    ap_hook_handler(core_upgrade_handler,NULL,NULL,APR_HOOK_REALLY_FIRST);
    ap_hook_handler(default_handler,NULL,NULL,APR_HOOK_REALLY_LAST);
    /* ... */
}

server/core.c 6141–6214행

셋 다 APR_HOOK_REALLY_LAST, 곧 "맨 마지막 순서"로 걸려 있습니다. 다른 모듈이 먼저 처리하면 그것을 쓰고, 아무도 처리하지 않을 때만 코어가 기본 동작을 합니다. 코어가 특별 대우를 받는 것이 아니라 기본값을 제공하는 한 모듈로 행동하는 것입니다.

HTTP도 모듈이다: http_module

더 놀라운 것은 HTTP 프로토콜 처리도 코어가 아니라 별도의 모듈이라는 점입니다. 파일도 server/가 아니라 modules/http/에 있습니다.

AP_DECLARE_MODULE(http) = {
    STANDARD20_MODULE_STUFF,
    NULL,              /* create per-directory config structure */
    NULL,              /* merge per-directory config structures */
    NULL,              /* create per-server config structure */
    NULL,              /* merge per-server config structures */
    http_cmds,         /* command apr_table_t */
    register_hooks     /* register hooks */
};

modules/http/http_core.c 396–404행

이 모듈의 register_hooks는 ap_hook_process_connection(ap_process_http_connection, ...)으로 "연결이 들어오면 HTTP/1.x 요청을 읽어 처리하는 함수"를 겁니다(http_core.c 355–359행). 응답 헤더를 쓰는 HTTP_HEADER, chunked 인코딩을 하는 CHUNK 같은 필터도 이 모듈이 등록합니다. 코어는 "연결을 처리하는 훅"만 알 뿐이고, 그 연결에서 HTTP를 말할지는 이 모듈이 정합니다. 그래서 다른 프로토콜을 말하는 모듈이 같은 훅에 끼어들 수 있습니다.

그 밖의 "코어 같은" 모듈들

모듈 변수 파일 하는 일 어떻게 실리나
core_module server/core.c 기본 디렉티브, 기본 훅(연결 만들기, 파일 경로 정하기, 정적 파일 핸들러), 코어 필터 실행 파일에 묶임
http_module modules/http/http_core.c HTTP/1.x 연결 처리, HTTP 필터 실행 파일에 묶임
so_module modules/core/mod_so.c LoadModule, LoadFile 디렉티브 실행 파일에 묶임
mpm_event_module server/mpm/event/event.c 프로세스·스레드 관리, 연결 받기 이 빌드에서는 LoadModule
unixd_module modules/arch/unix/mod_unixd.c User·Group으로 권한 낮추기 LoadModule

MPM의 모듈 구조체도 모양이 똑같습니다.

AP_DECLARE_MODULE(mpm_event) = {
    MPM20_MODULE_STUFF,
    NULL,                       /* hook to run before apache parses args */
    NULL,                       /* create per-directory config structure */
    NULL,                       /* merge per-directory config structures */
    NULL,                       /* create per-server config structure */
    NULL,                       /* merge per-server config structures */
    event_cmds,                 /* command apr_table_t */
    event_hooks                 /* register_hooks */
};

server/mpm/event/event.c 4535–4544행

event_hooks 안에서 ap_hook_mpm(event_run, ...)으로 "서버의 주 루프를 돌리는 함수"를 mpm 훅에 겁니다(event.c 4381행). StartServers, ThreadsPerChild 같은 디렉티브는 event_cmds에 있습니다. MPM 자체는 8편에서 읽습니다.

모든 모듈은 같은 모양의 module 구조체 하나이고, 앞쪽 칸(버전, module_index, name, magic)은 *_MODULE_STUFF 매크로가 채운다. server/core.c 의 core_module(실행 파일에 묶임)은 설정 함수 넷과 core_cmds(DocumentRoot, Directory 등)를 채우고, register_hooks 가 create_connection 훅에 core_create_conn, translate_name 훅에 ap_core_translate, handler 훅에 default_handler 를 모두 APR_HOOK_REALLY_LAST 로 건다. modules/http/http_core.c 의 http_module(실행 파일에 묶임)은 설정 함수 넷이 모두 NULL 이고 http_cmds 를 가지며, register_hooks 가 process_connection 훅에 ap_process_http_connection 을 걸고 HTTP_HEADER, CHUNK 필터를 등록한다. server/mpm/event/event.c 의 mpm_event_module(LoadModule 로 실림)은 설정 함수 넷이 모두 NULL 이고 event_cmds(StartServers 등)를 가지며, event_hooks 가 mpm 훅에 주 루프 event_run 을 건다 코어, HTTP, MPM 이 모두 같은 틀의 구조체 하나씩입니다. 노란 칸은 구조체에 적힌 값, 파란 칸은 register_hooks 가 훅에 거는 함수입니다.

헷갈리기 쉬운 곳

3. 실행 파일에 묶이는 모듈, LoadModule로 싣는 모듈

모듈을 서버에 넣는 길은 두 가지입니다. 빌드할 때 실행 파일에 정적으로 묶는(static) 길과, 실행 중에 .so 파일을 불러오는(shared, DSO) 길입니다.

모듈이 서버에 들어오는 두 길. 빌드할 때 configure가 만든 modules.c의 ap_prelinked_modules 배열(core, so, http)이 ap_setup_prelinked_modules로 먼저 등록되고, 그다음 ap_read_config가 설정 파일을 읽으며 LoadModule 줄마다 mod_so가 apr_dso_load와 apr_dso_sym으로 .so를 불러와 ap_add_loaded_module로 더한다. 설정을 다 읽은 뒤 ap_check_mpm이 mpm 훅에 함수가 정확히 하나인지 확인한다. 왼쪽 아래 갈래가 LoadModule 한 줄이 처리되는 길이고, 오른쪽 갈래가 MPM 검사입니다. 빨간 칸은 설정 오류로 서버가 뜨지 않는 경우입니다.

빌드가 만드는 파일: modules.c

정적으로 묶을 모듈의 목록은 저장소에 없는 파일, modules.c에 있습니다. ./configure를 돌릴 때 build/build-modules-c.awk가 이 파일을 만듭니다(configure.in 1118–1122행). 예제 이미지를 빌드한 컨테이너의 /usr/src/httpd/modules.c를 열어 보았습니다(빌드 결과물이라 GitHub 링크는 없습니다).

/*
 * modules.c --- automatically generated by Apache
 * configuration script.  DO NOT HAND EDIT!!!!!
 */
/* ... */
extern module core_module;
extern module so_module;
extern module http_module;
/* ... */
AP_DECLARE_DATA module *ap_prelinked_modules[] = {
  &core_module,
  &so_module,
  &http_module,
  NULL
};

같은 파일 아래쪽에는 같은 세 모듈을 담은 ap_preloaded_modules[]도 있습니다. 예제 이미지는 --enable-mods-shared=most --enable-mpms-shared=all로 빌드했기 때문에 나머지 모듈과 MPM은 모두 .so가 되었고, 실행 파일에 묶인 것은 이 셋뿐입니다. 셋이 남은 데는 이유가 있습니다.

시작할 때: 묶인 모듈부터 등록

main()은 설정 파일을 읽기도 전에 ap_setup_prelinked_modules()를 부릅니다(server/main.c 574행).

AP_DECLARE(const char *) ap_setup_prelinked_modules(process_rec *process)
{
    /* ... */
    total_modules = 0;
    for (m = ap_preloaded_modules; *m != NULL; m++)
        (*m)->module_index = total_modules++;

    max_modules = total_modules + DYNAMIC_MODULE_LIMIT + 1;
    conf_vector_length = max_modules;
    /* ... */

    /*
     *   Initialize chain of linked (=activate) modules
     */
    for (m = ap_prelinked_modules; *m != NULL; m++) {
        error = ap_add_module(*m, process->pconf, NULL);
        if (error) {
            return error;
        }
    }

    apr_hook_sort_all();
    /* ... */
}

server/config.c 763–816행

묶인 모듈에 0, 1, 2 순서로 module_index를 매기고, 나중에 LoadModule로 들어올 모듈을 위해 DYNAMIC_MODULE_LIMIT(기본 256, include/httpd.h 119–121행)만큼 자리를 더 잡아 둡니다. 그리고 셋을 ap_add_module()로 활성 모듈 목록에 넣습니다. 이 시점에는 core, so, http 세 모듈만 살아 있습니다.

설정을 읽을 때: LoadModule 한 줄

설정 파일에서 LoadModule을 만나면 mod_so의 load_module()이 불립니다. LoadModule이라는 디렉티브 자체가 mod_so의 디렉티브 표에 있기 때문입니다(mod_so.c 448–450행). 핵심만 남기면 이렇습니다.

    error = dso_load(cmd, &modhandle, filename, &module_file);
    /* ... */
    if (apr_dso_sym(&modsym, modhandle, modname) != APR_SUCCESS) {
        /* ... */
    }
    modp = (module*) modsym;
    modp->dynamic_load_handle = (apr_dso_handle_t *)modhandle;
    modi->modp = modp;

    /* ... */
    if (modp->magic != MODULE_MAGIC_COOKIE) {
        return apr_psprintf(cmd->pool, "API module structure '%s' in file %s "
                            /* ... */);
    }

    /* ... */
    error = ap_add_loaded_module(modp, cmd->pool, modname);

modules/core/mod_so.c 265–304행

  1. dso_load()가 1절에서 본 apr_dso_load()로 .so 파일을 프로세스 안으로 불러옵니다.
  2. apr_dso_sym()이 그 파일 안에서 LoadModule의 첫 인자(예: mime_module)라는 이름의 변수를 찾습니다. 이것이 그 모듈의 module 구조체입니다. AP_DECLARE_MODULE(mime)이 만든 바로 그 변수입니다.
  3. magic 값이 맞는지 봅니다. 엉뚱한 파일이거나 다른 버전용으로 컴파일된 모듈을 거르는 장치입니다.
  4. ap_add_loaded_module()로 모듈 목록에 더합니다. 이제 이 모듈도 core와 똑같이 취급됩니다.

MPM은 정확히 하나

설정 파일을 다 읽은 뒤 ap_read_config()는 ap_check_mpm()을 부릅니다(config.c 2355–2360행).

AP_DECLARE(const char *)ap_check_mpm(void)
{
    static const char *last_mpm_name = NULL;

    if (!_hooks.link_mpm || _hooks.link_mpm->nelts == 0)
        return "No MPM loaded.";
    else if (_hooks.link_mpm->nelts > 1)
        return "More than one MPM loaded.";

server/mpm_common.c 656–663행

"MPM이 실렸나"를 이름으로 확인하지 않고 mpm 훅에 걸린 함수의 개수(nelts)로 확인합니다. MPM이 무엇인지에 대한 정의가 "mpm 훅에 함수를 거는 모듈"이라는 뜻입니다.

예제: httpd -l, -M, -V

예제 컨테이너에서 세 옵션을 돌려 보았습니다. -M과 -V는 기본 설정 파일 /opt/httpd/conf/httpd.conf를 읽습니다.

### 1. httpd -l  (실행 파일 안에 정적으로 묶인 모듈)
Compiled in modules:
  core.c
  mod_so.c
  http_core.c

### 2. httpd -M  (기본 httpd.conf 로 실었을 때, 앞 14줄)
Loaded Modules:
 core_module (static)
 so_module (static)
 http_module (static)
 mpm_event_module (shared)
 autht_core_module (shared)
 authn_file_module (shared)
 authn_core_module (shared)
 authz_host_module (shared)
 authz_groupfile_module (shared)
 authz_user_module (shared)
 authz_core_module (shared)
 access_compat_module (shared)
 auth_basic_module (shared)
...
static: 3
shared: 24
.so files in /opt/httpd/modules: 101

### 3. httpd -V  (기본 httpd.conf, 앞 9줄)
Server version: Apache/2.5.1-dev (Unix)
Server built:   Oct 24 2026 11:41:11
Server's Module Magic Number: 20211221:35
Server loaded:  APR 1.7.2, APR-UTIL 1.6.3, PCRE 10.42 2022-12-11
Compiled using: APR 1.7.2, APR-UTIL 1.6.3, PCRE 10.42 2022-12-11
Architecture:   64-bit
Server MPM:     event
  threaded:     yes (fixed thread count)
    forked:     yes (variable process count)

헷갈리기 쉬운 곳: -V가 설정 파일을 읽는 이유

httpd -V는 빌드 정보를 보여 주는 옵션이라 설정 파일과 상관없을 것 같지만, MPM을 .so로 빌드했으면 설정 파일을 읽어야 MPM 이름을 알 수 있습니다. 소스가 그렇게 되어 있습니다.

        case 'V':
            if (strcmp(ap_show_mpm(), "")) { /* MPM built-in? */
                show_compile_settings();
                destroy_and_exit_process(process, 0);
            }
            else {
                showcompile = 1;
                ap_run_mode = AP_SQ_RM_CONFIG_DUMP;
            }
            break;

server/main.c 679–688행

MPM이 실행 파일에 묶여 있으면 바로 찍고 끝내고, 아니면 showcompile만 표시해 두고 설정을 읽은 뒤에 찍습니다. 그래서 MPM을 싣지 않은 설정 파일을 주면 이렇게 됩니다.

### 4. MPM 을 싣지 않은 설정으로 httpd -V / -t
AH00534: httpd: Configuration error: No MPM loaded.
Server MPM:     
AH00534: httpd: Configuration error: No MPM loaded.
(exit code 1)

-V는 오류를 찍고도 빌드 정보는 보여 주지만 Server MPM: 칸이 비어 있습니다. 설정 문법 검사 -t는 같은 오류로 종료 코드 1을 냅니다. 운영 중인 서버에서 httpd -V를 돌렸을 때 MPM이 어떻게 나오는지는 "어느 설정 파일을 읽었나"에 달려 있다는 것을 기억해 두면 좋습니다.

4. 세 개의 큰 구조체: server_rec, conn_rec, request_rec

httpd 소스의 함수들은 대부분 이 셋 가운데 하나를 첫 인자로 받습니다. 모두 include/httpd.h에 있습니다.

구조체 무엇 하나를 나타내나 언제 생기고 언제 사라지나 메모리(풀)
server_rec 서버 하나. 주 서버 하나 + <VirtualHost>마다 하나 설정을 읽을 때 생기고 재시작 때 사라진다 pconf
conn_rec 클라이언트와의 TCP 연결 하나 연결을 받을 때 생기고 연결이 닫히면 사라진다 연결 풀 c->pool
request_rec HTTP 요청 하나 요청 줄을 읽을 때 생기고 응답을 다 보내면 사라진다 요청 풀 r->pool

keep-alive 연결(응답을 보낸 뒤에도 연결을 닫지 않고 다음 요청을 기다리는 연결, httpd.conf의 KeepAlive On)이면 연결 하나에서 요청이 여러 개 오가므로, conn_rec 하나에 request_rec이 차례로 여러 개 생겼다 사라집니다. server_rec은 그보다 훨씬 오래 삽니다.

세 구조체가 서로를 가리키는 그림. request_rec은 r->connection으로 conn_rec을, r->server로 server_rec을 가리킨다. 같은 연결의 다음 요청도 같은 conn_rec을 가리킨다. conn_rec은 c->base_server로 server_rec을 가리킨다. 주 서버 server_rec(ap_server_conf)은 next로 가상 호스트 server_rec들을 잇고, 모든 server_rec은 process로 process_rec을 가리킨다. 화살표는 구조체 안의 포인터 필드입니다. 요청에서 연결로, 연결에서 서버로 가는 길은 있지만 거꾸로 가는 필드는 이 그림에 없습니다.

request_rec: 요청 하나

구조체 맨 앞 필드가 다른 구조체로 가는 길입니다.

struct request_rec {
    /** The pool associated with the request */
    apr_pool_t *pool;
    /** The connection to the client */
    conn_rec *connection;
    /** The virtual host for this request */
    server_rec *server;

    /** Pointer to the redirected request if this is an external redirect */
    request_rec *next;
    /** Pointer to the previous request if this is an internal redirect */
    request_rec *prev;

    /** Pointer to the main request if this is a sub-request
     * (see http_request.h) */
    request_rec *main;

include/httpd.h 866–881행

구조체 전체는 1169행까지 300줄이 넘습니다. 처음 읽을 때 자주 만나는 필드만 표로 모았습니다.

필드 뜻 행
pool, connection, server 요청 풀, 연결, 이 요청을 맡은 서버 868–872
next, prev, main 리다이렉트로 이어진 뒤·앞 요청, 서브요청(요청을 처리하는 도중 다른 URL을 서버 안에서 한 번 더 처리해 보는 것, 10편)이면 원래(main) 요청 875–881
the_request 요청 첫 줄 그대로(예: GET /three HTTP/1.1) 887
method, protocol, hostname 메서드, 프로토콜, Host로 정해진 호스트 이름 921, 900, 904
status 응답 상태 코드 912
headers_in, headers_out, err_headers_out 요청 헤더, 응답 헤더, 오류 응답에도 붙는 헤더 997–1002
notes, subprocess_env 모듈끼리 주고받는 메모, CGI 등에 넘길 환경 변수 1004–1006
content_type, handler 응답 종류, 이 요청을 처리할 핸들러 이름 1013–1015
user 인증된 사용자 이름 1026
unparsed_uri, uri, filename, path_info, args URL 원문, 경로, 디스크의 파일 경로, PATH_INFO, 쿼리 문자열 1035–1047
per_dir_config, request_config 이 요청에 적용될 디렉터리 설정, 요청별 모듈 메모 1068–1070
output_filters, input_filters 이 요청의 출력·입력 필터 사슬 1091–1093

uri가 filename이 되는 과정은 10편, handler로 핸들러가 골라지는 과정은 12편, 필터 사슬은 11편에서 봅니다.

conn_rec: 연결 하나

struct conn_rec {
    /** Pool associated with this connection */
    apr_pool_t *pool;
    /** Physical vhost this conn came in on */
    server_rec *base_server;
    /** used by http_vhost.c */
    void *vhost_lookup_data;

include/httpd.h 1203–1209행

필드 뜻 행
pool, base_server 연결 풀, 연결이 들어온 IP·포트로 정해진 서버 1205–1207
client_ip, local_ip 상대(바로 앞 홉)의 IP, 서버 쪽 IP 1222, 1233
id 연결 번호 1239
conn_config, notes 연결별 모듈 메모, 모듈끼리 주고받는 메모 1242–1245
input_filters, output_filters 연결 단위 필터 사슬(소켓에 가까운 쪽) 1247–1249
bucket_alloc 버킷을 만들 때 쓰는 할당기 1253
cs MPM이 쓰는 연결 상태(event MPM의 상태 기계) 1255
aborted, keepalive, keepalives 끊겼나, 다음 요청을 받을 건가, 이 연결로 몇 번째 요청인가 1267–1274

server_rec: 서버(가상 호스트) 하나

struct server_rec {
    /** The process this server is running in */
    process_rec *process;
    /** The next server in the list */
    server_rec *next;

include/httpd.h 1401–1405행

필드 뜻 행
process, next 프로세스 정보, 다음 서버(가상 호스트 목록) 1403–1405
error_log 이 서버의 오류 로그 파일 1412
module_config 모듈마다 한 칸씩인 서버 설정 배열 1424
lookup_defaults 이 서버의 기본 디렉터리 설정 1426
defn_name, is_virtual 설정 파일의 어디서 정의됐나, 가상 호스트인가 1429, 1433
port, server_hostname ServerName의 포트와 이름 1439, 1448
timeout, keep_alive_timeout, keep_alive_max, keep_alive Timeout, KeepAliveTimeout, MaxKeepAliveRequests, KeepAlive 1455–1461
names, wild_names ServerAlias 목록 1464–1466

server_rec 하나는 설정 파일의 <VirtualHost> 블록 하나입니다. <VirtualHost>를 처리하는 코드가 새 server_rec을 만들어 주 서버 바로 뒤에 끼웁니다.

    s->next = main_server->next;
    main_server->next = s;

server/core.c 3065–3066행

가상 호스트 목록이 만들어지는 순서. 처음에는 주 서버 localhost(ap_server_conf)만 있다. 설정 파일 14행의 a.example 을 읽으면 localhost 다음에 a.example 이 끼워진다. 17행의 b.example 을 읽으면 b.example 이 주 서버 바로 뒤에 끼워져 목록은 localhost, b.example, a.example 이 된다. 새 server_rec s 를 끼우는 두 줄 s->next = main_server->next; main_server->next = s; 때문에 새 가상 호스트는 늘 주 서버 바로 뒤에 들어간다 두 줄이 목록을 어떻게 바꾸는지 아래 예제의 설정(14행 a.example, 17행 b.example)으로 따라가 봤습니다. 초록 칸이 방금 끼운 가상 호스트입니다.

모듈의 설정은 어디에: void * 배열과 module_index

server_rec의 module_config와 request_rec의 per_dir_config에는 mod_mime의 설정, mod_dir의 설정처럼 모듈마다 다른 모양의 설정 구조체가 들어갑니다. 구조체 정의에는 그런 필드가 하나도 없습니다. 대신 모듈마다 한 칸씩인 void * 배열을 두고, 모듈은 자기 module_index번 칸만 씁니다.

#define ap_get_module_config(v,m)       \
    (((void **)(v))[(m)->module_index])
#define ap_set_module_config(v,m,val)   \
    ((((void **)(v))[(m)->module_index]) = (val))

include/http_config.h 553–556행

void *는 "무엇이든 가리킬 수 있는 포인터"입니다. 배열의 module_index번째 칸을 꺼내 각 모듈이 자기 구조체 타입으로 바꿔 씁니다. 3절에서 본 mod_so의 -M 출력 코드도 ap_get_module_config(s->module_config, &so_module)로 자기 칸을 꺼냅니다(mod_so.c 394–395행). 덕분에 모듈이 몇 개가 실리든 server_rec의 모양은 바뀌지 않습니다. 이 칸이 어떻게 채워지고 <Directory>마다 어떻게 합쳐지는지는 7편에서 봅니다. 타입 검사가 없다는 단점은 2편에서 다룹니다.

예제: 요청 하나에서 세 구조체 찍기

세 구조체가 실제로 이렇게 이어져 있는지 확인하려고 작은 모듈을 썼습니다. /three로 온 요청을 받으면 r, r->connection, r->server의 필드 몇 개와 서버 목록, 풀의 부모 관계를 응답 본문에 찍습니다.

/* 이 글을 위해 쓴 예제입니다 (httpd 의 소스가 아닙니다) */
/* 요청 하나를 받으면 request_rec, conn_rec, server_rec 의 필드 몇 개와
 * 세 구조체가 서로를 가리키는 모습, 풀의 부모 관계를 응답 본문에 찍는다. */
#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "http_main.h"     /* ap_server_conf */
#include "apr_pools.h"

static int bigthree_handler(request_rec *r)
{
    conn_rec *c = r->connection;     /* 요청 → 연결 */
    server_rec *s = r->server;       /* 요청 → 이 요청을 맡은 (가상) 서버 */
    server_rec *v;

    if (!r->handler || strcmp(r->handler, "bigthree")) {
        return DECLINED;             /* 내 일이 아니면 다음 핸들러에게 넘긴다 */
    }
    ap_set_content_type(r, "text/plain");

    ap_rprintf(r, "[request_rec r=%pp]\n", r);
    ap_rprintf(r, "  the_request      = %s\n", r->the_request);
    ap_rprintf(r, "  uri              = %s\n", r->uri);
    ap_rprintf(r, "  hostname         = %s\n", r->hostname);
    ap_rprintf(r, "  handler          = %s\n", r->handler);

    ap_rprintf(r, "[conn_rec c=%pp]\n", c);
    ap_rprintf(r, "  client_ip        = %s\n", c->client_ip);
    ap_rprintf(r, "  local_ip         = %s\n", c->local_ip);
    ap_rprintf(r, "  keepalives       = %d\n", c->keepalives);
    ap_rprintf(r, "  base_server      = %s\n", c->base_server->server_hostname);

    ap_rprintf(r, "[server_rec s=%pp]\n", s);
    ap_rprintf(r, "  server_hostname  = %s\n", s->server_hostname);
    ap_rprintf(r, "  is_virtual       = %d\n", s->is_virtual);
    ap_rprintf(r, "  defined at       = %s:%u\n", s->defn_name, s->defn_line_number);
    ap_rprintf(r, "  process          = %s\n", s->process->short_name);

    /* s->next 로 이어진 서버 목록 전체. 처음은 ap_server_conf(주 서버) */
    ap_rprintf(r, "[server list from ap_server_conf]\n");
    for (v = ap_server_conf; v; v = v->next) {
        ap_rprintf(r, "  %s%s\n", v->server_hostname, v == s ? "   <- r->server" : "");
    }

    /* 풀의 부모 관계 */
    ap_rprintf(r, "[pools]\n");
    ap_rprintf(r, "  parent(r->pool) == c->pool : %s\n",
               apr_pool_parent_get(r->pool) == c->pool ? "yes" : "no");
    ap_rprintf(r, "  pconf is ancestor of r->pool : %s\n",
               apr_pool_is_ancestor(s->process->pconf, r->pool) ? "yes" : "no");
    return OK;
}

static void register_hooks(apr_pool_t *p)
{
    ap_hook_handler(bigthree_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

AP_DECLARE_MODULE(bigthree) = {
    STANDARD20_MODULE_STUFF,
    NULL,                  /* 디렉터리 설정 만들기 */
    NULL,                  /* 디렉터리 설정 합치기 */
    NULL,                  /* 서버 설정 만들기 */
    NULL,                  /* 서버 설정 합치기 */
    NULL,                  /* 디렉티브 표 */
    register_hooks         /* 훅 등록 */
};

2절에서 본 core_module, http_module과 같은 모양의 module 구조체를 하나 만들고, register_hooks에서 handler 훅에 함수를 하나 겁니다. 이 모듈을 apxs -c로 .so로 컴파일해 LoadModule bigthree_module /work/.libs/mod_bigthree.so로 실었습니다. 설정 파일(/tmp/three.conf)에는 <Location /three>에 SetHandler bigthree를 두고, 같은 포트 8080에 가상 호스트 둘을 이 순서로 두었습니다.

<VirtualHost *:8080>
  ServerName a.example
</VirtualHost>
<VirtualHost *:8080>
  ServerName b.example
</VirtualHost>

a.example은 설정 파일 14행, b.example은 17행에서 시작합니다. 주 서버의 ServerName은 localhost입니다. curl로 Host: b.example을 보내며 같은 URL을 두 번 요청했습니다. curl은 한 명령에 URL을 둘 주면 연결 하나를 다시 씁니다.

--- curl: Host: b.example, 같은 연결로 두 번
[request_rec r=7f212c7620a0]
  the_request      = GET /three HTTP/1.1
  uri              = /three
  hostname         = b.example
  handler          = bigthree
[conn_rec c=7f212c797350]
  client_ip        = 127.0.0.1
  local_ip         = 127.0.0.1
  keepalives       = 0
  base_server      = a.example
[server_rec s=7f212d032330]
  server_hostname  = b.example
  is_virtual       = 1
  defined at       = /tmp/three.conf:17
  process          = httpd
[server list from ap_server_conf]
  localhost
  b.example   <- r->server
  a.example
[pools]
  parent(r->pool) == c->pool : yes
  pconf is ancestor of r->pool : yes
[request_rec r=7f212c7600a0]
...
[conn_rec c=7f212c797350]
...
  keepalives       = 1
...

두 번째 요청의 출력은 첫 번째와 달라진 줄만 남기고 ...로 줄였습니다. r=, c=, s= 뒤의 주소는 실행마다 다릅니다. 읽을 곳은 이렇습니다.

  1. 두 요청의 r 주소는 다르고 c 주소는 같습니다. 요청마다 request_rec이 새로 생기지만 둘 다 같은 conn_rec을 가리킵니다. keepalives가 0에서 1로 늘었습니다.
  2. c->base_server는 a.example, r->server는 b.example입니다. 연결이 들어온 순간에는 IP와 포트(*:8080)만 알기 때문에 그 주소의 첫 번째 가상 호스트가 base_server가 되고, 요청 헤더를 읽은 뒤 Host: b.example을 보고 r->server가 정해집니다. 이 선택 과정은 9편에서 server/vhost.c를 읽으며 봅니다.
  3. r->server가 가리키는 server_rec은 설정 파일 17행의 <VirtualHost>입니다. defn_name, defn_line_number로 확인했습니다.
  4. 요청 풀의 부모는 연결 풀입니다. 그리고 프로세스의 pconf는 요청 풀의 조상입니다(5절).

예제 출력을 옮긴 그림. 같은 연결로 보낸 두 요청은 request_rec 주소가 7f212c7620a0 과 7f212c7600a0 으로 다르지만, 둘 다 r->connection 으로 같은 conn_rec 7f212c797350 을 가리키고 그 keepalives 는 0 에서 1 로 늘었다. conn_rec 의 c->base_server 는 IP와 포트 *:8080 의 첫 가상 호스트인 a.example(three.conf 14행)이고, 첫 요청의 r->server 는 Host 헤더를 읽은 뒤 정해진 b.example(three.conf 17행, is_virtual=1)이다. ap_server_conf 부터 next 를 따라간 서버 목록은 localhost, b.example, a.example 순서다 위 출력의 주소와 줄 번호를 그대로 옮겼습니다(주소는 실행마다 다릅니다). 파란 칸이 요청, 노란 칸이 연결, 초록 칸이 서버입니다.

Host를 어느 가상 호스트에도 없는 이름으로 바꾸면 r->server는 ap_create_request()가 처음 넣어 둔 c->base_server(a.example) 그대로 남습니다(5절의 코드).

--- curl: Host: nosuch.example (conn_rec, server_rec 부분만)
[conn_rec c=7f212c797360]
  client_ip        = 127.0.0.1
  local_ip         = 127.0.0.1
  keepalives       = 0
  base_server      = a.example
[server_rec s=7f212d031ad0]
  server_hostname  = a.example
  is_virtual       = 1
  defined at       = /tmp/three.conf:14
  process          = httpd

헷갈리기 쉬운 곳

5. 풀 계층 한눈에

세 구조체의 수명은 각자의 풀이 정합니다. 풀은 부모·자식으로 이어져 있어서, 부모 풀을 지우면 자식 풀도 함께 지워집니다.

httpd의 풀 계층. 맨 위 process 풀(ap_pglobal) 아래 plog와 pconf가 있고, pconf 아래에 ptemp, pchild, 연결마다의 transaction 풀이 있다. transaction 아래 master_conn(c->pool), 그 아래 request(r->pool), 그 아래 subrequest 같은 작은 풀이 있다. 노란 칸은 모두 풀입니다. 오른쪽 아래로 내려갈수록 수명이 짧습니다. transaction 풀의 부모는 MPM마다 다릅니다(아래 표).

풀 어디서 만드나 수명 여기 담기는 것
process (ap_pglobal) init_process(), main.c 352·375행 프로세스 전체 몇 안 되는 전역 정보
pconf main.c 336–337행 재시작할 때마다 비운다 server_rec, 모듈 설정, 불러온 .so
plog, ptemp main.c 705–708행 로그용 / 설정을 읽는 동안만 로그 관련, 임시 값
transaction (ptrans) event: event.c 2250–2253행 (부모 pconf) 연결 하나 conn_rec 구조체 자체
master_conn = c->pool core_create_conn(), core.c 5525–5527행 연결 하나 연결의 필드들(IP 문자열, notes 등)
request = r->pool ap_create_request(), protocol.c 1063–1067행 요청 하나 request_rec과 헤더 표, URI 문자열 등

요청 풀을 만드는 코드는 짧습니다.

AP_DECLARE(request_rec *) ap_create_request(conn_rec *conn)
{
    request_rec *r;
    apr_pool_t *p;

    apr_pool_create(&p, conn->pool);
    apr_pool_tag(p, "request");
    r = apr_pcalloc(p, sizeof(request_rec));
    AP_READ_REQUEST_ENTRY((intptr_t)r, (uintptr_t)conn);
    r->pool            = p;
    r->connection      = conn;
    r->server          = conn->base_server;

server/protocol.c 1058–1069행

연결 풀(conn->pool)의 자식으로 요청 풀을 만들고, 그 풀 안에 request_rec을 만듭니다(apr_pcalloc은 풀에서 0으로 채운 메모리를 꺼내는 함수입니다). 그래서 요청 풀을 지우면 request_rec 자신과 그 안의 문자열·표가 한꺼번에 사라집니다. r->server를 처음에는 conn->base_server로 두는 것도 보입니다. Host 헤더를 읽은 뒤 이것을 바꾸는 곳은 server/vhost.c 1075행입니다(4절 예제의 2번).

요청 풀은 응답이 끝까지 나간 뒤 EOR(End Of Request)이라는 특별한 버킷이 지워질 때 함께 지워집니다(server/eor_bucket.c 100–112행의 apr_pool_destroy(r->pool)). 버킷은 11편에서 봅니다.

풀을 만들고 지우는 APR 쪽 구현, apr_pool_cleanup_register()로 "풀이 지워질 때 할 일"을 거는 방법은 4편의 주제입니다. pconf를 재시작 때마다 비우고 설정을 다시 읽는 과정은 5편에서 봅니다.

헷갈리기 쉬운 곳: trunk와 2.4.x는 연결 풀이 한 겹 다르다

기준 커밋(trunk — 다음 판을 준비하는 개발 줄기)에서는 conn_rec 구조체를 transaction 풀에 만들고, c->pool은 그 아래에 따로 만든 master_conn 풀입니다(core.c 5516행과 5525–5527행). 우리가 흔히 설치하는 2.4.x 브랜치의 core_create_conn()은 c->pool = ptrans;로 transaction 풀을 그대로 연결 풀로 씁니다. 모듈 입장에서 "c->pool은 연결과 수명이 같다"는 사실은 같지만, 2.4.x의 글이나 코드를 함께 볼 때 풀 계층이 한 단계 다르게 보일 수 있습니다.

transaction 풀의 부모도 MPM마다 다릅니다. event와 worker는 pconf 아래에 만들고(worker.c 673행), prefork는 자식 프로세스의 pchild 아래에 만듭니다(prefork.c 469행). 그림은 이 글의 예제가 쓴 event를 기준으로 그렸습니다.

6. 요청 하나가 지나가는 길

이제 층과 구조체를 한 줄로 꿰어 봅니다. 클라이언트가 연결을 열고 GET /index.html을 보내면 이 길을 지나갑니다.

요청 하나가 지나가는 길. MPM 리스너가 accept한 뒤 ap_run_create_connection이 core_create_conn으로 conn_rec을 만들고, ap_pre_connection이 연결에 필터를 끼우고, ap_run_process_connection이 ap_process_http_connection을 부른다. ap_read_request가 request_rec을 만들고 요청 줄과 헤더를 읽고, ap_process_request_internal이 translate_name부터 fixups까지 단계를 돌고, ap_invoke_handler가 handler 훅을 부르고, 응답은 출력 필터와 CORE 필터를 거쳐 소켓으로 나간다. 마지막에 EOR 버킷이 r->pool을 지우고, keep-alive면 같은 연결의 다음 요청으로 돌아간다. 파란 칸은 코어가 훅을 부르는 자리이고, 칸마다 그 단계를 자세히 읽는 글의 번호를 적었습니다.

단계 누가 함수 파일 자세히
연결 받기 MPM 리스너 스레드, process_socket() server/mpm/event/event.c 8편
conn_rec 만들기 core_module ap_run_create_connection() → core_create_conn() server/core.c 9편
연결 준비 core_module 등 ap_pre_connection() → core_pre_connection()이 CORE_IN·CORE 필터를 끼움 server/core.c 9편, 15편(TLS가 끼는 자리)
연결 처리 http_module ap_run_process_connection() → ap_process_http_connection() modules/http/http_core.c 9편
요청 읽기 코어 ap_read_request() → ap_create_request() server/protocol.c 9편
처리 단계 코어 + 여러 모듈 ap_process_request_internal(): translate_name → map_to_storage → 접근·인증·권한 → type_checker → fixups server/request.c 10편, 13편
응답 만들기 핸들러 모듈 ap_invoke_handler() → handler 훅(정적 파일이면 default_handler) server/config.c, server/core.c 12편
내보내기 필터들 출력 필터 사슬 → CORE 필터가 소켓에 씀 server/util_filter.c, server/core_filters.c 11편
마무리 코어 EOR 버킷 → 접근 로그 기록, r->pool 해제 server/eor_bucket.c 16편(로그)

표를 세로로 읽으면 1절의 층이 그대로 보입니다. 맨 위 한 칸만 MPM이고, 나머지는 코어가 훅을 부르면 core_module, http_module, 그리고 다른 모듈들이 차례로 일하는 구조입니다. 코어가 직접 하는 일은 생각보다 적고, 대부분 "다음 훅을 부르는 일"입니다.

연결 처리 칸의 ap_process_http_connection()은 MPM의 방식에 따라 두 갈래로 나뉩니다.

static int ap_process_http_connection(conn_rec *c)
{
    if (async_mpm && !c->clogging_input_filters) {
        return ap_process_http_async_connection(c);
    }
    else {
        return ap_process_http_sync_connection(c);
    }
}

modules/http/http_core.c 247–255행

prefork나 worker처럼 연결 하나를 스레드(또는 프로세스) 하나가 끝까지 붙잡는 방식이면 sync 쪽이 while ((r = ap_read_request(c)) != NULL) 루프로 keep-alive 요청을 차례로 처리합니다(http_core.c 197행). event는 async 쪽으로 가서 요청 하나만 처리하고 연결을 MPM에 돌려줍니다. 다음 요청이 오면 MPM이 다시 process_connection 훅을 부릅니다. 그림에서 되돌아가는 화살표가 ap_run_process_connection()을 가리키는 이유입니다.

헷갈리기 쉬운 곳: ap_process_connection()을 event는 부르지 않는다

server/connection.c에는 이름 그대로인 함수가 있습니다.

AP_CORE_DECLARE(void) ap_process_connection(conn_rec *c, void *csd)
{
    ap_update_vhost_given_ip(c);

    ap_pre_connection(c, csd);

    if (!c->aborted) {
        ap_run_process_connection(c);
    }
}

server/connection.c 244–253행

prefork(prefork.c 693행)와 worker(worker.c 496행)는 이 함수를 부릅니다. 그런데 event의 process_socket()은 이 함수를 거치지 않고 같은 일을 직접 합니다. ap_run_create_connection(), ap_update_vhost_given_ip(), ap_pre_connection()을 차례로 부르고, 나중에 ap_run_process_connection()을 부릅니다(event.c 1075–1153행). 연결을 MPM과 코어 사이에서 여러 번 주고받아야 하기 때문입니다. 소스를 따라 읽다가 "event에서는 ap_process_connection()에 중단점을 걸어도 안 멈춘다"는 일이 생기는 이유입니다.

이 글에서 뺀 것

지도를 한 장으로 유지하려고 뺀 것들입니다.

정리

개념 한 줄 요약
다섯 층 운영체제 ← APR·APR-util ← 코어(server/) ← MPM(server/mpm/)·모듈(modules/). 코어와 모듈·MPM 사이는 훅으로 서로 부른다
APR OS 차이를 apr_ 함수 뒤에 숨기는 별도 라이브러리. 풀·소켓·DSO·스레드 등
코어도 모듈 core_module(server/core.c), http_module(modules/http/http_core.c), so_module, MPM까지 모두 같은 module 구조체
정적 vs 공유 configure가 만든 modules.c의 모듈(예제 빌드에서는 core·so·http)은 실행 파일에, 나머지는 LoadModule → mod_so → apr_dso_load()
MPM 검사 ap_check_mpm()이 mpm 훅에 걸린 함수가 정확히 하나인지 본다
server_rec 서버(가상 호스트) 하나. pconf에 살고 next로 이어진다. 새 가상 호스트는 주 서버 바로 뒤에 끼운다
conn_rec TCP 연결 하나. c->pool, c->base_server(IP·포트로 고른 서버)
request_rec 요청 하나. r->pool, r->connection, r->server(Host로 고른 서버)
모듈 설정 구조체마다 void * 배열을 두고 module_index번 칸을 꺼낸다(ap_get_module_config)
풀 계층 process → pconf → transaction → c->pool → r->pool. 요청 풀은 EOR 버킷이 지운다
요청의 길 MPM accept → create_connection → pre_connection → process_connection → ap_read_request → ap_process_request_internal → handler → 필터 → 소켓

다음에 볼 것

지도를 접고 첫 번째 바닥부터 읽습니다. 다음 글은 이 글에서 계속 "풀"이라고만 부른 APR 메모리 풀입니다. apr_pool_create()로 부모·자식 풀을 만들고, apr_palloc()으로 꺼내 쓰고, apr_pool_destroy()로 한꺼번에 지울 때 APR 안에서 무슨 일이 일어나는지, 그리고 요청 풀에 cleanup을 걸어 요청이 끝나는 순간을 잡는 예제를 봅니다.

Apache httpd 소스 파헤치기 4: APR 메모리 풀 — free 없이 메모리를 관리하는 법

Spring 연재에서도 같은 자리에 전체 지도를 펴 보았습니다. Spring은 모듈을 빌드할 때 jar로 나누고 실행 중에는 컨테이너 하나가 빈을 엮는 반면, httpd는 실행 중에 .so를 불러와 같은 module 구조체로 다룹니다. 다른 모듈의 동작에 끼어드는 장치도 Spring은 BeanPostProcessor 같은 확장 지점을, httpd는 훅을 씁니다. 요청 하나를 단계별로 처리하는 DispatcherServlet은 Spring 21편에서 볼 수 있는데, 6절의 표와 나란히 놓고 보면 두 프로젝트가 "요청을 여러 단계로 나누고 각 단계에 꽂을 자리를 둔다"는 같은 생각을 다른 언어로 구현했다는 것이 보입니다.

다른 글