이 글에서 다루는 것
"Apache httpd 소스 파헤치기"의 세 번째 글입니다. 1편에서 소스를 받아 빌드하고 폴더 지도를 펴 보았고, 2편에서는 httpd 같은 큰 C 프로그램을 읽을 때 필요한 구조체·함수 포인터·매크로를 익혔습니다. 이번 글은 코드를 한 줄씩 읽기 전에 펴 놓는 전체 지도입니다.
httpd 저장소를 열면 server/, modules/, include/, os/ 같은 폴더에 C 파일이 수백 개 있습니다. 어디가 중심인지, 무엇이 무엇을 부르는지 처음에는 알 수 없습니다. 이 글은 다음 질문에 답합니다.
- httpd는 어떤 층으로 나뉘어 있고, 각 층은 저장소의 어느 폴더에 있는가
- "코어도 모듈이다"는 무슨 뜻인가. 실행 파일 안에 묶인 모듈과
LoadModule로 싣는 모듈은 어떻게 다른가 - 소스 곳곳에 나오는
server_rec,conn_rec,request_rec은 무엇이고 서로를 어떻게 가리키는가 - 메모리를 담는 풀은 어떤 계층으로 되어 있고, 요청 하나는 어떤 길로 서버를 지나가는가
| 항목 | 내용 |
|---|---|
| 대상 | 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를 층으로 나누면 이렇게 됩니다.
화살표는 "위가 아래를 가져다 쓴다"는 뜻입니다. 테두리가 굵은 코어가 가운데에서 훅을 정의하고 부릅니다.
- 운영체제. Linux, Windows 같은 OS입니다. httpd 저장소의
os/폴더에는 OS마다 조금씩 필요한 보충 코드가 있습니다(os/unix,os/win32등). - APR · APR-util. OS 위에 깔리는 바닥입니다. 풀, 소켓, 파일, 스레드, DSO 불러오기, 버킷, 훅 같은 기본 도구가 여기 있습니다. httpd 저장소 밖에 있는 별도 프로젝트입니다.
- 코어.
server/폴더입니다.main()이 있는main.c, 설정 파일을 읽는config.c, 요청을 읽는protocol.c, 요청 처리 단계를 정한request.c등 C 파일 41개가 있습니다. 코어는 "언제 무엇을 할지"의 순서를 쥐고, 그 순서의 각 자리를 훅으로 열어 둡니다. - MPM.
server/mpm/아래event,worker,prefork등입니다. 프로세스·스레드를 만들고 연결을 받아 코어에 넘깁니다. - 모듈.
modules/폴더입니다.aaa(인증·권한),mappers(URL을 파일로),filters,proxy,ssl같은 하위 폴더 26개로 나뉘어 있습니다. 코어의 훅에 함수를 걸어 기능을 더합니다.
그 밖에 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 */
};
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);
/* ... */
}
core_create_conn은 연결이 들어왔을 때conn_rec을 만드는 함수입니다(5·6절).ap_core_translate는 URL을DocumentRoot아래 파일 경로로 바꾸는 함수입니다(10편).default_handler는 정적 파일을 내보내는 함수입니다(12편).
셋 다 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편에서 읽습니다.
코어, HTTP, MPM 이 모두 같은 틀의 구조체 하나씩입니다. 노란 칸은 구조체에 적힌 값, 파란 칸은 register_hooks 가 훅에 거는 함수입니다.
헷갈리기 쉬운 곳
- "코어"라는 말이 두 가지로 쓰입니다. 넓게는
server/폴더 전체(설정 읽기, 요청 읽기, 훅 실행 등 서버의 뼈대)를 가리키고, 좁게는server/core.c의core_module하나를 가리킵니다.server/config.c나server/request.c는 모듈이 아니라 모든 모듈이 함께 쓰는 함수 모음입니다. 이 글에서 "코어 층"은 넓은 뜻,core_module은 좁은 뜻입니다. STANDARD20_MODULE_STUFF와MPM20_MODULE_STUFF는 한 칸 차이입니다. 일반 모듈용인 앞의 것은 끝에rewrite_args칸까지NULL로 채우고(http_config.h 489–496행), MPM용인 뒤의 것은 그 칸을 비워 둡니다. 그래서core_module과mpm_event_module은 구조체 초기값에 한 줄(AP_PLATFORM_REWRITE_ARGS_HOOK또는NULL)이 더 있습니다. 유닉스에서 이 매크로는ap_mpm_rewrite_args입니다(os/unix/os.h 45행).os/폴더가 하는 일의 한 예입니다.- 모듈의
name은 모듈 이름이 아니라 파일 이름입니다.STANDARD20_MODULE_STUFF안에__FILE__이 들어가므로core_module의name은"core.c"입니다. 그래서 훅 순서를 정할 때도"core.c"같은 파일 이름으로 다른 모듈을 가리킵니다(event_hooks의aszSucc[] = { "core.c", NULL }, event.c 4370행). 3절의httpd -l출력에 파일 이름이 나오는 것도 이 때문입니다.
3. 실행 파일에 묶이는 모듈, LoadModule로 싣는 모듈
모듈을 서버에 넣는 길은 두 가지입니다. 빌드할 때 실행 파일에 정적으로 묶는(static) 길과, 실행 중에 .so 파일을 불러오는(shared, DSO) 길입니다.
왼쪽 아래 갈래가 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가 되었고, 실행 파일에 묶인 것은 이 셋뿐입니다. 셋이 남은 데는 이유가 있습니다.
core는 늘 맨 앞입니다. 파일을 만드는 awk 스크립트가# the core module must come first라는 주석과 함께 처음부터 core를 넣고 시작합니다(build/build-modules-c.awk 17–19행).so는.so로 만들 수 없습니다..so를 불러오는 모듈이.so라면 처음에 그것을 불러올 방법이 없기 때문입니다. 빌드 설정이shared를 달라고 하면mod_so can not be built as a shared DSO오류를 내고(modules/core/config.m4 15–17행),yes는static으로 바꿉니다(25–28행).http는 기본으로 정적입니다.modules/http/config.m4의 설명에 "This module will always be linked statically."라고 적혀 있고, 기본값 인자도static입니다(modules/http/config.m4 17행). 기본값이static인 모듈은--enable-mods-shared=most같은 일괄 옵션으로는 바뀌지 않고, 이 모듈만 콕 집어 바꾸라고 지정해야 바뀝니다("enabled as static by default, must be explicitly changed", acinclude.m4 283행).
시작할 때: 묶인 모듈부터 등록
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();
/* ... */
}
묶인 모듈에 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행
dso_load()가 1절에서 본apr_dso_load()로.so파일을 프로세스 안으로 불러옵니다.apr_dso_sym()이 그 파일 안에서LoadModule의 첫 인자(예:mime_module)라는 이름의 변수를 찾습니다. 이것이 그 모듈의module구조체입니다.AP_DECLARE_MODULE(mime)이 만든 바로 그 변수입니다.magic값이 맞는지 봅니다. 엉뚱한 파일이거나 다른 버전용으로 컴파일된 모듈을 거르는 장치입니다.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.";
"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)
-l은 설정 파일을 읽지 않고 실행 파일에 묶인 모듈만 보여 줍니다. 찍는 것이 각 모듈의name이라서core.c처럼 파일 이름이 나옵니다(config.c 2507–2514행).-M은 설정 파일을 읽은 뒤mod_so가 목록을 찍습니다.modules.c의 목록은(static)으로,LoadModule로 실은 목록은(shared)로 찍습니다(mod_so.c 378–419행). 빌드된.so는 101개지만 기본 httpd.conf가 실제로 싣는 것은 24개입니다. 나머지는#LoadModule로 주석 처리되어 있습니다.-V의Server MPM: event는 기본 httpd.conf 66행의LoadModule mpm_event_module modules/mod_mpm_event.so에서 온 것입니다(다음 항목).
헷갈리기 쉬운 곳: -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;
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: 요청 하나
구조체 맨 앞 필드가 다른 구조체로 가는 길입니다.
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;
구조체 전체는 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;
| 필드 | 뜻 | 행 |
|---|---|---|
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;
| 필드 | 뜻 | 행 |
|---|---|---|
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;
두 줄이 목록을 어떻게 바꾸는지 아래 예제의 설정(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= 뒤의 주소는 실행마다 다릅니다. 읽을 곳은 이렇습니다.
- 두 요청의
r주소는 다르고c주소는 같습니다. 요청마다request_rec이 새로 생기지만 둘 다 같은conn_rec을 가리킵니다.keepalives가 0에서 1로 늘었습니다. c->base_server는a.example,r->server는b.example입니다. 연결이 들어온 순간에는 IP와 포트(*:8080)만 알기 때문에 그 주소의 첫 번째 가상 호스트가base_server가 되고, 요청 헤더를 읽은 뒤Host: b.example을 보고r->server가 정해집니다. 이 선택 과정은 9편에서server/vhost.c를 읽으며 봅니다.r->server가 가리키는server_rec은 설정 파일 17행의<VirtualHost>입니다.defn_name,defn_line_number로 확인했습니다.- 요청 풀의 부모는 연결 풀입니다. 그리고 프로세스의
pconf는 요청 풀의 조상입니다(5절).
위 출력의 주소와 줄 번호를 그대로 옮겼습니다(주소는 실행마다 다릅니다). 파란 칸이 요청, 노란 칸이 연결, 초록 칸이 서버입니다.
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
헷갈리기 쉬운 곳
- 서버 목록의 순서는 설정 파일의 순서와 반대입니다. 위 출력에서
ap_server_conf부터next를 따라가면localhost → b.example → a.example입니다. 설정 파일에는a.example이 먼저 있었습니다. 앞에서 본 core.c 3065–3066행이 새 가상 호스트를 목록 끝이 아니라 주 서버 바로 뒤에 끼우기 때문입니다. 그런데도base_server로 뽑힌 쪽은 설정 파일에서 먼저 나온a.example이었습니다. 설정을 다 읽은 뒤server/vhost.c가 주소별 표를 따로 만들면서 이 순서를 되돌려 놓기 때문입니다. 소스 주석에도 "They're listed off of main_s->next in the reverse order they occurred in the config file"(vhost.c 586–587행), "iterating backwards, so each one we see becomes the current default server"(539–540행)라고 적혀 있습니다. 그 표는 9편에서 봅니다. r->server와c->base_server는 다를 수 있습니다. 요청을 처리하는 코드에서 설정을 꺼낼 때는 보통r->server를 써야 합니다. 이름 기반 가상 호스트에서c->base_server를 쓰면 엉뚱한 가상 호스트의 설정을 읽게 됩니다.server_rec에는pool필드가 없습니다.request_rec과conn_rec은 맨 앞에 자기 풀을 갖지만,server_rec은pconf에 만들어지고 자기 풀을 따로 갖지 않습니다.ap_read_config()가process->pconf를 받아 주 서버를 만드는 것을 config.c 2301–2307행과 2224–2227행에서 볼 수 있습니다.c->id는 연결마다 고정된 번호가 아닐 수 있습니다. event MPM은 같은 연결의 다음 요청을 처리할 때 그 순간의 스레드 번호로c->id를 다시 정합니다(주석: "Subsequent request on a conn, and thread number is part of ID", event.c 1130–1131행). 그래서 예제에서 같은 연결인지는c의 주소로 확인했습니다.
5. 풀 계층 한눈에
세 구조체의 수명은 각자의 풀이 정합니다. 풀은 부모·자식으로 이어져 있어서, 부모 풀을 지우면 자식 풀도 함께 지워집니다.
노란 칸은 모두 풀입니다. 오른쪽 아래로 내려갈수록 수명이 짧습니다. 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;
연결 풀(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 | 리스너 스레드, 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);
}
}
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()에 중단점을 걸어도 안 멈춘다"는 일이 생기는 이유입니다.
이 글에서 뺀 것
지도를 한 장으로 유지하려고 뺀 것들입니다.
- 훅을 선언·구현하는 매크로(
AP_DECLARE_HOOK,AP_IMPLEMENT_HOOK_RUN_FIRST등)와 훅 정렬.include/ap_hooks.h, APR-util의include/apr_hooks.h— 6편 ap_add_module()과ap_add_loaded_module()의 내부,ap_single_module_configure().server/config.cprocess_rec의 나머지(argv,short_name)와main()의 두 번 읽기 루프.server/main.c— 5편conn_rec의slaves·master(한 연결 안에 따로 만드는 보조 연결),conn_state_t의 상태 기계.include/httpd.h1296–1299행·1359–1364행,server/mpm/event/event.crequest_rec의 본문 읽기 관련 필드(read_body,remaining등)와finfo,parsed_uri- 설정 배열을
conf_vector_length칸으로 만드는 함수들(server/config.c224·266–313행)과,AP_DEBUG빌드에서 매크로 대신 함수가 되는ap_get_module_config(include/http_config.h522행) modules/http2,modules/md같은 큰 모듈들의 구조
정리
| 개념 | 한 줄 요약 |
|---|---|
| 다섯 층 | 운영체제 ← 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절의 표와 나란히 놓고 보면 두 프로젝트가 "요청을 여러 단계로 나누고 각 단계에 꽂을 자리를 둔다"는 같은 생각을 다른 언어로 구현했다는 것이 보입니다.